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    can_toggle_fullscreen,
 31    charstr,
 32    clipboard_get_text,
 33    clipboard_has_text,
 34    clipboard_is_supported,
 35    clipboard_set_text,
 36    ContextCall,
 37    ContextRef,
 38    displaytime,
 39    displaytimer,
 40    DisplayTimer,
 41    do_once,
 42    env,
 43    Env,
 44    fade_screen,
 45    fatal_error,
 46    get_display_resolution,
 47    get_immediate_return_code,
 48    get_low_level_config_value,
 49    get_max_graphics_quality,
 50    get_replays_dir,
 51    get_string_height,
 52    get_string_width,
 53    get_v1_cloud_log_file_path,
 54    getsimplesound,
 55    has_user_run_commands,
 56    have_chars,
 57    have_permission,
 58    in_logic_thread,
 59    increment_analytics_count,
 60    is_os_playing_music,
 61    is_running_on_fire_tv,
 62    is_xcode_build,
 63    lock_all_input,
 64    mac_music_app_get_library_source,
 65    mac_music_app_get_playlists,
 66    mac_music_app_get_volume,
 67    mac_music_app_init,
 68    mac_music_app_play_playlist,
 69    mac_music_app_set_volume,
 70    mac_music_app_stop,
 71    music_player_play,
 72    music_player_set_volume,
 73    music_player_shutdown,
 74    music_player_stop,
 75    native_stack_trace,
 76    print_load_info,
 77    pushcall,
 78    quit,
 79    reload_media,
 80    request_permission,
 81    safecolor,
 82    screenmessage,
 83    set_analytics_screen,
 84    set_low_level_config_value,
 85    set_stress_testing,
 86    set_thread_name,
 87    set_ui_input_device,
 88    show_progress_bar,
 89    shutdown_suppress_begin,
 90    shutdown_suppress_end,
 91    shutdown_suppress_count,
 92    SimpleSound,
 93    supports_max_fps,
 94    supports_vsync,
 95    unlock_all_input,
 96    user_agent_string,
 97    Vec3,
 98    workspaces_in_use,
 99)
100
101from babase._accountv2 import AccountV2Handle, AccountV2Subsystem
102from babase._app import App
103from babase._appconfig import commit_app_config
104from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec
105from babase._appmode import AppMode
106from babase._appsubsystem import AppSubsystem
107from babase._appmodeselector import AppModeSelector
108from babase._appconfig import AppConfig
109from babase._apputils import (
110    handle_leftover_v1_cloud_log_file,
111    is_browser_likely_available,
112    garbage_collect,
113    get_remote_app_name,
114    AppHealthMonitor,
115)
116from babase._cloud import CloudSubsystem
117from babase._emptyappmode import EmptyAppMode
118from babase._error import (
119    print_exception,
120    print_error,
121    ContextError,
122    NotFoundError,
123    PlayerNotFoundError,
124    SessionPlayerNotFoundError,
125    NodeNotFoundError,
126    ActorNotFoundError,
127    InputDeviceNotFoundError,
128    WidgetNotFoundError,
129    ActivityNotFoundError,
130    TeamNotFoundError,
131    MapNotFoundError,
132    SessionTeamNotFoundError,
133    SessionNotFoundError,
134    DelegateNotFoundError,
135)
136from babase._general import (
137    utf8_all,
138    DisplayTime,
139    AppTime,
140    WeakCall,
141    Call,
142    existing,
143    Existable,
144    verify_object_death,
145    storagename,
146    getclass,
147    get_type_name,
148)
149from babase._keyboard import Keyboard
150from babase._language import Lstr, LanguageSubsystem
151from babase._login import LoginAdapter
152
153# noinspection PyProtectedMember
154# (PyCharm inspection bug?)
155from babase._mgen.enums import (
156    Permission,
157    SpecialChar,
158    InputType,
159    UIScale,
160)
161from babase._math import normalized_color, is_point_in_box, vec3validate
162from babase._meta import MetadataSubsystem
163from babase._net import get_ip_address_type, DEFAULT_REQUEST_TIMEOUT_SECONDS
164from babase._plugin import PluginSpec, Plugin, PluginSubsystem
165from babase._stringedit import StringEditAdapter, StringEditSubsystem
166from babase._text import timestring
167
168_babase.app = app = App()
169app.postinit()
170
171__all__ = [
172    'AccountV2Handle',
173    'AccountV2Subsystem',
174    'ActivityNotFoundError',
175    'ActorNotFoundError',
176    'add_clean_frame_callback',
177    'android_get_external_files_dir',
178    'app',
179    'app',
180    'App',
181    'AppConfig',
182    'AppHealthMonitor',
183    'AppIntent',
184    'AppIntentDefault',
185    'AppIntentExec',
186    'AppMode',
187    'appname',
188    'appnameupper',
189    'AppModeSelector',
190    'AppSubsystem',
191    'apptime',
192    'AppTime',
193    'apptime',
194    'apptimer',
195    'AppTimer',
196    'Call',
197    'can_toggle_fullscreen',
198    'charstr',
199    'clipboard_get_text',
200    'clipboard_has_text',
201    'clipboard_is_supported',
202    'clipboard_set_text',
203    'CloudSubsystem',
204    'commit_app_config',
205    'ContextCall',
206    'ContextError',
207    'ContextRef',
208    'DelegateNotFoundError',
209    'DisplayTime',
210    'displaytime',
211    'displaytimer',
212    'DisplayTimer',
213    'do_once',
214    'EmptyAppMode',
215    'env',
216    'Env',
217    'Existable',
218    'existing',
219    'fade_screen',
220    'fatal_error',
221    'garbage_collect',
222    'get_display_resolution',
223    'get_immediate_return_code',
224    'get_ip_address_type',
225    'get_low_level_config_value',
226    'get_max_graphics_quality',
227    'get_remote_app_name',
228    'get_replays_dir',
229    'get_string_height',
230    'get_string_width',
231    'get_v1_cloud_log_file_path',
232    'get_type_name',
233    'getclass',
234    'getsimplesound',
235    'handle_leftover_v1_cloud_log_file',
236    'has_user_run_commands',
237    'have_chars',
238    'have_permission',
239    'in_logic_thread',
240    'increment_analytics_count',
241    'InputDeviceNotFoundError',
242    'InputType',
243    'is_browser_likely_available',
244    'is_browser_likely_available',
245    'is_os_playing_music',
246    'is_point_in_box',
247    'is_running_on_fire_tv',
248    'is_xcode_build',
249    'Keyboard',
250    'LanguageSubsystem',
251    'lock_all_input',
252    'LoginAdapter',
253    'Lstr',
254    'mac_music_app_get_library_source',
255    'mac_music_app_get_playlists',
256    'mac_music_app_get_volume',
257    'mac_music_app_init',
258    'mac_music_app_play_playlist',
259    'mac_music_app_set_volume',
260    'mac_music_app_stop',
261    'MapNotFoundError',
262    'MetadataSubsystem',
263    'music_player_play',
264    'music_player_set_volume',
265    'music_player_shutdown',
266    'music_player_stop',
267    'native_stack_trace',
268    'NodeNotFoundError',
269    'normalized_color',
270    'NotFoundError',
271    'Permission',
272    'PlayerNotFoundError',
273    'Plugin',
274    'PluginSubsystem',
275    'PluginSpec',
276    'print_error',
277    'print_exception',
278    'print_load_info',
279    'pushcall',
280    'quit',
281    'reload_media',
282    'request_permission',
283    'safecolor',
284    'screenmessage',
285    'SessionNotFoundError',
286    'SessionPlayerNotFoundError',
287    'SessionTeamNotFoundError',
288    'set_analytics_screen',
289    'set_low_level_config_value',
290    'set_stress_testing',
291    'set_thread_name',
292    'set_ui_input_device',
293    'show_progress_bar',
294    'shutdown_suppress_begin',
295    'shutdown_suppress_end',
296    'shutdown_suppress_count',
297    'SimpleSound',
298    'SpecialChar',
299    'storagename',
300    'StringEditAdapter',
301    'StringEditSubsystem',
302    'supports_max_fps',
303    'supports_vsync',
304    'TeamNotFoundError',
305    'timestring',
306    'UIScale',
307    'unlock_all_input',
308    'user_agent_string',
309    'utf8_all',
310    'Vec3',
311    'vec3validate',
312    'verify_object_death',
313    'WeakCall',
314    'WidgetNotFoundError',
315    'workspaces_in_use',
316    'DEFAULT_REQUEST_TIMEOUT_SECONDS',
317]
318
319# We want stuff to show up as babase.Foo instead of babase._sub.Foo.
320set_canonical_module_names(globals())
321
322# Allow the native layer to wrap a few things up.
323_babase.reached_end_of_babase()
324
325# Marker we pop down at the very end so other modules can run sanity
326# checks to make sure we aren't importing them reciprocally when they
327# import us.
328_REACHED_END_OF_MODULE = True
class AccountV2Handle:
416class AccountV2Handle:
417    """Handle for interacting with a V2 account.
418
419    This class supports the 'with' statement, which is how it is
420    used with some operations such as cloud messaging.
421    """
422
423    def __init__(self) -> None:
424        self.tag = '?'
425
426        self.workspacename: str | None = None
427        self.workspaceid: str | None = None
428
429        # Login types and their display-names associated with this account.
430        self.logins: dict[LoginType, str] = {}
431
432    def __enter__(self) -> None:
433        """Support for "with" statement.
434
435        This allows cloud messages to be sent on our behalf.
436        """
437
438    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
439        """Support for "with" statement.
440
441        This allows cloud messages to be sent on our behalf.
442        """

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.

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

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

