diff --git a/.efrocachemap b/.efrocachemap
index 7c625ca3..69a613c4 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": "af600306cf8085f909e40a7d2129a73b",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "54f7cdbdbc16601ff1f841e48cecf05c",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "cc6387a5d3a8f36b7bf667f9b7e719df",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "0de15cf59051ba17ee91e3d3ac25be93",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "345c4646377145cf88b30fcba122f42b",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "4dae703893e19bed1e8a16029a646104",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "573adb2913309a6c375a6d3b92a20208",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "3db3c7e41182fd05f5f9731fee3002d0",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "1e3e5ee34645928ec3bdc3c52de98839",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "41354efa8cdbb83a3b2e4cd7fce6b308",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "a304c1a0d2d2f5bdbc31a27ec899a95b",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "6b236e3246ded0460fcc7ea50fc51b36",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "e26b4e3b5e2bca5606d2ac674fcc7496",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "ae954a12775213c64d45b535b289ebff",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "960c3e2fd9c3e19de75fd92546447890",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "e0c05531b48ec5e36da10783280957fe",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "e1c137fdefcf34a642c0962999973eff",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "bdf75b68e6b0e1c8dcfa76ac04306fbb",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "5f01ab596c3d389c95761db531b1766b",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "4826ca9c757dbf38749710bc94844aca",
+ "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": "99e44969ef0f9421f21cb32554464bc7",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "a14441d4ff9adae6e015a9f0d107a61a",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "90b5c353d5296be7e3b06e8c823564bb",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "84409ce44260a641295c1459e4d3af8f",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "35c58dd0a7482bb5f1ef5d6edb647c46",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "a7c2895f59f75b767258277c766e9ed4",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "e0430eeeb324ccfca8ace41d743e069c",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "d594057005fc56c8576f555f892b3dc4",
"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 968333b3..fa184489 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 21453, api 8, 2023-10-13)
- 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,20 @@
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.
+
### 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/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..363daa00 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
@@ -508,7 +510,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 +555,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()
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..d9117788 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 = 21453
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_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/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/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/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..6c182345 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 = 21453;
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..cb2051c5 100644
--- a/src/ballistica/shared/foundation/event_loop.cc
+++ b/src/ballistica/shared/foundation/event_loop.cc
@@ -248,24 +248,24 @@ void EventLoop::WaitForNextEvent_(bool single_cycle) {
}
}
-void EventLoop::LoopUpkeep_(bool single_cycle) {
- assert(g_core);
- // Keep our autorelease pool clean on mac/ios
- // FIXME: Should define a CorePlatform::ThreadHelper or something
- // so we don't have platform-specific code here.
-#if BA_XCODE_BUILD
- // Let's not do autorelease pools when being called ad-hoc,
- // since in that case we're part of another run loop
- // (and its crashing on drain for some reason)
- if (!single_cycle) {
- if (auto_release_pool_) {
- g_core->platform->DrainAutoReleasePool(auto_release_pool_);
- auto_release_pool_ = nullptr;
- }
- auto_release_pool_ = g_core->platform->NewAutoReleasePool();
- }
-#endif
-}
+// void EventLoop::LoopUpkeep_(bool single_cycle) {
+// assert(g_core);
+// // Keep our autorelease pool clean on mac/ios
+// // FIXME: Should define a CorePlatform::ThreadHelper or something
+// // so we don't have platform-specific code here.
+// #if BA_XCODE_BUILD
+// // Let's not do autorelease pools when being called ad-hoc,
+// // since in that case we're part of another run loop
+// // (and its crashing on drain for some reason)
+// if (!single_cycle) {
+// if (auto_release_pool_) {
+// g_core->platform->DrainAutoReleasePool(auto_release_pool_);
+// auto_release_pool_ = nullptr;
+// }
+// auto_release_pool_ = g_core->platform->NewAutoReleasePool();
+// }
+// #endif
+// }
void EventLoop::RunToCompletion() { Run_(false); }
void EventLoop::RunSingleCycle() { Run_(true); }
@@ -273,7 +273,7 @@ void EventLoop::RunSingleCycle() { Run_(true); }
void EventLoop::Run_(bool single_cycle) {
assert(g_core);
while (true) {
- LoopUpkeep_(single_cycle);
+ // LoopUpkeep_(single_cycle);
WaitForNextEvent_(single_cycle);
diff --git a/src/ballistica/shared/foundation/event_loop.h b/src/ballistica/shared/foundation/event_loop.h
index c8d93ac2..9c6c6457 100644
--- a/src/ballistica/shared/foundation/event_loop.h
+++ b/src/ballistica/shared/foundation/event_loop.h
@@ -118,7 +118,7 @@ class EventLoop {
auto CheckPushRunnableSafety_() -> bool;
void SetInternalThreadName_(const std::string& name);
void WaitForNextEvent_(bool single_cycle);
- void LoopUpkeep_(bool single_cycle);
+ // void LoopUpkeep_(bool single_cycle);
void LogThreadMessageTally_(
std::vector>* log_entries);
void PushLocalRunnable_(Runnable* runnable, bool* completion_flag);
@@ -155,32 +155,26 @@ class EventLoop {
void BootstrapThread_();
+ 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_{};
- 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_{};
-
- // FIXME: Should generalize this to some sort of PlatformThreadData class.
-#if BA_XCODE_BUILD
- void* auto_release_pool_{};
-#endif
-
- bool bootstrapped_{};
+ 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/types.h b/src/ballistica/shared/foundation/types.h
index 32989060..eed5c225 100644
--- a/src/ballistica/shared/foundation/types.h
+++ b/src/ballistica/shared/foundation/types.h
@@ -68,7 +68,7 @@ class Graphics;
///
/// Category: Enums
///
-enum class InputType {
+enum class InputType : uint8_t {
kUpDown = 2,
kLeftRight,
kJumpPress,
@@ -111,7 +111,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 +139,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 +162,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 +173,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 +183,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 +192,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 +288,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 +306,7 @@ enum class PyExcType {
kWidgetNotFound
};
-enum class LogLevel {
+enum class LogLevel : uint8_t {
kDebug,
kInfo,
kWarning,
@@ -314,7 +314,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 +323,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/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.