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
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.
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'.
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.
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.
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.
84 def do_get_primary(self) -> AccountV2Handle | None: 85 """Internal - should be overridden by subclass.""" 86 return None
Internal - should be overridden by subclass.
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.
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.
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).
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).
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.
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.
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.
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
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
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.
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__.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
Handle a deep link URL.
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.
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.
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
.
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.
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.
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.
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.
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.
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.
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.
Inherited Members
- enum.Enum
- name
- value
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
Inherited Members
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
Tells the app to simply run in its default mode.
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.
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
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.
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.
46 def handle_intent(self, intent: AppIntent) -> None: 47 """Handle an intent.""" 48 raise NotImplementedError('AppMode subclasses must override this.')
Handle an intent.
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.
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.
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.
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.
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.
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!'))
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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))
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
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()
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.
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
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.
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!'))
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)
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!')
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.
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.
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.
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
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
.
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.
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.
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.
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.
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.
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.
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.
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.
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
.
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
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)
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
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.
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().
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.
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.
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.
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.
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
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
Inherited Members
- enum.Enum
- name
- value
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.)
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).
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.
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