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

A class for high level app functionality and state.

Category: App Classes

Use babase.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: Env
state
threadpool
meta
net
workspaces
components
stringedit
devconsole
fg_state
def postinit(self) -> None:
224    def postinit(self) -> None:
225        """Called after we've been inited and assigned to babase.app.
226
227        Anything that accesses babase.app as part of its init process
228        must go here instead of __init__.
229        """
230
231        # Hack for docs-generation: we can be imported with dummy modules
232        # instead of our actual binary ones, but we don't function.
233        if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
234            return
235
236        self.lang = LanguageSubsystem()
237        self.plugins = PluginSubsystem()

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

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

active: bool
239    @property
240    def active(self) -> bool:
241        """Whether the app is currently front and center.
242
243        This will be False when the app is hidden, other activities
244        are covering it, etc. (depending on the platform).
245        """
246        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
248    @property
249    def asyncio_loop(self) -> asyncio.AbstractEventLoop:
250        """The logic thread's asyncio event loop.
251
252        This allow async tasks to be run in the logic thread.
253
254        Generally you should call App.create_async_task() to schedule
255        async code to run instead of using this directly. That will
256        handle retaining the task and logging errors automatically.
257        Only schedule tasks onto asyncio_loop yourself when you intend
258        to hold on to the returned task and await its results. Releasing
259        the task reference can lead to subtle bugs such as unreported
260        errors and garbage-collected tasks disappearing before their
261        work is done.
262
263        Note that, at this time, the asyncio loop is encapsulated
264        and explicitly stepped by the engine's logic thread loop and
265        thus things like asyncio.get_running_loop() will unintuitively
266        *not* return this loop from most places in the logic thread;
267        only from within a task explicitly created in this loop.
268        Hopefully this situation will be improved in the future with a
269        unified event loop.
270        """
271        assert _babase.in_logic_thread()
272        assert self._asyncio_loop is not None
273        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: Union[Generator[Any, Any, ~T], Coroutine[Any, Any, ~T]], *, name: str | None = None) -> None:
275    def create_async_task(
276        self,
277        coro: Generator[Any, Any, T] | Coroutine[Any, Any, T],
278        *,
279        name: str | None = None,
280    ) -> None:
281        """Create a fully managed async task.
282
283        This will automatically retain and release a reference to the task
284        and log any exceptions that occur in it. If you need to await a task
285        or otherwise need more control, schedule a task directly using
286        App.asyncio_loop.
287        """
288        assert _babase.in_logic_thread()
289        # Hold a strong reference to the task until it is done.
290        # Otherwise it is possible for it to be garbage collected and
291        # disappear midway if the caller does not hold on to the
292        # returned task, which seems like a great way to introduce
293        # hard-to-track bugs.
294        task = self.asyncio_loop.create_task(coro, name=name)
295        self._asyncio_tasks.add(task)
296        task.add_done_callback(self._on_task_done)
297        # return task

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
312    @property
313    def config(self) -> babase.AppConfig:
314        """The babase.AppConfig instance representing the app's config state."""
315        assert self._config is not None
316        return self._config

The babase.AppConfig instance representing the app's config state.

mode_selector: AppModeSelector
318    @property
319    def mode_selector(self) -> babase.AppModeSelector:
320        """Controls which app-modes are used for handling given intents.
321
322        Plugins can override this to change high level app behavior and
323        spinoff projects can change the default implementation for the
324        same effect.
325        """
326        if self._mode_selector is None:
327            raise RuntimeError(
328                'mode_selector cannot be used until the app reaches'
329                ' the running state.'
330            )
331        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._subsystem.ClassicSubsystem | None
340    @cached_property
341    def classic(self) -> ClassicSubsystem | None:
342        """Our classic subsystem (if available)."""
343        # pylint: disable=cyclic-import
344
345        try:
346            from baclassic import ClassicSubsystem
347
348            return ClassicSubsystem()
349        except ImportError:
350            return None
351        except Exception:
352            logging.exception('Error importing baclassic.')
353            return None

