babase

Common shared Ballistica components.

For modding purposes, this package should generally not be used directly. Instead one should use purpose-built packages such as bascenev1 or bauiv1 which themselves import various functionality from here and reexpose it in a more focused way.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Common shared Ballistica components.
  4
  5For modding purposes, this package should generally not be used directly.
  6Instead one should use purpose-built packages such as bascenev1 or bauiv1
  7which themselves import various functionality from here and reexpose it in
  8a more focused way.
  9"""
 10# pylint: disable=redefined-builtin
 11
 12# The stuff we expose here at the top level is our 'public' api for use
 13# from other modules/packages. Code *within* this package should import
 14# things from this package's submodules directly to reduce the chance of
 15# dependency loops. The exception is TYPE_CHECKING blocks and
 16# annotations since those aren't evaluated at runtime.
 17
 18from efro.util import set_canonical_module_names
 19
 20
 21import _babase
 22from _babase import (
 23    add_clean_frame_callback,
 24    allows_ticket_sales,
 25    android_get_external_files_dir,
 26    appname,
 27    appnameupper,
 28    apptime,
 29    apptimer,
 30    AppTimer,
 31    asset_loads_allowed,
 32    fullscreen_control_available,
 33    fullscreen_control_get,
 34    fullscreen_control_key_shortcut,
 35    fullscreen_control_set,
 36    charstr,
 37    clipboard_get_text,
 38    clipboard_has_text,
 39    clipboard_is_supported,
 40    clipboard_set_text,
 41    ContextCall,
 42    ContextRef,
 43    displaytime,
 44    displaytimer,
 45    DisplayTimer,
 46    do_once,
 47    env,
 48    Env,
 49    fade_screen,
 50    fatal_error,
 51    get_display_resolution,
 52    get_immediate_return_code,
 53    get_input_idle_time,
 54    get_low_level_config_value,
 55    get_max_graphics_quality,
 56    get_replays_dir,
 57    get_string_height,
 58    get_string_width,
 59    get_v1_cloud_log_file_path,
 60    getsimplesound,
 61    has_user_run_commands,
 62    have_chars,
 63    have_permission,
 64    in_logic_thread,
 65    increment_analytics_count,
 66    invoke_main_menu,
 67    is_os_playing_music,
 68    is_xcode_build,
 69    lock_all_input,
 70    mac_music_app_get_playlists,
 71    mac_music_app_get_volume,
 72    mac_music_app_init,
 73    mac_music_app_play_playlist,
 74    mac_music_app_set_volume,
 75    mac_music_app_stop,
 76    music_player_play,
 77    music_player_set_volume,
 78    music_player_shutdown,
 79    music_player_stop,
 80    native_review_request,
 81    native_review_request_supported,
 82    native_stack_trace,
 83    open_file_externally,
 84    open_url,
 85    overlay_web_browser_close,
 86    overlay_web_browser_is_open,
 87    overlay_web_browser_is_supported,
 88    overlay_web_browser_open_url,
 89    print_load_info,
 90    push_back_press,
 91    pushcall,
 92    quit,
 93    reload_media,
 94    request_permission,
 95    safecolor,
 96    screenmessage,
 97    set_analytics_screen,
 98    set_low_level_config_value,
 99    set_thread_name,
100    set_ui_input_device,
101    show_progress_bar,
102    shutdown_suppress_begin,
103    shutdown_suppress_end,
104    shutdown_suppress_count,
105    SimpleSound,
106    supports_max_fps,
107    supports_vsync,
108    unlock_all_input,
109    user_agent_string,
110    Vec3,
111    workspaces_in_use,
112)
113
114from babase._accountv2 import AccountV2Handle, AccountV2Subsystem
115from babase._app import App
116from babase._appconfig import commit_app_config
117from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec
118from babase._appmode import AppMode
119from babase._appsubsystem import AppSubsystem
120from babase._appmodeselector import AppModeSelector
121from babase._appconfig import AppConfig
122from babase._apputils import (
123    handle_leftover_v1_cloud_log_file,
124    is_browser_likely_available,
125    garbage_collect,
126    get_remote_app_name,
127    AppHealthMonitor,
128)
129from babase._devconsole import (
130    DevConsoleTab,
131    DevConsoleTabEntry,
132    DevConsoleSubsystem,
133)
134from babase._emptyappmode import EmptyAppMode
135from babase._error import (
136    print_exception,
137    print_error,
138    ContextError,
139    NotFoundError,
140    PlayerNotFoundError,
141    SessionPlayerNotFoundError,
142    NodeNotFoundError,
143    ActorNotFoundError,
144    InputDeviceNotFoundError,
145    WidgetNotFoundError,
146    ActivityNotFoundError,
147    TeamNotFoundError,
148    MapNotFoundError,
149    SessionTeamNotFoundError,
150    SessionNotFoundError,
151    DelegateNotFoundError,
152)
153from babase._general import (
154    utf8_all,
155    DisplayTime,
156    AppTime,
157    WeakCall,
158    Call,
159    existing,
160    Existable,
161    verify_object_death,
162    storagename,
163    getclass,
164    get_type_name,
165)
166from babase._language import Lstr, LanguageSubsystem
167from babase._login import LoginAdapter, LoginInfo
168
169# noinspection PyProtectedMember
170# (PyCharm inspection bug?)
171from babase._mgen.enums import (
172    Permission,
173    SpecialChar,
174    InputType,
175    UIScale,
176    QuitType,
177)
178from babase._math import normalized_color, is_point_in_box, vec3validate
179from babase._meta import MetadataSubsystem
180from babase._net import get_ip_address_type, DEFAULT_REQUEST_TIMEOUT_SECONDS
181from babase._plugin import PluginSpec, Plugin, PluginSubsystem
182from babase._stringedit import StringEditAdapter, StringEditSubsystem
183from babase._text import timestring
184
185_babase.app = app = App()
186app.postinit()
187
188__all__ = [
189    'AccountV2Handle',
190    'AccountV2Subsystem',
191    'ActivityNotFoundError',
192    'ActorNotFoundError',
193    'allows_ticket_sales',
194    'add_clean_frame_callback',
195    'android_get_external_files_dir',
196    'app',
197    'app',
198    'App',
199    'AppConfig',
200    'AppHealthMonitor',
201    'AppIntent',
202    'AppIntentDefault',
203    'AppIntentExec',
204    'AppMode',
205    'appname',
206    'appnameupper',
207    'AppModeSelector',
208    'AppSubsystem',
209    'apptime',
210    'AppTime',
211    'apptime',
212    'apptimer',
213    'AppTimer',
214    'asset_loads_allowed',
215    'Call',
216    'fullscreen_control_available',
217    'fullscreen_control_get',
218    'fullscreen_control_key_shortcut',
219    'fullscreen_control_set',
220    'charstr',
221    'clipboard_get_text',
222    'clipboard_has_text',
223    'clipboard_is_supported',
224    'clipboard_set_text',
225    'commit_app_config',
226    'ContextCall',
227    'ContextError',
228    'ContextRef',
229    'DelegateNotFoundError',
230    'DevConsoleTab',
231    'DevConsoleTabEntry',
232    'DevConsoleSubsystem',
233    'DisplayTime',
234    'displaytime',
235    'displaytimer',
236    'DisplayTimer',
237    'do_once',
238    'EmptyAppMode',
239    'env',
240    'Env',
241    'Existable',
242    'existing',
243    'fade_screen',
244    'fatal_error',
245    'garbage_collect',
246    'get_display_resolution',
247    'get_immediate_return_code',
248    'get_input_idle_time',
249    'get_ip_address_type',
250    'get_low_level_config_value',
251    'get_max_graphics_quality',
252    'get_remote_app_name',
253    'get_replays_dir',
254    'get_string_height',
255    'get_string_width',
256    'get_v1_cloud_log_file_path',
257    'get_type_name',
258    'getclass',
259    'getsimplesound',
260    'handle_leftover_v1_cloud_log_file',
261    'has_user_run_commands',
262    'have_chars',
263    'have_permission',
264    'in_logic_thread',
265    'increment_analytics_count',
266    'InputDeviceNotFoundError',
267    'InputType',
268    'invoke_main_menu',
269    'is_browser_likely_available',
270    'is_browser_likely_available',
271    'is_os_playing_music',
272    'is_point_in_box',
273    'is_xcode_build',
274    'LanguageSubsystem',
275    'lock_all_input',
276    'LoginAdapter',
277    'LoginInfo',
278    'Lstr',
279    'mac_music_app_get_playlists',
280    'mac_music_app_get_volume',
281    'mac_music_app_init',
282    'mac_music_app_play_playlist',
283    'mac_music_app_set_volume',
284    'mac_music_app_stop',
285    'MapNotFoundError',
286    'MetadataSubsystem',
287    'music_player_play',
288    'music_player_set_volume',
289    'music_player_shutdown',
290    'music_player_stop',
291    'native_review_request',
292    'native_review_request_supported',
293    'native_stack_trace',
294    'NodeNotFoundError',
295    'normalized_color',
296    'NotFoundError',
297    'open_file_externally',
298    'open_url',
299    'overlay_web_browser_close',
300    'overlay_web_browser_is_open',
301    'overlay_web_browser_is_supported',
302    'overlay_web_browser_open_url',
303    'Permission',
304    'PlayerNotFoundError',
305    'Plugin',
306    'PluginSubsystem',
307    'PluginSpec',
308    'print_error',
309    'print_exception',
310    'print_load_info',
311    'push_back_press',
312    'pushcall',
313    'quit',
314    'QuitType',
315    'reload_media',
316    'request_permission',
317    'safecolor',
318    'screenmessage',
319    'SessionNotFoundError',
320    'SessionPlayerNotFoundError',
321    'SessionTeamNotFoundError',
322    'set_analytics_screen',
323    'set_low_level_config_value',
324    'set_thread_name',
325    'set_ui_input_device',
326    'show_progress_bar',
327    'shutdown_suppress_begin',
328    'shutdown_suppress_end',
329    'shutdown_suppress_count',
330    'SimpleSound',
331    'SpecialChar',
332    'storagename',
333    'StringEditAdapter',
334    'StringEditSubsystem',
335    'supports_max_fps',
336    'supports_vsync',
337    'TeamNotFoundError',
338    'timestring',
339    'UIScale',
340    'unlock_all_input',
341    'user_agent_string',
342    'utf8_all',
343    'Vec3',
344    'vec3validate',
345    'verify_object_death',
346    'WeakCall',
347    'WidgetNotFoundError',
348    'workspaces_in_use',
349    'DEFAULT_REQUEST_TIMEOUT_SECONDS',
350]
351
352# We want stuff to show up as babase.Foo instead of babase._sub.Foo.
353set_canonical_module_names(globals())
354
355# Allow the native layer to wrap a few things up.
356_babase.reached_end_of_babase()
357
358# Marker we pop down at the very end so other modules can run sanity
359# checks to make sure we aren't importing them reciprocally when they
360# import us.
361_REACHED_END_OF_MODULE = True
class AccountV2Handle:
426class AccountV2Handle:
427    """Handle for interacting with a V2 account.
428
429    This class supports the 'with' statement, which is how it is
430    used with some operations such as cloud messaging.
431    """
432
433    accountid: str
434    tag: str
435    workspacename: str | None
436    workspaceid: str | None
437    logins: dict[LoginType, LoginInfo]
438
439    def __enter__(self) -> None:
440        """Support for "with" statement.
441
442        This allows cloud messages to be sent on our behalf.
443        """
444
445    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
446        """Support for "with" statement.
447
448        This allows cloud messages to be sent on our behalf.
449        """

Handle for interacting with a V2 account.

This class supports the 'with' statement, which is how it is used with some operations such as cloud messaging.

accountid: str
tag: str
workspacename: str | None
workspaceid: str | None
class AccountV2Subsystem:
 26class AccountV2Subsystem:
 27    """Subsystem for modern account handling in the app.
 28
 29    Category: **App Classes**
 30
 31    Access the single shared instance of this class at 'ba.app.plus.accounts'.
 32    """
 33
 34    def __init__(self) -> None:
 35        from babase._login import LoginAdapterGPGS, LoginAdapterGameCenter
 36
 37        # Whether or not everything related to an initial login
 38        # (or lack thereof) has completed. This includes things like
 39        # workspace syncing. Completion of this is what flips the app
 40        # into 'running' state.
 41        self._initial_sign_in_completed = False
 42
 43        self._kicked_off_workspace_load = False
 44
 45        self.login_adapters: dict[LoginType, LoginAdapter] = {}
 46
 47        self._implicit_signed_in_adapter: LoginAdapter | None = None
 48        self._implicit_state_changed = False
 49        self._can_do_auto_sign_in = True
 50
 51        adapter: LoginAdapter
 52        if _babase.using_google_play_game_services():
 53            adapter = LoginAdapterGPGS()
 54            self.login_adapters[adapter.login_type] = adapter
 55        if _babase.using_game_center():
 56            adapter = LoginAdapterGameCenter()
 57            self.login_adapters[adapter.login_type] = adapter
 58
 59    def on_app_loading(self) -> None:
 60        """Should be called at standard on_app_loading time."""
 61
 62        for adapter in self.login_adapters.values():
 63            adapter.on_app_loading()
 64
 65    def have_primary_credentials(self) -> bool:
 66        """Are credentials currently set for the primary app account?
 67
 68        Note that this does not mean these credentials are currently valid;
 69        only that they exist. If/when credentials are validated, the 'primary'
 70        account handle will be set.
 71        """
 72        raise NotImplementedError('This should be overridden.')
 73
 74    @property
 75    def primary(self) -> AccountV2Handle | None:
 76        """The primary account for the app, or None if not logged in."""
 77        return self.do_get_primary()
 78
 79    def on_primary_account_changed(
 80        self, account: AccountV2Handle | None
 81    ) -> None:
 82        """Callback run after the primary account changes.
 83
 84        Will be called with None on log-outs and when new credentials
 85        are set but have not yet been verified.
 86        """
 87        assert _babase.in_logic_thread()
 88
 89        # Currently don't do anything special on sign-outs.
 90        if account is None:
 91            return
 92
 93        # If this new account has a workspace, update it and ask to be
 94        # informed when that process completes.
 95        if account.workspaceid is not None:
 96            assert account.workspacename is not None
 97            if (
 98                not self._initial_sign_in_completed
 99                and not self._kicked_off_workspace_load
100            ):
101                self._kicked_off_workspace_load = True
102                _babase.app.workspaces.set_active_workspace(
103                    account=account,
104                    workspaceid=account.workspaceid,
105                    workspacename=account.workspacename,
106                    on_completed=self._on_set_active_workspace_completed,
107                )
108            else:
109                # Don't activate workspaces if we've already told the game
110                # that initial-log-in is done or if we've already kicked
111                # off a workspace load.
112                _babase.screenmessage(
113                    f'\'{account.workspacename}\''
114                    f' will be activated at next app launch.',
115                    color=(1, 1, 0),
116                )
117                _babase.getsimplesound('error').play()
118            return
119
120        # Ok; no workspace to worry about; carry on.
121        if not self._initial_sign_in_completed:
122            self._initial_sign_in_completed = True
123            _babase.app.on_initial_sign_in_complete()
124
125    def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
126        """Should be called when logins for the active account change."""
127
128        for adapter in self.login_adapters.values():
129            adapter.set_active_logins(logins)
130
131    def on_implicit_sign_in(
132        self, login_type: LoginType, login_id: str, display_name: str
133    ) -> None:
134        """An implicit sign-in happened (called by native layer)."""
135        from babase._login import LoginAdapter
136
137        assert _babase.in_logic_thread()
138
139        with _babase.ContextRef.empty():
140            self.login_adapters[login_type].set_implicit_login_state(
141                LoginAdapter.ImplicitLoginState(
142                    login_id=login_id, display_name=display_name
143                )
144            )
145
146    def on_implicit_sign_out(self, login_type: LoginType) -> None:
147        """An implicit sign-out happened (called by native layer)."""
148        assert _babase.in_logic_thread()
149        with _babase.ContextRef.empty():
150            self.login_adapters[login_type].set_implicit_login_state(None)
151
152    def on_no_initial_primary_account(self) -> None:
153        """Callback run if the app has no primary account after launch.
154
155        Either this callback or on_primary_account_changed will be called
156        within a few seconds of app launch; the app can move forward
157        with the startup sequence at that point.
158        """
159        if not self._initial_sign_in_completed:
160            self._initial_sign_in_completed = True
161            _babase.app.on_initial_sign_in_complete()
162
163    @staticmethod
164    def _hashstr(val: str) -> str:
165        md5 = hashlib.md5()
166        md5.update(val.encode())
167        return md5.hexdigest()
168
169    def on_implicit_login_state_changed(
170        self,
171        login_type: LoginType,
172        state: LoginAdapter.ImplicitLoginState | None,
173    ) -> None:
174        """Called when implicit login state changes.
175
176        Login systems that tend to sign themselves in/out in the
177        background are considered implicit. We may choose to honor or
178        ignore their states, allowing the user to opt for other login
179        types even if the default implicit one can't be explicitly
180        logged out or otherwise controlled.
181        """
182        from babase._language import Lstr
183
184        assert _babase.in_logic_thread()
185
186        cfg = _babase.app.config
187        cfgkey = 'ImplicitLoginStates'
188        cfgdict = _babase.app.config.setdefault(cfgkey, {})
189
190        # Store which (if any) adapter is currently implicitly signed
191        # in. Making the assumption there will only ever be one implicit
192        # adapter at a time; may need to revisit this logic if that
193        # changes.
194        prev_state = cfgdict.get(login_type.value)
195        if state is None:
196            self._implicit_signed_in_adapter = None
197            new_state = cfgdict[login_type.value] = None
198        else:
199            self._implicit_signed_in_adapter = self.login_adapters[login_type]
200            new_state = cfgdict[login_type.value] = self._hashstr(
201                state.login_id
202            )
203
204            # Special case: if the user is already signed in but not
205            # with this implicit login, let them know that the 'Welcome
206            # back FOO' they likely just saw is not actually accurate.
207            if (
208                self.primary is not None
209                and not self.login_adapters[login_type].is_back_end_active()
210            ):
211                service_str: Lstr | None
212                if login_type is LoginType.GPGS:
213                    service_str = Lstr(resource='googlePlayText')
214                elif login_type is LoginType.GAME_CENTER:
215                    # Note: Apparently Game Center is just called 'Game
216                    # Center' in all languages. Can revisit if not true.
217                    # https://developer.apple.com/forums/thread/725779
218                    service_str = Lstr(value='Game Center')
219                elif login_type is LoginType.EMAIL:
220                    # Not possible; just here for exhaustive coverage.
221                    service_str = None
222                else:
223                    assert_never(login_type)
224                if service_str is not None:
225                    _babase.apptimer(
226                        2.0,
227                        partial(
228                            _babase.screenmessage,
229                            Lstr(
230                                resource='notUsingAccountText',
231                                subs=[
232                                    ('${ACCOUNT}', state.display_name),
233                                    ('${SERVICE}', service_str),
234                                ],
235                            ),
236                            (1, 0.5, 0),
237                        ),
238                    )
239
240        cfg.commit()
241
242        # We want to respond any time the implicit state changes;
243        # generally this means the user has explicitly signed in/out or
244        # switched accounts within that back-end.
245        if prev_state != new_state:
246            if DEBUG_LOG:
247                logging.debug(
248                    'AccountV2: Implicit state changed (%s -> %s);'
249                    ' will update app sign-in state accordingly.',
250                    prev_state,
251                    new_state,
252                )
253            self._implicit_state_changed = True
254
255        # We may want to auto-sign-in based on this new state.
256        self._update_auto_sign_in()
257
258    def on_cloud_connectivity_changed(self, connected: bool) -> None:
259        """Should be called with cloud connectivity changes."""
260        del connected  # Unused.
261        assert _babase.in_logic_thread()
262
263        # We may want to auto-sign-in based on this new state.
264        self._update_auto_sign_in()
265
266    def do_get_primary(self) -> AccountV2Handle | None:
267        """Internal - should be overridden by subclass."""
268        raise NotImplementedError('This should be overridden.')
269
270    def set_primary_credentials(self, credentials: str | None) -> None:
271        """Set credentials for the primary app account."""
272        raise NotImplementedError('This should be overridden.')
273
274    def _update_auto_sign_in(self) -> None:
275        plus = _babase.app.plus
276        assert plus is not None
277
278        # If implicit state has changed, try to respond.
279        if self._implicit_state_changed:
280            if self._implicit_signed_in_adapter is None:
281                # If implicit back-end has signed out, we follow suit
282                # immediately; no need to wait for network connectivity.
283                if DEBUG_LOG:
284                    logging.debug(
285                        'AccountV2: Signing out as result'
286                        ' of implicit state change...',
287                    )
288                plus.accounts.set_primary_credentials(None)
289                self._implicit_state_changed = False
290
291                # Once we've made a move here we don't want to
292                # do any more automatic stuff.
293                self._can_do_auto_sign_in = False
294
295            else:
296                # Ok; we've got a new implicit state. If we've got
297                # connectivity, let's attempt to sign in with it.
298                # Consider this an 'explicit' sign in because the
299                # implicit-login state change presumably was triggered
300                # by some user action (signing in, signing out, or
301                # switching accounts via the back-end). NOTE: should
302                # test case where we don't have connectivity here.
303                if plus.cloud.is_connected():
304                    if DEBUG_LOG:
305                        logging.debug(
306                            'AccountV2: Signing in as result'
307                            ' of implicit state change...',
308                        )
309                    self._implicit_signed_in_adapter.sign_in(
310                        self._on_explicit_sign_in_completed,
311                        description='implicit state change',
312                    )
313                    self._implicit_state_changed = False
314
315                    # Once we've made a move here we don't want to
316                    # do any more automatic stuff.
317                    self._can_do_auto_sign_in = False
318
319        if not self._can_do_auto_sign_in:
320            return
321
322        # If we're not currently signed in, we have connectivity, and
323        # we have an available implicit login, auto-sign-in with it once.
324        # The implicit-state-change logic above should keep things
325        # mostly in-sync, but that might not always be the case due to
326        # connectivity or other issues. We prefer to keep people signed
327        # in as a rule, even if there are corner cases where this might
328        # not be what they want (A user signing out and then restarting
329        # may be auto-signed back in).
330        connected = plus.cloud.is_connected()
331        signed_in_v1 = plus.get_v1_account_state() == 'signed_in'
332        signed_in_v2 = plus.accounts.have_primary_credentials()
333        if (
334            connected
335            and not signed_in_v1
336            and not signed_in_v2
337            and self._implicit_signed_in_adapter is not None
338        ):
339            if DEBUG_LOG:
340                logging.debug(
341                    'AccountV2: Signing in due to on-launch-auto-sign-in...',
342                )
343            self._can_do_auto_sign_in = False  # Only ATTEMPT once
344            self._implicit_signed_in_adapter.sign_in(
345                self._on_implicit_sign_in_completed, description='auto-sign-in'
346            )
347
348    def _on_explicit_sign_in_completed(
349        self,
350        adapter: LoginAdapter,
351        result: LoginAdapter.SignInResult | Exception,
352    ) -> None:
353        """A sign-in has completed that the user asked for explicitly."""
354        from babase._language import Lstr
355
356        del adapter  # Unused.
357
358        plus = _babase.app.plus
359        assert plus is not None
360
361        # Make some noise on errors since the user knows a
362        # sign-in attempt is happening in this case (the 'explicit' part).
363        if isinstance(result, Exception):
364            # We expect the occasional communication errors;
365            # Log a full exception for anything else though.
366            if not isinstance(result, CommunicationError):
367                logging.warning(
368                    'Error on explicit accountv2 sign in attempt.',
369                    exc_info=result,
370                )
371
372            # For now just show 'error'. Should do better than this.
373            _babase.screenmessage(
374                Lstr(resource='internal.signInErrorText'),
375                color=(1, 0, 0),
376            )
377            _babase.getsimplesound('error').play()
378
379            # Also I suppose we should sign them out in this case since
380            # it could be misleading to be still signed in with the old
381            # account.
382            plus.accounts.set_primary_credentials(None)
383            return
384
385        plus.accounts.set_primary_credentials(result.credentials)
386
387    def _on_implicit_sign_in_completed(
388        self,
389        adapter: LoginAdapter,
390        result: LoginAdapter.SignInResult | Exception,
391    ) -> None:
392        """A sign-in has completed that the user didn't ask for explicitly."""
393        plus = _babase.app.plus
394        assert plus is not None
395
396        del adapter  # Unused.
397
398        # Log errors but don't inform the user; they're not aware of this
399        # attempt and ignorance is bliss.
400        if isinstance(result, Exception):
401            # We expect the occasional communication errors;
402            # Log a full exception for anything else though.
403            if not isinstance(result, CommunicationError):
404                logging.warning(
405                    'Error on implicit accountv2 sign in attempt.',
406                    exc_info=result,
407                )
408            return
409
410        # If we're still connected and still not signed in,
411        # plug in the credentials we got. We want to be extra cautious
412        # in case the user has since explicitly signed in since we
413        # kicked off.
414        connected = plus.cloud.is_connected()
415        signed_in_v1 = plus.get_v1_account_state() == 'signed_in'
416        signed_in_v2 = plus.accounts.have_primary_credentials()
417        if connected and not signed_in_v1 and not signed_in_v2:
418            plus.accounts.set_primary_credentials(result.credentials)
419
420    def _on_set_active_workspace_completed(self) -> None:
421        if not self._initial_sign_in_completed:
422            self._initial_sign_in_completed = True
423            _babase.app.on_initial_sign_in_complete()

