1.7.27 work in progress

This commit is contained in:
Eric 2023-08-30 09:37:33 -07:00
parent 82aa76b29b
commit 5ea2344e1d
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
76 changed files with 1096 additions and 523 deletions

88
.efrocachemap generated
View File

@ -4064,50 +4064,50 @@
"build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1", "build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1",
"build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae", "build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "bb5e0df17efe96476c3c2b8c41c7979b", "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "a039bab674a99b559440323b965d2274",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "056115be35ac1afc1f62b58dcc8f217a", "build/prefab/full/linux_arm64_gui/release/ballisticakit": "fb15d3a3e792163f18af727447564192",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "cf2b052caaa06d512ef379687b3aff86", "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "246782dc8e1f2114c62980f8addbc4f4",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "97b8d5f261f84b8047d43df1ca81777a", "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "8e59c9779e54f22b66ddfe2cd7c21528",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "f304ee46675e7552c31e174d81199307", "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "0c241652d1669e3bbaf8df19c3ae756c",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "70b54361b557f5e8d4271a0c976b28b6", "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "40ba4e0316c063238ab8e8b94f98351c",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "06c778dc4c2772acf6dbaf67eb7321c9", "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "9d80b87c57556a0877f260305f571c78",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "212a5c7206c1aa9a8427b61471e9e89b", "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "0e7d5147a5b1b9a7ecb8e6fc4cfc1174",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "f3a1028602c7bbd3f8d62bd843af628d", "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "943297ef2d247451140c08816fa0b46d",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "9c4c6d1c50e225dc61cfbab4a82a37a6", "build/prefab/full/mac_arm64_gui/release/ballisticakit": "af9ef217e000fb8e2d7fff8770b7bf44",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "4db4b600478f1767fdd0462a137912de", "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "c0e09234f16c75313eab30d576783549",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "628bc102cf6ef17b38804c4c9baa5265", "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "3f265457324e3a07a676b4db52a5f111",
"build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "fb9d165ab24084e8beaa6d7c51c81a77", "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "6482c468d8e798e081310c294553e4da",
"build/prefab/full/mac_x86_64_gui/release/ballisticakit": "ae88283e96a9238aab7561d2afcd9a5f", "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "c6354818c9abd243e9b9af03f1f075f7",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "e5fe958377b8dcf5d5203dbd17aaab72", "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "43908d43f107baa521cee51af01a9583",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "f22e8af184b19b4929162e901a056454", "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "c91f0c62c989a33caa7b4b4769754f1a",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "5fa2cb24b9e78bddb1bf9efb197d0c51", "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "79a0eb8f637e295447150a2c1e03357d",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "d36f11acfa5e68f303d347c3895979d0", "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "5900beafe0de9b11ce4d00e9163c2d15",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "570a7e325c15ecebcc419d48a046dd24", "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "7e7a5d0cc2f6fdd8fd9affbc05c5195c",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "3155f665993e5f850db5b87c9296abe7", "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "91a1f57c0f4e9ef6fb5eb590f883e167",
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "d1bfae5e75824ba89338892bc0f84c6b", "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "56ebc8c31e020e515395d3a81d2bb766",
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "b1466048e319c0d60139c46751f3eb79", "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "b9aabf060ca52f9957e5c0c68975dd0d",
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "d1bfae5e75824ba89338892bc0f84c6b", "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "56ebc8c31e020e515395d3a81d2bb766",
"build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "b1466048e319c0d60139c46751f3eb79", "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "b9aabf060ca52f9957e5c0c68975dd0d",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "656176631037184b6e22b0b68c3cd1fa", "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "21b01b868f38182cbe30dead5e6f6688",
"build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "12b633db4dad37bbb5a5c398db0c10dd", "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "5bafa4627b87a3cfc6558d51c2760560",
"build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "656176631037184b6e22b0b68c3cd1fa", "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "21b01b868f38182cbe30dead5e6f6688",
"build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "12b633db4dad37bbb5a5c398db0c10dd", "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "5bafa4627b87a3cfc6558d51c2760560",
"build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "7e014214c6cb9ddaa0e95f5186ba9df6", "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "9ba592d991ebb8724de8cab368bd1ac7",
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "45ddc559dd5ef563d2df5c5db9c9fbc0", "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "79630364e1f71cedf87140c40b913785",
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "7e014214c6cb9ddaa0e95f5186ba9df6", "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "9ba592d991ebb8724de8cab368bd1ac7",
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "45ddc559dd5ef563d2df5c5db9c9fbc0", "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "79630364e1f71cedf87140c40b913785",
"build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "1a15bf7d809addab4992827da9d89895", "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "7e3d1a1c0bdb8a814f7227f71862fa1d",
"build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "a1e03b7d13482beab8852691b5698974", "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "5bfe717b5f30a67822130299b7342bcf",
"build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "807e3629d9d4611cd93feb87face4e51", "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "6c7d4caaad12d39c61b291fe33eef2af",
"build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "a1e03b7d13482beab8852691b5698974", "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "5bfe717b5f30a67822130299b7342bcf",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "7f7bc04993982b164f6e86ad6ce350ef", "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "ac0e239be82c9f04383eb4400313ad90",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "d24102dd35c29b6a77cdf3d9921695da", "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "866ae37140298f2bd1ed9913afa773fb",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "8edef08a22594617d2b4273e0e4cba40", "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "ac8e60b59a767546d1bdb86d68c8e83d",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "cb6c0c6efad034b53fe1a8f930f2ce81", "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "8e4b7a2ae0cd444e818789ac919413d1",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "3ec4aadf128132116fc5479a46bd1f71", "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "2ea511bd7f4bf34ec1651cee263f3d11",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "14c2cf0812e3022391caffd9409d0650", "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "d9dd043cc3518ef0d02ceb302dfa71e1",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "67c207425afc5023cea9740e3bd459c3", "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "fbfab4ba2a24a212d4f3b22a259ae3f8",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "7219b9034f14c5b769818b80135ea61b", "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/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "f8cd3af311ac63147882590123b78318", "src/assets/ba_data/python/babase/_mgen/enums.py": "f8cd3af311ac63147882590123b78318",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "ad347097a38e0d7ede9eb6dec6a80ee9", "src/ballistica/base/mgen/pyembed/binding_base.inc": "ad347097a38e0d7ede9eb6dec6a80ee9",

View File

@ -1622,6 +1622,7 @@
<w>linearstep</w> <w>linearstep</w>
<w>linebegin</w> <w>linebegin</w>
<w>linebits</w> <w>linebits</w>
<w>lineend</w>
<w>lineheight</w> <w>lineheight</w>
<w>linemax</w> <w>linemax</w>
<w>lineno</w> <w>lineno</w>
@ -2777,6 +2778,7 @@
<w>sslcontext</w> <w>sslcontext</w>
<w>sslproto</w> <w>sslproto</w>
<w>ssval</w> <w>ssval</w>
<w>stacklevel</w>
<w>stackstr</w> <w>stackstr</w>
<w>stager</w> <w>stager</w>
<w>standin</w> <w>standin</w>
@ -2857,6 +2859,7 @@
<w>successmsg</w> <w>successmsg</w>
<w>suiciding</w> <w>suiciding</w>
<w>sunau</w> <w>sunau</w>
<w>suppressions</w>
<w>suter</w> <w>suter</w>
<w>sval</w> <w>sval</w>
<w>svalin</w> <w>svalin</w>

View File