Our classic subsystem (if available).

plus: baplus._subsystem.PlusSubsystem | None
355    @cached_property
356    def plus(self) -> PlusSubsystem | None:
357        """Our plus subsystem (if available)."""
358        # pylint: disable=cyclic-import
359
360        try:
361            from baplus import PlusSubsystem
362
363            return PlusSubsystem()
364        except ImportError:
365            return None
366        except Exception:
367            logging.exception('Error importing baplus.')
368            return None

Our plus subsystem (if available).

ui_v1: bauiv1._subsystem.UIV1Subsystem
370    @cached_property
371    def ui_v1(self) -> UIV1Subsystem:
372        """Our ui_v1 subsystem (always available)."""
373        # pylint: disable=cyclic-import
374
375        from bauiv1 import UIV1Subsystem
376
377        return UIV1Subsystem()

Our ui_v1 subsystem (always available).

def register_subsystem(self, subsystem: AppSubsystem) -> None:
381    def register_subsystem(self, subsystem: AppSubsystem) -> None:
382        """Called by the AppSubsystem class. Do not use directly."""
383
384        # We only allow registering new subsystems if we've not yet
385        # reached the 'running' state. This ensures that all subsystems
386        # receive a consistent set of callbacks starting with
387        # on_app_running().
388        if self._subsystem_registration_ended:
389            raise RuntimeError(
390                'Subsystems can no longer be registered at this point.'
391            )
392        self._subsystems.append(subsystem)

Called by the AppSubsystem class. Do not use directly.