Subsystem for modern account handling in the app.

Category: App Classes

Access the single shared instance of this class at 'ba.app.plus.accounts'.

login_adapters: dict[bacommon.login.LoginType, LoginAdapter]
def on_app_loading(self) -> None:
59    def on_app_loading(self) -> None:
60        """Should be called at standard on_app_loading time."""
61
62        for adapter in self.login_adapters.values():
63            adapter.on_app_loading()

Should be called at standard on_app_loading time.

def have_primary_credentials(self) -> bool:
65    def have_primary_credentials(self) -> bool:
66        """Are credentials currently set for the primary app account?
67
68        Note that this does not mean these credentials are currently valid;
69        only that they exist. If/when credentials are validated, the 'primary'
70        account handle will be set.
71        """
72        raise NotImplementedError('This should be overridden.')

Are credentials currently set for the primary app account?

Note that this does not mean these credentials are currently valid; only that they exist. If/when credentials are validated, the 'primary' account handle will be set.

primary: AccountV2Handle | None
74    @property
75    def primary(self) -> AccountV2Handle | None:
76        """The primary account for the app, or None if not logged in."""
77        return self.do_get_primary()

The primary account for the app, or None if not logged in.

def on_primary_account_changed(self, account: AccountV2Handle | None) -> None:
 79    def on_primary_account_changed(
 80        self, account: AccountV2Handle | None
 81    ) -> None:
 82        """Callback run after the primary account changes.
 83
 84        Will be called with None on log-outs and when new credentials
 85        are set but have not yet been verified.
 86        """
 87        assert _babase.in_logic_thread()
 88
 89        # Currently don't do anything special on sign-outs.
 90        if account is None:
 91            return
 92
 93        # If this new account has a workspace, update it and ask to be
 94        # informed when that process completes.
 95        if account.workspaceid is not None:
 96            assert account.workspacename is not None
 97            if (
 98                not self._initial_sign_in_completed
 99                and not self._kicked_off_workspace_load
100            ):
101                self._kicked_off_workspace_load = True
102                _babase.app.workspaces.set_active_workspace(
103                    account=account,
104                    workspaceid=account.workspaceid,
105                    workspacename=account.workspacename,
106                    on_completed=self._on_set_active_workspace_completed,
107                )
108            else:
109                # Don't activate workspaces if we've already told the game
110                # that initial-log-in is done or if we've already kicked
111                # off a workspace load.
112                _babase.screenmessage(
113                    f'\'{account.workspacename}\''
114                    f' will be activated at next app launch.',
115                    color=(1, 1, 0),
116                )
117                _babase.getsimplesound('error').play()
118            return
119
120        # Ok; no workspace to worry about; carry on.
121        if not self._initial_sign_in_completed:
122            self._initial_sign_in_completed = True
123            _babase.app.on_initial_sign_in_complete()

Callback run after the primary account changes.

Will be called with None on log-outs and when new credentials are set but have not yet been verified.

def on_active_logins_changed(self, logins: dict[bacommon.login.LoginType, str]) -> None:
125    def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
126        """Should be called when logins for the active account change."""
127
128        for adapter in self.login_adapters.values():
129            adapter.set_active_logins(logins)

Should be called when logins for the active account change.

def on_implicit_sign_in( self, login_type: bacommon.login.LoginType, login_id: str, display_name: str) -> None:
131    def on_implicit_sign_in(
132        self, login_type: LoginType, login_id: str, display_name: str
133    ) -> None:
134        """An implicit sign-in happened (called by native layer)."""
135        from babase._login import LoginAdapter
136
137        assert _babase.in_logic_thread()
138
139        with _babase.ContextRef.empty():
140            self.login_adapters[login_type].set_implicit_login_state(
141                LoginAdapter.ImplicitLoginState(
142                    login_id=login_id, display_name=display_name
143                )
144            )

An implicit sign-in happened (called by native layer).

def on_implicit_sign_out(self, login_type: bacommon.login.LoginType) -> None:
146    def on_implicit_sign_out(self, login_type: LoginType) -> None:
147        """An implicit sign-out happened (called by native layer)."""
148        assert _babase.in_logic_thread()
149        with _babase.ContextRef.empty():
150            self.login_adapters[login_type].set_implicit_login_state(None)

An implicit sign-out happened (called by native layer).

def on_no_initial_primary_account(self) -> None:
152    def on_no_initial_primary_account(self) -> None:
153        """Callback run if the app has no primary account after launch.
154
155        Either this callback or on_primary_account_changed will be called
156        within a few seconds of app launch; the app can move forward
157        with the startup sequence at that point.
158        """
159        if not self._initial_sign_in_completed:
160            self._initial_sign_in_completed = True
161            _babase.app.on_initial_sign_in_complete()

Callback run if the app has no primary account after launch.

Either this callback or on_primary_account_changed will be called within a few seconds of app launch; the app can move forward with the startup sequence at that point.

def on_implicit_login_state_changed( self, login_type: bacommon.login.LoginType, state: LoginAdapter.ImplicitLoginState | None) -> None:
169    def on_implicit_login_state_changed(
170        self,
171        login_type: LoginType,
172        state: LoginAdapter.ImplicitLoginState | None,
173    ) -> None:
174        """Called when implicit login state changes.
175
176        Login systems that tend to sign themselves in/out in the
177        background are considered implicit. We may choose to honor or
178        ignore their states, allowing the user to opt for other login
179        types even if the default implicit one can't be explicitly
180        logged out or otherwise controlled.
181        """
182        from babase._language import Lstr
183
184        assert _babase.in_logic_thread()
185
186        cfg = _babase.app.config
187        cfgkey = 'ImplicitLoginStates'
188        cfgdict = _babase.app.config.setdefault(cfgkey, {})
189
190        # Store which (if any) adapter is currently implicitly signed
191        # in. Making the assumption there will only ever be one implicit
192        # adapter at a time; may need to revisit this logic if that
193        # changes.
194        prev_state = cfgdict.get(login_type.value)
195        if state is None:
196            self._implicit_signed_in_adapter = None
197            new_state = cfgdict[login_type.value] = None
198        else:
199            self._implicit_signed_in_adapter = self.login_adapters[login_type]
200            new_state = cfgdict[login_type.value] = self._hashstr(
201                state.login_id
202            )
203
204            # Special case: if the user is already signed in but not
205            # with this implicit login, let them know that the 'Welcome
206            # back FOO' they likely just saw is not actually accurate.
207            if (
208                self.primary is not None
209                and not self.login_adapters[login_type].is_back_end_active()
210            ):
211                service_str: Lstr | None
212                if login_type is LoginType.GPGS:
213                    service_str = Lstr(resource='googlePlayText')
214                elif login_type is LoginType.GAME_CENTER:
215                    # Note: Apparently Game Center is just called 'Game
216                    # Center' in all languages. Can revisit if not true.
217                    # https://developer.apple.com/forums/thread/725779
218                    service_str = Lstr(value='Game Center')
219                elif login_type is LoginType.EMAIL:
220                    # Not possible; just here for exhaustive coverage.
221                    service_str = None
222                else:
223                    assert_never(login_type)
224                if service_str is not None:
225                    _babase.apptimer(
226                        2.0,
227                        partial(
228                            _babase.screenmessage,
229                            Lstr(
230                                resource='notUsingAccountText',
231                                subs=[
232                                    ('${ACCOUNT}', state.display_name),
233                                    ('${SERVICE}', service_str),
234                                ],
235                            ),
236                            (1, 0.5, 0),
237                        ),
238                    )
239
240        cfg.commit()
241
242        # We want to respond any time the implicit state changes;
243        # generally this means the user has explicitly signed in/out or
244        # switched accounts within that back-end.
245        if prev_state != new_state:
246            if DEBUG_LOG:
247                logging.debug(
248                    'AccountV2: Implicit state changed (%s -> %s);'
249                    ' will update app sign-in state accordingly.',
250                    prev_state,
251                    new_state,
252                )
253            self._implicit_state_changed = True
254
255        # We may want to auto-sign-in based on this new state.
256        self._update_auto_sign_in()

Called when implicit login state changes.

Login systems that tend to sign themselves in/out in the background are considered implicit. We may choose to honor or ignore their states, allowing the user to opt for other login types even if the default implicit one can't be explicitly logged out or otherwise controlled.

def on_cloud_connectivity_changed(self, connected: bool) -> None:
258    def on_cloud_connectivity_changed(self, connected: bool) -> None:
259        """Should be called with cloud connectivity changes."""
260        del connected  # Unused.
261        assert _babase.in_logic_thread()
262
263        # We may want to auto-sign-in based on this new state.
264        self._update_auto_sign_in()

Should be called with cloud connectivity changes.

def do_get_primary(self) -> AccountV2Handle | None:
266    def do_get_primary(self) -> AccountV2Handle | None:
267        """Internal - should be overridden by subclass."""
268        raise NotImplementedError('This should be overridden.')

Internal - should be overridden by subclass.

def set_primary_credentials(self, credentials: str | None) -> None:
270    def set_primary_credentials(self, credentials: str | None) -> None:
271        """Set credentials for the primary app account."""
272        raise NotImplementedError('This should be overridden.')

Set credentials for the primary app account.

class ActivityNotFoundError(babase.NotFoundError):
89class ActivityNotFoundError(NotFoundError):
90    """Exception raised when an expected bascenev1.Activity does not exist.
91
92    Category: **Exception Classes**
93    """

Exception raised when an expected bascenev1.Activity does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
class ActorNotFoundError(babase.NotFoundError):
82class ActorNotFoundError(NotFoundError):
83    """Exception raised when an expected actor does not exist.
84
85    Category: **Exception Classes**
86    """