Should be called at standard on_app_loading time.

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

Set credentials for the primary app account.

def have_primary_credentials(self) -> bool:
70    def have_primary_credentials(self) -> bool:
71        """Are credentials currently set for the primary app account?
72
73        Note that this does not mean these credentials are currently valid;
74        only that they exist. If/when credentials are validated, the 'primary'
75        account handle will be set.
76        """
77        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

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

def do_get_primary(self) -> AccountV2Handle | None:
84    def do_get_primary(self) -> AccountV2Handle | None:
85        """Internal - should be overridden by subclass."""
86        return None

Internal - should be overridden by subclass.

def on_primary_account_changed(self, account: AccountV2Handle | None) -> None:
 88    def on_primary_account_changed(
 89        self, account: AccountV2Handle | None
 90    ) -> None:
 91        """Callback run after the primary account changes.
 92
 93        Will be called with None on log-outs and when new credentials
 94        are set but have not yet been verified.
 95        """
 96        assert _babase.in_logic_thread()
 97
 98        # Currently don't do anything special on sign-outs.
 99        if account is None:
100            return
101
102        # If this new account has a workspace, update it and ask to be
103        # informed when that process completes.
104        if account.workspaceid is not None:
105            assert account.workspacename is not None
106            if (
107                not self._initial_sign_in_completed
108                and not self._kicked_off_workspace_load
109            ):
110                self._kicked_off_workspace_load = True
111                _babase.app.workspaces.set_active_workspace(
112                    account=account,
113                    workspaceid=account.workspaceid,
114                    workspacename=account.workspacename,
115                    on_completed=self._on_set_active_workspace_completed,
116                )
117            else:
118                # Don't activate workspaces if we've already told the game
119                # that initial-log-in is done or if we've already kicked
120                # off a workspace load.
121                _babase.screenmessage(
122                    f'\'{account.workspacename}\''
123                    f' will be activated at next app launch.',
124                    color=(1, 1, 0),
125                )
126                _babase.getsimplesound('error').play()
127            return
128
129        # Ok; no workspace to worry about; carry on.
130        if not self._initial_sign_in_completed:
131            self._initial_sign_in_completed = True
132            _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:
134    def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
135        """Should be called when logins for the active account change."""
136
137        for adapter in self.login_adapters.values():
138            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:
140    def on_implicit_sign_in(
141        self, login_type: LoginType, login_id: str, display_name: str
142    ) -> None:
143        """An implicit sign-in happened (called by native layer)."""
144        from babase._login import LoginAdapter
145
146        with _babase.ContextRef.empty():
147            self.login_adapters[login_type].set_implicit_login_state(
148                LoginAdapter.ImplicitLoginState(
149                    login_id=login_id, display_name=display_name
150                )
151            )

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

def on_implicit_sign_out(self, login_type: bacommon.login.LoginType) -> None:
153    def on_implicit_sign_out(self, login_type: LoginType) -> None:
154        """An implicit sign-out happened (called by native layer)."""
155        with _babase.ContextRef.empty():
156            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:
158    def on_no_initial_primary_account(self) -> None:
159        """Callback run if the app has no primary account after launch.
160
161        Either this callback or on_primary_account_changed will be called
162        within a few seconds of app launch; the app can move forward
163        with the startup sequence at that point.
164        """
165        if not self._initial_sign_in_completed:
166            self._initial_sign_in_completed = True
167            _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:
175    def on_implicit_login_state_changed(
176        self,
177        login_type: LoginType,
178        state: LoginAdapter.ImplicitLoginState | None,
179    ) -> None:
180        """Called when implicit login state changes.
181
182        Login systems that tend to sign themselves in/out in the
183        background are considered implicit. We may choose to honor or
184        ignore their states, allowing the user to opt for other login
185        types even if the default implicit one can't be explicitly
186        logged out or otherwise controlled.
187        """
188        from babase._language import Lstr
189
190        assert _babase.in_logic_thread()
191
192        cfg = _babase.app.config
193        cfgkey = 'ImplicitLoginStates'
194        cfgdict = _babase.app.config.setdefault(cfgkey, {})
195
196        # Store which (if any) adapter is currently implicitly signed in.
197        # Making the assumption there will only ever be one implicit
198        # adapter at a time; may need to update this if that changes.
199        prev_state = cfgdict.get(login_type.value)
200        if state is None:
201            self._implicit_signed_in_adapter = None
202            new_state = cfgdict[login_type.value] = None
203        else:
204            self._implicit_signed_in_adapter = self.login_adapters[login_type]
205            new_state = cfgdict[login_type.value] = self._hashstr(
206                state.login_id
207            )
208
209            # Special case: if the user is already signed in but not with
210            # this implicit login, we may want to let them know that the
211            # 'Welcome back FOO' they likely just saw is not actually
212            # accurate.
213            if (
214                self.primary is not None
215                and not self.login_adapters[login_type].is_back_end_active()
216            ):
217                if login_type is LoginType.GPGS:
218                    service_str = Lstr(resource='googlePlayText')
219                else:
220                    service_str = None
221                if service_str is not None:
222                    _babase.apptimer(
223                        2.0,
224                        tpartial(
225                            _babase.screenmessage,
226                            Lstr(
227                                resource='notUsingAccountText',
228                                subs=[
229                                    ('${ACCOUNT}', state.display_name),
230                                    ('${SERVICE}', service_str),
231                                ],
232                            ),
233                            (1, 0.5, 0),
234                        ),
235                    )
236
237        cfg.commit()
238
239        # We want to respond any time the implicit state changes;
240        # generally this means the user has explicitly signed in/out or
241        # switched accounts within that back-end.
242        if prev_state != new_state:
243            if DEBUG_LOG:
244                logging.debug(
245                    'AccountV2: Implicit state changed (%s -> %s);'
246                    ' will update app sign-in state accordingly.',
247                    prev_state,
248                    new_state,
249                )
250            self._implicit_state_changed = True
251
252        # We may want to auto-sign-in based on this new state.
253        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:
255    def on_cloud_connectivity_changed(self, connected: bool) -> None:
256        """Should be called with cloud connectivity changes."""
257        del connected  # Unused.
258        assert _babase.in_logic_thread()
259
260        # We may want to auto-sign-in based on this new state.
261        self._update_auto_sign_in()