def add_shutdown_task(self, coro: Coroutine[NoneType, NoneType, NoneType]) -> None:
394    def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
395        """Add a task to be run on app shutdown.
396
397        Note that shutdown tasks will be canceled after
398        App.SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.
399        """
400        if (
401            self.state is self.State.SHUTTING_DOWN
402            or self.state is self.State.SHUTDOWN_COMPLETE
403        ):
404            stname = self.state.name
405            raise RuntimeError(
406                f'Cannot add shutdown tasks with current state {stname}.'
407            )
408        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:
410    def run(self) -> None:
411        """Run the app to completion.
412
413        Note that this only works on builds where Ballistica manages
414        its own event loop.
415        """
416        _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:
418    def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
419        """Submit a call to the app threadpool where result is not needed.
420
421        Normally, doing work in a thread-pool involves creating a future
422        and waiting for its result, which is an important step because it
423        propagates any Exceptions raised by the submitted work. When the
424        result in not important, however, this call can be used. The app
425        will log any exceptions that occur.
426        """
427        fut = self.threadpool.submit(call)
428        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:
430    def set_intent(self, intent: AppIntent) -> None:
431        """Set the intent for the app.
432
433        Intent defines what the app is trying to do at a given time.
434        This call is asynchronous; the intent switch will happen in the
435        logic thread in the near future. If set_intent is called
436        repeatedly before the change takes place, the final intent to be
437        set will be used.
438        """
439
440        # Mark this one as pending. We do this synchronously so that the
441        # last one marked actually takes effect if there is overlap
442        # (doing this in the bg thread could result in race conditions).
443        self._pending_intent = intent
444
445        # Do the actual work of calcing our app-mode/etc. in a bg thread
446        # since it may block for a moment to load modules/etc.
447        self.threadpool_submit_no_wait(tpartial(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:
449    def push_apply_app_config(self) -> None:
450        """Internal. Use app.config.apply() to apply app config changes."""
451        # To be safe, let's run this by itself in the event loop.
452        # This avoids potential trouble if this gets called mid-draw or
453        # something like that.
454        self._pending_apply_app_config = True
455        _babase.pushcall(self._apply_app_config, raw=True)

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

def on_native_start(self) -> None:
457    def on_native_start(self) -> None:
458        """Called by the native layer when the app is being started."""
459        assert _babase.in_logic_thread()
460        assert not self._native_start_called
461        self._native_start_called = True
462        self._update_state()

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

def on_native_bootstrapping_complete(self) -> None:
464    def on_native_bootstrapping_complete(self) -> None:
465        """Called by the native layer once its ready to rock."""
466        assert _babase.in_logic_thread()
467        assert not self._native_bootstrapping_completed
468        self._native_bootstrapping_completed = True
469        self._update_state()

Called by the native layer once its ready to rock.

def on_native_suspend(self) -> None:
471    def on_native_suspend(self) -> None:
472        """Called by the native layer when the app is suspended."""
473        assert _babase.in_logic_thread()
474        assert not self._native_suspended  # Should avoid redundant calls.
475        self._native_suspended = True
476        self._update_state()

Called by the native layer when the app is suspended.

def on_native_unsuspend(self) -> None:
478    def on_native_unsuspend(self) -> None:
479        """Called by the native layer when the app suspension ends."""
480        assert _babase.in_logic_thread()
481        assert self._native_suspended  # Should avoid redundant calls.
482        self._native_suspended = False
483        self._update_state()

Called by the native layer when the app suspension ends.

def on_native_shutdown(self) -> None:
485    def on_native_shutdown(self) -> None:
486        """Called by the native layer when the app starts shutting down."""
487        assert _babase.in_logic_thread()
488        self._native_shutdown_called = True
489        self._update_state()

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

def on_native_shutdown_complete(self) -> None:
491    def on_native_shutdown_complete(self) -> None:
492        """Called by the native layer when the app is done shutting down."""
493        assert _babase.in_logic_thread()
494        self._native_shutdown_complete_called = True
495        self._update_state()

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

def on_native_active_changed(self) -> None:
497    def on_native_active_changed(self) -> None:
498        """Called by the native layer when the app active state changes."""
499        assert _babase.in_logic_thread()
500        if self._mode is not None:
501            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:
529    def on_initial_sign_in_complete(self) -> None:
530        """Called when initial sign-in (or lack thereof) completes.
531
532        This normally gets called by the plus subsystem. The
533        initial-sign-in process may include tasks such as syncing
534        account workspaces or other data so it may take a substantial
535        amount of time.
536        """
537        assert _babase.in_logic_thread()
538        assert not self._initial_sign_in_completed
539
540        # Tell meta it can start scanning extra stuff that just showed
541        # up (namely account workspaces).
542        self.meta.start_extra_scan()
543
544        self._initial_sign_in_completed = True
545        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):
 75    class State(Enum):
 76        """High level state the app can be in."""
 77
 78        # The app has not yet begun starting and should not be used in
 79        # any way.
 80        NOT_STARTED = 0
 81
 82        # The native layer is spinning up its machinery (screens,
 83        # renderers, etc.). Nothing should happen in the Python layer
 84        # until this completes.
 85        NATIVE_BOOTSTRAPPING = 1
 86
 87        # Python app subsystems are being inited but should not yet
 88        # interact or do any work.
 89        INITING = 2
 90
 91        # Python app subsystems are inited and interacting, but the app
 92        # has not yet embarked on a high level course of action. It is
 93        # doing initial account logins, workspace & asset downloads,
 94        # etc.
 95        LOADING = 3
 96
 97        # All pieces are in place and the app is now doing its thing.
 98        RUNNING = 4
 99
100        # Used on platforms such as mobile where the app basically needs
101        # to shut down while backgrounded. In this state, all event
102        # loops are suspended and all graphics and audio must cease
103        # completely. Be aware that the suspended state can be entered
104        # from any other state including NATIVE_BOOTSTRAPPING and
105        # SHUTTING_DOWN.
106        SUSPENDED = 5
107
108        # The app is shutting down. This process may involve sending
109        # network messages or other things that can take up to a few
110        # seconds, so ideally graphics and audio should remain
111        # functional (with fades or spinners or whatever to show
112        # something is happening).
113        SHUTTING_DOWN = 6
114
115        # The app has completed shutdown. Any code running here should
116        # be basically immediate.
117        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):
119    class DefaultAppModeSelector(AppModeSelector):
120        """Decides which AppModes to use to handle AppIntents.
121
122        This default version is generated by the project updater based
123        on the 'default_app_modes' value in the projectconfig.
124
125        It is also possible to modify app mode selection behavior by
126        setting app.mode_selector to an instance of a custom
127        AppModeSelector subclass. This is a good way to go if you are
128        modifying app behavior dynamically via a plugin instead of
129        statically in a spinoff project.
130        """
131
132        @override
133        def app_mode_for_intent(
134            self, intent: AppIntent
135        ) -> type[AppMode] | None:
136            # pylint: disable=cyclic-import
137
138            # __DEFAULT_APP_MODE_SELECTION_BEGIN__
139            # This section generated by batools.appmodule; do not edit.
140
141            # Ask our default app modes to handle it.
142            # (generated from 'default_app_modes' in projectconfig).
143            import bascenev1
144            import babase
145
146            for appmode in [
147                bascenev1.SceneV1AppMode,
148                babase.EmptyAppMode,
149            ]:
150                if appmode.can_handle_intent(intent):
151                    return appmode
152
153            return None
154
155            # __DEFAULT_APP_MODE_SELECTION_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:
132        @override
133        def app_mode_for_intent(
134            self, intent: AppIntent
135        ) -> type[AppMode] | None:
136            # pylint: disable=cyclic-import
137
138            # __DEFAULT_APP_MODE_SELECTION_BEGIN__
139            # This section generated by batools.appmodule; do not edit.
140
141            # Ask our default app modes to handle it.
142            # (generated from 'default_app_modes' in projectconfig).
143            import bascenev1
144            import babase
145
146            for appmode in [
147                bascenev1.SceneV1AppMode,
148                babase.EmptyAppMode,
149            ]:
150                if appmode.can_handle_intent(intent):
151                    return appmode
152
153            return None
154
155            # __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.

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 babase.App to access actual live state.

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

