From 183c5abf6024b786483276b2054d6735d91f8ef4 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 Sep 2023 17:40:39 -0700 Subject: [PATCH] Initial work on AppExperiences --- .efrocachemap | 56 ++++++------- CHANGELOG.md | 23 +++--- src/assets/.asset_manifest_public.json | 2 + src/assets/Makefile | 2 + .../ba_data/python/babase/_accountv2.py | 4 +- src/assets/ba_data/python/babase/_app.py | 78 ++++++++++++++----- .../ba_data/python/babase/_appconfig.py | 36 ++++----- src/assets/ba_data/python/babase/_appmode.py | 30 +++++-- .../ba_data/python/babase/_appmodeselector.py | 8 +- .../ba_data/python/babase/_emptyappmode.py | 12 ++- src/assets/ba_data/python/baenv.py | 2 +- .../ba_data/python/bascenev1/_appmode.py | 12 ++- src/assets/ba_data/python/bascenev1/_map.py | 2 +- .../ba_data/python/batemplatefs/_hooks.py | 6 +- src/ballistica/base/graphics/graphics.cc | 66 ++++++---------- src/ballistica/base/graphics/graphics.h | 3 +- .../base/python/methods/python_methods_app.cc | 32 ++++---- .../python/methods/python_methods_graphics.cc | 3 +- .../python/methods/python_methods_scene.cc | 32 ++++---- src/ballistica/shared/ballistica.cc | 2 +- tools/bacommon/app.py | 35 +++++++++ tools/batools/appmodule.py | 4 +- 22 files changed, 262 insertions(+), 188 deletions(-) create mode 100644 tools/bacommon/app.py diff --git a/.efrocachemap b/.efrocachemap index 94ca4119..83e3f67b 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -4064,26 +4064,26 @@ "build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1", "build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", - "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "95166586256dc94f679d3f15cf570ed8", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "820f8a69f86434ff6dab9cc23e675708", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "70903ec2f476fbc5908cc52522f69e0c", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "442f2bdde3c176d9a4431c9fdcca892e", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "5354821ede7348ba9bb90bee8fe5ccb7", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "ab0cb59c874beb50e554d3d7c552565e", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "dec2c09bd136aa413b9b94729f7d6d2e", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "a5c43eb8befc810d693ec9c9f83fcba4", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "56bf9b14517f0a579964b39218498fcf", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "d6a10e3a6bc1a07609598f962439c6c8", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "ab5cb01ed4a8a8db80ca65ab5cd0133b", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "231bd6e18145b06028e4f90fd7de106c", - "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "3cc88ed5471e9b7f2b84af4ee58a8061", - "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "4586629e8f114c1b5a22444ae26b5917", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "e1177b377c3e48d60057a9719bf9d0e3", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "518c529a7133d5ea41d730d12ae156c4", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "41e7afae9395843c575c65593aab423c", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "d95e4cc445b722f111e51b3d4b7bdde9", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "d75e3afcc1db962cd483f8b80a65c930", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "2b1b27fc2ca79803d79796a7414db3a2", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "ca001c9f2b4edf8d4918c1bcaf0cb561", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "fcb58aae4535a761be49420f4e4a7711", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "8f453b8193ba06a3cc59374cdd2a1c09", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "7c389cb83619516ce96118227668500e", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "2ffcc486e9ba8f0fd5279083a2e54aff", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "366bab5bff3ff9344e54bf7492f4d7c3", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "78277cc1bd40ba88fd1475f8755e0721", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "5f063e4d45e40ade22f1bad63658752e", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "f7114c78146f22e89143218c9930b43e", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "90c0ea0ce7a35d8f15b2a29bef704819", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "e1b4932fd7f359404c979604a1e33444", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "28ab454f71dfecd1986a245eb4a732e7", + "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "7b1a4db9c3f36fd428cdafbc1459bc1b", + "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "bc5a48ff49975901f212d18b1dbbb6f5", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "7742db7c9555907fc7b2d5c89d48dfd0", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "d81bc55d666f630a14de90f30be29fb4", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "5ab68bfee69a6fe07b5b8a68938220e6", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "59a2b3cb2c1dde98c6aedf72aa9fdbfb", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "2199e800eb102d56197e11782b7adf12", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "3193300cf9927276893960698d8246f6", "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "a3607fd941915ab11503f82acfc392b5", "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "b5a129d83796c9e7015ab5e319d2c22f", "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "a3607fd941915ab11503f82acfc392b5", @@ -4100,14 +4100,14 @@ "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "c5c40967e63471c9c4abd6dfbef892df", "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "d34c0a142e7d391a109a33ea3cc77c08", "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "c5c40967e63471c9c4abd6dfbef892df", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "aa15bc38411a8a4194b3711c9c6ebf56", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "3539534ba129d95e99f89ddf79d14f28", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "75e16f083a18621fe4502bc170e6a696", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "29b9a8543b1612c0451eaaf90647b905", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "4fb61373afaa6ce350601b245fcb1b7a", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "d8e606cdfef72b6170721d87ee1d9b11", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "240b54fa0d4334591c0876a2b72a4663", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "a7887447024ce5570da3f468bad5bbb4", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "13a3bb41dda8cfdb3ffdbc57a0390275", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "042ff1129bab0def6bf90250b5c08a32", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "699ab8e6fd3e8517acac76cd5b40dfbc", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "5b53317f8900cab59288be44b353dffc", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "978ea0ba60c026efd45729cd48c0feba", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "1fb524f11277090440cda18bfc3ee3a0", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "91452d47e879acd0d7fdd06a44fbcfed", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "7e9469f3cac9ab85c5e9d1256679604c", "src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c", "src/assets/ba_data/python/babase/_mgen/enums.py": "f8cd3af311ac63147882590123b78318", "src/ballistica/base/mgen/pyembed/binding_base.inc": "ad347097a38e0d7ede9eb6dec6a80ee9", diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bcb9d98..470270e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,19 @@ -### 1.7.28 (build 21299, api 8, 2023-09-01) +### 1.7.28 (build 21303, api 8, 2023-09-01) - Added some high level functionality for copying and deleting feature-sets to - the `tools/spinoff` tool. For example, to create your own `poo` feature-set, - do `tools/spinoff fset-copy template_fs poo`. Then do `make update` and `make - cmake` to build and run the app, and from within it you should be able to do - `import bapoo` to get at your nice shiny poo feature-set. When you are done - playing, you can do `tools/spinoff fset-delete poo` to blow away any traces of - it. + the `spinoff` tool. For example, to create your own `poo` feature-set based on + the existing `template_fs` one, do `tools/spinoff fset-copy template_fs poo`. + Then do `make update` and `make cmake` to build and run the app, and from + within it you should be able to do `import bapoo` to get at your nice shiny + poo feature-set. When you are done playing around, you can do `tools/spinoff + fset-delete poo` to blow away any traces of it. - Public builds now properly reconstruct the CMakeLists.txt file for project changes. - Efrocache now supports a starter-archive when building server builds. This means that if you do something like `make clean; make - prefab-server-release-build` you should only see a few file downloads - happening instead of hundreds or thousands which would happen before, which - should be significantly faster & more efficient. + prefab-server-release-build` you should see just a few file downloads + happening instead of the hundreds that would happen before, which should be + significantly faster & more efficient. ### 1.7.27 (build 21282, api 8, 2023-08-30) @@ -40,7 +40,8 @@ `build_number`, `device_name`, `config_file_path`, `version`, `debug_build`, `test_build`, `data_directory`, `python_directory_user`, `python_directory_app`, `python_directory_app_site`, `api_version`, `on_tv`, - `vr_mode`. + `vr_mode`, `toolbar_test`, `arcade_mode`, `headless_mode`, `demo_mode`, and + `protocol_version`. - Reverting the Android keyboard changes from 1.7.26, as I've received a few reports of bluetooth game controllers now thinking they are keyboards. I'm thinking I'll have to bite the bullet and implement something that asks the diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json index 86ec633c..d8e531ce 100644 --- a/src/assets/.asset_manifest_public.json +++ b/src/assets/.asset_manifest_public.json @@ -96,6 +96,7 @@ "ba_data/python/baclassic/osmusic.py", "ba_data/python/bacommon/__init__.py", "ba_data/python/bacommon/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/app.cpython-311.opt-1.pyc", "ba_data/python/bacommon/__pycache__/assets.cpython-311.opt-1.pyc", "ba_data/python/bacommon/__pycache__/bacloud.cpython-311.opt-1.pyc", "ba_data/python/bacommon/__pycache__/build.cpython-311.opt-1.pyc", @@ -104,6 +105,7 @@ "ba_data/python/bacommon/__pycache__/net.cpython-311.opt-1.pyc", "ba_data/python/bacommon/__pycache__/servermanager.cpython-311.opt-1.pyc", "ba_data/python/bacommon/__pycache__/transfer.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/app.py", "ba_data/python/bacommon/assets.py", "ba_data/python/bacommon/bacloud.py", "ba_data/python/bacommon/build.py", diff --git a/src/assets/Makefile b/src/assets/Makefile index be1741a5..ff2f78ce 100644 --- a/src/assets/Makefile +++ b/src/assets/Makefile @@ -712,6 +712,7 @@ $(eval $(call make-opt-pyc-target,$(element)))) SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \ $(BUILD_DIR)/ba_data/python/bacommon/__init__.py \ + $(BUILD_DIR)/ba_data/python/bacommon/app.py \ $(BUILD_DIR)/ba_data/python/bacommon/assets.py \ $(BUILD_DIR)/ba_data/python/bacommon/bacloud.py \ $(BUILD_DIR)/ba_data/python/bacommon/build.py \ @@ -746,6 +747,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/app.cpython-311.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/assets.cpython-311.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/bacloud.cpython-311.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/build.cpython-311.opt-1.pyc \ diff --git a/src/assets/ba_data/python/babase/_accountv2.py b/src/assets/ba_data/python/babase/_accountv2.py index 89be37fd..78d138f1 100644 --- a/src/assets/ba_data/python/babase/_accountv2.py +++ b/src/assets/ba_data/python/babase/_accountv2.py @@ -64,7 +64,7 @@ class AccountV2Subsystem: def set_primary_credentials(self, credentials: str | None) -> None: """Set credentials for the primary app account.""" - raise RuntimeError('This should be overridden.') + raise NotImplementedError('This should be overridden.') def have_primary_credentials(self) -> bool: """Are credentials currently set for the primary app account? @@ -73,7 +73,7 @@ class AccountV2Subsystem: only that they exist. If/when credentials are validated, the 'primary' account handle will be set. """ - raise RuntimeError('This should be overridden.') + raise NotImplementedError('This should be overridden.') @property def primary(self) -> AccountV2Handle | None: diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py index ab519610..339759a0 100644 --- a/src/assets/ba_data/python/babase/_app.py +++ b/src/assets/ba_data/python/babase/_app.py @@ -112,7 +112,9 @@ class App: project. """ - def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]: + def app_mode_for_intent( + self, intent: AppIntent + ) -> type[AppMode] | None: # pylint: disable=cyclic-import # __GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__ @@ -127,10 +129,10 @@ class App: import babase - if bascenev1.SceneV1AppMode.supports_intent(intent): + if bascenev1.SceneV1AppMode.can_handle_intent(intent): return bascenev1.SceneV1AppMode - if babase.EmptyAppMode.supports_intent(intent): + if babase.EmptyAppMode.can_handle_intent(intent): return babase.EmptyAppMode raise RuntimeError(f'No handler found for intent {type(intent)}.') @@ -144,19 +146,13 @@ class App: the single shared instance. """ + # Hack for docs-generation. if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1': return self.env: babase.Env = _babase.Env() - self.state = self.State.NOT_RUNNING - # Controls which app-modes we use for handling given - # app-intents. Plugins can override this to change high level - # app behavior and spinoff projects can change the default - # implementation for the same effect. - self.mode_selector: babase.AppModeSelector | None = None - # Default executor which can be used for misc background # processing. It should also be passed to any additional asyncio # loops we create so that everything shares the same single set @@ -193,6 +189,7 @@ class App: self._pending_intent: AppIntent | None = None self._intent: AppIntent | None = None self._mode: AppMode | None = None + self._mode_selector: babase.AppModeSelector | None = None self._shutdown_task: asyncio.Task[None] | None = None self._shutdown_tasks: list[Coroutine[None, None, None]] = [ self._wait_for_shutdown_suppressions() @@ -201,6 +198,7 @@ class App: def postinit(self) -> None: """Called after we've been inited and assigned to babase.app.""" + # Hack for docs-generation. if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1': return @@ -231,6 +229,31 @@ class App: assert self._config is not None return self._config + @property + def mode_selector(self) -> babase.AppModeSelector: + """Controls which app-modes are used for handling given intents. + + Plugins can override this to change high level app behavior and + spinoff projects can change the default implementation for the + same effect. + """ + if self._mode_selector is None: + raise RuntimeError( + 'mode_selector cannot be used until the app reaches' + ' the running state.' + ) + return self._mode_selector + + @mode_selector.setter + def mode_selector(self, selector: babase.AppModeSelector) -> None: + # Don't allow overriding this until after we've initially set it. + if self._mode_selector is None: + raise RuntimeError( + 'mode_selector cannot be used until the app reaches' + ' the running state.' + ) + self._mode_selector = selector + # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_BEGIN__ # This section generated by batools.appmodule; do not edit. @@ -303,8 +326,8 @@ class App: def run(self) -> None: """Run the app to completion. - Note that this only works on platforms where Ballistica - manages its own event loop. + Note that this only works on builds where Ballistica manages + its own event loop. """ _babase.run_app() @@ -423,7 +446,7 @@ class App: self._update_state() def _set_intent(self, intent: AppIntent) -> None: - # This should be running in a bg thread. + # This should be happening in a bg thread. assert not _babase.in_logic_thread() try: # Ask the selector what app-mode to use for this intent. @@ -431,15 +454,27 @@ class App: raise RuntimeError('No AppModeSelector set.') modetype = self.mode_selector.app_mode_for_intent(intent) - # Make sure the app-mode they return *actually* supports the - # intent. - if not modetype.supports_intent(intent): + # NOTE: Since intents are somewhat high level things, should + # we do some universal thing like a screenmessage saying + # 'The app cannot handle that request' on failure? + + if modetype is None: raise RuntimeError( - f'Intent {intent} is not supported by AppMode class' - f' {modetype}' + f'No app-mode found to handle app-intent' + f' type {type(intent)}.' ) - # Kick back to the logic thread to apply. + # Make sure the app-mode the selector gave us *actually* + # supports the intent. + if not modetype.can_handle_intent(intent): + raise RuntimeError( + f'Intent {intent} cannot be handled by AppMode type' + f' {modetype} (selector {self.mode_selector}' + f' incorrectly thinks that it can be).' + ) + + # Ok; seems legit. Now instantiate the mode if necessary and + # kick back to the logic thread to apply. mode = modetype() _babase.pushcall( tpartial(self._apply_intent, intent, mode), @@ -480,6 +515,9 @@ class App: # Hmm; what should we do in this case?... logging.exception('Error activating app-mode %s.', mode) + # Let the world know when we first have an app-mode; certain + # app stuff such as input processing can proceed at that + # point. if is_initial_mode: _babase.on_initial_app_mode_set() @@ -599,7 +637,7 @@ class App: # Set a default app-mode-selector. Plugins can then override # this if they want in the on_app_running callback below. - self.mode_selector = self.DefaultAppModeSelector() + self._mode_selector = self.DefaultAppModeSelector() # Inform all app subsystems in the same order they were # registered. Operate on a copy here because subsystems can diff --git a/src/assets/ba_data/python/babase/_appconfig.py b/src/assets/ba_data/python/babase/_appconfig.py index 86fda125..f34ff8ab 100644 --- a/src/assets/ba_data/python/babase/_appconfig.py +++ b/src/assets/ba_data/python/babase/_appconfig.py @@ -3,6 +3,7 @@ """Provides the AppConfig class.""" from __future__ import annotations +import logging from typing import TYPE_CHECKING import _babase @@ -120,33 +121,24 @@ def read_app_config() -> tuple[AppConfig, bool]: config = AppConfig() config_file_healthy = True - except Exception as exc: - print( - ( - 'error reading config file at time ' - + str(_babase.apptime()) - + ': \'' - + config_file_path - + '\':\n' - ), - exc, + except Exception: + logging.exception( + "Error reading config file at time %.3f: '%s'.", + _babase.apptime(), + config_file_path, ) # Whenever this happens lets back up the broken one just in case it # gets overwritten accidentally. - print( - ( - 'backing up current config file to \'' - + config_file_path - + ".broken\'" - ) + logging.info( + "Backing up current config file to '%s.broken'", config_file_path ) try: import shutil shutil.copyfile(config_file_path, config_file_path + '.broken') - except Exception as exc2: - print('EXC copying broken config:', exc2) + except Exception: + logging.exception('Error copying broken config.') config = AppConfig() # Now attempt to read one of our 'prev' backup copies. @@ -159,9 +151,9 @@ def read_app_config() -> tuple[AppConfig, bool]: else: config = AppConfig() config_file_healthy = True - print('successfully read backup config.') - except Exception as exc2: - print('EXC reading prev backup config:', exc2) + logging.info('Successfully read backup config.') + except Exception: + logging.exception('Error reading prev backup config.') return config, config_file_healthy @@ -176,7 +168,7 @@ def commit_app_config(force: bool = False) -> None: assert plus is not None if not _babase.app.config_file_healthy and not force: - print( + logging.warning( 'Current config file is broken; ' 'skipping write to avoid losing settings.' ) diff --git a/src/assets/ba_data/python/babase/_appmode.py b/src/assets/ba_data/python/babase/_appmode.py index fd53e126..fcaa77a9 100644 --- a/src/assets/ba_data/python/babase/_appmode.py +++ b/src/assets/ba_data/python/babase/_appmode.py @@ -6,6 +6,7 @@ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: + from bacommon.app import AppExperience from babase._appintent import AppIntent @@ -17,16 +18,33 @@ class AppMode: """ @classmethod - def supports_intent(cls, intent: AppIntent) -> bool: - """Return whether our mode can handle the provided intent.""" - del intent + def get_app_experience(cls) -> AppExperience: + """Return the overall experience provided by this mode.""" + raise NotImplementedError('AppMode subclasses must override this.') - # Say no to everything by default. Let's make mode explicitly - # lay out everything they *do* support. - return False + @classmethod + def can_handle_intent(cls, intent: AppIntent) -> bool: + """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. + """ + return cls._supports_intent(intent) + + @classmethod + def _supports_intent(cls, intent: AppIntent) -> bool: + """Return whether our mode can handle the provided intent. + + AppModes should override this to define what they can handle. + Note that AppExperience does not have to be considered here; that + is handled automatically by the can_handle_intent() call.""" + raise NotImplementedError('AppMode subclasses must override this.') def handle_intent(self, intent: AppIntent) -> None: """Handle an intent.""" + raise NotImplementedError('AppMode subclasses must override this.') def on_activate(self) -> None: """Called when the mode is being activated.""" diff --git a/src/assets/ba_data/python/babase/_appmodeselector.py b/src/assets/ba_data/python/babase/_appmodeselector.py index f89e4fe8..cfdf03ba 100644 --- a/src/assets/ba_data/python/babase/_appmodeselector.py +++ b/src/assets/ba_data/python/babase/_appmodeselector.py @@ -18,15 +18,15 @@ class AppModeSelector: 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 this. + modifying the app's mode-selector. """ - def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]: + def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode] | None: """Given an AppIntent, return the AppMode that should handle it. If None is returned, the AppIntent will be ignored. - This is called in a background thread, so avoid any calls + This may be called in a background thread, so avoid any calls limited to logic thread use/etc. """ - raise RuntimeError('app_mode_for_intent() should be overridden.') + raise NotImplementedError('app_mode_for_intent() should be overridden.') diff --git a/src/assets/ba_data/python/babase/_emptyappmode.py b/src/assets/ba_data/python/babase/_emptyappmode.py index 0456b1ec..f3905b71 100644 --- a/src/assets/ba_data/python/babase/_emptyappmode.py +++ b/src/assets/ba_data/python/babase/_emptyappmode.py @@ -5,6 +5,8 @@ from __future__ import annotations from typing import TYPE_CHECKING +from bacommon.app import AppExperience + import _babase from babase._appmode import AppMode from babase._appintent import AppIntentExec, AppIntentDefault @@ -17,7 +19,11 @@ class EmptyAppMode(AppMode): """An empty app mode that can be used as a fallback/etc.""" @classmethod - def supports_intent(cls, intent: AppIntent) -> bool: + def get_app_experience(cls) -> AppExperience: + return AppExperience.EMPTY + + @classmethod + def _supports_intent(cls, intent: AppIntent) -> bool: # We support default and exec intents currently. return isinstance(intent, AppIntentExec | AppIntentDefault) @@ -30,8 +36,8 @@ class EmptyAppMode(AppMode): def on_activate(self) -> None: # Let the native layer do its thing. - _babase.empty_app_mode_activate() + _babase.on_empty_app_mode_activate() def on_deactivate(self) -> None: # Let the native layer do its thing. - _babase.empty_app_mode_deactivate() + _babase.on_empty_app_mode_deactivate() diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index d7f58102..d5068fca 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -52,7 +52,7 @@ if TYPE_CHECKING: # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 21299 +TARGET_BALLISTICA_BUILD = 21303 TARGET_BALLISTICA_VERSION = '1.7.28' diff --git a/src/assets/ba_data/python/bascenev1/_appmode.py b/src/assets/ba_data/python/bascenev1/_appmode.py index 9988708a..10d1c320 100644 --- a/src/assets/ba_data/python/bascenev1/_appmode.py +++ b/src/assets/ba_data/python/bascenev1/_appmode.py @@ -5,7 +5,9 @@ from __future__ import annotations from typing import TYPE_CHECKING +from bacommon.app import AppExperience from babase import AppMode, AppIntentExec, AppIntentDefault + import _bascenev1 if TYPE_CHECKING: @@ -16,7 +18,11 @@ class SceneV1AppMode(AppMode): """Our app-mode.""" @classmethod - def supports_intent(cls, intent: AppIntent) -> bool: + def get_app_experience(cls) -> AppExperience: + return AppExperience.MELEE + + @classmethod + def _supports_intent(cls, intent: AppIntent) -> bool: # We support default and exec intents currently. return isinstance(intent, AppIntentExec | AppIntentDefault) @@ -29,8 +35,8 @@ class SceneV1AppMode(AppMode): def on_activate(self) -> None: # Let the native layer do its thing. - _bascenev1.app_mode_activate() + _bascenev1.on_app_mode_activate() def on_deactivate(self) -> None: # Let the native layer do its thing. - _bascenev1.app_mode_deactivate() + _bascenev1.on_app_mode_deactivate() diff --git a/src/assets/ba_data/python/bascenev1/_map.py b/src/assets/ba_data/python/bascenev1/_map.py index 89ab5d56..818098fd 100644 --- a/src/assets/ba_data/python/bascenev1/_map.py +++ b/src/assets/ba_data/python/bascenev1/_map.py @@ -371,5 +371,5 @@ def register_map(maptype: type[Map]) -> None: """Register a map class with the game.""" assert babase.app.classic is not None if maptype.name in babase.app.classic.maps: - raise RuntimeError('map "' + maptype.name + '" already registered') + raise RuntimeError(f'Map "{maptype.name}" is already registered.') babase.app.classic.maps[maptype.name] = maptype diff --git a/src/assets/ba_data/python/batemplatefs/_hooks.py b/src/assets/ba_data/python/batemplatefs/_hooks.py index 81e9a49a..f0a9703f 100644 --- a/src/assets/ba_data/python/batemplatefs/_hooks.py +++ b/src/assets/ba_data/python/batemplatefs/_hooks.py @@ -1,12 +1,10 @@ # Released under the MIT License. See LICENSE for details. # -"""Snippets of code for use by the c++ layer.""" - -# (most of these are self-explanatory) -# pylint: disable=missing-function-docstring +"""Snippets of code for use by the native layer.""" from __future__ import annotations def hello_world() -> None: + """The usual example.""" print('HELLO WORLD FROM TemplateFs!') diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc index 0029b232..5a4c1655 100644 --- a/src/ballistica/base/graphics/graphics.cc +++ b/src/ballistica/base/graphics/graphics.cc @@ -991,12 +991,13 @@ void Graphics::ClearFrameDefDeleteList() { } void Graphics::FadeScreen(bool to, millisecs_t time, PyObject* endcall) { + BA_PRECONDITION(g_base->InLogicThread()); // If there's an ourstanding fade-end command, go ahead and run it. // (otherwise, overlapping fades can cause things to get lost) if (fade_end_call_.Exists()) { if (g_buildconfig.debug_build()) { Log(LogLevel::kWarning, - "2 fades overlapping; running first fade-end-call early"); + "2 fades overlapping; running first fade-end-call early."); } fade_end_call_->Schedule(); fade_end_call_.Clear(); @@ -1089,7 +1090,7 @@ void Graphics::ApplyCamera(FrameDef* frame_def) { void Graphics::DrawWorld(FrameDef* frame_def) { assert(!g_core->HeadlessMode()); - // Draw all session contents (nodes, etc.) + // Draw the world. overlay_node_z_depth_ = -0.95f; g_base->app_mode()->DrawWorld(frame_def); g_base->bg_dynamics->Draw(frame_def); @@ -1120,9 +1121,6 @@ void Graphics::BuildAndPushFrameDef() { // layer is fully bootstrapped. BA_PRECONDITION_FATAL(g_base->logic->app_bootstrapping_complete()); - // This should no longer be necessary.. - WaitForRendererToExist(); - millisecs_t app_time_millisecs = g_core->GetAppTimeMillisecs(); // Store how much time this frame_def represents. @@ -1132,8 +1130,9 @@ void Graphics::BuildAndPushFrameDef() { millisecs_t{50}, display_time_millisecs - last_create_frame_def_time_); last_create_frame_def_time_ = display_time_millisecs; - // This probably should not be here. Though I guess we get the most up-to-date - // values possible this way. But it should probably live in g_input. + // This probably should not be here. Though I guess we get the most + // up-to-date values possible this way. But it should probably live in + // g_input. UpdateGyro(app_time_millisecs, elapsed); FrameDef* frame_def = GetEmptyFrameDef(); @@ -1184,9 +1183,9 @@ void Graphics::BuildAndPushFrameDef() { // Draw our light/shadow images to the screen if desired. DrawDebugBuffers(overlay_pass); - // In high-quality modes we draw a screen-quad as a catch-all for blitting - // the world buffer to the screen (other nodes can add their own blitters - // such as distortion shapes which will have priority). + // In high-quality modes we draw a screen-quad as a catch-all for + // blitting the world buffer to the screen (other nodes can add their + // own blitters such as distortion shapes which will have priority). if (frame_def->quality() >= GraphicsQuality::kHigh) { PostProcessComponent c(frame_def->blit_pass()); c.DrawScreenQuad(); @@ -1195,8 +1194,8 @@ void Graphics::BuildAndPushFrameDef() { DrawFades(frame_def, app_time_millisecs); - // Sanity test: If we're in VR, the only reason we should have stuff in the - // flat overlay pass is if there's windows present (we want to avoid + // Sanity test: If we're in VR, the only reason we should have stuff in + // the flat overlay pass is if there's windows present (we want to avoid // drawing/blitting the 2d UI buffer during gameplay for efficiency). if (g_core->IsVRMode()) { if (frame_def->GetOverlayFlatPass()->HasDrawCommands()) { @@ -1219,9 +1218,9 @@ void Graphics::BuildAndPushFrameDef() { frame_def->Finalize(); - // Include all mesh-data loads and unloads that have accumulated up to this - // point the graphics thread will have to handle these before rendering the - // frame_def. + // Include all mesh-data loads and unloads that have accumulated up to + // this point the graphics thread will have to handle these before + // rendering the frame_def. frame_def->set_mesh_data_creates(mesh_data_creates_); mesh_data_creates_.clear(); frame_def->set_mesh_data_destroys(mesh_data_destroys_); @@ -1327,7 +1326,7 @@ void Graphics::UpdateAndDrawProgressBar(FrameDef* frame_def, / static_cast(progress_bar_loads_)); DrawProgressBar(pass, 1.0f); - // If we were drawing a progress bar, see if everything is now loaded.. if + // If we were drawing a progress bar, see if everything is now loaded. If // so, start rendering normally next frame. int count = g_base->assets->GetGraphicalPendingLoadCount(); if (count <= 0) { @@ -1570,8 +1569,8 @@ void Graphics::AddMeshDataCreate(MeshData* d) { assert(g_base->graphics); // Add this to our list of new-mesh-datas. We'll include this with our - // next frame_def to have the graphics thread load before it processes - // the frame_def. + // next frame_def to have the graphics thread load before it processes the + // frame_def. mesh_data_creates_.push_back(d); } @@ -1580,8 +1579,8 @@ void Graphics::AddMeshDataDestroy(MeshData* d) { assert(g_base->graphics); // Add this to our list of delete-mesh-datas; we'll include this with our - // next frame_def to have the graphics thread kill before it processes - // the frame_def. + // next frame_def to have the graphics thread kill before it processes the + // frame_def. mesh_data_destroys_.push_back(d); } @@ -1635,24 +1634,6 @@ void Graphics::ToggleDebugDraw() { void Graphics::ReleaseFadeEndCommand() { fade_end_call_.Clear(); } -void Graphics::WaitForRendererToExist() { - // Conceivably we could hit this point before our graphics thread has created - // the renderer. In that case lets wait a moment. - int sleep_count = 0; - while (g_base->graphics_server == nullptr - || g_base->graphics_server->renderer() == nullptr) { - BA_LOG_ONCE( - LogLevel::kWarning, - "BuildAndPushFrameDef() called before renderer is up; spinning..."); - core::CorePlatform::SleepMillisecs(100); - sleep_count++; - if (sleep_count > 100) { - throw Exception( - "Aborting waiting for renderer to come up in BuildAndPushFrameDef()"); - } - } -} - auto Graphics::ValueTest(const std::string& arg, double* absval, double* deltaval, double* outval) -> bool { return false; @@ -1737,20 +1718,17 @@ void Graphics::DoDrawBlotch(std::vector* indices, } void Graphics::DrawRadialMeter(MeshIndexedSimpleFull* m, float amt) { - // FIXME - we're updating this every frame so we should use pure dynamic data; - // not a mix of static and dynamic. + // FIXME - we're updating this every frame so we should use pure dynamic + // data; not a mix of static and dynamic. if (amt >= 0.999f) { - // clang-format off uint16_t indices[] = {0, 1, 2, 1, 3, 2}; VertexSimpleFull vertices[] = { {-1, -1, 0, 0, 65535}, {1, -1, 0, 65535, 65535}, {-1, 1, 0, 0, 0}, - {1, 1, 0, 65535, 0, - } + {1, 1, 0, 65535, 0}, }; - // clang-format on m->SetIndexData(Object::New(6, indices)); m->SetData(Object::New>(4, vertices)); diff --git a/src/ballistica/base/graphics/graphics.h b/src/ballistica/base/graphics/graphics.h index 4708b7e4..79d9630a 100644 --- a/src/ballistica/base/graphics/graphics.h +++ b/src/ballistica/base/graphics/graphics.h @@ -322,7 +322,6 @@ class Graphics { void DrawCursor(RenderPass* pass, millisecs_t real_time); void DrawFades(FrameDef* frame_def, millisecs_t real_time); void DrawDebugBuffers(RenderPass* pass); - void WaitForRendererToExist(); void UpdateAndDrawProgressBar(FrameDef* frame_def, millisecs_t real_time); void DoDrawBlotch(std::vector* indices, @@ -344,7 +343,7 @@ class Graphics { std::vector mesh_data_creates_; std::vector mesh_data_destroys_; bool has_supports_high_quality_graphics_value_{}; - bool supports_high_quality_graphics_ = false; + bool supports_high_quality_graphics_{}; millisecs_t last_create_frame_def_time_{}; Vector3f shadow_offset_{0.0f, 0.0f, 0.0f}; Vector2f shadow_scale_{1.0f, 1.0f}; diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc index 94a959df..18110551 100644 --- a/src/ballistica/base/python/methods/python_methods_app.cc +++ b/src/ballistica/base/python/methods/python_methods_app.cc @@ -1370,9 +1370,9 @@ static PyMethodDef PyUserAgentStringDef = { "(internal)\n", }; -// ----------------------- empty_app_mode_activate ----------------------------- +// --------------------- on_empty_app_mode_activate ---------------------------- -static auto PyEmptyAppModeActivate(PyObject* self) -> PyObject* { +static auto PyOnEmptyAppModeActivate(PyObject* self) -> PyObject* { BA_PYTHON_TRY; BA_PRECONDITION(g_base->InLogicThread()); g_base->set_app_mode(AppModeEmpty::GetSingleton()); @@ -1382,19 +1382,19 @@ static auto PyEmptyAppModeActivate(PyObject* self) -> PyObject* { BA_PYTHON_CATCH; } -static PyMethodDef PyEmptyAppModeActivateDef = { - "empty_app_mode_activate", // name - (PyCFunction)PyEmptyAppModeActivate, // method - METH_NOARGS, // flags +static PyMethodDef PyOnEmptyAppModeActivateDef = { + "on_empty_app_mode_activate", // name + (PyCFunction)PyOnEmptyAppModeActivate, // method + METH_NOARGS, // flags - "empty_app_mode_activate() -> None\n" + "on_empty_app_mode_activate() -> None\n" "\n" "(internal)\n", }; -// ----------------------- empty_app_mode_deactivate --------------------------- +// --------------------- on_empty_app_mode_deactivate -------------------------- -static auto PyEmptyAppModeDeactivate(PyObject* self) -> PyObject* { +static auto PyOnEmptyAppModeDeactivate(PyObject* self) -> PyObject* { BA_PYTHON_TRY; BA_PRECONDITION(g_base->InLogicThread()); // Currently doing nothing. @@ -1402,12 +1402,12 @@ static auto PyEmptyAppModeDeactivate(PyObject* self) -> PyObject* { BA_PYTHON_CATCH; } -static PyMethodDef PyEmptyAppModeDeactivateDef = { - "empty_app_mode_deactivate", // name - (PyCFunction)PyEmptyAppModeDeactivate, // method - METH_NOARGS, // flags +static PyMethodDef PyOnEmptyAppModeDeactivateDef = { + "on_empty_app_mode_deactivate", // name + (PyCFunction)PyOnEmptyAppModeDeactivate, // method + METH_NOARGS, // flags - "empty_app_mode_deactivate() -> None\n" + "on_empty_app_mode_deactivate() -> None\n" "\n" "(internal)\n", }; @@ -1598,8 +1598,8 @@ auto PythonMethodsApp::GetMethods() -> std::vector { PyOnInitialAppModeSetDef, PyReachedEndOfBaBaseDef, PyUserAgentStringDef, - PyEmptyAppModeActivateDef, - PyEmptyAppModeDeactivateDef, + PyOnEmptyAppModeActivateDef, + PyOnEmptyAppModeDeactivateDef, PyEmptyAppModeHandleIntentDefaultDef, PyEmptyAppModeHandleIntentExecDef, PyGetImmediateReturnCodeDef, diff --git a/src/ballistica/base/python/methods/python_methods_graphics.cc b/src/ballistica/base/python/methods/python_methods_graphics.cc index 9652c041..e642b2d6 100644 --- a/src/ballistica/base/python/methods/python_methods_graphics.cc +++ b/src/ballistica/base/python/methods/python_methods_graphics.cc @@ -525,8 +525,7 @@ static auto PyFadeScreen(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - // This can only be called in the UI context. - int fade{0}; + int fade{}; float time{0.25f}; PyObject* endcall = nullptr; static const char* kwlist[] = {"to", "time", "endcall", nullptr}; diff --git a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc index 8015cff4..fcf376f7 100644 --- a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc +++ b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc @@ -1614,9 +1614,9 @@ static PyMethodDef PySetInternalMusicDef = { "(internal).", }; -// --------------------------- app_mode_activate ------------------------------- +// -------------------------- on_app_mode_activate ----------------------------- -static auto PyAppModeActivate(PyObject* self) -> PyObject* { +static auto PyOnAppModeActivate(PyObject* self) -> PyObject* { BA_PYTHON_TRY; BA_PRECONDITION(g_base->InLogicThread()); g_base->set_app_mode(SceneV1AppMode::GetSingleton()); @@ -1624,19 +1624,19 @@ static auto PyAppModeActivate(PyObject* self) -> PyObject* { BA_PYTHON_CATCH; } -static PyMethodDef PyAppModeActivateDef = { - "app_mode_activate", // name - (PyCFunction)PyAppModeActivate, // method - METH_NOARGS, // flags +static PyMethodDef PyOnAppModeActivateDef = { + "on_app_mode_activate", // name + (PyCFunction)PyOnAppModeActivate, // method + METH_NOARGS, // flags - "app_mode_activate() -> None\n" + "on_app_mode_activate() -> None\n" "\n" "(internal)\n", }; -// -------------------------- app_mode_deactivate ------------------------------ +// ------------------------- on_app_mode_deactivate ---------------------------- -static auto PyAppModeDeactivate(PyObject* self) -> PyObject* { +static auto PyOnAppModeDeactivate(PyObject* self) -> PyObject* { BA_PYTHON_TRY; BA_PRECONDITION(g_base->InLogicThread()); // Currently doing nothing. @@ -1644,12 +1644,12 @@ static auto PyAppModeDeactivate(PyObject* self) -> PyObject* { BA_PYTHON_CATCH; } -static PyMethodDef PyAppModeDeactivateDef = { - "app_mode_deactivate", // name - (PyCFunction)PyAppModeDeactivate, // method - METH_NOARGS, // flags +static PyMethodDef PyOnAppModeDeactivateDef = { + "on_app_mode_deactivate", // name + (PyCFunction)PyOnAppModeDeactivate, // method + METH_NOARGS, // flags - "app_mode_deactivate() -> None\n" + "on_app_mode_deactivate() -> None\n" "\n" "(internal)\n", }; @@ -1771,8 +1771,8 @@ auto PythonMethodsScene::GetMethods() -> std::vector { PyBaseTimeDef, PyBaseTimerDef, PyLsInputDevicesDef, - PyAppModeActivateDef, - PyAppModeDeactivateDef, + PyOnAppModeActivateDef, + PyOnAppModeDeactivateDef, PyHandleAppIntentDefaultDef, PyHandleAppIntentExecDef, PyProtocolVersionDef, diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index 4c0ebb4e..6c7019e6 100644 --- a/src/ballistica/shared/ballistica.cc +++ b/src/ballistica/shared/ballistica.cc @@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int { namespace ballistica { // These are set automatically via script; don't modify them here. -const int kEngineBuildNumber = 21299; +const int kEngineBuildNumber = 21303; const char* kEngineVersion = "1.7.28"; const int kEngineApiVersion = 8; diff --git a/tools/bacommon/app.py b/tools/bacommon/app.py new file mode 100644 index 00000000..d1b50fda --- /dev/null +++ b/tools/bacommon/app.py @@ -0,0 +1,35 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Common high level values/functionality related to apps.""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + pass + + +class AppExperience(Enum): + """Overall experience that can be provided by a Ballistica app. + + This corresponds generally, but not exactly, to distinct apps built + with Ballistica. However, a single app may support multiple experiences, + or there may be multiple apps targeting one experience. Cloud components + such as leagues are generally associated with an AppExperience. + """ + + # A special experience category that is supported everywhere. Used + # for the default empty AppMode when starting the app, etc. + EMPTY = 'empty' + + # The traditional BombSquad experience: multiple players using + # controllers in a single arena small enough for all action to be + # viewed on a single screen. + MELEE = 'melee' + + # The traditional BombSquad Remote experience; buttons on a + # touch-screen allowing a mobile device to be used as a game + # controller. + REMOTE = 'remote' diff --git a/tools/batools/appmodule.py b/tools/batools/appmodule.py index 4965afec..2f86ce9d 100755 --- a/tools/batools/appmodule.py +++ b/tools/batools/appmodule.py @@ -166,12 +166,12 @@ def generate_app_module( if 'scene_v1' in fsets: contents += ( - 'if bascenev1.SceneV1AppMode.supports_intent(intent):\n' + 'if bascenev1.SceneV1AppMode.can_handle_intent(intent):\n' ' return bascenev1.SceneV1AppMode\n\n' ) if 'base' in fsets: contents += ( - 'if babase.EmptyAppMode.supports_intent(intent):\n' + 'if babase.EmptyAppMode.can_handle_intent(intent):\n' ' return babase.EmptyAppMode\n\n' ) contents += (