Exception raised when an expected actor does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
app = <App object>
class App:
  49class App:
  50    """A class for high level app functionality and state.
  51
  52    Category: **App Classes**
  53
  54    Use babase.app to access the single shared instance of this class.
  55
  56    Note that properties not documented here should be considered internal
  57    and subject to change without warning.
  58    """
  59
  60    # pylint: disable=too-many-public-methods
  61
  62    # A few things defined as non-optional values but not actually
  63    # available until the app starts.
  64    plugins: PluginSubsystem
  65    lang: LanguageSubsystem
  66    health_monitor: AppHealthMonitor
  67
  68    # How long we allow shutdown tasks to run before killing them.
  69    # Currently the entire app hard-exits if shutdown takes 10 seconds,
  70    # so we need to keep it under that.
  71    SHUTDOWN_TASK_TIMEOUT_SECONDS = 5
  72
  73    class State(Enum):
  74        """High level state the app can be in."""
  75
  76        # The app has not yet begun starting and should not be used in
  77        # any way.
  78        NOT_STARTED = 0
  79
  80        # The native layer is spinning up its machinery (screens,
  81        # renderers, etc.). Nothing should happen in the Python layer
  82        # until this completes.
  83        NATIVE_BOOTSTRAPPING = 1
  84
  85        # Python app subsystems are being inited but should not yet
  86        # interact or do any work.
  87        INITING = 2
  88
  89        # Python app subsystems are inited and interacting, but the app
  90        # has not yet embarked on a high level course of action. It is
  91        # doing initial account logins, workspace & asset downloads,
  92        # etc.
  93        LOADING = 3
  94
  95        # All pieces are in place and the app is now doing its thing.
  96        RUNNING = 4
  97
  98        # Used on platforms such as mobile where the app basically needs
  99        # to shut down while backgrounded. In this state, all event
 100        # loops are suspended and all graphics and audio must cease
 101        # completely. Be aware that the suspended state can be entered
 102        # from any other state including NATIVE_BOOTSTRAPPING and
 103        # SHUTTING_DOWN.
 104        SUSPENDED = 5
 105
 106        # The app is shutting down. This process may involve sending
 107        # network messages or other things that can take up to a few
 108        # seconds, so ideally graphics and audio should remain
 109        # functional (with fades or spinners or whatever to show
 110        # something is happening).
 111        SHUTTING_DOWN = 6
 112
 113        # The app has completed shutdown. Any code running here should
 114        # be basically immediate.
 115        SHUTDOWN_COMPLETE = 7
 116
 117    class DefaultAppModeSelector(AppModeSelector):
 118        """Decides which AppModes to use to handle AppIntents.
 119
 120        This default version is generated by the project updater based
 121        on the 'default_app_modes' value in the projectconfig.
 122
 123        It is also possible to modify app mode selection behavior by
 124        setting app.mode_selector to an instance of a custom
 125        AppModeSelector subclass. This is a good way to go if you are
 126        modifying app behavior dynamically via a plugin instead of
 127        statically in a spinoff project.
 128        """
 129
 130        @override
 131        def app_mode_for_intent(
 132            self, intent: AppIntent
 133        ) -> type[AppMode] | None:
 134            # pylint: disable=cyclic-import
 135
 136            # __DEFAULT_APP_MODE_SELECTION_BEGIN__
 137            # This section generated by batools.appmodule; do not edit.
 138
 139            # Ask our default app modes to handle it.
 140            # (generated from 'default_app_modes' in projectconfig).
 141            import baclassic
 142            import babase
 143
 144            for appmode in [
 145                baclassic.ClassicAppMode,
 146                babase.EmptyAppMode,
 147            ]:
 148                if appmode.can_handle_intent(intent):
 149                    return appmode
 150
 151            return None
 152
 153            # __DEFAULT_APP_MODE_SELECTION_END__
 154
 155        @override
 156        def testable_app_modes(self) -> list[type[AppMode]]:
 157            # pylint: disable=cyclic-import
 158
 159            # __DEFAULT_TESTABLE_APP_MODES_BEGIN__
 160            # This section generated by batools.appmodule; do not edit.
 161
 162            # Return all our default_app_modes as testable.
 163            # (generated from 'default_app_modes' in projectconfig).
 164            import baclassic
 165            import babase
 166
 167            return [
 168                baclassic.ClassicAppMode,
 169                babase.EmptyAppMode,
 170            ]
 171            # __DEFAULT_TESTABLE_APP_MODES_END__
 172
 173    def __init__(self) -> None:
 174        """(internal)
 175
 176        Do not instantiate this class. You can access the single shared
 177        instance of it through various high level packages: 'babase.app',
 178        'bascenev1.app', 'bauiv1.app', etc.
 179        """
 180
 181        # Hack for docs-generation: we can be imported with dummy modules
 182        # instead of our actual binary ones, but we don't function.
 183        if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
 184            return
 185
 186        self.env: babase.Env = _babase.Env()
 187        self.state = self.State.NOT_STARTED
 188
 189        # Default executor which can be used for misc background
 190        # processing. It should also be passed to any additional asyncio
 191        # loops we create so that everything shares the same single set
 192        # of worker threads.
 193        self.threadpool = ThreadPoolExecutor(
 194            thread_name_prefix='baworker',
 195            initializer=self._thread_pool_thread_init,
 196        )
 197
 198        self.meta = MetadataSubsystem()
 199        self.net = NetworkSubsystem()
 200        self.workspaces = WorkspaceSubsystem()
 201        self.components = AppComponentSubsystem()
 202        self.stringedit = StringEditSubsystem()
 203        self.devconsole = DevConsoleSubsystem()
 204
 205        # This is incremented any time the app is backgrounded or
 206        # foregrounded; can be a simple way to determine if network data
 207        # should be refreshed/etc.
 208        self.fg_state = 0
 209
 210        self._subsystems: list[AppSubsystem] = []
 211        self._native_bootstrapping_completed = False
 212        self._init_completed = False
 213        self._meta_scan_completed = False
 214        self._native_start_called = False
 215        self._native_suspended = False
 216        self._native_shutdown_called = False
 217        self._native_shutdown_complete_called = False
 218        self._initial_sign_in_completed = False
 219        self._called_on_initing = False
 220        self._called_on_loading = False
 221        self._called_on_running = False
 222        self._subsystem_registration_ended = False
 223        self._pending_apply_app_config = False
 224        self._asyncio_loop: asyncio.AbstractEventLoop | None = None
 225        self._asyncio_tasks: set[asyncio.Task] = set()
 226        self._asyncio_timer: babase.AppTimer | None = None
 227        self._config: babase.AppConfig | None = None
 228        self._pending_intent: AppIntent | None = None
 229        self._intent: AppIntent | None = None
 230        self._mode_selector: babase.AppModeSelector | None = None
 231        self._mode_instances: dict[type[AppMode], AppMode] = {}
 232        self._mode: AppMode | None = None
 233        self._shutdown_task: asyncio.Task[None] | None = None
 234        self._shutdown_tasks: list[Coroutine[None, None, None]] = [
 235            self._wait_for_shutdown_suppressions(),
 236            self._fade_and_shutdown_graphics(),
 237            self._fade_and_shutdown_audio(),
 238        ]
 239        self._pool_thread_count = 0
 240
 241        # We hold a lock while lazy-loading our subsystem properties so
 242        # we don't spin up any subsystem more than once, but the lock is
 243        # recursive so that the subsystems can instantiate other
 244        # subsystems.
 245        self._subsystem_property_lock = RLock()
 246        self._subsystem_property_data: dict[str, AppSubsystem | bool] = {}
 247
 248    def postinit(self) -> None:
 249        """Called after we've been inited and assigned to babase.app.
 250
 251        Anything that accesses babase.app as part of its init process
 252        must go here instead of __init__.
 253        """
 254
 255        # Hack for docs-generation: We can be imported with dummy
 256        # modules instead of our actual binary ones, but we don't
 257        # function.
 258        if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
 259            return
 260
 261        self.lang = LanguageSubsystem()
 262        self.plugins = PluginSubsystem()
 263
 264    @property
 265    def active(self) -> bool:
 266        """Whether the app is currently front and center.
 267
 268        This will be False when the app is hidden, other activities
 269        are covering it, etc. (depending on the platform).
 270        """
 271        return _babase.app_is_active()
 272
 273    @property
 274    def asyncio_loop(self) -> asyncio.AbstractEventLoop:
 275        """The logic thread's asyncio event loop.
 276
 277        This allow async tasks to be run in the logic thread.
 278
 279        Generally you should call App.create_async_task() to schedule
 280        async code to run instead of using this directly. That will
 281        handle retaining the task and logging errors automatically.
 282        Only schedule tasks onto asyncio_loop yourself when you intend
 283        to hold on to the returned task and await its results. Releasing
 284        the task reference can lead to subtle bugs such as unreported
 285        errors and garbage-collected tasks disappearing before their
 286        work is done.
 287
 288        Note that, at this time, the asyncio loop is encapsulated
 289        and explicitly stepped by the engine's logic thread loop and
 290        thus things like asyncio.get_running_loop() will unintuitively
 291        *not* return this loop from most places in the logic thread;
 292        only from within a task explicitly created in this loop.
 293        Hopefully this situation will be improved in the future with a
 294        unified event loop.
 295        """
 296        assert _babase.in_logic_thread()
 297        assert self._asyncio_loop is not None
 298        return self._asyncio_loop
 299
 300    def create_async_task(
 301        self, coro: Coroutine[Any, Any, T], *, name: str | None = None
 302    ) -> None:
 303        """Create a fully managed async task.
 304
 305        This will automatically retain and release a reference to the task
 306        and log any exceptions that occur in it. If you need to await a task
 307        or otherwise need more control, schedule a task directly using
 308        App.asyncio_loop.
 309        """
 310        assert _babase.in_logic_thread()
 311
 312        # We hold a strong reference to the task until it is done.
 313        # Otherwise it is possible for it to be garbage collected and
 314        # disappear midway if the caller does not hold on to the
 315        # returned task, which seems like a great way to introduce
 316        # hard-to-track bugs.
 317        task = self.asyncio_loop.create_task(coro, name=name)
 318        self._asyncio_tasks.add(task)
 319        task.add_done_callback(self._on_task_done)
 320
 321    def _on_task_done(self, task: asyncio.Task) -> None:
 322        # Report any errors that occurred.
 323        try:
 324            exc = task.exception()
 325            if exc is not None:
 326                logging.error(
 327                    "Error in async task '%s'.", task.get_name(), exc_info=exc
 328                )
 329        except Exception:
 330            logging.exception('Error reporting async task error.')
 331
 332        self._asyncio_tasks.remove(task)
 333
 334    @property
 335    def config(self) -> babase.AppConfig:
 336        """The babase.AppConfig instance representing the app's config state."""
 337        assert self._config is not None
 338        return self._config
 339
 340    @property
 341    def mode_selector(self) -> babase.AppModeSelector:
 342        """Controls which app-modes are used for handling given intents.
 343
 344        Plugins can override this to change high level app behavior and
 345        spinoff projects can change the default implementation for the
 346        same effect.
 347        """
 348        if self._mode_selector is None:
 349            raise RuntimeError(
 350                'mode_selector cannot be used until the app reaches'
 351                ' the running state.'
 352            )
 353        return self._mode_selector
 354
 355    @mode_selector.setter
 356    def mode_selector(self, selector: babase.AppModeSelector) -> None:
 357        self._mode_selector = selector
 358
 359    def _get_subsystem_property(
 360        self, ssname: str, create_call: Callable[[], AppSubsystem | None]
 361    ) -> AppSubsystem | None:
 362
 363        # Quick-out: if a subsystem is present, just return it; no
 364        # locking necessary.
 365        val = self._subsystem_property_data.get(ssname)
 366        if val is not None:
 367            if val is False:
 368                # False means subsystem is confirmed as unavailable.
 369                return None
 370            if val is not True:
 371                # A subsystem has been set. Return it.
 372                return val
 373
 374        # Anything else (no val present or val True) requires locking.
 375        with self._subsystem_property_lock:
 376            val = self._subsystem_property_data.get(ssname)
 377            if val is not None:
 378                if val is False:
 379                    # False means confirmed as not present.
 380                    return None
 381                if val is True:
 382                    # True means this property is already being loaded,
 383                    # and the fact that we're holding the lock means
 384                    # we're doing the loading, so this is a dependency
 385                    # loop. Not good.
 386                    raise RuntimeError(
 387                        f'Subsystem dependency loop detected for {ssname}'
 388                    )
 389                # Must be an instantiated subsystem. Noice.
 390                return val
 391
 392            # Ok, there's nothing here for it. Instantiate and set it
 393            # while we hold the lock. Set a placeholder value of True
 394            # while we load so we can error if something we're loading
 395            # tries to recursively load us.
 396            self._subsystem_property_data[ssname] = True
 397
 398            # Do our one attempt to create the singleton.
 399            val = create_call()
 400            self._subsystem_property_data[ssname] = (
 401                False if val is None else val
 402            )
 403
 404        return val
 405
 406    # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_BEGIN__
 407    # This section generated by batools.appmodule; do not edit.
 408
 409    @property
 410    def classic(self) -> ClassicAppSubsystem | None:
 411        """Our classic subsystem (if available)."""
 412        return self._get_subsystem_property(
 413            'classic', self._create_classic_subsystem
 414        )  # type: ignore
 415
 416    @staticmethod
 417    def _create_classic_subsystem() -> ClassicAppSubsystem | None:
 418        # pylint: disable=cyclic-import
 419        try:
 420            from baclassic import ClassicAppSubsystem
 421
 422            return ClassicAppSubsystem()
 423        except ImportError:
 424            return None
 425        except Exception:
 426            logging.exception('Error importing baclassic.')
 427            return None
 428
 429    @property
 430    def plus(self) -> PlusAppSubsystem | None:
 431        """Our plus subsystem (if available)."""
 432        return self._get_subsystem_property(
 433            'plus', self._create_plus_subsystem
 434        )  # type: ignore
 435
 436    @staticmethod
 437    def _create_plus_subsystem() -> PlusAppSubsystem | None:
 438        # pylint: disable=cyclic-import
 439        try:
 440            from baplus import PlusAppSubsystem
 441
 442            return PlusAppSubsystem()
 443        except ImportError:
 444            return None
 445        except Exception:
 446            logging.exception('Error importing baplus.')
 447            return None
 448
 449    @property
 450    def ui_v1(self) -> UIV1AppSubsystem:
 451        """Our ui_v1 subsystem (always available)."""
 452        return self._get_subsystem_property(
 453            'ui_v1', self._create_ui_v1_subsystem
 454        )  # type: ignore
 455
 456    @staticmethod
 457    def _create_ui_v1_subsystem() -> UIV1AppSubsystem:
 458        # pylint: disable=cyclic-import
 459
 460        from bauiv1 import UIV1AppSubsystem
 461
 462        return UIV1AppSubsystem()
 463
 464    # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_END__
 465
 466    def register_subsystem(self, subsystem: AppSubsystem) -> None:
 467        """Called by the AppSubsystem class. Do not use directly."""
 468
 469        # We only allow registering new subsystems if we've not yet
 470        # reached the 'running' state. This ensures that all subsystems
 471        # receive a consistent set of callbacks starting with
 472        # on_app_running().
 473
 474        if self._subsystem_registration_ended:
 475            raise RuntimeError(
 476                'Subsystems can no longer be registered at this point.'
 477            )
 478        self._subsystems.append(subsystem)
 479
 480    def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
 481        """Add a task to be run on app shutdown.
 482
 483        Note that shutdown tasks will be canceled after
 484        App.SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.
 485        """
 486        if (
 487            self.state is self.State.SHUTTING_DOWN
 488            or self.state is self.State.SHUTDOWN_COMPLETE
 489        ):
 490            stname = self.state.name
 491            raise RuntimeError(
 492                f'Cannot add shutdown tasks with current state {stname}.'
 493            )
 494        self._shutdown_tasks.append(coro)
 495
 496    def run(self) -> None:
 497        """Run the app to completion.
 498
 499        Note that this only works on builds where Ballistica manages
 500        its own event loop.
 501        """
 502        _babase.run_app()
 503
 504    def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
 505        """Submit a call to the app threadpool where result is not needed.
 506
 507        Normally, doing work in a thread-pool involves creating a future
 508        and waiting for its result, which is an important step because it
 509        propagates any Exceptions raised by the submitted work. When the
 510        result in not important, however, this call can be used. The app
 511        will log any exceptions that occur.
 512        """
 513        fut = self.threadpool.submit(call)
 514        fut.add_done_callback(self._threadpool_no_wait_done)
 515
 516    def set_intent(self, intent: AppIntent) -> None:
 517        """Set the intent for the app.
 518
 519        Intent defines what the app is trying to do at a given time.
 520        This call is asynchronous; the intent switch will happen in the
 521        logic thread in the near future. If set_intent is called
 522        repeatedly before the change takes place, the final intent to be
 523        set will be used.
 524        """
 525
 526        # Mark this one as pending. We do this synchronously so that the
 527        # last one marked actually takes effect if there is overlap
 528        # (doing this in the bg thread could result in race conditions).
 529        self._pending_intent = intent
 530
 531        # Do the actual work of calcing our app-mode/etc. in a bg thread
 532        # since it may block for a moment to load modules/etc.
 533        self.threadpool_submit_no_wait(partial(self._set_intent, intent))
 534
 535    def push_apply_app_config(self) -> None:
 536        """Internal. Use app.config.apply() to apply app config changes."""
 537        # To be safe, let's run this by itself in the event loop.
 538        # This avoids potential trouble if this gets called mid-draw or
 539        # something like that.
 540        self._pending_apply_app_config = True
 541        _babase.pushcall(self._apply_app_config, raw=True)
 542
 543    def on_native_start(self) -> None:
 544        """Called by the native layer when the app is being started."""
 545        assert _babase.in_logic_thread()
 546        assert not self._native_start_called
 547        self._native_start_called = True
 548        self._update_state()
 549
 550    def on_native_bootstrapping_complete(self) -> None:
 551        """Called by the native layer once its ready to rock."""
 552        assert _babase.in_logic_thread()
 553        assert not self._native_bootstrapping_completed
 554        self._native_bootstrapping_completed = True
 555        self._update_state()
 556
 557    def on_native_suspend(self) -> None:
 558        """Called by the native layer when the app is suspended."""
 559        assert _babase.in_logic_thread()
 560        assert not self._native_suspended  # Should avoid redundant calls.
 561        self._native_suspended = True
 562        self._update_state()
 563
 564    def on_native_unsuspend(self) -> None:
 565        """Called by the native layer when the app suspension ends."""
 566        assert _babase.in_logic_thread()
 567        assert self._native_suspended  # Should avoid redundant calls.
 568        self._native_suspended = False
 569        self._update_state()
 570
 571    def on_native_shutdown(self) -> None:
 572        """Called by the native layer when the app starts shutting down."""
 573        assert _babase.in_logic_thread()
 574        self._native_shutdown_called = True
 575        self._update_state()
 576
 577    def on_native_shutdown_complete(self) -> None:
 578        """Called by the native layer when the app is done shutting down."""
 579        assert _babase.in_logic_thread()
 580        self._native_shutdown_complete_called = True
 581        self._update_state()
 582
 583    def on_native_active_changed(self) -> None:
 584        """Called by the native layer when the app active state changes."""
 585        assert _babase.in_logic_thread()
 586        if self._mode is not None:
 587            self._mode.on_app_active_changed()
 588
 589    def read_config(self) -> None:
 590        """(internal)"""
 591        from babase._appconfig import read_app_config
 592
 593        self._config = read_app_config()
 594
 595    def handle_deep_link(self, url: str) -> None:
 596        """Handle a deep link URL."""
 597        from babase._language import Lstr
 598
 599        assert _babase.in_logic_thread()
 600
 601        appname = _babase.appname()
 602        if url.startswith(f'{appname}://code/'):
 603            code = url.replace(f'{appname}://code/', '')
 604            if self.classic is not None:
 605                self.classic.accounts.add_pending_promo_code(code)
 606        else:
 607            try:
 608                _babase.screenmessage(
 609                    Lstr(resource='errorText'), color=(1, 0, 0)
 610                )
 611                _babase.getsimplesound('error').play()
 612            except ImportError:
 613                pass
 614
 615    def on_initial_sign_in_complete(self) -> None:
 616        """Called when initial sign-in (or lack thereof) completes.
 617
 618        This normally gets called by the plus subsystem. The
 619        initial-sign-in process may include tasks such as syncing
 620        account workspaces or other data so it may take a substantial
 621        amount of time.
 622        """
 623        assert _babase.in_logic_thread()
 624        assert not self._initial_sign_in_completed
 625
 626        # Tell meta it can start scanning extra stuff that just showed
 627        # up (namely account workspaces).
 628        self.meta.start_extra_scan()
 629
 630        self._initial_sign_in_completed = True
 631        self._update_state()
 632
 633    def _set_intent(self, intent: AppIntent) -> None:
 634        from babase._appmode import AppMode
 635
 636        # This should be happening in a bg thread.
 637        assert not _babase.in_logic_thread()
 638        try:
 639            # Ask the selector what app-mode to use for this intent.
 640            if self.mode_selector is None:
 641                raise RuntimeError('No AppModeSelector set.')
 642
 643            modetype: type[AppMode] | None
 644
 645            # Special case - for testing we may force a specific
 646            # app-mode to handle this intent instead of going through our
 647            # usual selector.
 648            forced_mode_type = getattr(intent, '_force_app_mode_handler', None)
 649            if isinstance(forced_mode_type, type) and issubclass(
 650                forced_mode_type, AppMode
 651            ):
 652                modetype = forced_mode_type
 653            else:
 654                modetype = self.mode_selector.app_mode_for_intent(intent)
 655
 656            # NOTE: Since intents are somewhat high level things,
 657            # perhaps we should do some universal thing like a
 658            # screenmessage saying 'The app cannot handle the request'
 659            # on failure.
 660
 661            if modetype is None:
 662                raise RuntimeError(
 663                    f'No app-mode found to handle app-intent'
 664                    f' type {type(intent)}.'
 665                )
 666
 667            # Make sure the app-mode the selector gave us *actually*
 668            # supports the intent.
 669            if not modetype.can_handle_intent(intent):
 670                raise RuntimeError(
 671                    f'Intent {intent} cannot be handled by AppMode type'
 672                    f' {modetype} (selector {self.mode_selector}'
 673                    f' incorrectly thinks that it can be).'
 674                )
 675
 676            # Ok; seems legit. Now instantiate the mode if necessary and
 677            # kick back to the logic thread to apply.
 678            mode = self._mode_instances.get(modetype)
 679            if mode is None:
 680                self._mode_instances[modetype] = mode = modetype()
 681            _babase.pushcall(
 682                partial(self._apply_intent, intent, mode),
 683                from_other_thread=True,
 684            )
 685        except Exception:
 686            logging.exception('Error setting app intent to %s.', intent)
 687            _babase.pushcall(
 688                partial(self._display_set_intent_error, intent),
 689                from_other_thread=True,
 690            )
 691
 692    def _apply_intent(self, intent: AppIntent, mode: AppMode) -> None:
 693        assert _babase.in_logic_thread()
 694
 695        # ONLY apply this intent if it is still the most recent one
 696        # submitted.
 697        if intent is not self._pending_intent:
 698            return
 699
 700        # If the app-mode for this intent is different than the active
 701        # one, switch modes.
 702        if type(mode) is not type(self._mode):
 703            if self._mode is None:
 704                is_initial_mode = True
 705            else:
 706                is_initial_mode = False
 707                try:
 708                    self._mode.on_deactivate()
 709                except Exception:
 710                    logging.exception(
 711                        'Error deactivating app-mode %s.', self._mode
 712                    )
 713
 714            # Reset all subsystems. We assume subsystems won't be added
 715            # at this point so we can use the list directly.
 716            assert self._subsystem_registration_ended
 717            for subsystem in self._subsystems:
 718                try:
 719                    subsystem.reset()
 720                except Exception:
 721                    logging.exception(
 722                        'Error in reset for subsystem %s.', subsystem
 723                    )
 724
 725            self._mode = mode
 726            try:
 727                mode.on_activate()
 728            except Exception:
 729                # Hmm; what should we do in this case?...
 730                logging.exception('Error activating app-mode %s.', mode)
 731
 732            # Let the world know when we first have an app-mode; certain
 733            # app stuff such as input processing can proceed at that
 734            # point.
 735            if is_initial_mode:
 736                _babase.on_initial_app_mode_set()
 737
 738        try:
 739            mode.handle_intent(intent)
 740        except Exception:
 741            logging.exception(
 742                'Error handling intent %s in app-mode %s.', intent, mode
 743            )
 744
 745    def _display_set_intent_error(self, intent: AppIntent) -> None:
 746        """Show the *user* something went wrong setting an intent."""
 747        from babase._language import Lstr
 748
 749        del intent
 750        _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
 751        _babase.getsimplesound('error').play()
 752
 753    def _on_initing(self) -> None:
 754        """Called when the app enters the initing state.
 755
 756        Here we can put together subsystems and other pieces for the
 757        app, but most things should not be doing any work yet.
 758        """
 759        # pylint: disable=cyclic-import
 760        from babase import _asyncio
 761        from babase import _appconfig
 762        from babase._apputils import AppHealthMonitor
 763        from babase import _env
 764
 765        assert _babase.in_logic_thread()
 766
 767        _env.on_app_state_initing()
 768
 769        self._asyncio_loop = _asyncio.setup_asyncio()
 770        self.health_monitor = AppHealthMonitor()
 771
 772        # __FEATURESET_APP_SUBSYSTEM_CREATE_BEGIN__
 773        # This section generated by batools.appmodule; do not edit.
 774
 775        # Poke these attrs to create all our subsystems.
 776        _ = self.plus
 777        _ = self.classic
 778        _ = self.ui_v1
 779
 780        # __FEATURESET_APP_SUBSYSTEM_CREATE_END__
 781
 782        # We're a pretty short-lived state. This should flip us to
 783        # 'loading'.
 784        self._init_completed = True
 785        self._update_state()
 786
 787    def _on_loading(self) -> None:
 788        """Called when we enter the loading state.
 789
 790        At this point, all built-in pieces of the app should be in place
 791        and can start talking to each other and doing work. Though at a
 792        high level, the goal of the app at this point is only to sign in
 793        to initial accounts, download workspaces, and otherwise prepare
 794        itself to really 'run'.
 795        """
 796        assert _babase.in_logic_thread()
 797
 798        # Get meta-system scanning built-in stuff in the bg.
 799        self.meta.start_scan(scan_complete_cb=self._on_meta_scan_complete)
 800
 801        # Inform all app subsystems in the same order they were inited.
 802        # Operate on a copy of the list here because subsystems can
 803        # still be added at this point.
 804        for subsystem in self._subsystems.copy():
 805            try:
 806                subsystem.on_app_loading()
 807            except Exception:
 808                logging.exception(
 809                    'Error in on_app_loading for subsystem %s.', subsystem
 810                )
 811
 812        # Normally plus tells us when initial sign-in is done. If plus
 813        # is not present, however, we just do it ourself so we can
 814        # proceed on to the running state.
 815        if self.plus is None:
 816            _babase.pushcall(self.on_initial_sign_in_complete)
 817
 818    def _on_meta_scan_complete(self) -> None:
 819        """Called when meta-scan is done doing its thing."""
 820        assert _babase.in_logic_thread()
 821
 822        # Now that we know what's out there, build our final plugin set.
 823        self.plugins.on_meta_scan_complete()
 824
 825        assert not self._meta_scan_completed
 826        self._meta_scan_completed = True
 827        self._update_state()
 828
 829    def _on_running(self) -> None:
 830        """Called when we enter the running state.
 831
 832        At this point, all workspaces, initial accounts, etc. are in place
 833        and we can actually get started doing whatever we're gonna do.
 834        """
 835        assert _babase.in_logic_thread()
 836
 837        # Let our native layer know.
 838        _babase.on_app_running()
 839
 840        # Set a default app-mode-selector if none has been set yet
 841        # by a plugin or whatnot.
 842        if self._mode_selector is None:
 843            self._mode_selector = self.DefaultAppModeSelector()
 844
 845        # Inform all app subsystems in the same order they were
 846        # registered. Operate on a copy here because subsystems can
 847        # still be added at this point.
 848        #
 849        # NOTE: Do we need to allow registering still at this point? If
 850        # something gets registered here, it won't have its
 851        # on_app_running callback called. Hmm; I suppose that's the only
 852        # way that plugins can register subsystems though.
 853        for subsystem in self._subsystems.copy():
 854            try:
 855                subsystem.on_app_running()
 856            except Exception:
 857                logging.exception(
 858                    'Error in on_app_running for subsystem %s.', subsystem
 859                )
 860
 861        # Cut off new subsystem additions at this point.
 862        self._subsystem_registration_ended = True
 863
 864        # If 'exec' code was provided to the app, always kick that off
 865        # here as an intent.
 866        exec_cmd = _babase.exec_arg()
 867        if exec_cmd is not None:
 868            self.set_intent(AppIntentExec(exec_cmd))
 869        elif self._pending_intent is None:
 870            # Otherwise tell the app to do its default thing *only* if a
 871            # plugin hasn't already told it to do something.
 872            self.set_intent(AppIntentDefault())
 873
 874    def _apply_app_config(self) -> None:
 875        assert _babase.in_logic_thread()
 876
 877        _babase.lifecyclelog('apply-app-config')
 878
 879        # If multiple apply calls have been made, only actually apply
 880        # once.
 881        if not self._pending_apply_app_config:
 882            return
 883
 884        _pending_apply_app_config = False
 885
 886        # Inform all app subsystems in the same order they were inited.
 887        # Operate on a copy here because subsystems may still be able to
 888        # be added at this point.
 889        for subsystem in self._subsystems.copy():
 890            try:
 891                subsystem.do_apply_app_config()
 892            except Exception:
 893                logging.exception(
 894                    'Error in do_apply_app_config for subsystem %s.', subsystem
 895                )
 896
 897        # Let the native layer do its thing.
 898        _babase.do_apply_app_config()
 899
 900    def _update_state(self) -> None:
 901        # pylint: disable=too-many-branches
 902        assert _babase.in_logic_thread()
 903
 904        # Shutdown-complete trumps absolutely all.
 905        if self._native_shutdown_complete_called:
 906            if self.state is not self.State.SHUTDOWN_COMPLETE:
 907                self.state = self.State.SHUTDOWN_COMPLETE
 908                _babase.lifecyclelog('app state shutdown complete')
 909                self._on_shutdown_complete()
 910
 911        # Shutdown trumps all. Though we can't start shutting down until
 912        # init is completed since we need our asyncio stuff to exist for
 913        # the shutdown process.
 914        elif self._native_shutdown_called and self._init_completed:
 915            # Entering shutdown state:
 916            if self.state is not self.State.SHUTTING_DOWN:
 917                self.state = self.State.SHUTTING_DOWN
 918                _babase.lifecyclelog('app state shutting down')
 919                self._on_shutting_down()
 920
 921        elif self._native_suspended:
 922            # Entering suspended state:
 923            if self.state is not self.State.SUSPENDED:
 924                self.state = self.State.SUSPENDED
 925                self._on_suspend()
 926        else:
 927            # Leaving suspended state:
 928            if self.state is self.State.SUSPENDED:
 929                self._on_unsuspend()
 930
 931            # Entering or returning to running state
 932            if self._initial_sign_in_completed and self._meta_scan_completed:
 933                if self.state != self.State.RUNNING:
 934                    self.state = self.State.RUNNING
 935                    _babase.lifecyclelog('app state running')
 936                    if not self._called_on_running:
 937                        self._called_on_running = True
 938                        self._on_running()
 939            # Entering or returning to loading state:
 940            elif self._init_completed:
 941                if self.state is not self.State.LOADING:
 942                    self.state = self.State.LOADING
 943                    _babase.lifecyclelog('app state loading')
 944                    if not self._called_on_loading:
 945                        self._called_on_loading = True
 946                        self._on_loading()
 947
 948            # Entering or returning to initing state:
 949            elif self._native_bootstrapping_completed:
 950                if self.state is not self.State.INITING:
 951                    self.state = self.State.INITING
 952                    _babase.lifecyclelog('app state initing')
 953                    if not self._called_on_initing:
 954                        self._called_on_initing = True
 955                        self._on_initing()
 956
 957            # Entering or returning to native bootstrapping:
 958            elif self._native_start_called:
 959                if self.state is not self.State.NATIVE_BOOTSTRAPPING:
 960                    self.state = self.State.NATIVE_BOOTSTRAPPING
 961                    _babase.lifecyclelog('app state native bootstrapping')
 962            else:
 963                # Only logical possibility left is NOT_STARTED, in which
 964                # case we should not be getting called.
 965                logging.warning(
 966                    'App._update_state called while in %s state;'
 967                    ' should not happen.',
 968                    self.state.value,
 969                    stack_info=True,
 970                )
 971
 972    async def _shutdown(self) -> None:
 973        import asyncio
 974
 975        _babase.lock_all_input()
 976        try:
 977            async with asyncio.TaskGroup() as task_group:
 978                for task_coro in self._shutdown_tasks:
 979                    # Note: Mypy currently complains if we don't take
 980                    # this return value, but we don't actually need to.
 981                    # https://github.com/python/mypy/issues/15036
 982                    _ = task_group.create_task(
 983                        self._run_shutdown_task(task_coro)
 984                    )
 985        except* Exception:
 986            logging.exception('Unexpected error(s) in shutdown.')
 987
 988        # Note: ideally we should run this directly here, but currently
 989        # it does some legacy stuff which blocks, so running it here
 990        # gives us asyncio task-took-too-long warnings. If we can
 991        # convert those to nice graceful async tasks we should revert
 992        # this to a direct call.
 993        _babase.pushcall(_babase.complete_shutdown)
 994
 995    async def _run_shutdown_task(
 996        self, coro: Coroutine[None, None, None]
 997    ) -> None:
 998        """Run a shutdown task; report errors and abort if taking too long."""
 999        import asyncio