Logs things like app-not-responding issues.

@override
def on_app_loading(self) -> None:
392    @override
393    def on_app_loading(self) -> None:
394        # If any traceback dumps happened last run, log and clear them.
395        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:
456    @override
457    def on_app_suspend(self) -> None:
458        assert _babase.in_logic_thread()
459        self._running = False

Called when the app enters the suspended state.

@override
def on_app_unsuspend(self) -> None:
461    @override
462    def on_app_unsuspend(self) -> None:
463        assert _babase.in_logic_thread()
464        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        # FIXME: 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        # FIXME: 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 to use 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('app_mode_for_intent() should be overridden.')

Defines which AppModes to use 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('app_mode_for_intent() should be overridden.')

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.

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

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 apptime() -> AppTime:
549def apptime() -> babase.AppTime:
550    """Return the current app-time in seconds.
551
552    Category: **General Utility Functions**
553
554    App-time is a monotonic time value; it starts at 0.0 when the app
555    launches and will never jump by large amounts or go backwards, even if
556    the system time changes. Its progression will pause when the app is in
557    a suspended state.
558
559    Note that the AppTime returned here is simply float; it just has a
560    unique type in the type-checker's eyes to help prevent it from being
561    accidentally used with time functionality expecting other time types.
562    """
563    import babase  # pylint: disable=cyclic-import
564
565    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:
568def apptimer(time: float, call: Callable[[], Any]) -> None:
569    """Schedule a callable object to run based on app-time.
570
571    Category: **General Utility Functions**
572
573    This function creates a one-off timer which cannot be canceled or
574    modified once created. If you require the ability to do so, or need
575    a repeating timer, use the babase.AppTimer class instead.
576
577    ##### Arguments
578    ###### time (float)
579    > Length of time in seconds that the timer will wait before firing.
580
581    ###### call (Callable[[], Any])
582    > A callable Python object. Note that the timer will retain a
583    strong reference to the callable for as long as the timer exists, so you
584    may want to look into concepts such as babase.WeakCall if that is not
585    desired.
586
587    ##### Examples
588    Print some stuff through time:
589    >>> babase.screenmessage('hello from now!')
590    >>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
591                              'hello from the future!'))
592    >>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
593    ...                       'hello from the future 2!'))
594    """
595    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 babase.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 babase.WeakCall if that is not desired.