Should be called with cloud connectivity changes.

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

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
fg_state
config_file_healthy: bool
def postinit(self) -> None:
201    def postinit(self) -> None:
202        """Called after we've been inited and assigned to babase.app.
203
204        Anything that accesses babase.app as part of its init process
205        must go here instead of __init__.
206        """
207
208        # Hack for docs-generation: we can be imported with dummy modules
209        # instead of our actual binary ones, but we don't function.
210        if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
211            return
212
213        self.lang = LanguageSubsystem()
214        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__.

aioloop: asyncio.events.AbstractEventLoop

The logic thread's asyncio event loop.

This allow async tasks to be run in the logic thread. 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 not return this loop from most places in the logic thread; only from within a task explicitly created in this loop.

config: AppConfig

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

mode_selector: AppModeSelector

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

Our classic subsystem (if available).

plus: baplus._subsystem.PlusSubsystem | None

Our plus subsystem (if available).

ui_v1: bauiv1._subsystem.UIV1Subsystem

Our ui_v1 subsystem (always available).

def register_subsystem(self, subsystem: AppSubsystem) -> None:
299    def register_subsystem(self, subsystem: AppSubsystem) -> None:
300        """Called by the AppSubsystem class. Do not use directly."""
301
302        # We only allow registering new subsystems if we've not yet
303        # reached the 'running' state. This ensures that all subsystems
304        # receive a consistent set of callbacks starting with
305        # on_app_running().
306        if self._subsystem_registration_ended:
307            raise RuntimeError(
308                'Subsystems can no longer be registered at this point.'
309            )
310        self._subsystems.append(subsystem)

Called by the AppSubsystem class. Do not use directly.

def add_shutdown_task(self, coro: Coroutine[NoneType, NoneType, NoneType]) -> None:
312    def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
313        """Add a task to be run on app shutdown.
314
315        Note that tasks will be killed after
316        App.SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.
317        """
318        if (
319            self.state is self.State.SHUTTING_DOWN
320            or self.state is self.State.SHUTDOWN_COMPLETE
321        ):
322            stname = self.state.name
323            raise RuntimeError(
324                f'Cannot add shutdown tasks with current state {stname}.'
325            )
326        self._shutdown_tasks.append(coro)

Add a task to be run on app shutdown.

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

def run(self) -> None:
328    def run(self) -> None:
329        """Run the app to completion.
330
331        Note that this only works on builds where Ballistica manages
332        its own event loop.
333        """
334        _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:
336    def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
337        """Submit a call to the app threadpool where result is not needed.
338
339        Normally, doing work in a thread-pool involves creating a future
340        and waiting for its result, which is an important step because it
341        propagates any Exceptions raised by the submitted work. When the
342        result in not important, however, this call can be used. The app
343        will log any exceptions that occur.
344        """
345        fut = self.threadpool.submit(call)
346        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:
348    def set_intent(self, intent: AppIntent) -> None:
349        """Set the intent for the app.
350
351        Intent defines what the app is trying to do at a given time.
352        This call is asynchronous; the intent switch will happen in the
353        logic thread in the near future. If set_intent is called
354        repeatedly before the change takes place, the final intent to be
355        set will be used.
356        """
357
358        # Mark this one as pending. We do this synchronously so that the
359        # last one marked actually takes effect if there is overlap
360        # (doing this in the bg thread could result in race conditions).
361        self._pending_intent = intent
362
363        # Do the actual work of calcing our app-mode/etc. in a bg thread
364        # since it may block for a moment to load modules/etc.
365        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:
367    def push_apply_app_config(self) -> None:
368        """Internal. Use app.config.apply() to apply app config changes."""
369        # To be safe, let's run this by itself in the event loop.
370        # This avoids potential trouble if this gets called mid-draw or
371        # something like that.
372        self._pending_apply_app_config = True
373        _babase.pushcall(self._apply_app_config, raw=True)

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

def on_native_start(self) -> None:
375    def on_native_start(self) -> None:
376        """Called by the native layer when the app is being started."""
377        assert _babase.in_logic_thread()
378        assert not self._native_start_called
379        self._native_start_called = True
380        self._update_state()

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

def on_native_bootstrapping_complete(self) -> None:
382    def on_native_bootstrapping_complete(self) -> None:
383        """Called by the native layer once its ready to rock."""
384        assert _babase.in_logic_thread()
385        assert not self._native_bootstrapping_completed
386        self._native_bootstrapping_completed = True
387        self._update_state()

Called by the native layer once its ready to rock.

def on_native_pause(self) -> None:
389    def on_native_pause(self) -> None:
390        """Called by the native layer when the app pauses."""
391        assert _babase.in_logic_thread()
392        assert not self._native_paused  # Should avoid redundant calls.
393        self._native_paused = True
394        self._update_state()

Called by the native layer when the app pauses.

def on_native_resume(self) -> None:
396    def on_native_resume(self) -> None:
397        """Called by the native layer when the app resumes."""
398        assert _babase.in_logic_thread()
399        assert self._native_paused  # Should avoid redundant calls.
400        self._native_paused = False
401        self._update_state()

Called by the native layer when the app resumes.

def on_native_shutdown(self) -> None:
403    def on_native_shutdown(self) -> None:
404        """Called by the native layer when the app starts shutting down."""
405        assert _babase.in_logic_thread()
406        self._native_shutdown_called = True
407        self._update_state()

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

def on_native_shutdown_complete(self) -> None:
409    def on_native_shutdown_complete(self) -> None:
410        """Called by the native layer when the app is done shutting down."""
411        assert _babase.in_logic_thread()
412        self._native_shutdown_complete_called = True
413        self._update_state()

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

def on_initial_sign_in_complete(self) -> None:
441    def on_initial_sign_in_complete(self) -> None:
442        """Called when initial sign-in (or lack thereof) completes.
443
444        This normally gets called by the plus subsystem. The
445        initial-sign-in process may include tasks such as syncing
446        account workspaces or other data so it may take a substantial
447        amount of time.
448        """
449        assert _babase.in_logic_thread()
450        assert not self._initial_sign_in_completed
451
452        # Tell meta it can start scanning extra stuff that just showed
453        # up (namely account workspaces).
454        self.meta.start_extra_scan()
455
456        self._initial_sign_in_completed = True
457        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.

build_number: int

Integer build number.

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

device_name: str

Name of the device running the app.

config_file_path: str

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

version: str

Human-readable engine version string; something like '1.3.24'.

This should not be interpreted as a number; it may contain string elements such as 'alpha', 'beta', 'test', etc. If a numeric version is needed, use build_number.

debug_build: bool

Whether the app was compiled in debug mode.

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

test_build: bool

Whether the app was compiled in test mode.

Test mode enables extra checks and features that are useful for release testing but which do not slow the game down significantly.

data_directory: str

Path where static app data lives.

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.

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.

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.

on_tv: bool

Whether the app is currently running on a TV.

vr_mode: bool

Whether the app is currently running in VR.

arcade_mode: bool

Whether the app is currently running on arcade hardware.

headless_mode: bool

Whether the app is running headlessly.

demo_mode: bool

Whether the app is targeting a demo experience.

class App.State(enum.Enum):
 70    class State(Enum):
 71        """High level state the app can be in."""
 72
 73        # The app has not yet begun starting and should not be used in
 74        # any way.
 75        NOT_RUNNING = 0
 76
 77        # The native layer is spinning up its machinery (screens,
 78        # renderers, etc.). Nothing should happen in the Python layer
 79        # until this completes.
 80        NATIVE_BOOTSTRAPPING = 1
 81
 82        # Python app subsystems are being inited but should not yet
 83        # interact or do any work.
 84        INITING = 2
 85
 86        # Python app subsystems are inited and interacting, but the app
 87        # has not yet embarked on a high level course of action. It is
 88        # doing initial account logins, workspace & asset downloads,
 89        # etc.
 90        LOADING = 3
 91
 92        # All pieces are in place and the app is now doing its thing.
 93        RUNNING = 4
 94
 95        # The app is backgrounded or otherwise suspended.
 96        PAUSED = 5
 97
 98        # The app is shutting down.
 99        SHUTTING_DOWN = 6
100
101        # The app has completed shutdown.
102        SHUTDOWN_COMPLETE = 7

High level state the app can be in.

NOT_RUNNING = <State.NOT_RUNNING: 0>
NATIVE_BOOTSTRAPPING = <State.NATIVE_BOOTSTRAPPING: 1>
INITING = <State.INITING: 2>
LOADING = <State.LOADING: 3>
RUNNING = <State.RUNNING: 4>
PAUSED = <State.PAUSED: 5>
SHUTTING_DOWN = <State.SHUTTING_DOWN: 6>
SHUTDOWN_COMPLETE = <State.SHUTDOWN_COMPLETE: 7>
Inherited Members
enum.Enum
name
value
class App.DefaultAppModeSelector(babase.AppModeSelector):
104    class DefaultAppModeSelector(AppModeSelector):
105        """Decides which AppModes to use to handle AppIntents.
106
107        This default version is generated by the project updater based
108        on the 'default_app_modes' value in the projectconfig.
109
110        It is also possible to modify app mode selection behavior by
111        setting app.mode_selector to an instance of a custom
112        AppModeSelector subclass. This is a good way to go if you are
113        modifying app behavior dynamically via a plugin instead of
114        statically in a spinoff project.
115        """
116
117        def app_mode_for_intent(
118            self, intent: AppIntent
119        ) -> type[AppMode] | None:
120            # pylint: disable=cyclic-import
121
122            # __DEFAULT_APP_MODE_SELECTION_BEGIN__
123            # This section generated by batools.appmodule; do not edit.
124
125            # Ask our default app modes to handle it.
126            # (generated from 'default_app_modes' in projectconfig).
127            import bascenev1
128            import babase
129
130            for appmode in [
131                bascenev1.SceneV1AppMode,
132                babase.EmptyAppMode,
133            ]:
134                if appmode.can_handle_intent(intent):
135                    return appmode
136
137            return None
138
139            # __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.

def app_mode_for_intent( self, intent: AppIntent) -> type[AppMode] | None:
117        def app_mode_for_intent(
118            self, intent: AppIntent
119        ) -> type[AppMode] | None:
120            # pylint: disable=cyclic-import
121
122            # __DEFAULT_APP_MODE_SELECTION_BEGIN__
123            # This section generated by batools.appmodule; do not edit.
124
125            # Ask our default app modes to handle it.
126            # (generated from 'default_app_modes' in projectconfig).
127            import bascenev1
128            import babase
129
130            for appmode in [
131                bascenev1.SceneV1AppMode,
132                babase.EmptyAppMode,
133            ]:
134                if appmode.can_handle_intent(intent):
135                    return appmode
136
137            return None
138
139            # __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):
371class AppHealthMonitor(AppSubsystem):
372    """Logs things like app-not-responding issues."""
373
374    def __init__(self) -> None:
375        assert _babase.in_logic_thread()
376        super().__init__()
377        self._running = True
378        self._thread = Thread(target=self._app_monitor_thread_main, daemon=True)
379        self._thread.start()
380        self._response = False
381        self._first_check = True
382
383    def on_app_loading(self) -> None:
384        # If any traceback dumps happened last run, log and clear them.
385        log_dumped_app_state()
386
387    def _app_monitor_thread_main(self) -> None:
388        try:
389            self._monitor_app()
390        except Exception:
391            logging.exception('Error in AppHealthMonitor thread.')
392
393    def _set_response(self) -> None:
394        assert _babase.in_logic_thread()
395        self._response = True
396
397    def _check_running(self) -> bool:
398        # Workaround for the fact that mypy assumes _running
399        # doesn't change during the course of a function.
400        return self._running
401
402    def _monitor_app(self) -> None:
403        import time
404
405        while bool(True):
406            # Always sleep a bit between checks.
407            time.sleep(1.234)
408
409            # Do nothing while backgrounded.
410            while not self._running:
411                time.sleep(2.3456)
412
413            # Wait for the logic thread to run something we send it.
414            starttime = time.monotonic()
415            self._response = False
416            _babase.pushcall(self._set_response, raw=True)
417            while not self._response:
418                # Abort this check if we went into the background.
419                if not self._check_running():
420                    break
421
422                # Wait a bit longer the first time through since the app
423                # could still be starting up; we generally don't want to
424                # report that.
425                threshold = 10 if self._first_check else 5
426
427                # If we've been waiting too long (and the app is running)
428                # dump the app state and bail. Make an exception for the
429                # first check though since the app could just be taking
430                # a while to get going; we don't want to report that.
431                duration = time.monotonic() - starttime
432                if duration > threshold:
433                    dump_app_state(
434                        reason=f'Logic thread unresponsive'
435                        f' for {threshold} seconds.'
436                    )
437
438                    # We just do one alert for now.
439                    return
440
441                time.sleep(1.042)
442
443            self._first_check = False
444
445    def on_app_pause(self) -> None:
446        assert _babase.in_logic_thread()
447        self._running = False
448
449    def on_app_resume(self) -> None:
450        assert _babase.in_logic_thread()
451        self._running = True

Logs things like app-not-responding issues.

def on_app_loading(self) -> None:
383    def on_app_loading(self) -> None:
384        # If any traceback dumps happened last run, log and clear them.
385        log_dumped_app_state()

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_pause(self) -> None:
445    def on_app_pause(self) -> None:
446        assert _babase.in_logic_thread()
447        self._running = False

Called when the app enters the paused state.

def on_app_resume(self) -> None:
449    def on_app_resume(self) -> None:
450        assert _babase.in_logic_thread()
451        self._running = True

Called when the app exits the paused 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        return cls._supports_intent(intent)
36
37    @classmethod
38    def _supports_intent(cls, intent: AppIntent) -> bool:
39        """Return whether our mode can handle the provided intent.
40
41        AppModes should override this to define what they can handle.
42        Note that AppExperience does not have to be considered here; that
43        is handled automatically by the can_handle_intent() call."""
44        raise NotImplementedError('AppMode subclasses must override this.')
45
46    def handle_intent(self, intent: AppIntent) -> None:
47        """Handle an intent."""
48        raise NotImplementedError('AppMode subclasses must override this.')
49
50    def on_activate(self) -> None:
51        """Called when the mode is being activated."""
52
53    def on_deactivate(self) -> None:
54        """Called when the mode is being deactivated."""

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        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:
46    def handle_intent(self, intent: AppIntent) -> None:
47        """Handle an intent."""
48        raise NotImplementedError('AppMode subclasses must override this.')

Handle an intent.

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

Called when the mode is being activated.

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

Called when the mode is being deactivated.

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_pause(self) -> None:
44        """Called when the app enters the paused state."""
45
46    def on_app_resume(self) -> None:
47        """Called when the app exits the paused state."""
48
49    def on_app_shutdown(self) -> None:
50        """Called when the app is shutting down."""
51
52    def on_app_shutdown_complete(self) -> None:
53        """Called when the app is done 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_pause(self) -> None:
43    def on_app_pause(self) -> None:
44        """Called when the app enters the paused state."""

Called when the app enters the paused state.

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

Called when the app exits the paused state.

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

Called when the app is shutting down.

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

Called when the app is done 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:
539def apptime() -> babase.AppTime:
540    """Return the current app-time in seconds.
541
542    Category: **General Utility Functions**
543
544    App-time is a monotonic time value; it starts at 0.0 when the app
545    launches and will never jump by large amounts or go backwards, even if
546    the system time changes. Its progression will pause when the app is in
547    a suspended state.
548
549    Note that the AppTime returned here is simply float; it just has a
550    unique type in the type-checker's eyes to help prevent it from being
551    accidentally used with time functionality expecting other time types.
552    """
553    import babase  # pylint: disable=cyclic-import
554
555    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:
558def apptimer(time: float, call: Callable[[], Any]) -> None:
559    """Schedule a callable object to run based on app-time.
560
561    Category: **General Utility Functions**
562
563    This function creates a one-off timer which cannot be canceled or
564    modified once created. If you require the ability to do so, or need
565    a repeating timer, use the babase.AppTimer class instead.
566
567    ##### Arguments
568    ###### time (float)
569    > Length of time in seconds that the timer will wait before firing.
570
571    ###### call (Callable[[], Any])
572    > A callable Python object. Note that the timer will retain a
573    strong reference to the callable for as long as the timer exists, so you
574    may want to look into concepts such as babase.WeakCall if that is not
575    desired.
576
577    ##### Examples
578    Print some stuff through time:
579    >>> babase.screenmessage('hello from now!')
580    >>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
581                              'hello from the future!'))
582    >>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
583    ...                       'hello from the future 2!'))
584    """
585    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:
53class AppTimer:
54    """Timers are used to run code at later points in time.
55
56    Category: **General Utility Classes**
57
58    This class encapsulates a timer based on app-time.
59    The underlying timer will be destroyed when this object is no longer
60    referenced. If you do not want to worry about keeping a reference to
61    your timer around, use the babase.apptimer() function instead to get a
62    one-off timer.
63
64    ##### Arguments
65    ###### time
66    > Length of time in seconds that the timer will wait before firing.
67
68    ###### call
69    > A callable Python object. Remember that the timer will retain a
70    strong reference to the callable for as long as it exists, so you
71    may want to look into concepts such as babase.WeakCall if that is not
72    desired.
73
74    ###### repeat
75    > If True, the timer will fire repeatedly, with each successive
76    firing having the same delay as the first.
77
78    ##### Example
79
80    Use a Timer object to print repeatedly for a few seconds:
81    ... def say_it():
82    ...     babase.screenmessage('BADGER!')
83    ... def stop_saying_it():
84    ...     global g_timer
85    ...     g_timer = None
86    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
87    ... # Create our timer; it will run as long as we have the self.t ref.
88    ... g_timer = babase.AppTimer(0.3, say_it, repeat=True)
89    ... # Now fire off a one-shot timer to kill it.
90    ... babase.apptimer(3.89, stop_saying_it)
91    """
92
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass

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

Category: General Utility Classes

This class encapsulates a timer based on app-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the 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)
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass
Call = <class 'babase._general._Call'>
def charstr(char_id: SpecialChar) -> str:
598def charstr(char_id: babase.SpecialChar) -> str:
599    """Get a unicode string representing a special character.
600
601    Category: **General Utility Functions**
602
603    Note that these utilize the private-use block of unicode characters
604    (U+E000-U+F8FF) and are specific to the game; exporting or rendering
605    them elsewhere will be meaningless.
606
607    See babase.SpecialChar for the list of available characters.
608    """
609    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:
612def clipboard_get_text() -> str:
613    """Return text currently on the system clipboard.
614
615    Category: **General Utility Functions**
616
617    Ensure that babase.clipboard_has_text() returns True before calling
618     this function.
619    """
620    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:
623def clipboard_has_text() -> bool:
624    """Return whether there is currently text on the clipboard.
625
626    Category: **General Utility Functions**
627
628    This will return False if no system clipboard is available; no need
629     to call babase.clipboard_is_supported() separately.
630    """
631    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:
634def clipboard_is_supported() -> bool:
635    """Return whether this platform supports clipboard operations at all.
636
637    Category: **General Utility Functions**
638
639    If this returns False, UIs should not show 'copy to clipboard'
640    buttons, etc.
641    """
642    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:
645def clipboard_set_text(value: str) -> None:
646    """Copy a string to the system clipboard.
647
648    Category: **General Utility Functions**
649
650    Ensure that babase.clipboard_is_supported() returns True before adding
651     buttons/etc. that make use of this functionality.
652    """
653    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 CloudSubsystem(babase.AppSubsystem):
 27class CloudSubsystem(AppSubsystem):
 28    """Manages communication with cloud components."""
 29
 30    def is_connected(self) -> bool:
 31        """Return whether a connection to the cloud is present.
 32
 33        This is a good indicator (though not for certain) that sending
 34        messages will succeed.
 35        """
 36        return False  # Needs to be overridden
 37
 38    def on_connectivity_changed(self, connected: bool) -> None:
 39        """Called when cloud connectivity state changes."""
 40        if DEBUG_LOG:
 41            logging.debug('CloudSubsystem: Connectivity is now %s.', connected)
 42
 43        plus = _babase.app.plus
 44        assert plus is not None
 45
 46        # Inform things that use this.
 47        # (TODO: should generalize this into some sort of registration system)
 48        plus.accounts.on_cloud_connectivity_changed(connected)
 49
 50    @overload
 51    def send_message_cb(
 52        self,
 53        msg: bacommon.cloud.LoginProxyRequestMessage,
 54        on_response: Callable[
 55            [bacommon.cloud.LoginProxyRequestResponse | Exception], None
 56        ],
 57    ) -> None:
 58        ...
 59
 60    @overload
 61    def send_message_cb(
 62        self,
 63        msg: bacommon.cloud.LoginProxyStateQueryMessage,
 64        on_response: Callable[
 65            [bacommon.cloud.LoginProxyStateQueryResponse | Exception], None
 66        ],
 67    ) -> None:
 68        ...
 69
 70    @overload
 71    def send_message_cb(
 72        self,
 73        msg: bacommon.cloud.LoginProxyCompleteMessage,
 74        on_response: Callable[[None | Exception], None],
 75    ) -> None:
 76        ...
 77
 78    @overload
 79    def send_message_cb(
 80        self,
 81        msg: bacommon.cloud.PingMessage,
 82        on_response: Callable[[bacommon.cloud.PingResponse | Exception], None],
 83    ) -> None:
 84        ...
 85
 86    @overload
 87    def send_message_cb(
 88        self,
 89        msg: bacommon.cloud.SignInMessage,
 90        on_response: Callable[
 91            [bacommon.cloud.SignInResponse | Exception], None
 92        ],
 93    ) -> None:
 94        ...
 95
 96    @overload
 97    def send_message_cb(
 98        self,
 99        msg: bacommon.cloud.ManageAccountMessage,
100        on_response: Callable[
101            [bacommon.cloud.ManageAccountResponse | Exception], None
102        ],
103    ) -> None:
104        ...
105
106    def send_message_cb(
107        self,
108        msg: Message,
109        on_response: Callable[[Any], None],
110    ) -> None:
111        """Asynchronously send a message to the cloud from the logic thread.
112
113        The provided on_response call will be run in the logic thread
114        and passed either the response or the error that occurred.
115        """
116        from babase._general import Call
117
118        del msg  # Unused.
119
120        _babase.pushcall(
121            Call(
122                on_response,
123                RuntimeError('Cloud functionality is not available.'),
124            )
125        )
126
127    @overload
128    def send_message(
129        self, msg: bacommon.cloud.WorkspaceFetchMessage
130    ) -> bacommon.cloud.WorkspaceFetchResponse:
131        ...
132
133    @overload
134    def send_message(
135        self, msg: bacommon.cloud.MerchAvailabilityMessage
136    ) -> bacommon.cloud.MerchAvailabilityResponse:
137        ...
138
139    @overload
140    def send_message(
141        self, msg: bacommon.cloud.TestMessage
142    ) -> bacommon.cloud.TestResponse:
143        ...
144
145    def send_message(self, msg: Message) -> Response | None:
146        """Synchronously send a message to the cloud.
147
148        Must be called from a background thread.
149        """
150        raise RuntimeError('Cloud functionality is not available.')

Manages communication with cloud components.

def is_connected(self) -> bool:
30    def is_connected(self) -> bool:
31        """Return whether a connection to the cloud is present.
32
33        This is a good indicator (though not for certain) that sending
34        messages will succeed.
35        """
36        return False  # Needs to be overridden

Return whether a connection to the cloud is present.

This is a good indicator (though not for certain) that sending messages will succeed.

def on_connectivity_changed(self, connected: bool) -> None:
38    def on_connectivity_changed(self, connected: bool) -> None:
39        """Called when cloud connectivity state changes."""
40        if DEBUG_LOG:
41            logging.debug('CloudSubsystem: Connectivity is now %s.', connected)
42
43        plus = _babase.app.plus
44        assert plus is not None
45
46        # Inform things that use this.
47        # (TODO: should generalize this into some sort of registration system)
48        plus.accounts.on_cloud_connectivity_changed(connected)

Called when cloud connectivity state changes.

def send_message_cb( self, msg: efro.message._message.Message, on_response: Callable[[Any], NoneType]) -> None:
106    def send_message_cb(
107        self,
108        msg: Message,
109        on_response: Callable[[Any], None],
110    ) -> None:
111        """Asynchronously send a message to the cloud from the logic thread.
112
113        The provided on_response call will be run in the logic thread
114        and passed either the response or the error that occurred.
115        """
116        from babase._general import Call
117
118        del msg  # Unused.
119
120        _babase.pushcall(
121            Call(
122                on_response,
123                RuntimeError('Cloud functionality is not available.'),
124            )
125        )

Asynchronously send a message to the cloud from the logic thread.

The provided on_response call will be run in the logic thread and passed either the response or the error that occurred.

def send_message( self, msg: efro.message._message.Message) -> efro.message._message.Response | None:
145    def send_message(self, msg: Message) -> Response | None:
146        """Synchronously send a message to the cloud.
147
148        Must be called from a background thread.
149        """
150        raise RuntimeError('Cloud functionality is not available.')

Synchronously send a message to the cloud.

Must be called from a background thread.

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

A context-preserving callable.

Category: General Utility Classes

A ContextCall wraps a callable object along with a reference to the current context (see 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)
140    def __init__(self, call: Callable) -> None:
141        pass
class ContextError(builtins.Exception):
16class ContextError(Exception):
17    """Exception raised when a call is made in an invalid context.
18
19    Category: **Exception Classes**
20
21    Examples of this include calling UI functions within an Activity context
22    or calling scene manipulation functions outside of a game context.
23    """

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

Category: Exception Classes

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

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

Store or use a ballistica context.

Category: General Utility Classes

Many operations such as bascenev1.newnode() or bascenev1.gettexture() operate implicitly on a current 'context'. A context is some sort of state that functionality can implicitly use. Context determines, for example, which scene nodes or textures get added to without having to specify it explicitly in the newnode()/gettexture() call. Contexts can also affect object lifecycles; for example a 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:
200    @classmethod
201    def empty(cls) -> ContextRef:
202        """Return a ContextRef pointing to no context.
203
204        This is useful when code should be run free of a context.
205        For example, UI code generally insists on being run this way.
206        Otherwise, callbacks set on the UI could inadvertently stop working
207        due to a game activity ending, which would be unintuitive behavior.
208        """
209        return ContextRef()

Return a ContextRef pointing to no context.

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

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

Whether the context was created as empty.

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

Whether the context has expired.

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

Exception raised when an expected delegate object does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
DisplayTime = DisplayTime
def displaytime() -> DisplayTime:
694def displaytime() -> babase.DisplayTime:
695    """Return the current display-time in seconds.
696
697    Category: **General Utility Functions**
698
699    Display-time is a time value intended to be used for animation and other
700    visual purposes. It will generally increment by a consistent amount each
701    frame. It will pass at an overall similar rate to AppTime, but trades
702    accuracy for smoothness.
703
704    Note that the value returned here is simply a float; it just has a
705    unique type in the type-checker's eyes to help prevent it from being
706    accidentally used with time functionality expecting other time types.
707    """
708    import babase  # pylint: disable=cyclic-import
709
710    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:
713def displaytimer(time: float, call: Callable[[], Any]) -> None:
714    """Schedule a callable object to run based on display-time.
715
716    Category: **General Utility Functions**
717
718    This function creates a one-off timer which cannot be canceled or
719    modified once created. If you require the ability to do so, or need
720    a repeating timer, use the babase.DisplayTimer class instead.
721
722    Display-time is a time value intended to be used for animation and other
723    visual purposes. It will generally increment by a consistent amount each
724    frame. It will pass at an overall similar rate to AppTime, but trades
725    accuracy for smoothness.
726
727    ##### Arguments
728    ###### time (float)
729    > Length of time in seconds that the timer will wait before firing.
730
731    ###### call (Callable[[], Any])
732    > A callable Python object. Note that the timer will retain a
733    strong reference to the callable for as long as the timer exists, so you
734    may want to look into concepts such as babase.WeakCall if that is not
735    desired.
736
737    ##### Examples
738    Print some stuff through time:
739    >>> babase.screenmessage('hello from now!')
740    >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
741    ...                       'hello from the future!'))
742    >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
743    ...                       'hello from the future 2!'))
744    """
745    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:
220class DisplayTimer:
221    """Timers are used to run code at later points in time.
222
223    Category: **General Utility Classes**
224
225    This class encapsulates a timer based on display-time.
226    The underlying timer will be destroyed when this object is no longer
227    referenced. If you do not want to worry about keeping a reference to
228    your timer around, use the babase.displaytimer() function instead to get a
229    one-off timer.
230
231    Display-time is a time value intended to be used for animation and
232    other visual purposes. It will generally increment by a consistent
233    amount each frame. It will pass at an overall similar rate to AppTime,
234    but trades accuracy for smoothness.
235
236    ##### Arguments
237    ###### time
238    > Length of time in seconds that the timer will wait before firing.
239
240    ###### call
241    > A callable Python object. Remember that the timer will retain a
242    strong reference to the callable for as long as it exists, so you
243    may want to look into concepts such as babase.WeakCall if that is not
244    desired.
245
246    ###### repeat
247    > If True, the timer will fire repeatedly, with each successive
248    firing having the same delay as the first.
249
250    ##### Example
251
252    Use a Timer object to print repeatedly for a few seconds:
253    ... def say_it():
254    ...     babase.screenmessage('BADGER!')
255    ... def stop_saying_it():
256    ...     global g_timer
257    ...     g_timer = None
258    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
259    ... # Create our timer; it will run as long as we have the self.t ref.
260    ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True)
261    ... # Now fire off a one-shot timer to kill it.
262    ... babase.displaytimer(3.89, stop_saying_it)
263    """
264
265    def __init__(
266        self, time: float, call: Callable[[], Any], repeat: bool = False
267    ) -> None:
268        pass

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

Category: General Utility Classes

This class encapsulates a timer based on display-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the 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)
265    def __init__(
266        self, time: float, call: Callable[[], Any], repeat: bool = False
267    ) -> None:
268        pass
def do_once() -> bool:
753def do_once() -> bool:
754    """Return whether this is the first time running a line of code.
755
756    Category: **General Utility Functions**
757
758    This is used by 'print_once()' type calls to keep from overflowing
759    logs. The call functions by registering the filename and line where
760    The call is made from.  Returns True if this location has not been
761    registered already, and False if it has.
762
763    ##### Example
764    This print will only fire for the first loop iteration:
765    >>> for i in range(10):
766    ... if babase.do_once():
767    ...     print('HelloWorld once from loop!')
768    """
769    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):
19class EmptyAppMode(AppMode):
20    """An empty app mode that can be used as a fallback/etc."""
21
22    @classmethod
23    def get_app_experience(cls) -> AppExperience:
24        return AppExperience.EMPTY
25
26    @classmethod
27    def _supports_intent(cls, intent: AppIntent) -> bool:
28        # We support default and exec intents currently.
29        return isinstance(intent, AppIntentExec | AppIntentDefault)
30
31    def handle_intent(self, intent: AppIntent) -> None:
32        if isinstance(intent, AppIntentExec):
33            _babase.empty_app_mode_handle_intent_exec(intent.code)
34            return
35        assert isinstance(intent, AppIntentDefault)
36        _babase.empty_app_mode_handle_intent_default()
37
38    def on_activate(self) -> None:
39        # Let the native layer do its thing.
40        _babase.on_empty_app_mode_activate()
41
42    def on_deactivate(self) -> None:
43        # Let the native layer do its thing.
44        _babase.on_empty_app_mode_deactivate()

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

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

Return the overall experience provided by this mode.

def handle_intent(self, intent: AppIntent) -> None:
31    def handle_intent(self, intent: AppIntent) -> None:
32        if isinstance(intent, AppIntentExec):
33            _babase.empty_app_mode_handle_intent_exec(intent.code)
34            return
35        assert isinstance(intent, AppIntentDefault)
36        _babase.empty_app_mode_handle_intent_default()

Handle an intent.

def on_activate(self) -> None:
38    def on_activate(self) -> None:
39        # Let the native layer do its thing.
40        _babase.on_empty_app_mode_activate()

Called when the mode is being activated.

def on_deactivate(self) -> None:
42    def on_deactivate(self) -> None:
43        # Let the native layer do its thing.
44        _babase.on_empty_app_mode_deactivate()

Called when the mode is being deactivated.

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

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

android: bool

Is this build targeting an Android based OS?

api_version: int

The app's api version.

Only Python modules and packages associated with the current API version number will be detected by the game (see the ba_meta tag). This value will change whenever substantial backward-incompatible changes are introduced to Ballistica APIs. When that happens, modules/packages should be updated accordingly and set to target the newer API version number.

arcade: bool

Whether the app is targeting an arcade-centric experience.

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):
36class Existable(Protocol):
37    """A Protocol for objects supporting an exists() method.
38
39    Category: **Protocols**
40    """
41
42    def exists(self) -> bool:
43        """Whether this object exists."""

A Protocol for objects supporting an exists() method.

Category: Protocols

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

Whether this object exists.

def existing(obj: Optional[~ExistableT]) -> Optional[~ExistableT]:
50def existing(obj: ExistableT | None) -> ExistableT | None:
51    """Convert invalid references to None for any babase.Existable object.
52
53    Category: **Gameplay Functions**
54
55    To best support type checking, it is important that invalid references
56    not be passed around and instead get converted to values of None.
57    That way the type checker can properly flag attempts to pass possibly-dead
58    objects (FooType | None) into functions expecting only live ones
59    (FooType), etc. This call can be used on any 'existable' object
60    (one with an exists() method) and will convert it to a None value
61    if it does not exist.
62
63    For more info, see notes on 'existables' here:
64    https://ballistica.net/wiki/Coding-Style-Guide
65    """
66    assert obj is None or hasattr(obj, 'exists'), f'No "exists" on {obj}'
67    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:
821def fatal_error(message: str) -> None:
822    """Trigger a fatal error. Use this in situations where it is not possible
823    for the engine to continue on in a useful way. This can sometimes
824    help provide more clear information at the exact source of a problem
825    as compared to raising an Exception. In the vast majority of cases,
826    however, Exceptions should be preferred.
827    """
828    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:
211def garbage_collect() -> None:
212    """Run an explicit pass of garbage collection.
213
214    category: General Utility Functions
215
216    May also print warnings/etc. if collection takes too long or if
217    uncollectible objects are found (so use this instead of simply
218    gc.collect().
219    """
220    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_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:
107def get_type_name(cls: type) -> str:
108    """Return a full type name including module for a class."""
109    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]:
70def getclass(name: str, subclassof: type[T]) -> type[T]:
71    """Given a full class name such as foo.bar.MyClass, return the class.
72
73    Category: **General Utility Functions**
74
75    The class will be checked to make sure it is a subclass of the provided
76    'subclassof' class, and a TypeError will be raised if not.
77    """
78    import importlib
79
80    splits = name.split('.')
81    modulename = '.'.join(splits[:-1])
82    classname = splits[-1]
83    module = importlib.import_module(modulename)
84    cls: type = getattr(module, classname)
85
86    if not issubclass(cls, subclassof):
87        raise TypeError(f'{name} is not a subclass of {subclassof}.')
88    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:
149def handle_leftover_v1_cloud_log_file() -> None:
150    """Handle an un-uploaded v1-cloud-log from a previous run."""
151
152    # Only applies with classic present.
153    if _babase.app.classic is None:
154        return
155    try:
156        import json
157
158        if os.path.exists(_babase.get_v1_cloud_log_file_path()):
159            with open(
160                _babase.get_v1_cloud_log_file_path(), encoding='utf-8'
161            ) as infile:
162                info = json.loads(infile.read())
163            infile.close()
164            do_send = should_submit_debug_info()
165            if do_send:
166
167                def response(data: Any) -> None:
168                    # Non-None response means we were successful;
169                    # lets kill it.
170                    if data is not None:
171                        try:
172                            os.remove(_babase.get_v1_cloud_log_file_path())
173                        except FileNotFoundError:
174                            # Saw this in the wild. The file just existed
175                            # a moment ago but I suppose something could have
176                            # killed it since. ¯\_(ツ)_/¯
177                            pass
178
179                _babase.app.classic.master_server_v1_post(
180                    'bsLog', info, response
181                )
182            else:
183                # If they don't want logs uploaded just kill it.
184                os.remove(_babase.get_v1_cloud_log_file_path())
185    except Exception:
186        from babase import _error
187
188        _error.print_exception('Error handling leftover log file.')

Handle an un-uploaded v1-cloud-log from a previous run.

class InputDeviceNotFoundError(babase.NotFoundError):
103class InputDeviceNotFoundError(NotFoundError):
104    """Exception raised when an expected input-device does not exist.
105
106    Category: **Exception Classes**
107    """

Exception raised when an expected input-device does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
class InputType(enum.Enum):
 8class InputType(Enum):
 9    """Types of input a controller can send to the game.
10
11    Category: Enums
12
13    """
14
15    UP_DOWN = 2
16    LEFT_RIGHT = 3
17    JUMP_PRESS = 4
18    JUMP_RELEASE = 5
19    PUNCH_PRESS = 6
20    PUNCH_RELEASE = 7
21    BOMB_PRESS = 8
22    BOMB_RELEASE = 9
23    PICK_UP_PRESS = 10
24    PICK_UP_RELEASE = 11
25    RUN = 12
26    FLY_PRESS = 13
27    FLY_RELEASE = 14
28    START_PRESS = 15
29    START_RELEASE = 16
30    HOLD_POSITION_PRESS = 17
31    HOLD_POSITION_RELEASE = 18
32    LEFT_PRESS = 19
33    LEFT_RELEASE = 20
34    RIGHT_PRESS = 21
35    RIGHT_RELEASE = 22
36    UP_PRESS = 23
37    UP_RELEASE = 24
38    DOWN_PRESS = 25
39    DOWN_RELEASE = 26

Types of input a controller can send to the game.

Category: Enums

UP_DOWN = <InputType.UP_DOWN: 2>
LEFT_RIGHT = <InputType.LEFT_RIGHT: 3>
JUMP_PRESS = <InputType.JUMP_PRESS: 4>
JUMP_RELEASE = <InputType.JUMP_RELEASE: 5>
PUNCH_PRESS = <InputType.PUNCH_PRESS: 6>
PUNCH_RELEASE = <InputType.PUNCH_RELEASE: 7>
BOMB_PRESS = <InputType.BOMB_PRESS: 8>
BOMB_RELEASE = <InputType.BOMB_RELEASE: 9>
PICK_UP_PRESS = <InputType.PICK_UP_PRESS: 10>
PICK_UP_RELEASE = <InputType.PICK_UP_RELEASE: 11>
RUN = <InputType.RUN: 12>
FLY_PRESS = <InputType.FLY_PRESS: 13>
FLY_RELEASE = <InputType.FLY_RELEASE: 14>
START_PRESS = <InputType.START_PRESS: 15>
START_RELEASE = <InputType.START_RELEASE: 16>
HOLD_POSITION_PRESS = <InputType.HOLD_POSITION_PRESS: 17>
HOLD_POSITION_RELEASE = <InputType.HOLD_POSITION_RELEASE: 18>
LEFT_PRESS = <InputType.LEFT_PRESS: 19>
LEFT_RELEASE = <InputType.LEFT_RELEASE: 20>
RIGHT_PRESS = <InputType.RIGHT_PRESS: 21>
RIGHT_RELEASE = <InputType.RIGHT_RELEASE: 22>
UP_PRESS = <InputType.UP_PRESS: 23>
UP_RELEASE = <InputType.UP_RELEASE: 24>
DOWN_PRESS = <InputType.DOWN_PRESS: 25>
DOWN_RELEASE = <InputType.DOWN_RELEASE: 26>
Inherited Members
enum.Enum
name
value
def is_browser_likely_available() -> bool:
26def is_browser_likely_available() -> bool:
27    """Return whether a browser likely exists on the current device.
28
29    category: General Utility Functions
30
31    If this returns False you may want to avoid calling babase.show_url()
32    with any lengthy addresses. (ba.show_url() will display an address
33    as a string in a window if unable to bring up a browser, but that
34    is only useful for simple URLs.)
35    """
36    app = _babase.app
37
38    if app.classic is None:
39        logging.warning(
40            'is_browser_likely_available() needs to be updated'
41            ' to work without classic.'
42        )
43        return True
44
45    platform = app.classic.platform
46    hastouchscreen = _babase.hastouchscreen()
47
48    # If we're on a vr device or an android device with no touchscreen,
49    # assume no browser.
50    # FIXME: Might not be the case anymore; should make this definable
51    #  at the platform level.
52    if app.env.vr or (platform == 'android' and not hastouchscreen):
53        return False
54
55    # Anywhere else assume we've got one.
56    return True

Return whether a browser likely exists on the current device.

category: General Utility Functions

If this returns False you may want to avoid calling babase.show_url() with any lengthy addresses. (ba.show_url() will display an address as a string in a window if unable to bring up a browser, but that is only useful for simple URLs.)

def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool:
38def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool:
39    """Return whether a given point is within a given box.
40
41    category: General Utility Functions
42
43    For use with standard def boxes (position|rotate|scale).
44    """
45    return (
46        (abs(pnt[0] - box[0]) <= box[6] * 0.5)
47        and (abs(pnt[1] - box[1]) <= box[7] * 0.5)
48        and (abs(pnt[2] - box[2]) <= box[8] * 0.5)
49    )

Return whether a given point is within a given box.

category: General Utility Functions

For use with standard def boxes (position|rotate|scale).

class Keyboard:
14class Keyboard:
15    """Chars definitions for on-screen keyboard.
16
17    Category: **App Classes**
18
19    Keyboards are discoverable by the meta-tag system
20    and the user can select which one they want to use.
21    On-screen keyboard uses chars from active babase.Keyboard.
22    """
23
24    name: str
25    """Displays when user selecting this keyboard."""
26
27    chars: list[tuple[str, ...]]
28    """Used for row/column lengths."""
29
30    pages: dict[str, tuple[str, ...]]
31    """Extra chars like emojis."""
32
33    nums: tuple[str, ...]
34    """The 'num' page."""

Chars definitions for on-screen keyboard.

Category: App Classes

Keyboards are discoverable by the meta-tag system and the user can select which one they want to use. On-screen keyboard uses chars from active babase.Keyboard.

name: str

Displays when user selecting this keyboard.

chars: list[tuple[str, ...]]

Used for row/column lengths.

pages: dict[str, tuple[str, ...]]

Extra chars like emojis.

nums: tuple[str, ...]

The 'num' page.

class LanguageSubsystem(babase.AppSubsystem):
 21class LanguageSubsystem(AppSubsystem):
 22    """Language functionality for the app.
 23
 24    Category: **App Classes**
 25
 26    Access the single instance of this class at 'babase.app.lang'.
 27    """
 28
 29    def __init__(self) -> None:
 30        super().__init__()
 31        self.default_language: str = self._get_default_language()
 32
 33        self._language: str | None = None
 34        self._language_target: AttrDict | None = None
 35        self._language_merged: AttrDict | None = None
 36
 37    @property
 38    def locale(self) -> str:
 39        """Raw country/language code detected by the game (such as 'en_US').
 40
 41        Generally for language-specific code you should look at
 42        babase.App.language, which is the language the game is using
 43        (which may differ from locale if the user sets a language, etc.)
 44        """
 45        env = _babase.env()
 46        assert isinstance(env['locale'], str)
 47        return env['locale']
 48
 49    @property
 50    def language(self) -> str:
 51        """The current active language for the app.
 52
 53        This can be selected explicitly by the user or may be set
 54        automatically based on locale or other factors.
 55        """
 56        if self._language is None:
 57            raise RuntimeError('App language is not yet set.')
 58        return self._language
 59
 60    @property
 61    def available_languages(self) -> list[str]:
 62        """A list of all availab