1000
1001        task = asyncio.create_task(coro)
1002        try:
1003            await asyncio.wait_for(task, self.SHUTDOWN_TASK_TIMEOUT_SECONDS)
1004        except Exception:
1005            logging.exception('Error in shutdown task (%s).', coro)
1006
1007    def _on_suspend(self) -> None:
1008        """Called when the app goes to a suspended state."""
1009        assert _babase.in_logic_thread()
1010
1011        # Suspend all app subsystems in the opposite order they were inited.
1012        for subsystem in reversed(self._subsystems):
1013            try:
1014                subsystem.on_app_suspend()
1015            except Exception:
1016                logging.exception(
1017                    'Error in on_app_suspend for subsystem %s.', subsystem
1018                )
1019
1020    def _on_unsuspend(self) -> None:
1021        """Called when unsuspending."""
1022        assert _babase.in_logic_thread()
1023        self.fg_state += 1
1024
1025        # Unsuspend all app subsystems in the same order they were inited.
1026        for subsystem in self._subsystems:
1027            try:
1028                subsystem.on_app_unsuspend()
1029            except Exception:
1030                logging.exception(
1031                    'Error in on_app_unsuspend for subsystem %s.', subsystem
1032                )
1033
1034    def _on_shutting_down(self) -> None:
1035        """(internal)"""
1036        assert _babase.in_logic_thread()
1037
1038        # Inform app subsystems that we're shutting down in the opposite
1039        # order they were inited.
1040        for subsystem in reversed(self._subsystems):
1041            try:
1042                subsystem.on_app_shutdown()
1043            except Exception:
1044                logging.exception(
1045                    'Error in on_app_shutdown for subsystem %s.', subsystem
1046                )
1047
1048        # Now kick off any async shutdown task(s).
1049        assert self._asyncio_loop is not None
1050        self._shutdown_task = self._asyncio_loop.create_task(self._shutdown())
1051
1052    def _on_shutdown_complete(self) -> None:
1053        """(internal)"""
1054        assert _babase.in_logic_thread()
1055
1056        # Inform app subsystems that we're done shutting down in the opposite
1057        # order they were inited.
1058        for subsystem in reversed(self._subsystems):
1059            try:
1060                subsystem.on_app_shutdown_complete()
1061            except Exception:
1062                logging.exception(
1063                    'Error in on_app_shutdown_complete for subsystem %s.',
1064                    subsystem,
1065                )
1066
1067    async def _wait_for_shutdown_suppressions(self) -> None:
1068        import asyncio
1069
1070        # Spin and wait for anything blocking shutdown to complete.
1071        starttime = _babase.apptime()
1072        _babase.lifecyclelog('shutdown-suppress wait begin')
1073        while _babase.shutdown_suppress_count() > 0:
1074            await asyncio.sleep(0.001)
1075        _babase.lifecyclelog('shutdown-suppress wait end')
1076        duration = _babase.apptime() - starttime
1077        if duration > 1.0:
1078            logging.warning(
1079                'Shutdown-suppressions lasted longer than ideal '
1080                '(%.2f seconds).',
1081                duration,
1082            )
1083
1084    async def _fade_and_shutdown_graphics(self) -> None:
1085        import asyncio
1086
1087        # Kick off a short fade and give it time to complete.
1088        _babase.lifecyclelog('fade-and-shutdown-graphics begin')
1089        _babase.fade_screen(False, time=0.15)
1090        await asyncio.sleep(0.15)
1091
1092        # Now tell the graphics system to go down and wait until
1093        # it has done so.
1094        _babase.graphics_shutdown_begin()
1095        while not _babase.graphics_shutdown_is_complete():
1096            await asyncio.sleep(0.01)
1097        _babase.lifecyclelog('fade-and-shutdown-graphics end')
1098
1099    async def _fade_and_shutdown_audio(self) -> None:
1100        import asyncio
1101
1102        # Tell the audio system to go down and give it a bit of
1103        # time to do so gracefully.
1104        _babase.lifecyclelog('fade-and-shutdown-audio begin')
1105        _babase.audio_shutdown_begin()
1106        await asyncio.sleep(0.15)
1107        while not _babase.audio_shutdown_is_complete():
1108            await asyncio.sleep(0.01)
1109        _babase.lifecyclelog('fade-and-shutdown-audio end')
1110
1111    def _threadpool_no_wait_done(self, fut: Future) -> None:
1112        try:
1113            fut.result()
1114        except Exception:
1115            logging.exception(
1116                'Error in work submitted via threadpool_submit_no_wait()'
1117            )
1118
1119    def _thread_pool_thread_init(self) -> None:
1120        # Help keep things clear in profiling tools/etc.
1121        self._pool_thread_count += 1
1122        _babase.set_thread_name(f'ballistica worker-{self._pool_thread_count}')

A class for high level app functionality and state.

Category: App Classes

Use app to access the single shared instance of this class.

Note that properties not documented here should be considered internal and subject to change without warning.

plugins: PluginSubsystem
health_monitor: AppHealthMonitor
SHUTDOWN_TASK_TIMEOUT_SECONDS = 5
env: _babase.Env
state
threadpool
meta
net
workspaces
components
stringedit
devconsole
fg_state
def postinit(self) -> None:
248    def postinit(self) -> None:
249        """Called after we've been inited and assigned to babase.app.
250
251        Anything that accesses babase.app as part of its init process
252        must go here instead of __init__.
253        """
254
255        # Hack for docs-generation: We can be imported with dummy
256        # modules instead of our actual binary ones, but we don't
257        # function.
258        if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
259            return
260
261        self.lang = LanguageSubsystem()
262        self.plugins = PluginSubsystem()

