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,