Examples

Print some stuff through time:

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

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

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

Return text currently on the system clipboard.

Category: General Utility Functions

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

def clipboard_has_text() -> bool:
643def clipboard_has_text() -> bool:
644    """Return whether there is currently text on the clipboard.
645
646    Category: **General Utility Functions**
647
648    This will return False if no system clipboard is available; no need
649     to call babase.clipboard_is_supported() separately.
650    """
651    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 babase.clipboard_is_supported() separately.

def clipboard_is_supported() -> bool:
654def clipboard_is_supported() -> bool:
655    """Return whether this platform supports clipboard operations at all.
656
657    Category: **General Utility Functions**
658
659    If this returns False, UIs should not show 'copy to clipboard'
660    buttons, etc.
661    """
662    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:
665def clipboard_set_text(value: str) -> None:
666    """Copy a string to the system clipboard.
667
668    Category: **General Utility Functions**
669
670    Ensure that babase.clipboard_is_supported() returns True before adding
671     buttons/etc. that make use of this functionality.
672    """
673    return None

Copy a string to the system clipboard.

Category: General Utility Functions

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

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

A context-preserving callable.

Category: General Utility Classes

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

Whether the context was created as empty.

def is_expired(self) -> bool:
217    def is_expired(self) -> bool:
218        """Whether the context has expired."""
219        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:
140@dataclass
141class DevConsoleTabEntry:
142    """Represents a distinct tab in the dev-console."""
143
144    name: str
145    factory: Callable[[], DevConsoleTab]

Represents a distinct tab in the dev-console.

DevConsoleTabEntry(name: str, factory: Callable[[], DevConsoleTab])
name: str
factory: Callable[[], DevConsoleTab]
class DevConsoleSubsystem:
148class DevConsoleSubsystem:
149    """Subsystem for wrangling the dev console.
150
151    The single instance of this class can be found at
152    babase.app.devconsole. The dev-console is a simple always-available
153    UI intended for use by developers; not end users. Traditionally it
154    is available by typing a backtick (`) key on a keyboard, but now can
155    be accessed via an on-screen button (see settings/advanced to enable
156    said button).
157    """
158
159    def __init__(self) -> None:
160        # All tabs in the dev-console. Add your own stuff here via
161        # plugins or whatnot.
162        self.tabs: list[DevConsoleTabEntry] = [
163            DevConsoleTabEntry('Python', DevConsoleTabPython)
164        ]
165        if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1':
166            self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest))
167        self.is_refreshing = False
168
169    def do_refresh_tab(self, tabname: str) -> None:
170        """Called by the C++ layer when a tab should be filled out."""
171        assert _babase.in_logic_thread()
172
173        # FIXME: We currently won't handle multiple tabs with the same
174        # name. We should give a clean error or something in that case.
175        tab: DevConsoleTab | None = None
176        for tabentry in self.tabs:
177            if tabentry.name == tabname:
178                tab = tabentry.factory()
179                break
180
181        if tab is None:
182            logging.error(
183                'DevConsole got refresh request for tab'
184                " '%s' which does not exist.",
185                tabname,
186            )
187            return
188
189        self.is_refreshing = True
190        try:
191            tab.refresh()
192        finally:
193            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:
169    def do_refresh_tab(self, tabname: str) -> None:
170        """Called by the C++ layer when a tab should be filled out."""
171        assert _babase.in_logic_thread()
172
173        # FIXME: We currently won't handle multiple tabs with the same
174        # name. We should give a clean error or something in that case.
175        tab: DevConsoleTab | None = None
176        for tabentry in self.tabs:
177            if tabentry.name == tabname:
178                tab = tabentry.factory()
179                break
180
181        if tab is None:
182            logging.error(
183                'DevConsole got refresh request for tab'
184                " '%s' which does not exist.",
185                tabname,
186            )
187            return
188
189        self.is_refreshing = True
190        try:
191            tab.refresh()
192        finally:
193            self.is_refreshing = False

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

DisplayTime = DisplayTime
def displaytime() -> DisplayTime:
758def displaytime() -> babase.DisplayTime:
759    """Return the current display-time in seconds.
760
761    Category: **General Utility Functions**
762
763    Display-time is a time value intended to be used for animation and other
764    visual purposes. It will generally increment by a consistent amount each
765    frame. It will pass at an overall similar rate to AppTime, but trades
766    accuracy for smoothness.
767
768    Note that the value returned here is simply a float; it just has a
769    unique type in the type-checker's eyes to help prevent it from being
770    accidentally used with time functionality expecting other time types.
771    """
772    import babase  # pylint: disable=cyclic-import
773
774    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:
777def displaytimer(time: float, call: Callable[[], Any]) -> None:
778    """Schedule a callable object to run based on display-time.
779
780    Category: **General Utility Functions**
781
782    This function creates a one-off timer which cannot be canceled or
783    modified once created. If you require the ability to do so, or need
784    a repeating timer, use the babase.DisplayTimer class instead.
785
786    Display-time is a time value intended to be used for animation and other
787    visual purposes. It will generally increment by a consistent amount each
788    frame. It will pass at an overall similar rate to AppTime, but trades
789    accuracy for smoothness.
790
791    ##### Arguments
792    ###### time (float)
793    > Length of time in seconds that the timer will wait before firing.
794
795    ###### call (Callable[[], Any])
796    > A callable Python object. Note that the timer will retain a
797    strong reference to the callable for as long as the timer exists, so you
798    may want to look into concepts such as babase.WeakCall if that is not
799    desired.
800
801    ##### Examples
802    Print some stuff through time:
803    >>> babase.screenmessage('hello from now!')
804    >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
805    ...                       'hello from the future!'))
806    >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
807    ...                       'hello from the future 2!'))
808    """
809    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 babase.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 babase.WeakCall if that is not desired.

Examples

Print some stuff through time:

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

DisplayTimer(time: float, call: Callable[[], Any], repeat: bool = False)
267    def __init__(
268        self, time: float, call: Callable[[], Any], repeat: bool = False
269    ) -> None:
270        pass
def do_once() -> bool:
817def do_once() -> bool:
818    """Return whether this is the first time running a line of code.
819
820    Category: **General Utility Functions**
821
822    This is used by 'print_once()' type calls to keep from overflowing
823    logs. The call functions by registering the filename and line where
824    The call is made from.  Returns True if this location has not been
825    registered already, and False if it has.
826
827    ##### Example
828    This print will only fire for the first loop iteration:
829    >>> for i in range(10):
830    ... if babase.do_once():
831    ...     print('HelloWorld once from loop!')
832    """
833    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 babase.do_once():
...     print('HelloWorld once from loop!')
class EmptyAppMode(babase.AppMode):
20class EmptyAppMode(AppMode):
21    """An empty app mode that can be used as a fallback/etc."""
22
23    @override
24    @classmethod
25    def get_app_experience(cls) -> AppExperience:
26        return AppExperience.EMPTY
27
28    @override
29    @classmethod
30    def _supports_intent(cls, intent: AppIntent) -> bool:
31        # We support default and exec intents currently.
32        return isinstance(intent, AppIntentExec | AppIntentDefault)
33
34    @override
35    def handle_intent(self, intent: AppIntent) -> None:
36        if isinstance(intent, AppIntentExec):
37            _babase.empty_app_mode_handle_intent_exec(intent.code)
38            return
39        assert isinstance(intent, AppIntentDefault)
40        _babase.empty_app_mode_handle_intent_default()
41
42    @override
43    def on_activate(self) -> None:
44        # Let the native layer do its thing.
45        _babase.on_empty_app_mode_activate()
46
47    @override
48    def on_deactivate(self) -> None:
49        # Let the native layer do its thing.
50        _babase.on_empty_app_mode_deactivate()