Called after we've been inited and assigned to app.

Anything that accesses app as part of its init process must go here instead of __init__.

active: bool
264    @property
265    def active(self) -> bool:
266        """Whether the app is currently front and center.
267
268        This will be False when the app is hidden, other activities
269        are covering it, etc. (depending on the platform).
270        """
271        return _babase.app_is_active()

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

asyncio_loop: asyncio.events.AbstractEventLoop
273    @property
274    def asyncio_loop(self) -> asyncio.AbstractEventLoop:
275        """The logic thread's asyncio event loop.
276
277        This allow async tasks to be run in the logic thread.
278
279        Generally you should call App.create_async_task() to schedule
280        async code to run instead of using this directly. That will
281        handle retaining the task and logging errors automatically.
282        Only schedule tasks onto asyncio_loop yourself when you intend
283        to hold on to the returned task and await its results. Releasing
284        the task reference can lead to subtle bugs such as unreported
285        errors and garbage-collected tasks disappearing before their
286        work is done.
287
288        Note that, at this time, the asyncio loop is encapsulated
289        and explicitly stepped by the engine's logic thread loop and
290        thus things like asyncio.get_running_loop() will unintuitively
291        *not* return this loop from most places in the logic thread;
292        only from within a task explicitly created in this loop.
293        Hopefully this situation will be improved in the future with a
294        unified event loop.
295        """
296        assert _babase.in_logic_thread()
297        assert self._asyncio_loop is not None
298        return self._asyncio_loop

The logic thread's asyncio event loop.

This allow async tasks to be run in the logic thread.

Generally you should call App.create_async_task() to schedule async code to run instead of using this directly. That will handle retaining the task and logging errors automatically. Only schedule tasks onto asyncio_loop yourself when you intend to hold on to the returned task and await its results. Releasing the task reference can lead to subtle bugs such as unreported errors and garbage-collected tasks disappearing before their work is done.

Note that, at this time, the asyncio loop is encapsulated and explicitly stepped by the engine's logic thread loop and thus things like asyncio.get_running_loop() will unintuitively not return this loop from most places in the logic thread; only from within a task explicitly created in this loop. Hopefully this situation will be improved in the future with a unified event loop.

def create_async_task(self, coro: Coroutine[Any, Any, ~T], *, name: str | None = None) -> None:
300    def create_async_task(
301        self, coro: Coroutine[Any, Any, T], *, name: str | None = None
302    ) -> None:
303        """Create a fully managed async task.
304
305        This will automatically retain and release a reference to the task
306        and log any exceptions that occur in it. If you need to await a task
307        or otherwise need more control, schedule a task directly using
308        App.asyncio_loop.
309        """
310        assert _babase.in_logic_thread()
311
312        # We hold a strong reference to the task until it is done.
313        # Otherwise it is possible for it to be garbage collected and
314        # disappear midway if the caller does not hold on to the
315        # returned task, which seems like a great way to introduce
316        # hard-to-track bugs.
317        task = self.asyncio_loop.create_task(coro, name=name)
318        self._asyncio_tasks.add(task)
319        task.add_done_callback(self._on_task_done)

Create a fully managed async task.

This will automatically retain and release a reference to the task and log any exceptions that occur in it. If you need to await a task or otherwise need more control, schedule a task directly using App.asyncio_loop.

config: AppConfig
334    @property
335    def config(self) -> babase.AppConfig:
336        """The babase.AppConfig instance representing the app's config state."""
337        assert self._config is not None
338        return self._config

The AppConfig instance representing the app's config state.

mode_selector: AppModeSelector
340    @property
341    def mode_selector(self) -> babase.AppModeSelector:
342        """Controls which app-modes are used for handling given intents.
343
344        Plugins can override this to change high level app behavior and
345        spinoff projects can change the default implementation for the
346        same effect.
347        """
348        if self._mode_selector is None:
349            raise RuntimeError(
350                'mode_selector cannot be used until the app reaches'
351                ' the running state.'
352            )
353        return self._mode_selector

Controls which app-modes are used for handling given intents.

Plugins can override this to change high level app behavior and spinoff projects can change the default implementation for the same effect.

classic: baclassic.ClassicAppSubsystem | None
409    @property
410    def classic(self) -> ClassicAppSubsystem | None:
411        """Our classic subsystem (if available)."""
412        return self._get_subsystem_property(
413            'classic', self._create_classic_subsystem
414        )  # type: ignore

Our classic subsystem (if available).

plus: baplus.PlusAppSubsystem | None
429    @property
430    def plus(self) -> PlusAppSubsystem | None:
431        """Our plus subsystem (if available)."""
432        return self._get_subsystem_property(
433            'plus', self._create_plus_subsystem
434        )  # type: ignore

Our plus subsystem (if available).

ui_v1: bauiv1.UIV1AppSubsystem
449    @property
450    def ui_v1(self) -> UIV1AppSubsystem:
451        """Our ui_v1 subsystem (always available)."""
452        return self._get_subsystem_property(
453            'ui_v1', self._create_ui_v1_subsystem
454        )  # type: ignore

Our ui_v1 subsystem (always available).

def register_subsystem(self, subsystem: AppSubsystem) -> None:
466    def register_subsystem(self, subsystem: AppSubsystem) -> None:
467        """Called by the AppSubsystem class. Do not use directly."""
468
469        # We only allow registering new subsystems if we've not yet
470        # reached the 'running' state. This ensures that all subsystems
471        # receive a consistent set of callbacks starting with
472        # on_app_running().
473
474        if self._subsystem_registration_ended:
475            raise RuntimeError(
476                'Subsystems can no longer be registered at this point.'
477            )
478        self._subsystems.append(subsystem)

Called by the AppSubsystem class. Do not use directly.

def add_shutdown_task(self, coro: Coroutine[NoneType, NoneType, NoneType]) -> None:
480    def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
481        """Add a task to be run on app shutdown.
482
483        Note that shutdown tasks will be canceled after
484        App.SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.
485        """
486        if (
487            self.state is self.State.SHUTTING_DOWN
488            or self.state is self.State.SHUTDOWN_COMPLETE
489        ):
490            stname = self.state.name
491            raise RuntimeError(
492                f'Cannot add shutdown tasks with current state {stname}.'
493            )
494        self._shutdown_tasks.append(coro)

Add a task to be run on app shutdown.

Note that shutdown tasks will be canceled after App.SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.

def run(self) -> None:
496    def run(self) -> None:
497        """Run the app to completion.
498
499        Note that this only works on builds where Ballistica manages
500        its own event loop.
501        """
502        _babase.run_app()

Run the app to completion.

Note that this only works on builds where Ballistica manages its own event loop.

def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
504    def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
505        """Submit a call to the app threadpool where result is not needed.
506
507        Normally, doing work in a thread-pool involves creating a future
508        and waiting for its result, which is an important step because it
509        propagates any Exceptions raised by the submitted work. When the
510        result in not important, however, this call can be used. The app
511        will log any exceptions that occur.
512        """
513        fut = self.threadpool.submit(call)
514        fut.add_done_callback(self._threadpool_no_wait_done)

Submit a call to the app threadpool where result is not needed.

Normally, doing work in a thread-pool involves creating a future and waiting for its result, which is an important step because it propagates any Exceptions raised by the submitted work. When the result in not important, however, this call can be used. The app will log any exceptions that occur.

def set_intent(self, intent: AppIntent) -> None:
516    def set_intent(self, intent: AppIntent) -> None:
517        """Set the intent for the app.
518
519        Intent defines what the app is trying to do at a given time.
520        This call is asynchronous; the intent switch will happen in the
521        logic thread in the near future. If set_intent is called
522        repeatedly before the change takes place, the final intent to be
523        set will be used.
524        """
525
526        # Mark this one as pending. We do this synchronously so that the
527        # last one marked actually takes effect if there is overlap
528        # (doing this in the bg thread could result in race conditions).
529        self._pending_intent = intent
530
531        # Do the actual work of calcing our app-mode/etc. in a bg thread
532        # since it may block for a moment to load modules/etc.
533        self.threadpool_submit_no_wait(partial(self._set_intent, intent))

Set the intent for the app.

Intent defines what the app is trying to do at a given time. This call is asynchronous; the intent switch will happen in the logic thread in the near future. If set_intent is called repeatedly before the change takes place, the final intent to be set will be used.

def push_apply_app_config(self) -> None:
535    def push_apply_app_config(self) -> None:
536        """Internal. Use app.config.apply() to apply app config changes."""
537        # To be safe, let's run this by itself in the event loop.
538        # This avoids potential trouble if this gets called mid-draw or
539        # something like that.
540        self._pending_apply_app_config = True
541        _babase.pushcall(self._apply_app_config, raw=True)

Internal. Use app.config.apply() to apply app config changes.

def on_native_start(self) -> None:
543    def on_native_start(self) -> None:
544        """Called by the native layer when the app is being started."""
545        assert _babase.in_logic_thread()
546        assert not self._native_start_called
547        self._native_start_called = True
548        self._update_state()

Called by the native layer when the app is being started.

def on_native_bootstrapping_complete(self) -> None:
550    def on_native_bootstrapping_complete(self) -> None:
551        """Called by the native layer once its ready to rock."""
552        assert _babase.in_logic_thread()
553        assert not self._native_bootstrapping_completed
554        self._native_bootstrapping_completed = True
555        self._update_state()

Called by the native layer once its ready to rock.

def on_native_suspend(self) -> None:
557    def on_native_suspend(self) -> None:
558        """Called by the native layer when the app is suspended."""
559        assert _babase.in_logic_thread()
560        assert not self._native_suspended  # Should avoid redundant calls.
561        self._native_suspended = True
562        self._update_state()

Called by the native layer when the app is suspended.

def on_native_unsuspend(self) -> None:
564    def on_native_unsuspend(self) -> None:
565        """Called by the native layer when the app suspension ends."""
566        assert _babase.in_logic_thread()
567        assert self._native_suspended  # Should avoid redundant calls.
568        self._native_suspended = False
569        self._update_state()

Called by the native layer when the app suspension ends.

def on_native_shutdown(self) -> None:
571    def on_native_shutdown(self) -> None:
572        """Called by the native layer when the app starts shutting down."""
573        assert _babase.in_logic_thread()
574        self._native_shutdown_called = True
575        self._update_state()

Called by the native layer when the app starts shutting down.

def on_native_shutdown_complete(self) -> None:
577    def on_native_shutdown_complete(self) -> None:
578        """Called by the native layer when the app is done shutting down."""
579        assert _babase.in_logic_thread()
580        self._native_shutdown_complete_called = True
581        self._update_state()

Called by the native layer when the app is done shutting down.

def on_native_active_changed(self) -> None:
583    def on_native_active_changed(self) -> None:
584        """Called by the native layer when the app active state changes."""
585        assert _babase.in_logic_thread()
586        if self._mode is not None:
587            self._mode.on_app_active_changed()

Called by the native layer when the app active state changes.

def on_initial_sign_in_complete(self) -> None:
615    def on_initial_sign_in_complete(self) -> None:
616        """Called when initial sign-in (or lack thereof) completes.
617
618        This normally gets called by the plus subsystem. The
619        initial-sign-in process may include tasks such as syncing
620        account workspaces or other data so it may take a substantial
621        amount of time.
622        """
623        assert _babase.in_logic_thread()
624        assert not self._initial_sign_in_completed
625
626        # Tell meta it can start scanning extra stuff that just showed
627        # up (namely account workspaces).
628        self.meta.start_extra_scan()
629
630        self._initial_sign_in_completed = True
631        self._update_state()

Called when initial sign-in (or lack thereof) completes.

This normally gets called by the plus subsystem. The initial-sign-in process may include tasks such as syncing account workspaces or other data so it may take a substantial amount of time.

class App.State(enum.Enum):
 73    class State(Enum):
 74        """High level state the app can be in."""
 75
 76        # The app has not yet begun starting and should not be used in
 77        # any way.
 78        NOT_STARTED = 0
 79
 80        # The native layer is spinning up its machinery (screens,
 81        # renderers, etc.). Nothing should happen in the Python layer
 82        # until this completes.
 83        NATIVE_BOOTSTRAPPING = 1
 84
 85        # Python app subsystems are being inited but should not yet
 86        # interact or do any work.
 87        INITING = 2
 88
 89        # Python app subsystems are inited and interacting, but the app
 90        # has not yet embarked on a high level course of action. It is
 91        # doing initial account logins, workspace & asset downloads,
 92        # etc.
 93        LOADING = 3
 94
 95        # All pieces are in place and the app is now doing its thing.
 96        RUNNING = 4
 97
 98        # Used on platforms such as mobile where the app basically needs
 99        # to shut down while backgrounded. In this state, all event
100        # loops are suspended and all graphics and audio must cease
101        # completely. Be aware that the suspended state can be entered
102        # from any other state including NATIVE_BOOTSTRAPPING and
103        # SHUTTING_DOWN.
104        SUSPENDED = 5
105
106        # The app is shutting down. This process may involve sending
107        # network messages or other things that can take up to a few
108        # seconds, so ideally graphics and audio should remain
109        # functional (with fades or spinners or whatever to show
110        # something is happening).
111        SHUTTING_DOWN = 6
112
113        # The app has completed shutdown. Any code running here should
114        # be basically immediate.
115        SHUTDOWN_COMPLETE = 7

High level state the app can be in.

NOT_STARTED = <State.NOT_STARTED: 0>
NATIVE_BOOTSTRAPPING = <State.NATIVE_BOOTSTRAPPING: 1>
INITING = <State.INITING: 2>
LOADING = <State.LOADING: 3>
RUNNING = <State.RUNNING: 4>
SUSPENDED = <State.SUSPENDED: 5>
SHUTTING_DOWN = <State.SHUTTING_DOWN: 6>
SHUTDOWN_COMPLETE = <State.SHUTDOWN_COMPLETE: 7>
Inherited Members
enum.Enum
name
value
class App.DefaultAppModeSelector(babase.AppModeSelector):
117    class DefaultAppModeSelector(AppModeSelector):
118        """Decides which AppModes to use to handle AppIntents.
119
120        This default version is generated by the project updater based
121        on the 'default_app_modes' value in the projectconfig.
122
123        It is also possible to modify app mode selection behavior by
124        setting app.mode_selector to an instance of a custom
125        AppModeSelector subclass. This is a good way to go if you are
126        modifying app behavior dynamically via a plugin instead of
127        statically in a spinoff project.
128        """
129
130        @override
131        def app_mode_for_intent(
132            self, intent: AppIntent
133        ) -> type[AppMode] | None:
134            # pylint: disable=cyclic-import
135
136            # __DEFAULT_APP_MODE_SELECTION_BEGIN__
137            # This section generated by batools.appmodule; do not edit.
138
139            # Ask our default app modes to handle it.
140            # (generated from 'default_app_modes' in projectconfig).
141            import baclassic
142            import babase
143
144            for appmode in [
145                baclassic.ClassicAppMode,
146                babase.EmptyAppMode,
147            ]:
148                if appmode.can_handle_intent(intent):
149                    return appmode
150
151            return None
152
153            # __DEFAULT_APP_MODE_SELECTION_END__
154
155        @override
156        def testable_app_modes(self) -> list[type[AppMode]]:
157            # pylint: disable=cyclic-import
158
159            # __DEFAULT_TESTABLE_APP_MODES_BEGIN__
160            # This section generated by batools.appmodule; do not edit.
161
162            # Return all our default_app_modes as testable.
163            # (generated from 'default_app_modes' in projectconfig).
164            import baclassic
165            import babase
166
167            return [
168                baclassic.ClassicAppMode,
169                babase.EmptyAppMode,
170            ]
171            # __DEFAULT_TESTABLE_APP_MODES_END__

Decides which AppModes to use to handle AppIntents.

This default version is generated by the project updater based on the 'default_app_modes' value in the projectconfig.

It is also possible to modify app mode selection behavior by setting app.mode_selector to an instance of a custom AppModeSelector subclass. This is a good way to go if you are modifying app behavior dynamically via a plugin instead of statically in a spinoff project.

@override
def app_mode_for_intent( self, intent: AppIntent) -> type[AppMode] | None:
130        @override
131        def app_mode_for_intent(
132            self, intent: AppIntent
133        ) -> type[AppMode] | None:
134            # pylint: disable=cyclic-import
135
136            # __DEFAULT_APP_MODE_SELECTION_BEGIN__
137            # This section generated by batools.appmodule; do not edit.
138
139            # Ask our default app modes to handle it.
140            # (generated from 'default_app_modes' in projectconfig).
141            import baclassic
142            import babase
143
144            for appmode in [
145                baclassic.ClassicAppMode,
146                babase.EmptyAppMode,
147            ]:
148                if appmode.can_handle_intent(intent):
149                    return appmode
150
151            return None
152
153            # __DEFAULT_APP_MODE_SELECTION_END__

Given an AppIntent, return the AppMode that should handle it.

If None is returned, the AppIntent will be ignored.

This may be called in a background thread, so avoid any calls limited to logic thread use/etc.

@override
def testable_app_modes(self) -> list[type[AppMode]]:
155        @override
156        def testable_app_modes(self) -> list[type[AppMode]]:
157            # pylint: disable=cyclic-import
158
159            # __DEFAULT_TESTABLE_APP_MODES_BEGIN__
160            # This section generated by batools.appmodule; do not edit.
161
162            # Return all our default_app_modes as testable.
163            # (generated from 'default_app_modes' in projectconfig).
164            import baclassic
165            import babase
166
167            return [
168                baclassic.ClassicAppMode,
169                babase.EmptyAppMode,
170            ]
171            # __DEFAULT_TESTABLE_APP_MODES_END__

Return a list of modes to appear in the dev-console app-mode ui.

The user can switch between these app modes for testing. App-modes will be passed an AppIntentDefault when selected by the user.

Note that in normal circumstances AppModes should never be selected explicitly by the user but rather determined implicitly based on AppIntents.

