diff --git a/.efrocachemap b/.efrocachemap
index 0628b6b6..415f07a1 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -4072,26 +4072,26 @@
"build/assets/workspace/ninjafightplug.py": "https://files.ballistica.net/cache/ba1/c5/09/4f10b8a21ba87aa5509cff7a164b",
"build/assets/workspace/onslaughtplug.py": "https://files.ballistica.net/cache/ba1/ff/0a/a354984f9c074dab0676ac7e4877",
"build/assets/workspace/runaroundplug.py": "https://files.ballistica.net/cache/ba1/2a/1c/9ee5db6d1bceca7fa6638fb8abde",
- "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/7b/fc/40202144638c394e88af0424ac38",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/bd/fb/9a2813b5a23b727b1b246d5b1a31",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/6f/99/9e5c25105d2bc8fd1db0402cdb0f",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/8b/4e/f6496d2ccd7a869e4e9d2eb57ee4",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/f6/ec/dab1171b1c99587a80e959dddaa5",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/2f/23/0d5dd4dd3dd190a3f81eca6d11f0",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/f3/ec/b238f5118def4ecac3ba4363f389",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/b2/d2/307b223ce8182e3c14a982dad7d4",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/08/67/672c0b7edb83bd06f3f08d25c1d1",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/5e/34/7189db32e52d599755bf59587671",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/ce/44/dec692802d8023cd48f5f29fa40d",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/8d/52/084e0817f587f035017bc2595a8d",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/46/33/1975fe658fc9ffc96b95c0071ecd",
- "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/66/ea/4e901a9ad2ad965bdc773ee3b127",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/2f/54/565f3150df759bffea9dcad21685",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/52/a5/594f6db4b4f0d8d9937bb67d80f6",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/0f/86/5228cc1d4a8d2317585028e115f3",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/73/b0/83918095162a68c12bffdb7a4d97",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/12/55/5d5b5a467a1c2ec10c2551465682",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/48/95/a35e7407f5f813cea477dd75130f",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/08/e3/a0b9223475c7706681fad7968ba7",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/6d/ef/b6d1a6b9754d3036f71ad3fa77e2",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/2b/b1/6f30bc9be42939f9399d4a3f2b3a",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/0e/ef/1ce03a67174a21bb153ea7e8e27b",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/d0/26/46d5ff486b95aedaa3caf3c299c9",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/10/ea/b9aa9951192cbc45cccb4c3c2289",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/e0/a0/32c52b4e83da434e22544bdda893",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/0b/09/2986300ce8c70c4e301da76a0533",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/b1/79/ba61f157a98f4ceab818d99edab9",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/09/78/0a5975081c617f7a52c6ddd3d7c0",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/36/91/561ed25a53dd05a321418ef95527",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/8d/27/ee747a3e85a28e124c21ac2b380d",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/a7/b8/ad830a02ee58f2e87466f011fb27",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/ca/71/6fd4afebc57e78d5986a2f657a3f",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/56/16/efa353be8adca6c233db12102463",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/85/98/0aa74afa4bfa7f12b4bb54fe76a3",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/e6/bb/308f33d3c4b549b8b701a6e94db1",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/68/c5/ad76162d865a7a14f2f2677300d1",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/c6/39/21bdc2e4e59855558bbc7d606f28",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/f9/eb/e2be79806312a8615a1e6da93078",
"build/prefab/lib/linux_arm64_gui/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/f4/e7/32cfbd534ac61e626f6c6fbe2b01",
"build/prefab/lib/linux_arm64_gui/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/f3/f8/52577356f2ff5229ed4e5a6764cf",
"build/prefab/lib/linux_arm64_server/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/60/2b/35acb337afc2d997ffc94d2da757",
@@ -4108,14 +4108,14 @@
"build/prefab/lib/mac_x86_64_gui/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/30/4a/aa281e0eb46722098ec29d7da4f8",
"build/prefab/lib/mac_x86_64_server/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/0b/e4/a9d278c1bc9a5d731f865ac91a0b",
"build/prefab/lib/mac_x86_64_server/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/92/f7/8898478ab4ef0a342c727dd64195",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/df/9f/960a5370e02cae685ae3452066f9",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/7d/f7/ba5e3f57b5c6fe96542f61ae21f1",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/34/b3/83428e86252522a10d0c097fc3eb",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/1d/31/12a6e62fc12477b4ed1eb3735b08",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/65/ca/8bd967cebd1836e03117c2b10f5d",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/8c/41/4689d7ca391e92fed73b81e5064c",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/7b/a9/f53d5f009e6cc89a265c076bbb51",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/d1/ed/acae7564f9d534d7f78a3a9373ee",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/14/3f/8bffa39ee2e862bfd61886f8dd20",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/de/69/ff2d73feab522f0be693900c534b",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/2a/ac/8e4a2b322079d57a65cf5ec686eb",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/9b/d8/f5dbc2b76e5d6992e6e0f957699d",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "https://files.ballistica.net/cache/ba1/68/f9/0e55e33d9782d596d65770e943d8",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "https://files.ballistica.net/cache/ba1/b4/49/32ebdaa15a795c84e90c3d16fdee",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "https://files.ballistica.net/cache/ba1/e9/3c/b86de4797400df2a7bbba4e957f7",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "https://files.ballistica.net/cache/ba1/93/88/176702f3743aa143dcd2ee651c4a",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "https://files.ballistica.net/cache/ba1/52/c6/c11130af7b10d6c0321add5518fa",
"src/assets/ba_data/python/babase/_mgen/enums.py": "https://files.ballistica.net/cache/ba1/38/c3/1dedd5e74f2508efc5974c8815a1",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "https://files.ballistica.net/cache/ba1/b4/3d/e352190a0e5673d101c0f3ee3ad2",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3590b6db..bcbbf597 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-### 1.7.20 (build 21022, api 8, 2023-05-24)
+### 1.7.20 (build 21023, api 8, 2023-05-26)
- This seems like a good time for a `refactoring` release in anticipation of
changes coming in 1.8. Basically this means that a lot of things will be
diff --git a/ballisticakit-cmake/CMakeLists.txt b/ballisticakit-cmake/CMakeLists.txt
index 0fd026b4..99580ad9 100644
--- a/ballisticakit-cmake/CMakeLists.txt
+++ b/ballisticakit-cmake/CMakeLists.txt
@@ -408,6 +408,7 @@ add_executable(ballisticakit
${BA_SRC_ROOT}/ballistica/base/python/support/python_context_call.h
${BA_SRC_ROOT}/ballistica/base/python/support/python_context_call_runnable.h
${BA_SRC_ROOT}/ballistica/base/support/app_timer.h
+ ${BA_SRC_ROOT}/ballistica/base/support/classic_soft.h
${BA_SRC_ROOT}/ballistica/base/support/context.cc
${BA_SRC_ROOT}/ballistica/base/support/context.h
${BA_SRC_ROOT}/ballistica/base/support/huffman.cc
diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
index 09735c75..0f78b098 100644
--- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
+++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
@@ -399,6 +399,7 @@
+
diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
index 25b38c56..a2e3a7f1 100644
--- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
+++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
@@ -631,6 +631,9 @@
ballistica\base\support
+
+ ballistica\base\support
+
ballistica\base\support
diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
index 318f42a0..2aa7f63c 100644
--- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
+++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
@@ -394,6 +394,7 @@
+
diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
index 25b38c56..a2e3a7f1 100644
--- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
+++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
@@ -631,6 +631,9 @@
ballistica\base\support
+
+ ballistica\base\support
+
ballistica\base\support
diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json
index f630e343..8a787e24 100644
--- a/src/assets/.asset_manifest_public.json
+++ b/src/assets/.asset_manifest_public.json
@@ -6,6 +6,9 @@
"ba_data/python/babase/__pycache__/_app.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_appcomponent.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_appconfig.cpython-311.opt-1.pyc",
+ "ba_data/python/babase/__pycache__/_appintent.cpython-311.opt-1.pyc",
+ "ba_data/python/babase/__pycache__/_appmode.cpython-311.opt-1.pyc",
+ "ba_data/python/babase/__pycache__/_appmodeselector.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_apputils.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_assetmanager.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_asyncio.cpython-311.opt-1.pyc",
@@ -27,6 +30,9 @@
"ba_data/python/babase/_app.py",
"ba_data/python/babase/_appcomponent.py",
"ba_data/python/babase/_appconfig.py",
+ "ba_data/python/babase/_appintent.py",
+ "ba_data/python/babase/_appmode.py",
+ "ba_data/python/babase/_appmodeselector.py",
"ba_data/python/babase/_apputils.py",
"ba_data/python/babase/_assetmanager.py",
"ba_data/python/babase/_asyncio.py",
@@ -122,6 +128,7 @@
"ba_data/python/bascenev1/__pycache__/_activity.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_activitytypes.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_actor.cpython-311.opt-1.pyc",
+ "ba_data/python/bascenev1/__pycache__/_appmode.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_collision.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_coopgame.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_coopsession.cpython-311.opt-1.pyc",
@@ -152,6 +159,7 @@
"ba_data/python/bascenev1/_activity.py",
"ba_data/python/bascenev1/_activitytypes.py",
"ba_data/python/bascenev1/_actor.py",
+ "ba_data/python/bascenev1/_appmode.py",
"ba_data/python/bascenev1/_collision.py",
"ba_data/python/bascenev1/_coopgame.py",
"ba_data/python/bascenev1/_coopsession.py",
diff --git a/src/assets/Makefile b/src/assets/Makefile
index 09dcc176..2c526191 100644
--- a/src/assets/Makefile
+++ b/src/assets/Makefile
@@ -139,6 +139,9 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/babase/_app.py \
$(BUILD_DIR)/ba_data/python/babase/_appcomponent.py \
$(BUILD_DIR)/ba_data/python/babase/_appconfig.py \
+ $(BUILD_DIR)/ba_data/python/babase/_appintent.py \
+ $(BUILD_DIR)/ba_data/python/babase/_appmode.py \
+ $(BUILD_DIR)/ba_data/python/babase/_appmodeselector.py \
$(BUILD_DIR)/ba_data/python/babase/_apputils.py \
$(BUILD_DIR)/ba_data/python/babase/_assetmanager.py \
$(BUILD_DIR)/ba_data/python/babase/_asyncio.py \
@@ -188,6 +191,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bascenev1/_activity.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_activitytypes.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_actor.py \
+ $(BUILD_DIR)/ba_data/python/bascenev1/_appmode.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_collision.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_coopgame.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_coopsession.py \
@@ -405,6 +409,9 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_app.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_appcomponent.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_appconfig.cpython-311.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/babase/__pycache__/_appintent.cpython-311.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/babase/__pycache__/_appmode.cpython-311.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/babase/__pycache__/_appmodeselector.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_apputils.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_assetmanager.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_asyncio.cpython-311.opt-1.pyc \
@@ -454,6 +461,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_activity.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_activitytypes.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_actor.cpython-311.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_appmode.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_collision.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_coopgame.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_coopsession.cpython-311.opt-1.pyc \
diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py
index fe5229a0..58de3015 100644
--- a/src/assets/ba_data/python/babase/__init__.py
+++ b/src/assets/ba_data/python/babase/__init__.py
@@ -33,6 +33,8 @@ from _babase import (
in_logic_thread,
)
+from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec
+from babase._appmode import AppMode
from babase._accountv2 import AccountV2Handle
from babase._plugin import PotentialPlugin, Plugin, PluginSubsystem
from babase._app import App
@@ -155,6 +157,10 @@ __all__ = [
'displaytimer',
'displaytime',
'DisplayTimer',
+ 'AppIntent',
+ 'AppIntentDefault',
+ 'AppIntentExec',
+ 'AppMode',
]
diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py
index cbd749b1..652ffeed 100644
--- a/src/assets/ba_data/python/babase/_app.py
+++ b/src/assets/ba_data/python/babase/_app.py
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
from concurrent.futures import ThreadPoolExecutor
from functools import cached_property
+from efro.call import tpartial
import _babase
from babase._language import LanguageSubsystem
from babase._plugin import PluginSubsystem
@@ -16,22 +17,28 @@ from babase._meta import MetadataSubsystem
from babase._net import NetworkSubsystem
from babase._workspace import WorkspaceSubsystem
from babase._appcomponent import AppComponentSubsystem
+from babase._appmodeselector import AppModeSelector
+from babase._appintent import AppIntentDefault, AppIntentExec
if TYPE_CHECKING:
- from typing import Any
import asyncio
+ from typing import Any, Callable
+ from concurrent.futures import Future
from efro.log import LogHandler
import babase
from babase._cloud import CloudSubsystem
from babase._accountv2 import AccountV2Subsystem
from babase._apputils import AppHealthMonitor
+ from babase._appintent import AppIntent
+ from babase._appmode import AppMode
+
+ # WOULD-AUTOGEN-BEGIN
- # Would autogen this begin
from baclassic import ClassicSubsystem
from baplus import PlusSubsystem
- # Would autogen this end
+ # WOULD-AUTOGEN-END
class App:
@@ -275,7 +282,15 @@ class App:
self.lang = LanguageSubsystem()
self.net = NetworkSubsystem()
self.workspaces = WorkspaceSubsystem()
- # self._classic: ClassicSubsystem | None = None
+ self._pending_intent: AppIntent | None = None
+ self._intent: AppIntent | None = None
+ self._mode: AppMode | None = None
+
+ # 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._asyncio_timer: babase.AppTimer | None = None
@@ -298,11 +313,28 @@ class App:
# if classic_subsystem_type is not None:
# self._classic = classic_subsystem_type()
- # Would autogen this begin
+ def _threadpool_no_wait_done(self, fut: Future) -> None:
+ try:
+ fut.result()
+ except Exception:
+ logging.exception(
+ 'Error in work submitted via threadpool_submit_no_wait()'
+ )
+
+ def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
+ """Submit work to our threadpool and log any errors.
+
+ Use this when you want to run something asynchronously but don't
+ intend to call result() on it to handle errors/etc.
+ """
+ fut = self.threadpool.submit(call)
+ fut.add_done_callback(self._threadpool_no_wait_done)
+
+ # WOULD-AUTOGEN-BEGIN
@cached_property
def classic(self) -> ClassicSubsystem | None:
- """Our classic subsystem."""
+ """Our classic subsystem (if available)."""
try:
from baclassic import ClassicSubsystem
@@ -316,7 +348,7 @@ class App:
@cached_property
def plus(self) -> PlusSubsystem | None:
- """Our plus subsystem."""
+ """Our plus subsystem (if available)."""
try:
from baplus import PlusSubsystem
@@ -328,7 +360,96 @@ class App:
logging.exception('Error importing baplus')
return None
- # Would autogen this begin
+ # WOULD-AUTOGEN-END
+
+ def set_intent(self, intent: AppIntent) -> None:
+ """Set the intent for the app.
+
+ Intent defines what the app is trying to do at a given time.
+ This call is asynchronous; the intent switch will happen in the
+ logic thread in the near future. If set_intent is
+ called repeatedly before the change takes place, the last intent
+ set will be used.
+ """
+
+ # Mark this one as pending. We do this synchronously so that the
+ # last one marked actually takes effect if there is overlap
+ # (doing this in the bg thread could result in race conditions).
+ self._pending_intent = intent
+
+ # Do the actual work of calcing our app-mode/etc. in a bg thread
+ # since it may block for a moment to load modules/etc.
+ self.threadpool_submit_no_wait(tpartial(self._set_intent, intent))
+
+ def _set_intent(self, intent: AppIntent) -> None:
+ # This should be running in a bg thread.
+ assert not _babase.in_logic_thread()
+ try:
+ # Ask the selector what app-mode to use for this intent.
+ if self.mode_selector is None:
+ raise RuntimeError('No AppModeSelector set.')
+ modetype = self.mode_selector.app_mode_for_intent(intent)
+
+ # Make sure the app-mode they return *actually* supports the
+ # intent.
+ if not modetype.supports_intent(intent):
+ raise RuntimeError(
+ f'Intent {intent} is not supported by AppMode class'
+ f' {modetype}'
+ )
+
+ # Kick back to the logic thread to apply.
+ mode = modetype()
+ _babase.pushcall(
+ tpartial(self._apply_intent, intent, mode),
+ from_other_thread=True,
+ )
+ except Exception:
+ logging.exception('Error setting app intent to %s.', intent)
+ _babase.pushcall(
+ tpartial(self._apply_intent_error, intent),
+ from_other_thread=True,
+ )
+
+ def _apply_intent(self, intent: AppIntent, mode: AppMode) -> None:
+ assert _babase.in_logic_thread()
+
+ # ONLY apply this intent if it is still the most recent one
+ # submitted.
+ if intent is not self._pending_intent:
+ return
+
+ # If the app-mode for this intent is different than the active
+ # one, switch.
+ # pylint: disable=unidiomatic-typecheck
+ if type(mode) is not type(self._mode):
+ if self._mode is not None:
+ try:
+ self._mode.on_deactivate()
+ except Exception:
+ logging.exception(
+ 'Error deactivating app-mode %s.', self._mode
+ )
+ self._mode = mode
+ try:
+ mode.on_activate()
+ except Exception:
+ # Hmm; what should we do in this case?...
+ logging.exception('Error activating app-mode %s.', mode)
+
+ try:
+ mode.handle_intent(intent)
+ except Exception:
+ logging.exception(
+ 'Error handling intent %s in app-mode %s.', intent, mode
+ )
+
+ def _apply_intent_error(self, intent: AppIntent) -> None:
+ from babase._language import Lstr
+
+ del intent # Unused.
+ _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
+ _babase.getsimplesound('error').play()
def run(self) -> None:
"""Run the app to completion.
@@ -381,12 +502,40 @@ class App:
def on_app_loading(self) -> None:
"""Called when initially entering the loading state."""
+ assert _babase.in_logic_thread()
+
+ class DefaultAppModeSelector(AppModeSelector):
+ """Decides which app modes to use to handle intents."""
+
+ def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]:
+ # WOULD-AUTOGEN-BEGIN
+
+ import bascenev1
+
+ return bascenev1.SceneV1AppMode
+
+ # WOULD-AUTOGEN-END
def on_app_running(self) -> None:
"""Called when initially entering the running state."""
+ assert _babase.in_logic_thread()
+
+ # Set a default app-mode-selector. Plugins can then override
+ # this if they want.
+ self.mode_selector = self.DefaultAppModeSelector()
self.plugins.on_app_running()
+ # If 'exec' code was provided to the app, always kick that off
+ # here.
+ exec_cmd = _babase.exec_arg()
+ if exec_cmd is not None:
+ self.set_intent(AppIntentExec(exec_cmd))
+ elif self._pending_intent is None:
+ # Otherwise tell the app to do its default thing *only* if a
+ # plugin hasn't already told it to do something.
+ self.set_intent(AppIntentDefault())
+
def on_app_bootstrapping_complete(self) -> None:
"""Called by the C++ layer once its ready to rock."""
assert _babase.in_logic_thread()
diff --git a/src/assets/ba_data/python/babase/_appcomponent.py b/src/assets/ba_data/python/babase/_appcomponent.py
index bbd4cc73..d1c844fc 100644
--- a/src/assets/ba_data/python/babase/_appcomponent.py
+++ b/src/assets/ba_data/python/babase/_appcomponent.py
@@ -19,19 +19,21 @@ class AppComponentSubsystem:
Category: **App Classes**
This subsystem acts as a registry for classes providing particular
- functionality for the app, and allows plugins or other custom code to
- easily override said functionality.
+ functionality for the app, and allows plugins or other custom code
+ to easily override said functionality.
- Use babase.app.components to get the single shared instance of this class.
+ Access the single shared instance of this class at
+ babase.app.components.
- The general idea with this setup is that a base-class is defined to
- provide some functionality and then anyone wanting that functionality
- uses the getclass() method with that base class to return the current
- registered implementation. The user should not know or care whether
- they are getting the base class itself or some other implementation.
+ The general idea with this setup is that a base-class Foo is defined
+ to provide some functionality and then anyone wanting that
+ functionality calls getclass(Foo) to return the current registered
+ implementation. The user should not know or care whether they are
+ getting Foo itself or some subclass of it.
Change-callbacks can also be requested for base classes which will
- fire in a deferred manner when particular base-classes are overridden.
+ fire in a deferred manner when particular base-classes are
+ overridden.
"""
def __init__(self) -> None:
@@ -45,8 +47,9 @@ class AppComponentSubsystem:
The provided implementation class must be a subclass of baseclass.
"""
- # Currently limiting this to logic-thread use; can revisit if needed
- # (would need to guard access to our implementations dict).
+ # Currently limiting this to logic-thread use; can revisit if
+ # needed (would need to guard access to our implementations
+ # dict).
assert _babase.in_logic_thread()
if not issubclass(implementation, baseclass):
@@ -58,16 +61,17 @@ class AppComponentSubsystem:
self._implementations[baseclass] = implementation
# If we're the first thing getting dirtied, set up a callback to
- # clean everything. And add ourself to the dirty list regardless.
+ # clean everything. And add ourself to the dirty list
+ # regardless.
if not self._dirty_base_classes:
_babase.pushcall(self._run_change_callbacks)
self._dirty_base_classes.add(baseclass)
def getclass(self, baseclass: T) -> T:
- """Given a base-class, return the currently set implementation class.
+ """Given a base-class, return the current implementation class.
- If no custom implementation has been set, the provided base-class
- is returned.
+ If no custom implementation has been set, the provided
+ base-class is returned.
"""
assert _babase.in_logic_thread()
@@ -77,7 +81,7 @@ class AppComponentSubsystem:
def register_change_callback(
self, baseclass: T, callback: Callable[[T], None]
) -> None:
- """Register a callback to fire when a class implementation changes.
+ """Register a callback to fire on class implementation changes.
The callback will be scheduled to run in the logic thread event
loop. Note that any further setclass calls before the callback
diff --git a/src/assets/ba_data/python/babase/_appintent.py b/src/assets/ba_data/python/babase/_appintent.py
new file mode 100644
index 00000000..76399af3
--- /dev/null
+++ b/src/assets/ba_data/python/babase/_appintent.py
@@ -0,0 +1,27 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Provides AppIntent functionality."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ pass
+
+
+class AppIntent:
+ """A high level directive given to the app.
+
+ Category: **App Classes**
+ """
+
+
+class AppIntentDefault(AppIntent):
+ """Tells the app to simply run in its default mode."""
+
+
+class AppIntentExec(AppIntent):
+ """Tells the app to exec some Python code."""
+
+ def __init__(self, code: str):
+ self.code = code
diff --git a/src/assets/ba_data/python/babase/_appmode.py b/src/assets/ba_data/python/babase/_appmode.py
new file mode 100644
index 00000000..fd53e126
--- /dev/null
+++ b/src/assets/ba_data/python/babase/_appmode.py
@@ -0,0 +1,35 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Provides AppMode functionality."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from babase._appintent import AppIntent
+
+
+class AppMode:
+ """A high level mode for the app.
+
+ Category: **App Classes**
+
+ """
+
+ @classmethod
+ def supports_intent(cls, intent: AppIntent) -> bool:
+ """Return whether our mode can handle the provided intent."""
+ del intent
+
+ # Say no to everything by default. Let's make mode explicitly
+ # lay out everything they *do* support.
+ return False
+
+ def handle_intent(self, intent: AppIntent) -> None:
+ """Handle an intent."""
+
+ def on_activate(self) -> None:
+ """Called when the mode is being activated."""
+
+ def on_deactivate(self) -> None:
+ """Called when the mode is being deactivated."""
diff --git a/src/assets/ba_data/python/babase/_appmodeselector.py b/src/assets/ba_data/python/babase/_appmodeselector.py
new file mode 100644
index 00000000..3ccefe1d
--- /dev/null
+++ b/src/assets/ba_data/python/babase/_appmodeselector.py
@@ -0,0 +1,34 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Provides AppMode functionality."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from babase._appintent import AppIntent
+ from babase._appmode import AppMode
+
+
+class AppModeSelector:
+ """Defines which AppModes to use to handle given AppIntents.
+
+ Category: **App Classes**
+
+ The app calls an instance of this class when passed an AppIntent to
+ determine which AppMode to use to handle the intent. Plugins or
+ spinoff projects can modify high level app behavior by replacing or
+ modifying this.
+ """
+
+ # pylint: disable=useless-return
+
+ def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]:
+ """Given an AppIntent, return the AppMode that should handle it.
+
+ If None is returned, the AppIntent will be ignored.
+
+ This is called in a background thread, so avoid any calls
+ limited to logic thread use/etc.
+ """
+ raise RuntimeError('app_mode_for_intent() should be overridden.')
diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py
index e8075868..4f42aeb7 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -31,7 +31,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 21022
+TARGET_BALLISTICA_BUILD = 21023
TARGET_BALLISTICA_VERSION = '1.7.20'
_g_env_config: EnvConfig | None = None
diff --git a/src/assets/ba_data/python/bascenev1/__init__.py b/src/assets/ba_data/python/bascenev1/__init__.py
index fb01b078..63e212fc 100644
--- a/src/assets/ba_data/python/bascenev1/__init__.py
+++ b/src/assets/ba_data/python/bascenev1/__init__.py
@@ -36,6 +36,8 @@ from _babase import (
displaytimer,
DisplayTimer,
)
+from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec
+from babase._appmode import AppMode
from babase._error import NotFoundError, NodeNotFoundError, ContextError
from babase._language import Lstr
from babase._general import (
@@ -55,7 +57,6 @@ from babase._mgen.enums import (
InputType,
)
-
from _bascenev1 import (
get_foreground_host_session,
get_foreground_host_activity,
@@ -135,6 +136,7 @@ from _bascenev1 import (
)
+from bascenev1._appmode import SceneV1AppMode
from bascenev1._session import Session
from bascenev1._map import Map
from bascenev1._coopsession import CoopSession
@@ -200,11 +202,6 @@ from bascenev1._dependency import (
AssetPackage,
)
-# if TYPE_CHECKING:
-# from babase._app import App
-
-# app: App
-
__all__ = [
'app',
'get_local_active_input_devices_count',
@@ -383,6 +380,11 @@ __all__ = [
'DisplayTimer',
'Time',
'BaseTime',
+ 'AppIntent',
+ 'AppIntentDefault',
+ 'AppIntentExec',
+ 'AppMode',
+ 'SceneV1AppMode',
]
# Sanity check: we want to keep ballistica's dependencies and
diff --git a/src/assets/ba_data/python/bascenev1/_appmode.py b/src/assets/ba_data/python/bascenev1/_appmode.py
new file mode 100644
index 00000000..9988708a
--- /dev/null
+++ b/src/assets/ba_data/python/bascenev1/_appmode.py
@@ -0,0 +1,36 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Provides AppMode functionality."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from babase import AppMode, AppIntentExec, AppIntentDefault
+import _bascenev1
+
+if TYPE_CHECKING:
+ from babase import AppIntent
+
+
+class SceneV1AppMode(AppMode):
+ """Our app-mode."""
+
+ @classmethod
+ def supports_intent(cls, intent: AppIntent) -> bool:
+ # We support default and exec intents currently.
+ return isinstance(intent, AppIntentExec | AppIntentDefault)
+
+ def handle_intent(self, intent: AppIntent) -> None:
+ if isinstance(intent, AppIntentExec):
+ _bascenev1.handle_app_intent_exec(intent.code)
+ return
+ assert isinstance(intent, AppIntentDefault)
+ _bascenev1.handle_app_intent_default()
+
+ def on_activate(self) -> None:
+ # Let the native layer do its thing.
+ _bascenev1.app_mode_activate()
+
+ def on_deactivate(self) -> None:
+ # Let the native layer do its thing.
+ _bascenev1.app_mode_deactivate()
diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py
index c92590ac..d24800d8 100644
--- a/src/assets/ba_data/python/bauiv1/__init__.py
+++ b/src/assets/ba_data/python/bauiv1/__init__.py
@@ -58,6 +58,8 @@ from _babase import (
)
from _babase import screenmessage
+from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec
+from babase._appmode import AppMode
from babase._general import Call, WeakCall, AppTime, DisplayTime
from babase._language import Lstr
from babase._plugin import PotentialPlugin, Plugin
@@ -201,6 +203,10 @@ __all__ = [
'displaytimer',
'DisplayTimer',
'uibounds',
+ 'AppIntent',
+ 'AppIntentDefault',
+ 'AppIntentExec',
+ 'AppMode',
]
# Sanity check: we want to keep ballistica's dependencies and
diff --git a/src/ballistica/base/assets/assets.cc b/src/ballistica/base/assets/assets.cc
index 3d47201e..37c15917 100644
--- a/src/ballistica/base/assets/assets.cc
+++ b/src/ballistica/base/assets/assets.cc
@@ -1326,7 +1326,7 @@ void Assets::SetLanguageKeys(
}
// Let some subsystems know that language has changed.
- g_base->app_mode->LanguageChanged();
+ g_base->app_mode()->LanguageChanged();
g_base->ui->LanguageChanged();
g_base->graphics->LanguageChanged();
}
diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc
index ce2864f4..9f0e172a 100644
--- a/src/ballistica/base/base.cc
+++ b/src/ballistica/base/base.cc
@@ -71,8 +71,8 @@ BaseFeatureSet::BaseFeatureSet()
text_graphics{new TextGraphics()},
audio_server{new AudioServer()},
assets{new Assets()},
- app_mode{TempSV1CreateAppMode()},
- // app_mode{AppModeEmpty::GetSingleton()},
+ // app_mode{TempSV1CreateAppMode()},
+ app_mode_{AppModeEmpty::GetSingleton()},
stdio_console{g_buildconfig.enable_stdio_console() ? new StdioConsole()
: nullptr} {
// We're a singleton. If there's already one of us, something's wrong.
@@ -202,6 +202,11 @@ void BaseFeatureSet::StartApp() {
g_core->BootLog("start-app end");
}
+void BaseFeatureSet::set_app_mode(AppMode* mode) {
+ assert(InLogicThread());
+ app_mode_ = mode;
+}
+
auto BaseFeatureSet::AppManagesEventLoop() -> bool {
return app->ManagesEventLoop();
}
diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h
index a047523d..65ded963 100644
--- a/src/ballistica/base/base.h
+++ b/src/ballistica/base/base.h
@@ -702,9 +702,11 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
UI* const ui;
Utils* const utils;
+ auto* console() const { return console_; }
+ auto* app_mode() const { return app_mode_; }
+ void set_app_mode(AppMode* mode);
+
// Non-const bits (fixme: clean up access to these).
- AppMode* app_mode;
- auto* console() { return console_; }
TouchInput* touch_input{};
private:
@@ -713,6 +715,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
void PrintContextForCallableLabel(const char* label);
void PrintContextUnavailable();
+ AppMode* app_mode_;
Console* console_{};
std::string console_startup_messages_;
bool called_start_app_{};
diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc
index d00e2a91..0e4af3ba 100644
--- a/src/ballistica/base/graphics/graphics.cc
+++ b/src/ballistica/base/graphics/graphics.cc
@@ -416,7 +416,7 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) {
}
if (show_ping_) {
- std::string ping_str = g_base->app_mode->GetPingString();
+ std::string ping_str = g_base->app_mode()->GetPingString();
float ping{};
if (!ping_str.empty()) {
if (ping_str != ping_string_) {
@@ -452,7 +452,7 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) {
}
if (show_net_info_) {
- auto net_info_str{g_base->app_mode->GetNetworkDebugString()};
+ auto net_info_str{g_base->app_mode()->GetNetworkDebugString()};
if (!net_info_str.empty()) {
if (net_info_str != net_info_string_) {
net_info_string_ = net_info_str;
@@ -908,6 +908,7 @@ void Graphics::AddScreenMessage(const std::string& msg, const Vector3f& color,
}
void Graphics::Reset() {
+ assert(g_base->InLogicThread());
fade_ = 0;
fade_start_ = 0;
@@ -1075,7 +1076,7 @@ void Graphics::DrawWorld(FrameDef* frame_def) {
// Draw all session contents (nodes, etc.)
overlay_node_z_depth_ = -0.95f;
- g_base->app_mode->DrawWorld(frame_def);
+ g_base->app_mode()->DrawWorld(frame_def);
g_base->bg_dynamics->Draw(frame_def);
// Lastly draw any blotches that have been building up.
@@ -1131,7 +1132,7 @@ void Graphics::BuildAndPushFrameDef() {
// wants to know.
if (last_frame_def_graphics_quality_ != frame_def->quality()) {
last_frame_def_graphics_quality_ = frame_def->quality();
- g_base->app_mode->GraphicsQualityChanged(frame_def->quality());
+ g_base->app_mode()->GraphicsQualityChanged(frame_def->quality());
}
ApplyCamera(frame_def);
@@ -1142,7 +1143,7 @@ void Graphics::BuildAndPushFrameDef() {
} else {
// Ok, we're drawing a real frame.
- bool session_fills_screen = g_base->app_mode->DoesWorldFillScreen();
+ bool session_fills_screen = g_base->app_mode()->DoesWorldFillScreen();
frame_def->set_needs_clear(!session_fills_screen);
DrawWorld(frame_def);
diff --git a/src/ballistica/base/input/device/joystick_input.cc b/src/ballistica/base/input/device/joystick_input.cc
index 1b7c9781..ed7e6e5e 100644
--- a/src/ballistica/base/input/device/joystick_input.cc
+++ b/src/ballistica/base/input/device/joystick_input.cc
@@ -995,8 +995,8 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) {
} else {
// FIXME: Need a call we can make for this.
bool do_party_button = false;
- int party_size = g_base->app_mode->GetPartySize();
- if (party_size > 1 || g_base->app_mode->HasConnectionToHost()
+ int party_size = g_base->app_mode()->GetPartySize();
+ if (party_size > 1 || g_base->app_mode()->HasConnectionToHost()
|| g_base->ui->root_ui()->always_draw_party_icon()) {
do_party_button = true;
}
diff --git a/src/ballistica/base/input/input.cc b/src/ballistica/base/input/input.cc
index 1efa8bd5..1e1762d0 100644
--- a/src/ballistica/base/input/input.cc
+++ b/src/ballistica/base/input/input.cc
@@ -266,7 +266,7 @@ void Input::AddInputDevice(InputDevice* device, bool standard_message) {
// Let the current app-mode assign it a delegate.
auto delegate = Object::CompleteDeferred(
- g_base->app_mode->CreateInputDeviceDelegate(device));
+ g_base->app_mode()->CreateInputDeviceDelegate(device));
device->set_delegate(delegate);
delegate->set_input_device(device);
@@ -980,12 +980,12 @@ void Input::HandleKeyPress(const SDL_Keysym* keysym) {
case SDLK_EQUALS:
case SDLK_PLUS:
- g_base->app_mode->ChangeGameSpeed(1);
+ g_base->app_mode()->ChangeGameSpeed(1);
handled = true;
break;
case SDLK_MINUS:
- g_base->app_mode->ChangeGameSpeed(-1);
+ g_base->app_mode()->ChangeGameSpeed(-1);
handled = true;
break;
diff --git a/src/ballistica/base/logic/logic.cc b/src/ballistica/base/logic/logic.cc
index 3e206943..4c359caa 100644
--- a/src/ballistica/base/logic/logic.cc
+++ b/src/ballistica/base/logic/logic.cc
@@ -63,7 +63,7 @@ void Logic::OnAppStart() {
g_base->input->OnAppStart();
g_base->ui->OnAppStart();
g_core->platform->OnAppStart();
- g_base->app_mode->OnAppStart();
+ g_base->app_mode()->OnAppStart();
if (g_base->HavePlus()) {
g_base->Plus()->OnAppStart();
}
@@ -91,7 +91,7 @@ void Logic::OnAppPause() {
if (g_base->HavePlus()) {
g_base->Plus()->OnAppPause();
}
- g_base->app_mode->OnAppPause();
+ g_base->app_mode()->OnAppPause();
g_core->platform->OnAppPause();
g_base->ui->OnAppPause();
g_base->input->OnAppPause();
@@ -108,7 +108,7 @@ void Logic::OnAppResume() {
g_base->input->OnAppResume();
g_base->ui->OnAppResume();
g_core->platform->OnAppResume();
- g_base->app_mode->OnAppResume();
+ g_base->app_mode()->OnAppResume();
if (g_base->HavePlus()) {
g_base->Plus()->OnAppResume();
}
@@ -127,7 +127,7 @@ void Logic::OnAppShutdown() {
if (g_base->HavePlus()) {
g_base->Plus()->OnAppShutdown();
}
- g_base->app_mode->OnAppShutdown();
+ g_base->app_mode()->OnAppShutdown();
g_core->platform->OnAppResume();
g_base->ui->OnAppShutdown();
g_base->input->OnAppShutdown();
@@ -152,7 +152,7 @@ void Logic::ApplyAppConfig() {
g_base->input->ApplyAppConfig();
g_base->ui->ApplyAppConfig();
g_core->platform->ApplyAppConfig();
- g_base->app_mode->ApplyAppConfig();
+ g_base->app_mode()->ApplyAppConfig();
if (g_base->HavePlus()) {
g_base->Plus()->ApplyAppConfig();
}
@@ -203,7 +203,7 @@ void Logic::OnInitialScreenCreated() {
1000 / 10, true, NewLambdaRunnable([this] { StepDisplayTime(); }));
}
// Let our initial app-mode know it has become active.
- g_base->app_mode->OnActivate();
+ g_base->app_mode()->OnActivate();
// Let the Python layer know what's up. It will probably flip to
// 'Launching' state.
@@ -224,6 +224,13 @@ void Logic::CompleteAppBootstrapping() {
g_core->BootLog("app bootstrapping complete");
+ // Reset our various subsystems to a default state.
+ g_base->ui->Reset();
+ g_base->input->Reset();
+ g_base->graphics->Reset();
+ g_base->python->Reset();
+ g_base->audio->Reset();
+
// Let Python know we're done bootstrapping so it can flip the app
// into the 'launching' state.
g_base->python->objs()
@@ -232,7 +239,7 @@ void Logic::CompleteAppBootstrapping() {
app_bootstrapping_complete_ = true;
// TODO(ericf): update this for the shiny new app-mode world.
- if (explicit_bool(true)) {
+ if (explicit_bool(false)) {
// If we were passed launch command args, run them.
if (g_core->core_config().exec_command.has_value()) {
bool success = PythonCommand(*g_core->core_config().exec_command,
@@ -248,6 +255,8 @@ void Logic::CompleteAppBootstrapping() {
if (!appmode->GetForegroundSession()) {
appmode->RunMainMenu();
}
+ } else {
+ // Reset various subsystems
}
UpdatePendingWorkTimer();
@@ -267,7 +276,7 @@ void Logic::OnScreenSizeChange(float virtual_width, float virtual_height,
g_base->input->OnScreenSizeChange();
g_base->ui->OnScreenSizeChange();
g_core->platform->OnScreenSizeChange();
- g_base->app_mode->OnScreenSizeChange();
+ g_base->app_mode()->OnScreenSizeChange();
if (g_base->HavePlus()) {
g_base->Plus()->OnScreenSizeChange();
}
@@ -287,7 +296,7 @@ void Logic::StepDisplayTime() {
g_base->input->StepDisplayTime();
g_base->ui->StepDisplayTime();
g_core->platform->StepDisplayTime();
- g_base->app_mode->StepDisplayTime();
+ g_base->app_mode()->StepDisplayTime();
if (g_base->HavePlus()) {
g_base->Plus()->StepDisplayTime();
}
diff --git a/src/ballistica/base/networking/network_reader.cc b/src/ballistica/base/networking/network_reader.cc
index 489ca4db..788c2251 100644
--- a/src/ballistica/base/networking/network_reader.cc
+++ b/src/ballistica/base/networking/network_reader.cc
@@ -234,7 +234,7 @@ auto NetworkReader::RunThread() -> int {
memcpy(s_buffer.data(), buffer + 1, rresult2 - 1);
s_buffer[rresult2 - 1] = 0; // terminate string
std::string response =
- g_base->app_mode->HandleJSONPing(s_buffer.data());
+ g_base->app_mode()->HandleJSONPing(s_buffer.data());
if (!response.empty()) {
std::vector msg(1 + response.size());
msg[0] = BA_PACKET_JSON_PONG;
@@ -302,7 +302,7 @@ auto NetworkReader::RunThread() -> int {
}
case BA_PACKET_HOST_QUERY: {
- g_base->app_mode->HandleGameQuery(buffer, rresult2, &from);
+ g_base->app_mode()->HandleGameQuery(buffer, rresult2, &from);
// HandleGameQuery(buffer, rresult2, &from);
break;
@@ -338,8 +338,9 @@ void NetworkReader::PushIncomingUDPPacketCall(const std::vector& data,
return;
}
- g_base->logic->event_loop()->PushCall(
- [data, addr] { g_base->app_mode->HandleIncomingUDPPacket(data, addr); });
+ g_base->logic->event_loop()->PushCall([data, addr] {
+ g_base->app_mode()->HandleIncomingUDPPacket(data, addr);
+ });
}
void NetworkReader::OpenSockets() {
diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc
index ae7c9dbf..121eb92c 100644
--- a/src/ballistica/base/python/methods/python_methods_app.cc
+++ b/src/ballistica/base/python/methods/python_methods_app.cc
@@ -239,7 +239,7 @@ static auto PyPushCall(PyObject* self, PyObject* args, PyObject* keywds)
// Run this with an empty context by default, or foreground if
// requested.
ScopedSetContext ssc(other_thread_use_fg_context
- ? g_base->app_mode->GetForegroundContext()
+ ? g_base->app_mode()->GetForegroundContext()
: ContextRef(nullptr));
PythonRef(call_obj, PythonRef::kSteal).Call();
@@ -1243,6 +1243,28 @@ static PyMethodDef PyIsOSPlayingMusicDef = {
"(Used to determine whether the game should avoid playing its own)",
};
+// -------------------------------- exec_arg -----------------------------------
+
+static auto PyExecArg(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+
+ if (g_core->core_config().exec_command.has_value()) {
+ return PyUnicode_FromString(g_core->core_config().exec_command->c_str());
+ }
+ Py_RETURN_NONE;
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyExecArgDef = {
+ "exec_arg", // name
+ (PyCFunction)PyExecArg, // method
+ METH_NOARGS, // flags
+
+ "exec_arg() -> str | None\n"
+ "\n"
+ "(internal)\n",
+};
+
// -----------------------------------------------------------------------------
auto PythonMethodsApp::GetMethods() -> std::vector {
@@ -1281,6 +1303,7 @@ auto PythonMethodsApp::GetMethods() -> std::vector {
PyMacMusicAppGetPlaylistsDef,
PyIsOSPlayingMusicDef,
PyBootLogDef,
+ PyExecArgDef,
};
}
diff --git a/src/ballistica/base/support/classic_soft.h b/src/ballistica/base/support/classic_soft.h
new file mode 100644
index 00000000..dd35cb4d
--- /dev/null
+++ b/src/ballistica/base/support/classic_soft.h
@@ -0,0 +1,18 @@
+// Released under the MIT License. See LICENSE for details.
+
+#ifndef BALLISTICA_BASE_SUPPORT_CLASSIC_SOFT_H_
+#define BALLISTICA_BASE_SUPPORT_CLASSIC_SOFT_H_
+
+namespace ballistica::base {
+
+/// 'Soft' interface to the classic feature-set.
+/// Feature-sets listing classic as a soft requirement must limit their use of
+/// it to these methods and should be prepared to handle the not-present
+/// case.
+class ClassicSoftInterface {
+ public:
+};
+
+} // namespace ballistica::base
+
+#endif // BALLISTICA_BASE_SUPPORT_CLASSIC_SOFT_H_
diff --git a/src/ballistica/base/support/stdio_console.cc b/src/ballistica/base/support/stdio_console.cc
index 30084ca9..7c84d6e8 100644
--- a/src/ballistica/base/support/stdio_console.cc
+++ b/src/ballistica/base/support/stdio_console.cc
@@ -107,7 +107,7 @@ void StdioConsole::OnMainThreadStartApp() {
void StdioConsole::PushCommand(const std::string& command) {
g_base->logic->event_loop()->PushCall([command] {
// These are always run in whichever context is 'visible'.
- ScopedSetContext ssc(g_base->app_mode->GetForegroundContext());
+ ScopedSetContext ssc(g_base->app_mode()->GetForegroundContext());
PythonCommand cmd(command, "");
if (!g_core->user_ran_commands) {
g_core->user_ran_commands = true;
diff --git a/src/ballistica/base/ui/console.cc b/src/ballistica/base/ui/console.cc
index a1d58b2d..d204085a 100644
--- a/src/ballistica/base/ui/console.cc
+++ b/src/ballistica/base/ui/console.cc
@@ -146,7 +146,7 @@ void Console::PushCommand(const std::string& command) {
assert(g_base);
g_base->logic->event_loop()->PushCall([command] {
// These are always run in whichever context is 'visible'.
- ScopedSetContext ssc(g_base->app_mode->GetForegroundContext());
+ ScopedSetContext ssc(g_base->app_mode()->GetForegroundContext());
PythonCommand cmd(command, "");
if (!g_core->user_ran_commands) {
g_core->user_ran_commands = true;
diff --git a/src/ballistica/scene_v1/python/methods/python_methods_networking.cc b/src/ballistica/scene_v1/python/methods/python_methods_networking.cc
index c9230427..b254752b 100644
--- a/src/ballistica/scene_v1/python/methods/python_methods_networking.cc
+++ b/src/ballistica/scene_v1/python/methods/python_methods_networking.cc
@@ -660,7 +660,7 @@ static PyMethodDef PyEndHostScanningDef = {
static auto PyHaveConnectedClients(PyObject* self, PyObject* args,
PyObject* keywds) -> PyObject* {
BA_PYTHON_TRY;
- if (g_base->app_mode->HasConnectionToClients()) {
+ if (g_base->app_mode()->HasConnectionToClients()) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
diff --git a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc
index 2fdf91f3..90c69a9f 100644
--- a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc
+++ b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc
@@ -30,6 +30,7 @@
#include "ballistica/scene_v1/support/session_stream.h"
#include "ballistica/shared/generic/json.h"
#include "ballistica/shared/generic/utils.h"
+#include "ballistica/shared/python/python_command.h"
namespace ballistica::scene_v1 {
@@ -1611,6 +1612,109 @@ static PyMethodDef PySetInternalMusicDef = {
"(internal).",
};
+// --------------------------- app_mode_activate -------------------------------
+
+static auto PyAppModeActivate(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+ BA_PRECONDITION(g_base->InLogicThread());
+ g_base->set_app_mode(SceneV1AppMode::GetSingleton());
+ Py_RETURN_NONE;
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyAppModeActivateDef = {
+ "app_mode_activate", // name
+ (PyCFunction)PyAppModeActivate, // method
+ METH_NOARGS, // flags
+
+ "app_mode_activate() -> None\n"
+ "\n"
+ "(internal)\n",
+};
+
+// -------------------------- app_mode_deactivate ------------------------------
+
+static auto PyAppModeDeactivate(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+ BA_PRECONDITION(g_base->InLogicThread());
+ // Currently doing nothing.
+ Py_RETURN_NONE;
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyAppModeDeactivateDef = {
+ "app_mode_deactivate", // name
+ (PyCFunction)PyAppModeDeactivate, // method
+ METH_NOARGS, // flags
+
+ "app_mode_deactivate() -> None\n"
+ "\n"
+ "(internal)\n",
+};
+
+// ----------------------- handle_app_intent_default ---------------------------
+
+static auto PyHandleAppIntentDefault(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+ BA_PRECONDITION(g_base->InLogicThread());
+ auto* appmode = SceneV1AppMode::GetActiveOrThrow();
+ appmode->RunMainMenu();
+ Py_RETURN_NONE;
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyHandleAppIntentDefaultDef = {
+ "handle_app_intent_default", // name
+ (PyCFunction)PyHandleAppIntentDefault, // method
+ METH_NOARGS, // flags
+
+ "handle_app_intent_default() -> None\n"
+ "\n"
+ "(internal)\n",
+};
+
+// ------------------------ handle_app_intent_exec -----------------------------
+
+static auto PyHandleAppIntentExec(PyObject* self, PyObject* args,
+ PyObject* keywds) -> PyObject* {
+ BA_PYTHON_TRY;
+ const char* command;
+ static const char* kwlist[] = {"command", nullptr};
+ if (!PyArg_ParseTupleAndKeywords(args, keywds, "s",
+ const_cast(kwlist), &command)) {
+ return nullptr;
+ }
+ auto* appmode = SceneV1AppMode::GetActiveOrThrow();
+
+ // Run the command.
+ if (g_core->core_config().exec_command.has_value()) {
+ bool success = PythonCommand(*g_core->core_config().exec_command,
+ BA_BUILD_COMMAND_FILENAME)
+ .Exec(true, nullptr, nullptr);
+ if (!success) {
+ // FIXME: what should we do in this case?
+ // exit(1);
+ }
+ }
+ // If the stuff we just ran didn't result in a session, create a default
+ // one.
+ if (!appmode->GetForegroundSession()) {
+ appmode->RunMainMenu();
+ }
+ Py_RETURN_NONE;
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyHandleAppIntentExecDef = {
+ "handle_app_intent_exec", // name
+ (PyCFunction)PyHandleAppIntentExec, // method
+ METH_VARARGS | METH_KEYWORDS, // flags
+
+ "handle_app_intent_exec(command: str) -> None\n"
+ "\n"
+ "(internal)",
+};
+
// -----------------------------------------------------------------------------
auto PythonMethodsScene::GetMethods() -> std::vector {
@@ -1646,6 +1750,10 @@ auto PythonMethodsScene::GetMethods() -> std::vector {
PyBaseTimeDef,
PyBaseTimerDef,
PyLsInputDevicesDef,
+ PyAppModeActivateDef,
+ PyAppModeDeactivateDef,
+ PyHandleAppIntentDefaultDef,
+ PyHandleAppIntentExecDef,
};
}
diff --git a/src/ballistica/scene_v1/support/scene_v1_app_mode.cc b/src/ballistica/scene_v1/support/scene_v1_app_mode.cc
index c2fb1542..d3f1c799 100644
--- a/src/ballistica/scene_v1/support/scene_v1_app_mode.cc
+++ b/src/ballistica/scene_v1/support/scene_v1_app_mode.cc
@@ -315,7 +315,7 @@ auto SceneV1AppMode::GetActive() -> SceneV1AppMode* {
// keep in mind that app-mode may change under them.
// Otherwise return our singleton only if it is current.
- if (g_base->app_mode == g_scene_v1_app_mode) {
+ if (g_base->app_mode() == g_scene_v1_app_mode) {
return g_scene_v1_app_mode;
}
return nullptr;
@@ -1389,7 +1389,7 @@ void SceneV1AppMode::HandleGameQuery(const char* buffer, size_t size,
if (size == 5) {
// If we're already in a party, don't advertise since they
// wouldn't be able to join us anyway.
- if (g_base->app_mode->HasConnectionToHost()) {
+ if (g_base->app_mode()->HasConnectionToHost()) {
return;
}
diff --git a/src/ballistica/scene_v1/support/scene_v1_context.cc b/src/ballistica/scene_v1/support/scene_v1_context.cc
index 8f1dc681..28f4f930 100644
--- a/src/ballistica/scene_v1/support/scene_v1_context.cc
+++ b/src/ballistica/scene_v1/support/scene_v1_context.cc
@@ -10,7 +10,7 @@
namespace ballistica::scene_v1 {
auto ContextRefSceneV1::FromAppForegroundContext() -> ContextRefSceneV1 {
- auto* c = g_base->app_mode->GetForegroundContext().Get();
+ auto* c = g_base->app_mode()->GetForegroundContext().Get();
return ContextRefSceneV1(c);
}
diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc
index 7acbaf8e..71d9a164 100644
--- a/src/ballistica/shared/ballistica.cc
+++ b/src/ballistica/shared/ballistica.cc
@@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
namespace ballistica {
// These are set automatically via script; don't modify them here.
-const int kEngineBuildNumber = 21022;
+const int kEngineBuildNumber = 21023;
const char* kEngineVersion = "1.7.20";
auto MonolithicMain(const core::CoreConfig& core_config) -> int {
diff --git a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc
index f15a3192..59c170d5 100644
--- a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc
+++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc
@@ -2665,8 +2665,8 @@ static auto PyCanShowAd(PyObject* self, PyObject* args, PyObject* keywds)
// them or whatnot). Also disallow ads if remote apps are connected; at least
// on Android, ads pause our activity which disconnects the remote app.
// (need to fix this).
- if (g_base->app_mode->HasConnectionToHost()
- || g_base->app_mode->HasConnectionToClients()
+ if (g_base->app_mode()->HasConnectionToHost()
+ || g_base->app_mode()->HasConnectionToClients()
|| g_base->input->HaveRemoteAppController()) {
Py_RETURN_FALSE;
}
@@ -2873,8 +2873,8 @@ static auto PyIsPartyIconVisible(PyObject* self, PyObject* args,
PyObject* keywds) -> PyObject* {
BA_PYTHON_TRY;
bool party_button_active =
- (g_base->app_mode->HasConnectionToClients()
- || g_base->app_mode->HasConnectionToHost()
+ (g_base->app_mode()->HasConnectionToClients()
+ || g_base->app_mode()->HasConnectionToHost()
|| g_base->ui->root_ui()->always_draw_party_icon());
if (party_button_active) {
Py_RETURN_TRUE;
diff --git a/src/ballistica/ui_v1/support/root_ui.cc b/src/ballistica/ui_v1/support/root_ui.cc
index d93bfbf1..e1362ca3 100644
--- a/src/ballistica/ui_v1/support/root_ui.cc
+++ b/src/ballistica/ui_v1/support/root_ui.cc
@@ -42,8 +42,9 @@ RootUI::~RootUI() = default;
void RootUI::TogglePartyWindowKeyPress() {
assert(g_base->InLogicThread());
- if (g_base->app_mode->GetPartySize() > 1
- || g_base->app_mode->HasConnectionToHost() || always_draw_party_icon()) {
+ if (g_base->app_mode()->GetPartySize() > 1
+ || g_base->app_mode()->HasConnectionToHost()
+ || always_draw_party_icon()) {
ActivatePartyIcon();
}
}
@@ -77,8 +78,8 @@ auto RootUI::HandleMouseButtonDown(float x, float y) -> bool {
// floats over the top). Party button is to the left of menu button.
if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) {
bool party_button_active = (!party_window_open_
- && (g_base->app_mode->HasConnectionToClients()
- || g_base->app_mode->HasConnectionToHost()
+ && (g_base->app_mode()->HasConnectionToClients()
+ || g_base->app_mode()->HasConnectionToHost()
|| always_draw_party_icon()));
float party_button_left =
menu_active ? 2 * menu_button_size_ : menu_button_size_;
@@ -202,17 +203,17 @@ void RootUI::Draw(base::FrameDef* frame_def) {
// To the left of the menu button, draw our connected-players indicator
// (this probably shouldn't live here).
bool draw_connected_players_icon = false;
- int party_size = g_base->app_mode->GetPartySize();
- bool is_host = (!g_base->app_mode->HasConnectionToHost());
+ int party_size = g_base->app_mode()->GetPartySize();
+ bool is_host = (!g_base->app_mode()->HasConnectionToHost());
millisecs_t last_connection_to_client_join_time =
- g_base->app_mode->LastClientJoinTime();
+ g_base->app_mode()->LastClientJoinTime();
bool show_client_joined =
(is_host && last_connection_to_client_join_time != 0
&& real_time - last_connection_to_client_join_time < 5000);
if (!party_window_open_
- && (party_size != 0 || g_base->app_mode->HasConnectionToHost()
+ && (party_size != 0 || g_base->app_mode()->HasConnectionToHost()
|| always_draw_party_icon_)) {
draw_connected_players_icon = true;
}
@@ -221,7 +222,7 @@ void RootUI::Draw(base::FrameDef* frame_def) {
// Flash and show a message if we're in the main menu instructing the
// player to start a game.
bool flash = false;
- bool in_main_menu = g_base->app_mode->InMainMenu();
+ bool in_main_menu = g_base->app_mode()->InMainMenu();
if (in_main_menu && party_size > 0 && show_client_joined) flash = true;
diff --git a/src/ballistica/ui_v1/widget/root_widget.cc b/src/ballistica/ui_v1/widget/root_widget.cc
index 72e59c2b..2fbee847 100644
--- a/src/ballistica/ui_v1/widget/root_widget.cc
+++ b/src/ballistica/ui_v1/widget/root_widget.cc
@@ -895,7 +895,7 @@ void RootWidget::UpdateForFocusedWindow() {
void RootWidget::UpdateForFocusedWindow(Widget* widget) {
// Take note if the current session is the main menu; we do a few things
// differently there.
- in_main_menu_ = g_base->app_mode->InMainMenu();
+ in_main_menu_ = g_base->app_mode()->InMainMenu();
if (widget == nullptr) {
toolbar_visibility_ = ToolbarVisibility::kInGame;
diff --git a/tools/batools/project/_updater.py b/tools/batools/project/_updater.py
index 3230dccc..353f0d3e 100755
--- a/tools/batools/project/_updater.py
+++ b/tools/batools/project/_updater.py
@@ -312,7 +312,10 @@ class ProjectUpdater:
)
for i, change in enumerate(auto_changes):
- print(f'{Clr.BLU}Correcting file: {change[0]}{Clr.RST}')
+ print(
+ f'{Clr.BLU}{Clr.BLD}Correcting'
+ f' {change[0]} line {change[1].line_number+1}{Clr.RST}'
+ )
with open(
os.path.join(self.projroot, change[0]), encoding='utf-8'
) as infile: