diff --git a/.efrocachemap b/.efrocachemap index a4b58656..ff784cce 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -4064,50 +4064,50 @@ "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": "bb5e0df17efe96476c3c2b8c41c7979b", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "056115be35ac1afc1f62b58dcc8f217a", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "cf2b052caaa06d512ef379687b3aff86", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "97b8d5f261f84b8047d43df1ca81777a", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "f304ee46675e7552c31e174d81199307", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "70b54361b557f5e8d4271a0c976b28b6", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "06c778dc4c2772acf6dbaf67eb7321c9", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "212a5c7206c1aa9a8427b61471e9e89b", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "f3a1028602c7bbd3f8d62bd843af628d", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "9c4c6d1c50e225dc61cfbab4a82a37a6", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "4db4b600478f1767fdd0462a137912de", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "628bc102cf6ef17b38804c4c9baa5265", - "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "fb9d165ab24084e8beaa6d7c51c81a77", - "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "ae88283e96a9238aab7561d2afcd9a5f", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "e5fe958377b8dcf5d5203dbd17aaab72", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "f22e8af184b19b4929162e901a056454", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "5fa2cb24b9e78bddb1bf9efb197d0c51", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "d36f11acfa5e68f303d347c3895979d0", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "570a7e325c15ecebcc419d48a046dd24", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "3155f665993e5f850db5b87c9296abe7", - "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "d1bfae5e75824ba89338892bc0f84c6b", - "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "b1466048e319c0d60139c46751f3eb79", - "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "d1bfae5e75824ba89338892bc0f84c6b", - "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "b1466048e319c0d60139c46751f3eb79", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "656176631037184b6e22b0b68c3cd1fa", - "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "12b633db4dad37bbb5a5c398db0c10dd", - "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "656176631037184b6e22b0b68c3cd1fa", - "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "12b633db4dad37bbb5a5c398db0c10dd", - "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "7e014214c6cb9ddaa0e95f5186ba9df6", - "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "45ddc559dd5ef563d2df5c5db9c9fbc0", - "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "7e014214c6cb9ddaa0e95f5186ba9df6", - "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "45ddc559dd5ef563d2df5c5db9c9fbc0", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "1a15bf7d809addab4992827da9d89895", - "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "a1e03b7d13482beab8852691b5698974", - "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "807e3629d9d4611cd93feb87face4e51", - "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "a1e03b7d13482beab8852691b5698974", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "7f7bc04993982b164f6e86ad6ce350ef", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "d24102dd35c29b6a77cdf3d9921695da", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "8edef08a22594617d2b4273e0e4cba40", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "cb6c0c6efad034b53fe1a8f930f2ce81", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "3ec4aadf128132116fc5479a46bd1f71", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "14c2cf0812e3022391caffd9409d0650", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "67c207425afc5023cea9740e3bd459c3", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "7219b9034f14c5b769818b80135ea61b", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "a039bab674a99b559440323b965d2274", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "fb15d3a3e792163f18af727447564192", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "246782dc8e1f2114c62980f8addbc4f4", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "8e59c9779e54f22b66ddfe2cd7c21528", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "0c241652d1669e3bbaf8df19c3ae756c", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "40ba4e0316c063238ab8e8b94f98351c", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "9d80b87c57556a0877f260305f571c78", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "0e7d5147a5b1b9a7ecb8e6fc4cfc1174", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "943297ef2d247451140c08816fa0b46d", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "af9ef217e000fb8e2d7fff8770b7bf44", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "c0e09234f16c75313eab30d576783549", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "3f265457324e3a07a676b4db52a5f111", + "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "6482c468d8e798e081310c294553e4da", + "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "c6354818c9abd243e9b9af03f1f075f7", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "43908d43f107baa521cee51af01a9583", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "c91f0c62c989a33caa7b4b4769754f1a", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "79a0eb8f637e295447150a2c1e03357d", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "5900beafe0de9b11ce4d00e9163c2d15", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "7e7a5d0cc2f6fdd8fd9affbc05c5195c", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "91a1f57c0f4e9ef6fb5eb590f883e167", + "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "56ebc8c31e020e515395d3a81d2bb766", + "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "b9aabf060ca52f9957e5c0c68975dd0d", + "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "56ebc8c31e020e515395d3a81d2bb766", + "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "b9aabf060ca52f9957e5c0c68975dd0d", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "21b01b868f38182cbe30dead5e6f6688", + "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "5bafa4627b87a3cfc6558d51c2760560", + "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "21b01b868f38182cbe30dead5e6f6688", + "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "5bafa4627b87a3cfc6558d51c2760560", + "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "9ba592d991ebb8724de8cab368bd1ac7", + "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "79630364e1f71cedf87140c40b913785", + "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "9ba592d991ebb8724de8cab368bd1ac7", + "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "79630364e1f71cedf87140c40b913785", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "7e3d1a1c0bdb8a814f7227f71862fa1d", + "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "5bfe717b5f30a67822130299b7342bcf", + "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "6c7d4caaad12d39c61b291fe33eef2af", + "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "5bfe717b5f30a67822130299b7342bcf", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "ac0e239be82c9f04383eb4400313ad90", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "866ae37140298f2bd1ed9913afa773fb", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "ac8e60b59a767546d1bdb86d68c8e83d", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "8e4b7a2ae0cd444e818789ac919413d1", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "2ea511bd7f4bf34ec1651cee263f3d11", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "d9dd043cc3518ef0d02ceb302dfa71e1", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "fbfab4ba2a24a212d4f3b22a259ae3f8", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "bb47df20836a1f0466f785b1458d7f48", "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/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index e7e921f3..8aef2757 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -1622,6 +1622,7 @@ linearstep linebegin linebits + lineend lineheight linemax lineno @@ -2777,6 +2778,7 @@ sslcontext sslproto ssval + stacklevel stackstr stager standin @@ -2857,6 +2859,7 @@ successmsg suiciding sunau + suppressions suter sval svalin diff --git a/CHANGELOG.md b/CHANGELOG.md index 4565e808..5f7efab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,32 @@ -### 1.7.26 (build 21256, api 8, 2023-08-25) +### 1.7.27 (build 21269, api 8, 2023-08-30) + +- Fixed a rare crash that could occur if the app shuts down while a background + thread is making a web request. The app will now try to wait for any such + attempts to complete. +- Added `babase.app.env` which is a type-friendly object containing various + environment/runtime values. Values directly under `app` such as + `babase.app.debug_build` will either be consolidated here or moved to classic + if they are considered deprecated. +- Started using Python's `warnings` module to announce deprecations, and turned + on deprecation warnings for the release build (by default in Python they are + mostly only on for debug builds). This way, when making minor changes, I can + keep old code paths intact for a few versions and warn modders that they + should transition to new code paths before the old ones disappear. I'd prefer + to avoid incrementing api-version again if at all possible since that is such + a dramatic event, so this alternative will hopefully allow gently evolving + some things without too much breakage. +- Following up on the above two entries, several attributes under `babase.app` + have been relocated to `babase.app.env` and the originals have been given + deprecation warnings and will disappear sometime soon. This includes + `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`. +- 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 + user what the thing is to solve cases like that. + +### 1.7.26 (build 21259, api 8, 2023-08-29) - Android should now be better at detecting hardware keyboards (you will see 'Configure Keyboard' and 'Configure Keyboard P2' buttons under @@ -30,9 +58,9 @@ should be more consistent use of the 'Quit?' confirm window. Please holler if you see any odd behavior when trying to quit the app. - Unix TERM signal now triggers graceful app shutdown. -- Added `ba.app.add_shutdown_task()` to register coroutines to be run as part of +- Added `app.add_shutdown_task()` to register coroutines to be run as part of shutdown. -- Removed `babase.app.iircade_mode`. RIP iiRcade :(. +- Removed `app.iircade_mode`. RIP iiRcade :(. - Changed `AppState.INITIAL` to `AppState.NOT_RUNNING`, added a `AppState.NATIVE_BOOTSTRAPPING`, and changed `AppState.LAUNCHING` to `AppState.INITING`. These better describe what the app is actually doing while diff --git a/ballisticakit-cmake/.idea/dictionaries/ericf.xml b/ballisticakit-cmake/.idea/dictionaries/ericf.xml index 6815396d..d2ef27e7 100644 --- a/ballisticakit-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticakit-cmake/.idea/dictionaries/ericf.xml @@ -960,6 +960,7 @@ linearsize linearstep linebegin + lineend linemax linestart linestripped @@ -1643,6 +1644,7 @@ sssssssi ssssssssssss ssval + stacklevel stager standin startedptr @@ -1690,6 +1692,7 @@ subsys subtypestr successmsg + suppressions sval swidth swiftc diff --git a/ballisticakit-cmake/CMakeLists.txt b/ballisticakit-cmake/CMakeLists.txt index 8da237d3..7bc72301 100644 --- a/ballisticakit-cmake/CMakeLists.txt +++ b/ballisticakit-cmake/CMakeLists.txt @@ -393,6 +393,8 @@ set(BALLISTICA_SOURCES ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_context_ref.h ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_display_timer.cc ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_display_timer.h + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_env.cc + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_env.h ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_feature_set_data.cc ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_feature_set_data.h ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_simple_sound.cc diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj index a24dd06b..25b311dc 100644 --- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj @@ -379,6 +379,8 @@ + + diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters index 01de57ed..82b2e114 100644 --- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters @@ -571,6 +571,12 @@ ballistica\base\python\class + + ballistica\base\python\class + + + ballistica\base\python\class + ballistica\base\python\class diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj index 094c7ecb..12edaef7 100644 --- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj @@ -374,6 +374,8 @@ + + diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters index 01de57ed..82b2e114 100644 --- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters @@ -571,6 +571,12 @@ ballistica\base\python\class + + ballistica\base\python\class + + + ballistica\base\python\class + ballistica\base\python\class diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py index ea5343bc..0dccbe5f 100644 --- a/src/assets/ba_data/python/babase/__init__.py +++ b/src/assets/ba_data/python/babase/__init__.py @@ -39,6 +39,7 @@ from _babase import ( DisplayTimer, do_once, env, + Env, fade_screen, fatal_error, get_display_resolution, @@ -48,8 +49,10 @@ from _babase import ( get_replays_dir, get_string_height, get_string_width, + get_v1_cloud_log_file_path, getsimplesound, has_gamma_control, + has_user_run_commands, have_chars, have_permission, in_logic_thread, @@ -83,6 +86,9 @@ from _babase import ( set_thread_name, set_ui_input_device, show_progress_bar, + shutdown_suppress_begin, + shutdown_suppress_end, + shutdown_suppress_count, SimpleSound, unlock_all_input, user_agent_string, @@ -96,6 +102,7 @@ from babase._appconfig import commit_app_config from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec from babase._appmode import AppMode from babase._appsubsystem import AppSubsystem +from babase._appmodeselector import AppModeSelector from babase._appconfig import AppConfig from babase._apputils import ( handle_leftover_v1_cloud_log_file, @@ -175,6 +182,7 @@ __all__ = [ 'AppMode', 'appname', 'appnameupper', + 'AppModeSelector', 'AppSubsystem', 'apptime', 'AppTime', @@ -200,6 +208,7 @@ __all__ = [ 'do_once', 'EmptyAppMode', 'env', + 'Env', 'Existable', 'existing', 'fade_screen', @@ -214,11 +223,13 @@ __all__ = [ 'get_replays_dir', 'get_string_height', 'get_string_width', + 'get_v1_cloud_log_file_path', 'get_type_name', 'getclass', 'getsimplesound', 'handle_leftover_v1_cloud_log_file', 'has_gamma_control', + 'has_user_run_commands', 'have_chars', 'have_permission', 'in_logic_thread', @@ -277,6 +288,9 @@ __all__ = [ 'set_thread_name', 'set_ui_input_device', 'show_progress_bar', + 'shutdown_suppress_begin', + 'shutdown_suppress_end', + 'shutdown_suppress_count', 'SimpleSound', 'SpecialChar', 'storagename', diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py index feb4ea01..e0072b7a 100644 --- a/src/assets/ba_data/python/babase/_app.py +++ b/src/assets/ba_data/python/babase/_app.py @@ -1,11 +1,14 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to the high level state of the app.""" +# pylint: disable=too-many-lines from __future__ import annotations import os import logging from enum import Enum + +import warnings from typing import TYPE_CHECKING from concurrent.futures import ThreadPoolExecutor from functools import cached_property @@ -55,6 +58,7 @@ class App: plugins: PluginSubsystem lang: LanguageSubsystem + env: babase.Env health_monitor: AppHealthMonitor @@ -68,173 +72,72 @@ class App: # The app has not yet begun starting and should not be used in # any way. - NOT_RUNNING = 'not_running' + NOT_RUNNING = 0 # The native layer is spinning up its machinery (screens, # renderers, etc.). Nothing should happen in the Python layer # until this completes. - NATIVE_BOOTSTRAPPING = 'native_bootstrapping' + NATIVE_BOOTSTRAPPING = 1 # Python app subsystems are being inited but should not yet # interact or do any work. - INITING = 'initing' + INITING = 2 # Python app subsystems are inited and interacting, but the app # has not yet embarked on a high level course of action. It is # doing initial account logins, workspace & asset downloads, # etc. - LOADING = 'loading' + LOADING = 3 # All pieces are in place and the app is now doing its thing. - RUNNING = 'running' + RUNNING = 4 # The app is backgrounded or otherwise suspended. - PAUSED = 'paused' + PAUSED = 5 # The app is shutting down. - SHUTTING_DOWN = 'shutting_down' + SHUTTING_DOWN = 6 - @property - def aioloop(self) -> asyncio.AbstractEventLoop: - """The logic thread's asyncio event loop. + class DefaultAppModeSelector(AppModeSelector): + """Decides which app modes to use to handle intents. - 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. + The behavior here is generated by the project updater based on + the feature-sets present in the project. Spinoff projects can + also inject their own behavior by replacing the text + '__GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__' with their own code + through spinoff filtering. + + It is also possible to modify mode selection behavior by writing + a custom AppModeSelector class and replacing app.mode_selector + with an instance of it. This is a good way to go if you are + modifying app behavior with a plugin instead of in a spinoff + project. """ - assert self._aioloop is not None - return self._aioloop - @property - def build_number(self) -> int: - """Integer build number. + def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]: + # pylint: disable=cyclic-import - This value increases by at least 1 with each release of the game. - It is independent of the human readable babase.App.version string. - """ - assert isinstance(self._env['build_number'], int) - return self._env['build_number'] + # __GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__ - @property - def device_name(self) -> str: - """Name of the device running the game.""" - assert isinstance(self._env['device_name'], str) - return self._env['device_name'] + # __DEFAULT_APP_MODE_SELECTION_BEGIN__ + # This section generated by batools.appmodule; do not edit. - @property - def config_file_path(self) -> str: - """Where the game's config file is stored on disk.""" - assert isinstance(self._env['config_file_path'], str) - return self._env['config_file_path'] + # Hmm; need to think about how we auto-construct this; how + # should we determine which app modes to check and in what + # order? + import bascenev1 - @property - def version(self) -> str: - """Human-readable version string; something like '1.3.24'. + import babase - 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 'babase.App.build_number'. - """ - assert isinstance(self._env['version'], str) - return self._env['version'] + if bascenev1.SceneV1AppMode.supports_intent(intent): + return bascenev1.SceneV1AppMode - @property - def debug_build(self) -> bool: - """Whether the app was compiled in debug mode. + if babase.EmptyAppMode.supports_intent(intent): + return babase.EmptyAppMode - Debug builds generally run substantially slower than non-debug - builds due to compiler optimizations being disabled and extra - checks being run. - """ - assert isinstance(self._env['debug_build'], bool) - return self._env['debug_build'] + raise RuntimeError(f'No handler found for intent {type(intent)}.') - @property - def test_build(self) -> bool: - """Whether the game 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. - """ - assert isinstance(self._env['test_build'], bool) - return self._env['test_build'] - - @property - def data_directory(self) -> str: - """Path where static app data lives.""" - assert isinstance(self._env['data_directory'], str) - return self._env['data_directory'] - - @property - def python_directory_user(self) -> str | None: - """Path where ballistica 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. - """ - assert isinstance(self._env['python_directory_user'], (str, type(None))) - return self._env['python_directory_user'] - - @property - def python_directory_app(self) -> str | None: - """Path where ballistica 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. - """ - assert isinstance(self._env['python_directory_app'], (str, type(None))) - return self._env['python_directory_app'] - - @property - def python_directory_app_site(self) -> str | None: - """Path where ballistica 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. - """ - assert isinstance( - self._env['python_directory_app_site'], (str, type(None)) - ) - return self._env['python_directory_app_site'] - - @property - def config(self) -> babase.AppConfig: - """The babase.AppConfig instance representing the app's config state.""" - assert self._config is not None - return self._config - - @property - def api_version(self) -> int: - """The app's api version. - - Only Python modules and packages associated with the current API - version number will be detected by the game (see the ba_meta tag). - This value will change whenever substantial backward-incompatible - changes are introduced to ballistica APIs. When that happens, - modules/packages should be updated accordingly and set to target - the newer API version number. - """ - from babase._meta import CURRENT_API_VERSION - - return CURRENT_API_VERSION - - @property - def on_tv(self) -> bool: - """Whether the game is currently running on a TV.""" - assert isinstance(self._env['on_tv'], bool) - return self._env['on_tv'] - - @property - def vr_mode(self) -> bool: - """Whether the game is currently running in VR.""" - assert isinstance(self._env['vr_mode'], bool) - return self._env['vr_mode'] + # __DEFAULT_APP_MODE_SELECTION_END__ def __init__(self) -> None: """(internal) @@ -248,6 +151,8 @@ class App: self.state = self.State.NOT_RUNNING + self.env: babase.Env = _babase.Env() + self._subsystems: list[AppSubsystem] = [] self._native_bootstrapping_completed = False @@ -263,12 +168,11 @@ class App: self._subsystem_registration_ended = False self._pending_apply_app_config = False - # Config. - self.config_file_healthy = False + self.config_file_healthy: bool = False - # This is incremented any time the app is - # backgrounded/foregrounded; can be a simple way to determine if - # network data should be refreshed/etc. + # This is incremented any time the app is backgrounded or + # foregrounded; can be a simple way to determine if network data + # should be refreshed/etc. self.fg_state = 0 self._aioloop: asyncio.AbstractEventLoop | None = None @@ -294,20 +198,25 @@ class App: self._config: babase.AppConfig | None = None self.components = AppComponentSubsystem() + + # Testing this. self.meta = MetadataSubsystem() + self.net = NetworkSubsystem() self.workspaces = WorkspaceSubsystem() self._pending_intent: AppIntent | None = None self._intent: AppIntent | None = None self._mode: AppMode | None = None self._shutdown_task: asyncio.Task[None] | None = None - self._shutdown_tasks: list[Coroutine[None, None, None]] = [] + self._shutdown_tasks: list[Coroutine[None, None, None]] = [ + self._wait_for_shutdown_suppressions() + ] # 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: AppModeSelector | None = None + self.mode_selector: babase.AppModeSelector | None = None self._asyncio_timer: babase.AppTimer | None = None @@ -324,35 +233,25 @@ class App: self.lang = LanguageSubsystem() self.plugins = PluginSubsystem() - def register_subsystem(self, subsystem: AppSubsystem) -> None: - """Called by the AppSubsystem class. Do not use directly.""" + @property + def aioloop(self) -> asyncio.AbstractEventLoop: + """The logic thread's asyncio event loop. - # We only allow registering new subsystems if we've not yet - # reached the 'running' state. This ensures that all subsystems - # receive a consistent set of callbacks starting with - # on_app_running(). - if self._subsystem_registration_ended: - raise RuntimeError( - 'Subsystems can no longer be registered at this point.' - ) - self._subsystems.append(subsystem) - - def _threadpool_no_wait_done(self, fut: Future) -> None: - try: - fut.result() - except Exception: - logging.exception( - 'Error in work submitted via threadpool_submit_no_wait()' - ) - - def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None: - """Submit work to our threadpool and log any errors. - - Use this when you want to run something asynchronously but don't - intend to call result() on it to handle errors/etc. + 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. """ - fut = self.threadpool.submit(call) - fut.add_done_callback(self._threadpool_no_wait_done) + assert self._aioloop is not None + return self._aioloop + + @property + def config(self) -> babase.AppConfig: + """The babase.AppConfig instance representing the app's config state.""" + assert self._config is not None + return self._config # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_BEGIN__ # This section generated by batools.appmodule; do not edit. @@ -398,6 +297,48 @@ class App: # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_END__ + def register_subsystem(self, subsystem: AppSubsystem) -> None: + """Called by the AppSubsystem class. Do not use directly.""" + + # We only allow registering new subsystems if we've not yet + # reached the 'running' state. This ensures that all subsystems + # receive a consistent set of callbacks starting with + # on_app_running(). + if self._subsystem_registration_ended: + raise RuntimeError( + 'Subsystems can no longer be registered at this point.' + ) + self._subsystems.append(subsystem) + + def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None: + """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. + """ + if self.state is self.State.SHUTTING_DOWN: + raise RuntimeError( + 'Cannot add shutdown tasks with state SHUTTING_DOWN.' + ) + self._shutdown_tasks.append(coro) + + def run(self) -> None: + """Run the app to completion. + + Note that this only works on platforms where Ballistica + manages its own event loop. + """ + _babase.run_app() + + def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None: + """Submit work to our threadpool and log any errors. + + Use this when you want to run something asynchronously but don't + intend to call result() on it to handle errors/etc. + """ + fut = self.threadpool.submit(call) + fut.add_done_callback(self._threadpool_no_wait_done) + def set_intent(self, intent: AppIntent) -> None: """Set the intent for the app. @@ -417,6 +358,92 @@ class App: # since it may block for a moment to load modules/etc. self.threadpool_submit_no_wait(tpartial(self._set_intent, intent)) + def push_apply_app_config(self) -> None: + """Internal. Use app.config.apply() to apply app config changes.""" + # To be safe, let's run this by itself in the event loop. + # This avoids potential trouble if this gets called mid-draw or + # something like that. + self._pending_apply_app_config = True + _babase.pushcall(self._apply_app_config, raw=True) + + def on_native_start(self) -> None: + """Called by the native layer when the app is being started.""" + assert _babase.in_logic_thread() + assert not self._native_start_called + self._native_start_called = True + self._update_state() + + def on_native_bootstrapping_complete(self) -> None: + """Called by the native layer once its ready to rock.""" + assert _babase.in_logic_thread() + assert not self._native_bootstrapping_completed + self._native_bootstrapping_completed = True + self._update_state() + + def on_native_pause(self) -> None: + """Called by the native layer when the app pauses.""" + assert _babase.in_logic_thread() + assert not self._native_paused # Should avoid redundant calls. + self._native_paused = True + self._update_state() + + def on_native_resume(self) -> None: + """Called by the native layer when the app resumes.""" + assert _babase.in_logic_thread() + assert self._native_paused # Should avoid redundant calls. + self._native_paused = False + self._update_state() + + def on_native_shutdown(self) -> None: + """Called by the native layer when the app starts shutting down.""" + assert _babase.in_logic_thread() + self._native_shutdown_called = True + self._update_state() + + def read_config(self) -> None: + """(internal)""" + from babase._appconfig import read_app_config + + self._config, self.config_file_healthy = read_app_config() + + def handle_deep_link(self, url: str) -> None: + """Handle a deep link URL.""" + from babase._language import Lstr + + assert _babase.in_logic_thread() + + appname = _babase.appname() + if url.startswith(f'{appname}://code/'): + code = url.replace(f'{appname}://code/', '') + if self.classic is not None: + self.classic.accounts.add_pending_promo_code(code) + else: + try: + _babase.screenmessage( + Lstr(resource='errorText'), color=(1, 0, 0) + ) + _babase.getsimplesound('error').play() + except ImportError: + pass + + def on_initial_sign_in_complete(self) -> None: + """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. + """ + assert _babase.in_logic_thread() + assert not self._initial_sign_in_completed + + # Tell meta it can start scanning extra stuff that just showed + # up (namely account workspaces). + self.meta.start_extra_scan() + + self._initial_sign_in_completed = True + self._update_state() + def _set_intent(self, intent: AppIntent) -> None: # This should be running in a bg thread. assert not _babase.in_logic_thread() @@ -492,14 +519,6 @@ class App: _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) _babase.getsimplesound('error').play() - def run(self) -> None: - """Run the app to completion. - - Note that this only works on platforms where Ballistica - manages its own event loop. - """ - _babase.run_app() - def _on_initing(self) -> None: """Called when the app enters the initing state. @@ -633,14 +652,6 @@ class App: # plugin hasn't already told it to do something. self.set_intent(AppIntentDefault()) - def push_apply_app_config(self) -> None: - """Internal. Use app.config.apply() to apply app config changes.""" - # To be safe, let's run this by itself in the event loop. - # This avoids potential trouble if this gets called mid-draw or - # something like that. - self._pending_apply_app_config = True - _babase.pushcall(self._apply_app_config, raw=True) - def _apply_app_config(self) -> None: assert _babase.in_logic_thread() @@ -667,47 +678,6 @@ class App: # Let the native layer do its thing. _babase.do_apply_app_config() - class DefaultAppModeSelector(AppModeSelector): - """Decides which app modes to use to handle intents. - - The behavior here is generated by the project updater based on - the feature-sets present in the project. Spinoff projects can - also inject their own behavior by replacing the text - '__GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__' with their own code - through spinoff filtering. - - It is also possible to modify mode selection behavior by writing - a custom AppModeSelector class and replacing app.mode_selector - with an instance of it. This is a good way to go if you are - modifying app behavior with a plugin instead of in a spinoff - project. - """ - - def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]: - # pylint: disable=cyclic-import - - # __GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__ - - # __DEFAULT_APP_MODE_SELECTION_BEGIN__ - # This section generated by batools.appmodule; do not edit. - - # Hmm; need to think about how we auto-construct this; how - # should we determine which app modes to check and in what - # order? - import bascenev1 - - import babase - - if bascenev1.SceneV1AppMode.supports_intent(intent): - return bascenev1.SceneV1AppMode - - if babase.EmptyAppMode.supports_intent(intent): - return babase.EmptyAppMode - - raise RuntimeError(f'No handler found for intent {type(intent)}.') - - # __DEFAULT_APP_MODE_SELECTION_END__ - def _update_state(self) -> None: # pylint: disable=too-many-branches assert _babase.in_logic_thread() @@ -761,18 +731,6 @@ class App: if bool(True): self.state = self.State.NATIVE_BOOTSTRAPPING - def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None: - """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. - """ - if self.state is self.State.SHUTTING_DOWN: - raise RuntimeError( - 'Cannot add shutdown tasks with state SHUTTING_DOWN.' - ) - self._shutdown_tasks.append(coro) - async def _shutdown(self) -> None: import asyncio @@ -802,40 +760,6 @@ class App: except Exception: logging.exception('Error in shutdown task.') - def on_native_start(self) -> None: - """Called by the native layer when the app is being started.""" - assert _babase.in_logic_thread() - assert not self._native_start_called - self._native_start_called = True - self._update_state() - - def on_native_bootstrapping_complete(self) -> None: - """Called by the native layer once its ready to rock.""" - assert _babase.in_logic_thread() - assert not self._native_bootstrapping_completed - self._native_bootstrapping_completed = True - self._update_state() - - def on_native_pause(self) -> None: - """Called by the native layer when the app pauses.""" - assert _babase.in_logic_thread() - assert not self._native_paused # Should avoid redundant calls. - self._native_paused = True - self._update_state() - - def on_native_resume(self) -> None: - """Called by the native layer when the app resumes.""" - assert _babase.in_logic_thread() - assert self._native_paused # Should avoid redundant calls. - self._native_paused = False - self._update_state() - - def on_native_shutdown(self) -> None: - """Called by the native layer when the app starts shutting down.""" - assert _babase.in_logic_thread() - self._native_shutdown_called = True - self._update_state() - def _on_pause(self) -> None: """Called when the app goes to a paused state.""" assert _babase.in_logic_thread() @@ -881,46 +805,198 @@ class App: assert self._aioloop is not None self._shutdown_task = self._aioloop.create_task(self._shutdown()) - def read_config(self) -> None: - """(internal)""" - from babase._appconfig import read_app_config + async def _wait_for_shutdown_suppressions(self) -> None: + import asyncio - self._config, self.config_file_healthy = read_app_config() + # Spin and wait for anything blocking shutdown to complete. + _babase.lifecyclelog('shutdown-suppress wait begin') + while _babase.shutdown_suppress_count() > 0: + await asyncio.sleep(0.001) + _babase.lifecyclelog('shutdown-suppress wait end') - def handle_deep_link(self, url: str) -> None: - """Handle a deep link URL.""" - from babase._language import Lstr + def _threadpool_no_wait_done(self, fut: Future) -> None: + try: + fut.result() + except Exception: + logging.exception( + 'Error in work submitted via threadpool_submit_no_wait()' + ) - assert _babase.in_logic_thread() + # -------------------------------------------------------------------- + # THE FOLLOWING ARE DEPRECATED AND WILL BE REMOVED IN A FUTURE UPDATE. + # -------------------------------------------------------------------- - appname = _babase.appname() - if url.startswith(f'{appname}://code/'): - code = url.replace(f'{appname}://code/', '') - if self.classic is not None: - self.classic.accounts.add_pending_promo_code(code) - else: - try: - _babase.screenmessage( - Lstr(resource='errorText'), color=(1, 0, 0) - ) - _babase.getsimplesound('error').play() - except ImportError: - pass + @property + def build_number(self) -> int: + """Integer build number. - def on_initial_sign_in_complete(self) -> None: - """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. + This value increases by at least 1 with each release of the engine. + It is independent of the human readable babase.App.version string. """ - assert _babase.in_logic_thread() - assert not self._initial_sign_in_completed + warnings.warn( + 'app.build_number is deprecated; use app.env.build_number', + DeprecationWarning, + stacklevel=2, + ) + return self.env.build_number - # Tell meta it can start scanning extra stuff that just showed - # up (namely account workspaces). - self.meta.start_extra_scan() + @property + def device_name(self) -> str: + """Name of the device running the app.""" + warnings.warn( + 'app.device_name is deprecated; use app.env.device_name', + DeprecationWarning, + stacklevel=2, + ) + return self.env.device_name - self._initial_sign_in_completed = True - self._update_state() + @property + def config_file_path(self) -> str: + """Where the app's config file is stored on disk.""" + warnings.warn( + 'app.config_file_path is deprecated;' + ' use app.env.config_file_path', + DeprecationWarning, + stacklevel=2, + ) + return self.env.config_file_path + + @property + def version(self) -> str: + """Human-readable engine version string; something like '1.3.24'. + + This should not be interpreted as a number; it may contain + string elements such as 'alpha', 'beta', 'test', etc. + If a numeric version is needed, use `build_number`. + """ + warnings.warn( + 'app.version is deprecated; use app.env.version', + DeprecationWarning, + stacklevel=2, + ) + return self.env.version + + @property + def debug_build(self) -> bool: + """Whether the app was compiled in debug mode. + + Debug builds generally run substantially slower than non-debug + builds due to compiler optimizations being disabled and extra + checks being run. + """ + warnings.warn( + 'app.debug_build is deprecated; use app.env.debug', + DeprecationWarning, + stacklevel=2, + ) + return self.env.debug + + @property + def test_build(self) -> bool: + """Whether the app was compiled in test mode. + + Test mode enables extra checks and features that are useful for + release testing but which do not slow the game down significantly. + """ + warnings.warn( + 'app.test_build is deprecated; use app.env.test', + DeprecationWarning, + stacklevel=2, + ) + return self.env.test + + @property + def data_directory(self) -> str: + """Path where static app data lives.""" + warnings.warn( + 'app.data_directory is deprecated; use app.env.data_directory', + DeprecationWarning, + stacklevel=2, + ) + return self.env.data_directory + + @property + def python_directory_user(self) -> str | None: + """Path where the app expects its user scripts (mods) to live. + + Be aware that this value may be None if ballistica is running in + a non-standard environment, and that python-path modifications may + cause modules to be loaded from other locations. + """ + warnings.warn( + 'app.python_directory_user is deprecated;' + ' use app.env.python_directory_user', + DeprecationWarning, + stacklevel=2, + ) + return self.env.python_directory_user + + @property + def python_directory_app(self) -> str | None: + """Path where the app expects its bundled modules to live. + + Be aware that this value may be None if Ballistica is running in + a non-standard environment, and that python-path modifications may + cause modules to be loaded from other locations. + """ + warnings.warn( + 'app.python_directory_app is deprecated;' + ' use app.env.python_directory_app', + DeprecationWarning, + stacklevel=2, + ) + return self.env.python_directory_app + + @property + def python_directory_app_site(self) -> str | None: + """Path where the app expects its bundled pip modules to live. + + Be aware that this value may be None if Ballistica is running in + a non-standard environment, and that python-path modifications may + cause modules to be loaded from other locations. + """ + warnings.warn( + 'app.python_directory_app_site is deprecated;' + ' use app.env.python_directory_app_site', + DeprecationWarning, + stacklevel=2, + ) + return self.env.python_directory_app_site + + @property + def api_version(self) -> int: + """The app's api version. + + Only Python modules and packages associated with the current API + version number will be detected by the game (see the ba_meta tag). + This value will change whenever substantial backward-incompatible + changes are introduced to ballistica APIs. When that happens, + modules/packages should be updated accordingly and set to target + the newer API version number. + """ + warnings.warn( + 'app.api_version is deprecated; use app.env.api_version', + DeprecationWarning, + stacklevel=2, + ) + return self.env.api_version + + @property + def on_tv(self) -> bool: + """Whether the app is currently running on a TV.""" + warnings.warn( + 'app.on_tv is deprecated; use app.env.tv', + DeprecationWarning, + stacklevel=2, + ) + return self.env.tv + + @property + def vr_mode(self) -> bool: + """Whether the app is currently running in VR.""" + warnings.warn( + 'app.vr_mode is deprecated; use app.env.vr', + DeprecationWarning, + stacklevel=2, + ) + return self.env.vr diff --git a/src/assets/ba_data/python/babase/_appconfig.py b/src/assets/ba_data/python/babase/_appconfig.py index 8edb454a..86fda125 100644 --- a/src/assets/ba_data/python/babase/_appconfig.py +++ b/src/assets/ba_data/python/babase/_appconfig.py @@ -109,7 +109,7 @@ def read_app_config() -> tuple[AppConfig, bool]: # NOTE: it is assumed that this only gets called once and the # config object will not change from here on out - config_file_path = _babase.app.config_file_path + config_file_path = _babase.app.env.config_file_path config_contents = '' try: if os.path.exists(config_file_path): diff --git a/src/assets/ba_data/python/babase/_apputils.py b/src/assets/ba_data/python/babase/_apputils.py index 70fc4e32..4543ca60 100644 --- a/src/assets/ba_data/python/babase/_apputils.py +++ b/src/assets/ba_data/python/babase/_apputils.py @@ -48,7 +48,7 @@ def is_browser_likely_available() -> bool: # assume no browser. # FIXME: Might not be the case anymore; should make this definable # at the platform level. - if app.vr_mode or (platform == 'android' and not hastouchscreen): + if app.env.vr or (platform == 'android' and not hastouchscreen): return False # Anywhere else assume we've got one. @@ -103,8 +103,8 @@ def handle_v1_cloud_log() -> None: info = { 'log': _babase.get_v1_cloud_log(), - 'version': app.version, - 'build': app.build_number, + 'version': app.env.version, + 'build': app.env.build_number, 'userAgentString': classic.legacy_user_agent_string, 'session': sessionname, 'activity': activityname, @@ -279,7 +279,8 @@ def dump_app_state( # the dump in that case. try: mdpath = os.path.join( - os.path.dirname(_babase.app.config_file_path), '_appstate_dump_md' + os.path.dirname(_babase.app.env.config_file_path), + '_appstate_dump_md', ) with open(mdpath, 'w', encoding='utf-8') as outfile: outfile.write( @@ -297,7 +298,7 @@ def dump_app_state( return tbpath = os.path.join( - os.path.dirname(_babase.app.config_file_path), '_appstate_dump_tb' + os.path.dirname(_babase.app.env.config_file_path), '_appstate_dump_tb' ) tbfile = open(tbpath, 'w', encoding='utf-8') @@ -329,7 +330,8 @@ def log_dumped_app_state() -> None: try: out = '' mdpath = os.path.join( - os.path.dirname(_babase.app.config_file_path), '_appstate_dump_md' + os.path.dirname(_babase.app.env.config_file_path), + '_appstate_dump_md', ) if os.path.exists(mdpath): # We may be hanging on to open file descriptors for use by @@ -354,7 +356,7 @@ def log_dumped_app_state() -> None: f'Time: {metadata.app_time:.2f}' ) tbpath = os.path.join( - os.path.dirname(_babase.app.config_file_path), + os.path.dirname(_babase.app.env.config_file_path), '_appstate_dump_tb', ) if os.path.exists(tbpath): diff --git a/src/assets/ba_data/python/babase/_env.py b/src/assets/ba_data/python/babase/_env.py index 4ee0b12c..b47fb5cf 100644 --- a/src/assets/ba_data/python/babase/_env.py +++ b/src/assets/ba_data/python/babase/_env.py @@ -6,6 +6,7 @@ from __future__ import annotations import sys import signal import logging +import warnings from typing import TYPE_CHECKING from efro.log import LogLevel @@ -103,6 +104,12 @@ def on_main_thread_start_app() -> None: signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling. _babase.setup_sigint() + # Turn on deprecation warnings. By default these are off for release + # builds except for in __main__. However this is a key way to + # communicate api changes to modders and most modders are running + # release builds so its good to have this on everywhere. + warnings.simplefilter('default', DeprecationWarning) + # Turn off fancy-pants cyclic garbage-collection. We run it only at # explicit times to avoid random hitches and keep things more # deterministic. Non-reference-looped objects will still get cleaned diff --git a/src/assets/ba_data/python/babase/_hooks.py b/src/assets/ba_data/python/babase/_hooks.py index e310e5b3..1bb03363 100644 --- a/src/assets/ba_data/python/babase/_hooks.py +++ b/src/assets/ba_data/python/babase/_hooks.py @@ -354,7 +354,7 @@ def show_client_too_old_error() -> None: # a newer build. if ( _babase.app.config.get('SuppressClientTooOldErrorForBuild') - == _babase.app.build_number + == _babase.app.env.build_number ): return diff --git a/src/assets/ba_data/python/babase/_language.py b/src/assets/ba_data/python/babase/_language.py index 0466c707..059e60ab 100644 --- a/src/assets/ba_data/python/babase/_language.py +++ b/src/assets/ba_data/python/babase/_language.py @@ -68,7 +68,10 @@ class LanguageSubsystem(AppSubsystem): try: names = os.listdir( os.path.join( - _babase.app.data_directory, 'ba_data', 'data', 'languages' + _babase.app.env.data_directory, + 'ba_data', + 'data', + 'languages', ) ) names = [n.replace('.json', '').capitalize() for n in names] @@ -121,7 +124,7 @@ class LanguageSubsystem(AppSubsystem): with open( os.path.join( - _babase.app.data_directory, + _babase.app.env.data_directory, 'ba_data', 'data', 'languages', @@ -139,7 +142,7 @@ class LanguageSubsystem(AppSubsystem): lmodvalues = None else: lmodfile = os.path.join( - _babase.app.data_directory, + _babase.app.env.data_directory, 'ba_data', 'data', 'languages', diff --git a/src/assets/ba_data/python/babase/_meta.py b/src/assets/ba_data/python/babase/_meta.py index 9c67f5f1..5e2a0ec5 100644 --- a/src/assets/ba_data/python/babase/_meta.py +++ b/src/assets/ba_data/python/babase/_meta.py @@ -18,11 +18,6 @@ import _babase if TYPE_CHECKING: from typing import Callable -# The meta api version of this build of the game. -# Only packages and modules requiring this exact api version -# will be considered when scanning directories. -# See: https://ballistica.net/wiki/Meta-Tag-System -CURRENT_API_VERSION = 8 # Meta export lines can use these names to represent these classes. # This is purely a convenience; it is possible to use full class paths @@ -76,14 +71,15 @@ class MetadataSubsystem: """ assert self._scan_complete_cb is None assert self._scan is None + env = _babase.app.env self._scan_complete_cb = scan_complete_cb self._scan = DirectoryScan( [ path for path in [ - _babase.app.python_directory_app, - _babase.app.python_directory_user, + env.python_directory_app, + env.python_directory_user, ] if path is not None ] @@ -212,7 +208,7 @@ class MetadataSubsystem: '${NUM}', str(len(results.incorrect_api_modules) - 1), ), - ('${API}', str(CURRENT_API_VERSION)), + ('${API}', str(_babase.app.env.api_version)), ], ) else: @@ -220,7 +216,7 @@ class MetadataSubsystem: resource='scanScriptsSingleModuleNeedsUpdatesText', subs=[ ('${PATH}', results.incorrect_api_modules[0]), - ('${API}', str(CURRENT_API_VERSION)), + ('${API}', str(_babase.app.env.api_version)), ], ) _babase.screenmessage(msg, color=(1, 0, 0)) @@ -344,13 +340,16 @@ class DirectoryScan: # If we find a module requiring a different api version, warn # and ignore. - if required_api is not None and required_api != CURRENT_API_VERSION: + if ( + required_api is not None + and required_api != _babase.app.env.api_version + ): logging.warning( 'metascan: %s requires api %s but we are running' ' %s. Ignoring module.', subpath, required_api, - CURRENT_API_VERSION, + _babase.app.env.api_version, ) self.results.incorrect_api_modules.append( self._module_name_for_subpath(subpath) diff --git a/src/assets/ba_data/python/babase/modutils.py b/src/assets/ba_data/python/babase/modutils.py index e5bbd69b..240c5cbf 100644 --- a/src/assets/ba_data/python/babase/modutils.py +++ b/src/assets/ba_data/python/babase/modutils.py @@ -18,7 +18,7 @@ def get_human_readable_user_scripts_path() -> str: This is NOT a valid filesystem path; may be something like "(SD Card)". """ app = _babase.app - path: str | None = app.python_directory_user + path: str | None = app.env.python_directory_user if path is None: return '' @@ -66,19 +66,20 @@ def _request_storage_permission() -> bool: def show_user_scripts() -> None: """Open or nicely print the location of the user-scripts directory.""" app = _babase.app + env = app.env # First off, if we need permission for this, ask for it. if _request_storage_permission(): return # If we're running in a nonstandard environment its possible this is unset. - if app.python_directory_user is None: + if env.python_directory_user is None: _babase.screenmessage('') return # Secondly, if the dir doesn't exist, attempt to make it. - if not os.path.exists(app.python_directory_user): - os.makedirs(app.python_directory_user) + if not os.path.exists(env.python_directory_user): + os.makedirs(env.python_directory_user) # On android, attempt to write a file in their user-scripts dir telling # them about modding. This also has the side-effect of allowing us to @@ -88,7 +89,7 @@ def show_user_scripts() -> None: # they can see it. if app.classic is not None and app.classic.platform == 'android': try: - usd: str | None = app.python_directory_user + usd: str | None = env.python_directory_user if usd is not None and os.path.isdir(usd): file_name = usd + '/about_this_folder.txt' with open(file_name, 'w', encoding='utf-8') as outfile: @@ -105,7 +106,7 @@ def show_user_scripts() -> None: # On a few platforms we try to open the dir in the UI. if app.classic is not None and app.classic.platform in ['mac', 'windows']: - _babase.open_dir_externally(app.python_directory_user) + _babase.open_dir_externally(env.python_directory_user) # Otherwise we just print a pretty version of it. else: @@ -120,18 +121,19 @@ def create_user_system_scripts() -> None: import shutil app = _babase.app + env = app.env # First off, if we need permission for this, ask for it. if _request_storage_permission(): return # Its possible these are unset in non-standard environments. - if app.python_directory_user is None: + if env.python_directory_user is None: raise RuntimeError('user python dir unset') - if app.python_directory_app is None: + if env.python_directory_app is None: raise RuntimeError('app python dir unset') - path = app.python_directory_user + '/sys/' + app.version + path = f'{env.python_directory_user}/sys/{env.version}' pathtmp = path + '_tmp' if os.path.exists(path): shutil.rmtree(path) @@ -147,8 +149,8 @@ def create_user_system_scripts() -> None: # /Knowledge-Nuggets#python-cache-files-gotcha return ('__pycache__',) - print(f'COPYING "{app.python_directory_app}" -> "{pathtmp}".') - shutil.copytree(app.python_directory_app, pathtmp, ignore=_ignore_filter) + print(f'COPYING "{env.python_directory_app}" -> "{pathtmp}".') + shutil.copytree(env.python_directory_app, pathtmp, ignore=_ignore_filter) print(f'MOVING "{pathtmp}" -> "{path}".') shutil.move(pathtmp, path) @@ -168,12 +170,12 @@ def delete_user_system_scripts() -> None: """Clean out the scripts created by create_user_system_scripts().""" import shutil - app = _babase.app + env = _babase.app.env - if app.python_directory_user is None: + if env.python_directory_user is None: raise RuntimeError('user python dir unset') - path = app.python_directory_user + '/sys/' + app.version + path = f'{env.python_directory_user}/sys/{env.version}' if os.path.exists(path): shutil.rmtree(path) print( @@ -185,6 +187,6 @@ def delete_user_system_scripts() -> None: print(f"User system scripts not found at '{path}'.") # If the sys path is empty, kill it. - dpath = app.python_directory_user + '/sys' + dpath = env.python_directory_user + '/sys' if os.path.isdir(dpath) and not os.listdir(dpath): os.rmdir(dpath) diff --git a/src/assets/ba_data/python/baclassic/_net.py b/src/assets/ba_data/python/baclassic/_net.py index 3c899900..7fd41d8f 100644 --- a/src/assets/ba_data/python/baclassic/_net.py +++ b/src/assets/ba_data/python/baclassic/_net.py @@ -4,14 +4,12 @@ from __future__ import annotations import copy -import logging import weakref import threading from enum import Enum from typing import TYPE_CHECKING import babase -from babase import DEFAULT_REQUEST_TIMEOUT_SECONDS import bascenev1 if TYPE_CHECKING: @@ -72,6 +70,7 @@ class MasterServerV1CallThread(threading.Thread): def run(self) -> None: # pylint: disable=consider-using-with + # pylint: disable=too-many-branches import urllib.request import urllib.parse import urllib.error @@ -79,20 +78,15 @@ class MasterServerV1CallThread(threading.Thread): from efro.error import is_urllib_communication_error - # If the app is going down, this is a no-op. Trying to avoid the - # rare odd crash I see from (presumably) SSL stuff getting used - # while the app is being torn down. - if babase.app.state is babase.app.State.SHUTTING_DOWN: - logging.warning( - 'MasterServerV1CallThread.run() during app' - ' shutdown is a no-op.' - ) - return - plus = babase.app.plus assert plus is not None response_data: Any = None url: str | None = None + + # Tearing the app down while this is running can lead to + # rare crashes in LibSSL, so avoid that if at all possible. + babase.shutdown_suppress_begin() + try: classic = babase.app.classic assert classic is not None @@ -114,7 +108,7 @@ class MasterServerV1CallThread(threading.Thread): {'User-Agent': classic.legacy_user_agent_string}, ), context=babase.app.net.sslcontext, - timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS, + timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS, ) elif self._request_type == 'post': url = plus.get_master_server_address() + '/' + self._request @@ -126,7 +120,7 @@ class MasterServerV1CallThread(threading.Thread): {'User-Agent': classic.legacy_user_agent_string}, ), context=babase.app.net.sslcontext, - timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS, + timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS, ) else: raise TypeError('Invalid request_type: ' + self._request_type) @@ -160,6 +154,9 @@ class MasterServerV1CallThread(threading.Thread): response_data = None + finally: + babase.shutdown_suppress_end() + if self._callback is not None: babase.pushcall( babase.Call(self._run_callback, response_data), diff --git a/src/assets/ba_data/python/baclassic/_servermode.py b/src/assets/ba_data/python/baclassic/_servermode.py index d9f0f443..8cb9b911 100644 --- a/src/assets/ba_data/python/baclassic/_servermode.py +++ b/src/assets/ba_data/python/baclassic/_servermode.py @@ -214,7 +214,10 @@ class ServerController: babase.app.classic.master_server_v1_get( 'bsAccessCheck', - {'port': bascenev1.get_game_port(), 'b': babase.app.build_number}, + { + 'port': bascenev1.get_game_port(), + 'b': babase.app.env.build_number, + }, callback=self._access_check_response, ) @@ -379,8 +382,8 @@ class ServerController: if self._first_run: curtimestr = time.strftime('%c') startupmsg = ( - f'{Clr.BLD}{Clr.BLU}{babase.appnameupper()} {app.version}' - f' ({app.build_number})' + f'{Clr.BLD}{Clr.BLU}{babase.appnameupper()} {app.env.version}' + f' ({app.env.build_number})' f' entering server-mode {curtimestr}{Clr.RST}' ) logging.info(startupmsg) diff --git a/src/assets/ba_data/python/baclassic/_subsystem.py b/src/assets/ba_data/python/baclassic/_subsystem.py index 4241a6c4..108d5ca4 100644 --- a/src/assets/ba_data/python/baclassic/_subsystem.py +++ b/src/assets/ba_data/python/baclassic/_subsystem.py @@ -153,6 +153,7 @@ class ClassicSubsystem(babase.AppSubsystem): plus = babase.app.plus assert plus is not None + env = babase.app.env cfg = babase.app.config self.music.on_app_loading() @@ -161,11 +162,7 @@ class ClassicSubsystem(babase.AppSubsystem): # Non-test, non-debug builds should generally be blessed; warn if not. # (so I don't accidentally release a build that can't play tourneys) - if ( - not babase.app.debug_build - and not babase.app.test_build - and not plus.is_blessed() - ): + if not env.debug and not env.test and not plus.is_blessed(): babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) # FIXME: This should not be hard-coded. diff --git a/src/assets/ba_data/python/baclassic/_tips.py b/src/assets/ba_data/python/baclassic/_tips.py index f0469ed6..10b4410b 100644 --- a/src/assets/ba_data/python/baclassic/_tips.py +++ b/src/assets/ba_data/python/baclassic/_tips.py @@ -113,7 +113,7 @@ def get_all_tips() -> list[str]: if ( app.classic is not None and app.classic.platform in ('android', 'ios') - and not app.on_tv + and not app.env.tv ): tips += [ ( diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index c806cbf9..20380121 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -52,8 +52,8 @@ if TYPE_CHECKING: # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 21256 -TARGET_BALLISTICA_VERSION = '1.7.26' +TARGET_BALLISTICA_BUILD = 21269 +TARGET_BALLISTICA_VERSION = '1.7.27' @dataclass diff --git a/src/assets/ba_data/python/bascenev1/_coopgame.py b/src/assets/ba_data/python/bascenev1/_coopgame.py index 4e065721..c7c0d085 100644 --- a/src/assets/ba_data/python/bascenev1/_coopgame.py +++ b/src/assets/ba_data/python/bascenev1/_coopgame.py @@ -108,7 +108,7 @@ class CoopGameActivity(GameActivity[PlayerT, TeamT]): ) if not a.complete ] - vrmode = babase.app.vr_mode + vrmode = babase.app.env.vr if achievements: Text( babase.Lstr(resource='achievementsRemainingText'), diff --git a/src/assets/ba_data/python/bascenev1/_gameactivity.py b/src/assets/ba_data/python/bascenev1/_gameactivity.py index 80e51a3f..7135b6d4 100644 --- a/src/assets/ba_data/python/bascenev1/_gameactivity.py +++ b/src/assets/ba_data/python/bascenev1/_gameactivity.py @@ -600,7 +600,7 @@ class GameActivity(Activity[PlayerT, TeamT]): translate=('gameDescriptions', sb_desc_l[0]), subs=subs ) sb_desc = translation - vrmode = babase.app.vr_mode + vrmode = babase.app.env.vr yval = -34 if is_empty else -20 yval -= 16 sbpos = ( @@ -706,7 +706,7 @@ class GameActivity(Activity[PlayerT, TeamT]): resource='epicDescriptionFilterText', subs=[('${DESCRIPTION}', translation)], ) - vrmode = babase.app.vr_mode + vrmode = babase.app.env.vr dnode = _bascenev1.newnode( 'text', attrs={ @@ -761,7 +761,7 @@ class GameActivity(Activity[PlayerT, TeamT]): base_position = (75, 50) tip_scale = 0.8 tip_title_scale = 1.2 - vrmode = babase.app.vr_mode + vrmode = babase.app.env.vr t_offs = -350.0 tnode = _bascenev1.newnode( diff --git a/src/assets/ba_data/python/bascenev1/_gameutils.py b/src/assets/ba_data/python/bascenev1/_gameutils.py index e1ba0b12..814e4bb1 100644 --- a/src/assets/ba_data/python/bascenev1/_gameutils.py +++ b/src/assets/ba_data/python/bascenev1/_gameutils.py @@ -185,7 +185,7 @@ def show_damage_count( # (connected clients may have differing configs so they won't # get the intended results). assert app.classic is not None - do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.vr_mode + do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.env.vr txtnode = _bascenev1.newnode( 'text', attrs={ diff --git a/src/assets/ba_data/python/bascenev1/_lobby.py b/src/assets/ba_data/python/bascenev1/_lobby.py index 2de1cbdf..3bc9b894 100644 --- a/src/assets/ba_data/python/bascenev1/_lobby.py +++ b/src/assets/ba_data/python/bascenev1/_lobby.py @@ -49,7 +49,7 @@ class JoinInfo: if keyboard is not None: self._update_for_keyboard(keyboard) - flatness = 1.0 if babase.app.vr_mode else 0.0 + flatness = 1.0 if babase.app.env.vr else 0.0 self._text = NodeActor( _bascenev1.newnode( 'text', diff --git a/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py b/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py index 64675338..f0cdf5ec 100644 --- a/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py +++ b/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py @@ -76,7 +76,7 @@ class CoopJoinActivity(bs.JoinActivity): ] have_achievements = bool(achievements) achievements = [a for a in achievements if not a.complete] - vrmode = bs.app.vr_mode + vrmode = bs.app.env.vr if have_achievements: Text( bs.Lstr(resource='achievementsRemainingText'), diff --git a/src/assets/ba_data/python/bascenev1lib/actor/background.py b/src/assets/ba_data/python/bascenev1lib/actor/background.py index 49b63dad..2ad629a2 100644 --- a/src/assets/ba_data/python/bascenev1lib/actor/background.py +++ b/src/assets/ba_data/python/bascenev1lib/actor/background.py @@ -74,7 +74,7 @@ class Background(bs.Actor): self.node.connectattr('opacity', self.logo, 'opacity') # add jitter/pulse for a stop-motion-y look unless we're in VR # in which case stillness is better - if not bs.app.vr_mode: + if not bs.app.env.vr: self.cmb = bs.newnode( 'combine', owner=self.node, attrs={'size': 2} ) diff --git a/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py b/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py index a847edaf..18854e5d 100644 --- a/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py +++ b/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py @@ -208,13 +208,13 @@ class ControlsGuide(bs.Actor): clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0) self._run_text_pos_top = (position[0], position[1] - 135.0 * scale) self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale) - sval = 1.0 * scale if bs.app.vr_mode else 0.8 * scale + sval = 1.0 * scale if bs.app.env.vr else 0.8 * scale self._run_text = bs.newnode( 'text', attrs={ 'scale': sval, 'host_only': True, - 'shadow': 1.0 if bs.app.vr_mode else 0.5, + 'shadow': 1.0 if bs.app.env.vr else 0.5, 'flatness': 1.0, 'maxwidth': 380, 'v_align': 'top', diff --git a/src/assets/ba_data/python/bascenev1lib/actor/scoreboard.py b/src/assets/ba_data/python/bascenev1lib/actor/scoreboard.py index 0cdc8b5f..05a1ba50 100644 --- a/src/assets/ba_data/python/bascenev1lib/actor/scoreboard.py +++ b/src/assets/ba_data/python/bascenev1lib/actor/scoreboard.py @@ -45,7 +45,7 @@ class _Entry: # FIXME: Should not do things conditionally for vr-mode, as there may # be non-vr clients connected which will also get these value. - vrmode = bs.app.vr_mode + vrmode = bs.app.env.vr if self._do_cover: if vrmode: diff --git a/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py b/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py index ad6276cd..e50a16a9 100644 --- a/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py +++ b/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py @@ -69,7 +69,7 @@ class ZoomText(bs.Actor): ) # we never jitter in vr mode.. - if bs.app.vr_mode: + if bs.app.env.vr: jitter = 0.0 # if they want jitter, animate its position slightly... diff --git a/src/assets/ba_data/python/bascenev1lib/game/runaround.py b/src/assets/ba_data/python/bascenev1lib/game/runaround.py index fb9d8dc5..6640601f 100644 --- a/src/assets/ba_data/python/bascenev1lib/game/runaround.py +++ b/src/assets/ba_data/python/bascenev1lib/game/runaround.py @@ -478,7 +478,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): ) # FIXME; should not set things based on vr mode. # (won't look right to non-vr connected clients, etc) - vrmode = bs.app.vr_mode + vrmode = bs.app.env.vr self._lives_text = bs.NodeActor( bs.newnode( 'text', diff --git a/src/assets/ba_data/python/bascenev1lib/mainmenu.py b/src/assets/ba_data/python/bascenev1lib/mainmenu.py index d6663c21..dce9b4a9 100644 --- a/src/assets/ba_data/python/bascenev1lib/mainmenu.py +++ b/src/assets/ba_data/python/bascenev1lib/mainmenu.py @@ -50,6 +50,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): super().on_transition_in() random.seed(123) app = bs.app + env = app.env assert app.classic is not None plus = bui.app.plus @@ -59,7 +60,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): # the host is VR mode or not (clients may differ in that regard). # Any differences need to happen at the engine level so everyone # sees things in their own optimal way. - vr_mode = bs.app.vr_mode + vr_mode = bs.app.env.vr if not bs.app.toolbar_test: color = (1.0, 1.0, 1.0, 1.0) if vr_mode else (0.5, 0.6, 0.5, 0.6) @@ -117,7 +118,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): # the host is vr mode or not (clients may not be or vice versa). # Any differences need to happen at the engine level so everyone sees # things in their own optimal way. - vr_mode = app.vr_mode + vr_mode = app.env.vr uiscale = app.ui_v1.uiscale # In cases where we're doing lots of dev work lets always show the @@ -125,13 +126,13 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): force_show_build_number = False if not bs.app.toolbar_test: - if app.debug_build or app.test_build or force_show_build_number: - if app.debug_build: + if env.debug or env.test or force_show_build_number: + if env.debug: text = bs.Lstr( value='${V} (${B}) (${D})', subs=[ - ('${V}', app.version), - ('${B}', str(app.build_number)), + ('${V}', app.env.version), + ('${B}', str(app.env.build_number)), ('${D}', bs.Lstr(resource='debugText')), ], ) @@ -139,12 +140,12 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): text = bs.Lstr( value='${V} (${B})', subs=[ - ('${V}', app.version), - ('${B}', str(app.build_number)), + ('${V}', app.env.version), + ('${B}', str(app.env.build_number)), ], ) else: - text = bs.Lstr(value='${V}', subs=[('${V}', app.version)]) + text = bs.Lstr(value='${V}', subs=[('${V}', app.env.version)]) scale = 0.9 if (uiscale is bs.UIScale.SMALL or vr_mode) else 0.7 color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7) self.version = bs.NodeActor( @@ -170,7 +171,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): # Throw in test build info. self.beta_info = self.beta_info_2 = None - if app.test_build and not (app.demo_mode or app.arcade_mode): + if env.test and not (app.demo_mode or app.arcade_mode): pos = (230, 35) self.beta_info = bs.NodeActor( bs.newnode( @@ -655,7 +656,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): # Add a bit of stop-motion-y jitter to the logo # (unless we're in VR mode in which case its best to # leave things still). - if not bs.app.vr_mode: + if not bs.app.env.vr: cmb: bs.Node | None cmb2: bs.Node | None if not shadow: @@ -774,7 +775,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): # (unless we're in VR mode in which case its best to # leave things still). assert logo.node - if not bs.app.vr_mode: + if not bs.app.env.vr: cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) cmb.connectattr('output', logo.node, 'position') keys = {} @@ -882,7 +883,7 @@ class NewsDisplay: self._phrases.insert(0, phr) val = self._phrases.pop() if val == '__ACH__': - vrmode = app.vr_mode + vrmode = app.env.vr Text( bs.Lstr(resource='nextAchievementsText'), color=((1, 1, 1, 1) if vrmode else (0.95, 0.9, 1, 0.4)), @@ -948,7 +949,7 @@ class NewsDisplay: # Show upcoming achievements in non-vr versions # (currently too hard to read in vr). - self._used_phrases = (['__ACH__'] if not bs.app.vr_mode else []) + [ + self._used_phrases = (['__ACH__'] if not bs.app.env.vr else []) + [ s for s in news.split('
\n') if s != '' ] self._phrase_change_timer = bs.Timer( @@ -960,12 +961,12 @@ class NewsDisplay: assert bs.app.classic is not None scl = ( 1.2 - if (bs.app.ui_v1.uiscale is bs.UIScale.SMALL or bs.app.vr_mode) + if (bs.app.ui_v1.uiscale is bs.UIScale.SMALL or bs.app.env.vr) else 0.8 ) - color2 = (1, 1, 1, 1) if bs.app.vr_mode else (0.7, 0.65, 0.75, 1.0) - shadow = 1.0 if bs.app.vr_mode else 0.4 + color2 = (1, 1, 1, 1) if bs.app.env.vr else (0.7, 0.65, 0.75, 1.0) + shadow = 1.0 if bs.app.env.vr else 0.4 self._text = bs.NodeActor( bs.newnode( 'text', diff --git a/src/assets/ba_data/python/bauiv1lib/account/viewer.py b/src/assets/ba_data/python/bauiv1lib/account/viewer.py index 6c5c4800..914bd930 100644 --- a/src/assets/ba_data/python/bauiv1lib/account/viewer.py +++ b/src/assets/ba_data/python/bauiv1lib/account/viewer.py @@ -141,7 +141,7 @@ class AccountViewerWindow(PopupWindow): bui.app.classic.master_server_v1_get( 'bsAccountInfo', { - 'buildNumber': bui.app.build_number, + 'buildNumber': bui.app.env.build_number, 'accountID': self._account_id, 'profileID': self._profile_id, }, diff --git a/src/assets/ba_data/python/bauiv1lib/configerror.py b/src/assets/ba_data/python/bauiv1lib/configerror.py index da87065d..c64a31d6 100644 --- a/src/assets/ba_data/python/bauiv1lib/configerror.py +++ b/src/assets/ba_data/python/bauiv1lib/configerror.py @@ -11,7 +11,7 @@ class ConfigErrorWindow(bui.Window): """Window for dealing with a broken config.""" def __init__(self) -> None: - self._config_file_path = bui.app.config_file_path + self._config_file_path = bui.app.env.config_file_path width = 800 super().__init__( bui.containerwidget(size=(width, 400), transition='in_right') diff --git a/src/assets/ba_data/python/bauiv1lib/confirm.py b/src/assets/ba_data/python/bauiv1lib/confirm.py index dfa9a697..f378a94c 100644 --- a/src/assets/ba_data/python/bauiv1lib/confirm.py +++ b/src/assets/ba_data/python/bauiv1lib/confirm.py @@ -197,13 +197,19 @@ class QuitWindow: time=0.2, endcall=lambda: bui.quit(soft=True, back=self._back), ) + + # Prevent the user from doing anything else while we're on our + # way out. bui.lock_all_input() - # Unlock and fade back in shortly. Just in case something goes - # wrong (or on Android where quit just backs out of our activity - # and we may come back after). - def _come_back() -> None: - bui.unlock_all_input() - bui.fade_screen(True, time=0.1) + # On systems supporting soft-quit, unlock and fade back in shortly + # (soft-quit basically just backgrounds/hides the app). + if bui.app.env.supports_soft_quit: + # Unlock and fade back in shortly. Just in case something goes + # wrong (or on Android where quit just backs out of our activity + # and we may come back after). + def _come_back() -> None: + bui.unlock_all_input() + bui.fade_screen(True) - bui.apptimer(0.3, _come_back) + bui.apptimer(0.5, _come_back) diff --git a/src/assets/ba_data/python/bauiv1lib/creditslist.py b/src/assets/ba_data/python/bauiv1lib/creditslist.py index 869d410f..e6a0769f 100644 --- a/src/assets/ba_data/python/bauiv1lib/creditslist.py +++ b/src/assets/ba_data/python/bauiv1lib/creditslist.py @@ -212,7 +212,10 @@ class CreditsListWindow(bui.Window): try: with open( os.path.join( - bui.app.data_directory, 'ba_data', 'data', 'langdata.json' + bui.app.env.data_directory, + 'ba_data', + 'data', + 'langdata.json', ), encoding='utf-8', ) as infile: diff --git a/src/assets/ba_data/python/bauiv1lib/feedback.py b/src/assets/ba_data/python/bauiv1lib/feedback.py index d3074aca..09568a3f 100644 --- a/src/assets/ba_data/python/bauiv1lib/feedback.py +++ b/src/assets/ba_data/python/bauiv1lib/feedback.py @@ -15,7 +15,7 @@ def ask_for_rating() -> bui.Widget | None: subplatform = app.classic.subplatform # FIXME: should whitelist platforms we *do* want this for. - if bui.app.test_build: + if bui.app.env.test: return None if not ( platform == 'mac' diff --git a/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py b/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py index a03597ce..66c3f480 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py @@ -43,7 +43,7 @@ class AboutGatherTab(GatherTab): # Let's not talk about sharing in vr-mode; its tricky to fit more # than one head in a VR-headset ;-) - if not bui.app.vr_mode: + if not bui.app.env.vr: message = bui.Lstr( value='${A}\n\n${B}', subs=[ diff --git a/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py b/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py index 5d4714c9..3202d812 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py @@ -1010,7 +1010,7 @@ class ManualGatherTab(GatherTab): self._t_accessible_extra = t_accessible_extra bui.app.classic.master_server_v1_get( 'bsAccessCheck', - {'b': bui.app.build_number}, + {'b': bui.app.env.build_number}, callback=bui.WeakCall(self._on_accessible_response), ) diff --git a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py index d8b1e2b1..fa592372 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py @@ -1327,7 +1327,7 @@ class PublicGatherTab(GatherTab): ) bui.app.classic.master_server_v1_get( 'bsAccessCheck', - {'b': bui.app.build_number}, + {'b': bui.app.env.build_number}, callback=bui.WeakCall(self._on_public_party_accessible_response), ) diff --git a/src/assets/ba_data/python/bauiv1lib/getcurrency.py b/src/assets/ba_data/python/bauiv1lib/getcurrency.py index 2127864b..a87ec18f 100644 --- a/src/assets/ba_data/python/bauiv1lib/getcurrency.py +++ b/src/assets/ba_data/python/bauiv1lib/getcurrency.py @@ -621,7 +621,7 @@ class GetCurrencyWindow(bui.Window): app = bui.app assert app.classic is not None if ( - app.test_build + app.env.test or ( app.classic.platform == 'android' and app.classic.subplatform in ['oculus', 'cardboard'] @@ -664,8 +664,8 @@ class GetCurrencyWindow(bui.Window): 'item': item, 'platform': app.classic.platform, 'subplatform': app.classic.subplatform, - 'version': app.version, - 'buildNumber': app.build_number, + 'version': app.env.version, + 'buildNumber': app.env.build_number, }, callback=bui.WeakCall(self._purchase_check_result, item), ) diff --git a/src/assets/ba_data/python/bauiv1lib/helpui.py b/src/assets/ba_data/python/bauiv1lib/helpui.py index 1b430292..4c404992 100644 --- a/src/assets/ba_data/python/bauiv1lib/helpui.py +++ b/src/assets/ba_data/python/bauiv1lib/helpui.py @@ -353,7 +353,7 @@ class HelpWindow(bui.Window): v -= spacing * 45.0 txt = ( bui.Lstr(resource=self._r + '.devicesText').evaluate() - if app.vr_mode + if app.env.vr else bui.Lstr(resource=self._r + '.controllersText').evaluate() ) txt_scale = 0.74 @@ -372,7 +372,7 @@ class HelpWindow(bui.Window): ) txt_scale = 0.7 - if not app.vr_mode: + if not app.env.vr: infotxt = '.controllersInfoText' txt = bui.Lstr( resource=self._r + infotxt, diff --git a/src/assets/ba_data/python/bauiv1lib/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py index 4925d0eb..78d6b0ea 100644 --- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py +++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py @@ -117,7 +117,7 @@ class MainMenuWindow(bui.Window): force_test = False bs.get_local_active_input_devices_count() if ( - (app.on_tv or app.classic.platform == 'mac') + (app.env.tv or app.classic.platform == 'mac') and bui.app.config.get('launchCount', 0) <= 1 ) or force_test: diff --git a/src/assets/ba_data/python/bauiv1lib/popup.py b/src/assets/ba_data/python/bauiv1lib/popup.py index 97a9fc2e..34c591ca 100644 --- a/src/assets/ba_data/python/bauiv1lib/popup.py +++ b/src/assets/ba_data/python/bauiv1lib/popup.py @@ -34,7 +34,7 @@ class PopupWindow: focus_size = size # In vr mode we can't have windows going outside the screen. - if bui.app.vr_mode: + if bui.app.env.vr: focus_size = size focus_position = (0, 0) diff --git a/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py b/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py index 61fe52ae..b4f0243a 100644 --- a/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py +++ b/src/assets/ba_data/python/bauiv1lib/profile/upgrade.py @@ -155,7 +155,7 @@ class ProfileUpgradeWindow(bui.Window): bui.app.classic.master_server_v1_get( 'bsGlobalProfileCheck', - {'name': self._name, 'b': bui.app.build_number}, + {'name': self._name, 'b': bui.app.env.build_number}, callback=bui.WeakCall(self._profile_check_result), ) self._cost = plus.get_v1_account_misc_read_val( diff --git a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py index 25dcc443..58d730c7 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py @@ -88,7 +88,7 @@ class AdvancedSettingsWindow(bui.Window): # In vr-mode, the internal keyboard is currently the *only* option, # so no need to show this. - self._show_always_use_internal_keyboard = not app.vr_mode + self._show_always_use_internal_keyboard = not app.env.vr self._scroll_width = self._width - (100 + 2 * x_inset) self._scroll_height = self._height - 115.0 @@ -102,7 +102,7 @@ class AdvancedSettingsWindow(bui.Window): if self._show_disable_gyro: self._sub_height += 42 - self._do_vr_test_button = app.vr_mode + self._do_vr_test_button = app.env.vr self._do_net_test_button = True self._extra_button_spacing = self._spacing * 2.5 @@ -178,7 +178,7 @@ class AdvancedSettingsWindow(bui.Window): # Fetch the list of completed languages. bui.app.classic.master_server_v1_get( 'bsLangGetCompleted', - {'b': app.build_number}, + {'b': app.env.build_number}, callback=bui.WeakCall(self._completed_langs_cb), ) @@ -322,7 +322,10 @@ class AdvancedSettingsWindow(bui.Window): with open( os.path.join( - bui.app.data_directory, 'ba_data', 'data', 'langdata.json' + bui.app.env.data_directory, + 'ba_data', + 'data', + 'langdata.json', ), encoding='utf-8', ) as infile: diff --git a/src/assets/ba_data/python/bauiv1lib/settings/controls.py b/src/assets/ba_data/python/bauiv1lib/settings/controls.py index f23ecda6..2d9e4c33 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/controls.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/controls.py @@ -47,14 +47,14 @@ class ControlsSettingsWindow(bui.Window): space_height = spacing * 0.3 - # FIXME: should create vis settings in platform for these, - # not hard code them here. + # FIXME: should create vis settings under platform or app-adapter + # to determine whether to show this stuff; not hard code it. show_gamepads = False platform = app.classic.platform subplatform = app.classic.subplatform non_vr_windows = platform == 'windows' and ( - subplatform != 'oculus' or not app.vr_mode + subplatform != 'oculus' or not app.env.vr ) if platform in ('linux', 'android', 'mac') or non_vr_windows: show_gamepads = True @@ -70,11 +70,12 @@ class ControlsSettingsWindow(bui.Window): show_space_1 = True height += space_height + print('hello') show_keyboard = False if bs.getinputdevice('Keyboard', '#1', doraise=False) is not None: show_keyboard = True height += spacing - show_keyboard_p2 = False if app.vr_mode else show_keyboard + show_keyboard_p2 = False if app.env.vr else show_keyboard if show_keyboard_p2: height += spacing @@ -91,7 +92,7 @@ class ControlsSettingsWindow(bui.Window): # On windows (outside of oculus/vr), show an option to disable xinput. show_xinput_toggle = False - if platform == 'windows' and not app.vr_mode: + if platform == 'windows' and not app.env.vr: show_xinput_toggle = True # On mac builds, show an option to switch between generic and @@ -352,6 +353,7 @@ class ControlsSettingsWindow(bui.Window): maxwidth=width * 0.8, ) v -= spacing * 1.5 + self._restore_state() def _set_mac_controller_subsystem(self, val: str) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py index d708551c..c8cc20a1 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py @@ -829,7 +829,7 @@ class GamepadSettingsWindow(bui.Window): 'controllerConfig', { 'ua': classic.legacy_user_agent_string, - 'b': bui.app.build_number, + 'b': bui.app.env.build_number, 'name': self._name, 'inputMapHash': inputhash, 'config': dst2, diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepadadvanced.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepadadvanced.py index fae272ae..ae50be7b 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/gamepadadvanced.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepadadvanced.py @@ -91,7 +91,7 @@ class GamepadAdvancedSettingsWindow(bui.Window): self._sub_height = ( 940 if self._parent_window.get_is_secondary() else 1040 ) - if app.vr_mode: + if app.env.vr: self._sub_height += 50 self._scrollwidget = bui.scrollwidget( parent=self._root_widget, @@ -183,7 +183,7 @@ class GamepadAdvancedSettingsWindow(bui.Window): ) # in vr mode, allow assigning a reset-view button - if app.vr_mode: + if app.env.vr: v -= 50 self._capture_button( pos=(h2, v), diff --git a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py index 06ea503b..f95a5547 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py @@ -61,7 +61,7 @@ class GraphicsSettingsWindow(bui.Window): show_vsync = True show_resolution = True - if app.vr_mode: + if app.env.vr: show_resolution = ( app.classic.platform == 'android' and app.classic.subplatform == 'cardboard' @@ -400,7 +400,7 @@ class GraphicsSettingsWindow(bui.Window): ) # (tv mode doesnt apply to vr) - if not bui.app.vr_mode: + if not bui.app.env.vr: tvc = ConfigCheckBox( parent=self._root_widget, position=(240, v - 6), diff --git a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py index 47403ceb..6f37aabb 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py @@ -301,7 +301,7 @@ class ConfigKeyboardWindow(bui.Window): { 'ua': bui.app.classic.legacy_user_agent_string, 'name': self._name, - 'b': bui.app.build_number, + 'b': bui.app.env.build_number, 'config': dst2, 'v': 2, }, diff --git a/src/assets/ba_data/python/bauiv1lib/specialoffer.py b/src/assets/ba_data/python/bauiv1lib/specialoffer.py index 057d5a53..7fe7729c 100644 --- a/src/assets/ba_data/python/bauiv1lib/specialoffer.py +++ b/src/assets/ba_data/python/bauiv1lib/specialoffer.py @@ -44,14 +44,14 @@ class SpecialOfferWindow(bui.Window): real_price = plus.get_price( 'pro' if offer['item'] == 'pro_fullprice' else 'pro_sale' ) - if real_price is None and bui.app.debug_build: + if real_price is None and bui.app.env.debug: print('NOTE: Faking prices for debug build.') real_price = '$1.23' zombie = real_price is None elif isinstance(offer['price'], str): # (a string price implies IAP id) real_price = plus.get_price(offer['price']) - if real_price is None and bui.app.debug_build: + if real_price is None and bui.app.env.debug: print('NOTE: Faking price for debug build.') real_price = '$1.23' zombie = real_price is None diff --git a/src/assets/ba_data/python/bauiv1lib/store/browser.py b/src/assets/ba_data/python/bauiv1lib/store/browser.py index 43ce844a..6365b879 100644 --- a/src/assets/ba_data/python/bauiv1lib/store/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/store/browser.py @@ -566,8 +566,8 @@ class StoreBrowserWindow(bui.Window): 'item': item, 'platform': app.classic.platform, 'subplatform': app.classic.subplatform, - 'version': app.version, - 'buildNumber': app.build_number, + 'version': app.env.version, + 'buildNumber': app.env.build_number, 'purchaseType': 'ticket' if is_ticket_purchase else 'real', }, callback=bui.WeakCall( diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc index 3ab54f02..c757166c 100644 --- a/src/ballistica/base/base.cc +++ b/src/ballistica/base/base.cc @@ -715,5 +715,10 @@ void BaseFeatureSet::DoPushObjCall(const PythonObjectSetBase* objset, int id, } auto BaseFeatureSet::IsAppStarted() const -> bool { return app_started_; } +void BaseFeatureSet::ShutdownSuppressBegin() { shutdown_suppress_count_++; } +void BaseFeatureSet::ShutdownSuppressEnd() { + shutdown_suppress_count_--; + assert(shutdown_suppress_count_ >= 0); +} } // namespace ballistica::base diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h index b8211f92..c142b604 100644 --- a/src/ballistica/base/base.h +++ b/src/ballistica/base/base.h @@ -690,6 +690,9 @@ class BaseFeatureSet : public FeatureSetNativeComponent, void DoPushObjCall(const PythonObjectSetBase* objset, int id, const std::string& arg) override; void OnReachedEndOfBaBaseImport(); + void ShutdownSuppressBegin(); + void ShutdownSuppressEnd(); + auto shutdown_suppress_count() const { return shutdown_suppress_count_; } /// Called in the logic thread once our screen is up and assets are /// loading. @@ -748,6 +751,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent, StressTest* stress_test_; std::string console_startup_messages_; + int shutdown_suppress_count_{}; bool tried_importing_plus_{}; bool tried_importing_classic_{}; bool tried_importing_ui_v1_{}; diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc index 08914000..0029b232 100644 --- a/src/ballistica/base/graphics/graphics.cc +++ b/src/ballistica/base/graphics/graphics.cc @@ -1931,8 +1931,10 @@ auto Graphics::ScreenMessageEntry::GetText() -> TextGroup& { return *s_mesh_; } -void Graphics::OnScreenSizeChange(float virtual_width, float virtual_height, - float pixel_width, float pixel_height) { +void Graphics::OnScreenSizeChange() {} + +void Graphics::SetScreenSize(float virtual_width, float virtual_height, + float pixel_width, float pixel_height) { assert(g_base->InLogicThread()); res_x_virtual_ = virtual_width; res_y_virtual_ = virtual_height; diff --git a/src/ballistica/base/graphics/graphics.h b/src/ballistica/base/graphics/graphics.h index adc46b47..4708b7e4 100644 --- a/src/ballistica/base/graphics/graphics.h +++ b/src/ballistica/base/graphics/graphics.h @@ -54,9 +54,11 @@ class Graphics { void OnAppPause(); void OnAppResume(); void OnAppShutdown(); + void OnScreenSizeChange(); void DoApplyAppConfig(); - void OnScreenSizeChange(float virtual_width, float virtual_height, - float physical_width, float physical_height); + + void SetScreenSize(float virtual_width, float virtual_height, + float physical_width, float physical_height); void StepDisplayTime(); static auto IsShaderTransparent(ShadingType c) -> bool; diff --git a/src/ballistica/base/graphics/graphics_server.cc b/src/ballistica/base/graphics/graphics_server.cc index 64f9f6d1..572b7804 100644 --- a/src/ballistica/base/graphics/graphics_server.cc +++ b/src/ballistica/base/graphics/graphics_server.cc @@ -419,9 +419,10 @@ void GraphicsServer::HandleFullContextScreenRebuild( UpdateVirtualScreenRes(); - // Inform the logic thread of the latest values. + // Inform graphics client and logic thread subsystems of the change. g_base->logic->event_loop()->PushCall( [vx = res_x_virtual_, vy = res_y_virtual_, x = res_x_, y = res_y_] { + g_base->graphics->SetScreenSize(vx, vy, x, y); g_base->logic->OnScreenSizeChange(vx, vy, x, y); }); } @@ -568,9 +569,10 @@ void GraphicsServer::SetScreenResolution(float h, float v) { renderer_->ScreenSizeChanged(); } - // Inform logic thread of the change. + // Inform graphics client and logic thread subsystems of the change. g_base->logic->event_loop()->PushCall( [vx = res_x_virtual_, vy = res_y_virtual_, x = res_x_, y = res_y_] { + g_base->graphics->SetScreenSize(vx, vy, x, y); g_base->logic->OnScreenSizeChange(vx, vy, x, y); }); } diff --git a/src/ballistica/base/logic/logic.cc b/src/ballistica/base/logic/logic.cc index dfc4f3cf..75b62fa1 100644 --- a/src/ballistica/base/logic/logic.cc +++ b/src/ballistica/base/logic/logic.cc @@ -7,6 +7,7 @@ #include "ballistica/base/audio/audio.h" #include "ballistica/base/input/input.h" #include "ballistica/base/networking/networking.h" +#include "ballistica/base/platform/base_platform.h" #include "ballistica/base/python/base_python.h" #include "ballistica/base/support/plus_soft.h" #include "ballistica/base/support/stdio_console.h" @@ -58,6 +59,7 @@ void Logic::OnAppStart() { // it will be the most variable; that way it will interact with other // subsystems in their normal states which is less likely to lead to // problems. + g_base->platform->OnAppStart(); g_base->graphics->OnAppStart(); g_base->audio->OnAppStart(); g_base->input->OnAppStart(); @@ -184,6 +186,7 @@ void Logic::OnAppPause() { g_base->input->OnAppPause(); g_base->audio->OnAppPause(); g_base->graphics->OnAppPause(); + g_base->platform->OnAppPause(); } void Logic::OnAppResume() { @@ -191,6 +194,7 @@ void Logic::OnAppResume() { assert(g_base->CurrentContext().IsEmpty()); // Note: keep these in the same order as OnAppStart. + g_base->platform->OnAppResume(); g_base->graphics->OnAppResume(); g_base->audio->OnAppResume(); g_base->input->OnAppResume(); @@ -235,6 +239,7 @@ void Logic::OnAppShutdown() { g_base->input->OnAppShutdown(); g_base->audio->OnAppShutdown(); g_base->graphics->OnAppShutdown(); + g_base->platform->OnAppShutdown(); } void Logic::CompleteShutdown() { @@ -283,12 +288,10 @@ void Logic::OnScreenSizeChange(float virtual_width, float virtual_height, float pixel_width, float pixel_height) { assert(g_base->InLogicThread()); - // First, pass the new values to the graphics subsystem. Then inform - // everyone else simply that they changed; they can ask g_graphics for - // whatever specific values they need. Note: keep these in the same order - // as OnAppStart. - g_base->graphics->OnScreenSizeChange(virtual_width, virtual_height, - pixel_width, pixel_height); + // Inform all subsystems. + // Note: keep these in the same order as OnAppStart. + g_base->platform->OnScreenSizeChange(); + g_base->graphics->OnScreenSizeChange(); g_base->audio->OnScreenSizeChange(); g_base->input->OnScreenSizeChange(); g_base->ui->OnScreenSizeChange(); diff --git a/src/ballistica/base/platform/base_platform.cc b/src/ballistica/base/platform/base_platform.cc index d3e5ccc7..f3d3ff66 100644 --- a/src/ballistica/base/platform/base_platform.cc +++ b/src/ballistica/base/platform/base_platform.cc @@ -314,4 +314,11 @@ void BasePlatform::GetCursorPosition(float* x, float* y) { void BasePlatform::OnMainThreadStartAppComplete() {} +void BasePlatform::OnAppStart() { assert(g_base->InLogicThread()); } +void BasePlatform::OnAppPause() { assert(g_base->InLogicThread()); } +void BasePlatform::OnAppResume() { assert(g_base->InLogicThread()); } +void BasePlatform::OnAppShutdown() { assert(g_base->InLogicThread()); } +void BasePlatform::OnScreenSizeChange() { assert(g_base->InLogicThread()); } +void BasePlatform::DoApplyAppConfig() { assert(g_base->InLogicThread()); } + } // namespace ballistica::base diff --git a/src/ballistica/base/platform/base_platform.h b/src/ballistica/base/platform/base_platform.h index f9270d38..61a2cc82 100644 --- a/src/ballistica/base/platform/base_platform.h +++ b/src/ballistica/base/platform/base_platform.h @@ -26,6 +26,13 @@ class BasePlatform { /// start talking to them. virtual void OnMainThreadStartAppComplete(); + virtual void OnAppStart(); + virtual void OnAppPause(); + virtual void OnAppResume(); + virtual void OnAppShutdown(); + virtual void OnScreenSizeChange(); + virtual void DoApplyAppConfig(); + #pragma mark IN APP PURCHASES -------------------------------------------------- void Purchase(const std::string& item); diff --git a/src/ballistica/base/python/base_python.cc b/src/ballistica/base/python/base_python.cc index 2960f3b2..6662d58b 100644 --- a/src/ballistica/base/python/base_python.cc +++ b/src/ballistica/base/python/base_python.cc @@ -7,6 +7,7 @@ #include "ballistica/base/python/class/python_class_context_call.h" #include "ballistica/base/python/class/python_class_context_ref.h" #include "ballistica/base/python/class/python_class_display_timer.h" +#include "ballistica/base/python/class/python_class_env.h" #include "ballistica/base/python/class/python_class_feature_set_data.h" #include "ballistica/base/python/class/python_class_simple_sound.h" #include "ballistica/base/python/class/python_class_vec3.h" @@ -45,6 +46,7 @@ void BasePython::AddPythonClasses(PyObject* module) { PythonModuleBuilder::AddClass(module); PythonModuleBuilder::AddClass(module); PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); PythonModuleBuilder::AddClass(module); PythonModuleBuilder::AddClass(module); PyObject* vec3 = PythonModuleBuilder::AddClass(module); diff --git a/src/ballistica/base/python/class/python_class_env.cc b/src/ballistica/base/python/class/python_class_env.cc new file mode 100644 index 00000000..75a1b0b8 --- /dev/null +++ b/src/ballistica/base/python/class/python_class_env.cc @@ -0,0 +1,223 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/class/python_class_env.h" + +#include "ballistica/base/base.h" +#include "ballistica/core/platform/core_platform.h" +namespace ballistica::base { + +struct EnvEntry_ { + PyObject* obj; + const char* typestr; + const char* docs; +}; + +static std::map* g_entries_{}; + +auto PythonClassEnv::type_name() -> const char* { return "Env"; } + +static auto BoolEntry_(bool val, const char* docs) -> EnvEntry_ { + PyObject* pyval = val ? Py_True : Py_False; + Py_INCREF(pyval); + return {pyval, "bool", docs}; +} + +static auto StrEntry_(const char* val, const char* docs) -> EnvEntry_ { + return {PyUnicode_FromString(val), "str", docs}; +} + +static auto OptionalStrEntry_(const char* val, const char* docs) -> EnvEntry_ { + if (val) { + return {PyUnicode_FromString(val), "str | None", docs}; + } else { + Py_INCREF(Py_None); + return {Py_None, "str | None", docs}; + } +} + +static auto IntEntry_(int val, const char* docs) -> EnvEntry_ { + return {PyLong_FromLong(val), "int", docs}; +} + +void PythonClassEnv::SetupType(PyTypeObject* cls) { + // Dynamically allocate this since Python needs to keep it around. + auto* docsptr = new std::string( + "Unchanging values for the current running app instance.\n" + "Access the single shared instance of this class at `babase.app.env`.\n" + "\n" + "Attributes:\n"); + auto& docs{*docsptr}; + + // Populate our static entries dict. We'll generate Python class docs + // from that so we don't have to manually keep doc strings in sync. + assert(!g_entries_); + assert(Python::HaveGIL()); + g_entries_ = new std::map(); + auto& envs{*g_entries_}; + + envs["android"] = BoolEntry_(g_buildconfig.ostype_android(), + "Is this build targeting an Android based OS?"); + + envs["build_number"] = IntEntry_( + kEngineBuildNumber, + "Integer build number for the engine.\n" + "\n" + "This value increases by at least 1 with each release of the engine.\n" + "It is independent of the human readable `version` string."); + + envs["version"] = StrEntry_( + kEngineVersion, + "Human-readable version string for the engine; something like '1.3.24'.\n" + "\n" + "This should not be interpreted as a number; it may contain\n" + "string elements such as 'alpha', 'beta', 'test', etc.\n" + "If a numeric version is needed, use `build_number`."); + + envs["device_name"] = + StrEntry_(g_core->platform->GetDeviceName().c_str(), + "Human readable name of the device running this app."); + + envs["supports_soft_quit"] = BoolEntry_( + g_buildconfig.ostype_android() || g_buildconfig.ostype_ios_tvos(), + "Whether the running app supports 'soft' quit options.\n" + "\n" + "This generally applies to mobile derived OSs, where an act of\n" + "'quitting' may leave the app running in the background waiting\n" + "in case it is used again."); + + envs["debug"] = BoolEntry_( + g_buildconfig.debug_build(), + "Whether the app is running in debug mode.\n" + "\n" + "Debug builds generally run substantially slower than non-debug\n" + "builds due to compiler optimizations being disabled and extra\n" + "checks being run."); + + envs["test"] = BoolEntry_( + g_buildconfig.test_build(), + "Whether the app is running in test mode.\n" + "\n" + "Test mode enables extra checks and features that are useful for\n" + "release testing but which do not slow the game down significantly."); + + envs["config_file_path"] = + StrEntry_(g_core->platform->GetConfigFilePath().c_str(), + "Where the app's config file is stored on disk."); + + envs["data_directory"] = StrEntry_(g_core->GetDataDirectory().c_str(), + "Where bundled static app data lives."); + + envs["api_version"] = IntEntry_( + kEngineApiVersion, + "The app's api version.\n" + "\n" + "Only Python modules and packages associated with the current API\n" + "version number will be detected by the game (see the ba_meta tag).\n" + "This value will change whenever substantial backward-incompatible\n" + "changes are introduced to Ballistica APIs. When that happens,\n" + "modules/packages should be updated accordingly and set to target\n" + "the newer API version number."); + + std::optional user_py_dir = g_core->GetUserPythonDirectory(); + envs["python_directory_user"] = OptionalStrEntry_( + user_py_dir ? user_py_dir->c_str() : nullptr, + "Path where the app expects its user scripts (mods) to live.\n" + "\n" + "Be aware that this value may be None if Ballistica is running in\n" + "a non-standard environment, and that python-path modifications may\n" + "cause modules to be loaded from other locations."); + + std::optional app_py_dir = g_core->GetAppPythonDirectory(); + envs["python_directory_app"] = OptionalStrEntry_( + app_py_dir ? app_py_dir->c_str() : nullptr, + "Path where the app expects its bundled modules to live.\n" + "\n" + "Be aware that this value may be None if Ballistica is running in\n" + "a non-standard environment, and that python-path modifications may\n" + "cause modules to be loaded from other locations."); + + std::optional site_py_dir = g_core->GetSitePythonDirectory(); + envs["python_directory_app_site"] = OptionalStrEntry_( + site_py_dir ? site_py_dir->c_str() : nullptr, + "Path where the app expects its bundled pip modules to live.\n" + "\n" + "Be aware that this value may be None if Ballistica is running in\n" + "a non-standard environment, and that python-path modifications may\n" + "cause modules to be loaded from other locations."); + + envs["tv"] = BoolEntry_(g_core->platform->IsRunningOnTV(), + "Whether the app is currently running on a TV."); + + envs["vr"] = BoolEntry_(g_core->IsVRMode(), + "Whether the app is currently running in VR."); + bool first = true; + for (auto&& entry : envs) { + if (!first) { + docs += "\n"; + } + docs += " " + entry.first + " (" + entry.second.typestr + "):\n " + + entry.second.docs + "\n"; + first = false; + } + + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.Env"; + cls->tp_basicsize = sizeof(PythonClassEnv); + cls->tp_doc = docs.c_str(); + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_getattro = (getattrofunc)tp_getattro; + cls->tp_methods = tp_methods; +} + +auto PythonClassEnv::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = type->tp_alloc(type, 0); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + // Using placement new here. Remember that this means we can be destructed + // in any thread. If that's a problem we need to move to manual + // allocation/deallocation so we can push deallocation to a specific + // thread. + new (self) PythonClassEnv(); + return self; + BA_PYTHON_NEW_CATCH; +} + +void PythonClassEnv::tp_dealloc(PythonClassEnv* self) { + BA_PYTHON_TRY; + self->~PythonClassEnv(); + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassEnv::tp_getattro(PythonClassEnv* self, PyObject* attr) + -> PyObject* { + BA_PYTHON_TRY; + + // Do we need to support other attr types? + assert(PyUnicode_Check(attr)); + + auto&& entry = (*g_entries_).find(PyUnicode_AsUTF8(attr)); + if (entry != g_entries_->end()) { + Py_INCREF(entry->second.obj); + return entry->second.obj; + } else { + return PyObject_GenericGetAttr(reinterpret_cast(self), attr); + } + BA_PYTHON_CATCH; +} + +PythonClassEnv::PythonClassEnv() = default; + +PythonClassEnv::~PythonClassEnv() = default; + +PyTypeObject PythonClassEnv::type_obj; + +// Any methods for our class go here. +PyMethodDef PythonClassEnv::tp_methods[] = {{nullptr}}; + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/class/python_class_env.h b/src/ballistica/base/python/class/python_class_env.h new file mode 100644 index 00000000..087429c4 --- /dev/null +++ b/src/ballistica/base/python/class/python_class_env.h @@ -0,0 +1,44 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_ENV_H_ +#define BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_ENV_H_ + +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::base { + +/// A simple example native class. +class PythonClassEnv : public PythonClass { + public: + static void SetupType(PyTypeObject* cls); + static auto type_name() -> const char*; + static auto tp_getattro(PythonClassEnv* self, PyObject* attr) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + + /// Cast raw Python pointer to our type; throws an exception on wrong types. + static auto FromPyObj(PyObject* o) -> PythonClassEnv& { + if (Check(o)) { + return *reinterpret_cast(o); + } + throw Exception(std::string("Expected a ") + type_name() + "; got a " + + Python::ObjTypeToString(o), + PyExcType::kType); + } + + static PyTypeObject type_obj; + + private: + PythonClassEnv(); + ~PythonClassEnv(); + static PyMethodDef tp_methods[]; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassEnv* self); +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_ENV_H_ diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc index 0e7ba225..e6e42efb 100644 --- a/src/ballistica/base/python/methods/python_methods_app.cc +++ b/src/ballistica/base/python/methods/python_methods_app.cc @@ -1265,7 +1265,7 @@ static PyMethodDef PyIsOSPlayingMusicDef = { "\n" "Tells whether the OS is currently playing music of some sort.\n" "\n" - "(Used to determine whether the game should avoid playing its own)", + "(Used to determine whether the app should avoid playing its own)", }; // -------------------------------- exec_arg ----------------------------------- @@ -1491,6 +1491,67 @@ static PyMethodDef PyGetImmediateReturnCodeDef = { "(internal)\n", }; +// ----------------------- shutdown_suppress_begin ----------------------------- + +static auto PyShutdownSuppressBegin(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + assert(g_base); + g_base->ShutdownSuppressBegin(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyShutdownSuppressBeginDef = { + "shutdown_suppress_begin", // name + (PyCFunction)PyShutdownSuppressBegin, // method + METH_NOARGS, // flags + + "shutdown_suppress_begin() -> None\n" + "\n" + "(internal)\n", +}; + +// ------------------------ shutdown_suppress_end ------------------------------ + +static auto PyShutdownSuppressEnd(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + assert(g_base); + g_base->ShutdownSuppressEnd(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyShutdownSuppressEndDef = { + "shutdown_suppress_end", // name + (PyCFunction)PyShutdownSuppressEnd, // method + METH_NOARGS, // flags + + "shutdown_suppress_end() -> None\n" + "\n" + "(internal)\n", +}; + +// ------------------------ shutdown_suppress_count +// ------------------------------ + +static auto PyShutdownSuppressCount(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + assert(g_base); + return PyLong_FromLong(g_base->shutdown_suppress_count()); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyShutdownSuppressCountDef = { + "shutdown_suppress_count", // name + (PyCFunction)PyShutdownSuppressCount, // method + METH_NOARGS, // flags + + "shutdown_suppress_count() -> int\n" + "\n" + "(internal)\n", +}; + // ----------------------------------------------------------------------------- auto PythonMethodsApp::GetMethods() -> std::vector { @@ -1540,6 +1601,9 @@ auto PythonMethodsApp::GetMethods() -> std::vector { PyEmptyAppModeHandleIntentExecDef, PyGetImmediateReturnCodeDef, PyCompleteShutdownDef, + PyShutdownSuppressBeginDef, + PyShutdownSuppressEndDef, + PyShutdownSuppressCountDef, }; } diff --git a/src/ballistica/core/core.cc b/src/ballistica/core/core.cc index 7b09ca64..7d421e0f 100644 --- a/src/ballistica/core/core.cc +++ b/src/ballistica/core/core.cc @@ -134,7 +134,7 @@ auto CoreFeatureSet::core_config() const -> const CoreConfig& { // we don't interfere with low-level stuff like FatalError handling that // might need core_config access at any time. if (!g_buildconfig.monolithic_build()) { - if (!HaveBaEnvVals()) { + if (!have_ba_env_vals()) { static bool did_warn = false; if (!did_warn) { did_warn = true; diff --git a/src/ballistica/core/core.h b/src/ballistica/core/core.h index fe812afa..fc9fc856 100644 --- a/src/ballistica/core/core.h +++ b/src/ballistica/core/core.h @@ -115,7 +115,7 @@ class CoreFeatureSet { /// Return true if baenv values have been locked in: python paths, log /// handling, etc. Early-running code may wish to explicitly avoid making log /// calls until this condition is met to ensure predictable behavior. - auto HaveBaEnvVals() const { return have_ba_env_vals_; } + auto have_ba_env_vals() const { return have_ba_env_vals_; } /// Return the directory where the app expects to find its bundled Python /// files. diff --git a/src/ballistica/core/python/core_python.cc b/src/ballistica/core/python/core_python.cc index 36f27548..701f8db9 100644 --- a/src/ballistica/core/python/core_python.cc +++ b/src/ballistica/core/python/core_python.cc @@ -396,7 +396,7 @@ auto CorePython::FetchPythonArgs(std::vector* buffer) // argv pointers to it. std::vector out; out.reserve(buffer->size()); - for (int i = 0; i < buffer->size(); ++i) { + for (size_t i = 0; i < buffer->size(); ++i) { out.push_back(const_cast((*buffer)[i].c_str())); } return out; diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index 4a6ea18f..97ec299e 100644 --- a/src/ballistica/shared/ballistica.cc +++ b/src/ballistica/shared/ballistica.cc @@ -39,8 +39,9 @@ auto main(int argc, char** argv) -> int { namespace ballistica { // These are set automatically via script; don't modify them here. -const int kEngineBuildNumber = 21256; -const char* kEngineVersion = "1.7.26"; +const int kEngineBuildNumber = 21269; +const char* kEngineVersion = "1.7.27"; +const int kEngineApiVersion = 8; #if BA_MONOLITHIC_BUILD diff --git a/src/ballistica/shared/ballistica.h b/src/ballistica/shared/ballistica.h index 0d7114ea..7d9ef7b3 100644 --- a/src/ballistica/shared/ballistica.h +++ b/src/ballistica/shared/ballistica.h @@ -29,6 +29,7 @@ namespace ballistica { extern const int kEngineBuildNumber; extern const char* kEngineVersion; +extern const int kEngineApiVersion; // Protocol version we host games with and write replays to. // This should be incremented whenever there are changes made to the diff --git a/src/ballistica/template_fs/python/class/python_class_hello.cc b/src/ballistica/template_fs/python/class/python_class_hello.cc index c8c76b74..ec93b6a2 100644 --- a/src/ballistica/template_fs/python/class/python_class_hello.cc +++ b/src/ballistica/template_fs/python/class/python_class_hello.cc @@ -35,16 +35,15 @@ auto PythonClassHello::tp_new(PyTypeObject* type, PyObject* args, void PythonClassHello::tp_dealloc(PythonClassHello* self) { BA_PYTHON_TRY; - // Because we used placement-new we need to manually run the equivalent - // destructor to balance things. Note that if anything goes wrong here it'll - // simply print an error; we don't set any Python error state. Not sure if - // that is ever even allowed from destructors anyway. + // Because we used placement-new, we need to manually run the equivalent + // destructor to clean ourself up. Note that if anything goes wrong here + // it'll simply print an error; we don't set any Python error state. Not + // sure if that is ever even allowed from destructors anyway. - // IMPORTANT: With Python objects we can't guarantee that this destructor runs - // in a particular thread; if our object contains anything that must be - // destructed in a particular thread then we should manually allocate & - // deallocate things so we can ship it off to the proper thread for cleanup as - // needed. + // IMPORTANT: With Python objects we can't guarantee that this destructor + // runs in a particular thread, so if that is something we need then we + // should manually allocate stuff in tp_new and then ship a pointer off + // from here to whatever thread needs to clean it up. self->~PythonClassHello(); BA_PYTHON_DEALLOC_CATCH; Py_TYPE(self)->tp_free(reinterpret_cast(self)); @@ -58,7 +57,32 @@ PythonClassHello::~PythonClassHello() { Log(LogLevel::kInfo, "Goodbye from PythonClassHello destructor!!!"); } +auto PythonClassHello::TestMethod(PythonClassHello* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + int val{}; + static const char* kwlist[] = {"val", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|i", + const_cast(kwlist), &val)) { + return nullptr; + } + Log(LogLevel::kInfo, "Hello from PythonClassHello.test_method!!! (val=" + + std::to_string(val) + ")"); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + PyTypeObject PythonClassHello::type_obj; -PyMethodDef PythonClassHello::tp_methods[] = {{nullptr}}; + +// Any methods for our class go here. +PyMethodDef PythonClassHello::tp_methods[] = { + {"testmethod", (PyCFunction)PythonClassHello::TestMethod, + METH_VARARGS | METH_KEYWORDS, + "testmethod(val: int = 0) -> None\n" + "\n" + "Just testing.\n" + ""}, + + {nullptr}}; } // namespace ballistica::template_fs diff --git a/src/ballistica/template_fs/python/class/python_class_hello.h b/src/ballistica/template_fs/python/class/python_class_hello.h index e72b6d63..e2e3f28d 100644 --- a/src/ballistica/template_fs/python/class/python_class_hello.h +++ b/src/ballistica/template_fs/python/class/python_class_hello.h @@ -36,8 +36,8 @@ class PythonClassHello : public PythonClass { static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject*; static void tp_dealloc(PythonClassHello* self); - static auto Play(PythonClassHello* self, PyObject* args, PyObject* keywds) - -> PyObject*; + static auto TestMethod(PythonClassHello* self, PyObject* args, + PyObject* keywds) -> PyObject*; }; } // namespace ballistica::template_fs diff --git a/tools/batools/version.py b/tools/batools/version.py index 8de4babd..3b24bf6e 100755 --- a/tools/batools/version.py +++ b/tools/batools/version.py @@ -71,13 +71,19 @@ def get_current_version() -> tuple[str, int]: def get_current_api_version() -> int: """Pull current api version from the project.""" with open( - 'src/assets/ba_data/python/babase/_meta.py', encoding='utf-8' + 'src/ballistica/shared/ballistica.cc', encoding='utf-8' ) as infile: lines = infile.readlines() - linestart = 'CURRENT_API_VERSION = ' + linestart = 'const int kEngineApiVersion = ' + lineend = ';' for line in lines: if line.startswith(linestart): - return int(line.strip().removeprefix(linestart).strip()) + return int( + line.strip() + .removeprefix(linestart) + .removesuffix(lineend) + .strip() + ) raise RuntimeError('Api version line not found.') diff --git a/tools/efrotools/pybuild.py b/tools/efrotools/pybuild.py index 4bac4be6..4796d540 100644 --- a/tools/efrotools/pybuild.py +++ b/tools/efrotools/pybuild.py @@ -35,6 +35,7 @@ OPENSSL_VER_APPLE = '3.0.8' OPENSSL_VER_ANDROID = '3.0.8' ZLIB_VER_ANDROID = '1.3' +XZ_VER_ANDROID = '5.4.4' # Filenames we prune from Python lib dirs in source repo to cut down on size. PRUNE_LIB_NAMES = [ @@ -301,6 +302,14 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None: count=1, ) + # Set specific XZ version. + ftxt = replace_exact( + ftxt, + "source = 'https://tukaani.org/xz/xz-5.2.7.tar.xz'", + f"source = 'https://tukaani.org/xz/xz-{XZ_VER_ANDROID}.tar.xz'", + count=1, + ) + # Give ourselves a handle to patch the OpenSSL build. ftxt = replace_exact( ftxt,