class AppConfig(builtins.dict):
 18class AppConfig(dict):
 19    """A special dict that holds the game's persistent configuration values.
 20
 21    Category: **App Classes**
 22
 23    It also provides methods for fetching values with app-defined fallback
 24    defaults, applying contained values to the game, and committing the
 25    config to storage.
 26
 27    Call babase.appconfig() to get the single shared instance of this class.
 28
 29    AppConfig data is stored as json on disk on so make sure to only place
 30    json-friendly values in it (dict, list, str, float, int, bool).
 31    Be aware that tuples will be quietly converted to lists when stored.
 32    """
 33
 34    def resolve(self, key: str) -> Any:
 35        """Given a string key, return a config value (type varies).
 36
 37        This will substitute application defaults for values not present in
 38        the config dict, filter some invalid values, etc.  Note that these
 39        values do not represent the state of the app; simply the state of its
 40        config. Use babase.App to access actual live state.
 41
 42        Raises an Exception for unrecognized key names. To get the list of keys
 43        supported by this method, use babase.AppConfig.builtin_keys(). Note
 44        that it is perfectly legal to store other data in the config; it just
 45        needs to be accessed through standard dict methods and missing values
 46        handled manually.
 47        """
 48        return _babase.resolve_appconfig_value(key)
 49
 50    def default_value(self, key: str) -> Any:
 51        """Given a string key, return its predefined default value.
 52
 53        This is the value that will be returned by babase.AppConfig.resolve()
 54        if the key is not present in the config dict or of an incompatible
 55        type.
 56
 57        Raises an Exception for unrecognized key names. To get the list of keys
 58        supported by this method, use babase.AppConfig.builtin_keys(). Note
 59        that it is perfectly legal to store other data in the config; it just
 60        needs to be accessed through standard dict methods and missing values
 61        handled manually.
 62        """
 63        return _babase.get_appconfig_default_value(key)
 64
 65    def builtin_keys(self) -> list[str]:
 66        """Return the list of valid key names recognized by babase.AppConfig.
 67
 68        This set of keys can be used with resolve(), default_value(), etc.
 69        It does not vary across platforms and may include keys that are
 70        obsolete or not relevant on the current running version. (for instance,
 71        VR related keys on non-VR platforms). This is to minimize the amount
 72        of platform checking necessary)
 73
 74        Note that it is perfectly legal to store arbitrary named data in the
 75        config, but in that case it is up to the user to test for the existence
 76        of the key in the config dict, fall back to consistent defaults, etc.
 77        """
 78        return _babase.get_appconfig_builtin_keys()
 79
 80    def apply(self) -> None:
 81        """Apply config values to the running app.
 82
 83        This call is thread-safe and asynchronous; changes will happen
 84        in the next logic event loop cycle.
 85        """
 86        _babase.app.push_apply_app_config()
 87
 88    def commit(self) -> None:
 89        """Commits the config to local storage.
 90
 91        Note that this call is asynchronous so the actual write to disk may not
 92        occur immediately.
 93        """
 94        commit_app_config()
 95
 96    def apply_and_commit(self) -> None:
 97        """Run apply() followed by commit(); for convenience.
 98
 99        (This way the commit() will not occur if apply() hits invalid data)
100        """
101        self.apply()
102        self.commit()

A special dict that holds the game's persistent configuration values.

Category: App Classes

It also provides methods for fetching values with app-defined fallback defaults, applying contained values to the game, and committing the config to storage.

Call babase.appconfig() to get the single shared instance of this class.

AppConfig data is stored as json on disk on so make sure to only place json-friendly values in it (dict, list, str, float, int, bool). Be aware that tuples will be quietly converted to lists when stored.

def resolve(self, key: str) -> Any:
34    def resolve(self, key: str) -> Any:
35        """Given a string key, return a config value (type varies).
36
37        This will substitute application defaults for values not present in
38        the config dict, filter some invalid values, etc.  Note that these
39        values do not represent the state of the app; simply the state of its
40        config. Use babase.App to access actual live state.
41
42        Raises an Exception for unrecognized key names. To get the list of keys
43        supported by this method, use babase.AppConfig.builtin_keys(). Note
44        that it is perfectly legal to store other data in the config; it just
45        needs to be accessed through standard dict methods and missing values
46        handled manually.
47        """
48        return _babase.resolve_appconfig_value(key)

Given a string key, return a config value (type varies).

This will substitute application defaults for values not present in the config dict, filter some invalid values, etc. Note that these values do not represent the state of the app; simply the state of its config. Use App to access actual live state.

Raises an Exception for unrecognized key names. To get the list of keys supported by this method, use AppConfig.builtin_keys(). Note that it is perfectly legal to store other data in the config; it just needs to be accessed through standard dict methods and missing values handled manually.

def default_value(self, key: str) -> Any:
50    def default_value(self, key: str) -> Any:
51        """Given a string key, return its predefined default value.
52
53        This is the value that will be returned by babase.AppConfig.resolve()
54        if the key is not present in the config dict or of an incompatible
55        type.
56
57        Raises an Exception for unrecognized key names. To get the list of keys
58        supported by this method, use babase.AppConfig.builtin_keys(). Note
59        that it is perfectly legal to store other data in the config; it just
60        needs to be accessed through standard dict methods and missing values
61        handled manually.
62        """
63        return _babase.get_appconfig_default_value(key)

Given a string key, return its predefined default value.

This is the value that will be returned by AppConfig.resolve() if the key is not present in the config dict or of an incompatible type.

Raises an Exception for unrecognized key names. To get the list of keys supported by this method, use AppConfig.builtin_keys(). Note that it is perfectly legal to store other data in the config; it just needs to be accessed through standard dict methods and missing values handled manually.

def builtin_keys(self) -> list[str]:
65    def builtin_keys(self) -> list[str]:
66        """Return the list of valid key names recognized by babase.AppConfig.
67
68        This set of keys can be used with resolve(), default_value(), etc.
69        It does not vary across platforms and may include keys that are
70        obsolete or not relevant on the current running version. (for instance,
71        VR related keys on non-VR platforms). This is to minimize the amount
72        of platform checking necessary)
73
74        Note that it is perfectly legal to store arbitrary named data in the
75        config, but in that case it is up to the user to test for the existence
76        of the key in the config dict, fall back to consistent defaults, etc.
77        """
78        return _babase.get_appconfig_builtin_keys()

Return the list of valid key names recognized by AppConfig.

This set of keys can be used with resolve(), default_value(), etc. It does not vary across platforms and may include keys that are obsolete or not relevant on the current running version. (for instance, VR related keys on non-VR platforms). This is to minimize the amount of platform checking necessary)

Note that it is perfectly legal to store arbitrary named data in the config, but in that case it is up to the user to test for the existence of the key in the config dict, fall back to consistent defaults, etc.

def apply(self) -> None:
80    def apply(self) -> None:
81        """Apply config values to the running app.
82
83        This call is thread-safe and asynchronous; changes will happen
84        in the next logic event loop cycle.
85        """
86        _babase.app.push_apply_app_config()

Apply config values to the running app.

This call is thread-safe and asynchronous; changes will happen in the next logic event loop cycle.

def commit(self) -> None:
88    def commit(self) -> None:
89        """Commits the config to local storage.
90
91        Note that this call is asynchronous so the actual write to disk may not
92        occur immediately.
93        """
94        commit_app_config()

Commits the config to local storage.

Note that this call is asynchronous so the actual write to disk may not occur immediately.

def apply_and_commit(self) -> None:
 96    def apply_and_commit(self) -> None:
 97        """Run apply() followed by commit(); for convenience.
 98
 99        (This way the commit() will not occur if apply() hits invalid data)
100        """
101        self.apply()
102        self.commit()

Run apply() followed by commit(); for convenience.

(This way the commit() will not occur if apply() hits invalid data)

Inherited Members
builtins.dict
get
setdefault
pop
popitem
keys
items
values
update
fromkeys
clear
copy
class AppHealthMonitor(babase.AppSubsystem):
379class AppHealthMonitor(AppSubsystem):
380    """Logs things like app-not-responding issues."""
381
382    def __init__(self) -> None:
383        assert _babase.in_logic_thread()
384        super().__init__()
385        self._running = True
386        self._thread = Thread(target=self._app_monitor_thread_main, daemon=True)
387        self._thread.start()
388        self._response = False
389        self._first_check = True
390
391    @override
392    def on_app_loading(self) -> None:
393        # If any traceback dumps happened last run, log and clear them.
394        log_dumped_app_state(from_previous_run=True)
395
396    def _app_monitor_thread_main(self) -> None:
397        _babase.set_thread_name('ballistica app-monitor')
398        try:
399            self._monitor_app()
400        except Exception:
401            logging.exception('Error in AppHealthMonitor thread.')
402
403    def _set_response(self) -> None:
404        assert _babase.in_logic_thread()
405        self._response = True
406
407    def _check_running(self) -> bool:
408        # Workaround for the fact that mypy assumes _running
409        # doesn't change during the course of a function.
410        return self._running
411
412    def _monitor_app(self) -> None:
413        import time
414
415        while bool(True):
416            # Always sleep a bit between checks.
417            time.sleep(1.234)
418
419            # Do nothing while backgrounded.
420            while not self._running:
421                time.sleep(2.3456)
422
423            # Wait for the logic thread to run something we send it.
424            starttime = time.monotonic()
425            self._response = False
426            _babase.pushcall(self._set_response, raw=True)
427            while not self._response:
428                # Abort this check if we went into the background.
429                if not self._check_running():
430                    break
431
432                # Wait a bit longer the first time through since the app
433                # could still be starting up; we generally don't want to
434                # report that.
435                threshold = 10 if self._first_check else 5
436
437                # If we've been waiting too long (and the app is running)
438                # dump the app state and bail. Make an exception for the
439                # first check though since the app could just be taking
440                # a while to get going; we don't want to report that.
441                duration = time.monotonic() - starttime
442                if duration > threshold:
443                    dump_app_state(
444                        reason=f'Logic thread unresponsive'
445                        f' for {threshold} seconds.'
446                    )
447
448                    # We just do one alert for now.
449                    return
450
451                time.sleep(1.042)
452
453            self._first_check = False
454
455    @override
456    def on_app_suspend(self) -> None:
457        assert _babase.in_logic_thread()
458        self._running = False
459
460    @override
461    def on_app_unsuspend(self) -> None:
462        assert _babase.in_logic_thread()
463        self._running = True

Logs things like app-not-responding issues.

@override
def on_app_loading(self) -> None:
391    @override
392    def on_app_loading(self) -> None:
393        # If any traceback dumps happened last run, log and clear them.
394        log_dumped_app_state(from_previous_run=True)

Called when the app reaches the loading state.

Note that subsystems created after the app switches to the loading state will not receive this callback. Subsystems created by plugins are an example of this.

@override
def on_app_suspend(self) -> None:
455    @override
456    def on_app_suspend(self) -> None:
457        assert _babase.in_logic_thread()
458        self._running = False

Called when the app enters the suspended state.

@override
def on_app_unsuspend(self) -> None:
460    @override
461    def on_app_unsuspend(self) -> None:
462        assert _babase.in_logic_thread()
463        self._running = True

Called when the app exits the suspended state.

class AppIntent:
13class AppIntent:
14    """A high level directive given to the app.
15
16    Category: **App Classes**
17    """

A high level directive given to the app.

Category: App Classes

class AppIntentDefault(babase.AppIntent):
20class AppIntentDefault(AppIntent):
21    """Tells the app to simply run in its default mode."""

Tells the app to simply run in its default mode.

class AppIntentExec(babase.AppIntent):
24class AppIntentExec(AppIntent):
25    """Tells the app to exec some Python code."""
26
27    def __init__(self, code: str):
28        self.code = code

Tells the app to exec some Python code.

AppIntentExec(code: str)
27    def __init__(self, code: str):
28        self.code = code
code
class AppMode:
14class AppMode:
15    """A high level mode for the app.
16
17    Category: **App Classes**
18
19    """
20
21    @classmethod
22    def get_app_experience(cls) -> AppExperience:
23        """Return the overall experience provided by this mode."""
24        raise NotImplementedError('AppMode subclasses must override this.')
25
26    @classmethod
27    def can_handle_intent(cls, intent: AppIntent) -> bool:
28        """Return whether this mode can handle the provided intent.
29
30        For this to return True, the AppMode must claim to support the
31        provided intent (via its _supports_intent() method) AND the
32        AppExperience associated with the AppMode must be supported by
33        the current app and runtime environment.
34        """
35        # TODO: check AppExperience.
36        return cls._supports_intent(intent)
37
38    @classmethod
39    def _supports_intent(cls, intent: AppIntent) -> bool:
40        """Return whether our mode can handle the provided intent.
41
42        AppModes should override this to define what they can handle.
43        Note that AppExperience does not have to be considered here; that
44        is handled automatically by the can_handle_intent() call."""
45        raise NotImplementedError('AppMode subclasses must override this.')
46
47    def handle_intent(self, intent: AppIntent) -> None:
48        """Handle an intent."""
49        raise NotImplementedError('AppMode subclasses must override this.')
50
51    def on_activate(self) -> None:
52        """Called when the mode is being activated."""
53
54    def on_deactivate(self) -> None:
55        """Called when the mode is being deactivated."""
56
57    def on_app_active_changed(self) -> None:
58        """Called when babase.app.active changes.
59
60        The app-mode may want to take action such as pausing a running
61        game in such cases.
62        """

A high level mode for the app.

Category: App Classes

@classmethod
def get_app_experience(cls) -> bacommon.app.AppExperience:
21    @classmethod
22    def get_app_experience(cls) -> AppExperience:
23        """Return the overall experience provided by this mode."""
24        raise NotImplementedError('AppMode subclasses must override this.')

Return the overall experience provided by this mode.

@classmethod
def can_handle_intent(cls, intent: AppIntent) -> bool:
26    @classmethod
27    def can_handle_intent(cls, intent: AppIntent) -> bool:
28        """Return whether this mode can handle the provided intent.
29
30        For this to return True, the AppMode must claim to support the
31        provided intent (via its _supports_intent() method) AND the
32        AppExperience associated with the AppMode must be supported by
33        the current app and runtime environment.
34        """
35        # TODO: check AppExperience.
36        return cls._supports_intent(intent)

Return whether this mode can handle the provided intent.

For this to return True, the AppMode must claim to support the provided intent (via its _supports_intent() method) AND the AppExperience associated with the AppMode must be supported by the current app and runtime environment.

def handle_intent(self, intent: AppIntent) -> None:
47    def handle_intent(self, intent: AppIntent) -> None:
48        """Handle an intent."""
49        raise NotImplementedError('AppMode subclasses must override this.')

Handle an intent.

def on_activate(self) -> None:
51    def on_activate(self) -> None:
52        """Called when the mode is being activated."""

Called when the mode is being activated.

def on_deactivate(self) -> None:
54    def on_deactivate(self) -> None:
55        """Called when the mode is being deactivated."""

Called when the mode is being deactivated.

def on_app_active_changed(self) -> None:
57    def on_app_active_changed(self) -> None:
58        """Called when babase.app.active changes.
59
60        The app-mode may want to take action such as pausing a running
61        game in such cases.
62        """

Called when babase.app.active changes.

The app-mode may want to take action such as pausing a running game in such cases.

class AppModeSelector:
14class AppModeSelector:
15    """Defines which AppModes are available or used to handle given AppIntents.
16
17    Category: **App Classes**
18
19    The app calls an instance of this class when passed an AppIntent to
20    determine which AppMode to use to handle the intent. Plugins or
21    spinoff projects can modify high level app behavior by replacing or
22    modifying the app's mode-selector.
23    """
24
25    def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode] | None:
26        """Given an AppIntent, return the AppMode that should handle it.
27
28        If None is returned, the AppIntent will be ignored.
29
30        This may be called in a background thread, so avoid any calls
31        limited to logic thread use/etc.
32        """
33        raise NotImplementedError()
34
35    def testable_app_modes(self) -> list[type[AppMode]]:
36        """Return a list of modes to appear in the dev-console app-mode ui.
37
38        The user can switch between these app modes for testing. App-modes
39        will be passed an AppIntentDefault when selected by the user.
40
41        Note that in normal circumstances AppModes should never be
42        selected explicitly by the user but rather determined implicitly
43        based on AppIntents.
44        """
45        raise NotImplementedError()

Defines which AppModes are available or used to handle given AppIntents.

Category: App Classes

The app calls an instance of this class when passed an AppIntent to determine which AppMode to use to handle the intent. Plugins or spinoff projects can modify high level app behavior by replacing or modifying the app's mode-selector.

def app_mode_for_intent( self, intent: AppIntent) -> type[AppMode] | None:
25    def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode] | None:
26        """Given an AppIntent, return the AppMode that should handle it.
27
28        If None is returned, the AppIntent will be ignored.
29
30        This may be called in a background thread, so avoid any calls
31        limited to logic thread use/etc.
32        """
33        raise NotImplementedError()

Given an AppIntent, return the AppMode that should handle it.

If None is returned, the AppIntent will be ignored.

This may be called in a background thread, so avoid any calls limited to logic thread use/etc.

def testable_app_modes(self) -> list[type[AppMode]]:
35    def testable_app_modes(self) -> list[type[AppMode]]:
36        """Return a list of modes to appear in the dev-console app-mode ui.
37
38        The user can switch between these app modes for testing. App-modes
39        will be passed an AppIntentDefault when selected by the user.
40
41        Note that in normal circumstances AppModes should never be
42        selected explicitly by the user but rather determined implicitly
43        based on AppIntents.
44        """
45        raise NotImplementedError()

Return a list of modes to appear in the dev-console app-mode ui.

The user can switch between these app modes for testing. App-modes will be passed an AppIntentDefault when selected by the user.

Note that in normal circumstances AppModes should never be selected explicitly by the user but rather determined implicitly based on AppIntents.

class AppSubsystem:
15class AppSubsystem:
16    """Base class for an app subsystem.
17
18    Category: **App Classes**
19
20    An app 'subsystem' is a bit of a vague term, as pieces of the app
21    can technically be any class and are not required to use this, but
22    building one out of this base class provides conveniences such as
23    predefined callbacks during app state changes.
24
25    Subsystems must be registered with the app before it completes its
26    transition to the 'running' state.
27    """
28
29    def __init__(self) -> None:
30        _babase.app.register_subsystem(self)
31
32    def on_app_loading(self) -> None:
33        """Called when the app reaches the loading state.
34
35        Note that subsystems created after the app switches to the
36        loading state will not receive this callback. Subsystems created
37        by plugins are an example of this.
38        """
39
40    def on_app_running(self) -> None:
41        """Called when the app reaches the running state."""
42
43    def on_app_suspend(self) -> None:
44        """Called when the app enters the suspended state."""
45
46    def on_app_unsuspend(self) -> None:
47        """Called when the app exits the suspended state."""
48
49    def on_app_shutdown(self) -> None:
50        """Called when the app begins shutting down."""
51
52    def on_app_shutdown_complete(self) -> None:
53        """Called when the app completes shutting down."""
54
55    def do_apply_app_config(self) -> None:
56        """Called when the app config should be applied."""
57
58    def reset(self) -> None:
59        """Reset the subsystem to a default state.
60
61        This is called when switching app modes, but may be called
62        at other times too.
63        """