@ -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 - Android should now be better at detecting hardware keyboards (you will see
'Configure Keyboard' and 'Configure Keyboard P2' buttons under '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 should be more consistent use of the 'Quit?' confirm window. Please holler if
you see any odd behavior when trying to quit the app. you see any odd behavior when trying to quit the app.
- Unix TERM signal now triggers graceful app shutdown. - 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. shutdown.
- Removed `babase.app.iircade_mode`. RIP iiRcade :(. - Removed `app.iircade_mode`. RIP iiRcade :(.
- Changed `AppState.INITIAL` to `AppState.NOT_RUNNING`, added a - Changed `AppState.INITIAL` to `AppState.NOT_RUNNING`, added a
`AppState.NATIVE_BOOTSTRAPPING`, and changed `AppState.LAUNCHING` to `AppState.NATIVE_BOOTSTRAPPING`, and changed `AppState.LAUNCHING` to
`AppState.INITING`. These better describe what the app is actually doing while `AppState.INITING`. These better describe what the app is actually doing while

View File

@ -960,6 +960,7 @@
<w>linearsize</w> <w>linearsize</w>
<w>linearstep</w> <w>linearstep</w>
<w>linebegin</w> <w>linebegin</w>
<w>lineend</w>
<w>linemax</w> <w>linemax</w>
<w>linestart</w> <w>linestart</w>
<w>linestripped</w> <w>linestripped</w>
@ -1643,6 +1644,7 @@
<w>sssssssi</w> <w>sssssssi</w>
<w>ssssssssssss</w> <w>ssssssssssss</w>
<w>ssval</w> <w>ssval</w>
<w>stacklevel</w>
<w>stager</w> <w>stager</w>
<w>standin</w> <w>standin</w>
<w>startedptr</w> <w>startedptr</w>
@ -1690,6 +1692,7 @@
<w>subsys</w> <w>subsys</w>
<w>subtypestr</w> <w>subtypestr</w>
<w>successmsg</w> <w>successmsg</w>
<w>suppressions</w>
<w>sval</w> <w>sval</w>
<w>swidth</w> <w>swidth</w>
<w>swiftc</w> <w>swiftc</w>

View File

@ -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_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.cc
${BA_SRC_ROOT}/ballistica/base/python/class/python_class_display_timer.h ${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.cc
${BA_SRC_ROOT}/ballistica/base/python/class/python_class_feature_set_data.h ${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 ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_simple_sound.cc

View File

@ -379,6 +379,8 @@
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_context_ref.h" /> <ClInclude Include="..\..\src\ballistica\base\python\class\python_class_context_ref.h" />
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_display_timer.cc" /> <ClCompile Include="..\..\src\ballistica\base\python\class\python_class_display_timer.cc" />
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_display_timer.h" /> <ClInclude Include="..\..\src\ballistica\base\python\class\python_class_display_timer.h" />
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_env.cc" />
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_env.h" />
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.cc" /> <ClCompile Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.cc" />
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.h" /> <ClInclude Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.h" />
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_simple_sound.cc" /> <ClCompile Include="..\..\src\ballistica\base\python\class\python_class_simple_sound.cc" />

View File

@ -571,6 +571,12 @@
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_display_timer.h"> <ClInclude Include="..\..\src\ballistica\base\python\class\python_class_display_timer.h">
<Filter>ballistica\base\python\class</Filter> <Filter>ballistica\base\python\class</Filter>
</ClInclude> </ClInclude>
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_env.cc">
<Filter>ballistica\base\python\class</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_env.h">
<Filter>ballistica\base\python\class</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.cc"> <ClCompile Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.cc">
<Filter>ballistica\base\python\class</Filter> <Filter>ballistica\base\python\class</Filter>
</ClCompile> </ClCompile>

View File

@ -374,6 +374,8 @@
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_context_ref.h" /> <ClInclude Include="..\..\src\ballistica\base\python\class\python_class_context_ref.h" />
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_display_timer.cc" /> <ClCompile Include="..\..\src\ballistica\base\python\class\python_class_display_timer.cc" />
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_display_timer.h" /> <ClInclude Include="..\..\src\ballistica\base\python\class\python_class_display_timer.h" />
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_env.cc" />
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_env.h" />
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.cc" /> <ClCompile Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.cc" />
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.h" /> <ClInclude Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.h" />
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_simple_sound.cc" /> <ClCompile Include="..\..\src\ballistica\base\python\class\python_class_simple_sound.cc" />

View File

@ -571,6 +571,12 @@
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_display_timer.h"> <ClInclude Include="..\..\src\ballistica\base\python\class\python_class_display_timer.h">
<Filter>ballistica\base\python\class</Filter> <Filter>ballistica\base\python\class</Filter>
</ClInclude> </ClInclude>
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_env.cc">
<Filter>ballistica\base\python\class</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_env.h">
<Filter>ballistica\base\python\class</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.cc"> <ClCompile Include="..\..\src\ballistica\base\python\class\python_class_feature_set_data.cc">
<Filter>ballistica\base\python\class</Filter> <Filter>ballistica\base\python\class</Filter>
</ClCompile> </ClCompile>

View File

@ -39,6 +39,7 @@ from _babase import (
DisplayTimer, DisplayTimer,
do_once, do_once,
env, env,
Env,
fade_screen, fade_screen,
fatal_error, fatal_error,
get_display_resolution, get_display_resolution,
@ -48,8 +49,10 @@ from _babase import (
get_replays_dir, get_replays_dir,
get_string_height, get_string_height,
get_string_width, get_string_width,
get_v1_cloud_log_file_path,
getsimplesound, getsimplesound,
has_gamma_control, has_gamma_control,
has_user_run_commands,
have_chars, have_chars,
have_permission, have_permission,
in_logic_thread, in_logic_thread,
@ -83,6 +86,9 @@ from _babase import (
set_thread_name, set_thread_name,
set_ui_input_device, set_ui_input_device,
show_progress_bar, show_progress_bar,
shutdown_suppress_begin,
shutdown_suppress_end,
shutdown_suppress_count,
SimpleSound, SimpleSound,
unlock_all_input, unlock_all_input,
user_agent_string, user_agent_string,
@ -96,6 +102,7 @@ from babase._appconfig import commit_app_config
from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec
from babase._appmode import AppMode from babase._appmode import AppMode
from babase._appsubsystem import AppSubsystem from babase._appsubsystem import AppSubsystem
from babase._appmodeselector import AppModeSelector
from babase._appconfig import AppConfig from babase._appconfig import AppConfig
from babase._apputils import ( from babase._apputils import (
handle_leftover_v1_cloud_log_file, handle_leftover_v1_cloud_log_file,
@ -175,6 +182,7 @@ __all__ = [
'AppMode', 'AppMode',
'appname', 'appname',
'appnameupper', 'appnameupper',
'AppModeSelector',
'AppSubsystem', 'AppSubsystem',
'apptime', 'apptime',
'AppTime', 'AppTime',
@ -200,6 +208,7 @@ __all__ = [
'do_once', 'do_once',
'EmptyAppMode', 'EmptyAppMode',
'env', 'env',
'Env',
'Existable', 'Existable',
'existing', 'existing',
'fade_screen', 'fade_screen',
@ -214,11 +223,13 @@ __all__ = [
'get_replays_dir', 'get_replays_dir',
'get_string_height', 'get_string_height',
'get_string_width', 'get_string_width',
'get_v1_cloud_log_file_path',
'get_type_name', 'get_type_name',
'getclass', 'getclass',
'getsimplesound', 'getsimplesound',
'handle_leftover_v1_cloud_log_file', 'handle_leftover_v1_cloud_log_file',
'has_gamma_control', 'has_gamma_control',
'has_user_run_commands',
'have_chars', 'have_chars',
'have_permission', 'have_permission',
'in_logic_thread', 'in_logic_thread',
@ -277,6 +288,9 @@ __all__ = [
'set_thread_name', 'set_thread_name',
'set_ui_input_device', 'set_ui_input_device',
'show_progress_bar', 'show_progress_bar',
'shutdown_suppress_begin',
'shutdown_suppress_end',
'shutdown_suppress_count',
'SimpleSound', 'SimpleSound',
'SpecialChar', 'SpecialChar',
'storagename', 'storagename',

View File

@ -1,11 +1,14 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to the high level state of the app.""" """Functionality related to the high level state of the app."""
# pylint: disable=too-many-lines
from __future__ import annotations from __future__ import annotations
import os import os
import logging import logging
from enum import Enum from enum import Enum
import warnings
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from functools import cached_property from functools import cached_property
@ -55,6 +58,7 @@ class App:
plugins: PluginSubsystem plugins: PluginSubsystem
lang: LanguageSubsystem lang: LanguageSubsystem
env: babase.Env
health_monitor: AppHealthMonitor health_monitor: AppHealthMonitor
@ -68,173 +72,72 @@ class App:
# The app has not yet begun starting and should not be used in # The app has not yet begun starting and should not be used in
# any way. # any way.
NOT_RUNNING = 'not_running' NOT_RUNNING = 0
# The native layer is spinning up its machinery (screens, # The native layer is spinning up its machinery (screens,
# renderers, etc.). Nothing should happen in the Python layer # renderers, etc.). Nothing should happen in the Python layer
# until this completes. # until this completes.
NATIVE_BOOTSTRAPPING = 'native_bootstrapping' NATIVE_BOOTSTRAPPING = 1
# Python app subsystems are being inited but should not yet # Python app subsystems are being inited but should not yet
# interact or do any work. # interact or do any work.
INITING = 'initing' INITING = 2
# Python app subsystems are inited and interacting, but the app # Python app subsystems are inited and interacting, but the app
# has not yet embarked on a high level course of action. It is # has not yet embarked on a high level course of action. It is
# doing initial account logins, workspace & asset downloads, # doing initial account logins, workspace & asset downloads,
# etc. # etc.
LOADING = 'loading' LOADING = 3
# All pieces are in place and the app is now doing its thing. # All pieces are in place and the app is now doing its thing.
RUNNING = 'running' RUNNING = 4
# The app is backgrounded or otherwise suspended. # The app is backgrounded or otherwise suspended.
PAUSED = 'paused' PAUSED = 5
# The app is shutting down. # The app is shutting down.
SHUTTING_DOWN = 'shutting_down' SHUTTING_DOWN = 6
@property class DefaultAppModeSelector(AppModeSelector):
def aioloop(self) -> asyncio.AbstractEventLoop: """Decides which app modes to use to handle intents.
"""The logic thread's asyncio event loop.
This allow async tasks to be run in the logic thread. The behavior here is generated by the project updater based on
Note that, at this time, the asyncio loop is encapsulated the feature-sets present in the project. Spinoff projects can
and explicitly stepped by the engine's logic thread loop and also inject their own behavior by replacing the text
thus things like asyncio.get_running_loop() will not return this '__GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__' with their own code
loop from most places in the logic thread; only from within a through spinoff filtering.
task explicitly created in this loop.
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 app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]:
def build_number(self) -> int: # pylint: disable=cyclic-import
"""Integer build number.
This value increases by at least 1 with each release of the game. # __GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__
It is independent of the human readable babase.App.version string.
"""
assert isinstance(self._env['build_number'], int)
return self._env['build_number']
@property # __DEFAULT_APP_MODE_SELECTION_BEGIN__
def device_name(self) -> str: # This section generated by batools.appmodule; do not edit.
"""Name of the device running the game."""
assert isinstance(self._env['device_name'], str)
return self._env['device_name']
@property # Hmm; need to think about how we auto-construct this; how
def config_file_path(self) -> str: # should we determine which app modes to check and in what
"""Where the game's config file is stored on disk.""" # order?
assert isinstance(self._env['config_file_path'], str) import bascenev1
return self._env['config_file_path']
@property import babase
def version(self) -> str:
"""Human-readable version string; something like '1.3.24'.
This should not be interpreted as a number; it may contain if bascenev1.SceneV1AppMode.supports_intent(intent):
string elements such as 'alpha', 'beta', 'test', etc. return bascenev1.SceneV1AppMode
If a numeric version is needed, use 'babase.App.build_number'.
"""
assert isinstance(self._env['version'], str)
return self._env['version']
@property if babase.EmptyAppMode.supports_intent(intent):
def debug_build(self) -> bool: return babase.EmptyAppMode
"""Whether the app was compiled in debug mode.
Debug builds generally run substantially slower than non-debug raise RuntimeError(f'No handler found for intent {type(intent)}.')
builds due to compiler optimizations being disabled and extra
checks being run.
"""
assert isinstance(self._env['debug_build'], bool)
return self._env['debug_build']
@property # __DEFAULT_APP_MODE_SELECTION_END__
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']
def __init__(self) -> None: def __init__(self) -> None:
"""(internal) """(internal)
@ -248,6 +151,8 @@ class App:
self.state = self.State.NOT_RUNNING self.state = self.State.NOT_RUNNING
self.env: babase.Env = _babase.Env()
self._subsystems: list[AppSubsystem] = [] self._subsystems: list[AppSubsystem] = []
self._native_bootstrapping_completed = False self._native_bootstrapping_completed = False
@ -263,12 +168,11 @@ class App:
self._subsystem_registration_ended = False self._subsystem_registration_ended = False
self._pending_apply_app_config = False self._pending_apply_app_config = False
# Config. self.config_file_healthy: bool = False
self.config_file_healthy = False
# This is incremented any time the app is # This is incremented any time the app is backgrounded or
# backgrounded/foregrounded; can be a simple way to determine if # foregrounded; can be a simple way to determine if network data
# network data should be refreshed/etc. # should be refreshed/etc.
self.fg_state = 0 self.fg_state = 0
self._aioloop: asyncio.AbstractEventLoop | None = None self._aioloop: asyncio.AbstractEventLoop | None = None
@ -294,20 +198,25 @@ class App:
self._config: babase.AppConfig | None = None self._config: babase.AppConfig | None = None
self.components = AppComponentSubsystem() self.components = AppComponentSubsystem()
# Testing this.
self.meta = MetadataSubsystem() self.meta = MetadataSubsystem()
self.net = NetworkSubsystem() self.net = NetworkSubsystem()
self.workspaces = WorkspaceSubsystem() self.workspaces = WorkspaceSubsystem()
self._pending_intent: AppIntent | None = None self._pending_intent: AppIntent | None = None
self._intent: AppIntent | None = None self._intent: AppIntent | None = None
self._mode: AppMode | None = None self._mode: AppMode | None = None
self._shutdown_task: asyncio.Task[None] | 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 # Controls which app-modes we use for handling given
# app-intents. Plugins can override this to change high level # app-intents. Plugins can override this to change high level
# app behavior and spinoff projects can change the default # app behavior and spinoff projects can change the default
# implementation for the same effect. # 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 self._asyncio_timer: babase.AppTimer | None = None
@ -324,35 +233,25 @@ class App:
self.lang = LanguageSubsystem() self.lang = LanguageSubsystem()
self.plugins = PluginSubsystem() self.plugins = PluginSubsystem()
def register_subsystem(self, subsystem: AppSubsystem) -> None: @property
"""Called by the AppSubsystem class. Do not use directly.""" def aioloop(self) -> asyncio.AbstractEventLoop:
"""The logic thread's asyncio event loop.
# We only allow registering new subsystems if we've not yet This allow async tasks to be run in the logic thread.
# reached the 'running' state. This ensures that all subsystems Note that, at this time, the asyncio loop is encapsulated
# receive a consistent set of callbacks starting with and explicitly stepped by the engine's logic thread loop and
# on_app_running(). thus things like asyncio.get_running_loop() will not return this
if self._subsystem_registration_ended: loop from most places in the logic thread; only from within a
raise RuntimeError( task explicitly created in this loop.
'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.
""" """
fut = self.threadpool.submit(call) assert self._aioloop is not None
fut.add_done_callback(self._threadpool_no_wait_done) 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__ # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_BEGIN__
# This section generated by batools.appmodule; do not edit. # This section generated by batools.appmodule; do not edit.
@ -398,6 +297,48 @@ class App:
# __FEATURESET_APP_SUBSYSTEM_PROPERTIES_END__ # __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: def set_intent(self, intent: AppIntent) -> None:
"""Set the intent for the app. """Set the intent for the app.
@ -417,6 +358,92 @@ class App:
# since it may block for a moment to load modules/etc. # since it may block for a moment to load modules/etc.
self.threadpool_submit_no_wait(tpartial(self._set_intent, intent)) 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: def _set_intent(self, intent: AppIntent) -> None:
# This should be running in a bg thread. # This should be running in a bg thread.
assert not _babase.in_logic_thread() assert not _babase.in_logic_thread()
@ -492,14 +519,6 @@ class App:
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
_babase.getsimplesound('error').play() _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: def _on_initing(self) -> None:
"""Called when the app enters the initing state. """Called when the app enters the initing state.
@ -633,14 +652,6 @@ class App:
# plugin hasn't already told it to do something. # plugin hasn't already told it to do something.
self.set_intent(AppIntentDefault()) 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: def _apply_app_config(self) -> None:
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
@ -667,47 +678,6 @@ class App:
# Let the native layer do its thing. # Let the native layer do its thing.
_babase.do_apply_app_config() _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: def _update_state(self) -> None:
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
@ -761,18 +731,6 @@ class App:
if bool(True): if bool(True):
self.state = self.State.NATIVE_BOOTSTRAPPING 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: async def _shutdown(self) -> None:
import asyncio import asyncio
@ -802,40 +760,6 @@ class App:
except Exception: except Exception:
logging.exception('Error in shutdown task.') 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: def _on_pause(self) -> None:
"""Called when the app goes to a paused state.""" """Called when the app goes to a paused state."""
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
@ -881,46 +805,198 @@ class App:
assert self._aioloop is not None assert self._aioloop is not None
self._shutdown_task = self._aioloop.create_task(self._shutdown()) self._shutdown_task = self._aioloop.create_task(self._shutdown())
def read_config(self) -> None: async def _wait_for_shutdown_suppressions(self) -> None:
"""(internal)""" import asyncio
from babase._appconfig import read_app_config
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: def _threadpool_no_wait_done(self, fut: Future) -> None:
"""Handle a deep link URL.""" try:
from babase._language import Lstr 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() @property
if url.startswith(f'{appname}://code/'): def build_number(self) -> int:
code = url.replace(f'{appname}://code/', '') """Integer build number.
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: This value increases by at least 1 with each release of the engine.
"""Called when initial sign-in (or lack thereof) completes. It is independent of the human readable babase.App.version string.
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() warnings.warn(
assert not self._initial_sign_in_completed '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 @property
# up (namely account workspaces). def device_name(self) -> str:
self.meta.start_extra_scan() """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 @property
self._update_state() 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

View File

@ -109,7 +109,7 @@ def read_app_config() -> tuple[AppConfig, bool]:
# NOTE: it is assumed that this only gets called once and the # NOTE: it is assumed that this only gets called once and the
# config object will not change from here on out # 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 = '' config_contents = ''
try: try:
if os.path.exists(config_file_path): if os.path.exists(config_file_path):

View File

@ -48,7 +48,7 @@ def is_browser_likely_available() -> bool:
# assume no browser. # assume no browser.
# FIXME: Might not be the case anymore; should make this definable # FIXME: Might not be the case anymore; should make this definable
# at the platform level. # 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 return False
# Anywhere else assume we've got one. # Anywhere else assume we've got one.
@ -103,8 +103,8 @@ def handle_v1_cloud_log() -> None:
info = { info = {
'log': _babase.get_v1_cloud_log(), 'log': _babase.get_v1_cloud_log(),
'version': app.version, 'version': app.env.version,
'build': app.build_number, 'build': app.env.build_number,
'userAgentString': classic.legacy_user_agent_string, 'userAgentString': classic.legacy_user_agent_string,
'session': sessionname, 'session': sessionname,
'activity': activityname, 'activity': activityname,
@ -279,7 +279,8 @@ def dump_app_state(
# the dump in that case. # the dump in that case.
try: try:
mdpath = os.path.join( 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: with open(mdpath, 'w', encoding='utf-8') as outfile:
outfile.write( outfile.write(
@ -297,7 +298,7 @@ def dump_app_state(
return return
tbpath = os.path.join( 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') tbfile = open(tbpath, 'w', encoding='utf-8')
@ -329,7 +330,8 @@ def log_dumped_app_state() -> None:
try: try:
out = '' out = ''
mdpath = os.path.join( 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): if os.path.exists(mdpath):
# We may be hanging on to open file descriptors for use by # 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}' f'Time: {metadata.app_time:.2f}'
) )
tbpath = os.path.join( tbpath = os.path.join(
os.path.dirname(_babase.app.config_file_path), os.path.dirname(_babase.app.env.config_file_path),
'_appstate_dump_tb', '_appstate_dump_tb',
) )
if os.path.exists(tbpath): if os.path.exists(tbpath):

View File

@ -6,6 +6,7 @@ from __future__ import annotations
import sys import sys
import signal import signal
import logging import logging
import warnings
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from efro.log import LogLevel 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. signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling.
_babase.setup_sigint() _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 # Turn off fancy-pants cyclic garbage-collection. We run it only at
# explicit times to avoid random hitches and keep things more # explicit times to avoid random hitches and keep things more
# deterministic. Non-reference-looped objects will still get cleaned # deterministic. Non-reference-looped objects will still get cleaned

View File

@ -354,7 +354,7 @@ def show_client_too_old_error() -> None:
# a newer build. # a newer build.
if ( if (
_babase.app.config.get('SuppressClientTooOldErrorForBuild') _babase.app.config.get('SuppressClientTooOldErrorForBuild')
== _babase.app.build_number == _babase.app.env.build_number
): ):
return return

View File

@ -68,7 +68,10 @@ class LanguageSubsystem(AppSubsystem):
try: try:
names = os.listdir( names = os.listdir(
os.path.join( 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] names = [n.replace('.json', '').capitalize() for n in names]
@ -121,7 +124,7 @@ class LanguageSubsystem(AppSubsystem):
with open( with open(
os.path.join( os.path.join(
_babase.app.data_directory, _babase.app.env.data_directory,
'ba_data', 'ba_data',
'data', 'data',
'languages', 'languages',
@ -139,7 +142,7 @@ class LanguageSubsystem(AppSubsystem):
lmodvalues = None lmodvalues = None
else: else:
lmodfile = os.path.join( lmodfile = os.path.join(
_babase.app.data_directory, _babase.app.env.data_directory,
'ba_data', 'ba_data',
'data', 'data',
'languages', 'languages',

View File

@ -18,11 +18,6 @@ import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable 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. # Meta export lines can use these names to represent these classes.
# This is purely a convenience; it is possible to use full class paths # 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_complete_cb is None
assert self._scan is None assert self._scan is None
env = _babase.app.env
self._scan_complete_cb = scan_complete_cb self._scan_complete_cb = scan_complete_cb
self._scan = DirectoryScan( self._scan = DirectoryScan(
[ [
path path
for path in [ for path in [
_babase.app.python_directory_app, env.python_directory_app,
_babase.app.python_directory_user, env.python_directory_user,
] ]
if path is not None if path is not None
] ]
@ -212,7 +208,7 @@ class MetadataSubsystem:
'${NUM}', '${NUM}',
str(len(results.incorrect_api_modules) - 1), str(len(results.incorrect_api_modules) - 1),
), ),
('${API}', str(CURRENT_API_VERSION)), ('${API}', str(_babase.app.env.api_version)),
], ],
) )
else: else:
@ -220,7 +216,7 @@ class MetadataSubsystem:
resource='scanScriptsSingleModuleNeedsUpdatesText', resource='scanScriptsSingleModuleNeedsUpdatesText',
subs=[ subs=[
('${PATH}', results.incorrect_api_modules[0]), ('${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)) _babase.screenmessage(msg, color=(1, 0, 0))
@ -344,13 +340,16 @@ class DirectoryScan:
# If we find a module requiring a different api version, warn # If we find a module requiring a different api version, warn
# and ignore. # 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( logging.warning(
'metascan: %s requires api %s but we are running' 'metascan: %s requires api %s but we are running'
' %s. Ignoring module.', ' %s. Ignoring module.',
subpath, subpath,
required_api, required_api,
CURRENT_API_VERSION, _babase.app.env.api_version,
) )
self.results.incorrect_api_modules.append( self.results.incorrect_api_modules.append(
self._module_name_for_subpath(subpath) self._module_name_for_subpath(subpath)

View File

@ -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)". This is NOT a valid filesystem path; may be something like "(SD Card)".
""" """
app = _babase.app app = _babase.app
path: str | None = app.python_directory_user path: str | None = app.env.python_directory_user
if path is None: if path is None:
return '<Not Available>' return '<Not Available>'
@ -66,19 +66,20 @@ def _request_storage_permission() -> bool:
def show_user_scripts() -> None: def show_user_scripts() -> None:
"""Open or nicely print the location of the user-scripts directory.""" """Open or nicely print the location of the user-scripts directory."""
app = _babase.app app = _babase.app
env = app.env
# First off, if we need permission for this, ask for it. # First off, if we need permission for this, ask for it.
if _request_storage_permission(): if _request_storage_permission():
return return
# If we're running in a nonstandard environment its possible this is unset. # 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('<unset>') _babase.screenmessage('<unset>')
return return
# Secondly, if the dir doesn't exist, attempt to make it. # Secondly, if the dir doesn't exist, attempt to make it.
if not os.path.exists(app.python_directory_user): if not os.path.exists(env.python_directory_user):
os.makedirs(app.python_directory_user) os.makedirs(env.python_directory_user)
# On android, attempt to write a file in their user-scripts dir telling # 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 # 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. # they can see it.
if app.classic is not None and app.classic.platform == 'android': if app.classic is not None and app.classic.platform == 'android':
try: 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): if usd is not None and os.path.isdir(usd):
file_name = usd + '/about_this_folder.txt' file_name = usd + '/about_this_folder.txt'
with open(file_name, 'w', encoding='utf-8') as outfile: 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. # 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']: 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. # Otherwise we just print a pretty version of it.
else: else:
@ -120,18 +121,19 @@ def create_user_system_scripts() -> None:
import shutil import shutil
app = _babase.app app = _babase.app
env = app.env
# First off, if we need permission for this, ask for it. # First off, if we need permission for this, ask for it.
if _request_storage_permission(): if _request_storage_permission():
return return
# Its possible these are unset in non-standard environments. # 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') 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') 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' pathtmp = path + '_tmp'
if os.path.exists(path): if os.path.exists(path):
shutil.rmtree(path) shutil.rmtree(path)
@ -147,8 +149,8 @@ def create_user_system_scripts() -> None:
# /Knowledge-Nuggets#python-cache-files-gotcha # /Knowledge-Nuggets#python-cache-files-gotcha
return ('__pycache__',) return ('__pycache__',)
print(f'COPYING "{app.python_directory_app}" -> "{pathtmp}".') print(f'COPYING "{env.python_directory_app}" -> "{pathtmp}".')
shutil.copytree(app.python_directory_app, pathtmp, ignore=_ignore_filter) shutil.copytree(env.python_directory_app, pathtmp, ignore=_ignore_filter)
print(f'MOVING "{pathtmp}" -> "{path}".') print(f'MOVING "{pathtmp}" -> "{path}".')
shutil.move(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().""" """Clean out the scripts created by create_user_system_scripts()."""
import shutil 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') 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): if os.path.exists(path):
shutil.rmtree(path) shutil.rmtree(path)
print( print(
@ -185,6 +187,6 @@ def delete_user_system_scripts() -> None:
print(f"User system scripts not found at '{path}'.") print(f"User system scripts not found at '{path}'.")
# If the sys path is empty, kill it. # 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): if os.path.isdir(dpath) and not os.listdir(dpath):
os.rmdir(dpath) os.rmdir(dpath)

View File

@ -4,14 +4,12 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
import logging
import weakref import weakref
import threading import threading
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import babase import babase
from babase import DEFAULT_REQUEST_TIMEOUT_SECONDS
import bascenev1 import bascenev1
if TYPE_CHECKING: if TYPE_CHECKING:
@ -72,6 +70,7 @@ class MasterServerV1CallThread(threading.Thread):
def run(self) -> None: def run(self) -> None:
# pylint: disable=consider-using-with # pylint: disable=consider-using-with
# pylint: disable=too-many-branches
import urllib.request import urllib.request
import urllib.parse import urllib.parse
import urllib.error import urllib.error
@ -79,20 +78,15 @@ class MasterServerV1CallThread(threading.Thread):
from efro.error import is_urllib_communication_error 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 plus = babase.app.plus
assert plus is not None assert plus is not None
response_data: Any = None response_data: Any = None
url: str | None = 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: try:
classic = babase.app.classic classic = babase.app.classic
assert classic is not None assert classic is not None
@ -114,7 +108,7 @@ class MasterServerV1CallThread(threading.Thread):
{'User-Agent': classic.legacy_user_agent_string}, {'User-Agent': classic.legacy_user_agent_string},
), ),
context=babase.app.net.sslcontext, context=babase.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS, timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
) )
elif self._request_type == 'post': elif self._request_type == 'post':
url = plus.get_master_server_address() + '/' + self._request url = plus.get_master_server_address() + '/' + self._request
@ -126,7 +120,7 @@ class MasterServerV1CallThread(threading.Thread):
{'User-Agent': classic.legacy_user_agent_string}, {'User-Agent': classic.legacy_user_agent_string},
), ),
context=babase.app.net.sslcontext, context=babase.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS, timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
) )
else: else:
raise TypeError('Invalid request_type: ' + self._request_type) raise TypeError('Invalid request_type: ' + self._request_type)
@ -160,6 +154,9 @@ class MasterServerV1CallThread(threading.Thread):
response_data = None response_data = None
finally:
babase.shutdown_suppress_end()
if self._callback is not None: if self._callback is not None:
babase.pushcall( babase.pushcall(
babase.Call(self._run_callback, response_data), babase.Call(self._run_callback, response_data),

View File

@ -214,7 +214,10 @@ class ServerController:
babase.app.classic.master_server_v1_get( babase.app.classic.master_server_v1_get(
'bsAccessCheck', '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, callback=self._access_check_response,
) )
@ -379,8 +382,8 @@ class ServerController:
if self._first_run: if self._first_run:
curtimestr = time.strftime('%c') curtimestr = time.strftime('%c')
startupmsg = ( startupmsg = (
f'{Clr.BLD}{Clr.BLU}{babase.appnameupper()} {app.version}' f'{Clr.BLD}{Clr.BLU}{babase.appnameupper()} {app.env.version}'
f' ({app.build_number})' f' ({app.env.build_number})'
f' entering server-mode {curtimestr}{Clr.RST}' f' entering server-mode {curtimestr}{Clr.RST}'
) )
logging.info(startupmsg) logging.info(startupmsg)

View File

@ -153,6 +153,7 @@ class ClassicSubsystem(babase.AppSubsystem):
plus = babase.app.plus plus = babase.app.plus
assert plus is not None assert plus is not None
env = babase.app.env
cfg = babase.app.config cfg = babase.app.config
self.music.on_app_loading() 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. # 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) # (so I don't accidentally release a build that can't play tourneys)
if ( if not env.debug and not env.test and not plus.is_blessed():
not babase.app.debug_build
and not babase.app.test_build
and not plus.is_blessed()
):
babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
# FIXME: This should not be hard-coded. # FIXME: This should not be hard-coded.

View File

@ -113,7 +113,7 @@ def get_all_tips() -> list[str]:
if ( if (
app.classic is not None app.classic is not None
and app.classic.platform in ('android', 'ios') and app.classic.platform in ('android', 'ios')
and not app.on_tv and not app.env.tv
): ):
tips += [ tips += [
( (

View File

@ -52,8 +52,8 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be # Build number and version of the ballistica binary we expect to be
# using. # using.
TARGET_BALLISTICA_BUILD = 21256 TARGET_BALLISTICA_BUILD = 21269
TARGET_BALLISTICA_VERSION = '1.7.26' TARGET_BALLISTICA_VERSION = '1.7.27'
@dataclass @dataclass

View File

@ -108,7 +108,7 @@ class CoopGameActivity(GameActivity[PlayerT, TeamT]):
) )
if not a.complete if not a.complete
] ]
vrmode = babase.app.vr_mode vrmode = babase.app.env.vr
if achievements: if achievements:
Text( Text(
babase.Lstr(resource='achievementsRemainingText'), babase.Lstr(resource='achievementsRemainingText'),

View File

@ -600,7 +600,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
translate=('gameDescriptions', sb_desc_l[0]), subs=subs translate=('gameDescriptions', sb_desc_l[0]), subs=subs
) )
sb_desc = translation sb_desc = translation
vrmode = babase.app.vr_mode vrmode = babase.app.env.vr
yval = -34 if is_empty else -20 yval = -34 if is_empty else -20
yval -= 16 yval -= 16
sbpos = ( sbpos = (
@ -706,7 +706,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
resource='epicDescriptionFilterText', resource='epicDescriptionFilterText',
subs=[('${DESCRIPTION}', translation)], subs=[('${DESCRIPTION}', translation)],
) )
vrmode = babase.app.vr_mode vrmode = babase.app.env.vr
dnode = _bascenev1.newnode( dnode = _bascenev1.newnode(
'text', 'text',
attrs={ attrs={
@ -761,7 +761,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
base_position = (75, 50) base_position = (75, 50)
tip_scale = 0.8 tip_scale = 0.8
tip_title_scale = 1.2 tip_title_scale = 1.2
vrmode = babase.app.vr_mode vrmode = babase.app.env.vr
t_offs = -350.0 t_offs = -350.0
tnode = _bascenev1.newnode( tnode = _bascenev1.newnode(

View File

@ -185,7 +185,7 @@ def show_damage_count(
# (connected clients may have differing configs so they won't # (connected clients may have differing configs so they won't
# get the intended results). # get the intended results).
assert app.classic is not None 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( txtnode = _bascenev1.newnode(
'text', 'text',
attrs={ attrs={

View File

@ -49,7 +49,7 @@ class JoinInfo:
if keyboard is not None: if keyboard is not None:
self._update_for_keyboard(keyboard) 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( self._text = NodeActor(
_bascenev1.newnode( _bascenev1.newnode(
'text', 'text',

View File

@ -76,7 +76,7 @@ class CoopJoinActivity(bs.JoinActivity):
] ]
have_achievements = bool(achievements) have_achievements = bool(achievements)
achievements = [a for a in achievements if not a.complete] achievements = [a for a in achievements if not a.complete]
vrmode = bs.app.vr_mode vrmode = bs.app.env.vr
if have_achievements: if have_achievements:
Text( Text(
bs.Lstr(resource='achievementsRemainingText'), bs.Lstr(resource='achievementsRemainingText'),

View File

@ -74,7 +74,7 @@ class Background(bs.Actor):
self.node.connectattr('opacity', self.logo, 'opacity') self.node.connectattr('opacity', self.logo, 'opacity')
# add jitter/pulse for a stop-motion-y look unless we're in VR # add jitter/pulse for a stop-motion-y look unless we're in VR
# in which case stillness is better # in which case stillness is better
if not bs.app.vr_mode: if not bs.app.env.vr:
self.cmb = bs.newnode( self.cmb = bs.newnode(
'combine', owner=self.node, attrs={'size': 2} 'combine', owner=self.node, attrs={'size': 2}
) )

View File

@ -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) 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_top = (position[0], position[1] - 135.0 * scale)
self._run_text_pos_bottom = (position[0], position[1] - 172.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( self._run_text = bs.newnode(
'text', 'text',
attrs={ attrs={
'scale': sval, 'scale': sval,
'host_only': True, '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, 'flatness': 1.0,
'maxwidth': 380, 'maxwidth': 380,
'v_align': 'top', 'v_align': 'top',

View File

@ -45,7 +45,7 @@ class _Entry:
# FIXME: Should not do things conditionally for vr-mode, as there may # FIXME: Should not do things conditionally for vr-mode, as there may
# be non-vr clients connected which will also get these value. # 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 self._do_cover:
if vrmode: if vrmode:

View File

@ -69,7 +69,7 @@ class ZoomText(bs.Actor):
) )
# we never jitter in vr mode.. # we never jitter in vr mode..
if bs.app.vr_mode: if bs.app.env.vr:
jitter = 0.0 jitter = 0.0
# if they want jitter, animate its position slightly... # if they want jitter, animate its position slightly...

View File

@ -478,7 +478,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
) )
# FIXME; should not set things based on vr mode. # FIXME; should not set things based on vr mode.
# (won't look right to non-vr connected clients, etc) # (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( self._lives_text = bs.NodeActor(
bs.newnode( bs.newnode(
'text', 'text',

View File

@ -50,6 +50,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
super().on_transition_in() super().on_transition_in()
random.seed(123) random.seed(123)
app = bs.app app = bs.app
env = app.env
assert app.classic is not None assert app.classic is not None
plus = bui.app.plus 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). # the host is VR mode or not (clients may differ in that regard).
# Any differences need to happen at the engine level so everyone # Any differences need to happen at the engine level so everyone
# sees things in their own optimal way. # 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: 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) 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). # 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 # Any differences need to happen at the engine level so everyone sees
# things in their own optimal way. # things in their own optimal way.
vr_mode = app.vr_mode vr_mode = app.env.vr
uiscale = app.ui_v1.uiscale uiscale = app.ui_v1.uiscale
# In cases where we're doing lots of dev work lets always show the # 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 force_show_build_number = False
if not bs.app.toolbar_test: if not bs.app.toolbar_test:
if app.debug_build or app.test_build or force_show_build_number: if env.debug or env.test or force_show_build_number:
if app.debug_build: if env.debug:
text = bs.Lstr( text = bs.Lstr(
value='${V} (${B}) (${D})', value='${V} (${B}) (${D})',
subs=[ subs=[
('${V}', app.version), ('${V}', app.env.version),
('${B}', str(app.build_number)), ('${B}', str(app.env.build_number)),
('${D}', bs.Lstr(resource='debugText')), ('${D}', bs.Lstr(resource='debugText')),
], ],
) )
@ -139,12 +140,12 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
text = bs.Lstr( text = bs.Lstr(
value='${V} (${B})', value='${V} (${B})',
subs=[ subs=[
('${V}', app.version), ('${V}', app.env.version),
('${B}', str(app.build_number)), ('${B}', str(app.env.build_number)),
], ],
) )
else: 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 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) color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7)
self.version = bs.NodeActor( self.version = bs.NodeActor(
@ -170,7 +171,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
# Throw in test build info. # Throw in test build info.
self.beta_info = self.beta_info_2 = None 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) pos = (230, 35)
self.beta_info = bs.NodeActor( self.beta_info = bs.NodeActor(
bs.newnode( 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 # Add a bit of stop-motion-y jitter to the logo
# (unless we're in VR mode in which case its best to # (unless we're in VR mode in which case its best to
# leave things still). # leave things still).
if not bs.app.vr_mode: if not bs.app.env.vr:
cmb: bs.Node | None cmb: bs.Node | None
cmb2: bs.Node | None cmb2: bs.Node | None
if not shadow: 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 # (unless we're in VR mode in which case its best to
# leave things still). # leave things still).
assert logo.node 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 = bs.newnode('combine', owner=logo.node, attrs={'size': 2})
cmb.connectattr('output', logo.node, 'position') cmb.connectattr('output', logo.node, 'position')
keys = {} keys = {}
@ -882,7 +883,7 @@ class NewsDisplay:
self._phrases.insert(0, phr) self._phrases.insert(0, phr)
val = self._phrases.pop() val = self._phrases.pop()
if val == '__ACH__': if val == '__ACH__':
vrmode = app.vr_mode vrmode = app.env.vr
Text( Text(
bs.Lstr(resource='nextAchievementsText'), bs.Lstr(resource='nextAchievementsText'),
color=((1, 1, 1, 1) if vrmode else (0.95, 0.9, 1, 0.4)), 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 # Show upcoming achievements in non-vr versions
# (currently too hard to read in vr). # (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('<br>\n') if s != '' s for s in news.split('<br>\n') if s != ''
] ]
self._phrase_change_timer = bs.Timer( self._phrase_change_timer = bs.Timer(
@ -960,12 +961,12 @@ class NewsDisplay:
assert bs.app.classic is not None assert bs.app.classic is not None
scl = ( scl = (
1.2 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 else 0.8
) )
color2 = (1, 1, 1, 1) if bs.app.vr_mode else (0.7, 0.65, 0.75, 1.0) 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.vr_mode else 0.4 shadow = 1.0 if bs.app.env.vr else 0.4
self._text = bs.NodeActor( self._text = bs.NodeActor(
bs.newnode( bs.newnode(
'text', 'text',

View File

@ -141,7 +141,7 @@ class AccountViewerWindow(PopupWindow):
bui.app.classic.master_server_v1_get( bui.app.classic.master_server_v1_get(
'bsAccountInfo', 'bsAccountInfo',
{ {
'buildNumber': bui.app.build_number, 'buildNumber': bui.app.env.build_number,
'accountID': self._account_id, 'accountID': self._account_id,
'profileID': self._profile_id, 'profileID': self._profile_id,
}, },

View File

@ -11,7 +11,7 @@ class ConfigErrorWindow(bui.Window):
"""Window for dealing with a broken config.""" """Window for dealing with a broken config."""
def __init__(self) -> None: 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 width = 800
super().__init__( super().__init__(
bui.containerwidget(size=(width, 400), transition='in_right') bui.containerwidget(size=(width, 400), transition='in_right')

View File

@ -197,13 +197,19 @@ class QuitWindow:
time=0.2, time=0.2,
endcall=lambda: bui.quit(soft=True, back=self._back), 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() bui.lock_all_input()
# Unlock and fade back in shortly. Just in case something goes # On systems supporting soft-quit, unlock and fade back in shortly
# wrong (or on Android where quit just backs out of our activity # (soft-quit basically just backgrounds/hides the app).
# and we may come back after). if bui.app.env.supports_soft_quit:
def _come_back() -> None: # Unlock and fade back in shortly. Just in case something goes
bui.unlock_all_input() # wrong (or on Android where quit just backs out of our activity
bui.fade_screen(True, time=0.1) # 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)

View File

@ -212,7 +212,10 @@ class CreditsListWindow(bui.Window):
try: try:
with open( with open(
os.path.join( 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', encoding='utf-8',
) as infile: ) as infile:

View File

@ -15,7 +15,7 @@ def ask_for_rating() -> bui.Widget | None:
subplatform = app.classic.subplatform subplatform = app.classic.subplatform
# FIXME: should whitelist platforms we *do* want this for. # FIXME: should whitelist platforms we *do* want this for.
if bui.app.test_build: if bui.app.env.test:
return None return None
if not ( if not (
platform == 'mac' platform == 'mac'

View File

@ -43,7 +43,7 @@ class AboutGatherTab(GatherTab):
# Let's not talk about sharing in vr-mode; its tricky to fit more # Let's not talk about sharing in vr-mode; its tricky to fit more
# than one head in a VR-headset ;-) # than one head in a VR-headset ;-)
if not bui.app.vr_mode: if not bui.app.env.vr:
message = bui.Lstr( message = bui.Lstr(
value='${A}\n\n${B}', value='${A}\n\n${B}',
subs=[ subs=[

View File

@ -1010,7 +1010,7 @@ class ManualGatherTab(GatherTab):
self._t_accessible_extra = t_accessible_extra self._t_accessible_extra = t_accessible_extra
bui.app.classic.master_server_v1_get( bui.app.classic.master_server_v1_get(
'bsAccessCheck', 'bsAccessCheck',
{'b': bui.app.build_number}, {'b': bui.app.env.build_number},
callback=bui.WeakCall(self._on_accessible_response), callback=bui.WeakCall(self._on_accessible_response),
) )

View File

@ -1327,7 +1327,7 @@ class PublicGatherTab(GatherTab):
) )
bui.app.classic.master_server_v1_get( bui.app.classic.master_server_v1_get(
'bsAccessCheck', 'bsAccessCheck',
{'b': bui.app.build_number}, {'b': bui.app.env.build_number},
callback=bui.WeakCall(self._on_public_party_accessible_response), callback=bui.WeakCall(self._on_public_party_accessible_response),
) )

View File

@ -621,7 +621,7 @@ class GetCurrencyWindow(bui.Window):
app = bui.app app = bui.app
assert app.classic is not None assert app.classic is not None
if ( if (
app.test_build app.env.test
or ( or (
app.classic.platform == 'android' app.classic.platform == 'android'
and app.classic.subplatform in ['oculus', 'cardboard'] and app.classic.subplatform in ['oculus', 'cardboard']
@ -664,8 +664,8 @@ class GetCurrencyWindow(bui.Window):
'item': item, 'item': item,
'platform': app.classic.platform, 'platform': app.classic.platform,
'subplatform': app.classic.subplatform, 'subplatform': app.classic.subplatform,
'version': app.version, 'version': app.env.version,
'buildNumber': app.build_number, 'buildNumber': app.env.build_number,
}, },
callback=bui.WeakCall(self._purchase_check_result, item), callback=bui.WeakCall(self._purchase_check_result, item),
) )

View File

@ -353,7 +353,7 @@ class HelpWindow(bui.Window):
v -= spacing * 45.0 v -= spacing * 45.0
txt = ( txt = (
bui.Lstr(resource=self._r + '.devicesText').evaluate() bui.Lstr(resource=self._r + '.devicesText').evaluate()
if app.vr_mode if app.env.vr
else bui.Lstr(resource=self._r + '.controllersText').evaluate() else bui.Lstr(resource=self._r + '.controllersText').evaluate()
) )
txt_scale = 0.74 txt_scale = 0.74
@ -372,7 +372,7 @@ class HelpWindow(bui.Window):
) )
txt_scale = 0.7 txt_scale = 0.7
if not app.vr_mode: if not app.env.vr:
infotxt = '.controllersInfoText' infotxt = '.controllersInfoText'
txt = bui.Lstr( txt = bui.Lstr(
resource=self._r + infotxt, resource=self._r + infotxt,

View File

@ -117,7 +117,7 @@ class MainMenuWindow(bui.Window):
force_test = False force_test = False
bs.get_local_active_input_devices_count() bs.get_local_active_input_devices_count()
if ( 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 and bui.app.config.get('launchCount', 0) <= 1
) or force_test: ) or force_test:

View File

@ -34,7 +34,7 @@ class PopupWindow:
focus_size = size focus_size = size
# In vr mode we can't have windows going outside the screen. # 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_size = size
focus_position = (0, 0) focus_position = (0, 0)

View File

@ -155,7 +155,7 @@ class ProfileUpgradeWindow(bui.Window):
bui.app.classic.master_server_v1_get( bui.app.classic.master_server_v1_get(
'bsGlobalProfileCheck', '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), callback=bui.WeakCall(self._profile_check_result),
) )
self._cost = plus.get_v1_account_misc_read_val( self._cost = plus.get_v1_account_misc_read_val(

View File

@ -88,7 +88,7 @@ class AdvancedSettingsWindow(bui.Window):
# In vr-mode, the internal keyboard is currently the *only* option, # In vr-mode, the internal keyboard is currently the *only* option,
# so no need to show this. # 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_width = self._width - (100 + 2 * x_inset)
self._scroll_height = self._height - 115.0 self._scroll_height = self._height - 115.0
@ -102,7 +102,7 @@ class AdvancedSettingsWindow(bui.Window):
if self._show_disable_gyro: if self._show_disable_gyro:
self._sub_height += 42 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._do_net_test_button = True
self._extra_button_spacing = self._spacing * 2.5 self._extra_button_spacing = self._spacing * 2.5
@ -178,7 +178,7 @@ class AdvancedSettingsWindow(bui.Window):
# Fetch the list of completed languages. # Fetch the list of completed languages.
bui.app.classic.master_server_v1_get( bui.app.classic.master_server_v1_get(
'bsLangGetCompleted', 'bsLangGetCompleted',
{'b': app.build_number}, {'b': app.env.build_number},
callback=bui.WeakCall(self._completed_langs_cb), callback=bui.WeakCall(self._completed_langs_cb),
) )
@ -322,7 +322,10 @@ class AdvancedSettingsWindow(bui.Window):
with open( with open(
os.path.join( 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', encoding='utf-8',
) as infile: ) as infile:

View File

@ -47,14 +47,14 @@ class ControlsSettingsWindow(bui.Window):
space_height = spacing * 0.3 space_height = spacing * 0.3
# FIXME: should create vis settings in platform for these, # FIXME: should create vis settings under platform or app-adapter
# not hard code them here. # to determine whether to show this stuff; not hard code it.
show_gamepads = False show_gamepads = False
platform = app.classic.platform platform = app.classic.platform
subplatform = app.classic.subplatform subplatform = app.classic.subplatform
non_vr_windows = platform == 'windows' and ( 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: if platform in ('linux', 'android', 'mac') or non_vr_windows:
show_gamepads = True show_gamepads = True
@ -70,11 +70,12 @@ class ControlsSettingsWindow(bui.Window):
show_space_1 = True show_space_1 = True
height += space_height height += space_height
print('hello')
show_keyboard = False show_keyboard = False
if bs.getinputdevice('Keyboard', '#1', doraise=False) is not None: if bs.getinputdevice('Keyboard', '#1', doraise=False) is not None:
show_keyboard = True show_keyboard = True
height += spacing 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: if show_keyboard_p2:
height += spacing height += spacing
@ -91,7 +92,7 @@ class ControlsSettingsWindow(bui.Window):
# On windows (outside of oculus/vr), show an option to disable xinput. # On windows (outside of oculus/vr), show an option to disable xinput.
show_xinput_toggle = False show_xinput_toggle = False
if platform == 'windows' and not app.vr_mode: if platform == 'windows' and not app.env.vr:
show_xinput_toggle = True show_xinput_toggle = True
# On mac builds, show an option to switch between generic and # On mac builds, show an option to switch between generic and
@ -352,6 +353,7 @@ class ControlsSettingsWindow(bui.Window):
maxwidth=width * 0.8, maxwidth=width * 0.8,
) )
v -= spacing * 1.5 v -= spacing * 1.5
self._restore_state() self._restore_state()
def _set_mac_controller_subsystem(self, val: str) -> None: def _set_mac_controller_subsystem(self, val: str) -> None:

View File

@ -829,7 +829,7 @@ class GamepadSettingsWindow(bui.Window):
'controllerConfig', 'controllerConfig',
{ {
'ua': classic.legacy_user_agent_string, 'ua': classic.legacy_user_agent_string,
'b': bui.app.build_number, 'b': bui.app.env.build_number,
'name': self._name, 'name': self._name,
'inputMapHash': inputhash, 'inputMapHash': inputhash,
'config': dst2, 'config': dst2,

View File

@ -91,7 +91,7 @@ class GamepadAdvancedSettingsWindow(bui.Window):
self._sub_height = ( self._sub_height = (
940 if self._parent_window.get_is_secondary() else 1040 940 if self._parent_window.get_is_secondary() else 1040
) )
if app.vr_mode: if app.env.vr:
self._sub_height += 50 self._sub_height += 50
self._scrollwidget = bui.scrollwidget( self._scrollwidget = bui.scrollwidget(
parent=self._root_widget, parent=self._root_widget,
@ -183,7 +183,7 @@ class GamepadAdvancedSettingsWindow(bui.Window):
) )
# in vr mode, allow assigning a reset-view button # in vr mode, allow assigning a reset-view button
if app.vr_mode: if app.env.vr:
v -= 50 v -= 50
self._capture_button( self._capture_button(
pos=(h2, v), pos=(h2, v),

View File

@ -61,7 +61,7 @@ class GraphicsSettingsWindow(bui.Window):
show_vsync = True show_vsync = True
show_resolution = True show_resolution = True
if app.vr_mode: if app.env.vr:
show_resolution = ( show_resolution = (
app.classic.platform == 'android' app.classic.platform == 'android'
and app.classic.subplatform == 'cardboard' and app.classic.subplatform == 'cardboard'
@ -400,7 +400,7 @@ class GraphicsSettingsWindow(bui.Window):
) )
# (tv mode doesnt apply to vr) # (tv mode doesnt apply to vr)
if not bui.app.vr_mode: if not bui.app.env.vr:
tvc = ConfigCheckBox( tvc = ConfigCheckBox(
parent=self._root_widget, parent=self._root_widget,
position=(240, v - 6), position=(240, v - 6),

View File

@ -301,7 +301,7 @@ class ConfigKeyboardWindow(bui.Window):
{ {
'ua': bui.app.classic.legacy_user_agent_string, 'ua': bui.app.classic.legacy_user_agent_string,
'name': self._name, 'name': self._name,
'b': bui.app.build_number, 'b': bui.app.env.build_number,
'config': dst2, 'config': dst2,
'v': 2, 'v': 2,
}, },

View File

@ -44,14 +44,14 @@ class SpecialOfferWindow(bui.Window):
real_price = plus.get_price( real_price = plus.get_price(
'pro' if offer['item'] == 'pro_fullprice' else 'pro_sale' '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.') print('NOTE: Faking prices for debug build.')
real_price = '$1.23' real_price = '$1.23'
zombie = real_price is None zombie = real_price is None
elif isinstance(offer['price'], str): elif isinstance(offer['price'], str):
# (a string price implies IAP id) # (a string price implies IAP id)
real_price = plus.get_price(offer['price']) 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.') print('NOTE: Faking price for debug build.')
real_price = '$1.23' real_price = '$1.23'
zombie = real_price is None zombie = real_price is None

View File

@ -566,8 +566,8 @@ class StoreBrowserWindow(bui.Window):
'item': item, 'item': item,
'platform': app.classic.platform, 'platform': app.classic.platform,
'subplatform': app.classic.subplatform, 'subplatform': app.classic.subplatform,
'version': app.version, 'version': app.env.version,
'buildNumber': app.build_number, 'buildNumber': app.env.build_number,
'purchaseType': 'ticket' if is_ticket_purchase else 'real', 'purchaseType': 'ticket' if is_ticket_purchase else 'real',
}, },
callback=bui.WeakCall( callback=bui.WeakCall(

View File

@ -715,5 +715,10 @@ void BaseFeatureSet::DoPushObjCall(const PythonObjectSetBase* objset, int id,
} }
auto BaseFeatureSet::IsAppStarted() const -> bool { return app_started_; } 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 } // namespace ballistica::base

View File

@ -690,6 +690,9 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
void DoPushObjCall(const PythonObjectSetBase* objset, int id, void DoPushObjCall(const PythonObjectSetBase* objset, int id,
const std::string& arg) override; const std::string& arg) override;
void OnReachedEndOfBaBaseImport(); 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 /// Called in the logic thread once our screen is up and assets are
/// loading. /// loading.
@ -748,6 +751,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
StressTest* stress_test_; StressTest* stress_test_;
std::string console_startup_messages_; std::string console_startup_messages_;
int shutdown_suppress_count_{};
bool tried_importing_plus_{}; bool tried_importing_plus_{};
bool tried_importing_classic_{}; bool tried_importing_classic_{};
bool tried_importing_ui_v1_{}; bool tried_importing_ui_v1_{};

View File

@ -1931,8 +1931,10 @@ auto Graphics::ScreenMessageEntry::GetText() -> TextGroup& {
return *s_mesh_; return *s_mesh_;
} }
void Graphics::OnScreenSizeChange(float virtual_width, float virtual_height, void Graphics::OnScreenSizeChange() {}
float pixel_width, float pixel_height) {
void Graphics::SetScreenSize(float virtual_width, float virtual_height,
float pixel_width, float pixel_height) {
assert(g_base->InLogicThread()); assert(g_base->InLogicThread());
res_x_virtual_ = virtual_width; res_x_virtual_ = virtual_width;
res_y_virtual_ = virtual_height; res_y_virtual_ = virtual_height;

View File

@ -54,9 +54,11 @@ class Graphics {
void OnAppPause(); void OnAppPause();
void OnAppResume(); void OnAppResume();
void OnAppShutdown(); void OnAppShutdown();
void OnScreenSizeChange();
void DoApplyAppConfig(); 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(); void StepDisplayTime();
static auto IsShaderTransparent(ShadingType c) -> bool; static auto IsShaderTransparent(ShadingType c) -> bool;

View File

@ -419,9 +419,10 @@ void GraphicsServer::HandleFullContextScreenRebuild(
UpdateVirtualScreenRes(); 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( g_base->logic->event_loop()->PushCall(
[vx = res_x_virtual_, vy = res_y_virtual_, x = res_x_, y = res_y_] { [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); g_base->logic->OnScreenSizeChange(vx, vy, x, y);
}); });
} }
@ -568,9 +569,10 @@ void GraphicsServer::SetScreenResolution(float h, float v) {
renderer_->ScreenSizeChanged(); renderer_->ScreenSizeChanged();
} }
// Inform logic thread of the change. // Inform graphics client and logic thread subsystems of the change.
g_base->logic->event_loop()->PushCall( g_base->logic->event_loop()->PushCall(
[vx = res_x_virtual_, vy = res_y_virtual_, x = res_x_, y = res_y_] { [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); g_base->logic->OnScreenSizeChange(vx, vy, x, y);
}); });
} }

View File

@ -7,6 +7,7 @@
#include "ballistica/base/audio/audio.h" #include "ballistica/base/audio/audio.h"
#include "ballistica/base/input/input.h" #include "ballistica/base/input/input.h"
#include "ballistica/base/networking/networking.h" #include "ballistica/base/networking/networking.h"
#include "ballistica/base/platform/base_platform.h"
#include "ballistica/base/python/base_python.h" #include "ballistica/base/python/base_python.h"
#include "ballistica/base/support/plus_soft.h" #include "ballistica/base/support/plus_soft.h"
#include "ballistica/base/support/stdio_console.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 // 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 // subsystems in their normal states which is less likely to lead to
// problems. // problems.
g_base->platform->OnAppStart();
g_base->graphics->OnAppStart(); g_base->graphics->OnAppStart();
g_base->audio->OnAppStart(); g_base->audio->OnAppStart();
g_base->input->OnAppStart(); g_base->input->OnAppStart();
@ -184,6 +186,7 @@ void Logic::OnAppPause() {
g_base->input->OnAppPause(); g_base->input->OnAppPause();
g_base->audio->OnAppPause(); g_base->audio->OnAppPause();
g_base->graphics->OnAppPause(); g_base->graphics->OnAppPause();
g_base->platform->OnAppPause();
} }
void Logic::OnAppResume() { void Logic::OnAppResume() {
@ -191,6 +194,7 @@ void Logic::OnAppResume() {
assert(g_base->CurrentContext().IsEmpty()); assert(g_base->CurrentContext().IsEmpty());
// Note: keep these in the same order as OnAppStart. // Note: keep these in the same order as OnAppStart.
g_base->platform->OnAppResume();
g_base->graphics->OnAppResume(); g_base->graphics->OnAppResume();
g_base->audio->OnAppResume(); g_base->audio->OnAppResume();
g_base->input->OnAppResume(); g_base->input->OnAppResume();
@ -235,6 +239,7 @@ void Logic::OnAppShutdown() {
g_base->input->OnAppShutdown(); g_base->input->OnAppShutdown();
g_base->audio->OnAppShutdown(); g_base->audio->OnAppShutdown();
g_base->graphics->OnAppShutdown(); g_base->graphics->OnAppShutdown();
g_base->platform->OnAppShutdown();
} }
void Logic::CompleteShutdown() { void Logic::CompleteShutdown() {
@ -283,12 +288,10 @@ void Logic::OnScreenSizeChange(float virtual_width, float virtual_height,
float pixel_width, float pixel_height) { float pixel_width, float pixel_height) {
assert(g_base->InLogicThread()); assert(g_base->InLogicThread());
// First, pass the new values to the graphics subsystem. Then inform // Inform all subsystems.
// everyone else simply that they changed; they can ask g_graphics for // Note: keep these in the same order as OnAppStart.
// whatever specific values they need. Note: keep these in the same order g_base->platform->OnScreenSizeChange();
// as OnAppStart. g_base->graphics->OnScreenSizeChange();
g_base->graphics->OnScreenSizeChange(virtual_width, virtual_height,
pixel_width, pixel_height);
g_base->audio->OnScreenSizeChange(); g_base->audio->OnScreenSizeChange();
g_base->input->OnScreenSizeChange(); g_base->input->OnScreenSizeChange();
g_base->ui->OnScreenSizeChange(); g_base->ui->OnScreenSizeChange();

View File

@ -314,4 +314,11 @@ void BasePlatform::GetCursorPosition(float* x, float* y) {
void BasePlatform::OnMainThreadStartAppComplete() {} 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 } // namespace ballistica::base

View File

@ -26,6 +26,13 @@ class BasePlatform {
/// start talking to them. /// start talking to them.
virtual void OnMainThreadStartAppComplete(); 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 -------------------------------------------------- #pragma mark IN APP PURCHASES --------------------------------------------------
void Purchase(const std::string& item); void Purchase(const std::string& item);

View File

@ -7,6 +7,7 @@
#include "ballistica/base/python/class/python_class_context_call.h" #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_context_ref.h"
#include "ballistica/base/python/class/python_class_display_timer.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_feature_set_data.h"
#include "ballistica/base/python/class/python_class_simple_sound.h" #include "ballistica/base/python/class/python_class_simple_sound.h"
#include "ballistica/base/python/class/python_class_vec3.h" #include "ballistica/base/python/class/python_class_vec3.h"
@ -45,6 +46,7 @@ void BasePython::AddPythonClasses(PyObject* module) {
PythonModuleBuilder::AddClass<PythonClassContextRef>(module); PythonModuleBuilder::AddClass<PythonClassContextRef>(module);
PythonModuleBuilder::AddClass<PythonClassAppTimer>(module); PythonModuleBuilder::AddClass<PythonClassAppTimer>(module);
PythonModuleBuilder::AddClass<PythonClassDisplayTimer>(module); PythonModuleBuilder::AddClass<PythonClassDisplayTimer>(module);
PythonModuleBuilder::AddClass<PythonClassEnv>(module);
PythonModuleBuilder::AddClass<PythonClassSimpleSound>(module); PythonModuleBuilder::AddClass<PythonClassSimpleSound>(module);
PythonModuleBuilder::AddClass<PythonClassContextCall>(module); PythonModuleBuilder::AddClass<PythonClassContextCall>(module);
PyObject* vec3 = PythonModuleBuilder::AddClass<PythonClassVec3>(module); PyObject* vec3 = PythonModuleBuilder::AddClass<PythonClassVec3>(module);

View File

@ -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<std::string, EnvEntry_>* 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<std::string, EnvEntry_>();
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<std::string> 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<std::string> 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<std::string> 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<PyObject*>(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<PyObject*>(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

View File

@ -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<PythonClassEnv*>(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_

View File

@ -1265,7 +1265,7 @@ static PyMethodDef PyIsOSPlayingMusicDef = {
"\n" "\n"
"Tells whether the OS is currently playing music of some sort.\n" "Tells whether the OS is currently playing music of some sort.\n"
"\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 ----------------------------------- // -------------------------------- exec_arg -----------------------------------
@ -1491,6 +1491,67 @@ static PyMethodDef PyGetImmediateReturnCodeDef = {
"(internal)\n", "(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<PyMethodDef> { auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
@ -1540,6 +1601,9 @@ auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
PyEmptyAppModeHandleIntentExecDef, PyEmptyAppModeHandleIntentExecDef,
PyGetImmediateReturnCodeDef, PyGetImmediateReturnCodeDef,
PyCompleteShutdownDef, PyCompleteShutdownDef,
PyShutdownSuppressBeginDef,
PyShutdownSuppressEndDef,
PyShutdownSuppressCountDef,
}; };
} }

View File

@ -134,7 +134,7 @@ auto CoreFeatureSet::core_config() const -> const CoreConfig& {
// we don't interfere with low-level stuff like FatalError handling that // we don't interfere with low-level stuff like FatalError handling that
// might need core_config access at any time. // might need core_config access at any time.
if (!g_buildconfig.monolithic_build()) { if (!g_buildconfig.monolithic_build()) {
if (!HaveBaEnvVals()) { if (!have_ba_env_vals()) {
static bool did_warn = false; static bool did_warn = false;
if (!did_warn) { if (!did_warn) {
did_warn = true; did_warn = true;

View File

@ -115,7 +115,7 @@ class CoreFeatureSet {
/// Return true if baenv values have been locked in: python paths, log /// Return true if baenv values have been locked in: python paths, log
/// handling, etc. Early-running code may wish to explicitly avoid making log /// handling, etc. Early-running code may wish to explicitly avoid making log
/// calls until this condition is met to ensure predictable behavior. /// 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 /// Return the directory where the app expects to find its bundled Python
/// files. /// files.

View File

@ -396,7 +396,7 @@ auto CorePython::FetchPythonArgs(std::vector<std::string>* buffer)
// argv pointers to it. // argv pointers to it.
std::vector<char*> out; std::vector<char*> out;
out.reserve(buffer->size()); 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<char*>((*buffer)[i].c_str())); out.push_back(const_cast<char*>((*buffer)[i].c_str()));
} }
return out; return out;

View File

@ -39,8 +39,9 @@ auto main(int argc, char** argv) -> int {
namespace ballistica { namespace ballistica {
// These are set automatically via script; don't modify them here. // These are set automatically via script; don't modify them here.
const int kEngineBuildNumber = 21256; const int kEngineBuildNumber = 21269;
const char* kEngineVersion = "1.7.26"; const char* kEngineVersion = "1.7.27";
const int kEngineApiVersion = 8;
#if BA_MONOLITHIC_BUILD #if BA_MONOLITHIC_BUILD

View File

@ -29,6 +29,7 @@ namespace ballistica {
extern const int kEngineBuildNumber; extern const int kEngineBuildNumber;
extern const char* kEngineVersion; extern const char* kEngineVersion;
extern const int kEngineApiVersion;
// Protocol version we host games with and write replays to. // Protocol version we host games with and write replays to.
// This should be incremented whenever there are changes made to the // This should be incremented whenever there are changes made to the

View File

@ -35,16 +35,15 @@ auto PythonClassHello::tp_new(PyTypeObject* type, PyObject* args,
void PythonClassHello::tp_dealloc(PythonClassHello* self) { void PythonClassHello::tp_dealloc(PythonClassHello* self) {
BA_PYTHON_TRY; BA_PYTHON_TRY;
// Because we used placement-new we need to manually run the equivalent // 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 // destructor to clean ourself up. Note that if anything goes wrong here
// simply print an error; we don't set any Python error state. Not sure if // it'll simply print an error; we don't set any Python error state. Not
// that is ever even allowed from destructors anyway. // sure if that is ever even allowed from destructors anyway.
// IMPORTANT: With Python objects we can't guarantee that this destructor runs // IMPORTANT: With Python objects we can't guarantee that this destructor
// in a particular thread; if our object contains anything that must be // runs in a particular thread, so if that is something we need then we
// destructed in a particular thread then we should manually allocate & // should manually allocate stuff in tp_new and then ship a pointer off
// deallocate things so we can ship it off to the proper thread for cleanup as // from here to whatever thread needs to clean it up.
// needed.
self->~PythonClassHello(); self->~PythonClassHello();
BA_PYTHON_DEALLOC_CATCH; BA_PYTHON_DEALLOC_CATCH;
Py_TYPE(self)->tp_free(reinterpret_cast<PyObject*>(self)); Py_TYPE(self)->tp_free(reinterpret_cast<PyObject*>(self));
@ -58,7 +57,32 @@ PythonClassHello::~PythonClassHello() {
Log(LogLevel::kInfo, "Goodbye from PythonClassHello destructor!!!"); 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<char**>(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; 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 } // namespace ballistica::template_fs

View File

@ -36,8 +36,8 @@ class PythonClassHello : public PythonClass {
static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds)
-> PyObject*; -> PyObject*;
static void tp_dealloc(PythonClassHello* self); static void tp_dealloc(PythonClassHello* self);
static auto Play(PythonClassHello* self, PyObject* args, PyObject* keywds) static auto TestMethod(PythonClassHello* self, PyObject* args,
-> PyObject*; PyObject* keywds) -> PyObject*;
}; };
} // namespace ballistica::template_fs } // namespace ballistica::template_fs

View File

@ -71,13 +71,19 @@ def get_current_version() -> tuple[str, int]:
def get_current_api_version() -> int: def get_current_api_version() -> int:
"""Pull current api version from the project.""" """Pull current api version from the project."""
with open( with open(
'src/assets/ba_data/python/babase/_meta.py', encoding='utf-8' 'src/ballistica/shared/ballistica.cc', encoding='utf-8'
) as infile: ) as infile:
lines = infile.readlines() lines = infile.readlines()
linestart = 'CURRENT_API_VERSION = ' linestart = 'const int kEngineApiVersion = '
lineend = ';'
for line in lines: for line in lines:
if line.startswith(linestart): 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.') raise RuntimeError('Api version line not found.')

View File

@ -35,6 +35,7 @@ OPENSSL_VER_APPLE = '3.0.8'
OPENSSL_VER_ANDROID = '3.0.8' OPENSSL_VER_ANDROID = '3.0.8'
ZLIB_VER_ANDROID = '1.3' 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. # Filenames we prune from Python lib dirs in source repo to cut down on size.
PRUNE_LIB_NAMES = [ PRUNE_LIB_NAMES = [
@ -301,6 +302,14 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None:
count=1, 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. # Give ourselves a handle to patch the OpenSSL build.
ftxt = replace_exact( ftxt = replace_exact(
ftxt, ftxt,