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

View File

@ -1622,6 +1622,7 @@
<w>linearstep</w>
<w>linebegin</w>
<w>linebits</w>
<w>lineend</w>
<w>lineheight</w>
<w>linemax</w>
<w>lineno</w>
@ -2777,6 +2778,7 @@
<w>sslcontext</w>
<w>sslproto</w>
<w>ssval</w>
<w>stacklevel</w>
<w>stackstr</w>
<w>stager</w>
<w>standin</w>
@ -2857,6 +2859,7 @@
<w>successmsg</w>
<w>suiciding</w>
<w>sunau</w>
<w>suppressions</w>
<w>suter</w>
<w>sval</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
'Configure Keyboard' and 'Configure Keyboard P2' buttons under
@ -30,9 +58,9 @@
should be more consistent use of the 'Quit?' confirm window. Please holler if
you see any odd behavior when trying to quit the app.
- Unix TERM signal now triggers graceful app shutdown.
- Added `ba.app.add_shutdown_task()` to register coroutines to be run as part of
- Added `app.add_shutdown_task()` to register coroutines to be run as part of
shutdown.
- Removed `babase.app.iircade_mode`. RIP iiRcade :(.
- Removed `app.iircade_mode`. RIP iiRcade :(.
- Changed `AppState.INITIAL` to `AppState.NOT_RUNNING`, added a
`AppState.NATIVE_BOOTSTRAPPING`, and changed `AppState.LAUNCHING` to
`AppState.INITING`. These better describe what the app is actually doing while

View File

@ -960,6 +960,7 @@
<w>linearsize</w>
<w>linearstep</w>
<w>linebegin</w>
<w>lineend</w>
<w>linemax</w>
<w>linestart</w>
<w>linestripped</w>
@ -1643,6 +1644,7 @@
<w>sssssssi</w>
<w>ssssssssssss</w>
<w>ssval</w>
<w>stacklevel</w>
<w>stager</w>
<w>standin</w>
<w>startedptr</w>
@ -1690,6 +1692,7 @@
<w>subsys</w>
<w>subtypestr</w>
<w>successmsg</w>
<w>suppressions</w>
<w>sval</w>
<w>swidth</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_display_timer.cc
${BA_SRC_ROOT}/ballistica/base/python/class/python_class_display_timer.h
${BA_SRC_ROOT}/ballistica/base/python/class/python_class_env.cc
${BA_SRC_ROOT}/ballistica/base/python/class/python_class_env.h
${BA_SRC_ROOT}/ballistica/base/python/class/python_class_feature_set_data.cc
${BA_SRC_ROOT}/ballistica/base/python/class/python_class_feature_set_data.h
${BA_SRC_ROOT}/ballistica/base/python/class/python_class_simple_sound.cc

View File

@ -379,6 +379,8 @@
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_context_ref.h" />
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_display_timer.cc" />
<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" />
<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" />

View File

@ -571,6 +571,12 @@
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_display_timer.h">
<Filter>ballistica\base\python\class</Filter>
</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">
<Filter>ballistica\base\python\class</Filter>
</ClCompile>

View File

@ -374,6 +374,8 @@
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_context_ref.h" />
<ClCompile Include="..\..\src\ballistica\base\python\class\python_class_display_timer.cc" />
<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" />
<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" />

View File

@ -571,6 +571,12 @@
<ClInclude Include="..\..\src\ballistica\base\python\class\python_class_display_timer.h">
<Filter>ballistica\base\python\class</Filter>
</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">
<Filter>ballistica\base\python\class</Filter>
</ClCompile>

View File

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

View File

@ -1,11 +1,14 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the high level state of the app."""
# pylint: disable=too-many-lines
from __future__ import annotations
import os
import logging
from enum import Enum
import warnings
from typing import TYPE_CHECKING
from concurrent.futures import ThreadPoolExecutor
from functools import cached_property
@ -55,6 +58,7 @@ class App:
plugins: PluginSubsystem
lang: LanguageSubsystem
env: babase.Env
health_monitor: AppHealthMonitor
@ -68,173 +72,72 @@ class App:
# The app has not yet begun starting and should not be used in
# any way.
NOT_RUNNING = 'not_running'
NOT_RUNNING = 0
# The native layer is spinning up its machinery (screens,
# renderers, etc.). Nothing should happen in the Python layer
# until this completes.
NATIVE_BOOTSTRAPPING = 'native_bootstrapping'
NATIVE_BOOTSTRAPPING = 1
# Python app subsystems are being inited but should not yet
# interact or do any work.
INITING = 'initing'
INITING = 2
# Python app subsystems are inited and interacting, but the app
# has not yet embarked on a high level course of action. It is
# doing initial account logins, workspace & asset downloads,
# etc.
LOADING = 'loading'
LOADING = 3
# All pieces are in place and the app is now doing its thing.
RUNNING = 'running'
RUNNING = 4
# The app is backgrounded or otherwise suspended.
PAUSED = 'paused'
PAUSED = 5
# The app is shutting down.
SHUTTING_DOWN = 'shutting_down'
SHUTTING_DOWN = 6
@property
def aioloop(self) -> asyncio.AbstractEventLoop:
"""The logic thread's asyncio event loop.
class DefaultAppModeSelector(AppModeSelector):
"""Decides which app modes to use to handle intents.
This allow async tasks to be run in the logic thread.
Note that, at this time, the asyncio loop is encapsulated
and explicitly stepped by the engine's logic thread loop and
thus things like asyncio.get_running_loop() will not return this
loop from most places in the logic thread; only from within a
task explicitly created in this loop.
The behavior here is generated by the project updater based on
the feature-sets present in the project. Spinoff projects can
also inject their own behavior by replacing the text
'__GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__' with their own code
through spinoff filtering.
It is also possible to modify mode selection behavior by writing
a custom AppModeSelector class and replacing app.mode_selector
with an instance of it. This is a good way to go if you are
modifying app behavior with a plugin instead of in a spinoff
project.
"""
assert self._aioloop is not None
return self._aioloop
@property
def build_number(self) -> int:
"""Integer build number.
def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]:
# pylint: disable=cyclic-import
This value increases by at least 1 with each release of the game.
It is independent of the human readable babase.App.version string.
"""
assert isinstance(self._env['build_number'], int)
return self._env['build_number']
# __GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__
@property
def device_name(self) -> str:
"""Name of the device running the game."""
assert isinstance(self._env['device_name'], str)
return self._env['device_name']
# __DEFAULT_APP_MODE_SELECTION_BEGIN__
# This section generated by batools.appmodule; do not edit.
@property
def config_file_path(self) -> str:
"""Where the game's config file is stored on disk."""
assert isinstance(self._env['config_file_path'], str)
return self._env['config_file_path']
# Hmm; need to think about how we auto-construct this; how
# should we determine which app modes to check and in what
# order?
import bascenev1
@property
def version(self) -> str:
"""Human-readable version string; something like '1.3.24'.
import babase
This should not be interpreted as a number; it may contain
string elements such as 'alpha', 'beta', 'test', etc.
If a numeric version is needed, use 'babase.App.build_number'.
"""
assert isinstance(self._env['version'], str)
return self._env['version']
if bascenev1.SceneV1AppMode.supports_intent(intent):
return bascenev1.SceneV1AppMode
@property
def debug_build(self) -> bool:
"""Whether the app was compiled in debug mode.
if babase.EmptyAppMode.supports_intent(intent):
return babase.EmptyAppMode
Debug builds generally run substantially slower than non-debug
builds due to compiler optimizations being disabled and extra
checks being run.
"""
assert isinstance(self._env['debug_build'], bool)
return self._env['debug_build']
raise RuntimeError(f'No handler found for intent {type(intent)}.')
@property
def test_build(self) -> bool:
"""Whether the game was compiled in test mode.
Test mode enables extra checks and features that are useful for
release testing but which do not slow the game down significantly.
"""
assert isinstance(self._env['test_build'], bool)
return self._env['test_build']
@property
def data_directory(self) -> str:
"""Path where static app data lives."""
assert isinstance(self._env['data_directory'], str)
return self._env['data_directory']
@property
def python_directory_user(self) -> str | None:
"""Path where ballistica expects its user scripts (mods) to live.
Be aware that this value may be None if ballistica is running in
a non-standard environment, and that python-path modifications may
cause modules to be loaded from other locations.
"""
assert isinstance(self._env['python_directory_user'], (str, type(None)))
return self._env['python_directory_user']
@property
def python_directory_app(self) -> str | None:
"""Path where ballistica expects its bundled modules to live.
Be aware that this value may be None if ballistica is running in
a non-standard environment, and that python-path modifications may
cause modules to be loaded from other locations.
"""
assert isinstance(self._env['python_directory_app'], (str, type(None)))
return self._env['python_directory_app']
@property
def python_directory_app_site(self) -> str | None:
"""Path where ballistica expects its bundled pip modules to live.
Be aware that this value may be None if ballistica is running in
a non-standard environment, and that python-path modifications may
cause modules to be loaded from other locations.
"""
assert isinstance(
self._env['python_directory_app_site'], (str, type(None))
)
return self._env['python_directory_app_site']
@property
def config(self) -> babase.AppConfig:
"""The babase.AppConfig instance representing the app's config state."""
assert self._config is not None
return self._config
@property
def api_version(self) -> int:
"""The app's api version.
Only Python modules and packages associated with the current API
version number will be detected by the game (see the ba_meta tag).
This value will change whenever substantial backward-incompatible
changes are introduced to ballistica APIs. When that happens,
modules/packages should be updated accordingly and set to target
the newer API version number.
"""
from babase._meta import CURRENT_API_VERSION
return CURRENT_API_VERSION
@property
def on_tv(self) -> bool:
"""Whether the game is currently running on a TV."""
assert isinstance(self._env['on_tv'], bool)
return self._env['on_tv']
@property
def vr_mode(self) -> bool:
"""Whether the game is currently running in VR."""
assert isinstance(self._env['vr_mode'], bool)
return self._env['vr_mode']
# __DEFAULT_APP_MODE_SELECTION_END__
def __init__(self) -> None:
"""(internal)
@ -248,6 +151,8 @@ class App:
self.state = self.State.NOT_RUNNING
self.env: babase.Env = _babase.Env()
self._subsystems: list[AppSubsystem] = []
self._native_bootstrapping_completed = False
@ -263,12 +168,11 @@ class App:
self._subsystem_registration_ended = False
self._pending_apply_app_config = False
# Config.
self.config_file_healthy = False
self.config_file_healthy: bool = False
# This is incremented any time the app is
# backgrounded/foregrounded; can be a simple way to determine if
# network data should be refreshed/etc.
# This is incremented any time the app is backgrounded or
# foregrounded; can be a simple way to determine if network data
# should be refreshed/etc.
self.fg_state = 0
self._aioloop: asyncio.AbstractEventLoop | None = None
@ -294,20 +198,25 @@ class App:
self._config: babase.AppConfig | None = None
self.components = AppComponentSubsystem()
# Testing this.
self.meta = MetadataSubsystem()
self.net = NetworkSubsystem()
self.workspaces = WorkspaceSubsystem()
self._pending_intent: AppIntent | None = None
self._intent: AppIntent | None = None
self._mode: AppMode | None = None
self._shutdown_task: asyncio.Task[None] | None = None
self._shutdown_tasks: list[Coroutine[None, None, None]] = []
self._shutdown_tasks: list[Coroutine[None, None, None]] = [
self._wait_for_shutdown_suppressions()
]
# Controls which app-modes we use for handling given
# app-intents. Plugins can override this to change high level
# app behavior and spinoff projects can change the default
# implementation for the same effect.
self.mode_selector: AppModeSelector | None = None
self.mode_selector: babase.AppModeSelector | None = None
self._asyncio_timer: babase.AppTimer | None = None
@ -324,35 +233,25 @@ class App:
self.lang = LanguageSubsystem()
self.plugins = PluginSubsystem()
def register_subsystem(self, subsystem: AppSubsystem) -> None:
"""Called by the AppSubsystem class. Do not use directly."""
@property
def aioloop(self) -> asyncio.AbstractEventLoop:
"""The logic thread's asyncio event loop.
# We only allow registering new subsystems if we've not yet
# reached the 'running' state. This ensures that all subsystems
# receive a consistent set of callbacks starting with
# on_app_running().
if self._subsystem_registration_ended:
raise RuntimeError(
'Subsystems can no longer be registered at this point.'
)
self._subsystems.append(subsystem)
def _threadpool_no_wait_done(self, fut: Future) -> None:
try:
fut.result()
except Exception:
logging.exception(
'Error in work submitted via threadpool_submit_no_wait()'
)
def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
"""Submit work to our threadpool and log any errors.
Use this when you want to run something asynchronously but don't
intend to call result() on it to handle errors/etc.
This allow async tasks to be run in the logic thread.
Note that, at this time, the asyncio loop is encapsulated
and explicitly stepped by the engine's logic thread loop and
thus things like asyncio.get_running_loop() will not return this
loop from most places in the logic thread; only from within a
task explicitly created in this loop.
"""
fut = self.threadpool.submit(call)
fut.add_done_callback(self._threadpool_no_wait_done)
assert self._aioloop is not None
return self._aioloop
@property
def config(self) -> babase.AppConfig:
"""The babase.AppConfig instance representing the app's config state."""
assert self._config is not None
return self._config
# __FEATURESET_APP_SUBSYSTEM_PROPERTIES_BEGIN__
# This section generated by batools.appmodule; do not edit.
@ -398,6 +297,48 @@ class App:
# __FEATURESET_APP_SUBSYSTEM_PROPERTIES_END__
def register_subsystem(self, subsystem: AppSubsystem) -> None:
"""Called by the AppSubsystem class. Do not use directly."""
# We only allow registering new subsystems if we've not yet
# reached the 'running' state. This ensures that all subsystems
# receive a consistent set of callbacks starting with
# on_app_running().
if self._subsystem_registration_ended:
raise RuntimeError(
'Subsystems can no longer be registered at this point.'
)
self._subsystems.append(subsystem)
def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
"""Add a task to be run on app shutdown.
Note that tasks will be killed after
App.SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.
"""
if self.state is self.State.SHUTTING_DOWN:
raise RuntimeError(
'Cannot add shutdown tasks with state SHUTTING_DOWN.'
)
self._shutdown_tasks.append(coro)
def run(self) -> None:
"""Run the app to completion.
Note that this only works on platforms where Ballistica
manages its own event loop.
"""
_babase.run_app()
def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
"""Submit work to our threadpool and log any errors.
Use this when you want to run something asynchronously but don't
intend to call result() on it to handle errors/etc.
"""
fut = self.threadpool.submit(call)
fut.add_done_callback(self._threadpool_no_wait_done)
def set_intent(self, intent: AppIntent) -> None:
"""Set the intent for the app.
@ -417,6 +358,92 @@ class App:
# since it may block for a moment to load modules/etc.
self.threadpool_submit_no_wait(tpartial(self._set_intent, intent))
def push_apply_app_config(self) -> None:
"""Internal. Use app.config.apply() to apply app config changes."""
# To be safe, let's run this by itself in the event loop.
# This avoids potential trouble if this gets called mid-draw or
# something like that.
self._pending_apply_app_config = True
_babase.pushcall(self._apply_app_config, raw=True)
def on_native_start(self) -> None:
"""Called by the native layer when the app is being started."""
assert _babase.in_logic_thread()
assert not self._native_start_called
self._native_start_called = True
self._update_state()
def on_native_bootstrapping_complete(self) -> None:
"""Called by the native layer once its ready to rock."""
assert _babase.in_logic_thread()
assert not self._native_bootstrapping_completed
self._native_bootstrapping_completed = True
self._update_state()
def on_native_pause(self) -> None:
"""Called by the native layer when the app pauses."""
assert _babase.in_logic_thread()
assert not self._native_paused # Should avoid redundant calls.
self._native_paused = True
self._update_state()
def on_native_resume(self) -> None:
"""Called by the native layer when the app resumes."""
assert _babase.in_logic_thread()
assert self._native_paused # Should avoid redundant calls.
self._native_paused = False
self._update_state()
def on_native_shutdown(self) -> None:
"""Called by the native layer when the app starts shutting down."""
assert _babase.in_logic_thread()
self._native_shutdown_called = True
self._update_state()
def read_config(self) -> None:
"""(internal)"""
from babase._appconfig import read_app_config
self._config, self.config_file_healthy = read_app_config()
def handle_deep_link(self, url: str) -> None:
"""Handle a deep link URL."""
from babase._language import Lstr
assert _babase.in_logic_thread()
appname = _babase.appname()
if url.startswith(f'{appname}://code/'):
code = url.replace(f'{appname}://code/', '')
if self.classic is not None:
self.classic.accounts.add_pending_promo_code(code)
else:
try:
_babase.screenmessage(
Lstr(resource='errorText'), color=(1, 0, 0)
)
_babase.getsimplesound('error').play()
except ImportError:
pass
def on_initial_sign_in_complete(self) -> None:
"""Called when initial sign-in (or lack thereof) completes.
This normally gets called by the plus subsystem. The
initial-sign-in process may include tasks such as syncing
account workspaces or other data so it may take a substantial
amount of time.
"""
assert _babase.in_logic_thread()
assert not self._initial_sign_in_completed
# Tell meta it can start scanning extra stuff that just showed
# up (namely account workspaces).
self.meta.start_extra_scan()
self._initial_sign_in_completed = True
self._update_state()
def _set_intent(self, intent: AppIntent) -> None:
# This should be running in a bg thread.
assert not _babase.in_logic_thread()
@ -492,14 +519,6 @@ class App:
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
_babase.getsimplesound('error').play()
def run(self) -> None:
"""Run the app to completion.
Note that this only works on platforms where Ballistica
manages its own event loop.
"""
_babase.run_app()
def _on_initing(self) -> None:
"""Called when the app enters the initing state.
@ -633,14 +652,6 @@ class App:
# plugin hasn't already told it to do something.
self.set_intent(AppIntentDefault())
def push_apply_app_config(self) -> None:
"""Internal. Use app.config.apply() to apply app config changes."""
# To be safe, let's run this by itself in the event loop.
# This avoids potential trouble if this gets called mid-draw or
# something like that.
self._pending_apply_app_config = True
_babase.pushcall(self._apply_app_config, raw=True)
def _apply_app_config(self) -> None:
assert _babase.in_logic_thread()
@ -667,47 +678,6 @@ class App:
# Let the native layer do its thing.
_babase.do_apply_app_config()
class DefaultAppModeSelector(AppModeSelector):
"""Decides which app modes to use to handle intents.
The behavior here is generated by the project updater based on
the feature-sets present in the project. Spinoff projects can
also inject their own behavior by replacing the text
'__GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__' with their own code
through spinoff filtering.
It is also possible to modify mode selection behavior by writing
a custom AppModeSelector class and replacing app.mode_selector
with an instance of it. This is a good way to go if you are
modifying app behavior with a plugin instead of in a spinoff
project.
"""
def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]:
# pylint: disable=cyclic-import
# __GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__
# __DEFAULT_APP_MODE_SELECTION_BEGIN__
# This section generated by batools.appmodule; do not edit.
# Hmm; need to think about how we auto-construct this; how
# should we determine which app modes to check and in what
# order?
import bascenev1
import babase
if bascenev1.SceneV1AppMode.supports_intent(intent):
return bascenev1.SceneV1AppMode
if babase.EmptyAppMode.supports_intent(intent):
return babase.EmptyAppMode
raise RuntimeError(f'No handler found for intent {type(intent)}.')
# __DEFAULT_APP_MODE_SELECTION_END__
def _update_state(self) -> None:
# pylint: disable=too-many-branches
assert _babase.in_logic_thread()
@ -761,18 +731,6 @@ class App:
if bool(True):
self.state = self.State.NATIVE_BOOTSTRAPPING
def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
"""Add a task to be run on app shutdown.
Note that tasks will be killed after
App.SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.
"""
if self.state is self.State.SHUTTING_DOWN:
raise RuntimeError(
'Cannot add shutdown tasks with state SHUTTING_DOWN.'
)
self._shutdown_tasks.append(coro)
async def _shutdown(self) -> None:
import asyncio
@ -802,40 +760,6 @@ class App:
except Exception:
logging.exception('Error in shutdown task.')
def on_native_start(self) -> None:
"""Called by the native layer when the app is being started."""
assert _babase.in_logic_thread()
assert not self._native_start_called
self._native_start_called = True
self._update_state()
def on_native_bootstrapping_complete(self) -> None:
"""Called by the native layer once its ready to rock."""
assert _babase.in_logic_thread()
assert not self._native_bootstrapping_completed
self._native_bootstrapping_completed = True
self._update_state()
def on_native_pause(self) -> None:
"""Called by the native layer when the app pauses."""
assert _babase.in_logic_thread()
assert not self._native_paused # Should avoid redundant calls.
self._native_paused = True
self._update_state()
def on_native_resume(self) -> None:
"""Called by the native layer when the app resumes."""
assert _babase.in_logic_thread()
assert self._native_paused # Should avoid redundant calls.
self._native_paused = False
self._update_state()
def on_native_shutdown(self) -> None:
"""Called by the native layer when the app starts shutting down."""
assert _babase.in_logic_thread()
self._native_shutdown_called = True
self._update_state()
def _on_pause(self) -> None:
"""Called when the app goes to a paused state."""
assert _babase.in_logic_thread()
@ -881,46 +805,198 @@ class App:
assert self._aioloop is not None
self._shutdown_task = self._aioloop.create_task(self._shutdown())
def read_config(self) -> None:
"""(internal)"""
from babase._appconfig import read_app_config
async def _wait_for_shutdown_suppressions(self) -> None:
import asyncio
self._config, self.config_file_healthy = read_app_config()
# Spin and wait for anything blocking shutdown to complete.
_babase.lifecyclelog('shutdown-suppress wait begin')
while _babase.shutdown_suppress_count() > 0:
await asyncio.sleep(0.001)
_babase.lifecyclelog('shutdown-suppress wait end')
def handle_deep_link(self, url: str) -> None:
"""Handle a deep link URL."""
from babase._language import Lstr
def _threadpool_no_wait_done(self, fut: Future) -> None:
try:
fut.result()
except Exception:
logging.exception(
'Error in work submitted via threadpool_submit_no_wait()'
)
assert _babase.in_logic_thread()
# --------------------------------------------------------------------
# THE FOLLOWING ARE DEPRECATED AND WILL BE REMOVED IN A FUTURE UPDATE.
# --------------------------------------------------------------------
appname = _babase.appname()
if url.startswith(f'{appname}://code/'):
code = url.replace(f'{appname}://code/', '')
if self.classic is not None:
self.classic.accounts.add_pending_promo_code(code)
else:
try:
_babase.screenmessage(
Lstr(resource='errorText'), color=(1, 0, 0)
)
_babase.getsimplesound('error').play()
except ImportError:
pass
@property
def build_number(self) -> int:
"""Integer build number.
def on_initial_sign_in_complete(self) -> None:
"""Called when initial sign-in (or lack thereof) completes.
This normally gets called by the plus subsystem. The
initial-sign-in process may include tasks such as syncing
account workspaces or other data so it may take a substantial
amount of time.
This value increases by at least 1 with each release of the engine.
It is independent of the human readable babase.App.version string.
"""
assert _babase.in_logic_thread()
assert not self._initial_sign_in_completed
warnings.warn(
'app.build_number is deprecated; use app.env.build_number',
DeprecationWarning,
stacklevel=2,
)
return self.env.build_number
# Tell meta it can start scanning extra stuff that just showed
# up (namely account workspaces).
self.meta.start_extra_scan()
@property
def device_name(self) -> str:
"""Name of the device running the app."""
warnings.warn(
'app.device_name is deprecated; use app.env.device_name',
DeprecationWarning,
stacklevel=2,
)
return self.env.device_name
self._initial_sign_in_completed = True
self._update_state()
@property
def config_file_path(self) -> str:
"""Where the app's config file is stored on disk."""
warnings.warn(
'app.config_file_path is deprecated;'
' use app.env.config_file_path',
DeprecationWarning,
stacklevel=2,
)
return self.env.config_file_path
@property
def version(self) -> str:
"""Human-readable engine version string; something like '1.3.24'.
This should not be interpreted as a number; it may contain
string elements such as 'alpha', 'beta', 'test', etc.
If a numeric version is needed, use `build_number`.
"""
warnings.warn(
'app.version is deprecated; use app.env.version',
DeprecationWarning,
stacklevel=2,
)
return self.env.version
@property
def debug_build(self) -> bool:
"""Whether the app was compiled in debug mode.
Debug builds generally run substantially slower than non-debug
builds due to compiler optimizations being disabled and extra
checks being run.
"""
warnings.warn(
'app.debug_build is deprecated; use app.env.debug',
DeprecationWarning,
stacklevel=2,
)
return self.env.debug
@property
def test_build(self) -> bool:
"""Whether the app was compiled in test mode.
Test mode enables extra checks and features that are useful for
release testing but which do not slow the game down significantly.
"""
warnings.warn(
'app.test_build is deprecated; use app.env.test',
DeprecationWarning,
stacklevel=2,
)
return self.env.test
@property
def data_directory(self) -> str:
"""Path where static app data lives."""
warnings.warn(
'app.data_directory is deprecated; use app.env.data_directory',
DeprecationWarning,
stacklevel=2,
)
return self.env.data_directory
@property
def python_directory_user(self) -> str | None:
"""Path where the app expects its user scripts (mods) to live.
Be aware that this value may be None if ballistica is running in
a non-standard environment, and that python-path modifications may
cause modules to be loaded from other locations.
"""
warnings.warn(
'app.python_directory_user is deprecated;'
' use app.env.python_directory_user',
DeprecationWarning,
stacklevel=2,
)
return self.env.python_directory_user
@property
def python_directory_app(self) -> str | None:
"""Path where the app expects its bundled modules to live.
Be aware that this value may be None if Ballistica is running in
a non-standard environment, and that python-path modifications may
cause modules to be loaded from other locations.
"""
warnings.warn(
'app.python_directory_app is deprecated;'
' use app.env.python_directory_app',
DeprecationWarning,
stacklevel=2,
)
return self.env.python_directory_app
@property
def python_directory_app_site(self) -> str | None:
"""Path where the app expects its bundled pip modules to live.
Be aware that this value may be None if Ballistica is running in
a non-standard environment, and that python-path modifications may
cause modules to be loaded from other locations.
"""
warnings.warn(
'app.python_directory_app_site is deprecated;'
' use app.env.python_directory_app_site',
DeprecationWarning,
stacklevel=2,
)
return self.env.python_directory_app_site
@property
def api_version(self) -> int:
"""The app's api version.
Only Python modules and packages associated with the current API
version number will be detected by the game (see the ba_meta tag).
This value will change whenever substantial backward-incompatible
changes are introduced to ballistica APIs. When that happens,
modules/packages should be updated accordingly and set to target
the newer API version number.
"""
warnings.warn(
'app.api_version is deprecated; use app.env.api_version',
DeprecationWarning,
stacklevel=2,
)
return self.env.api_version
@property
def on_tv(self) -> bool:
"""Whether the app is currently running on a TV."""
warnings.warn(
'app.on_tv is deprecated; use app.env.tv',
DeprecationWarning,
stacklevel=2,
)
return self.env.tv
@property
def vr_mode(self) -> bool:
"""Whether the app is currently running in VR."""
warnings.warn(
'app.vr_mode is deprecated; use app.env.vr',
DeprecationWarning,
stacklevel=2,
)
return self.env.vr

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
# config object will not change from here on out
config_file_path = _babase.app.config_file_path
config_file_path = _babase.app.env.config_file_path
config_contents = ''
try:
if os.path.exists(config_file_path):

View File

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

View File

@ -6,6 +6,7 @@ from __future__ import annotations
import sys
import signal
import logging
import warnings
from typing import TYPE_CHECKING
from efro.log import LogLevel
@ -103,6 +104,12 @@ def on_main_thread_start_app() -> None:
signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling.
_babase.setup_sigint()
# Turn on deprecation warnings. By default these are off for release
# builds except for in __main__. However this is a key way to
# communicate api changes to modders and most modders are running
# release builds so its good to have this on everywhere.
warnings.simplefilter('default', DeprecationWarning)
# Turn off fancy-pants cyclic garbage-collection. We run it only at
# explicit times to avoid random hitches and keep things more
# deterministic. Non-reference-looped objects will still get cleaned

View File

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

View File

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

View File

@ -18,11 +18,6 @@ import _babase
if TYPE_CHECKING:
from typing import Callable
# The meta api version of this build of the game.
# Only packages and modules requiring this exact api version
# will be considered when scanning directories.
# See: https://ballistica.net/wiki/Meta-Tag-System
CURRENT_API_VERSION = 8
# Meta export lines can use these names to represent these classes.
# This is purely a convenience; it is possible to use full class paths
@ -76,14 +71,15 @@ class MetadataSubsystem:
"""
assert self._scan_complete_cb is None
assert self._scan is None
env = _babase.app.env
self._scan_complete_cb = scan_complete_cb
self._scan = DirectoryScan(
[
path
for path in [
_babase.app.python_directory_app,
_babase.app.python_directory_user,
env.python_directory_app,
env.python_directory_user,
]
if path is not None
]
@ -212,7 +208,7 @@ class MetadataSubsystem:
'${NUM}',
str(len(results.incorrect_api_modules) - 1),
),
('${API}', str(CURRENT_API_VERSION)),
('${API}', str(_babase.app.env.api_version)),
],
)
else:
@ -220,7 +216,7 @@ class MetadataSubsystem:
resource='scanScriptsSingleModuleNeedsUpdatesText',
subs=[
('${PATH}', results.incorrect_api_modules[0]),
('${API}', str(CURRENT_API_VERSION)),
('${API}', str(_babase.app.env.api_version)),
],
)
_babase.screenmessage(msg, color=(1, 0, 0))
@ -344,13 +340,16 @@ class DirectoryScan:
# If we find a module requiring a different api version, warn
# and ignore.
if required_api is not None and required_api != CURRENT_API_VERSION:
if (
required_api is not None
and required_api != _babase.app.env.api_version
):
logging.warning(
'metascan: %s requires api %s but we are running'
' %s. Ignoring module.',
subpath,
required_api,
CURRENT_API_VERSION,
_babase.app.env.api_version,
)
self.results.incorrect_api_modules.append(
self._module_name_for_subpath(subpath)

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

View File

@ -4,14 +4,12 @@
from __future__ import annotations
import copy
import logging
import weakref
import threading
from enum import Enum
from typing import TYPE_CHECKING
import babase
from babase import DEFAULT_REQUEST_TIMEOUT_SECONDS
import bascenev1
if TYPE_CHECKING:
@ -72,6 +70,7 @@ class MasterServerV1CallThread(threading.Thread):
def run(self) -> None:
# pylint: disable=consider-using-with
# pylint: disable=too-many-branches
import urllib.request
import urllib.parse
import urllib.error
@ -79,20 +78,15 @@ class MasterServerV1CallThread(threading.Thread):
from efro.error import is_urllib_communication_error
# If the app is going down, this is a no-op. Trying to avoid the
# rare odd crash I see from (presumably) SSL stuff getting used
# while the app is being torn down.
if babase.app.state is babase.app.State.SHUTTING_DOWN:
logging.warning(
'MasterServerV1CallThread.run() during app'
' shutdown is a no-op.'
)
return
plus = babase.app.plus
assert plus is not None
response_data: Any = None
url: str | None = None
# Tearing the app down while this is running can lead to
# rare crashes in LibSSL, so avoid that if at all possible.
babase.shutdown_suppress_begin()
try:
classic = babase.app.classic
assert classic is not None
@ -114,7 +108,7 @@ class MasterServerV1CallThread(threading.Thread):
{'User-Agent': classic.legacy_user_agent_string},
),
context=babase.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
elif self._request_type == 'post':
url = plus.get_master_server_address() + '/' + self._request
@ -126,7 +120,7 @@ class MasterServerV1CallThread(threading.Thread):
{'User-Agent': classic.legacy_user_agent_string},
),
context=babase.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
else:
raise TypeError('Invalid request_type: ' + self._request_type)
@ -160,6 +154,9 @@ class MasterServerV1CallThread(threading.Thread):
response_data = None
finally:
babase.shutdown_suppress_end()
if self._callback is not None:
babase.pushcall(
babase.Call(self._run_callback, response_data),

View File

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

View File

@ -153,6 +153,7 @@ class ClassicSubsystem(babase.AppSubsystem):
plus = babase.app.plus
assert plus is not None
env = babase.app.env
cfg = babase.app.config
self.music.on_app_loading()
@ -161,11 +162,7 @@ class ClassicSubsystem(babase.AppSubsystem):
# Non-test, non-debug builds should generally be blessed; warn if not.
# (so I don't accidentally release a build that can't play tourneys)
if (
not babase.app.debug_build
and not babase.app.test_build
and not plus.is_blessed()
):
if not env.debug and not env.test and not plus.is_blessed():
babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
# FIXME: This should not be hard-coded.

View File

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

View File

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

View File

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

View File

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

View File

@ -185,7 +185,7 @@ def show_damage_count(
# (connected clients may have differing configs so they won't
# get the intended results).
assert app.classic is not None
do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.vr_mode
do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.env.vr
txtnode = _bascenev1.newnode(
'text',
attrs={

View File

@ -49,7 +49,7 @@ class JoinInfo:
if keyboard is not None:
self._update_for_keyboard(keyboard)
flatness = 1.0 if babase.app.vr_mode else 0.0
flatness = 1.0 if babase.app.env.vr else 0.0
self._text = NodeActor(
_bascenev1.newnode(
'text',

View File

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

View File

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

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)
self._run_text_pos_top = (position[0], position[1] - 135.0 * scale)
self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale)
sval = 1.0 * scale if bs.app.vr_mode else 0.8 * scale
sval = 1.0 * scale if bs.app.env.vr else 0.8 * scale
self._run_text = bs.newnode(
'text',
attrs={
'scale': sval,
'host_only': True,
'shadow': 1.0 if bs.app.vr_mode else 0.5,
'shadow': 1.0 if bs.app.env.vr else 0.5,
'flatness': 1.0,
'maxwidth': 380,
'v_align': 'top',

View File

@ -45,7 +45,7 @@ class _Entry:
# FIXME: Should not do things conditionally for vr-mode, as there may
# be non-vr clients connected which will also get these value.
vrmode = bs.app.vr_mode
vrmode = bs.app.env.vr
if self._do_cover:
if vrmode:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -197,13 +197,19 @@ class QuitWindow:
time=0.2,
endcall=lambda: bui.quit(soft=True, back=self._back),
)
# Prevent the user from doing anything else while we're on our
# way out.
bui.lock_all_input()
# Unlock and fade back in shortly. Just in case something goes
# wrong (or on Android where quit just backs out of our activity
# and we may come back after).
def _come_back() -> None:
bui.unlock_all_input()
bui.fade_screen(True, time=0.1)
# On systems supporting soft-quit, unlock and fade back in shortly
# (soft-quit basically just backgrounds/hides the app).
if bui.app.env.supports_soft_quit:
# Unlock and fade back in shortly. Just in case something goes
# wrong (or on Android where quit just backs out of our activity
# and we may come back after).
def _come_back() -> None:
bui.unlock_all_input()
bui.fade_screen(True)
bui.apptimer(0.3, _come_back)
bui.apptimer(0.5, _come_back)

View File

@ -212,7 +212,10 @@ class CreditsListWindow(bui.Window):
try:
with open(
os.path.join(
bui.app.data_directory, 'ba_data', 'data', 'langdata.json'
bui.app.env.data_directory,
'ba_data',
'data',
'langdata.json',
),
encoding='utf-8',
) as infile:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -117,7 +117,7 @@ class MainMenuWindow(bui.Window):
force_test = False
bs.get_local_active_input_devices_count()
if (
(app.on_tv or app.classic.platform == 'mac')
(app.env.tv or app.classic.platform == 'mac')
and bui.app.config.get('launchCount', 0) <= 1
) or force_test:

View File

@ -34,7 +34,7 @@ class PopupWindow:
focus_size = size
# In vr mode we can't have windows going outside the screen.
if bui.app.vr_mode:
if bui.app.env.vr:
focus_size = size
focus_position = (0, 0)

View File

@ -155,7 +155,7 @@ class ProfileUpgradeWindow(bui.Window):
bui.app.classic.master_server_v1_get(
'bsGlobalProfileCheck',
{'name': self._name, 'b': bui.app.build_number},
{'name': self._name, 'b': bui.app.env.build_number},
callback=bui.WeakCall(self._profile_check_result),
)
self._cost = plus.get_v1_account_misc_read_val(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -715,5 +715,10 @@ void BaseFeatureSet::DoPushObjCall(const PythonObjectSetBase* objset, int id,
}
auto BaseFeatureSet::IsAppStarted() const -> bool { return app_started_; }
void BaseFeatureSet::ShutdownSuppressBegin() { shutdown_suppress_count_++; }
void BaseFeatureSet::ShutdownSuppressEnd() {
shutdown_suppress_count_--;
assert(shutdown_suppress_count_ >= 0);
}
} // namespace ballistica::base

View File

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

View File

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

View File

@ -54,9 +54,11 @@ class Graphics {
void OnAppPause();
void OnAppResume();
void OnAppShutdown();
void OnScreenSizeChange();
void DoApplyAppConfig();
void OnScreenSizeChange(float virtual_width, float virtual_height,
float physical_width, float physical_height);
void SetScreenSize(float virtual_width, float virtual_height,
float physical_width, float physical_height);
void StepDisplayTime();
static auto IsShaderTransparent(ShadingType c) -> bool;

View File

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

View File

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

View File

@ -314,4 +314,11 @@ void BasePlatform::GetCursorPosition(float* x, float* y) {
void BasePlatform::OnMainThreadStartAppComplete() {}
void BasePlatform::OnAppStart() { assert(g_base->InLogicThread()); }
void BasePlatform::OnAppPause() { assert(g_base->InLogicThread()); }
void BasePlatform::OnAppResume() { assert(g_base->InLogicThread()); }
void BasePlatform::OnAppShutdown() { assert(g_base->InLogicThread()); }
void BasePlatform::OnScreenSizeChange() { assert(g_base->InLogicThread()); }
void BasePlatform::DoApplyAppConfig() { assert(g_base->InLogicThread()); }
} // namespace ballistica::base

View File

@ -26,6 +26,13 @@ class BasePlatform {
/// start talking to them.
virtual void OnMainThreadStartAppComplete();
virtual void OnAppStart();
virtual void OnAppPause();
virtual void OnAppResume();
virtual void OnAppShutdown();
virtual void OnScreenSizeChange();
virtual void DoApplyAppConfig();
#pragma mark IN APP PURCHASES --------------------------------------------------
void Purchase(const std::string& item);

View File

@ -7,6 +7,7 @@
#include "ballistica/base/python/class/python_class_context_call.h"
#include "ballistica/base/python/class/python_class_context_ref.h"
#include "ballistica/base/python/class/python_class_display_timer.h"
#include "ballistica/base/python/class/python_class_env.h"
#include "ballistica/base/python/class/python_class_feature_set_data.h"
#include "ballistica/base/python/class/python_class_simple_sound.h"
#include "ballistica/base/python/class/python_class_vec3.h"
@ -45,6 +46,7 @@ void BasePython::AddPythonClasses(PyObject* module) {
PythonModuleBuilder::AddClass<PythonClassContextRef>(module);
PythonModuleBuilder::AddClass<PythonClassAppTimer>(module);
PythonModuleBuilder::AddClass<PythonClassDisplayTimer>(module);
PythonModuleBuilder::AddClass<PythonClassEnv>(module);
PythonModuleBuilder::AddClass<PythonClassSimpleSound>(module);
PythonModuleBuilder::AddClass<PythonClassContextCall>(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"
"Tells whether the OS is currently playing music of some sort.\n"
"\n"
"(Used to determine whether the game should avoid playing its own)",
"(Used to determine whether the app should avoid playing its own)",
};
// -------------------------------- exec_arg -----------------------------------
@ -1491,6 +1491,67 @@ static PyMethodDef PyGetImmediateReturnCodeDef = {
"(internal)\n",
};
// ----------------------- shutdown_suppress_begin -----------------------------
static auto PyShutdownSuppressBegin(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
assert(g_base);
g_base->ShutdownSuppressBegin();
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
static PyMethodDef PyShutdownSuppressBeginDef = {
"shutdown_suppress_begin", // name
(PyCFunction)PyShutdownSuppressBegin, // method
METH_NOARGS, // flags
"shutdown_suppress_begin() -> None\n"
"\n"
"(internal)\n",
};
// ------------------------ shutdown_suppress_end ------------------------------
static auto PyShutdownSuppressEnd(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
assert(g_base);
g_base->ShutdownSuppressEnd();
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
static PyMethodDef PyShutdownSuppressEndDef = {
"shutdown_suppress_end", // name
(PyCFunction)PyShutdownSuppressEnd, // method
METH_NOARGS, // flags
"shutdown_suppress_end() -> None\n"
"\n"
"(internal)\n",
};
// ------------------------ shutdown_suppress_count
// ------------------------------
static auto PyShutdownSuppressCount(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
assert(g_base);
return PyLong_FromLong(g_base->shutdown_suppress_count());
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
static PyMethodDef PyShutdownSuppressCountDef = {
"shutdown_suppress_count", // name
(PyCFunction)PyShutdownSuppressCount, // method
METH_NOARGS, // flags
"shutdown_suppress_count() -> int\n"
"\n"
"(internal)\n",
};
// -----------------------------------------------------------------------------
auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
@ -1540,6 +1601,9 @@ auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
PyEmptyAppModeHandleIntentExecDef,
PyGetImmediateReturnCodeDef,
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
// might need core_config access at any time.
if (!g_buildconfig.monolithic_build()) {
if (!HaveBaEnvVals()) {
if (!have_ba_env_vals()) {
static bool did_warn = false;
if (!did_warn) {
did_warn = true;

View File

@ -115,7 +115,7 @@ class CoreFeatureSet {
/// Return true if baenv values have been locked in: python paths, log
/// handling, etc. Early-running code may wish to explicitly avoid making log
/// calls until this condition is met to ensure predictable behavior.
auto HaveBaEnvVals() const { return have_ba_env_vals_; }
auto have_ba_env_vals() const { return have_ba_env_vals_; }
/// Return the directory where the app expects to find its bundled Python
/// files.

View File

@ -396,7 +396,7 @@ auto CorePython::FetchPythonArgs(std::vector<std::string>* buffer)
// argv pointers to it.
std::vector<char*> out;
out.reserve(buffer->size());
for (int i = 0; i < buffer->size(); ++i) {
for (size_t i = 0; i < buffer->size(); ++i) {
out.push_back(const_cast<char*>((*buffer)[i].c_str()));
}
return out;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,6 +35,7 @@ OPENSSL_VER_APPLE = '3.0.8'
OPENSSL_VER_ANDROID = '3.0.8'
ZLIB_VER_ANDROID = '1.3'
XZ_VER_ANDROID = '5.4.4'
# Filenames we prune from Python lib dirs in source repo to cut down on size.
PRUNE_LIB_NAMES = [
@ -301,6 +302,14 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None:
count=1,
)
# Set specific XZ version.
ftxt = replace_exact(
ftxt,
"source = 'https://tukaani.org/xz/xz-5.2.7.tar.xz'",
f"source = 'https://tukaani.org/xz/xz-{XZ_VER_ANDROID}.tar.xz'",
count=1,
)
# Give ourselves a handle to patch the OpenSSL build.
ftxt = replace_exact(
ftxt,