An empty app mode that can be used as a fallback/etc.

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

Return the overall experience provided by this mode.

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

Handle an intent.

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

Called when the mode is being activated.

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

Called when the mode is being deactivated.

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

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.

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.

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.

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.

vr: bool

Whether the app is currently running in VR.

class Existable(typing.Protocol):
37class Existable(Protocol):
38    """A Protocol for objects supporting an exists() method.
39
40    Category: **Protocols**
41    """
42
43    def exists(self) -> bool:
44        """Whether this object exists."""

A Protocol for objects supporting an exists() method.

Category: Protocols

Existable(*args, **kwargs)
1934def _no_init_or_replace_init(self, *args, **kwargs):
1935    cls = type(self)
1936
1937    if cls._is_protocol:
1938        raise TypeError('Protocols cannot be instantiated')
1939
1940    # Already using a custom `__init__`. No need to calculate correct
1941    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1942    if cls.__init__ is not _no_init_or_replace_init:
1943        return
1944
1945    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1946    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1947    # searches for a proper new `__init__` in the MRO. The new `__init__`
1948    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1949    # instantiation of the protocol subclass will thus use the new
1950    # `__init__` and no longer call `_no_init_or_replace_init`.
1951    for base in cls.__mro__:
1952        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1953        if init is not _no_init_or_replace_init:
1954            cls.__init__ = init
1955            break
1956    else:
1957        # should not happen
1958        cls.__init__ = object.__init__
1959
1960    cls.__init__(self, *args, **kwargs)
def exists(self) -> bool:
43    def exists(self) -> bool:
44        """Whether this object exists."""

Whether this object exists.

def existing(obj: Optional[~ExistableT]) -> Optional[~ExistableT]:
51def existing(obj: ExistableT | None) -> ExistableT | None:
52    """Convert invalid references to None for any babase.Existable object.
53
54    Category: **Gameplay Functions**
55
56    To best support type checking, it is important that invalid references
57    not be passed around and instead get converted to values of None.
58    That way the type checker can properly flag attempts to pass possibly-dead
59    objects (FooType | None) into functions expecting only live ones
60    (FooType), etc. This call can be used on any 'existable' object
61    (one with an exists() method) and will convert it to a None value
62    if it does not exist.
63
64    For more info, see notes on 'existables' here:
65    https://ballistica.net/wiki/Coding-Style-Guide
66    """
67    assert obj is None or hasattr(obj, 'exists'), f'No "exists" on {obj}'
68    return obj if obj is not None and obj.exists() else None

Convert invalid references to None for any babase.Existable object.

Category: Gameplay Functions

To best support type checking, it is important that invalid references not be passed around and instead get converted to values of None. That way the type checker can properly flag attempts to pass possibly-dead objects (FooType | None) into functions expecting only live ones (FooType), etc. This call can be used on any 'existable' object (one with an exists() method) and will convert it to a None value if it does not exist.

For more info, see notes on 'existables' here: https://ballistica.net/wiki/Coding-Style-Guide

