diff --git a/.efrocachemap b/.efrocachemap
index 7c625ca3..665bb33f 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -1,5 +1,5 @@
{
- "ballisticakit-windows/Generic/BallisticaKit.ico": "be1b956dcd7f7a261b1afe5bce2a0336",
+ "ballisticakit-windows/Generic/BallisticaKit.ico": "6f33e74cb282f070871413f092983fcd",
"build/assets/ba_data/audio/achievement.ogg": "079a366ce183b25a63550ef7072af605",
"build/assets/ba_data/audio/actionHero1.ogg": "f0f986f268f036a5ac2f940e07f2f27e",
"build/assets/ba_data/audio/actionHero2.ogg": "204a6735dc655f0975cf8308b585f2fd",
@@ -4056,50 +4056,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": "4129fd84c3c64b770c4343d347bff97a",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "76b293b2d942716c2606c56e13483e66",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "9cc2a5d464da1e0822ecf5493ac28b64",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "40e605e595f00286805cf15ffc096813",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "bdc47e8dd94bcfc2a7b8268ea366f9b5",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "6b57b2de2d4aefcb3a5f7df6cef53a9d",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "34c65f2a9f280420387af135d5bc6a2d",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "79e4d857fbd0871940c7fd6985d11af1",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "ce179db11df98c7dc53bd2fd2a710909",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "d47c86f2654185ab0690949c3b8f2913",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "a33fb01bdbeb7be34046d7b64673991c",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "5f4cb90415aed9415231e95394384d2a",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "26540ab81b0ad717818e077efcb9029d",
- "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "11fc4a0cdf83c362b2f5c0c62a53014e",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "ba10cfebc435f211be18dbdc7416097d",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "551b8dba96bfc65c5166cde6bec37539",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "bc18f0765c9d96731a1b1a7acf5151db",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "1f1a8227190894db122fb43691371a92",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "9d546413ee365044d77e0e9c39ed77bb",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "470b427c4e51254538291ac56298c212",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "a61cf6ac9afd43081df7f63ff81f4036",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "8ea626f6dd70d998ee77c58fffc51545",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "d7ad10904fe7c4d4555366ccb1feedcb",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "a9eea92d521e97b1772b5e44b402ce8a",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "57043f40971d800d27ee6d646aae8d61",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "3e4009d7fa4b90abc526f56361ecab05",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "334cdc0688e66a1bc75cd05bae1729c7",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "95278d80378be5b61026253492cbfa70",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "0a3da8e5264a7b733960e83a0e8c4bba",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "a089d4aaa39e18553cf0a70a77b4cfcd",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "e66c4eceb79710b8fda2bfea781e241a",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "6b3021b9a7584da86bbb95324e81e851",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "6fe90d50f905a0da9fa52c39a458d1e3",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "afc7ed826486aec82613832865177570",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "ae8b6dad770793188aad8e2966189bb3",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "7622dde1a021152cac42e7db3e803392",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "8a4b1e521bf668cc6ec6a65519defb12",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "9a69c0d4c9ae319595843b16f14795fc",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "89c02260fb4781f5e293658cecbb363f",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "3165d4230069c22300abfff8abf1d714",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "41e46dfdbb542e3e823f6aee87e93ac9",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "20cc128ff9d44f9d74e4301c6d49f48f",
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "f93bc8f98ee31f39b54ab46264eccb22",
- "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "a61cf6ac9afd43081df7f63ff81f4036",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "20cc128ff9d44f9d74e4301c6d49f48f",
"build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "f93bc8f98ee31f39b54ab46264eccb22",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "117f4fc8b60372763ecae00d332947a8",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "1565f3b227843827d692cb3ef65847b6",
"build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "d8d74c6c40db43054ccc7d27920cfbfe",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "117f4fc8b60372763ecae00d332947a8",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "1565f3b227843827d692cb3ef65847b6",
"build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "d8d74c6c40db43054ccc7d27920cfbfe",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "ba3a67e11c268a5b81b3416bc90cc297",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "43bdfa8acd84e9cf2e443ce8e923c229",
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "d935dc21becdfd65bec51c5f5b2fd770",
- "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "ba3a67e11c268a5b81b3416bc90cc297",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "43bdfa8acd84e9cf2e443ce8e923c229",
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "d935dc21becdfd65bec51c5f5b2fd770",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "c5fb4b7f765cfd68ff09bcef3c5f84e6",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "6d49ad39f194480da458b431405f5a2b",
"build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "c8715c85010ea431d7346f40f5421819",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "737778fc5e6a6ed5ca154c7953fcb377",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "49775819d4ba9af15061080d17377a18",
"build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "c8715c85010ea431d7346f40f5421819",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "dfd5dca061d6eacaccc38c12d391cc82",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "96ab39f16820a39fa7c42af6779c3556",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "d87f20b0a0192b90687b936b6c2ad103",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "4a336c3e976924b70b39d18af6040e41",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "77d10f03566f29816de9b2ff806cc905",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "d90ef473768f7ba232ae3ca58c5c8c04",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "743d872f2c848a84e4e5d49ca6b426e9",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "36272a00b45729022167baa81749a37e",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "750c2964308cd3f3e5986fcda9a25706",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "772af7da6a115f53b0b3f6a4afd3baec",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "178c1a53a7ad50297aed68d0ca3a1476",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "b011bc2b6437995f2d33f5215b4ffa36",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "a758a4f98336208381b093aacb735878",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "6c86545fab2327105114676a20ca5e68",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "f75525fdc9f7db4a81ca9bae6a79add5",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "2e297069baec404d43ccdb18abeef658",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "28323912b56ec07701eda3d41a6a4101",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "ba8ce3ca3858b4c2d20db68f99b788b2",
diff --git a/.gitignore b/.gitignore
index 1c856436..273c7b58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -120,10 +120,10 @@ xcuserdata/
/ballisticakit-android/BallisticaKit/src/main/res/mipmap-*/ic_launcher*.png
/ballisticakit-android/BallisticaKit/src/cardboard/res/mipmap-*/ic_launcher*.png
BallisticaKit.ico
-/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/Cursor macOS.appiconset/cursor_*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/AppIcon iOS.appiconset/icon_*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/AppIcon macOS.appiconset/icon_*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Layer*.imagestacklayer/Content.imageset/*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Layer*.imagestacklayer/Content.imageset/*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/*.png
+/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/Cursor macOS.imageset/cursor_*.png
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 0143897a..e94ce45b 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -4,7 +4,7 @@
-
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8eb8e41b..4cabf797 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-### 1.7.28 (build 21447, api 8, 2023-10-12)
+### 1.7.28 (build 21465, api 8, 2023-10-14)
- Massively cleaned up code related to rendering and window systems (OpenGL,
SDL, etc). This code had been growing into a nasty tangle for 15 years
@@ -12,8 +12,10 @@
newer on mobile. This means we're cutting off a few percent of old devices on
Android that only support ES 2, but ES 3 has been out for 10 years now so I
feel it is time. As mentioned above, this allows massively cleaning up the
- graphics code which means we can start to improve it.
-- Removed gamma controls. These were only active on the old Mac version anyway
+ graphics code which means we can start to improve it. Ideally now the GL
+ renderer can be abstracted a bit more which will make the process of writing
+ other renderers easier.
+- Removed gamma controls. These were only active on the old Mac builds anyway
and are being removed from the upcoming SDL3, so if we want this sort of thing
we should do it through shading in the renderer now.
- Implemented both vsync and max-fps for the SDL build of the game. This means
@@ -129,7 +131,22 @@
before. It also takes a `confirm` bool arg which allows it to be used to bring
up a confirm dialog.
- Clicking on a window close button to quit no longer brings up a confirm dialog
- and instead quits immediately (though with a proper graceful shutdown).
+ and instead quits immediately (though with a proper graceful shutdown and a
+ lovely little fade).
+- Camera shake is now supported in network games and replays. Somehow I didn't
+ notice that was missing for years. The downside is this requires a server to
+ be hosting protocol 35, which cuts off support for 1.4 clients. So for now I
+ am keeping the default at 33. Once there a fewer 1.4 clients around we can
+ consider changing this (if everything hasn't moved to SceneV2 by then).
+- Added a server option to set the hosting protocol for servers who might want
+ to allow camera shake (or other minor features/fixes) that don't work in the
+ default protocol 33. See `protocol_version` in `config.yaml`. Just remember
+ that you will be cutting off support for older clients if you use 35.
+- Fixed a bug with screen-messages animating off screen too fast when frame
+ rates are high.
+- Added a proper graceful shutdown process for the audio server. This should
+ result in fewer ugly pops and warning messages when the app is quit.
+
### 1.7.27 (build 21282, api 8, 2023-08-30)
diff --git a/Makefile b/Makefile
index 36637945..d21ab3e5 100644
--- a/Makefile
+++ b/Makefile
@@ -779,13 +779,12 @@ check-full: py_check_prereqs
# Same as 'check' plus optional/slow extra checks.
check2: py_check_prereqs
- @$(DMAKE) -j$(CPUS) update-check cpplint pylint mypy pycharm
+ @$(DMAKE) -j$(CPUS) update-check cpplint pylint mypy
@$(PCOMMANDBATCH) echo SGRN BLD ALL CHECKS PASSED!
# Same as check2 but no caching (all files are checked).
check2-full: py_check_prereqs
- @$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full \
- pycharm-full
+ @$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full
@$(PCOMMANDBATCH) echo SGRN BLD ALL CHECKS PASSED!
# Run Cpplint checks on all C/C++ code.
@@ -924,14 +923,14 @@ preflight-full:
preflight2:
@$(MAKE) format
@$(MAKE) update
- @$(MAKE) -j$(CPUS) cpplint pylint mypy pycharm test
+ @$(MAKE) -j$(CPUS) cpplint pylint mypy test
@$(PCOMMANDBATCH) echo SGRN BLD PREFLIGHT SUCCESSFUL!
# Same as 'preflight2' but without caching (all files visited).
preflight2-full:
@$(MAKE) format-full
@$(MAKE) update
- @$(MAKE) -j$(CPUS) cpplint-full pylint-full mypy-full pycharm-full test-full
+ @$(MAKE) -j$(CPUS) cpplint-full pylint-full mypy-full test-full
@$(PCOMMANDBATCH) echo SGRN BLD PREFLIGHT SUCCESSFUL!
# Tell make which of these targets don't represent files.
diff --git a/config/projectconfig.json b/config/projectconfig.json
index a60bf91d..6dbb5ae0 100644
--- a/config/projectconfig.json
+++ b/config/projectconfig.json
@@ -14,7 +14,6 @@
"src/ballistica/core/platform/android/utf8/checked.h",
"src/ballistica/core/platform/android/utf8/unchecked.h",
"src/ballistica/core/platform/android/utf8/core.h",
- "src/ballistica/base/platform/apple/sdl_main_mac.h",
"src/ballistica/base/platform/oculus/main_rift.cc",
"src/ballistica/core/platform/android/android_gl3.c"
],
diff --git a/config/spinoffconfig.py b/config/spinoffconfig.py
index 41dda47b..490697b0 100644
--- a/config/spinoffconfig.py
+++ b/config/spinoffconfig.py
@@ -53,47 +53,58 @@ ctx.src_omit_paths = {
'src/assets/workspace',
}
-# Use this to 'carve out' directories or exact file paths which will be
-# git-managed on dst. By default, spinoff will consider dirs containing
-# the files it syncs from src as 'spinoff-managed'; it will set them as
-# git-ignored and will complain if any files appear in them that it does
-# not manage itself (to prevent accidentally doing work in such places).
-# Note that adding a dir to src_write_paths does not prevent files
-# within it from being synced by spinoff; it just means that each of
-# those individual spinoff-managed files will have their own gitignore
-# entry since there is no longer one covering the whole dir. So to keep
-# things tidy, carve out the minimal set of exact file/dir paths that
-# you need.
+# Use this to 'carve out' files or directories which will be git-managed
+# on dst.
+#
+# By default, spinoff will consider dirs containing the files it syncs
+# from src as 'spinoff-managed'; it will set them as git-ignored and
+# will complain if any files appear in them that it does not manage
+# itself (to prevent accidentally doing work in such places). Note that
+# adding a dir to src_write_paths does not prevent files within it from
+# being synced by spinoff; it just means that each of those individual
+# spinoff-managed files will have their own gitignore entry since there
+# can't be a single one covering the whole dir. So to keep things tidy,
+# carve out the minimal set of exact file/dir paths that you need.
ctx.src_write_paths = {
'tools/spinoff',
'config/spinoffconfig.py',
}
-# Normally spinoff errors if it finds any files in its managed dirs that
-# it did not put there. This is to prevent accidentally working in these
-# parts of a dst project; since these sections are git-ignored, git
-# itself won't raise any warnings in such cases and it would be easy to
-# accidentally lose work otherwise.
+# Use this to 'carve out' files or directories under spinoff managed
+# dirs which will be completely ignored by spinoff (but *not* placed
+# under git control).
#
-# This list can be used to suppress spinoff's errors for specific
-# locations. This is generally used to allow build output or other
-# dynamically generated files to exist within spinoff-managed
-# directories. It is possible to use src_write_paths for such purposes,
-# but this has the side-effect of greatly complicating the dst project's
-# gitignore list; selectively marking a few dirs as unchecked makes for
-# a cleaner setup. Just be careful to not set excessively broad regions
-# as unchecked; you don't want to mask actual useful error messages.
+# Normally spinoff will error if it finds any files under its managed
+# dirs that it did not put there. This is to prevent accidentally
+# working in these parts of a dst project; since spinoff-controlled
+# stuff is git-ignored, git itself won't raise any warnings in such
+# cases and it would be easy to accidentally blow away changes if
+# spinoff didn't raise a stink.
+#
+# This list is used to suppress raising of said stink for specific
+# locations. This allows build output or other dynamically generated
+# files to exist under spinoff-managed directories. It is also possible
+# to use src_write_paths for such carve-outs, but that can have the
+# negative side-effect of greatly complicating the dst project's
+# .gitignore file. Selectively marking a few specific files or dirs as
+# unchecked instead can keep things tidier and more understandable.
+#
+# Note that files and paths marked as unchecked cannot be the
+# destination for synced files, as that would be ambiguous (We can
+# either sync the file ourself or expect someone else to write it, but
+# not both).
ctx.src_unchecked_paths = {
'src/ballistica/mgen',
'src/ballistica/*/mgen',
'src/assets/ba_data/python/*/_mgen',
'src/meta/*/mgen',
'ballisticakit-cmake/.clang-format',
- 'ballisticakit-android/BallisticaKit/src/cardboard/res',
'ballisticakit-windows/*/BallisticaKit.ico',
- 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets',
- 'ballisticakit-android/BallisticaKit/src/*/res',
- 'ballisticakit-android/BallisticaKit/src/*/assets',
+ 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*.png',
+ 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*/*.png',
+ 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*/*/*/*.png',
+ 'ballisticakit-android/BallisticaKit/src/*/res/*/*.png',
+ 'ballisticakit-android/BallisticaKit/src/*/assets/ballistica_files',
'ballisticakit-android/local.properties',
'ballisticakit-android/.gradle',
'ballisticakit-android/build',
diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py
index 134caf5d..84147e5d 100644
--- a/src/assets/ba_data/python/babase/_app.py
+++ b/src/assets/ba_data/python/babase/_app.py
@@ -56,6 +56,8 @@ class App:
# pylint: disable=too-many-public-methods
+ # A few things defined as non-optional values but not actually
+ # available until the app starts.
plugins: PluginSubsystem
lang: LanguageSubsystem
health_monitor: AppHealthMonitor
@@ -92,7 +94,7 @@ class App:
# Used on platforms such as mobile where the app basically needs
# to shut down while backgrounded. In this state, all event
- # loops are suspended and all graphics and audio should cease
+ # loops are suspended and all graphics and audio must cease
# completely. Be aware that the suspended state can be entered
# from any other state including NATIVE_BOOTSTRAPPING and
# SHUTTING_DOWN.
@@ -149,9 +151,9 @@ class App:
def __init__(self) -> None:
"""(internal)
- Do not instantiate this class; access the single shared instance
- of it as 'app' which is available in various Ballistica
- feature-set modules such as babase.
+ Do not instantiate this class. You can access the single shared
+ instance of it through various high level packages: 'babase.app',
+ 'bascenev1.app', 'bauiv1.app', etc.
"""
# Hack for docs-generation: we can be imported with dummy modules
@@ -208,7 +210,8 @@ class App:
self._shutdown_task: asyncio.Task[None] | None = None
self._shutdown_tasks: list[Coroutine[None, None, None]] = [
self._wait_for_shutdown_suppressions(),
- self._fade_for_shutdown(),
+ self._fade_and_shutdown_graphics(),
+ self._fade_and_shutdown_audio(),
]
self._pool_thread_count = 0
@@ -508,7 +511,7 @@ class App:
except Exception:
logging.exception('Error setting app intent to %s.', intent)
_babase.pushcall(
- tpartial(self._apply_intent_error, intent),
+ tpartial(self._display_set_intent_error, intent),
from_other_thread=True,
)
@@ -553,10 +556,11 @@ class App:
'Error handling intent %s in app-mode %s.', intent, mode
)
- def _apply_intent_error(self, intent: AppIntent) -> None:
+ def _display_set_intent_error(self, intent: AppIntent) -> None:
+ """Show the *user* something went wrong setting an intent."""
from babase._language import Lstr
- del intent # Unused.
+ del intent
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
_babase.getsimplesound('error').play()
@@ -795,6 +799,7 @@ class App:
async def _shutdown(self) -> None:
import asyncio
+ _babase.lock_all_input()
try:
async with asyncio.TaskGroup() as task_group:
for task_coro in self._shutdown_tasks:
@@ -895,18 +900,26 @@ class App:
await asyncio.sleep(0.001)
_babase.lifecyclelog('shutdown-suppress wait end')
- async def _fade_for_shutdown(self) -> None:
+ async def _fade_and_shutdown_graphics(self) -> None:
import asyncio
- # Kick off a fade, block input, and wait for a short bit.
- # Ideally most shutdown activity completes during the fade so
- # there's no tangible wait.
- _babase.lifecyclelog('fade-for-shutdown begin')
+ # Kick off a short fade and give it time to complete.
+ _babase.lifecyclelog('fade-and-shutdown-graphics begin')
_babase.fade_screen(False, time=0.15)
- _babase.lock_all_input()
- # _babase.getsimplesound('swish2').play()
await asyncio.sleep(0.15)
- _babase.lifecyclelog('fade-for-shutdown end')
+ _babase.lifecyclelog('fade-and-shutdown-graphics end')
+
+ async def _fade_and_shutdown_audio(self) -> None:
+ import asyncio
+
+ # Tell the audio system to go down and give it a bit of
+ # time to do so gracefully.
+ _babase.lifecyclelog('fade-and-shutdown-audio begin')
+ _babase.audio_shutdown_begin()
+ await asyncio.sleep(0.15)
+ while not _babase.audio_shutdown_is_complete():
+ await asyncio.sleep(0.01)
+ _babase.lifecyclelog('fade-and-shutdown-audio end')
def _threadpool_no_wait_done(self, fut: Future) -> None:
try:
diff --git a/src/assets/ba_data/python/baclassic/_accountv1.py b/src/assets/ba_data/python/baclassic/_accountv1.py
index f4667edf..e732f135 100644
--- a/src/assets/ba_data/python/baclassic/_accountv1.py
+++ b/src/assets/ba_data/python/baclassic/_accountv1.py
@@ -302,6 +302,11 @@ class AccountV1Subsystem:
"""(internal)"""
plus = babase.app.plus
if plus is None:
+ import logging
+
+ logging.warning(
+ 'Error adding pending promo code; plus not present.'
+ )
babase.screenmessage(
babase.Lstr(resource='errorText'), color=(1, 0, 0)
)
diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py
index 5524a9e8..b832f68b 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 21447
+TARGET_BALLISTICA_BUILD = 21465
TARGET_BALLISTICA_VERSION = '1.7.28'
diff --git a/src/assets/ba_data/python/bauiv1lib/account/settings.py b/src/assets/ba_data/python/bauiv1lib/account/settings.py
index b0e56c1e..50959d01 100644
--- a/src/assets/ba_data/python/bauiv1lib/account/settings.py
+++ b/src/assets/ba_data/python/bauiv1lib/account/settings.py
@@ -1184,6 +1184,9 @@ class AccountSettingsWindow(bui.Window):
self, response: bacommon.cloud.ManageAccountResponse | Exception
) -> None:
if isinstance(response, Exception) or response.url is None:
+ logging.warning(
+ 'Got error in manage-account-response: %s.', response
+ )
bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
bui.getsound('error').play()
return
@@ -1466,6 +1469,7 @@ class AccountSettingsWindow(bui.Window):
if isinstance(result, Exception):
# For now just make a bit of noise if anything went wrong;
# can get more specific as needed later.
+ logging.warning('Got error in v2 sign-in result: %s.', result)
bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
bui.getsound('error').play()
else:
diff --git a/src/assets/server_package/ballisticakit_server.py b/src/assets/server_package/ballisticakit_server.py
index 840079dd..2ccd1152 100755
--- a/src/assets/server_package/ballisticakit_server.py
+++ b/src/assets/server_package/ballisticakit_server.py
@@ -15,8 +15,8 @@ from threading import Lock, Thread, current_thread
from typing import TYPE_CHECKING
# We make use of the bacommon and efro packages as well as site-packages
-# included with our bundled Ballistica dist, so we need to add those paths
-# before we import them.
+# included with our bundled Ballistica dist, so we need to add those
+# paths before we import them.
sys.path += [
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')),
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')),
@@ -34,31 +34,55 @@ if TYPE_CHECKING:
VERSION_STR = '1.3.1'
# Version history:
+#
# 1.3.1
-# Windows binary is now named BallisticaKitHeadless.exe
+#
+# - Windows binary is now named 'BallisticaKitHeadless.exe'.
+#
# 1.3:
-# Added show_tutorial config option
-# Added team_names config option
-# Added team_colors config option
-# Added playlist_inline config option
+#
+# - Added show_tutorial config option.
+#
+# - Added team_names config option.
+#
+# - Added team_colors config option.
+#
+# - Added playlist_inline config option.
+#
# 1.2:
-# Added optional --help arg
-# Added --config arg for specifying config file and --root for ba_root path
-# Added noninteractive mode and --interactive/--noninteractive args to
-# explicitly enable/disable it (it is autodetected by default)
-# Added explicit control for auto-restart: --no-auto-restart
-# Config file is now reloaded each time server binary is restarted; no more
-# need to bring down server wrapper to pick up changes
-# Now automatically restarts server binary when config file is modified
-# (use --no-config-auto-restart to disable that behavior)
+#
+# - Added optional --help arg.
+#
+# - Added --config arg for specifying config file and --root for
+# ba_root path.
+#
+# - Added noninteractive mode and --interactive/--noninteractive args
+# to explicitly enable/disable it (it is autodetected by default).
+#
+# - Added explicit control for auto-restart: --no-auto-restart.
+#
+# - Config file is now reloaded each time server binary is restarted;
+# no more need to bring down server wrapper to pick up changes.
+#
+# - Now automatically restarts server binary when config file is
+# modified (use --no-config-auto-restart to disable that behavior).
+#
# 1.1.1:
-# Switched config reading to use efro.dataclasses.dataclass_from_dict()
+#
+# - Switched config reading to use
+# efro.dataclasses.dataclass_from_dict().
+#
# 1.1.0:
-# Added shutdown command
-# Changed restart to default to immediate=True
-# Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes
+#
+# - Added shutdown command.
+#
+# - Changed restart to default to immediate=True.
+#
+# - Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes.
+#
# 1.0.0:
-# Initial release
+#
+# - Initial release.
class ServerManagerApp:
@@ -101,8 +125,9 @@ class ServerManagerApp:
# This may override the above defaults.
self._parse_command_line_args()
- # Do an initial config-load. If the config is invalid at this point
- # we can cleanly die (we're more lenient later on reloads).
+ # Do an initial config-load. If the config is invalid at this
+ # point we can cleanly die (we're more lenient later on
+ # reloads).
self.load_config(strict=True, print_confirmation=False)
@property
@@ -131,9 +156,9 @@ class ServerManagerApp:
)
# Python will handle SIGINT for us (as KeyboardInterrupt) but we
- # need to register a SIGTERM handler so we have a chance to clean
- # up our subprocess when someone tells us to die. (and avoid
- # zombie processes)
+ # need to register a SIGTERM handler so we have a chance to
+ # clean up our subprocess when someone tells us to die. (and
+ # avoid zombie processes)
signal.signal(signal.SIGTERM, self._handle_term_signal)
# During a run, we make the assumption that cwd is the dir
@@ -155,7 +180,8 @@ class ServerManagerApp:
f'{Clr.CYN}Waiting for subprocess exit...{Clr.RST}', flush=True
)
- # Mark ourselves as shutting down and wait for the process to wrap up.
+ # Mark ourselves as shutting down and wait for the process to
+ # wrap up.
self._done = True
self._subprocess_thread.join()
@@ -181,9 +207,10 @@ class ServerManagerApp:
# Gracefully bow out if we kill ourself via keyboard.
pass
except SystemExit:
- # We get this from the builtin quit(), our signal handler, etc.
- # Need to catch this so we can clean up, otherwise we'll be
- # left in limbo with our process thread still running.
+ # We get this from the builtin quit(), our signal handler,
+ # etc. Need to catch this so we can clean up, otherwise
+ # we'll be left in limbo with our process thread still
+ # running.
pass
self._postrun()
@@ -207,14 +234,17 @@ class ServerManagerApp:
self._enable_tab_completion(context)
# Now just sit in an interpreter.
- # TODO: make it possible to use IPython if the user has it available.
+ #
+ # TODO: make it possible to use IPython if the user has it
+ # available.
try:
self._interpreter_start_time = time.time()
code.interact(local=context, banner='', exitmsg='')
except SystemExit:
- # We get this from the builtin quit(), our signal handler, etc.
- # Need to catch this so we can clean up, otherwise we'll be
- # left in limbo with our process thread still running.
+ # We get this from the builtin quit(), our signal handler,
+ # etc. Need to catch this so we can clean up, otherwise
+ # we'll be left in limbo with our process thread still
+ # running.
pass
except BaseException as exc:
print(
@@ -238,19 +268,21 @@ class ServerManagerApp:
self._block_for_command_completion()
def _block_for_command_completion(self) -> None:
- # Ideally we'd block here until the command was run so our prompt would
- # print after it's results. We currently don't get any response from
- # the app so the best we can do is block until our bg thread has sent
- # it. In the future we can perhaps add a proper 'command port'
- # interface for proper blocking two way communication.
+ # Ideally we'd block here until the command was run so our
+ # prompt would print after it's results. We currently don't get
+ # any response from the app so the best we can do is block until
+ # our bg thread has sent it. In the future we can perhaps add a
+ # proper 'command port' interface for proper blocking two way
+ # communication.
while True:
with self._subprocess_commands_lock:
if not self._subprocess_commands:
break
time.sleep(0.1)
- # One last short delay so if we come out *just* as the command is sent
- # we'll hopefully still give it enough time to process/print.
+ # One last short delay so if we come out *just* as the command
+ # is sent we'll hopefully still give it enough time to
+ # process/print.
time.sleep(0.1)
def screenmessage(
@@ -320,8 +352,8 @@ class ServerManagerApp:
)
)
- # If we're asking for an immediate restart but don't get one within
- # the grace period, bring down the hammer.
+ # If we're asking for an immediate restart but don't get one
+ # within the grace period, bring down the hammer.
if immediate:
self._subprocess_force_kill_time = (
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
@@ -340,12 +372,12 @@ class ServerManagerApp:
ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate)
)
- # An explicit shutdown means we know to bail completely once this
- # subprocess completes.
+ # An explicit shutdown means we know to bail completely once
+ # this subprocess completes.
self._wrapper_shutdown_desired = True
- # If we're asking for an immediate shutdown but don't get one within
- # the grace period, bring down the hammer.
+ # If we're asking for an immediate shutdown but don't get one
+ # within the grace period, bring down the hammer.
if immediate:
self._subprocess_force_kill_time = (
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
@@ -378,9 +410,10 @@ class ServerManagerApp:
if i + 1 >= argc:
raise CleanError('Expected a path as next arg.')
path = sys.argv[i + 1]
- # Unlike config_path, this one doesn't have to exist now.
- # We do however need an abs path because we may be in a
- # different cwd currently than we will be during the run.
+ # Unlike config_path, this one doesn't have to exist
+ # now. We do however need an abs path because we may be
+ # in a different cwd currently than we will be during
+ # the run.
self._ba_root_path = os.path.abspath(path)
i += 2
elif arg == '--interactive':
@@ -538,6 +571,7 @@ class ServerManagerApp:
if not os.path.exists(self._config_path):
# Special case:
+ #
# If the user didn't specify a particular config file, allow
# gracefully falling back to defaults if the default one is
# missing.
@@ -606,24 +640,26 @@ class ServerManagerApp:
"""Spin up the server subprocess and run it until exit."""
# pylint: disable=consider-using-with
- # Reload our config, and update our overall behavior based on it.
- # We do non-strict this time to give the user repeated attempts if
- # if they mess up while modifying the config on the fly.
+ # Reload our config, and update our overall behavior based on
+ # it. We do non-strict this time to give the user repeated
+ # attempts if if they mess up while modifying the config on the
+ # fly.
self.load_config(strict=False, print_confirmation=True)
self._prep_subprocess_environment()
- # Launch the binary and grab its stdin;
- # we'll use this to feed it commands.
+ # Launch the binary and grab its stdin; we'll use this to feed
+ # it commands.
self._subprocess_launch_time = time.time()
# Set an environment var so the server process knows its being
- # run under us. This causes it to ignore ctrl-c presses and other
- # slight behavior tweaks. Hmm; should this be an argument instead?
+ # run under us. This causes it to ignore ctrl-c presses and
+ # other slight behavior tweaks. Hmm; should this be an argument
+ # instead?
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
- # Set an environment var to change the device name.
- # Device name is used while making connection with master server,
+ # Set an environment var to change the device name. Device name
+ # is used while making connection with master server,
# cloud-console recognize us with this name.
os.environ['BA_DEVICE_NAME'] = self._config.party_name
@@ -663,9 +699,10 @@ class ServerManagerApp:
assert self._subprocess_exited_cleanly is not None
- # EW: it seems that if we die before the main thread has fully started
- # up the interpreter, its possible that it will not break out of its
- # loop via the usual SystemExit that gets sent when we die.
+ # EW: it seems that if we die before the main thread has fully
+ # started up the interpreter, its possible that it will not
+ # break out of its loop via the usual SystemExit that gets sent
+ # when we die.
if self._interactive:
while (
self._interpreter_start_time is None
@@ -694,8 +731,8 @@ class ServerManagerApp:
# tell the main thread to die.
if self._wrapper_shutdown_desired:
# Only do this if the main thread is not already waiting for
- # us to die; otherwise it can lead to deadlock.
- # (we hang in os.kill while main thread is blocked in Thread.join)
+ # us to die; otherwise it can lead to deadlock. (we hang in
+ # os.kill while main thread is blocked in Thread.join)
if not self._done:
self._done = True
@@ -721,6 +758,8 @@ class ServerManagerApp:
bincfg['Auto Balance Teams'] = self._config.auto_balance_teams
bincfg['Show Tutorial'] = self._config.show_tutorial
+ if self._config.protocol_version is not None:
+ bincfg['SceneV1 Host Protocol'] = self._config.protocol_version
if self._config.team_names is not None:
bincfg['Custom Team Names'] = self._config.team_names
elif 'Custom Team Names' in bincfg:
@@ -769,8 +808,8 @@ class ServerManagerApp:
assert current_thread() is self._subprocess_thread
assert self._subprocess.stdin is not None
- # Send the initial server config which should kick things off.
- # (but make sure its values are still valid first)
+ # Send the initial server config which should kick things off
+ # (but make sure its values are still valid first).
dataclass_validate(self._config)
self._send_server_command(StartServerModeCommand(self._config))
@@ -782,8 +821,8 @@ class ServerManagerApp:
# Pass along any commands to our process.
with self._subprocess_commands_lock:
for incmd in self._subprocess_commands:
- # If we're passing a raw string to exec, no need to wrap it
- # in any proper structure.
+ # If we're passing a raw string to exec, no need to
+ # wrap it in any proper structure.
if isinstance(incmd, str):
self._subprocess.stdin.write((incmd + '\n').encode())
self._subprocess.stdin.flush()
@@ -794,9 +833,9 @@ class ServerManagerApp:
# Request restarts/shut-downs for various reasons.
self._request_shutdowns_or_restarts()
- # If they want to force-kill our subprocess, simply exit this
- # loop; the cleanup code will kill the process if its still
- # alive.
+ # If they want to force-kill our subprocess, simply exit
+ # this loop; the cleanup code will kill the process if its
+ # still alive.
if (
self._subprocess_force_kill_time is not None
and time.time() > self._subprocess_force_kill_time
@@ -855,8 +894,8 @@ class ServerManagerApp:
self.restart(immediate=True)
self._subprocess_sent_config_auto_restart = True
- # Attempt clean exit if our clean-exit-time passes.
- # (and enforce a 6 hour max if not provided)
+ # Attempt clean exit if our clean-exit-time passes (and enforce
+ # a 6 hour max if not provided).
clean_exit_minutes = 360.0
if self._config.clean_exit_minutes is not None:
clean_exit_minutes = min(
@@ -881,8 +920,8 @@ class ServerManagerApp:
self.shutdown(immediate=False)
self._subprocess_sent_clean_exit = True
- # Attempt unclean exit if our unclean-exit-time passes.
- # (and enforce a 7 hour max if not provided)
+ # Attempt unclean exit if our unclean-exit-time passes (and
+ # enforce a 7 hour max if not provided).
unclean_exit_minutes = 420.0
if self._config.unclean_exit_minutes is not None:
unclean_exit_minutes = min(
@@ -924,8 +963,8 @@ class ServerManagerApp:
print(f'{Clr.CYN}Stopping subprocess...{Clr.RST}', flush=True)
- # First, ask it nicely to die and give it a moment.
- # If that doesn't work, bring down the hammer.
+ # First, ask it nicely to die and give it a moment. If that
+ # doesn't work, bring down the hammer.
self._subprocess.terminate()
try:
self._subprocess.wait(timeout=10)
@@ -941,8 +980,9 @@ def main() -> None:
try:
ServerManagerApp().run()
except CleanError as exc:
- # For clean errors, do a simple print and fail; no tracebacks/etc.
- # Any others will bubble up and give us the usual mess.
+ # For clean errors, do a simple print and fail; no
+ # tracebacks/etc. Any others will bubble up and give us the
+ # usual mess.
exc.pretty_print()
sys.exit(1)
diff --git a/src/ballistica/base/app_adapter/app_adapter_apple.cc b/src/ballistica/base/app_adapter/app_adapter_apple.cc
index c5c55b03..13ebe5a8 100644
--- a/src/ballistica/base/app_adapter/app_adapter_apple.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_apple.cc
@@ -190,13 +190,13 @@ void AppAdapterApple::SetHardwareCursorVisible(bool visible) {
assert(g_core->InMainThread());
#if BA_OSTYPE_MACOS
- BallisticaKit::CocoaSupportSetCursorVisible(visible);
+ BallisticaKit::CocoaFromCppSetCursorVisible(visible);
#endif
}
void AppAdapterApple::TerminateApp() {
#if BA_OSTYPE_MACOS
- BallisticaKit::CocoaSupportTerminateApp();
+ BallisticaKit::CocoaFromCppTerminateApp();
#else
AppAdapter::TerminateApp();
#endif
diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.cc b/src/ballistica/base/app_adapter/app_adapter_sdl.cc
index 8a5a4655..75443228 100644
--- a/src/ballistica/base/app_adapter/app_adapter_sdl.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_sdl.cc
@@ -59,6 +59,7 @@ void AppAdapterSDL::OnMainThreadStartApp() {
"AppAdapterSDL strict_graphics_context_ is enabled."
" Remember to turn this off.");
}
+
// We may or may not want xinput on windows.
if (g_buildconfig.ostype_windows()) {
if (!g_core->platform->GetLowLevelConfigValue("enablexinput", 1)) {
@@ -66,6 +67,9 @@ void AppAdapterSDL::OnMainThreadStartApp() {
}
}
+ // We wrangle our own signal handling; don't bring SDL into it.
+ SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1");
+
int result = SDL_Init(sdl_flags);
if (result < 0) {
FatalError(std::string("SDL_Init failed: ") + SDL_GetError());
@@ -96,7 +100,7 @@ void AppAdapterSDL::OnMainThreadStartApp() {
}
}
- // We currently use a software cursor, so hide the system one.
+ // This adapter draws a software cursor; hide the actual OS one.
SDL_ShowCursor(SDL_DISABLE);
}
diff --git a/src/ballistica/base/app_mode/app_mode.cc b/src/ballistica/base/app_mode/app_mode.cc
index 268f2f52..5953f720 100644
--- a/src/ballistica/base/app_mode/app_mode.cc
+++ b/src/ballistica/base/app_mode/app_mode.cc
@@ -3,6 +3,7 @@
#include "ballistica/base/app_mode/app_mode.h"
#include "ballistica/base/input/device/input_device_delegate.h"
+#include "ballistica/base/logic/logic.h"
#include "ballistica/base/support/context.h"
namespace ballistica::base {
@@ -43,8 +44,8 @@ void AppMode::ChangeGameSpeed(int offs) {}
void AppMode::StepDisplayTime() {}
-auto AppMode::GetHeadlessDisplayStep() -> microsecs_t {
- return kAppModeMaxHeadlessDisplayStep;
+auto AppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t {
+ return kHeadlessMaxDisplayTimeStep;
}
auto AppMode::GetPartySize() const -> int { return 0; }
diff --git a/src/ballistica/base/app_mode/app_mode.h b/src/ballistica/base/app_mode/app_mode.h
index b98fa2a4..119f7659 100644
--- a/src/ballistica/base/app_mode/app_mode.h
+++ b/src/ballistica/base/app_mode/app_mode.h
@@ -9,15 +9,6 @@
namespace ballistica::base {
-/// The max amount of time a headless app can sleep if no events are pending.
-/// This should not be *too* high or it might cause delays when going from
-/// no events present to events present.
-const microsecs_t kAppModeMaxHeadlessDisplayStep{500000};
-
-/// The min amount of time a headless app can sleep. This provides an upper
-/// limit on stepping overhead in cases where events are densely packed.
-const microsecs_t kAppModeMinHeadlessDisplayStep{1000};
-
/// Represents 'what the app is doing'. The global app-mode can be switched
/// as the app is running. The Python layer has its own Python AppMode
/// classes, and generally when one of them becomes active it calls down
@@ -51,9 +42,9 @@ class AppMode {
/// Called right after stepping; should return the exact microseconds
/// between the current display time and the next event the app-mode has
/// scheduled. If no events are pending, should return
- /// kAppModeMaxHeadlessDisplayStep. This will only be called on headless
+ /// kHeadlessMaxDisplayTimeStep. This will only be called on headless
/// builds.
- virtual auto GetHeadlessDisplayStep() -> microsecs_t;
+ virtual auto GetHeadlessNextDisplayTimeStep() -> microsecs_t;
/// Create a delegate for an input-device.
/// Return a raw pointer allocated using Object::NewDeferred.
diff --git a/src/ballistica/base/audio/audio.cc b/src/ballistica/base/audio/audio.cc
index 8971b449..f7daba43 100644
--- a/src/ballistica/base/audio/audio.cc
+++ b/src/ballistica/base/audio/audio.cc
@@ -101,7 +101,7 @@ auto Audio::SourceBeginNew() -> AudioSource* {
#pragma clang diagnostic pop
auto Audio::IsSoundPlaying(uint32_t play_id) -> bool {
- uint32_t source_id = AudioServer::source_id_from_play_id(play_id);
+ uint32_t source_id = AudioServer::SourceIdFromPlayId(play_id);
assert(client_sources_.size() > source_id);
client_sources_[source_id]->Lock(2);
bool result = (client_sources_[source_id]->play_id() == play_id);
@@ -112,7 +112,7 @@ auto Audio::IsSoundPlaying(uint32_t play_id) -> bool {
auto Audio::SourceBeginExisting(uint32_t play_id, int debug_id)
-> AudioSource* {
BA_DEBUG_FUNCTION_TIMER_BEGIN();
- uint32_t source_id = AudioServer::source_id_from_play_id(play_id);
+ uint32_t source_id = AudioServer::SourceIdFromPlayId(play_id);
// Ok, the audio thread fills in this source list,
// so theoretically a client could call this before the audio thread
diff --git a/src/ballistica/base/audio/audio_server.cc b/src/ballistica/base/audio/audio_server.cc
index 5ba5a304..1e873026 100644
--- a/src/ballistica/base/audio/audio_server.cc
+++ b/src/ballistica/base/audio/audio_server.cc
@@ -2,6 +2,8 @@
#include "ballistica/base/audio/audio_server.h"
+#include
+
#include "ballistica/base/assets/assets.h"
#include "ballistica/base/assets/sound_asset.h"
#include "ballistica/base/audio/al_sys.h"
@@ -33,15 +35,14 @@ LPALCDEVICERESUMESOFT alcDeviceResumeSOFT;
const int kAudioProcessIntervalNormal{500};
const int kAudioProcessIntervalFade{50};
const int kAudioProcessIntervalPendingLoad{1};
-#if (BA_DEBUG_BUILD || BA_TEST_BUILD)
+
+#if BA_DEBUG_BUILD || BA_TEST_BUILD
const bool kShowInUseSounds{};
#endif
-int AudioServer::al_source_count_ = 0;
-
-struct AudioServer::Impl {
- Impl() = default;
- ~Impl() = default;
+struct AudioServer::Impl_ {
+ Impl_() = default;
+ ~Impl_() = default;
#if BA_ENABLE_AUDIO
ALCcontext* alc_context{};
@@ -49,14 +50,14 @@ struct AudioServer::Impl {
};
/// Location for sound emission (server version).
-class AudioServer::ThreadSource : public Object {
+class AudioServer::ThreadSource_ : public Object {
public:
// The id is returned as the lo-word of the identifier
// returned by "play". If valid is returned as false, there are no
// hardware channels available (or another error) and the source should
// not be used.
- ThreadSource(AudioServer* audio_thread, int id, bool* valid);
- ~ThreadSource() override;
+ ThreadSource_(AudioServer* audio_thread, int id, bool* valid);
+ ~ThreadSource_() override;
void Reset() {
SetIsMusic(false);
SetPositional(true);
@@ -101,6 +102,10 @@ class AudioServer::ThreadSource : public Object {
void ExecPlay();
void Update();
+ void CreateClientSource(int id) {
+ client_source_ = std::make_unique(id);
+ }
+
private:
bool looping_{};
std::unique_ptr client_source_;
@@ -127,11 +132,11 @@ class AudioServer::ThreadSource : public Object {
#if BA_ENABLE_AUDIO
Object::Ref streamer_;
#endif
-
- friend class AudioServer;
}; // ThreadSource
-AudioServer::AudioServer() : impl_{new AudioServer::Impl()} {}
+AudioServer::AudioServer() : impl_{std::make_unique()} {}
+
+AudioServer::~AudioServer() = default;
void AudioServer::OnMainThreadStartApp() {
// Spin up our thread.
@@ -142,21 +147,21 @@ void AudioServer::OnMainThreadStartApp() {
event_loop_->PushCall([this] {
// We want to be informed when our event-loop is pausing and unpausing.
event_loop()->AddSuspendCallback(
- NewLambdaRunnableUnmanaged([this] { OnThreadPause(); }));
+ NewLambdaRunnableUnmanaged([this] { OnThreadSuspend_(); }));
event_loop()->AddUnsuspendCallback(
- NewLambdaRunnableUnmanaged([this] { OnThreadResume(); }));
+ NewLambdaRunnableUnmanaged([this] { OnThreadUnsuspend_(); }));
});
- event_loop_->PushCallSynchronous([this] { OnAppStartInThread(); });
+ event_loop_->PushCallSynchronous([this] { OnAppStartInThread_(); });
}
-void AudioServer::OnAppStartInThread() {
+void AudioServer::OnAppStartInThread_() {
assert(g_base->InAudioThread());
// Get our thread to give us periodic processing time.
process_timer_ =
event_loop()->NewTimer(kAudioProcessIntervalNormal, true,
- NewLambdaRunnable([this] { Process(); }));
+ NewLambdaRunnable([this] { Process_(); }));
#if BA_ENABLE_AUDIO
@@ -235,10 +240,10 @@ void AudioServer::OnAppStartInThread() {
int target_source_count = 30;
for (int i = 0; i < target_source_count; i++) {
bool valid = false;
- auto s(Object::New(this, i, &valid));
+ auto s(Object::New(this, i, &valid));
if (valid) {
- s->client_source_ = std::make_unique(i);
- g_base->audio->AddClientSource(&(*s->client_source_));
+ s->CreateClientSource(i);
+ g_base->audio->AddClientSource(&(*s->client_source()));
sound_source_refs_.push_back(s);
sources_.push_back(&(*s));
} else {
@@ -250,47 +255,74 @@ void AudioServer::OnAppStartInThread() {
CHECK_AL_ERROR;
// Now make available any stopped sources (should be all of them).
- UpdateAvailableSources();
+ UpdateAvailableSources_();
#endif // BA_ENABLE_AUDIO
}
-AudioServer::~AudioServer() {
-#if BA_ENABLE_AUDIO
- sound_source_refs_.clear();
-
- // Take down AL stuff.
- {
- ALCdevice* device;
- BA_PRECONDITION_LOG(alcMakeContextCurrent(nullptr));
- device = alcGetContextsDevice(impl_->alc_context);
- alcDestroyContext(impl_->alc_context);
- assert(alcGetError(device) == ALC_NO_ERROR);
- alcCloseDevice(device);
+void AudioServer::Shutdown() {
+ BA_PRECONDITION(g_base->InAudioThread());
+ if (shutting_down_) {
+ return;
}
- assert(streaming_sources_.empty());
- assert(al_source_count_ == 0);
+ shutting_down_ = true;
+ shutdown_start_time_ = g_core->GetAppTimeSeconds();
-#endif // BA_ENABLE_AUDIO
- delete impl_;
+ // Stop all playing sounds and note the time. We'll then give everything a
+ // moment to come to a halt before we tear down the audio context to
+ // hopefully minimize errors/pops/etc.
+ for (auto&& i : sources_) {
+ i->Stop();
+ }
+ UpdateTimerInterval_();
}
-struct AudioServer::SoundFadeNode {
+void AudioServer::CompleteShutdown_() {
+ assert(g_base->InAudioThread());
+ assert(shutting_down_);
+ assert(!shutdown_completed_);
+
+#if BA_ENABLE_AUDIO
+ ALCboolean check = alcMakeContextCurrent(nullptr);
+ if (!check) {
+ Log(LogLevel::kWarning, "Error on alcMakeContextCurrent at shutdown.");
+ }
+ auto* device = alcGetContextsDevice(impl_->alc_context);
+ if (!device) {
+ Log(LogLevel::kWarning, "Unable to get ALCdevice at shutdown.");
+ } else {
+ alcDestroyContext(impl_->alc_context);
+ ALenum err = alcGetError(device);
+ if (err != ALC_NO_ERROR) {
+ Log(LogLevel::kWarning, "Error on AL shutdown.");
+ }
+ check = alcCloseDevice(device);
+ if (!check) {
+ Log(LogLevel::kWarning, "Error on alcCloseDevice at shutdown.");
+ }
+ }
+#endif
+
+ shutdown_completed_ = true;
+}
+
+struct AudioServer::SoundFadeNode_ {
uint32_t play_id;
millisecs_t starttime;
millisecs_t endtime;
bool out;
- SoundFadeNode(uint32_t play_id_in, millisecs_t duration_in, bool out_in)
+ SoundFadeNode_(uint32_t play_id_in, millisecs_t duration_in, bool out_in)
: play_id(play_id_in),
starttime(g_core->GetAppTimeMillisecs()),
endtime(g_core->GetAppTimeMillisecs() + duration_in),
out(out_in) {}
};
-void AudioServer::SetPaused(bool pause) {
- if (!paused_) {
- if (!pause) {
- Log(LogLevel::kError, "Got audio unpause request when already unpaused.");
+void AudioServer::SetSuspended_(bool suspend) {
+ if (!suspended_) {
+ if (!suspend) {
+ Log(LogLevel::kError,
+ "Got audio unsuspend request when already unsuspended.");
} else {
#if BA_OSTYPE_IOS_TVOS
// apple recommends this during audio-interruptions..
@@ -302,14 +334,15 @@ void AudioServer::SetPaused(bool pause) {
// On android lets tell open-sl to stop its processing.
#if BA_OSTYPE_ANDROID
alcDevicePauseSOFT(alcGetContextsDevice(impl_->alc_context));
-#endif // BA_OSTYPE_ANDROID
+#endif
- paused_ = true;
+ suspended_ = true;
}
} else {
- // unpause if requested..
- if (pause) {
- Log(LogLevel::kError, "Got audio pause request when already paused.");
+ // unsuspend if requested..
+ if (suspend) {
+ Log(LogLevel::kError,
+ "Got audio suspend request when already suspended.");
} else {
#if BA_OSTYPE_IOS_TVOS
// apple recommends this during audio-interruptions..
@@ -320,18 +353,18 @@ void AudioServer::SetPaused(bool pause) {
alcMakeContextCurrent(impl_->alc_context); // hmm is this necessary?..
#endif
#endif
+
// On android lets tell openal-soft to stop processing.
#if BA_OSTYPE_ANDROID
alcDeviceResumeSOFT(alcGetContextsDevice(impl_->alc_context));
-#endif // BA_OSTYPE_ANDROID
-
- paused_ = false;
+#endif
+ suspended_ = false;
#if BA_ENABLE_AUDIO
CHECK_AL_ERROR;
-#endif // BA_ENABLE_AUDIO
+#endif
- // Go through all of our sources and stop any we've wanted to stop while
- // paused.
+ // Go through all of our sources and stop any we've wanted to stop
+ // while we were suspended.
for (auto&& i : sources_) {
if ((!i->want_to_play()) && (i->is_actually_playing())) {
i->ExecStop();
@@ -343,7 +376,7 @@ void AudioServer::SetPaused(bool pause) {
void AudioServer::PushSourceSetIsMusicCall(uint32_t play_id, bool val) {
event_loop()->PushCall([this, play_id, val] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetIsMusic(val);
}
@@ -352,7 +385,7 @@ void AudioServer::PushSourceSetIsMusicCall(uint32_t play_id, bool val) {
void AudioServer::PushSourceSetPositionalCall(uint32_t play_id, bool val) {
event_loop()->PushCall([this, play_id, val] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetPositional(val);
}
@@ -362,7 +395,7 @@ void AudioServer::PushSourceSetPositionalCall(uint32_t play_id, bool val) {
void AudioServer::PushSourceSetPositionCall(uint32_t play_id,
const Vector3f& p) {
event_loop()->PushCall([this, play_id, p] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetPosition(p.x, p.y, p.z);
}
@@ -371,7 +404,7 @@ void AudioServer::PushSourceSetPositionCall(uint32_t play_id,
void AudioServer::PushSourceSetGainCall(uint32_t play_id, float val) {
event_loop()->PushCall([this, play_id, val] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetGain(val);
}
@@ -380,7 +413,7 @@ void AudioServer::PushSourceSetGainCall(uint32_t play_id, float val) {
void AudioServer::PushSourceSetFadeCall(uint32_t play_id, float val) {
event_loop()->PushCall([this, play_id, val] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetFade(val);
}
@@ -389,7 +422,7 @@ void AudioServer::PushSourceSetFadeCall(uint32_t play_id, float val) {
void AudioServer::PushSourceSetLoopingCall(uint32_t play_id, bool val) {
event_loop()->PushCall([this, play_id, val] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetLooping(val);
}
@@ -399,7 +432,7 @@ void AudioServer::PushSourceSetLoopingCall(uint32_t play_id, bool val) {
void AudioServer::PushSourcePlayCall(uint32_t play_id,
Object::Ref* sound) {
event_loop()->PushCall([this, play_id, sound] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
// If this play command is valid, pass it along.
// Otherwise, return it immediately for deletion.
@@ -413,13 +446,13 @@ void AudioServer::PushSourcePlayCall(uint32_t play_id,
// This way the more things clients are playing, the more
// tight our source availability checking gets (instead of solely relying on
// our periodic process() calls).
- UpdateAvailableSources();
+ UpdateAvailableSources_();
});
}
void AudioServer::PushSourceStopCall(uint32_t play_id) {
event_loop()->PushCall([this, play_id] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->Stop();
}
@@ -428,7 +461,7 @@ void AudioServer::PushSourceStopCall(uint32_t play_id) {
void AudioServer::PushSourceEndCall(uint32_t play_id) {
event_loop()->PushCall([this, play_id] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
assert(s);
s->client_source()->Lock(5);
s->client_source()->set_client_queue_size(
@@ -439,13 +472,13 @@ void AudioServer::PushSourceEndCall(uint32_t play_id) {
}
void AudioServer::PushResetCall() {
- event_loop()->PushCall([this] { Reset(); });
+ event_loop()->PushCall([this] { Reset_(); });
}
void AudioServer::PushSetListenerPositionCall(const Vector3f& p) {
event_loop()->PushCall([this, p] {
#if BA_ENABLE_AUDIO
- if (!paused_) {
+ if (!suspended_ && !shutting_down_) {
ALfloat lpos[3] = {p.x, p.y, p.z};
alListenerfv(AL_POSITION, lpos);
CHECK_AL_ERROR;
@@ -458,7 +491,7 @@ void AudioServer::PushSetListenerOrientationCall(const Vector3f& forward,
const Vector3f& up) {
event_loop()->PushCall([this, forward, up] {
#if BA_ENABLE_AUDIO
- if (!paused_) {
+ if (!suspended_ && !shutting_down_) {
ALfloat lorient[6] = {forward.x, forward.y, forward.z, up.x, up.y, up.z};
alListenerfv(AL_ORIENTATION, lorient);
CHECK_AL_ERROR;
@@ -467,7 +500,7 @@ void AudioServer::PushSetListenerOrientationCall(const Vector3f& forward,
});
}
-void AudioServer::UpdateAvailableSources() {
+void AudioServer::UpdateAvailableSources_() {
for (auto&& i : sources_) {
i->UpdateAvailability();
}
@@ -525,17 +558,19 @@ void AudioServer::UpdateAvailableSources() {
}
void AudioServer::StopSound(uint32_t play_id) {
- uint32_t source = source_id_from_play_id(play_id);
- uint32_t count = play_count_from_play_id(play_id);
+ uint32_t source = SourceIdFromPlayId(play_id);
+ uint32_t count = PlayCountFromPlayId(play_id);
if (source < sources_.size()) {
- if (count == sources_[source]->play_count()) sources_[source]->Stop();
+ if (count == sources_[source]->play_count()) {
+ sources_[source]->Stop();
+ }
}
}
-auto AudioServer::GetPlayingSound(uint32_t play_id)
- -> AudioServer::ThreadSource* {
- uint32_t source = source_id_from_play_id(play_id);
- uint32_t count = play_count_from_play_id(play_id);
+auto AudioServer::GetPlayingSound_(uint32_t play_id)
+ -> AudioServer::ThreadSource_* {
+ uint32_t source = SourceIdFromPlayId(play_id);
+ uint32_t count = PlayCountFromPlayId(play_id);
assert(source < sources_.size());
if (source < sources_.size()) {
// If the sound has finished playing or whatnot, we
@@ -551,9 +586,10 @@ auto AudioServer::GetPlayingSound(uint32_t play_id)
return nullptr;
}
-void AudioServer::UpdateTimerInterval() {
- // If we've got pending loads, go into uber-hyperactive mode.
- if (have_pending_loads_) {
+void AudioServer::UpdateTimerInterval_() {
+ // If we've got pending loads or are shutting down, go into
+ // uber-hyperactive mode.
+ if (have_pending_loads_ || shutting_down_) {
assert(process_timer_);
process_timer_->SetLength(kAudioProcessIntervalPendingLoad);
} else {
@@ -563,48 +599,40 @@ void AudioServer::UpdateTimerInterval() {
assert(process_timer_);
process_timer_->SetLength(kAudioProcessIntervalFade);
} else {
- // Nothing but normal activity; just run enough to keep
- // buffers filled and whatnot.
+ // Nothing but normal activity; just run often enough to keep buffers
+ // filled and whatnot.
assert(process_timer_);
process_timer_->SetLength(kAudioProcessIntervalNormal);
}
}
}
-void AudioServer::SetSoundPitch(float pitch) {
- sound_pitch_ = pitch;
- if (sound_pitch_ < 0.01f) sound_pitch_ = 0.01f;
+void AudioServer::SetSoundPitch_(float pitch) {
+ sound_pitch_ = std::clamp(pitch, 0.1f, 10.0f);
for (auto&& i : sources_) {
i->UpdatePitch();
}
}
-void AudioServer::SetSoundVolume(float volume) {
- sound_volume_ = volume;
- if (sound_volume_ > 3.0f) {
- sound_volume_ = 3.0f;
- }
- if (sound_volume_ < 0) {
- sound_volume_ = 0;
- }
+void AudioServer::SetSoundVolume_(float volume) {
+ sound_volume_ = std::clamp(volume, 0.0f, 3.0f);
for (auto&& i : sources_) {
i->UpdateVolume();
}
}
-void AudioServer::SetMusicVolume(float volume) {
- music_volume_ = volume;
- if (music_volume_ > 3.0f) music_volume_ = 3.0f;
- if (music_volume_ < 0) music_volume_ = 0;
- UpdateMusicPlayState();
+void AudioServer::SetMusicVolume_(float volume) {
+ music_volume_ = std::clamp(volume, 0.0f, 3.0f);
+ UpdateMusicPlayState_();
for (auto&& i : sources_) {
i->UpdateVolume();
}
}
-// Start or stop music playback based on volume/pause-state/etc.
-void AudioServer::UpdateMusicPlayState() {
- bool should_be_playing = ((music_volume_ > 0.000001f) && !paused_);
+// Start or stop music playback based on volume/suspend-state/etc.
+void AudioServer::UpdateMusicPlayState_() {
+ bool should_be_playing =
+ (music_volume_ > 0.000001f && !suspended_ && !shutting_down_);
// Flip any playing music off.
if (!should_be_playing) {
@@ -624,21 +652,21 @@ void AudioServer::UpdateMusicPlayState() {
}
}
-void AudioServer::Process() {
+void AudioServer::Process_() {
assert(g_base->InAudioThread());
millisecs_t real_time = g_core->GetAppTimeMillisecs();
- // If we're paused we don't do nothin'.
- if (!paused_) {
+ // If we're suspended we don't do nothin'.
+ if (!suspended_ && !shutting_down_) {
// Do some loading...
have_pending_loads_ = g_base->assets->RunPendingAudioLoads();
// Keep that available-sources list filled.
- UpdateAvailableSources();
+ UpdateAvailableSources_();
// Update our fading sound volumes.
if (real_time - last_sound_fade_process_time_ > 50) {
- ProcessSoundFades();
+ ProcessSoundFades_();
last_sound_fade_process_time_ = real_time;
}
@@ -649,14 +677,25 @@ void AudioServer::Process() {
i->Update();
}
}
+
#if BA_ENABLE_AUDIO
CHECK_AL_ERROR;
#endif
}
- UpdateTimerInterval();
+ UpdateTimerInterval_();
+
+ // In my brief unscientific testing with my airpods, a 0.2 second delay
+ // between stopping sounds and killing the sound-system seems to be enough
+ // for the mixer to spit out some silence so we don't hear sudden cut-offs
+ // in one or both ears.
+ if (shutting_down_ && !shutdown_completed_) {
+ if (g_core->GetAppTimeSeconds() - shutdown_start_time_ > 0.2) {
+ CompleteShutdown_();
+ }
+ }
}
-void AudioServer::Reset() {
+void AudioServer::Reset_() {
// Note: up until version 1.7.20, the audio server would stop all playing
// sounds when reset. This would prevent against long sounds playing at
// the end of a game session 'bleeding' into the main menu/etc. However,
@@ -665,10 +704,11 @@ void AudioServer::Reset() {
// growing. In particular, a 'power down' sound at launch when a plugin is
// no longer found is being cut off by the initial app-mode switch.
- // So disabling the stop behavior for now and hoping that doesn't bite us.
- // Ideally we should have sounds contexts so that we can stop sounds for
- // a particular scene when that scene ends/etc. This would also fix our
- // current problem where epic mode screws up the pitch on our UI sounds.
+ // So I'm disabling the stop behavior for now and hoping that doesn't bite
+ // us. Ideally we should have sounds contexts so that we can stop sounds
+ // for a particular scene when that scene ends/etc. This could also
+ // address our current problem where epic mode screws up the pitch on our
+ // UI sounds.
if (explicit_bool(false)) {
// Stop all playing sounds.
@@ -677,17 +717,17 @@ void AudioServer::Reset() {
}
}
// Still need to reset this though or epic-mode will screw us up.
- SetSoundPitch(1.0f);
+ SetSoundPitch_(1.0f);
}
-void AudioServer::ProcessSoundFades() {
+void AudioServer::ProcessSoundFades_() {
auto i = sound_fade_nodes_.begin();
decltype(i) i_next;
while (i != sound_fade_nodes_.end()) {
i_next = i;
i_next++;
- AudioServer::ThreadSource* s = GetPlayingSound(i->second.play_id);
+ AudioServer::ThreadSource_* s = GetPlayingSound_(i->second.play_id);
if (s) {
if (g_core->GetAppTimeMillisecs() > i->second.endtime) {
StopSound(i->second.play_id);
@@ -710,17 +750,17 @@ void AudioServer::ProcessSoundFades() {
void AudioServer::FadeSoundOut(uint32_t play_id, uint32_t time) {
// Pop a new node on the list (this won't overwrite the old if there is one).
sound_fade_nodes_.insert(
- std::make_pair(play_id, SoundFadeNode(play_id, time, true)));
+ std::make_pair(play_id, SoundFadeNode_(play_id, time, true)));
}
-void AudioServer::DeleteAssetComponent(Asset* c) {
- assert(g_base->InAudioThread());
- c->Unload();
- delete c;
-}
+// void AudioServer::DeleteAssetComponent_(Asset* c) {
+// assert(g_base->InAudioThread());
+// c->Unload();
+// delete c;
+// }
-AudioServer::ThreadSource::ThreadSource(AudioServer* audio_thread_in, int id_in,
- bool* valid_out)
+AudioServer::ThreadSource_::ThreadSource_(AudioServer* audio_thread_in,
+ int id_in, bool* valid_out)
: id_(id_in), audio_thread_(audio_thread_in) {
#if BA_ENABLE_AUDIO
assert(g_core);
@@ -751,13 +791,13 @@ AudioServer::ThreadSource::ThreadSource(AudioServer* audio_thread_in, int id_in,
}
*valid_out = valid_;
if (valid_) {
- al_source_count_++;
+ g_base->audio_server->al_source_count_++;
}
#endif // BA_ENABLE_AUDIO
}
-AudioServer::ThreadSource::~ThreadSource() {
+AudioServer::ThreadSource_::~ThreadSource_() {
#if BA_ENABLE_AUDIO
if (!valid_) {
@@ -779,16 +819,16 @@ AudioServer::ThreadSource::~ThreadSource() {
alDeleteSources(1, &source_);
CHECK_AL_ERROR;
- al_source_count_--;
+ g_base->audio_server->al_source_count_--;
#endif // BA_ENABLE_AUDIO
}
-auto AudioServer::ThreadSource::GetDefaultOwnerThread() const -> EventLoopID {
+auto AudioServer::ThreadSource_::GetDefaultOwnerThread() const -> EventLoopID {
return EventLoopID::kAudio;
}
-void AudioServer::ThreadSource::UpdateAvailability() {
+void AudioServer::ThreadSource_::UpdateAvailability() {
#if BA_ENABLE_AUDIO
assert(g_base->InAudioThread());
@@ -814,9 +854,10 @@ void AudioServer::ThreadSource::UpdateAvailability() {
if (looping_ || (is_streamed_ && streamer_.Exists() && streamer_->loops())) {
busy = want_to_play_;
} else {
- // If our context is paused, we know nothing is playing
+ // If our context is suspended, we know nothing is playing
// (and we can't ask AL cuz we have no context).
- if (g_base->audio_server->paused()) {
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
busy = false;
} else {
ALint state;
@@ -827,8 +868,9 @@ void AudioServer::ThreadSource::UpdateAvailability() {
}
// Ok, now if we can get a lock on the availability list, go ahead and
- // make this guy available; give him a new play id and reset his state.
- // If we can't get a lock it's no biggie... we'll come back to this guy later.
+ // make this guy available; give him a new play id and reset his state. If
+ // we can't get a lock it's no biggie... we'll come back to this guy
+ // later.
if (!busy) {
if (g_base->audio->available_sources_mutex().try_lock()) {
@@ -850,86 +892,144 @@ void AudioServer::ThreadSource::UpdateAvailability() {
#endif // BA_ENABLE_AUDIO
}
-void AudioServer::ThreadSource::Update() {
+void AudioServer::ThreadSource_::Update() {
#if BA_ENABLE_AUDIO
assert(is_streamed_ && is_actually_playing_);
streamer_->Update();
#endif
}
-void AudioServer::ThreadSource::SetIsMusic(bool m) { is_music_ = m; }
+void AudioServer::ThreadSource_::SetIsMusic(bool m) { is_music_ = m; }
-void AudioServer::ThreadSource::SetGain(float g) {
+void AudioServer::ThreadSource_::SetGain(float g) {
gain_ = g;
UpdateVolume();
}
-void AudioServer::ThreadSource::SetFade(float f) {
+void AudioServer::ThreadSource_::SetFade(float f) {
fade_ = f;
UpdateVolume();
}
-void AudioServer::ThreadSource::SetLooping(bool loop) {
+void AudioServer::ThreadSource_::SetLooping(bool loop) {
looping_ = loop;
- if (!g_base->audio_server->paused()) {
#if BA_ENABLE_AUDIO
- alSourcei(source_, AL_LOOPING, loop);
- CHECK_AL_ERROR;
-#endif
- }
-}
-
-void AudioServer::ThreadSource::SetPositional(bool p) {
-#if BA_ENABLE_AUDIO
- if (!g_base->audio_server->paused()) {
- // TODO(ericf): Don't allow setting of positional
- // on stereo sounds - we check this at initial play()
- // but should do it here too.
- alSourcei(source_, AL_SOURCE_RELATIVE, !p);
- CHECK_AL_ERROR;
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
+ return;
}
+ alSourcei(source_, AL_LOOPING, loop);
+ CHECK_AL_ERROR;
#endif
}
-void AudioServer::ThreadSource::SetPosition(float x, float y, float z) {
+void AudioServer::ThreadSource_::SetPositional(bool p) {
#if BA_ENABLE_AUDIO
- if (!g_base->audio_server->paused()) {
- bool oob = false;
- if (x < -500) {
- oob = true;
- x = -500;
- } else if (x > 500) {
- oob = true;
- x = 500;
- }
- if (y < -500) {
- oob = true;
- y = -500;
- } else if (y > 500) {
- oob = true;
- y = 500;
- }
- if (z < -500) {
- oob = true;
- z = -500;
- } else if (z > 500) {
- oob = true;
- z = 500;
- }
- if (oob) {
- BA_LOG_ONCE(LogLevel::kError,
- "AudioServer::ThreadSource::SetPosition"
- " got out-of-bounds value.");
- }
- ALfloat source_pos[] = {x, y, z};
- alSourcefv(source_, AL_POSITION, source_pos);
- CHECK_AL_ERROR;
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
+ return;
}
+ // TODO(ericf): Don't allow setting of positional
+ // on stereo sounds - we check this at initial play()
+ // but should do it here too.
+ alSourcei(source_, AL_SOURCE_RELATIVE, !p);
+ CHECK_AL_ERROR;
+
+#endif
+}
+
+void AudioServer::ThreadSource_::SetPosition(float x, float y, float z) {
+#if BA_ENABLE_AUDIO
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
+ return;
+ }
+ bool oob = false;
+ if (x < -500) {
+ oob = true;
+ x = -500;
+ } else if (x > 500) {
+ oob = true;
+ x = 500;
+ }
+ if (y < -500) {
+ oob = true;
+ y = -500;
+ } else if (y > 500) {
+ oob = true;
+ y = 500;
+ }
+ if (z < -500) {
+ oob = true;
+ z = -500;
+ } else if (z > 500) {
+ oob = true;
+ z = 500;
+ }
+ if (oob) {
+ BA_LOG_ONCE(LogLevel::kError,
+ "AudioServer::ThreadSource::SetPosition"
+ " got out-of-bounds value.");
+ }
+ ALfloat source_pos[] = {x, y, z};
+ alSourcefv(source_, AL_POSITION, source_pos);
+ CHECK_AL_ERROR;
+
#endif // BA_ENABLE_AUDIO
}
+auto AudioServer::ThreadSource_::Play(const Object::Ref* sound)
+ -> uint32_t {
+#if BA_ENABLE_AUDIO
+
+ assert(g_base->InAudioThread());
+ assert(sound->Exists());
+
+ // Stop whatever we were doing.
+ Stop();
+
+ assert(source_sound_ == nullptr);
+ source_sound_ = sound;
+
+ if (!g_base->audio_server->suspended_
+ && !g_base->audio_server->shutting_down_) {
+ // Ok, here's where we might start needing to access our media... can't
+ // hold off any longer...
+ (**source_sound_).Load();
+
+ is_streamed_ = (**source_sound_).is_streamed();
+ current_is_music_ = is_music_;
+
+ if (is_streamed_) {
+ streamer_ = Object::New(
+ (**source_sound_).file_name_full().c_str(), source_, looping_);
+ } else {
+ alSourcei(source_, AL_BUFFER,
+ static_cast((**source_sound_).buffer()));
+ }
+ CHECK_AL_ERROR;
+
+ // Always update our volume and pitch here (we may be changing from
+ // music to nonMusic, etc.)
+ UpdateVolume();
+ UpdatePitch();
+
+ bool music_should_play = ((g_base->audio_server->music_volume_ > 0.000001f)
+ && !g_base->audio_server->suspended_
+ && !g_base->audio_server->shutting_down_);
+ if ((!current_is_music_) || music_should_play) {
+ ExecPlay();
+ }
+ }
+ want_to_play_ = true;
+
+#endif // BA_ENABLE_AUDIO
+
+ return play_id();
+}
+
// Actually begin playback.
-void AudioServer::ThreadSource::ExecPlay() {
+void AudioServer::ThreadSource_::ExecPlay() {
#if BA_ENABLE_AUDIO
assert(g_core);
@@ -992,60 +1092,39 @@ void AudioServer::ThreadSource::ExecPlay() {
#endif // BA_ENABLE_AUDIO
}
-auto AudioServer::ThreadSource::Play(const Object::Ref* sound)
- -> uint32_t {
+// Do a complete stop... take us off the music list, detach our source, etc.
+void AudioServer::ThreadSource_::Stop() {
#if BA_ENABLE_AUDIO
+ assert(g_base->audio_server);
- // FatalError("Testing other thread.");
-
- assert(g_base->InAudioThread());
- assert(sound->Exists());
-
- // Stop whatever we were doing.
- Stop();
-
- assert(source_sound_ == nullptr);
- source_sound_ = sound;
-
- if (!g_base->audio_server->paused()) {
- // Ok, here's where we might start needing to access our media... can't hold
- // off any longer...
- (**source_sound_).Load();
-
- is_streamed_ = (**source_sound_).is_streamed();
- current_is_music_ = is_music_;
-
- if (is_streamed_) {
- streamer_ = Object::New(
- (**source_sound_).file_name_full().c_str(), source_, looping_);
- } else {
- alSourcei(source_, AL_BUFFER,
- static_cast((**source_sound_).buffer()));
+ // If our context is suspended we can't actually stop now; just record our
+ // intent.
+ if (g_base->audio_server->suspended_) {
+ want_to_play_ = false;
+ } else {
+ if (is_actually_playing_) {
+ ExecStop();
}
- CHECK_AL_ERROR;
-
- // Always update our volume and pitch here (we may be changing from music to
- // nonMusic, etc.)
- UpdateVolume();
- UpdatePitch();
-
- bool music_should_play = ((g_base->audio_server->music_volume_ > 0.000001f)
- && !g_base->audio_server->paused());
- if ((!current_is_music_) || music_should_play) {
- ExecPlay();
+ if (streamer_.Exists()) {
+ streamer_.Clear();
}
+ // If we've got an attached sound, toss it back to the main thread
+ // to free up...
+ // (we can't kill media-refs outside the main thread)
+ if (source_sound_) {
+ assert(g_base->assets);
+ g_base->audio_server->AddSoundRefDelete(source_sound_);
+ source_sound_ = nullptr;
+ }
+ want_to_play_ = false;
}
- want_to_play_ = true;
-
#endif // BA_ENABLE_AUDIO
-
- return play_id();
}
-void AudioServer::ThreadSource::ExecStop() {
+void AudioServer::ThreadSource_::ExecStop() {
#if BA_ENABLE_AUDIO
assert(g_base->InAudioThread());
- assert(!g_base->audio_server->paused());
+ assert(!g_base->audio_server->suspended_);
assert(is_actually_playing_);
if (streamer_.Exists()) {
assert(is_streamed_);
@@ -1067,83 +1146,63 @@ void AudioServer::ThreadSource::ExecStop() {
#endif // BA_ENABLE_AUDIO
}
-// Do a complete stop... take us off the music list, detach our source, etc.
-void AudioServer::ThreadSource::Stop() {
+void AudioServer::ThreadSource_::UpdateVolume() {
#if BA_ENABLE_AUDIO
- assert(g_base->audio_server);
-
- // If our context is paused we can't actually stop now; just record our
- // intent.
- if (g_base->audio_server->paused()) {
- want_to_play_ = false;
+ assert(g_base->InAudioThread());
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
+ return;
+ }
+ float val = gain_ * fade_;
+ if (current_is_music()) {
+ val *= audio_thread_->music_volume_ / 7.0f;
} else {
- if (is_actually_playing_) ExecStop();
- if (streamer_.Exists()) {
- streamer_.Clear();
- }
- // If we've got an attached sound, toss it back to the main thread
- // to free up...
- // (we can't kill media-refs outside the main thread)
- if (source_sound_) {
- assert(g_base->assets);
- g_base->audio_server->AddSoundRefDelete(source_sound_);
- source_sound_ = nullptr;
- }
- want_to_play_ = false;
+ val *= audio_thread_->sound_volume_;
}
+ alSourcef(source_, AL_GAIN, std::max(0.0f, val));
+ CHECK_AL_ERROR;
+
#endif // BA_ENABLE_AUDIO
}
-void AudioServer::ThreadSource::UpdateVolume() {
+void AudioServer::ThreadSource_::UpdatePitch() {
#if BA_ENABLE_AUDIO
assert(g_base->InAudioThread());
- if (!g_base->audio_server->paused()) {
- float val = gain_ * fade_;
- if (current_is_music()) {
- val *= audio_thread_->music_volume() / 7.0f;
- } else {
- val *= audio_thread_->sound_volume();
- }
- alSourcef(source_, AL_GAIN, std::max(0.0f, val));
- CHECK_AL_ERROR;
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
+ return;
}
-#endif // BA_ENABLE_AUDIO
-}
+ float val = 1.0f;
+ if (current_is_music()) {
+ } else {
+ val *= audio_thread_->sound_pitch_;
+ }
+ alSourcef(source_, AL_PITCH, val);
+ CHECK_AL_ERROR;
-void AudioServer::ThreadSource::UpdatePitch() {
-#if BA_ENABLE_AUDIO
- assert(g_base->InAudioThread());
- if (!g_base->audio_server->paused()) {
- float val = 1.0f;
- if (current_is_music()) {
- } else {
- val *= audio_thread_->sound_pitch();
- }
- alSourcef(source_, AL_PITCH, val);
- CHECK_AL_ERROR;
- }
#endif // BA_ENABLE_AUDIO
}
void AudioServer::PushSetVolumesCall(float music_volume, float sound_volume) {
event_loop()->PushCall([this, music_volume, sound_volume] {
- SetSoundVolume(sound_volume);
- SetMusicVolume(music_volume);
+ SetSoundVolume_(sound_volume);
+ SetMusicVolume_(music_volume);
});
}
void AudioServer::PushSetSoundPitchCall(float val) {
- event_loop()->PushCall([this, val] { SetSoundPitch(val); });
+ event_loop()->PushCall([this, val] { SetSoundPitch_(val); });
}
-void AudioServer::PushSetSuspendedCall(bool pause) {
- event_loop()->PushCall([this, pause] {
- if (g_buildconfig.ostype_android()) {
- Log(LogLevel::kError, "Shouldn't be getting SetPausedCall on android.");
- }
- SetPaused(pause);
- });
-}
+// void AudioServer::PushSetSuspendedCall(bool suspend) {
+// event_loop()->PushCall([this, suspend] {
+// if (g_buildconfig.ostype_android()) {
+// Log(LogLevel::kError, "Shouldn't be getting SetSuspendedCall on
+// android.");
+// }
+// SetSuspended_(suspend);
+// });
+// }
void AudioServer::PushComponentUnloadCall(
const std::vector*>& components) {
@@ -1164,7 +1223,7 @@ void AudioServer::PushComponentUnloadCall(
void AudioServer::PushHavePendingLoadsCall() {
event_loop()->PushCall([this] {
have_pending_loads_ = true;
- UpdateTimerInterval();
+ UpdateTimerInterval_();
});
}
@@ -1187,44 +1246,44 @@ void AudioServer::ClearSoundRefDeleteList() {
sound_ref_delete_list_.clear();
}
-void AudioServer::BeginInterruption() {
- assert(!g_base->InAudioThread());
- g_base->audio_server->PushSetSuspendedCall(true);
+// void AudioServer::BeginInterruption() {
+// assert(!g_base->InAudioThread());
+// g_base->audio_server->PushSetSuspendedCall(true);
- // Wait a reasonable amount of time for the thread to act on it.
- millisecs_t t = g_core->GetAppTimeMillisecs();
- while (true) {
- if (g_base->audio_server->paused()) {
- break;
- }
- if (g_core->GetAppTimeMillisecs() - t > 1000) {
- Log(LogLevel::kError, "Timed out waiting for audio pause.");
- break;
- }
- core::CorePlatform::SleepMillisecs(2);
- }
-}
+// // Wait a reasonable amount of time for the thread to act on it.
+// millisecs_t t = g_core->GetAppTimeMillisecs();
+// while (true) {
+// if (g_base->audio_server->suspended()) {
+// break;
+// }
+// if (g_core->GetAppTimeMillisecs() - t > 1000) {
+// Log(LogLevel::kError, "Timed out waiting for audio suspend.");
+// break;
+// }
+// core::CorePlatform::SleepMillisecs(2);
+// }
+// }
-void AudioServer::OnThreadPause() { SetPaused(true); }
+// void AudioServer::EndInterruption() {
+// assert(!g_base->InAudioThread());
+// g_base->audio_server->PushSetSuspendedCall(false);
-void AudioServer::OnThreadResume() { SetPaused(false); }
+// // Wait a reasonable amount of time for the thread to act on it.
+// millisecs_t t = g_core->GetAppTimeMillisecs();
+// while (true) {
+// if (!g_base->audio_server->suspended()) {
+// break;
+// }
+// if (g_core->GetAppTimeMillisecs() - t > 1000) {
+// Log(LogLevel::kError, "Timed out waiting for audio unsuspend.");
+// break;
+// }
+// core::CorePlatform::SleepMillisecs(2);
+// }
+// }
-void AudioServer::EndInterruption() {
- assert(!g_base->InAudioThread());
- g_base->audio_server->PushSetSuspendedCall(false);
+void AudioServer::OnThreadSuspend_() { SetSuspended_(true); }
- // Wait a reasonable amount of time for the thread to act on it.
- millisecs_t t = g_core->GetAppTimeMillisecs();
- while (true) {
- if (!g_base->audio_server->paused()) {
- break;
- }
- if (g_core->GetAppTimeMillisecs() - t > 1000) {
- Log(LogLevel::kError, "Timed out waiting for audio unpause.");
- break;
- }
- core::CorePlatform::SleepMillisecs(2);
- }
-}
+void AudioServer::OnThreadUnsuspend_() { SetSuspended_(false); }
} // namespace ballistica::base
diff --git a/src/ballistica/base/audio/audio_server.h b/src/ballistica/base/audio/audio_server.h
index 5c5202ec..a47bf68a 100644
--- a/src/ballistica/base/audio/audio_server.h
+++ b/src/ballistica/base/audio/audio_server.h
@@ -12,14 +12,14 @@
namespace ballistica::base {
-/// A module that handles audio processing.
+/// Wrangles audio off in its own thread.
class AudioServer {
public:
- static auto source_id_from_play_id(uint32_t play_id) -> uint32_t {
+ static auto SourceIdFromPlayId(uint32_t play_id) -> uint32_t {
return play_id & 0xFFFFu;
}
- static auto play_count_from_play_id(uint32_t play_id) -> uint32_t {
+ static auto PlayCountFromPlayId(uint32_t play_id) -> uint32_t {
return play_id >> 16u;
}
@@ -28,10 +28,9 @@ class AudioServer {
void PushSetVolumesCall(float music_volume, float sound_volume);
void PushSetSoundPitchCall(float val);
- void PushSetSuspendedCall(bool pause);
- static void BeginInterruption();
- static void EndInterruption();
+ // static void BeginInterruption();
+ // static void EndInterruption();
void PushSetListenerPositionCall(const Vector3f& p);
void PushSetListenerOrientationCall(const Vector3f& forward,
@@ -41,10 +40,12 @@ class AudioServer {
void PushComponentUnloadCall(
const std::vector*>& components);
- /// For use by g_logic_module().
void ClearSoundRefDeleteList();
- auto paused() const -> bool { return paused_; }
+ auto paused() const -> bool { return suspended_; }
+
+ void Shutdown();
+ auto shutdown_completed() const { return shutdown_completed_; }
// Client sources use these to pass settings to the server.
void PushSourceSetIsMusicCall(uint32_t play_id, bool val);
@@ -67,37 +68,36 @@ class AudioServer {
auto event_loop() const -> EventLoop* { return event_loop_; }
private:
- class ThreadSource;
- struct Impl;
+ class ThreadSource_;
+ struct Impl_;
- void OnAppStartInThread();
+ void OnAppStartInThread_();
~AudioServer();
- void OnThreadPause();
- void OnThreadResume();
+ void OnThreadSuspend_();
+ void OnThreadUnsuspend_();
- void SetPaused(bool paused);
+ void SetSuspended_(bool suspended);
- void SetMusicVolume(float volume);
- void SetSoundVolume(float volume);
- void SetSoundPitch(float pitch);
- auto music_volume() -> float { return music_volume_; }
- auto sound_volume() -> float { return sound_volume_; }
- auto sound_pitch() -> float { return sound_pitch_; }
+ void SetMusicVolume_(float volume);
+ void SetSoundVolume_(float volume);
+ void SetSoundPitch_(float pitch);
+
+ void CompleteShutdown_();
/// If a sound play id is currently playing, return the sound.
- auto GetPlayingSound(uint32_t play_id) -> ThreadSource*;
+ auto GetPlayingSound_(uint32_t play_id) -> ThreadSource_*;
- void Reset();
- void Process();
+ void Reset_();
+ void Process_();
/// Send a component to the audio thread to delete.
- void DeleteAssetComponent(Asset* c);
+ // void DeleteAssetComponent_(Asset* c);
- void UpdateTimerInterval();
- void UpdateAvailableSources();
- void UpdateMusicPlayState();
- void ProcessSoundFades();
+ void UpdateTimerInterval_();
+ void UpdateAvailableSources_();
+ void UpdateMusicPlayState_();
+ void ProcessSoundFades_();
// Some threads such as audio hold onto allocated Media-Component-Refs to keep
// media components alive that they need. Media-Component-Refs, however, must
@@ -107,32 +107,36 @@ class AudioServer {
// Note: should use unique_ptr for this, but build fails on raspberry pi
// (gcc 8.3.0). Works on Ubuntu 9.3 so should try again later.
- // std::unique_ptr impl_{};
- Impl* impl_{};
+ std::unique_ptr impl_{};
+ // Impl* impl_{};
EventLoop* event_loop_{};
Timer* process_timer_{};
- bool have_pending_loads_{};
- bool paused_{};
- millisecs_t last_sound_fade_process_time_{};
-
float sound_volume_{1.0f};
float sound_pitch_{1.0f};
float music_volume_{1.0f};
+ bool have_pending_loads_ : 1 {};
+ bool suspended_ : 1 {};
+ bool shutdown_completed_ : 1 {};
+ bool shutting_down_ : 1 {};
+ seconds_t shutdown_start_time_{};
+ millisecs_t last_sound_fade_process_time_{};
+
/// Indexed list of sources.
- std::vector sources_;
- std::vector streaming_sources_;
+ std::vector sources_;
+ std::vector streaming_sources_;
millisecs_t last_stream_process_time_{};
+ millisecs_t last_sanity_check_time_{};
// Holds refs to all sources.
// Use sources, not this, for faster iterating.
- std::vector > sound_source_refs_;
- struct SoundFadeNode;
+ std::vector> sound_source_refs_;
+ struct SoundFadeNode_;
// NOTE: would use unordered_map here but gcc doesn't seem to allow
// forward-declared template params with them.
- std::map sound_fade_nodes_;
+ std::map sound_fade_nodes_;
// This mutex controls access to our list of media component shared ptrs to
// delete in the main thread.
@@ -141,9 +145,7 @@ class AudioServer {
// Our list of sound media components to delete via the main thread.
std::vector*> sound_ref_delete_list_;
- millisecs_t last_sanity_check_time_{};
-
- static int al_source_count_;
+ int al_source_count_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/audio/audio_source.cc b/src/ballistica/base/audio/audio_source.cc
index 541d6d89..07d0bbdf 100644
--- a/src/ballistica/base/audio/audio_source.cc
+++ b/src/ballistica/base/audio/audio_source.cc
@@ -15,7 +15,7 @@ AudioSource::AudioSource(int id_in) : id_(id_in) {}
AudioSource::~AudioSource() { assert(client_queue_size_ == 0); }
void AudioSource::MakeAvailable(uint32_t play_id_new) {
- assert(AudioServer::source_id_from_play_id(play_id_new) == id_);
+ assert(AudioServer::SourceIdFromPlayId(play_id_new) == id_);
assert(client_queue_size_ == 0);
assert(locked());
play_id_ = play_id_new;
diff --git a/src/ballistica/base/audio/audio_streamer.cc b/src/ballistica/base/audio/audio_streamer.cc
index 60e03fa5..bc6f1488 100644
--- a/src/ballistica/base/audio/audio_streamer.cc
+++ b/src/ballistica/base/audio/audio_streamer.cc
@@ -68,7 +68,9 @@ void AudioStreamer::Stop() {
}
void AudioStreamer::Update() {
- if (eof_) return;
+ if (eof_) {
+ return;
+ }
CHECK_AL_ERROR;
diff --git a/src/ballistica/base/audio/audio_streamer.h b/src/ballistica/base/audio/audio_streamer.h
index e4104fa6..934a5ee6 100644
--- a/src/ballistica/base/audio/audio_streamer.h
+++ b/src/ballistica/base/audio/audio_streamer.h
@@ -23,17 +23,18 @@ class AudioStreamer : public Object {
auto Play() -> bool;
void Stop();
void Update();
- enum Format { INVALID_FORMAT, MONO16_FORMAT, STEREO16_FORMAT };
+ enum class Format : uint8_t { kInvalid, kMono16, kStereo16 };
auto al_format() const -> ALenum {
switch (format_) {
- case MONO16_FORMAT:
+ case Format::kMono16:
return AL_FORMAT_MONO16;
- case STEREO16_FORMAT:
+ case Format::kStereo16:
return AL_FORMAT_STEREO16;
default:
break;
}
- return INVALID_FORMAT;
+ FatalError("Invalid AL format.");
+ return AL_FORMAT_MONO16;
}
auto loops() const -> bool { return loops_; }
auto file_name() const -> const std::string& { return file_name_; }
@@ -46,13 +47,13 @@ class AudioStreamer : public Object {
void set_format(Format format) { format_ = format; }
private:
- Format format_ = INVALID_FORMAT;
- bool playing_ = false;
+ Format format_{Format::kInvalid};
+ bool playing_ : 1 {};
+ bool loops_ : 1 {};
+ bool eof_ : 1 {};
ALuint buffers_[kAudioStreamBufferCount]{};
- ALuint source_ = 0;
+ ALuint source_{};
std::string file_name_;
- bool loops_ = false;
- bool eof_ = false;
};
#endif // BA_ENABLE_AUDIO
diff --git a/src/ballistica/base/audio/ogg_stream.cc b/src/ballistica/base/audio/ogg_stream.cc
index abe45317..5b51d427 100644
--- a/src/ballistica/base/audio/ogg_stream.cc
+++ b/src/ballistica/base/audio/ogg_stream.cc
@@ -53,9 +53,9 @@ OggStream::OggStream(const char* file_name, ALuint source, bool loop)
vorbis_info_ = ov_info(&ogg_file_, -1);
if (vorbis_info_->channels == 1) {
- set_format(MONO16_FORMAT);
+ set_format(Format::kMono16);
} else {
- set_format(STEREO16_FORMAT);
+ set_format(Format::kStereo16);
}
}
diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc
index dcedd3c3..822b02fd 100644
--- a/src/ballistica/base/base.cc
+++ b/src/ballistica/base/base.cc
@@ -197,9 +197,9 @@ void BaseFeatureSet::StartApp() {
assets_server->OnMainThreadStartApp();
app_adapter->OnMainThreadStartApp();
- // Take note that we're now 'running'. Various code such as anything that
- // pushes messages to threads can watch for this state to avoid crashing
- // if called early.
+ // Ok; we're now official 'started'. Various code such as anything that
+ // pushes messages to threads can watch for this state (via IsAppStarted()
+ // to avoid crashing if called early.
app_started_ = true;
// Inform anyone who wants to know that we're done starting.
diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h
index b45aa0b6..c2bd3a43 100644
--- a/src/ballistica/base/base.h
+++ b/src/ballistica/base/base.h
@@ -603,14 +603,13 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// Start app systems in motion.
void StartApp() override;
- /// Issue a high level app quit request. Can be called from any thread.
- /// 'soft' means the app can simply reset/hide itself instead of actually
- /// exiting the process (common behavior on mobile platforms). 'back'
- /// means that a soft-quit should behave as if a back-button was pressed,
- /// which may trigger different behavior in the OS than a standard soft
- /// quit. If 'confirm' is true, a confirmation dialog will be presented if
- /// the current app-mode provides one and the app is in gui mode.
- /// Otherwise the quit will be immediate.
+ /// Issue a high level app quit request. Can be called from any thread and
+ /// can be safely called repeatedly. If 'confirm' is true, a confirmation
+ /// dialog will be presented if the environment and situation allows;
+ /// otherwise the quit will be immediate. A QuitType arg can optionally be
+ /// passed to influence quit behavior; on some platforms such as mobile
+ /// the default is for the app to recede to the background but physically
+ /// remain running.
void QuitApp(bool confirm = false, QuitType quit_type = QuitType::kSoft);
/// Called when app shutdown process completes. Sets app to exit.
diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc
index c2e873ef..5582a189 100644
--- a/src/ballistica/base/graphics/graphics.cc
+++ b/src/ballistica/base/graphics/graphics.cc
@@ -336,6 +336,7 @@ class Graphics::ScreenMessageEntry {
float v_smoothed{};
bool translation_dirty{true};
bool mesh_dirty{true};
+ millisecs_t smooth_time{};
private:
Object::Ref s_mesh_;
@@ -594,13 +595,21 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
{
auto xf = c.ScopedTransform();
- if (i->v_smoothed == 0.0f) {
- i->v_smoothed = v + v_extra;
- } else {
- float smoothing = 0.8f;
- i->v_smoothed = smoothing * i->v_smoothed
- + (1.0f - smoothing) * (v + v_extra);
+ // This logic needs to run at a fixed hz or it breaks on high frame
+ // rates.
+ auto now_millisecs = pass->frame_def()->display_time_millisecs();
+ i->smooth_time = std::max(i->smooth_time, now_millisecs - 100);
+ while (i->smooth_time < now_millisecs) {
+ i->smooth_time += 1000 / 60;
+ if (i->v_smoothed == 0.0f) {
+ i->v_smoothed = v + v_extra;
+ } else {
+ float smoothing = 0.8f;
+ i->v_smoothed = smoothing * i->v_smoothed
+ + (1.0f - smoothing) * (v + v_extra);
+ }
}
+
c.Translate(screen_width * 0.5f, i->v_smoothed,
vr ? 60 : kScreenMessageZDepth);
if (vr) {
@@ -767,10 +776,17 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
a = 1;
}
- i->v_smoothed += 0.1f;
- if (i->v_smoothed - last_v < min_spacing) {
- i->v_smoothed +=
- 8.0f * (1.0f - ((i->v_smoothed - last_v) / min_spacing));
+ // This logic needs to run at a fixed hz or it breaks on high frame
+ // rates.
+ auto now_millisecs = pass->frame_def()->display_time_millisecs();
+ i->smooth_time = std::max(i->smooth_time, now_millisecs - 100);
+ while (i->smooth_time < now_millisecs) {
+ i->smooth_time += 1000 / 60;
+ i->v_smoothed += 0.1f;
+ if (i->v_smoothed - last_v < min_spacing) {
+ i->v_smoothed +=
+ 8.0f * (1.0f - ((i->v_smoothed - last_v) / min_spacing));
+ }
}
last_v = i->v_smoothed;
@@ -864,11 +880,11 @@ void Graphics::GetSafeColor(float* red, float* green, float* blue,
*blue = std::min(1.0f, (*blue) * s);
}
- // We may still be short of our target intensity due to clamping (ie: (10,0,0)
- // will not look any brighter than (1,0,0)) if that's the case, just convert
- // the difference to a grey value and add that to all channels... this *still*
- // might not get us there so lets do it a few times if need be. (i'm sure
- // there's a less bone-headed way to do this)
+ // We may still be short of our target intensity due to clamping (ie:
+ // (10,0,0) will not look any brighter than (1,0,0)) if that's the case,
+ // just convert the difference to a grey value and add that to all
+ // channels... this *still* might not get us there so lets do it a few times
+ // if need be. (i'm sure there's a less bone-headed way to do this)
for (int i = 0; i < 4; i++) {
float remaining =
(0.2989f * (*red) + 0.5870f * (*green) + 0.1140f * (*blue)) - 1.0f;
@@ -1050,14 +1066,13 @@ void Graphics::UpdateGyro(microsecs_t time_microsecs,
tilt_vel_ = tilt_smoothed_ * 3.0f;
tilt_pos_ += tilt_vel_ * timescale;
- // Technically this will behave slightly differently at different time scales,
- // but it should be close to correct..
- // tilt_pos_ *= 0.991f;
+ // Technically this will behave slightly differently at different time
+ // scales, but it should be close to correct.. tilt_pos_ *= 0.991f;
tilt_pos_ *= std::max(0.0f, 1.0f - 0.01f * timescale);
// Some gyros seem wonky and either give us crazy big values or consistently
- // offset ones. Let's keep a running tally of magnitude that slowly drops over
- // time, and if it reaches a certain value lets just kill gyro input.
+ // offset ones. Let's keep a running tally of magnitude that slowly drops
+ // over time, and if it reaches a certain value lets just kill gyro input.
if (gyro_broken_) {
tilt_pos_ *= 0.0f;
} else {
@@ -1485,9 +1500,9 @@ void Graphics::DrawCursor(FrameDef* frame_def) {
{
auto xf = c.ScopedTransform();
- // Note: we don't plug in known cursor position values here; we tell the
- // renderer to insert the latest values on its end; this can lessen
- // cursor lag substantially.
+ // Note: we don't plug in known cursor position values here; we tell
+ // the renderer to insert the latest values on its end; this can
+ // lessen cursor lag substantially.
c.CursorTranslate();
c.Translate(csize * 0.40f, csize * -0.38f, kCursorZDepth);
c.Scale(csize, csize);
diff --git a/src/ballistica/base/logic/logic.cc b/src/ballistica/base/logic/logic.cc
index e56450e5..f752c635 100644
--- a/src/ballistica/base/logic/logic.cc
+++ b/src/ballistica/base/logic/logic.cc
@@ -99,7 +99,7 @@ void Logic::OnGraphicsReady() {
// Anyone dealing in display-time should be able to handle a wide
// variety of rates anyway. NOTE: This length is currently milliseconds.
headless_display_time_step_timer_ = event_loop()->NewTimer(
- kAppModeMinHeadlessDisplayStep / 1000, true,
+ kHeadlessMinDisplayTimeStep / 1000, true,
NewLambdaRunnable([this] { StepDisplayTime_(); }));
} else {
// In gui mode, push an initial frame to the graphics server. From this
@@ -382,7 +382,7 @@ void Logic::OnAppModeChanged() {
}
assert(headless_display_time_step_timer_);
// NOTE: This is currently milliseconds.
- headless_display_time_step_timer_->SetLength(kAppModeMinHeadlessDisplayStep
+ headless_display_time_step_timer_->SetLength(kHeadlessMinDisplayTimeStep
/ 1000);
}
}
@@ -424,9 +424,9 @@ void Logic::PostUpdateDisplayTimeForHeadlessMode_() {
// we've got until the next event. We'll plug this into our display-update
// timer so we can try to sleep exactly until that point.
auto headless_display_step_microsecs =
- std::max(std::min(g_base->app_mode()->GetHeadlessDisplayStep(),
- kAppModeMaxHeadlessDisplayStep),
- kAppModeMinHeadlessDisplayStep);
+ std::max(std::min(g_base->app_mode()->GetHeadlessNextDisplayTimeStep(),
+ kHeadlessMaxDisplayTimeStep),
+ kHeadlessMinDisplayTimeStep);
if (debug_log_display_time_) {
auto sleepsecs =
diff --git a/src/ballistica/base/logic/logic.h b/src/ballistica/base/logic/logic.h
index 55439274..18852d62 100644
--- a/src/ballistica/base/logic/logic.h
+++ b/src/ballistica/base/logic/logic.h
@@ -13,6 +13,15 @@ namespace ballistica::base {
const int kDisplayTimeSampleCount{15};
+/// The max amount of time a headless app can sleep if no events are pending.
+/// This should not be *too* high or it might cause delays when going from
+/// no events present to events present.
+const microsecs_t kHeadlessMaxDisplayTimeStep{500000};
+
+/// The min amount of time a headless app can sleep. This provides an upper
+/// limit on stepping overhead in cases where events are densely packed.
+const microsecs_t kHeadlessMinDisplayTimeStep{1000};
+
/// The logic subsystem of the app. This runs on a dedicated thread and is
/// where most high level app logic happens. Much app functionality
/// including UI calls must be run on the logic thread.
@@ -63,8 +72,9 @@ class Logic {
/// graphical builds we also use this opportunity to step our logic.
void Draw();
- /// Kick off an app shutdown. Shutdown is an asynchronous process which
- /// may take a bit of time to complete. Safe to call repeatedly.
+ /// Kick off a low level app shutdown. Shutdown is an asynchronous process
+ /// which may take up to a few seconds to complete. This is safe to call
+ /// repeatedly but must be called from the logic thread.
void Shutdown();
/// Should be called by the Python layer when it has completed all
diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc
index 4b583e29..94792ad4 100644
--- a/src/ballistica/base/python/methods/python_methods_app.cc
+++ b/src/ballistica/base/python/methods/python_methods_app.cc
@@ -4,6 +4,7 @@
#include "ballistica/base/app_adapter/app_adapter.h"
#include "ballistica/base/app_mode/app_mode_empty.h"
+#include "ballistica/base/audio/audio_server.h"
#include "ballistica/base/graphics/graphics_server.h"
#include "ballistica/base/logic/logic.h"
#include "ballistica/base/python/base_python.h"
@@ -729,11 +730,8 @@ static auto PyEnv(PyObject* self) -> PyObject* {
"ss" // ui_scale
"sO" // on_tv
"sO" // vr_mode
- // "sO" // toolbar_test
"sO" // demo_mode
"sO" // arcade_mode
- // "sO" // iircade_mode
- // "si" // protocol_version
"sO" // headless_mode
"sO" // python_directory_app_site
"ss" // device_name
@@ -757,8 +755,6 @@ static auto PyEnv(PyObject* self) -> PyObject* {
"vr_mode", g_core->IsVRMode() ? Py_True : Py_False,
"demo_mode", g_buildconfig.demo_build() ? Py_True : Py_False,
"arcade_mode", g_buildconfig.arcade_build() ? Py_True : Py_False,
- // "iircade_mode", g_buildconfig.iircade_build() ? Py_True: Py_False,
- // "protocol_version", kProtocolVersion,
"headless_mode", g_core->HeadlessMode() ? Py_True : Py_False,
"python_directory_app_site",
site_py_dir ? *PythonRef::FromString(*site_py_dir) : Py_None,
@@ -1603,6 +1599,52 @@ static PyMethodDef PyDevConsoleInputAdapterFinishDef = {
"(internal)\n",
};
+// -------------------------- audio_shutdown_begin -----------------------------
+
+static auto PyAudioShutdownBegin(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+
+ auto* audio_event_loop = g_base->audio_server->event_loop();
+ BA_PRECONDITION(audio_event_loop);
+ audio_event_loop->PushCall([] { g_base->audio_server->Shutdown(); });
+ Py_RETURN_NONE;
+
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyAudioShutdownBeginDef = {
+ "audio_shutdown_begin", // name
+ (PyCFunction)PyAudioShutdownBegin, // method
+ METH_NOARGS, // flags
+
+ "audio_shutdown_begin() -> None\n"
+ "\n"
+ "(internal)\n",
+};
+
+// ----------------------- audio_shutdown_is_complete --------------------------
+
+static auto PyAudioShutdownIsComplete(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+
+ if (g_base->audio_server->shutdown_completed()) {
+ Py_RETURN_TRUE;
+ }
+ Py_RETURN_FALSE;
+
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyAudioShutdownIsCompleteDef = {
+ "audio_shutdown_is_complete", // name
+ (PyCFunction)PyAudioShutdownIsComplete, // method
+ METH_NOARGS, // flags
+
+ "audio_shutdown_is_complete() -> bool\n"
+ "\n"
+ "(internal)\n",
+};
+
// -----------------------------------------------------------------------------
auto PythonMethodsApp::GetMethods() -> std::vector {
@@ -1658,6 +1700,8 @@ auto PythonMethodsApp::GetMethods() -> std::vector {
PyGetDevConsoleInputTextDef,
PySetDevConsoleInputTextDef,
PyDevConsoleInputAdapterFinishDef,
+ PyAudioShutdownBeginDef,
+ PyAudioShutdownIsCompleteDef,
};
}
diff --git a/src/ballistica/base/support/app_config.cc b/src/ballistica/base/support/app_config.cc
index ab3b5815..7fdc7957 100644
--- a/src/ballistica/base/support/app_config.cc
+++ b/src/ballistica/base/support/app_config.cc
@@ -208,6 +208,8 @@ void AppConfig::SetupEntries() {
int_entries_[IntID::kPort] = IntEntry("Port", kDefaultPort);
int_entries_[IntID::kMaxFPS] = IntEntry("Max FPS", 60);
+ int_entries_[IntID::kSceneV1HostProtocol] =
+ IntEntry("SceneV1 Host Protocol", 33);
bool_entries_[BoolID::kTouchControlsSwipeHidden] =
BoolEntry("Touch Controls Swipe Hidden", false);
diff --git a/src/ballistica/base/support/app_config.h b/src/ballistica/base/support/app_config.h
index e8057d5b..9381c49e 100644
--- a/src/ballistica/base/support/app_config.h
+++ b/src/ballistica/base/support/app_config.h
@@ -55,6 +55,7 @@ class AppConfig {
enum class IntID {
kPort,
kMaxFPS,
+ kSceneV1HostProtocol,
kLast // Sentinel.
};
diff --git a/src/ballistica/base/ui/ui.cc b/src/ballistica/base/ui/ui.cc
index 0b774b9c..34ce797d 100644
--- a/src/ballistica/base/ui/ui.cc
+++ b/src/ballistica/base/ui/ui.cc
@@ -42,7 +42,7 @@ UI::UI() {
// VR and TV modes always use medium.
scale_ = UIScale::kMedium;
} else {
- scale_ = g_core->platform->GetUIScale();
+ scale_ = g_core->platform->GetDefaultUIScale();
}
}
}
diff --git a/src/ballistica/core/platform/apple/core_platform_apple.cc b/src/ballistica/core/platform/apple/core_platform_apple.cc
index 5320a8e5..b227b817 100644
--- a/src/ballistica/core/platform/apple/core_platform_apple.cc
+++ b/src/ballistica/core/platform/apple/core_platform_apple.cc
@@ -4,8 +4,10 @@
#include "ballistica/core/platform/apple/core_platform_apple.h"
#if BA_XCODE_BUILD
+#include
#include
#endif
+
#include
#if BA_XCODE_BUILD
@@ -102,7 +104,7 @@ auto CorePlatformApple::DoGetConfigDirectoryMonolithicDefault()
auto CorePlatformApple::GetLocale() -> std::string {
#if BA_XCODE_BUILD
- return base::AppleUtils::GetLocaleString();
+ return BallisticaKit::FromCppGetLocaleString();
#else
return CorePlatform::GetLocale();
#endif
@@ -124,7 +126,7 @@ auto CorePlatformApple::DoHasTouchScreen() -> bool {
#endif
}
-auto CorePlatformApple::GetUIScale() -> UIScale {
+auto CorePlatformApple::GetDefaultUIScale() -> UIScale {
#if BA_OSTYPE_IOS
if (base::AppleUtils::IsTablet()) {
return UIScale::kMedium;
@@ -132,8 +134,8 @@ auto CorePlatformApple::GetUIScale() -> UIScale {
return UIScale::kSmall;
}
#else
- // default case handles mac/tvos
- return CorePlatform::GetUIScale();
+ // Default case handles mac & tvos.
+ return CorePlatform::GetDefaultUIScale();
#endif
}
@@ -160,9 +162,8 @@ void CorePlatformApple::DisplayLog(const std::string& name, LogLevel level,
}
auto CorePlatformApple::DoGetDataDirectoryMonolithicDefault() -> std::string {
-#if BA_XCODE_BUILD && !BA_HEADLESS_BUILD
- // On Apple package-y builds use our resources dir.
- return base::AppleUtils::GetResourcesPath();
+#if BA_XCODE_BUILD
+ return BallisticaKit::FromCppGetResourcesPath();
#else
// Fall back to default.
return CorePlatform::DoGetDataDirectoryMonolithicDefault();
@@ -292,13 +293,8 @@ void CorePlatformApple::OpenFileExternally(const std::string& path) {
}
void CorePlatformApple::OpenDirExternally(const std::string& path) {
-#if BA_OSTYPE_MACOS
- std::string cmd = std::string("open \"") + path + "\"";
- int result = system(cmd.c_str());
- if (result != 0) {
- Log(LogLevel::kError, "Got return value " + std::to_string(result)
- + " on open cmd '" + cmd + "'");
- }
+#if BA_OSTYPE_MACOS && BA_XCODE_BUILD
+ BallisticaKit::CocoaFromCppOpenDirExternally(path);
#else
CorePlatform::OpenDirExternally(path);
#endif
@@ -380,7 +376,7 @@ auto CorePlatformApple::DoClipboardIsSupported() -> bool {
return base::AppleUtils::ClipboardIsSupported();
#else
return CorePlatform::DoClipboardIsSupported();
-#endif // BA_XCODE_BUILD
+#endif
}
auto CorePlatformApple::DoClipboardHasText() -> bool {
@@ -388,7 +384,7 @@ auto CorePlatformApple::DoClipboardHasText() -> bool {
return base::AppleUtils::ClipboardHasText();
#else
return CorePlatform::DoClipboardHasText();
-#endif // BA_XCODE_BUILD
+#endif
}
void CorePlatformApple::DoClipboardSetText(const std::string& text) {
@@ -396,7 +392,7 @@ void CorePlatformApple::DoClipboardSetText(const std::string& text) {
base::AppleUtils::ClipboardSetText(text);
#else
CorePlatform::DoClipboardSetText(text);
-#endif // BA_XCODE_BUILD
+#endif
}
auto CorePlatformApple::DoClipboardGetText() -> std::string {
@@ -404,7 +400,7 @@ auto CorePlatformApple::DoClipboardGetText() -> std::string {
return base::AppleUtils::ClipboardGetText();
#else
return CorePlatform::DoClipboardGetText();
-#endif // BA_XCODE_BUILD
+#endif
}
} // namespace ballistica::core
diff --git a/src/ballistica/core/platform/apple/core_platform_apple.h b/src/ballistica/core/platform/apple/core_platform_apple.h
index 6e8d337f..4198f7e8 100644
--- a/src/ballistica/core/platform/apple/core_platform_apple.h
+++ b/src/ballistica/core/platform/apple/core_platform_apple.h
@@ -24,7 +24,7 @@ class CorePlatformApple : public CorePlatform {
auto GetLocale() -> std::string override;
auto DoGetDeviceName() -> std::string override;
auto DoHasTouchScreen() -> bool override;
- auto GetUIScale() -> UIScale override;
+ auto GetDefaultUIScale() -> UIScale override;
auto IsRunningOnDesktop() -> bool override;
void DisplayLog(const std::string& name, LogLevel level,
const std::string& msg) override;
diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc
index b28976d3..338d451d 100644
--- a/src/ballistica/core/platform/core_platform.cc
+++ b/src/ballistica/core/platform/core_platform.cc
@@ -476,7 +476,7 @@ void CorePlatform::SleepMicrosecs(millisecs_t ms) {
#pragma clang diagnostic push
#pragma ide diagnostic ignored "NullDereferences"
-auto CorePlatform::GetUIScale() -> UIScale {
+auto CorePlatform::GetDefaultUIScale() -> UIScale {
// Handles mac/pc/linux cases.
return UIScale::kLarge;
}
diff --git a/src/ballistica/core/platform/core_platform.h b/src/ballistica/core/platform/core_platform.h
index f01e9940..df05291a 100644
--- a/src/ballistica/core/platform/core_platform.h
+++ b/src/ballistica/core/platform/core_platform.h
@@ -97,26 +97,22 @@ class CorePlatform {
#pragma mark PRINTING/LOGGING --------------------------------------------------
/// Display a message to any default log for the platform (android log,
- /// etc.) Note that this can be called from any thread.
+ /// etc.) Note that this can be called from any thread. Default
+ /// implementation does nothing.
virtual void DisplayLog(const std::string& name, LogLevel level,
const std::string& msg);
#pragma mark ENVIRONMENT -------------------------------------------------------
- // Return a simple name for the platform: 'mac', 'windows', 'linux', etc.
+ /// Return a simple name for the platform: 'mac', 'windows', 'linux', etc.
virtual auto GetPlatformName() -> std::string;
- // Return a simple name for the subplatform: 'amazon', 'google', etc.
+ /// Return a simple name for the subplatform: 'amazon', 'google', etc.
virtual auto GetSubplatformName() -> std::string;
- // Are we running in event-push-mode? With this on, we return from Main()
- // and the system handles the event loop. With it off, we loop in Main()
- // ourself.
- // virtual auto IsEventPushMode() -> bool;
-
/// Return the interface type based on the environment (phone, tablet,
/// etc).
- virtual auto GetUIScale() -> UIScale;
+ virtual auto GetDefaultUIScale() -> UIScale;
/// Return default DataDirectory value for monolithic builds.
auto GetDataDirectoryMonolithicDefault() -> std::string;
@@ -387,7 +383,7 @@ class CorePlatform {
static void SleepMillisecs(millisecs_t ms);
- static void SleepMicrosecs(millisecs_t ms);
+ static void SleepMicrosecs(microsecs_t ms);
/// Given a C++ symbol, attempt to return a pretty one.
virtual auto DemangleCXXSymbol(const std::string& s) -> std::string;
diff --git a/src/ballistica/scene_v1/connection/connection_to_client.cc b/src/ballistica/scene_v1/connection/connection_to_client.cc
index 876e50e7..61e4c061 100644
--- a/src/ballistica/scene_v1/connection/connection_to_client.cc
+++ b/src/ballistica/scene_v1/connection/connection_to_client.cc
@@ -24,7 +24,10 @@ namespace ballistica::scene_v1 {
// How long new clients have to wait before starting a kick vote.
const int kNewClientKickVoteDelay = 60000;
-ConnectionToClient::ConnectionToClient(int id) : id_(id) {
+ConnectionToClient::ConnectionToClient(int id)
+ : id_(id),
+ protocol_version_{
+ SceneV1AppMode::GetSingleton()->host_protocol_version()} {
// We calc this once just in case it changes on our end
// (the client uses it for their verification hash so we need to
// ensure it stays consistent).
@@ -33,7 +36,7 @@ ConnectionToClient::ConnectionToClient(int id) : id_(id) {
// On newer protocols we include an extra salt value
// to ensure the hash the client generates can't be recycled.
- if (explicit_bool(kProtocolVersion >= 33)) {
+ if (explicit_bool(protocol_version() >= 33)) {
our_handshake_salt_ = std::to_string(rand()); // NOLINT
}
}
@@ -95,7 +98,7 @@ void ConnectionToClient::Update() {
// In newer protocols we embed a json dict as the second part of the
// handshake packet; this way we can evolve the protocol more
// easily in the future.
- if (explicit_bool(kProtocolVersion >= 33)) {
+ if (explicit_bool(protocol_version() >= 33)) {
// Construct a json dict with our player-spec-string as one element.
JsonDict dict;
dict.AddString("s", our_handshake_player_spec_str_);
@@ -106,17 +109,17 @@ void ConnectionToClient::Update() {
std::string out = dict.PrintUnformatted();
std::vector data(3 + out.size());
data[0] = BA_SCENEPACKET_HANDSHAKE;
- uint16_t val = kProtocolVersion;
+ uint16_t val = protocol_version();
memcpy(data.data() + 1, &val, sizeof(val));
memcpy(data.data() + 3, out.c_str(), out.size());
SendGamePacket(data);
} else {
- // (KILL THIS WHEN kProtocolVersionMin >= 33)
+ // (KILL THIS WHEN kProtocolVersionClientMin >= 33)
// on older protocols, we simply embedded our spec-string as the second
// part of the handshake packet
std::vector data(3 + our_handshake_player_spec_str_.size());
data[0] = BA_SCENEPACKET_HANDSHAKE;
- uint16_t val = kProtocolVersion;
+ uint16_t val = protocol_version();
memcpy(data.data() + 1, &val, sizeof(val));
memcpy(data.data() + 3, our_handshake_player_spec_str_.c_str(),
our_handshake_player_spec_str_.size());
@@ -155,7 +158,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) {
// In newer builds we expect to be sent a json dict here;
// pull client's spec from that.
- if (explicit_bool(kProtocolVersion >= 33)) {
+ if (protocol_version() >= 33) {
std::vector string_buffer(data.size() - 3 + 1);
memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3);
string_buffer[string_buffer.size() - 1] = 0;
@@ -173,7 +176,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) {
cJSON_Delete(handshake);
}
} else {
- // (KILL THIS WHEN kProtocolVersionMin >= 33)
+ // (KILL THIS WHEN kProtocolVersionClientMin >= 33)
// older versions only contained the client spec
// pull client's spec from the handshake packet..
std::vector string_buffer(data.size() - 3 + 1);
@@ -195,7 +198,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) {
// Bytes 2 and 3 are their protocol version.
uint16_t val;
memcpy(&val, data.data() + 1, sizeof(val));
- if (val != kProtocolVersion) {
+ if (val != protocol_version()) {
// Depending on the connection type we may print the connection
// failure or not. (If we invited them it'd be good to know about the
// failure).
diff --git a/src/ballistica/scene_v1/connection/connection_to_client.h b/src/ballistica/scene_v1/connection/connection_to_client.h
index bf27b1af..63050f20 100644
--- a/src/ballistica/scene_v1/connection/connection_to_client.h
+++ b/src/ballistica/scene_v1/connection/connection_to_client.h
@@ -55,10 +55,17 @@ class ConnectionToClient : public Connection {
// or their peer name if they have no players.
auto GetCombinedSpec() -> PlayerSpec;
+ auto protocol_version() const {
+ assert(protocol_version_ != -1);
+ return protocol_version_;
+ }
+
private:
virtual auto ShouldPrintIncompatibleClientErrors() const -> bool;
auto GetClientInputDevice(int remote_id) -> ClientInputDevice*;
void Error(const std::string& error_msg) override;
+
+ int protocol_version_;
std::string our_handshake_player_spec_str_;
std::string our_handshake_salt_;
std::string peer_public_account_id_;
diff --git a/src/ballistica/scene_v1/connection/connection_to_host.cc b/src/ballistica/scene_v1/connection/connection_to_host.cc
index 0e9e3f7e..93503988 100644
--- a/src/ballistica/scene_v1/connection/connection_to_host.cc
+++ b/src/ballistica/scene_v1/connection/connection_to_host.cc
@@ -22,7 +22,9 @@ namespace ballistica::scene_v1 {
// How long to go between sending out null packets for pings.
const int kPingSendInterval = 2000;
-ConnectionToHost::ConnectionToHost() = default;
+ConnectionToHost::ConnectionToHost()
+ : protocol_version_{
+ SceneV1AppMode::GetSingleton()->host_protocol_version()} {}
auto ConnectionToHost::GetAsUDP() -> ConnectionToHostUDP* { return nullptr; }
@@ -103,8 +105,8 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) {
uint16_t their_protocol_version;
memcpy(&their_protocol_version, data.data() + 1,
sizeof(their_protocol_version));
- if (their_protocol_version >= kProtocolVersionMin
- && their_protocol_version <= kProtocolVersion) {
+ if (their_protocol_version >= kProtocolVersionClientMin
+ && their_protocol_version <= kProtocolVersionMax) {
compatible = true;
// If we are compatible, set our protocol version to match
@@ -136,7 +138,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) {
memcpy(data2.data() + 3, out.c_str(), out.size());
SendGamePacket(data2);
} else {
- // (KILL THIS WHEN kProtocolVersionMin >= 33)
+ // (KILL THIS WHEN kProtocolVersionClientMin >= 33)
std::string our_spec_str =
PlayerSpec::GetAccountPlayerSpec().GetSpecString();
std::vector response(3 + our_spec_str.size());
@@ -148,7 +150,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) {
}
if (!compatible) {
- if (their_protocol_version > kProtocolVersion) {
+ if (their_protocol_version > protocol_version()) {
Error(g_base->assets->GetResourceString(
"incompatibleNewerVersionHostText"));
} else {
@@ -183,7 +185,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) {
cJSON_Delete(handshake);
}
} else {
- // (KILL THIS WHEN kProtocolVersionMin >= 33)
+ // (KILL THIS WHEN kProtocolVersionClientMin >= 33)
// In older protocols, handshake simply contained a
// player-spec for the host.
diff --git a/src/ballistica/scene_v1/connection/connection_to_host.h b/src/ballistica/scene_v1/connection/connection_to_host.h
index 978e8da1..56669055 100644
--- a/src/ballistica/scene_v1/connection/connection_to_host.h
+++ b/src/ballistica/scene_v1/connection/connection_to_host.h
@@ -33,12 +33,12 @@ class ConnectionToHost : public Connection {
std::string party_name_;
std::string peer_hash_input_;
std::string peer_hash_;
- bool printed_connect_message_ = false;
- int protocol_version_ = kProtocolVersion;
- int build_number_ = 0;
- bool got_host_info_ = false;
- // can remove once back-compat protocol is > 29
- bool ignore_old_attach_remote_player_packets_ = false;
+ // Can remove once back-compat protocol is > 29
+ bool ignore_old_attach_remote_player_packets_ : 1 {};
+ bool printed_connect_message_ : 1 {};
+ bool got_host_info_ : 1 {};
+ int protocol_version_{-1};
+ int build_number_{};
millisecs_t last_ping_send_time_{};
// the client-session that we're driving
Object::WeakRef client_session_;
diff --git a/src/ballistica/scene_v1/connection/connection_to_host_udp.cc b/src/ballistica/scene_v1/connection/connection_to_host_udp.cc
index d73a7800..97bf9271 100644
--- a/src/ballistica/scene_v1/connection/connection_to_host_udp.cc
+++ b/src/ballistica/scene_v1/connection/connection_to_host_udp.cc
@@ -13,7 +13,7 @@
namespace ballistica::scene_v1 {
auto ConnectionToHostUDP::SwitchProtocol() -> bool {
- if (protocol_version() > kProtocolVersionMin) {
+ if (protocol_version() > kProtocolVersionClientMin) {
set_protocol_version(protocol_version() - 1);
// Need a new request id so we ignore further responses to our previous
diff --git a/src/ballistica/scene_v1/node/image_node.h b/src/ballistica/scene_v1/node/image_node.h
index f4c36437..40029933 100644
--- a/src/ballistica/scene_v1/node/image_node.h
+++ b/src/ballistica/scene_v1/node/image_node.h
@@ -72,7 +72,7 @@ class ImageNode : public Node {
void set_front(bool val) { front_ = val; }
private:
- enum class Attach {
+ enum class Attach : uint8_t {
CENTER,
TOP_LEFT,
TOP_CENTER,
@@ -83,32 +83,24 @@ class ImageNode : public Node {
BOTTOM_LEFT,
CENTER_LEFT
};
- bool host_only_{};
- bool front_{};
- float vr_depth_{};
- std::vector scale_{1.0f, 1.0f};
- std::vector position_{0.0f, 0.0f};
- std::vector color_{1.0f, 1.0f, 1.0f};
- std::vector tint_color_{1.0f, 1.0f, 1.0f};
- std::vector tint2_color_{1.0f, 1.0f, 1.0f};
- Object::Ref texture_;
- Object::Ref tint_texture_;
- Object::Ref mask_texture_;
- Object::Ref mesh_opaque_;
- Object::Ref mesh_transparent_;
- bool fill_screen_{};
- bool has_alpha_channel_{true};
- bool dirty_{true};
- float opacity_{1.0f};
+
+ bool host_only_ : 1 {};
+ bool front_ : 1 {};
+ bool absolute_scale_ : 1 {true};
+ bool premultiplied_ : 1 {};
+ bool fill_screen_ : 1 {};
+ bool has_alpha_channel_ : 1 {true};
+ bool dirty_ : 1 {true};
Attach attach_{Attach::CENTER};
- bool absolute_scale_{true};
+
+ float vr_depth_{};
+ float opacity_{1.0f};
float center_x_{};
float center_y_{};
float width_{};
float height_{};
float tilt_translate_{};
float rotate_{};
- bool premultiplied_{};
float red_{1.0f};
float green_{1.0f};
float blue_{1.0f};
@@ -119,6 +111,16 @@ class ImageNode : public Node {
float tint2_red_{1.0f};
float tint2_green_{1.0f};
float tint2_blue_{1.0f};
+ std::vector scale_{1.0f, 1.0f};
+ std::vector position_{0.0f, 0.0f};
+ std::vector color_{1.0f, 1.0f, 1.0f};
+ std::vector tint_color_{1.0f, 1.0f, 1.0f};
+ std::vector tint2_color_{1.0f, 1.0f, 1.0f};
+ Object::Ref texture_;
+ Object::Ref tint_texture_;
+ Object::Ref mask_texture_;
+ Object::Ref mesh_opaque_;
+ Object::Ref mesh_transparent_;
};
} // namespace ballistica::scene_v1
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 fcf376f7..77b887bb 100644
--- a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc
+++ b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc
@@ -1046,7 +1046,24 @@ static auto PyCameraShake(PyObject* self, PyObject* args, PyObject* keywds)
const_cast(kwlist), &intensity)) {
return nullptr;
}
- g_base->graphics->LocalCameraShake(intensity);
+
+ if (Scene* scene = ContextRefSceneV1::FromCurrent().GetMutableScene()) {
+ // Send to clients/replays (IF we're servering protocol 35+).
+ if (SceneV1AppMode::GetSingleton()->host_protocol_version() >= 35) {
+ if (SessionStream* output_stream = scene->GetSceneStream()) {
+ output_stream->EmitCameraShake(intensity);
+ }
+ }
+
+ // Depict locally.
+ if (!g_core->HeadlessMode()) {
+ g_base->graphics->LocalCameraShake(intensity);
+ }
+ } else {
+ throw Exception("Can't shake the camera in this context_ref.",
+ PyExcType::kContext);
+ }
+
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
@@ -1172,9 +1189,13 @@ static auto PyEmitFx(PyObject* self, PyObject* args, PyObject* keywds)
e.spread = spread;
e.chunk_type = chunk_type;
e.tendril_type = tendril_type;
+
+ // Send to clients/replays.
if (SessionStream* output_stream = scene->GetSceneStream()) {
output_stream->EmitBGDynamics(e);
}
+
+ // Depict locally.
if (!g_core->HeadlessMode()) {
g_base->bg_dynamics->Emit(e);
}
@@ -1722,7 +1743,8 @@ static PyMethodDef PyHandleAppIntentExecDef = {
static auto PyProtocolVersion(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
- return PyLong_FromLong(kProtocolVersion);
+ return PyLong_FromLong(
+ SceneV1AppMode::GetSingleton()->host_protocol_version());
BA_PYTHON_CATCH;
}
diff --git a/src/ballistica/scene_v1/scene_v1.cc b/src/ballistica/scene_v1/scene_v1.cc
index fd086a8e..6ad16791 100644
--- a/src/ballistica/scene_v1/scene_v1.cc
+++ b/src/ballistica/scene_v1/scene_v1.cc
@@ -103,20 +103,27 @@ SceneV1FeatureSet::SceneV1FeatureSet() : python{new SceneV1Python()} {
// Types: I is 32 bit int, i is 16 bit int, c is 8 bit int,
// F is 32 bit float, f is 16 bit float,
// s is string, b is bool.
- SetupNodeMessageType("flash", NodeMessageType::kFlash, "");
- SetupNodeMessageType("footing", NodeMessageType::kFooting, "c");
- SetupNodeMessageType("impulse", NodeMessageType::kImpulse, "fffffffffifff");
- SetupNodeMessageType("kick_back", NodeMessageType::kKickback, "fffffff");
- SetupNodeMessageType("celebrate", NodeMessageType::kCelebrate, "i");
- SetupNodeMessageType("celebrate_l", NodeMessageType::kCelebrateL, "i");
- SetupNodeMessageType("celebrate_r", NodeMessageType::kCelebrateR, "i");
- SetupNodeMessageType("knockout", NodeMessageType::kKnockout, "f");
- SetupNodeMessageType("hurt_sound", NodeMessageType::kHurtSound, "");
- SetupNodeMessageType("picked_up", NodeMessageType::kPickedUp, "");
- SetupNodeMessageType("jump_sound", NodeMessageType::kJumpSound, "");
- SetupNodeMessageType("attack_sound", NodeMessageType::kAttackSound, "");
- SetupNodeMessageType("scream_sound", NodeMessageType::kScreamSound, "");
- SetupNodeMessageType("stand", NodeMessageType::kStand, "ffff");
+ SetupNodeMessageType_("flash", NodeMessageType::kFlash, "");
+ SetupNodeMessageType_("footing", NodeMessageType::kFooting, "c");
+ SetupNodeMessageType_("impulse", NodeMessageType::kImpulse, "fffffffffifff");
+ SetupNodeMessageType_("kick_back", NodeMessageType::kKickback, "fffffff");
+ SetupNodeMessageType_("celebrate", NodeMessageType::kCelebrate, "i");
+ SetupNodeMessageType_("celebrate_l", NodeMessageType::kCelebrateL, "i");
+ SetupNodeMessageType_("celebrate_r", NodeMessageType::kCelebrateR, "i");
+ SetupNodeMessageType_("knockout", NodeMessageType::kKnockout, "f");
+ SetupNodeMessageType_("hurt_sound", NodeMessageType::kHurtSound, "");
+ SetupNodeMessageType_("picked_up", NodeMessageType::kPickedUp, "");
+ SetupNodeMessageType_("jump_sound", NodeMessageType::kJumpSound, "");
+ SetupNodeMessageType_("attack_sound", NodeMessageType::kAttackSound, "");
+ SetupNodeMessageType_("scream_sound", NodeMessageType::kScreamSound, "");
+ SetupNodeMessageType_("stand", NodeMessageType::kStand, "ffff");
+}
+
+auto SceneV1FeatureSet::Import() -> SceneV1FeatureSet* {
+ // Since we provide a native Python module, we piggyback our C++ front-end
+ // on top of that. This way our C++ and Python dependencies are resolved
+ // consistently no matter which side we are imported from.
+ return ImportThroughPythonModule("_bascenev1");
}
void SceneV1FeatureSet::Reset() {
@@ -132,13 +139,6 @@ void SceneV1FeatureSet::ResetRandomNames() {
random_name_registry_->clear();
}
-auto SceneV1FeatureSet::Import() -> SceneV1FeatureSet* {
- // Since we provide a native Python module, we piggyback our C++ front-end
- // on top of that. This way our C++ and Python dependencies are resolved
- // consistently no matter which side we are imported from.
- return ImportThroughPythonModule("_bascenev1");
-}
-
auto SceneV1FeatureSet::GetRandomName(const std::string& full_name)
-> std::string {
assert(g_base->InLogicThread());
@@ -172,9 +172,9 @@ auto SceneV1FeatureSet::GetRandomName(const std::string& full_name)
return (*random_name_registry_)[full_name];
}
-void SceneV1FeatureSet::SetupNodeMessageType(const std::string& name,
- NodeMessageType val,
- const std::string& format) {
+void SceneV1FeatureSet::SetupNodeMessageType_(const std::string& name,
+ NodeMessageType val,
+ const std::string& format) {
node_message_types_[name] = val;
assert(static_cast(val) >= 0);
if (node_message_formats_.size() <= static_cast(val)) {
diff --git a/src/ballistica/scene_v1/scene_v1.h b/src/ballistica/scene_v1/scene_v1.h
index 63415c81..ae8c78bf 100644
--- a/src/ballistica/scene_v1/scene_v1.h
+++ b/src/ballistica/scene_v1/scene_v1.h
@@ -27,39 +27,54 @@ namespace ballistica::scene_v1 {
// Protocol version we host games with and write replays to. This should be
// incremented whenever there are changes made to the session-commands layer
-// (new/removed/changed nodes, attrs, data files, behavior, etc.)
+// (new/removed/changed nodes, attrs, data files, behavior, etc.).
// Note that the packet/gamepacket/message layer can vary more organically
// based on build-numbers of connected clients/servers since none of that
-// data is stored; this just needs to be observed for all the scene stuff
-// that goes into replays since a single stream can get played/replayed on
-// different builds (as long as they support that protocol version).
-const int kProtocolVersion = 33;
+// data is stored; these protocol versions just need to be observed by
+// anything emitting or ingesting scene streams.
+
+// Oldest protocol version we can act as a host for.
+const int kProtocolVersionHostMin = 33;
// Oldest protocol version we can act as a client to. This can generally be
-// left as-is as long as only new nodes/attrs/commands are added and
-// existing stuff is unchanged.
-const int kProtocolVersionMin = 24;
+// left as-is as long as only new nodes/attrs/commands are added and old
+// behavior remains the same when not using the new stuff.
+const int kProtocolVersionClientMin = 24;
-// FIXME: We should separate out connection protocol from scene protocol. We
-// want to be able to watch really old replays if possible but being able
-// to connect to old clients is much less important (and slows progress).
+// Newest protocol version we can act as a client OR host for.
+const int kProtocolVersionMax = 35;
-// Protocol additions:
-// 25: added a few new achievement graphics and new node attrs for displaying
-// stuff in front of the UI
-// 26: added penguin
-// 27: added templates for LOTS of characters
-// 28: added cyborg and enabled fallback sounds and textures
-// 29: added bunny and eggs
-// 30: added support for resource-strings in text-nodes and screen-messages
-// 31: added support for short-form resource-strings, time-display-node, and
-// string-to-string attr connections
-// 32: added json based player profiles message, added shield
-// alwaysShowHealthBar attr
-// 33: handshake/handshake-response now send json dicts instead of
-// just player-specs
-// 34: new image_node enums, data assets.
+// The protocol version we actually host is now read as a setting; see
+// kSceneV1HostProtocol in ballistica/base/support/app_config.h.
+
+// Protocol changes:
+//
+// 25: Added a few new achievement graphics and new node attrs for displaying
+// stuff in front of the UI.
+//
+// 26: Added penguin.
+//
+// 27: Added templates for LOTS of characters.
+//
+// 28: Added cyborg and enabled fallback sounds and textures.
+//
+// 29: Added bunny and eggs.
+//
+// 30: Added support for resource-strings in text-nodes and screen-messages.
+//
+// 31: Added support for short-form resource-strings, time-display-node, and
+// string-to-string attr connections.
+//
+// 32: Added json based player profiles message, added shield
+// always_show_health_bar attr.
+//
+// 33: Handshake/handshake-response now send json dicts instead of
+// just player-specs.
+//
+// 34: New image_node enums, data assets.
+//
+// 35: Camera shake in netplay. how did I apparently miss this for 10 years!?!
// Sim step size in milliseconds.
const int kGameStepMilliseconds = 8;
@@ -215,7 +230,8 @@ enum class SessionCommand {
kScreenMessageBottom,
kScreenMessageTop,
kAddData,
- kRemoveData
+ kRemoveData,
+ kCameraShake
};
enum class NodeCollideAttr {
@@ -373,8 +389,8 @@ class SceneV1FeatureSet : public FeatureSetNativeComponent {
SceneV1Python* const python;
private:
- void SetupNodeMessageType(const std::string& name, NodeMessageType val,
- const std::string& format);
+ void SetupNodeMessageType_(const std::string& name, NodeMessageType val,
+ const std::string& format);
SceneV1FeatureSet();
std::unordered_map node_types_;
diff --git a/src/ballistica/scene_v1/support/client_session.cc b/src/ballistica/scene_v1/support/client_session.cc
index 6920eb31..d10e11e4 100644
--- a/src/ballistica/scene_v1/support/client_session.cc
+++ b/src/ballistica/scene_v1/support/client_session.cc
@@ -835,6 +835,11 @@ void ClientSession::Update(int time_advance_millisecs, double time_advance) {
y, z);
break;
}
+ case SessionCommand::kCameraShake: {
+ auto intensity = ReadFloat();
+ g_base->graphics->LocalCameraShake(intensity);
+ break;
+ }
case SessionCommand::kEmitBGDynamics: {
int cmdvals[4];
ReadInt32_4(cmdvals);
diff --git a/src/ballistica/scene_v1/support/client_session_net.cc b/src/ballistica/scene_v1/support/client_session_net.cc
index 896fd4ec..aaf089ee 100644
--- a/src/ballistica/scene_v1/support/client_session_net.cc
+++ b/src/ballistica/scene_v1/support/client_session_net.cc
@@ -17,7 +17,9 @@ ClientSessionNet::ClientSessionNet() {
"g_replay_open true at netclient start; shouldn't happen.");
}
assert(g_base->assets_server);
- g_base->assets_server->PushBeginWriteReplayCall(kProtocolVersion);
+
+ // We always write replays as the highest protocol version we support.
+ g_base->assets_server->PushBeginWriteReplayCall(kProtocolVersionMax);
writing_replay_ = true;
g_core->replay_open = true;
}
diff --git a/src/ballistica/scene_v1/support/client_session_net.h b/src/ballistica/scene_v1/support/client_session_net.h
index a7c7051c..075dda57 100644
--- a/src/ballistica/scene_v1/support/client_session_net.h
+++ b/src/ballistica/scene_v1/support/client_session_net.h
@@ -26,8 +26,6 @@ class ClientSessionNet : public ClientSession {
private:
struct SampleBucket {
- // int least_buffered_count{};
- // int most_buffered_count{};
int max_delay_from_projection{};
};
diff --git a/src/ballistica/scene_v1/support/client_session_replay.cc b/src/ballistica/scene_v1/support/client_session_replay.cc
index c57d76ac..298bb420 100644
--- a/src/ballistica/scene_v1/support/client_session_replay.cc
+++ b/src/ballistica/scene_v1/support/client_session_replay.cc
@@ -253,7 +253,7 @@ void ClientSessionReplay::OnReset(bool rewind) {
Error("error reading version");
return;
}
- if (version > kProtocolVersion || version < kProtocolVersionMin) {
+ if (version > kProtocolVersionMax || version < kProtocolVersionClientMin) {
ScreenMessage(g_base->assets->GetResourceString("replayVersionErrorText"),
{1, 0, 0});
End();
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 e7807406..4b3d3c01 100644
--- a/src/ballistica/scene_v1/support/scene_v1_app_mode.cc
+++ b/src/ballistica/scene_v1/support/scene_v1_app_mode.cc
@@ -46,7 +46,7 @@ const int kKickVoteFailRetryDelayInitiatorExtra = 120000;
// to kick).
const int kKickVoteMinimumClients = (g_buildconfig.headless_build() ? 3 : 4);
-struct SceneV1AppMode::ScanResultsEntryPriv {
+struct SceneV1AppMode::ScanResultsEntryPriv_ {
scene_v1::PlayerSpec player_spec;
std::string address;
uint32_t last_query_id{};
@@ -78,7 +78,16 @@ static SceneV1AppMode* g_scene_v1_app_mode{};
void SceneV1AppMode::OnActivate() {
assert(g_base->InLogicThread());
- Reset();
+
+ // Make sure we pull this only once when we are first active.
+ if (host_protocol_version_ == -1) {
+ host_protocol_version_ =
+ std::clamp(g_base->app_config->Resolve(
+ base::AppConfig::IntID::kSceneV1HostProtocol),
+ kProtocolVersionHostMin, kProtocolVersionMax);
+ }
+
+ Reset_();
// We use UIV1.
if (!g_core->HeadlessMode()) {
@@ -108,9 +117,10 @@ void SceneV1AppMode::OnAppPause() {
void SceneV1AppMode::OnAppResume() { assert(g_base->InLogicThread()); }
// Note: for now we're making our host-scan network calls directly from the
-// logic thread. This is generally not a good idea since it appears that even in
-// non-blocking mode they're still blocking for 3-4ms sometimes. But for now
-// since this is only used minimally and only while in the UI I guess it's ok.
+// logic thread. This is generally not a good idea since it appears that even
+// in non-blocking mode they're still blocking for 3-4ms sometimes. But for
+// now since this is only used minimally and only while in the UI I guess it's
+// ok.
void SceneV1AppMode::HostScanCycle() {
assert(g_base->InLogicThread());
@@ -258,7 +268,7 @@ void SceneV1AppMode::HostScanCycle() {
bool do_update_entry = (i == scan_results_.end()
|| i->second.last_query_id != query_id);
if (do_update_entry) {
- ScanResultsEntryPriv& entry(scan_results_[key]);
+ ScanResultsEntryPriv_& entry(scan_results_[key]);
entry.player_spec = scene_v1::PlayerSpec(player_spec_str);
char buffer2[256];
entry.address = inet_ntop(
@@ -269,7 +279,7 @@ void SceneV1AppMode::HostScanCycle() {
entry.last_contact_time = g_core->GetAppTimeMillisecs();
}
}
- PruneScanResults();
+ PruneScanResults_();
}
} else {
Log(LogLevel::kError,
@@ -290,7 +300,7 @@ void SceneV1AppMode::EndHostScanning() {
}
}
-void SceneV1AppMode::PruneScanResults() {
+void SceneV1AppMode::PruneScanResults_() {
millisecs_t t = g_core->GetAppTimeMillisecs();
auto i = scan_results_.begin();
while (i != scan_results_.end()) {
@@ -311,13 +321,13 @@ auto SceneV1AppMode::GetScanResults()
std::scoped_lock lock(scan_results_mutex_);
int out_num = 0;
for (auto&& i : scan_results_) {
- ScanResultsEntryPriv& in(i.second);
+ ScanResultsEntryPriv_& in(i.second);
ScanResultsEntry& out(results[out_num]);
out.display_string = in.player_spec.GetDisplayString();
out.address = in.address;
out_num++;
}
- PruneScanResults();
+ PruneScanResults_();
}
return results;
}
@@ -399,8 +409,8 @@ auto SceneV1AppMode::HandleJSONPing(const std::string& data_str)
}
cJSON_Delete(data);
- // Ok lets include some basic info that might be pertinent to someone pinging
- // us. Currently that includes our current/max connection count.
+ // Ok lets include some basic info that might be pertinent to someone
+ // pinging us. Currently that includes our current/max connection count.
char buffer[256];
snprintf(buffer, sizeof(buffer), R"({"b":%d,"ps":%d,"psmx":%d})",
kEngineBuildNumber, public_party_size(), public_party_max_size());
@@ -420,7 +430,7 @@ auto SceneV1AppMode::GetPartySize() const -> int {
return cJSON_GetArraySize(game_roster_);
}
-auto SceneV1AppMode::GetHeadlessDisplayStep() -> microsecs_t {
+auto SceneV1AppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t {
std::optional min_time_to_next;
for (auto&& i : sessions_) {
if (!i.Exists()) {
@@ -436,7 +446,7 @@ auto SceneV1AppMode::GetHeadlessDisplayStep() -> microsecs_t {
}
}
return min_time_to_next.has_value() ? *min_time_to_next
- : base::kAppModeMaxHeadlessDisplayStep;
+ : base::kHeadlessMaxDisplayTimeStep;
}
void SceneV1AppMode::StepDisplayTime() {
@@ -468,15 +478,15 @@ void SceneV1AppMode::StepDisplayTime() {
}
legacy_display_time_millisecs_prev_ = legacy_display_time_millisecs_;
- UpdateKickVote();
+ UpdateKickVote_();
- HandleQuitOnIdle();
+ HandleQuitOnIdle_();
// Send the game roster to our clients if it's changed recently.
if (game_roster_dirty_) {
if (app_time > last_game_roster_send_time_ + 2500) {
// Now send it to all connected clients.
- std::vector msg = GetGameRosterMessage();
+ std::vector msg = GetGameRosterMessage_();
for (auto&& c : connections()->GetConnectionsToClients()) {
c->SendReliableMessage(msg);
}
@@ -501,7 +511,7 @@ void SceneV1AppMode::StepDisplayTime() {
}
// Go ahead and prune dead ones.
- PruneSessions();
+ PruneSessions_();
in_update_ = false;
@@ -521,7 +531,7 @@ void SceneV1AppMode::StepDisplayTime() {
}
}
-auto SceneV1AppMode::GetGameRosterMessage() -> std::vector {
+auto SceneV1AppMode::GetGameRosterMessage_() -> std::vector {
// This message is simply a flattened json string of our roster (including
// terminating char).
char* s = cJSON_PrintUnformatted(game_roster_);
@@ -661,7 +671,7 @@ void SceneV1AppMode::UpdateGameRoster() {
game_roster_dirty_ = true;
}
-void SceneV1AppMode::UpdateKickVote() {
+void SceneV1AppMode::UpdateKickVote_() {
if (!kick_vote_in_progress_) {
return;
}
@@ -973,7 +983,7 @@ void SceneV1AppMode::LaunchHostSession(PyObject* session_type_obj,
base::ScopedSetContext ssc(nullptr);
// This should kill any current session and get us back to a blank slate.
- Reset();
+ Reset_();
Object::WeakRef old_foreground_session(foreground_session_);
try {
@@ -1004,7 +1014,7 @@ void SceneV1AppMode::LaunchReplaySession(const std::string& file_name) {
base::ScopedSetContext ssc(nullptr);
// This should kill any current session and get us back to a blank slate.
- Reset();
+ Reset_();
// Create the new session.
Object::WeakRef old_foreground_session(foreground_session_);
@@ -1034,7 +1044,7 @@ void SceneV1AppMode::LaunchClientSession() {
base::ScopedSetContext ssc(nullptr);
// This should kill any current session and get us back to a blank slate.
- Reset();
+ Reset_();
// Create the new session.
Object::WeakRef old_foreground_session(foreground_session_);
@@ -1052,13 +1062,13 @@ void SceneV1AppMode::LaunchClientSession() {
}
// Reset to a blank slate.
-void SceneV1AppMode::Reset() {
+void SceneV1AppMode::Reset_() {
assert(g_base);
assert(g_base->InLogicThread());
// Tear down our existing session.
foreground_session_.Clear();
- PruneSessions();
+ PruneSessions_();
// If all is well our sessions should all be dead.
if (g_core->session_count != 0) {
@@ -1174,7 +1184,7 @@ void SceneV1AppMode::DoApplyAppConfig() {
base::AppConfig::OptionalFloatID::kIdleExitMinutes);
}
-void SceneV1AppMode::PruneSessions() {
+void SceneV1AppMode::PruneSessions_() {
bool have_dead_session = false;
for (auto&& i : sessions_) {
if (i.Exists()) {
@@ -1368,7 +1378,7 @@ void SceneV1AppMode::BanPlayer(const PlayerSpec& spec, millisecs_t duration) {
banned_players_.emplace_back(g_core->GetAppTimeMillisecs() + duration, spec);
}
-void SceneV1AppMode::HandleQuitOnIdle() {
+void SceneV1AppMode::HandleQuitOnIdle_() {
if (idle_exit_minutes_) {
auto idle_seconds{static_cast(g_base->input->input_idle_time())
* 0.001f};
@@ -1443,7 +1453,7 @@ void SceneV1AppMode::HandleGameQuery(const char* buffer, size_t size,
msg[0] = BA_PACKET_HOST_QUERY_RESPONSE;
memcpy(msg + 1, &query_id, 4);
- uint32_t protocol_version = kProtocolVersion;
+ uint32_t protocol_version = host_protocol_version();
memcpy(msg + 5, &protocol_version, 4);
msg[9] = static_cast(usid.size());
msg[10] = static_cast(player_spec_string.size());
diff --git a/src/ballistica/scene_v1/support/scene_v1_app_mode.h b/src/ballistica/scene_v1/support/scene_v1_app_mode.h
index 34952109..769c7081 100644
--- a/src/ballistica/scene_v1/support/scene_v1_app_mode.h
+++ b/src/ballistica/scene_v1/support/scene_v1_app_mode.h
@@ -182,41 +182,56 @@ class SceneV1AppMode : public base::AppMode {
auto buffer_time() const { return buffer_time_; }
void set_buffer_time(int val) { buffer_time_ = val; }
void OnActivate() override;
- auto GetHeadlessDisplayStep() -> microsecs_t override;
+ auto GetHeadlessNextDisplayTimeStep() -> microsecs_t override;
+
+ auto host_protocol_version() const {
+ assert(host_protocol_version_ != -1);
+ return host_protocol_version_;
+ }
private:
- void PruneScanResults();
- void UpdateKickVote();
SceneV1AppMode();
- auto GetGameRosterMessage() -> std::vector;
- void Reset();
- void PruneSessions();
- void HandleQuitOnIdle();
- struct ScanResultsEntryPriv;
+ void PruneScanResults_();
+ void UpdateKickVote_();
+ auto GetGameRosterMessage_() -> std::vector;
+ void Reset_();
+ void PruneSessions_();
+ void HandleQuitOnIdle_();
+
+ struct ScanResultsEntryPriv_;
+
// Note: would use an unordered_map here but gcc doesn't seem to allow
// forward declarations of their template params.
- std::map scan_results_;
+ std::map scan_results_;
std::mutex scan_results_mutex_;
uint32_t next_scan_query_id_{};
int scan_socket_{-1};
+ int host_protocol_version_{-1};
std::list chat_messages_;
- bool chat_muted_{};
// *All* existing sessions (including old ones waiting to shut down).
std::vector > sessions_;
Object::WeakRef foreground_scene_;
Object::WeakRef foreground_session_;
- bool game_roster_dirty_{};
+ bool chat_muted_ : 1 {};
+ bool in_update_ : 1 {};
+ bool kick_idle_players_ : 1 {};
+ bool public_party_enabled_ : 1 {};
+ bool public_party_queue_enabled_ : 1 {true};
+ bool require_client_authentication_ : 1 {};
+ bool idle_exiting_ : 1 {};
+ bool game_roster_dirty_ : 1 {};
+ bool kick_vote_in_progress_ : 1 {};
+ bool kick_voting_enabled_ : 1 {true};
+
+ cJSON* game_roster_{};
millisecs_t last_game_roster_send_time_{};
std::unique_ptr connections_;
- cJSON* game_roster_{};
Object::WeakRef kick_vote_starter_;
Object::WeakRef kick_vote_target_;
millisecs_t kick_vote_end_time_{};
- bool kick_vote_in_progress_{};
int last_kick_votes_needed_{-1};
- bool kick_voting_enabled_{true};
millisecs_t legacy_display_time_millisecs_{};
millisecs_t legacy_display_time_millisecs_prev_{-1};
@@ -229,28 +244,22 @@ class SceneV1AppMode : public base::AppMode {
// it over the network.
int buffer_time_{};
- bool in_update_{};
millisecs_t next_long_update_report_time_{};
int debug_speed_exponent_{};
- float debug_speed_mult_{1.0f};
int replay_speed_exponent_{};
- float replay_speed_mult_{1.0f};
- bool kick_idle_players_{};
- std::set admin_public_ids_;
- millisecs_t last_connection_to_client_join_time_{};
- bool public_party_enabled_{};
int public_party_size_{1}; // Always count ourself (is that what we want?).
int public_party_max_size_{8};
- bool public_party_queue_enabled_{true};
int public_party_player_count_{0};
int public_party_max_player_count_{8};
+ float debug_speed_mult_{1.0f};
+ float replay_speed_mult_{1.0f};
+ std::set admin_public_ids_;
+ millisecs_t last_connection_to_client_join_time_{};
std::string public_party_name_;
std::string public_party_min_league_;
std::string public_party_stats_url_;
- bool require_client_authentication_{};
std::list > banned_players_;
std::optional idle_exit_minutes_{};
- bool idle_exiting_{};
std::optional internal_music_play_id_{};
};
diff --git a/src/ballistica/scene_v1/support/session_stream.cc b/src/ballistica/scene_v1/support/session_stream.cc
index 51258baa..32ff4bc3 100644
--- a/src/ballistica/scene_v1/support/session_stream.cc
+++ b/src/ballistica/scene_v1/support/session_stream.cc
@@ -31,8 +31,9 @@ SessionStream::SessionStream(HostSession* host_session, bool save_replay)
Log(LogLevel::kError,
"g_replay_open true at replay start; shouldn't happen.");
}
+ // We always write replays as the max protocol version we support.
assert(g_base->assets_server);
- g_base->assets_server->PushBeginWriteReplayCall(kProtocolVersion);
+ g_base->assets_server->PushBeginWriteReplayCall(kProtocolVersionMax);
writing_replay_ = true;
g_core->replay_open = true;
}
@@ -205,9 +206,6 @@ void SessionStream::Flush() {
}
}
-#pragma clang diagnostic push
-#pragma ide diagnostic ignored "ConstantParameter"
-
// Writes just a command.
void SessionStream::WriteCommand(SessionCommand cmd) {
assert(out_command_.empty());
@@ -219,8 +217,6 @@ void SessionStream::WriteCommand(SessionCommand cmd) {
*ptr = static_cast(cmd);
}
-#pragma clang diagnostic pop
-
// Writes a command plus an int to the stream, using whatever size is optimal.
void SessionStream::WriteCommandInt32(SessionCommand cmd, int32_t value) {
assert(out_command_.empty());
@@ -1123,6 +1119,13 @@ void SessionStream::EmitBGDynamics(const base::BGDynamicsEmission& e) {
EndCommand();
}
+void SessionStream::EmitCameraShake(float intensity) {
+ WriteCommand(SessionCommand::kCameraShake);
+ // FIXME: We shouldn't need to be passing all these as full floats. :-(
+ WriteFloat(intensity);
+ EndCommand();
+}
+
void SessionStream::PlaySound(SceneSound* sound, float volume) {
assert(IsValidSound(sound));
assert(IsValidScene(sound->scene()));
diff --git a/src/ballistica/scene_v1/support/session_stream.h b/src/ballistica/scene_v1/support/session_stream.h
index f34e02f2..053771f2 100644
--- a/src/ballistica/scene_v1/support/session_stream.h
+++ b/src/ballistica/scene_v1/support/session_stream.h
@@ -69,6 +69,7 @@ class SessionStream : public Object, public ClientControllerInterface {
float z);
void PlaySound(SceneSound* sound, float volume);
void EmitBGDynamics(const base::BGDynamicsEmission& e);
+ void EmitCameraShake(float intensity);
auto GetSoundID(SceneSound* s) -> int64_t;
auto GetMaterialID(Material* m) -> int64_t;
void ScreenMessageBottom(const std::string& val, float r, float g, float b);
diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc
index d9cf17fc..c67f264a 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 = 21447;
+const int kEngineBuildNumber = 21465;
const char* kEngineVersion = "1.7.28";
const int kEngineApiVersion = 8;
diff --git a/src/ballistica/shared/foundation/event_loop.cc b/src/ballistica/shared/foundation/event_loop.cc
index 9c6a1ad3..9133557b 100644
--- a/src/ballistica/shared/foundation/event_loop.cc
+++ b/src/ballistica/shared/foundation/event_loop.cc
@@ -248,6 +248,9 @@ void EventLoop::WaitForNextEvent_(bool single_cycle) {
}
}
+// Note to self (Oct '23): can probably kill this at some point,
+// but am still using some non-ARC objc stuff from logic thread
+// so should keep it around just a bit longer just in case.
void EventLoop::LoopUpkeep_(bool single_cycle) {
assert(g_core);
// Keep our autorelease pool clean on mac/ios
diff --git a/src/ballistica/shared/foundation/event_loop.h b/src/ballistica/shared/foundation/event_loop.h
index c8d93ac2..40f03631 100644
--- a/src/ballistica/shared/foundation/event_loop.h
+++ b/src/ballistica/shared/foundation/event_loop.h
@@ -118,7 +118,6 @@ class EventLoop {
auto CheckPushRunnableSafety_() -> bool;
void SetInternalThreadName_(const std::string& name);
void WaitForNextEvent_(bool single_cycle);
- void LoopUpkeep_(bool single_cycle);
void LogThreadMessageTally_(
std::vector>* log_entries);
void PushLocalRunnable_(Runnable* runnable, bool* completion_flag);
@@ -155,18 +154,7 @@ class EventLoop {
void BootstrapThread_();
- bool writing_tally_{};
- bool suspended_{};
- millisecs_t last_suspend_time_{};
- int messages_since_suspended_{};
- millisecs_t last_suspended_message_report_time_{};
- bool done_{};
- ThreadSource source_;
- int listen_sd_{};
- std::thread::id thread_id_{};
- EventLoopID identifier_{EventLoopID::kInvalid};
- millisecs_t last_complaint_time_{};
- bool acquires_python_gil_{};
+ void LoopUpkeep_(bool single_cycle);
// FIXME: Should generalize this to some sort of PlatformThreadData class.
#if BA_XCODE_BUILD
@@ -174,13 +162,25 @@ class EventLoop {
#endif
bool bootstrapped_{};
+ bool writing_tally_{};
+ bool suspended_{};
+ bool done_{};
+ bool acquires_python_gil_{};
+ EventLoopID identifier_{EventLoopID::kInvalid};
+ millisecs_t last_suspend_time_{};
+ int listen_sd_{};
+ int messages_since_suspended_{};
+ millisecs_t last_suspended_message_report_time_{};
+ millisecs_t last_complaint_time_{};
+ ThreadSource source_;
+ std::thread::id thread_id_{};
std::list> runnables_;
std::list suspend_callbacks_;
std::list unsuspend_callbacks_;
std::condition_variable thread_message_cv_;
+ std::condition_variable client_listener_cv_;
std::mutex thread_message_mutex_;
std::list thread_messages_;
- std::condition_variable client_listener_cv_;
std::mutex client_listener_mutex_;
std::list> data_to_client_;
PyThreadState* py_thread_state_{};
diff --git a/src/ballistica/shared/foundation/inline.h b/src/ballistica/shared/foundation/inline.h
index db9d8e1d..5376eeb3 100644
--- a/src/ballistica/shared/foundation/inline.h
+++ b/src/ballistica/shared/foundation/inline.h
@@ -88,8 +88,7 @@ template
auto static_cast_check_type(IN_TYPE in) -> OUT_TYPE {
auto out_static = static_cast(in);
if (g_buildconfig.debug_build()) {
- auto out_dynamic = dynamic_cast(in);
- assert(out_static == out_dynamic);
+ assert(out_static == dynamic_cast(in));
}
return out_static;
}
diff --git a/src/ballistica/shared/foundation/types.h b/src/ballistica/shared/foundation/types.h
index 32989060..fd35d729 100644
--- a/src/ballistica/shared/foundation/types.h
+++ b/src/ballistica/shared/foundation/types.h
@@ -30,6 +30,7 @@ typedef struct _SDL_Joystick SDL_Joystick;
namespace ballistica {
// Used internally for time values.
+typedef double seconds_t;
typedef int64_t millisecs_t;
typedef int64_t microsecs_t;
@@ -68,7 +69,7 @@ class Graphics;
///
/// Category: Enums
///
-enum class InputType {
+enum class InputType : uint8_t {
kUpDown = 2,
kLeftRight,
kJumpPress,
@@ -111,7 +112,7 @@ enum class InputType {
///
/// 'hard' leads to the process exiting. This generally should be avoided
/// on platforms such as mobile.
-enum class QuitType {
+enum class QuitType : uint8_t {
kSoft,
kBack,
kHard,
@@ -139,7 +140,7 @@ typedef int64_t TimerMedium;
/// 'small' is used primarily for phones or other small devices where
/// content needs to be presented as large and clear in order to remain
/// readable from an average distance.
-enum class UIScale {
+enum class UIScale : uint8_t {
kLarge,
kMedium,
kSmall,
@@ -162,7 +163,7 @@ enum class UIScale {
/// 'real' time is mostly based on clock time, with a few exceptions. It may
/// not advance while the app is backgrounded for instance. (the engine
/// attempts to prevent single large time jumps from occurring)
-enum class TimeType {
+enum class TimeType : uint8_t {
kSim,
kBase,
kReal,
@@ -173,7 +174,7 @@ enum class TimeType {
/// Specifies the format time values are provided in.
///
/// Category: Enums
-enum class TimeFormat {
+enum class TimeFormat : uint8_t {
kSeconds,
kMilliseconds,
kLast // Sentinel.
@@ -183,7 +184,7 @@ enum class TimeFormat {
/// Permissions that can be requested from the OS.
///
/// Category: Enums
-enum class Permission {
+enum class Permission : uint8_t {
kStorage,
kLast // Sentinel.
};
@@ -192,7 +193,7 @@ enum class Permission {
/// Special characters the game can print.
///
/// Category: Enums
-enum class SpecialChar {
+enum class SpecialChar : uint8_t {
kDownArrow,
kUpArrow,
kLeftArrow,
@@ -288,7 +289,7 @@ enum class SpecialChar {
};
/// Python exception types we can raise from our own exceptions.
-enum class PyExcType {
+enum class PyExcType : uint8_t {
kRuntime,
kAttribute,
kIndex,
@@ -306,7 +307,7 @@ enum class PyExcType {
kWidgetNotFound
};
-enum class LogLevel {
+enum class LogLevel : uint8_t {
kDebug,
kInfo,
kWarning,
@@ -314,7 +315,7 @@ enum class LogLevel {
kCritical,
};
-enum class ThreadSource {
+enum class ThreadSource : uint8_t {
/// Spin up a new thread for the event loop.
kCreate,
/// Wrap the event loop around the current thread.
@@ -323,7 +324,7 @@ enum class ThreadSource {
/// Used for thread identification.
/// Mostly just for debugging.
-enum class EventLoopID {
+enum class EventLoopID : uint8_t {
kInvalid,
kLogic,
kAssets,
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 ac51c315..f03b6841 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
@@ -23,10 +23,6 @@
#include "ballistica/ui_v1/widget/row_widget.h"
#include "ballistica/ui_v1/widget/scroll_widget.h"
-// #if !BA_HEADLESS_BUILD && !BA_XCODE_NEW_PROJECT
-// extern "C" void SDL_ericf_focus(void);
-// #endif
-
namespace ballistica::ui_v1 {
// Ignore signed bitwise stuff; python macros do it quite a bit.
diff --git a/tools/bacommon/servermanager.py b/tools/bacommon/servermanager.py
index 7c13910a..e059e43a 100644
--- a/tools/bacommon/servermanager.py
+++ b/tools/bacommon/servermanager.py
@@ -143,6 +143,12 @@ class ServerConfig:
# queue spamming attacks.
enable_queue: bool = True
+ # Protocol version we host with. Currently the default is 33 which
+ # still allows older 1.4 game clients to connect. Explicitly setting
+ # to 35 no longer allows those clients but adds/fixes a few things
+ # such as making camera shake properly work in net games.
+ protocol_version: int | None = None
+
# (internal) stress-testing mode.
stress_test_players: int | None = None
diff --git a/tools/batools/build.py b/tools/batools/build.py
index 22818bcd..682c7cf3 100644
--- a/tools/batools/build.py
+++ b/tools/batools/build.py
@@ -601,6 +601,7 @@ def _get_server_config_raw_contents(projroot: str) -> str:
def _get_server_config_template_yaml(projroot: str) -> str:
# pylint: disable=too-many-branches
+ # pylint: disable=too-many-statements
import yaml
lines_in = _get_server_config_raw_contents(projroot).splitlines()
@@ -664,6 +665,8 @@ def _get_server_config_template_yaml(projroot: str) -> str:
vval = 'https://mystatssite.com/showstats?player=${ACCOUNT}'
elif vname == 'admins':
vval = ['pb-yOuRAccOuNtIdHErE', 'pb-aNdMayBeAnotherHeRE']
+ elif vname == 'protocol_version':
+ vval = 35
lines_out += [
'#' + l for l in yaml.dump({vname: vval}).strip().splitlines()
]
diff --git a/tools/batools/pythonenumsmodule.py b/tools/batools/pythonenumsmodule.py
index d40259f3..e7cf4cbf 100755
--- a/tools/batools/pythonenumsmodule.py
+++ b/tools/batools/pythonenumsmodule.py
@@ -59,6 +59,10 @@ def _gen_enums(infilename: str) -> str:
def _parse_name(lines: list[str], lnum: int) -> str:
bits = lines[lnum].split(' ')
+
+ # Special case: allow for specifying underlying type.
+ if len(bits) == 6 and bits[3] == ':' and bits[4] in {'uint8_t', 'uint16_t'}:
+ bits = [bits[0], bits[1], bits[2], bits[5]]
if (
len(bits) != 4
or bits[0] != 'enum'
diff --git a/tools/batools/spinoff/_context.py b/tools/batools/spinoff/_context.py
index 1c2b60c2..7efa7dac 100644
--- a/tools/batools/spinoff/_context.py
+++ b/tools/batools/spinoff/_context.py
@@ -29,7 +29,7 @@ from batools.spinoff._state import (
)
if TYPE_CHECKING:
- from typing import Callable, Iterable
+ from typing import Callable, Iterable, Any
from batools.project import ProjectUpdater
@@ -54,6 +54,7 @@ class SpinoffContext:
OVERRIDE = 'override'
DIFF = 'diff'
BACKPORT = 'backport'
+ DESCRIBE_PATH = 'describe_path'
def __init__(
self,
@@ -66,6 +67,7 @@ class SpinoffContext:
override_paths: list[str] | None = None,
backport_file: str | None = None,
auto_backport: bool = False,
+ describe_path: str | None = None,
) -> None:
# pylint: disable=too-many-statements
@@ -82,6 +84,7 @@ class SpinoffContext:
self._override_paths = override_paths
self._backport_file = backport_file
self._auto_backport = auto_backport
+ self._describe_path = describe_path
self._project_updater: ProjectUpdater | None = None
@@ -278,10 +281,10 @@ class SpinoffContext:
self._src_omit_feature_sets,
) = self._calc_src_retain_omit_feature_sets()
- # Generate a version of src_omit_paths that includes our feature-set
- # omissions. Basically, omitting a feature set simply omits
- # particular names at a few particular places.
+ # Generate a version of src_omit_paths that includes some extra values
self._src_omit_paths_expanded = self.src_omit_paths.copy()
+ # Include feature-set omissions. Basically, omitting a feature
+ # set simply omits particular names at a few particular places.
self._add_feature_set_omit_paths(self._src_omit_paths_expanded)
# Create a version of dst-write-paths that also includes filtered
@@ -362,6 +365,7 @@ class SpinoffContext:
def run(self) -> None:
"""Do the thing."""
# pylint: disable=too-many-branches
+ # pylint: disable=too-many-statements
self._read_state()
@@ -400,6 +404,20 @@ class SpinoffContext:
# Ignore anything under omitted paths/names.
self._filter_src_git_file_list()
+ # Go through the final set of files we're syncing to dst and
+ # make sure none of them fall under our unchecked-paths list.
+ # That would mean we are writing a file but we're also declaring
+ # that we don't care if anyone else writes that file, which
+ # could lead to ambiguous/dangerous situations where spinoff as
+ # well as some command on dst write to the same file.
+ for path in self._src_git_files:
+ if _any_path_contains(self.src_unchecked_paths, path):
+ self._src_error_entities[path] = (
+ 'Synced file falls under src_unchecked_paths, which'
+ " is not allowed. Either don't sync the file or carve"
+ ' it out from src_unchecked_paths.'
+ )
+
# Now map whatever is left to paths in dst.
self._dst_git_files = set(
self._filter_path(s) for s in self._src_git_files
@@ -454,8 +472,10 @@ class SpinoffContext:
)
raise self.BackportInProgressError
+ if self._mode is self.Mode.DESCRIBE_PATH:
+ self._do_describe_path()
# If anything is off, print errors; otherwise actually do the deed.
- if self._src_error_entities or self._dst_error_entities:
+ elif self._src_error_entities or self._dst_error_entities:
self._print_error_entities()
else:
if (
@@ -511,6 +531,76 @@ class SpinoffContext:
if self._mode is self.Mode.UPDATE or self._mode is self.Mode.OVERRIDE:
self._write_gitignore()
+ def _do_describe_path(self) -> None:
+ assert self._describe_path is not None
+ path = self._describe_path
+
+ # Currently operating only on dst paths.
+ if path.startswith('/') and not path.startswith(self._dst_root):
+ raise CleanError('Please supply a path in the dst dir.')
+
+ # Allow abs paths.
+ path = path.removeprefix(f'{self._dst_root}/')
+
+ if self._src_error_entities or self._dst_error_entities:
+ print(
+ f'{Clr.RED}Note: Errors are present;'
+ f' this info may not be fully accurate.{Clr.RST}'
+ )
+ print(f'{Clr.BLD}dstpath: {Clr.BLU}{path}{Clr.RST}')
+
+ def _printval(name: Any, val: Any) -> None:
+ print(f' {name}: {Clr.BLU}{val}{Clr.RST}')
+
+ _printval('exists', os.path.exists(os.path.join(self._dst_root, path)))
+
+ # Adapted from code in _check_spinoff_managed_dirs.
+ managed = False
+ unchecked = False
+ git_mirrored = False
+
+ dstrootsl = f'{self._dst_root}/'
+ assert self._spinoff_managed_dirs is not None
+ for rdir in self._spinoff_managed_dirs:
+ for root, dirnames, fnames in os.walk(
+ os.path.join(self._dst_root, rdir),
+ topdown=True,
+ ):
+ # Completely ignore ignore-names in both dirs and files
+ # and cruft-file names in files.
+ for dirname in dirnames.copy():
+ if dirname in self.ignore_names:
+ dirnames.remove(dirname)
+ for fname in fnames.copy():
+ if (
+ fname in self.ignore_names
+ or fname in self.cruft_file_names
+ ):
+ fnames.remove(fname)
+
+ for fname in fnames:
+ dst_path_full = os.path.join(root, fname)
+ assert dst_path_full.startswith(dstrootsl)
+ dst_path = dst_path_full.removeprefix(dstrootsl)
+ if dst_path == path:
+ managed = True
+ if _any_path_contains(self._dst_unchecked_paths, dst_path):
+ unchecked = True
+ if _any_path_contains(self.git_mirrored_paths, dst_path):
+ git_mirrored = True
+ _printval(
+ 'spinoff-managed',
+ managed,
+ )
+ _printval(
+ 'unchecked',
+ unchecked,
+ )
+ _printval(
+ 'git-mirrored',
+ git_mirrored,
+ )
+
def _apply_project_configs(self) -> None:
# pylint: disable=exec-used
try:
@@ -744,9 +834,6 @@ class SpinoffContext:
# Strip out any sections frames by our strip-begin/end tags.
- # strip_tag_pairs: list[tuple[str, str]] = []
- # print('HELLO WORLD')
-
def _first_index_containing_string(
items: list[str], substring: str
) -> int | None:
@@ -795,7 +882,7 @@ class SpinoffContext:
'make any edits in source project)'
)
lines = self.default_filter_text(text).splitlines()
- return '\n'.join(lines[:1] + ['', blurb] + lines[1:])
+ return '\n'.join([blurb, ' '] + lines)
if 'Jenkinsfile' in src_path:
blurb = (
'// THIS FILE IS AUTOGENERATED BY SPINOFF;'
@@ -1028,8 +1115,10 @@ class SpinoffContext:
"""Print info about entity errors encountered."""
print(
'\nSpinoff Error(s) Found:\n'
- " Tip: to resolve 'spinoff-managed file modified' errors,\n"
- " use the 'backport' subcommand.\n",
+ " Tips: To resolve 'spinoff-managed file modified' errors,\n"
+ " use the 'backport' subcommand.\n"
+ " To debug other issues, try the 'describe-path'"
+ ' subcommand.\n',
file=sys.stderr,
)
for key, val in sorted(self._src_error_entities.items()):
@@ -1046,7 +1135,18 @@ class SpinoffContext:
print('')
def _validate_final_lists(self) -> None:
- """Make sure we never delete the few files we're letting git store."""
+ """Check some last things on our entities lists before we update."""
+
+ # Go through the final set of files we're syncing to dst and
+ # make sure none of them fall under our unchecked-paths list.
+ # That would mean we are writing a file but we're also declaring
+ # that we don't care if anyone else writes that file, which
+ # could lead to ambiguous/dangerous situations where spinoff as
+ # well as some command on dst write to the same file.
+ # print('CHECKING', self._src_copy_entities)
+ # for ent in self._src_copy_entities:
+ # if _any_path_contains(self._dst_unchecked_paths, ent):
+ # raise CleanError('FOUND BAD PATH', ent)
for ent in self._dst_purge_entities.copy():
if _any_path_contains(self.git_mirrored_paths, ent):
@@ -1806,7 +1906,8 @@ class SpinoffContext:
)
def _filter_src_git_file_list(self) -> None:
- # Crate a filtered version of src git files based on our omit entries.
+ # Create a filtered version of src git files based on our omit
+ # entries.
out = set[str]()
assert self._src_git_files is not None
for gitpath in self._src_git_files:
@@ -1958,7 +2059,7 @@ class SpinoffContext:
# In strict mode we want it to always be an error if dst mod-time
# varies from the version we wrote (we want to track down anyone
- # writing to our files who is not us).
+ # writing to our managed files who is not us).
# Note that we need to ignore git-mirrored-paths because git might
# be mucking with modtimes itself.
if (
diff --git a/tools/batools/spinoff/_main.py b/tools/batools/spinoff/_main.py
index 8c833cc6..db0fa80e 100644
--- a/tools/batools/spinoff/_main.py
+++ b/tools/batools/spinoff/_main.py
@@ -32,6 +32,7 @@ class Command(Enum):
CLEAN_CHECK = 'cleancheck'
OVERRIDE = 'override'
DIFF = 'diff'
+ DESCRIBE_PATH = 'describe-path'
BACKPORT = 'backport'
CREATE = 'create'
ADD_SUBMODULE_PARENT = 'add-submodule-parent'
@@ -94,6 +95,8 @@ def _main() -> None:
single_run_mode = SpinoffContext.Mode.CLEAN_CHECK
elif cmd is Command.DIFF:
single_run_mode = SpinoffContext.Mode.DIFF
+ elif cmd is Command.DESCRIBE_PATH:
+ single_run_mode = SpinoffContext.Mode.DESCRIBE_PATH
elif cmd is Command.OVERRIDE:
_do_override(src_root, dst_root)
elif cmd is Command.BACKPORT:
@@ -115,6 +118,12 @@ def _main() -> None:
assert_never(cmd)
if single_run_mode is not None:
+ from efrotools import extract_flag
+
+ args = sys.argv[2:]
+ force = extract_flag(args, '--force')
+ verbose = extract_flag(args, '--verbose')
+ print_full_lists = extract_flag(args, '--full')
if src_root is None:
if '--soft' in sys.argv:
return
@@ -123,6 +132,15 @@ def _main() -> None:
' you appear to be in a src project.'
" To silently no-op in this case, pass '--soft'."
)
+
+ describe_path: str | None
+ if single_run_mode is SpinoffContext.Mode.DESCRIBE_PATH:
+ if len(args) != 1:
+ raise CleanError(f'Expected a single path arg; got {args}.')
+ describe_path = args[0]
+ else:
+ describe_path = None
+
# SpinoffContext should never be relying on relative paths, so let's
# keep ourself honest by making sure.
os.chdir('/')
@@ -130,9 +148,10 @@ def _main() -> None:
src_root,
dst_root,
single_run_mode,
- force='--force' in sys.argv,
- verbose='--verbose' in sys.argv,
- print_full_lists='--full' in sys.argv,
+ force=force,
+ verbose=verbose,
+ print_full_lists=print_full_lists,
+ describe_path=describe_path,
).run()
@@ -581,6 +600,8 @@ def _print_available_commands() -> None:
'Remove files from spinoff, leaving local copies in place.\n'
f' {bgn}backport [file]{end} '
'Help get changes to spinoff dst files back to src.\n'
+ f' {bgn}describe-path [path]{end}'
+ ' Tells whether a path is spinoff-managed/etc.\n'
f' {bgn}create [name, path]{end} '
'Create a new spinoff project based on this src one.\n'
' Name should be passed in CamelCase form.\n'
diff --git a/tools/efro/dataclassio/_outputter.py b/tools/efro/dataclassio/_outputter.py
index b084cfeb..03e1d20f 100644
--- a/tools/efro/dataclassio/_outputter.py
+++ b/tools/efro/dataclassio/_outputter.py
@@ -165,7 +165,6 @@ class _Outputter:
)
return value if self._create else None
- # noinspection PyPep8
if origin is typing.Union or origin is types.UnionType:
# Currently, the only unions we support are None/Value
# (translated from Optional), which we verified on prep.