Base class for an app subsystem.

Category: App Classes

An app 'subsystem' is a bit of a vague term, as pieces of the app can technically be any class and are not required to use this, but building one out of this base class provides conveniences such as predefined callbacks during app state changes.

Subsystems must be registered with the app before it completes its transition to the 'running' state.

def on_app_loading(self) -> None:
32    def on_app_loading(self) -> None:
33        """Called when the app reaches the loading state.
34
35        Note that subsystems created after the app switches to the
36        loading state will not receive this callback. Subsystems created
37        by plugins are an example of this.
38        """

Called when the app reaches the loading state.

Note that subsystems created after the app switches to the loading state will not receive this callback. Subsystems created by plugins are an example of this.

def on_app_running(self) -> None:
40    def on_app_running(self) -> None:
41        """Called when the app reaches the running state."""

Called when the app reaches the running state.

def on_app_suspend(self) -> None:
43    def on_app_suspend(self) -> None:
44        """Called when the app enters the suspended state."""

Called when the app enters the suspended state.

def on_app_unsuspend(self) -> None:
46    def on_app_unsuspend(self) -> None:
47        """Called when the app exits the suspended state."""

Called when the app exits the suspended state.

def on_app_shutdown(self) -> None:
49    def on_app_shutdown(self) -> None:
50        """Called when the app begins shutting down."""

Called when the app begins shutting down.

def on_app_shutdown_complete(self) -> None:
52    def on_app_shutdown_complete(self) -> None:
53        """Called when the app completes shutting down."""

Called when the app completes shutting down.

def do_apply_app_config(self) -> None:
55    def do_apply_app_config(self) -> None:
56        """Called when the app config should be applied."""

Called when the app config should be applied.

def reset(self) -> None:
58    def reset(self) -> None:
59        """Reset the subsystem to a default state.
60
61        This is called when switching app modes, but may be called
62        at other times too.
63        """

Reset the subsystem to a default state.

This is called when switching app modes, but may be called at other times too.

def apptime() -> AppTime:
552def apptime() -> babase.AppTime:
553    """Return the current app-time in seconds.
554
555    Category: **General Utility Functions**
556
557    App-time is a monotonic time value; it starts at 0.0 when the app
558    launches and will never jump by large amounts or go backwards, even if
559    the system time changes. Its progression will pause when the app is in
560    a suspended state.
561
562    Note that the AppTime returned here is simply float; it just has a
563    unique type in the type-checker's eyes to help prevent it from being
564    accidentally used with time functionality expecting other time types.
565    """
566    import babase  # pylint: disable=cyclic-import
567
568    return babase.AppTime(0.0)

Return the current app-time in seconds.

Category: General Utility Functions

App-time is a monotonic time value; it starts at 0.0 when the app launches and will never jump by large amounts or go backwards, even if the system time changes. Its progression will pause when the app is in a suspended state.

Note that the AppTime returned here is simply float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

AppTime = AppTime
def apptimer(time: float, call: Callable[[], Any]) -> None:
571def apptimer(time: float, call: Callable[[], Any]) -> None:
572    """Schedule a callable object to run based on app-time.
573
574    Category: **General Utility Functions**
575
576    This function creates a one-off timer which cannot be canceled or
577    modified once created. If you require the ability to do so, or need
578    a repeating timer, use the babase.AppTimer class instead.
579
580    ##### Arguments
581    ###### time (float)
582    > Length of time in seconds that the timer will wait before firing.
583
584    ###### call (Callable[[], Any])
585    > A callable Python object. Note that the timer will retain a
586    strong reference to the callable for as long as the timer exists, so you
587    may want to look into concepts such as babase.WeakCall if that is not
588    desired.
589
590    ##### Examples
591    Print some stuff through time:
592    >>> babase.screenmessage('hello from now!')
593    >>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
594                              'hello from the future!'))
595    >>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
596    ...                       'hello from the future 2!'))
597    """
598    return None

Schedule a callable object to run based on app-time.

Category: General Utility Functions

This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the AppTimer class instead.

Arguments
time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as WeakCall if that is not desired.

Examples

Print some stuff through time:

>>> screenmessage('hello from now!')
>>> apptimer(1.0, Call(screenmessage,
                          'hello from the future!'))
>>> apptimer(2.0, Call(screenmessage,
...                       'hello from the future 2!'))
class AppTimer:
53class AppTimer:
54    """Timers are used to run code at later points in time.
55
56    Category: **General Utility Classes**
57
58    This class encapsulates a timer based on app-time.
59    The underlying timer will be destroyed when this object is no longer
60    referenced. If you do not want to worry about keeping a reference to
61    your timer around, use the babase.apptimer() function instead to get a
62    one-off timer.
63
64    ##### Arguments
65    ###### time
66    > Length of time in seconds that the timer will wait before firing.
67
68    ###### call
69    > A callable Python object. Remember that the timer will retain a
70    strong reference to the callable for as long as it exists, so you
71    may want to look into concepts such as babase.WeakCall if that is not
72    desired.
73
74    ###### repeat
75    > If True, the timer will fire repeatedly, with each successive
76    firing having the same delay as the first.
77
78    ##### Example
79
80    Use a Timer object to print repeatedly for a few seconds:
81    ... def say_it():
82    ...     babase.screenmessage('BADGER!')
83    ... def stop_saying_it():
84    ...     global g_timer
85    ...     g_timer = None
86    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
87    ... # Create our timer; it will run as long as we have the self.t ref.
88    ... g_timer = babase.AppTimer(0.3, say_it, repeat=True)
89    ... # Now fire off a one-shot timer to kill it.
90    ... babase.apptimer(3.89, stop_saying_it)
91    """
92
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass

Timers are used to run code at later points in time.

Category: General Utility Classes

This class encapsulates a timer based on app-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the apptimer() function instead to get a one-off timer.

Arguments
time

Length of time in seconds that the timer will wait before firing.

call

A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as WeakCall if that is not desired.

repeat

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = AppTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... apptimer(3.89, stop_saying_it)

AppTimer(time: float, call: Callable[[], Any], repeat: bool = False)
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass
Call = <class 'babase._general._Call'>
def charstr(char_id: SpecialChar) -> str:
621def charstr(char_id: babase.SpecialChar) -> str:
622    """Get a unicode string representing a special character.
623
624    Category: **General Utility Functions**
625
626    Note that these utilize the private-use block of unicode characters
627    (U+E000-U+F8FF) and are specific to the game; exporting or rendering
628    them elsewhere will be meaningless.
629
630    See babase.SpecialChar for the list of available characters.
631    """
632    return str()

Get a unicode string representing a special character.

Category: General Utility Functions

Note that these utilize the private-use block of unicode characters (U+E000-U+F8FF) and are specific to the game; exporting or rendering them elsewhere will be meaningless.

See SpecialChar for the list of available characters.

def clipboard_get_text() -> str:
635def clipboard_get_text() -> str:
636    """Return text currently on the system clipboard.
637
638    Category: **General Utility Functions**
639
640    Ensure that babase.clipboard_has_text() returns True before calling
641     this function.
642    """
643    return str()

Return text currently on the system clipboard.

Category: General Utility Functions

Ensure that clipboard_has_text() returns True before calling this function.

def clipboard_has_text() -> bool:
646def clipboard_has_text() -> bool:
647    """Return whether there is currently text on the clipboard.
648
649    Category: **General Utility Functions**
650
651    This will return False if no system clipboard is available; no need
652     to call babase.clipboard_is_supported() separately.
653    """
654    return bool()

Return whether there is currently text on the clipboard.

Category: General Utility Functions

This will return False if no system clipboard is available; no need to call clipboard_is_supported() separately.

def clipboard_is_supported() -> bool:
657def clipboard_is_supported() -> bool:
658    """Return whether this platform supports clipboard operations at all.
659
660    Category: **General Utility Functions**
661
662    If this returns False, UIs should not show 'copy to clipboard'
663    buttons, etc.
664    """
665    return bool()

Return whether this platform supports clipboard operations at all.

Category: General Utility Functions

If this returns False, UIs should not show 'copy to clipboard' buttons, etc.

def clipboard_set_text(value: str) -> None:
668def clipboard_set_text(value: str) -> None:
669    """Copy a string to the system clipboard.
670
671    Category: **General Utility Functions**
672
673    Ensure that babase.clipboard_is_supported() returns True before adding
674     buttons/etc. that make use of this functionality.
675    """
676    return None

Copy a string to the system clipboard.

Category: General Utility Functions

Ensure that clipboard_is_supported() returns True before adding buttons/etc. that make use of this functionality.

class ContextCall:
 99class ContextCall:
100    """A context-preserving callable.
101
102    Category: **General Utility Classes**
103
104    A ContextCall wraps a callable object along with a reference
105    to the current context (see babase.ContextRef); it handles restoring
106    the context when run and automatically clears itself if the context
107    it belongs to dies.
108
109    Generally you should not need to use this directly; all standard
110    Ballistica callbacks involved with timers, materials, UI functions,
111    etc. handle this under-the-hood so you don't have to worry about it.
112    The only time it may be necessary is if you are implementing your
113    own callbacks, such as a worker thread that does some action and then
114    runs some game code when done. By wrapping said callback in one of
115    these, you can ensure that you will not inadvertently be keeping the
116    current activity alive or running code in a torn-down (expired)
117    context_ref.
118
119    You can also use babase.WeakCall for similar functionality, but
120    ContextCall has the added bonus that it will not run during context_ref
121    shutdown, whereas babase.WeakCall simply looks at whether the target
122    object instance still exists.
123
124    ##### Examples
125    **Example A:** code like this can inadvertently prevent our activity
126    (self) from ending until the operation completes, since the bound
127    method we're passing (self.dosomething) contains a strong-reference
128    to self).
129    >>> start_some_long_action(callback_when_done=self.dosomething)
130
131    **Example B:** in this case our activity (self) can still die
132    properly; the callback will clear itself when the activity starts
133    shutting down, becoming a harmless no-op and releasing the reference
134    to our activity.
135
136    >>> start_long_action(
137    ...     callback_when_done=babase.ContextCall(self.mycallback))
138    """
139
140    def __init__(self, call: Callable) -> None:
141        pass
142
143    def __call__(self) -> None:
144        """Support for calling."""
145        pass

A context-preserving callable.

Category: General Utility Classes

A ContextCall wraps a callable object along with a reference to the current context (see ContextRef); it handles restoring the context when run and automatically clears itself if the context it belongs to dies.

Generally you should not need to use this directly; all standard Ballistica callbacks involved with timers, materials, UI functions, etc. handle this under-the-hood so you don't have to worry about it. The only time it may be necessary is if you are implementing your own callbacks, such as a worker thread that does some action and then runs some game code when done. By wrapping said callback in one of these, you can ensure that you will not inadvertently be keeping the current activity alive or running code in a torn-down (expired) context_ref.

You can also use WeakCall for similar functionality, but ContextCall has the added bonus that it will not run during context_ref shutdown, whereas WeakCall simply looks at whether the target object instance still exists.

Examples

Example A: code like this can inadvertently prevent our activity (self) from ending until the operation completes, since the bound method we're passing (self.dosomething) contains a strong-reference to self).

>>> start_some_long_action(callback_when_done=self.dosomething)

Example B: in this case our activity (self) can still die properly; the callback will clear itself when the activity starts shutting down, becoming a harmless no-op and releasing the reference to our activity.

>>> start_long_action(
...     callback_when_done=ContextCall(self.mycallback))
ContextCall(call: Callable)
140    def __init__(self, call: Callable) -> None:
141        pass
class ContextError(builtins.Exception):
16class ContextError(Exception):
17    """Exception raised when a call is made in an invalid context.
18
19    Category: **Exception Classes**
20
21    Examples of this include calling UI functions within an Activity context
22    or calling scene manipulation functions outside of a game context.
23    """

Exception raised when a call is made in an invalid context.

Category: Exception Classes

Examples of this include calling UI functions within an Activity context or calling scene manipulation functions outside of a game context.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
class ContextRef:
148class ContextRef:
149    """Store or use a ballistica context.
150
151    Category: **General Utility Classes**
152
153    Many operations such as bascenev1.newnode() or bascenev1.gettexture()
154    operate implicitly on a current 'context'. A context is some sort of
155    state that functionality can implicitly use. Context determines, for
156    example, which scene nodes or textures get added to without having to
157    specify it explicitly in the newnode()/gettexture() call. Contexts can
158    also affect object lifecycles; for example a babase.ContextCall will
159    become a no-op when the context it was created in is destroyed.
160
161    In general, if you are a modder, you should not need to worry about
162    contexts; mod code should mostly be getting run in the correct
163    context and timers and other callbacks will take care of saving
164    and restoring contexts automatically. There may be rare cases,
165    however, where you need to deal directly with contexts, and that is
166    where this class comes in.
167
168    Creating a babase.ContextRef() will capture a reference to the current
169    context. Other modules may provide ways to access their contexts; for
170    example a bascenev1.Activity instance has a 'context' attribute. You
171    can also use babase.ContextRef.empty() to create a reference to *no*
172    context. Some code such as UI calls may expect this and may complain
173    if you try to use them within a context.
174
175    ##### Usage
176    ContextRefs are generally used with the Python 'with' statement, which
177    sets the context they point to as current on entry and resets it to
178    the previous value on exit.
179
180    ##### Example
181    Explicitly create a few UI bits with no context set.
182    (UI stuff may complain if called within a context):
183    >>> with bui.ContextRef.empty():
184    ...     my_container = bui.containerwidget()
185    """
186
187    def __init__(
188        self,
189    ) -> None:
190        pass
191
192    def __enter__(self) -> None:
193        """Support for "with" statement."""
194        pass
195
196    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
197        """Support for "with" statement."""
198        pass
199
200    @classmethod
201    def empty(cls) -> ContextRef:
202        """Return a ContextRef pointing to no context.
203
204        This is useful when code should be run free of a context.
205        For example, UI code generally insists on being run this way.
206        Otherwise, callbacks set on the UI could inadvertently stop working
207        due to a game activity ending, which would be unintuitive behavior.
208        """
209        return ContextRef()
210
211    def is_empty(self) -> bool:
212        """Whether the context was created as empty."""
213        return bool()
214
215    def is_expired(self) -> bool:
216        """Whether the context has expired."""
217        return bool()

Store or use a ballistica context.

Category: General Utility Classes

Many operations such as bascenev1.newnode() or bascenev1.gettexture() operate implicitly on a current 'context'. A context is some sort of state that functionality can implicitly use. Context determines, for example, which scene nodes or textures get added to without having to specify it explicitly in the newnode()/gettexture() call. Contexts can also affect object lifecycles; for example a ContextCall will become a no-op when the context it was created in is destroyed.

In general, if you are a modder, you should not need to worry about contexts; mod code should mostly be getting run in the correct context and timers and other callbacks will take care of saving and restoring contexts automatically. There may be rare cases, however, where you need to deal directly with contexts, and that is where this class comes in.

Creating a ContextRef() will capture a reference to the current context. Other modules may provide ways to access their contexts; for example a bascenev1.Activity instance has a 'context' attribute. You can also use ContextRef.empty() to create a reference to no context. Some code such as UI calls may expect this and may complain if you try to use them within a context.

Usage

ContextRefs are generally used with the Python 'with' statement, which sets the context they point to as current on entry and resets it to the previous value on exit.

Example

Explicitly create a few UI bits with no context set. (UI stuff may complain if called within a context):

>>> with bui.ContextRef.empty():
...     my_container = bui.containerwidget()
@classmethod
def empty(cls) -> _babase.ContextRef:
200    @classmethod
201    def empty(cls) -> ContextRef:
202        """Return a ContextRef pointing to no context.
203
204        This is useful when code should be run free of a context.
205        For example, UI code generally insists on being run this way.
206        Otherwise, callbacks set on the UI could inadvertently stop working
207        due to a game activity ending, which would be unintuitive behavior.
208        """
209        return ContextRef()

Return a ContextRef pointing to no context.

This is useful when code should be run free of a context. For example, UI code generally insists on being run this way. Otherwise, callbacks set on the UI could inadvertently stop working due to a game activity ending, which would be unintuitive behavior.

def is_empty(self) -> bool:
211    def is_empty(self) -> bool:
212        """Whether the context was created as empty."""
213        return bool()

Whether the context was created as empty.

def is_expired(self) -> bool:
215    def is_expired(self) -> bool:
216        """Whether the context has expired."""
217        return bool()

Whether the context has expired.

class DelegateNotFoundError(babase.NotFoundError):
61class DelegateNotFoundError(NotFoundError):
62    """Exception raised when an expected delegate object does not exist.
63
64    Category: **Exception Classes**
65    """

Exception raised when an expected delegate object does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
class DevConsoleTab:
20class DevConsoleTab:
21    """Defines behavior for a tab in the dev-console."""
22
23    def refresh(self) -> None:
24        """Called when the tab should refresh itself."""
25
26    def request_refresh(self) -> None:
27        """The tab can call this to request that it be refreshed."""
28        _babase.dev_console_request_refresh()
29
30    def button(
31        self,
32        label: str,
33        pos: tuple[float, float],
34        size: tuple[float, float],
35        call: Callable[[], Any] | None = None,
36        h_anchor: Literal['left', 'center', 'right'] = 'center',
37        label_scale: float = 1.0,
38        corner_radius: float = 8.0,
39        style: Literal['normal', 'dark'] = 'normal',
40    ) -> None:
41        """Add a button to the tab being refreshed."""
42        assert _babase.app.devconsole.is_refreshing
43        _babase.dev_console_add_button(
44            label,
45            pos[0],
46            pos[1],
47            size[0],
48            size[1],
49            call,
50            h_anchor,
51            label_scale,
52            corner_radius,
53            style,
54        )
55
56    def text(
57        self,
58        text: str,
59        pos: tuple[float, float],
60        h_anchor: Literal['left', 'center', 'right'] = 'center',
61        h_align: Literal['left', 'center', 'right'] = 'center',
62        v_align: Literal['top', 'center', 'bottom', 'none'] = 'center',
63        scale: float = 1.0,
64    ) -> None:
65        """Add a button to the tab being refreshed."""
66        assert _babase.app.devconsole.is_refreshing
67        _babase.dev_console_add_text(
68            text, pos[0], pos[1], h_anchor, h_align, v_align, scale
69        )
70
71    def python_terminal(self) -> None:
72        """Add a Python Terminal to the tab being refreshed."""
73        assert _babase.app.devconsole.is_refreshing
74        _babase.dev_console_add_python_terminal()
75
76    @property
77    def width(self) -> float:
78        """Return the current tab width. Only call during refreshes."""
79        assert _babase.app.devconsole.is_refreshing
80        return _babase.dev_console_tab_width()
81
82    @property
83    def height(self) -> float:
84        """Return the current tab height. Only call during refreshes."""
85        assert _babase.app.devconsole.is_refreshing
86        return _babase.dev_console_tab_height()
87
88    @property
89    def base_scale(self) -> float:
90        """A scale value set depending on the app's UI scale.
91
92        Dev-console tabs can incorporate this into their UI sizes and
93        positions if they desire. This must be done manually however.
94        """
95        assert _babase.app.devconsole.is_refreshing
96        return _babase.dev_console_base_scale()

Defines behavior for a tab in the dev-console.

def refresh(self) -> None:
23    def refresh(self) -> None:
24        """Called when the tab should refresh itself."""

Called when the tab should refresh itself.

def request_refresh(self) -> None:
26    def request_refresh(self) -> None:
27        """The tab can call this to request that it be refreshed."""
28        _babase.dev_console_request_refresh()

The tab can call this to request that it be refreshed.

def button( self, label: str, pos: tuple[float, float], size: tuple[float, float], call: Optional[Callable[[], Any]] = None, h_anchor: Literal['left', 'center', 'right'] = 'center', label_scale: float = 1.0, corner_radius: float = 8.0, style: Literal['normal', 'dark'] = 'normal') -> None:
30    def button(
31        self,
32        label: str,
33        pos: tuple[float, float],
34        size: tuple[float, float],
35        call: Callable[[], Any] | None = None,
36        h_anchor: Literal['left', 'center', 'right'] = 'center',
37        label_scale: float = 1.0,
38        corner_radius: float = 8.0,
39        style: Literal['normal', 'dark'] = 'normal',
40    ) -> None:
41        """Add a button to the tab being refreshed."""
42        assert _babase.app.devconsole.is_refreshing
43        _babase.dev_console_add_button(
44            label,
45            pos[0],
46            pos[1],
47            size[0],
48            size[1],
49            call,
50            h_anchor,
51            label_scale,
52            corner_radius,
53            style,
54        )

Add a button to the tab being refreshed.

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:
56    def text(
57        self,
58        text: str,
59        pos: tuple[float, float],
60        h_anchor: Literal['left', 'center', 'right'] = 'center',
61        h_align: Literal['left', 'center', 'right'] = 'center',
62        v_align: Literal['top', 'center', 'bottom', 'none'] = 'center',
63        scale: float = 1.0,
64    ) -> None:
65        """Add a button to the tab being refreshed."""
66        assert _babase.app.devconsole.is_refreshing
67        _babase.dev_console_add_text(
68            text, pos[0], pos[1], h_anchor, h_align, v_align, scale
69        )

Add a button to the tab being refreshed.

def python_terminal(self) -> None:
71    def python_terminal(self) -> None:
72        """Add a Python Terminal to the tab being refreshed."""
73        assert _babase.app.devconsole.is_refreshing
74        _babase.dev_console_add_python_terminal()

Add a Python Terminal to the tab being refreshed.

width: float
76    @property
77    def width(self) -> float:
78        """Return the current tab width. Only call during refreshes."""
79        assert _babase.app.devconsole.is_refreshing
80        return _babase.dev_console_tab_width()

Return the current tab width. Only call during refreshes.

height: float
82    @property
83    def height(self) -> float:
84        """Return the current tab height. Only call during refreshes."""
85        assert _babase.app.devconsole.is_refreshing
86        return _babase.dev_console_tab_height()

Return the current tab height. Only call during refreshes.

base_scale: float
88    @property
89    def base_scale(self) -> float:
90        """A scale value set depending on the app's UI scale.
91
92        Dev-console tabs can incorporate this into their UI sizes and
93        positions if they desire. This must be done manually however.
94        """
95        assert _babase.app.devconsole.is_refreshing
96        return _babase.dev_console_base_scale()

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.

@dataclass
class DevConsoleTabEntry:
236@dataclass
237class DevConsoleTabEntry:
238    """Represents a distinct tab in the dev-console."""
239
240    name: str
241    factory: Callable[[], DevConsoleTab]

Represents a distinct tab in the dev-console.

DevConsoleTabEntry(name: str, factory: Callable[[], DevConsoleTab])
name: str
factory: Callable[[], DevConsoleTab]
class DevConsoleSubsystem:
244class DevConsoleSubsystem:
245    """Subsystem for wrangling the dev console.
246
247    The single instance of this class can be found at
248    babase.app.devconsole. The dev-console is a simple always-available
249    UI intended for use by developers; not end users. Traditionally it
250    is available by typing a backtick (`) key on a keyboard, but now can
251    be accessed via an on-screen button (see settings/advanced to enable
252    said button).
253    """
254
255    def __init__(self) -> None:
256        # All tabs in the dev-console. Add your own stuff here via
257        # plugins or whatnot.
258        self.tabs: list[DevConsoleTabEntry] = [
259            DevConsoleTabEntry('Python', DevConsoleTabPython),
260            DevConsoleTabEntry('AppModes', DevConsoleTabAppModes),
261            DevConsoleTabEntry('UI', DevConsoleTabUI),
262        ]
263        if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1':
264            self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest))
265        self.is_refreshing = False
266
267    def do_refresh_tab(self, tabname: str) -> None:
268        """Called by the C++ layer when a tab should be filled out."""
269        assert _babase.in_logic_thread()
270
271        # FIXME: We currently won't handle multiple tabs with the same
272        # name. We should give a clean error or something in that case.
273        tab: DevConsoleTab | None = None
274        for tabentry in self.tabs:
275            if tabentry.name == tabname:
276                tab = tabentry.factory()
277                break
278
279        if tab is None:
280            logging.error(
281                'DevConsole got refresh request for tab'
282                " '%s' which does not exist.",
283                tabname,
284            )
285            return
286
287        self.is_refreshing = True
288        try:
289            tab.refresh()
290        finally:
291            self.is_refreshing = False

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

tabs: list[DevConsoleTabEntry]
is_refreshing
def do_refresh_tab(self, tabname: str) -> None:
267    def do_refresh_tab(self, tabname: str) -> None:
268        """Called by the C++ layer when a tab should be filled out."""
269        assert _babase.in_logic_thread()
270
271        # FIXME: We currently won't handle multiple tabs with the same
272        # name. We should give a clean error or something in that case.
273        tab: DevConsoleTab | None = None
274        for tabentry in self.tabs:
275            if tabentry.name == tabname:
276                tab = tabentry.factory()
277                break
278
279        if tab is None:
280            logging.error(
281                'DevConsole got refresh request for tab'
282                " '%s' which does not exist.",
283                tabname,
284            )
285            return
286
287        self.is_refreshing = True
288        try:
289            tab.refresh()
290        finally:
291            self.is_refreshing = False

Called by the C++ layer when a tab should be filled out.

DisplayTime = DisplayTime
def displaytime() -> DisplayTime:
761def displaytime() -> babase.DisplayTime:
762    """Return the current display-time in seconds.
763
764    Category: **General Utility Functions**
765
766    Display-time is a time value intended to be used for animation and other
767    visual purposes. It will generally increment by a consistent amount each
768    frame. It will pass at an overall similar rate to AppTime, but trades
769    accuracy for smoothness.
770
771    Note that the value returned here is simply a float; it just has a
772    unique type in the type-checker's eyes to help prevent it from being
773    accidentally used with time functionality expecting other time types.
774    """
775    import babase  # pylint: disable=cyclic-import
776
777    return babase.DisplayTime(0.0)

Return the current display-time in seconds.

Category: General Utility Functions

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

def displaytimer(time: float, call: Callable[[], Any]) -> None:
780def displaytimer(time: float, call: Callable[[], Any]) -> None:
781    """Schedule a callable object to run based on display-time.
782
783    Category: **General Utility Functions**
784
785    This function creates a one-off timer which cannot be canceled or
786    modified once created. If you require the ability to do so, or need
787    a repeating timer, use the babase.DisplayTimer class instead.
788
789    Display-time is a time value intended to be used for animation and other
790    visual purposes. It will generally increment by a consistent amount each
791    frame. It will pass at an overall similar rate to AppTime, but trades
792    accuracy for smoothness.
793
794    ##### Arguments
795    ###### time (float)
796    > Length of time in seconds that the timer will wait before firing.
797
798    ###### call (Callable[[], Any])
799    > A callable Python object. Note that the timer will retain a
800    strong reference to the callable for as long as the timer exists, so you
801    may want to look into concepts such as babase.WeakCall if that is not
802    desired.
803
804    ##### Examples
805    Print some stuff through time:
806    >>> babase.screenmessage('hello from now!')
807    >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
808    ...                       'hello from the future!'))
809    >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
810    ...                       'hello from the future 2!'))
811    """
812    return None

Schedule a callable object to run based on display-time.

Category: General Utility Functions

This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the DisplayTimer class instead.

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Arguments
time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as WeakCall if that is not desired.

Examples

Print some stuff through time:

>>> screenmessage('hello from now!')
>>> displaytimer(1.0, Call(screenmessage,
...                       'hello from the future!'))
>>> displaytimer(2.0, Call(screenmessage,
...                       'hello from the future 2!'))
class DisplayTimer:
220class DisplayTimer:
221    """Timers are used to run code at later points in time.
222
223    Category: **General Utility Classes**
224
225    This class encapsulates a timer based on display-time.
226    The underlying timer will be destroyed when this object is no longer
227    referenced. If you do not want to worry about keeping a reference to
228    your timer around, use the babase.displaytimer() function instead to get a
229    one-off timer.
230
231    Display-time is a time value intended to be used for animation and
232    other visual purposes. It will generally increment by a consistent
233    amount each frame. It will pass at an overall similar rate to AppTime,
234    but trades accuracy for smoothness.
235
236    ##### Arguments
237    ###### time
238    > Length of time in seconds that the timer will wait before firing.
239
240    ###### call
241    > A callable Python object. Remember that the timer will retain a
242    strong reference to the callable for as long as it exists, so you
243    may want to look into concepts such as babase.WeakCall if that is not
244    desired.
245
246    ###### repeat
247    > If True, the timer will fire repeatedly, with each successive
248    firing having the same delay as the first.
249
250    ##### Example
251
252    Use a Timer object to print repeatedly for a few seconds:
253    ... def say_it():
254    ...     babase.screenmessage('BADGER!')
255    ... def stop_saying_it():
256    ...     global g_timer
257    ...     g_timer = None
258    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
259    ... # Create our timer; it will run as long as we have the self.t ref.
260    ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True)
261    ... # Now fire off a one-shot timer to kill it.
262    ... babase.displaytimer(3.89, stop_saying_it)
263    """
264
265    def __init__(
266        self, time: float, call: Callable[[], Any], repeat: bool = False
267    ) -> None:
268        pass

Timers are used to run code at later points in time.

Category: General Utility Classes

This class encapsulates a timer based on display-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the displaytimer() function instead to get a one-off timer.

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Arguments
time

Length of time in seconds that the timer will wait before firing.

call

A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as WeakCall if that is not desired.

repeat

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = DisplayTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... displaytimer(3.89, stop_saying_it)

DisplayTimer(time: float, call: Callable[[], Any], repeat: bool = False)
265    def __init__(
266        self, time: float, call: Callable[[], Any], repeat: bool = False
267    ) -> None:
268        pass
def do_once() -> bool:
820def do_once() -> bool:
821    """Return whether this is the first time running a line of code.
822
823    Category: **General Utility Functions**
824
825    This is used by 'print_once()' type calls to keep from overflowing
826    logs. The call functions by registering the filename and line where
827    The call is made from.  Returns True if this location has not been
828    registered already, and False if it has.
829
830    ##### Example
831    This print will only fire for the first loop iteration:
832    >>> for i in range(10):
833    ... if babase.do_once():
834    ...     print('HelloWorld once from loop!')
835    """
836    return bool()

Return whether this is the first time running a line of code.

Category: General Utility Functions

This is used by 'print_once()' type calls to keep from overflowing logs. The call functions by registering the filename and line where The call is made from. Returns True if this location has not been registered already, and False if it has.

Example

This print will only fire for the first loop iteration:

>>> for i in range(10):
... if do_once():
...     print('HelloWorld once from loop!')
class EmptyAppMode(babase.AppMode):
19class EmptyAppMode(AppMode):
20    """An AppMode that does not do much at all."""
21
22    @override
23    @classmethod
24    def get_app_experience(cls) -> AppExperience:
25        return AppExperience.EMPTY
26
27    @override
28    @classmethod
29    def _supports_intent(cls, intent: AppIntent) -> bool:
30        # We support default and exec intents currently.
31        return isinstance(intent, AppIntentExec | AppIntentDefault)
32
33    @override
34    def handle_intent(self, intent: AppIntent) -> None:
35        if isinstance(intent, AppIntentExec):
36            _babase.empty_app_mode_handle_app_intent_exec(intent.code)
37            return
38        assert isinstance(intent, AppIntentDefault)
39        _babase.empty_app_mode_handle_app_intent_default()
40
41    @override
42    def on_activate(self) -> None:
43        # Let the native layer do its thing.
44        _babase.empty_app_mode_activate()
45
46    @override
47    def on_deactivate(self) -> None:
48        # Let the native layer do its thing.
49        _babase.empty_app_mode_deactivate()

An AppMode that does not do much at all.

@override
@classmethod
def get_app_experience(cls) -> bacommon.app.AppExperience:
22    @override
23    @classmethod
24    def get_app_experience(cls) -> AppExperience:
25        return AppExperience.EMPTY

Return the overall experience provided by this mode.

@override
def handle_intent(self, intent: AppIntent) -> None:
33    @override
34    def handle_intent(self, intent: AppIntent) -> None:
35        if isinstance(intent, AppIntentExec):
36            _babase.empty_app_mode_handle_app_intent_exec(intent.code)
37            return
38        assert isinstance(intent, AppIntentDefault)
39        _babase.empty_app_mode_handle_app_intent_default()

Handle an intent.

@override
def on_activate(self) -> None:
41    @override
42    def on_activate(self) -> None:
43        # Let the native layer do its thing.
44        _babase.empty_app_mode_activate()

Called when the mode is being activated.

@override
def on_deactivate(self) -> None:
46    @override
47    def on_deactivate(self) -> None:
48        # Let the native layer do its thing.
49        _babase.empty_app_mode_deactivate()

Called when the mode is being deactivated.

class Env:
271class Env:
272    """Unchanging values for the current running app instance.
273    Access the single shared instance of this class at `babase.app.env`.
274    """
275
276    android: bool
277    """Is this build targeting an Android based OS?"""
278
279    api_version: int
280    """The app's api version.
281
282       Only Python modules and packages associated with the current API
283       version number will be detected by the game (see the ba_meta tag).
284       This value will change whenever substantial backward-incompatible
285       changes are introduced to Ballistica APIs. When that happens,
286       modules/packages should be updated accordingly and set to target
287       the newer API version number."""
288
289    arcade: bool
290    """Whether the app is targeting an arcade-centric experience."""
291
292    config_file_path: str
293    """Where the app's config file is stored on disk."""
294
295    data_directory: str
296    """Where bundled static app data lives."""
297
298    debug: bool
299    """Whether the app is running in debug mode.
300
301       Debug builds generally run substantially slower than non-debug
302       builds due to compiler optimizations being disabled and extra
303       checks being run."""
304
305    demo: bool
306    """Whether the app is targeting a demo experience."""
307
308    device_name: str
309    """Human readable name of the device running this app."""
310
311    engine_build_number: int
312    """Integer build number for the engine.
313
314       This value increases by at least 1 with each release of the engine.
315       It is independent of the human readable `version` string."""
316
317    engine_version: str
318    """Human-readable version string for the engine; something like '1.3.24'.
319
320       This should not be interpreted as a number; it may contain
321       string elements such as 'alpha', 'beta', 'test', etc.
322       If a numeric version is needed, use `build_number`."""
323
324    gui: bool
325    """Whether the app is running with a gui.
326
327       This is the opposite of `headless`."""
328
329    headless: bool
330    """Whether the app is running headlessly (without a gui).
331
332       This is the opposite of `gui`."""
333
334    python_directory_app: str | None
335    """Path where the app expects its bundled modules to live.
336
337       Be aware that this value may be None if Ballistica is running in
338       a non-standard environment, and that python-path modifications may
339       cause modules to be loaded from other locations."""
340
341    python_directory_app_site: str | None
342    """Path where the app expects its bundled pip modules to live.
343
344       Be aware that this value may be None if Ballistica is running in
345       a non-standard environment, and that python-path modifications may
346       cause modules to be loaded from other locations."""
347
348    python_directory_user: str | None
349    """Path where the app expects its user scripts (mods) to live.
350
351       Be aware that this value may be None if Ballistica is running in
352       a non-standard environment, and that python-path modifications may
353       cause modules to be loaded from other locations."""
354
355    supports_soft_quit: bool
356    """Whether the running app supports 'soft' quit options.
357
358       This generally applies to mobile derived OSs, where an act of
359       'quitting' may leave the app running in the background waiting
360       in case it is used again."""
361
362    test: bool
363    """Whether the app is running in test mode.
364
365       Test mode enables extra checks and features that are useful for
366       release testing but which do not slow the game down significantly."""
367
368    tv: bool
369    """Whether the app is targeting a TV-centric experience."""
370
371    vr: bool
372    """Whether the app is currently running in VR."""
373
374    pass

Unchanging values for the current running app instance. Access the single shared instance of this class at babase.app.env.

android: bool

Is this build targeting an Android based OS?

api_version: 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.

arcade: bool

Whether the app is targeting an arcade-centric experience.

config_file_path: str

Where the app's config file is stored on disk.

data_directory: str

Where bundled static app data lives.

debug: bool

Whether the app is running in debug mode.

Debug builds generally run substantially slower than non-debug builds due to compiler optimizations being disabled and extra checks being run.

demo: bool

Whether the app is targeting a demo experience.

device_name: str

Human readable name of the device running this app.

engine_build_number: int

Integer build number for the engine.

This value increases by at least 1 with each release of the engine. It is independent of the human readable version string.

engine_version: str

Human-readable version string for the engine; 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.

gui: bool

Whether the app is running with a gui.

This is the opposite of headless.

headless: bool

Whether the app is running headlessly (without a gui).

This is the opposite of gui.

python_directory_app: 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.

python_directory_app_site: 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.

python_directory_user: 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.

supports_soft_quit: bool

Whether the running app supports 'soft' quit options.

This generally applies to mobile derived OSs, where an act of 'quitting' may leave the app running in the background waiting in case it is used again.

test: bool

Whether the app is running 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.

tv: bool

Whether the app is targeting a TV-centric experience.

vr: bool

Whether the app is currently running in VR.

class Existable(typing.Protocol):