def fatal_error(message: str) -> None:
895def fatal_error(message: str) -> None:
896    """Trigger a fatal error. Use this in situations where it is not possible
897    for the engine to continue on in a useful way. This can sometimes
898    help provide more clear information at the exact source of a problem
899    as compared to raising an Exception. In the vast majority of cases,
900    however, Exceptions should be preferred.
901    """
902    return None

Trigger a fatal error. Use this in situations where it is not possible for the engine to continue on in a useful way. This can sometimes help provide more clear information at the exact source of a problem as compared to raising an Exception. In the vast majority of cases, however, Exceptions should be preferred.

def garbage_collect() -> None:
215def garbage_collect() -> None:
216    """Run an explicit pass of garbage collection.
217
218    category: General Utility Functions
219
220    May also print warnings/etc. if collection takes too long or if
221    uncollectible objects are found (so use this instead of simply
222    gc.collect().
223    """
224    gc.collect()

Run an explicit pass of garbage collection.

category: General Utility Functions

May also print warnings/etc. if collection takes too long or if uncollectible objects are found (so use this instead of simply gc.collect().

def get_input_idle_time() -> float:
982def get_input_idle_time() -> float:
983    """Return seconds since any local input occurred (touch, keypress, etc.)."""
984    return float()

Return seconds since any local input occurred (touch, keypress, etc.).

def get_ip_address_type(addr: str) -> socket.AddressFamily:
54def get_ip_address_type(addr: str) -> socket.AddressFamily:
55    """Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
56    import socket
57
58    socket_type = None
59
60    # First try it as an ipv4 address.
61    try:
62        socket.inet_pton(socket.AF_INET, addr)
63        socket_type = socket.AF_INET
64    except OSError:
65        pass
66
67    # Hmm apparently not ipv4; try ipv6.
68    if socket_type is None:
69        try:
70            socket.inet_pton(socket.AF_INET6, addr)
71            socket_type = socket.AF_INET6
72        except OSError:
73            pass
74    if socket_type is None:
75        raise ValueError(f'addr seems to be neither v4 or v6: {addr}')
76    return socket_type

Return socket.AF_INET6 or socket.AF_INET4 for the provided address.

def get_type_name(cls: type) -> str:
108def get_type_name(cls: type) -> str:
109    """Return a full type name including module for a class."""
110    return f'{cls.__module__}.{cls.__name__}'

Return a full type name including module for a class.

def getclass(name: str, subclassof: type[~T]) -> type[~T]:
71def getclass(name: str, subclassof: type[T]) -> type[T]:
72    """Given a full class name such as foo.bar.MyClass, return the class.
73
74    Category: **General Utility Functions**
75
76    The class will be checked to make sure it is a subclass of the provided
77    'subclassof' class, and a TypeError will be raised if not.
78    """
79    import importlib
80
81    splits = name.split('.')
82    modulename = '.'.join(splits[:-1])
83    classname = splits[-1]
84    module = importlib.import_module(modulename)
85    cls: type = getattr(module, classname)
86
87    if not issubclass(cls, subclassof):
88        raise TypeError(f'{name} is not a subclass of {subclassof}.')
89    return cls

Given a full class name such as foo.bar.MyClass, return the class.

Category: General Utility Functions

The class will be checked to make sure it is a subclass of the provided 'subclassof' class, and a TypeError will be raised if not.

def handle_leftover_v1_cloud_log_file() -> None:
153def handle_leftover_v1_cloud_log_file() -> None:
154    """Handle an un-uploaded v1-cloud-log from a previous run."""
155
156    # Only applies with classic present.
157    if _babase.app.classic is None:
158        return
159    try:
160        import json
161
162        if os.path.exists(_babase.get_v1_cloud_log_file_path()):
163            with open(
164                _babase.get_v1_cloud_log_file_path(), encoding='utf-8'
165            ) as infile:
166                info = json.loads(infile.read())
167            infile.close()
168            do_send = should_submit_debug_info()
169            if do_send