Merge branch 'efroemling:master' into master

This commit is contained in:
Vishal 2023-10-15 13:05:03 +05:30 committed by GitHub
commit 847b2c8e05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1274 additions and 850 deletions

74
.efrocachemap generated
View File

@ -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/achievement.ogg": "079a366ce183b25a63550ef7072af605",
"build/assets/ba_data/audio/actionHero1.ogg": "f0f986f268f036a5ac2f940e07f2f27e", "build/assets/ba_data/audio/actionHero1.ogg": "f0f986f268f036a5ac2f940e07f2f27e",
"build/assets/ba_data/audio/actionHero2.ogg": "204a6735dc655f0975cf8308b585f2fd", "build/assets/ba_data/audio/actionHero2.ogg": "204a6735dc655f0975cf8308b585f2fd",
@ -4056,50 +4056,50 @@
"build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1", "build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1",
"build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae", "build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "4129fd84c3c64b770c4343d347bff97a", "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "8ea626f6dd70d998ee77c58fffc51545",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "76b293b2d942716c2606c56e13483e66", "build/prefab/full/linux_arm64_gui/release/ballisticakit": "d7ad10904fe7c4d4555366ccb1feedcb",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "9cc2a5d464da1e0822ecf5493ac28b64", "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "a9eea92d521e97b1772b5e44b402ce8a",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "40e605e595f00286805cf15ffc096813", "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "57043f40971d800d27ee6d646aae8d61",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "bdc47e8dd94bcfc2a7b8268ea366f9b5", "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "3e4009d7fa4b90abc526f56361ecab05",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "6b57b2de2d4aefcb3a5f7df6cef53a9d", "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "334cdc0688e66a1bc75cd05bae1729c7",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "34c65f2a9f280420387af135d5bc6a2d", "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "95278d80378be5b61026253492cbfa70",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "79e4d857fbd0871940c7fd6985d11af1", "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "0a3da8e5264a7b733960e83a0e8c4bba",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "ce179db11df98c7dc53bd2fd2a710909", "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "a089d4aaa39e18553cf0a70a77b4cfcd",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "d47c86f2654185ab0690949c3b8f2913", "build/prefab/full/mac_arm64_gui/release/ballisticakit": "e66c4eceb79710b8fda2bfea781e241a",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "a33fb01bdbeb7be34046d7b64673991c", "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "6b3021b9a7584da86bbb95324e81e851",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "5f4cb90415aed9415231e95394384d2a", "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "6fe90d50f905a0da9fa52c39a458d1e3",
"build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "26540ab81b0ad717818e077efcb9029d", "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "afc7ed826486aec82613832865177570",
"build/prefab/full/mac_x86_64_gui/release/ballisticakit": "11fc4a0cdf83c362b2f5c0c62a53014e", "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "ae8b6dad770793188aad8e2966189bb3",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "ba10cfebc435f211be18dbdc7416097d", "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "7622dde1a021152cac42e7db3e803392",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "551b8dba96bfc65c5166cde6bec37539", "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "8a4b1e521bf668cc6ec6a65519defb12",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "bc18f0765c9d96731a1b1a7acf5151db", "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "9a69c0d4c9ae319595843b16f14795fc",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "1f1a8227190894db122fb43691371a92", "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "89c02260fb4781f5e293658cecbb363f",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "9d546413ee365044d77e0e9c39ed77bb", "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "3165d4230069c22300abfff8abf1d714",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "470b427c4e51254538291ac56298c212", "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "41e46dfdbb542e3e823f6aee87e93ac9",
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "a61cf6ac9afd43081df7f63ff81f4036", "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_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_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_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/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_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_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_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/mac_x86_64_server/release/libballisticaplus.a": "c8715c85010ea431d7346f40f5421819",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "dfd5dca061d6eacaccc38c12d391cc82", "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "750c2964308cd3f3e5986fcda9a25706",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "96ab39f16820a39fa7c42af6779c3556", "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "772af7da6a115f53b0b3f6a4afd3baec",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "d87f20b0a0192b90687b936b6c2ad103", "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "178c1a53a7ad50297aed68d0ca3a1476",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "4a336c3e976924b70b39d18af6040e41", "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "b011bc2b6437995f2d33f5215b4ffa36",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "77d10f03566f29816de9b2ff806cc905", "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "a758a4f98336208381b093aacb735878",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "d90ef473768f7ba232ae3ca58c5c8c04", "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "6c86545fab2327105114676a20ca5e68",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "743d872f2c848a84e4e5d49ca6b426e9", "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "f75525fdc9f7db4a81ca9bae6a79add5",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "36272a00b45729022167baa81749a37e", "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "2e297069baec404d43ccdb18abeef658",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c", "src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "28323912b56ec07701eda3d41a6a4101", "src/assets/ba_data/python/babase/_mgen/enums.py": "28323912b56ec07701eda3d41a6a4101",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "ba8ce3ca3858b4c2d20db68f99b788b2", "src/ballistica/base/mgen/pyembed/binding_base.inc": "ba8ce3ca3858b4c2d20db68f99b788b2",

2
.gitignore vendored
View File

@ -120,10 +120,10 @@ xcuserdata/
/ballisticakit-android/BallisticaKit/src/main/res/mipmap-*/ic_launcher*.png /ballisticakit-android/BallisticaKit/src/main/res/mipmap-*/ic_launcher*.png
/ballisticakit-android/BallisticaKit/src/cardboard/res/mipmap-*/ic_launcher*.png /ballisticakit-android/BallisticaKit/src/cardboard/res/mipmap-*/ic_launcher*.png
BallisticaKit.ico 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 iOS.appiconset/icon_*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/AppIcon macOS.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 - 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/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 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/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/Cursor macOS.imageset/cursor_*.png

2
.idea/misc.xml generated
View File

@ -4,7 +4,7 @@
<option name="cmdArguments" value="--line-length 80 --skip-string-normalization" /> <option name="cmdArguments" value="--line-length 80 --skip-string-normalization" />
<option name="enabledOnReformat" value="true" /> <option name="enabledOnReformat" value="true" />
<option name="pathToExecutable" value="/opt/homebrew/bin/black" /> <option name="pathToExecutable" value="/opt/homebrew/bin/black" />
<option name="sdkUUID" value="1b270adb-5261-4492-85e8-d79b3894255d" /> <option name="sdkName" value="Python 3.11" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser"> <component name="PythonCompatibilityInspectionAdvertiser">

View File

@ -1,4 +1,4 @@
### 1.7.28 (build 21447, api 8, 2023-10-12) ### 1.7.28 (build 21465, api 8, 2023-10-14)
- Massively cleaned up code related to rendering and window systems (OpenGL, - 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 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 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 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 feel it is time. As mentioned above, this allows massively cleaning up the
graphics code which means we can start to improve it. graphics code which means we can start to improve it. Ideally now the GL
- Removed gamma controls. These were only active on the old Mac version anyway 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 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. 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 - Implemented both vsync and max-fps for the SDL build of the game. This means
@ -129,7 +131,22 @@
before. It also takes a `confirm` bool arg which allows it to be used to bring before. It also takes a `confirm` bool arg which allows it to be used to bring
up a confirm dialog. up a confirm dialog.
- Clicking on a window close button to quit no longer brings up a confirm dialog - Clicking on a window close button to quit no longer brings up a confirm dialog
and instead quits immediately (though with a proper graceful shutdown). and instead quits immediately (though with a proper graceful shutdown and a
lovely little fade).
- Camera shake is now supported in network games and replays. Somehow I didn't
notice that was missing for years. The downside is this requires a server to
be hosting protocol 35, which cuts off support for 1.4 clients. So for now I
am keeping the default at 33. Once there a fewer 1.4 clients around we can
consider changing this (if everything hasn't moved to SceneV2 by then).
- Added a server option to set the hosting protocol for servers who might want
to allow camera shake (or other minor features/fixes) that don't work in the
default protocol 33. See `protocol_version` in `config.yaml`. Just remember
that you will be cutting off support for older clients if you use 35.
- Fixed a bug with screen-messages animating off screen too fast when frame
rates are high.
- Added a proper graceful shutdown process for the audio server. This should
result in fewer ugly pops and warning messages when the app is quit.
### 1.7.27 (build 21282, api 8, 2023-08-30) ### 1.7.27 (build 21282, api 8, 2023-08-30)

View File

@ -779,13 +779,12 @@ check-full: py_check_prereqs
# Same as 'check' plus optional/slow extra checks. # Same as 'check' plus optional/slow extra checks.
check2: py_check_prereqs 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! @$(PCOMMANDBATCH) echo SGRN BLD ALL CHECKS PASSED!
# Same as check2 but no caching (all files are checked). # Same as check2 but no caching (all files are checked).
check2-full: py_check_prereqs check2-full: py_check_prereqs
@$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full \ @$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full
pycharm-full
@$(PCOMMANDBATCH) echo SGRN BLD ALL CHECKS PASSED! @$(PCOMMANDBATCH) echo SGRN BLD ALL CHECKS PASSED!
# Run Cpplint checks on all C/C++ code. # Run Cpplint checks on all C/C++ code.
@ -924,14 +923,14 @@ preflight-full:
preflight2: preflight2:
@$(MAKE) format @$(MAKE) format
@$(MAKE) update @$(MAKE) update
@$(MAKE) -j$(CPUS) cpplint pylint mypy pycharm test @$(MAKE) -j$(CPUS) cpplint pylint mypy test
@$(PCOMMANDBATCH) echo SGRN BLD PREFLIGHT SUCCESSFUL! @$(PCOMMANDBATCH) echo SGRN BLD PREFLIGHT SUCCESSFUL!
# Same as 'preflight2' but without caching (all files visited). # Same as 'preflight2' but without caching (all files visited).
preflight2-full: preflight2-full:
@$(MAKE) format-full @$(MAKE) format-full
@$(MAKE) update @$(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! @$(PCOMMANDBATCH) echo SGRN BLD PREFLIGHT SUCCESSFUL!
# Tell make which of these targets don't represent files. # Tell make which of these targets don't represent files.

View File

@ -14,7 +14,6 @@
"src/ballistica/core/platform/android/utf8/checked.h", "src/ballistica/core/platform/android/utf8/checked.h",
"src/ballistica/core/platform/android/utf8/unchecked.h", "src/ballistica/core/platform/android/utf8/unchecked.h",
"src/ballistica/core/platform/android/utf8/core.h", "src/ballistica/core/platform/android/utf8/core.h",
"src/ballistica/base/platform/apple/sdl_main_mac.h",
"src/ballistica/base/platform/oculus/main_rift.cc", "src/ballistica/base/platform/oculus/main_rift.cc",
"src/ballistica/core/platform/android/android_gl3.c" "src/ballistica/core/platform/android/android_gl3.c"
], ],

View File

@ -53,47 +53,58 @@ ctx.src_omit_paths = {
'src/assets/workspace', 'src/assets/workspace',
} }
# Use this to 'carve out' directories or exact file paths which will be # Use this to 'carve out' files or directories which will be git-managed
# git-managed on dst. By default, spinoff will consider dirs containing # on dst.
# 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 # By default, spinoff will consider dirs containing the files it syncs
# not manage itself (to prevent accidentally doing work in such places). # from src as 'spinoff-managed'; it will set them as git-ignored and
# Note that adding a dir to src_write_paths does not prevent files # will complain if any files appear in them that it does not manage
# within it from being synced by spinoff; it just means that each of # itself (to prevent accidentally doing work in such places). Note that
# those individual spinoff-managed files will have their own gitignore # adding a dir to src_write_paths does not prevent files within it from
# entry since there is no longer one covering the whole dir. So to keep # being synced by spinoff; it just means that each of those individual
# things tidy, carve out the minimal set of exact file/dir paths that # spinoff-managed files will have their own gitignore entry since there
# you need. # 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 = { ctx.src_write_paths = {
'tools/spinoff', 'tools/spinoff',
'config/spinoffconfig.py', 'config/spinoffconfig.py',
} }
# Normally spinoff errors if it finds any files in its managed dirs that # Use this to 'carve out' files or directories under spinoff managed
# it did not put there. This is to prevent accidentally working in these # dirs which will be completely ignored by spinoff (but *not* placed
# parts of a dst project; since these sections are git-ignored, git # under git control).
# itself won't raise any warnings in such cases and it would be easy to
# accidentally lose work otherwise.
# #
# This list can be used to suppress spinoff's errors for specific # Normally spinoff will error if it finds any files under its managed
# locations. This is generally used to allow build output or other # dirs that it did not put there. This is to prevent accidentally
# dynamically generated files to exist within spinoff-managed # working in these parts of a dst project; since spinoff-controlled
# directories. It is possible to use src_write_paths for such purposes, # stuff is git-ignored, git itself won't raise any warnings in such
# but this has the side-effect of greatly complicating the dst project's # cases and it would be easy to accidentally blow away changes if
# gitignore list; selectively marking a few dirs as unchecked makes for # spinoff didn't raise a stink.
# a cleaner setup. Just be careful to not set excessively broad regions #
# as unchecked; you don't want to mask actual useful error messages. # 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 = { ctx.src_unchecked_paths = {
'src/ballistica/mgen', 'src/ballistica/mgen',
'src/ballistica/*/mgen', 'src/ballistica/*/mgen',
'src/assets/ba_data/python/*/_mgen', 'src/assets/ba_data/python/*/_mgen',
'src/meta/*/mgen', 'src/meta/*/mgen',
'ballisticakit-cmake/.clang-format', 'ballisticakit-cmake/.clang-format',
'ballisticakit-android/BallisticaKit/src/cardboard/res',
'ballisticakit-windows/*/BallisticaKit.ico', 'ballisticakit-windows/*/BallisticaKit.ico',
'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets', 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*.png',
'ballisticakit-android/BallisticaKit/src/*/res', 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*/*.png',
'ballisticakit-android/BallisticaKit/src/*/assets', '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/local.properties',
'ballisticakit-android/.gradle', 'ballisticakit-android/.gradle',
'ballisticakit-android/build', 'ballisticakit-android/build',

View File

@ -56,6 +56,8 @@ class App:
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
# A few things defined as non-optional values but not actually
# available until the app starts.
plugins: PluginSubsystem plugins: PluginSubsystem
lang: LanguageSubsystem lang: LanguageSubsystem
health_monitor: AppHealthMonitor health_monitor: AppHealthMonitor
@ -92,7 +94,7 @@ class App:
# Used on platforms such as mobile where the app basically needs # Used on platforms such as mobile where the app basically needs
# to shut down while backgrounded. In this state, all event # 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 # completely. Be aware that the suspended state can be entered
# from any other state including NATIVE_BOOTSTRAPPING and # from any other state including NATIVE_BOOTSTRAPPING and
# SHUTTING_DOWN. # SHUTTING_DOWN.
@ -149,9 +151,9 @@ class App:
def __init__(self) -> None: def __init__(self) -> None:
"""(internal) """(internal)
Do not instantiate this class; access the single shared instance Do not instantiate this class. You can access the single shared
of it as 'app' which is available in various Ballistica instance of it through various high level packages: 'babase.app',
feature-set modules such as babase. 'bascenev1.app', 'bauiv1.app', etc.
""" """
# Hack for docs-generation: we can be imported with dummy modules # Hack for docs-generation: we can be imported with dummy modules
@ -208,7 +210,8 @@ class App:
self._shutdown_task: asyncio.Task[None] | None = None self._shutdown_task: asyncio.Task[None] | None = None
self._shutdown_tasks: list[Coroutine[None, None, None]] = [ self._shutdown_tasks: list[Coroutine[None, None, None]] = [
self._wait_for_shutdown_suppressions(), self._wait_for_shutdown_suppressions(),
self._fade_for_shutdown(), self._fade_and_shutdown_graphics(),
self._fade_and_shutdown_audio(),
] ]
self._pool_thread_count = 0 self._pool_thread_count = 0
@ -508,7 +511,7 @@ class App:
except Exception: except Exception:
logging.exception('Error setting app intent to %s.', intent) logging.exception('Error setting app intent to %s.', intent)
_babase.pushcall( _babase.pushcall(
tpartial(self._apply_intent_error, intent), tpartial(self._display_set_intent_error, intent),
from_other_thread=True, from_other_thread=True,
) )
@ -553,10 +556,11 @@ class App:
'Error handling intent %s in app-mode %s.', intent, mode '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 from babase._language import Lstr
del intent # Unused. del intent
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
_babase.getsimplesound('error').play() _babase.getsimplesound('error').play()
@ -795,6 +799,7 @@ class App:
async def _shutdown(self) -> None: async def _shutdown(self) -> None:
import asyncio import asyncio
_babase.lock_all_input()
try: try:
async with asyncio.TaskGroup() as task_group: async with asyncio.TaskGroup() as task_group:
for task_coro in self._shutdown_tasks: for task_coro in self._shutdown_tasks:
@ -895,18 +900,26 @@ class App:
await asyncio.sleep(0.001) await asyncio.sleep(0.001)
_babase.lifecyclelog('shutdown-suppress wait end') _babase.lifecyclelog('shutdown-suppress wait end')
async def _fade_for_shutdown(self) -> None: async def _fade_and_shutdown_graphics(self) -> None:
import asyncio import asyncio
# Kick off a fade, block input, and wait for a short bit. # Kick off a short fade and give it time to complete.
# Ideally most shutdown activity completes during the fade so _babase.lifecyclelog('fade-and-shutdown-graphics begin')
# there's no tangible wait.
_babase.lifecyclelog('fade-for-shutdown begin')
_babase.fade_screen(False, time=0.15) _babase.fade_screen(False, time=0.15)
_babase.lock_all_input()
# _babase.getsimplesound('swish2').play()
await asyncio.sleep(0.15) await asyncio.sleep(0.15)
_babase.lifecyclelog('fade-for-shutdown end') _babase.lifecyclelog('fade-and-shutdown-graphics end')
async def _fade_and_shutdown_audio(self) -> None:
import asyncio
# Tell the audio system to go down and give it a bit of
# time to do so gracefully.
_babase.lifecyclelog('fade-and-shutdown-audio begin')
_babase.audio_shutdown_begin()
await asyncio.sleep(0.15)
while not _babase.audio_shutdown_is_complete():
await asyncio.sleep(0.01)
_babase.lifecyclelog('fade-and-shutdown-audio end')
def _threadpool_no_wait_done(self, fut: Future) -> None: def _threadpool_no_wait_done(self, fut: Future) -> None:
try: try:

View File

@ -302,6 +302,11 @@ class AccountV1Subsystem:
"""(internal)""" """(internal)"""
plus = babase.app.plus plus = babase.app.plus
if plus is None: if plus is None:
import logging
logging.warning(
'Error adding pending promo code; plus not present.'
)
babase.screenmessage( babase.screenmessage(
babase.Lstr(resource='errorText'), color=(1, 0, 0) babase.Lstr(resource='errorText'), color=(1, 0, 0)
) )

View File

@ -52,7 +52,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be # Build number and version of the ballistica binary we expect to be
# using. # using.
TARGET_BALLISTICA_BUILD = 21447 TARGET_BALLISTICA_BUILD = 21465
TARGET_BALLISTICA_VERSION = '1.7.28' TARGET_BALLISTICA_VERSION = '1.7.28'

View File

@ -1184,6 +1184,9 @@ class AccountSettingsWindow(bui.Window):
self, response: bacommon.cloud.ManageAccountResponse | Exception self, response: bacommon.cloud.ManageAccountResponse | Exception
) -> None: ) -> None:
if isinstance(response, Exception) or response.url is 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.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
bui.getsound('error').play() bui.getsound('error').play()
return return
@ -1466,6 +1469,7 @@ class AccountSettingsWindow(bui.Window):
if isinstance(result, Exception): if isinstance(result, Exception):
# For now just make a bit of noise if anything went wrong; # For now just make a bit of noise if anything went wrong;
# can get more specific as needed later. # 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.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
bui.getsound('error').play() bui.getsound('error').play()
else: else:

View File

@ -15,8 +15,8 @@ from threading import Lock, Thread, current_thread
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
# We make use of the bacommon and efro packages as well as site-packages # 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 # included with our bundled Ballistica dist, so we need to add those
# before we import them. # paths before we import them.
sys.path += [ sys.path += [
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')), str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')),
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')), str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')),
@ -34,31 +34,55 @@ if TYPE_CHECKING:
VERSION_STR = '1.3.1' VERSION_STR = '1.3.1'
# Version history: # Version history:
#
# 1.3.1 # 1.3.1
# Windows binary is now named BallisticaKitHeadless.exe #
# - Windows binary is now named 'BallisticaKitHeadless.exe'.
#
# 1.3: # 1.3:
# Added show_tutorial config option #
# Added team_names config option # - Added show_tutorial config option.
# Added team_colors config option #
# Added playlist_inline config option # - Added team_names config option.
#
# - Added team_colors config option.
#
# - Added playlist_inline config option.
#
# 1.2: # 1.2:
# Added optional --help arg #
# Added --config arg for specifying config file and --root for ba_root path # - Added optional --help arg.
# Added noninteractive mode and --interactive/--noninteractive args to #
# explicitly enable/disable it (it is autodetected by default) # - Added --config arg for specifying config file and --root for
# Added explicit control for auto-restart: --no-auto-restart # ba_root path.
# Config file is now reloaded each time server binary is restarted; no more #
# need to bring down server wrapper to pick up changes # - Added noninteractive mode and --interactive/--noninteractive args
# Now automatically restarts server binary when config file is modified # to explicitly enable/disable it (it is autodetected by default).
# (use --no-config-auto-restart to disable that behavior) #
# - 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: # 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: # 1.1.0:
# Added shutdown command #
# Changed restart to default to immediate=True # - Added shutdown command.
# Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes #
# - Changed restart to default to immediate=True.
#
# - Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes.
#
# 1.0.0: # 1.0.0:
# Initial release #
# - Initial release.
class ServerManagerApp: class ServerManagerApp:
@ -101,8 +125,9 @@ class ServerManagerApp:
# This may override the above defaults. # This may override the above defaults.
self._parse_command_line_args() self._parse_command_line_args()
# Do an initial config-load. If the config is invalid at this point # Do an initial config-load. If the config is invalid at this
# we can cleanly die (we're more lenient later on reloads). # point we can cleanly die (we're more lenient later on
# reloads).
self.load_config(strict=True, print_confirmation=False) self.load_config(strict=True, print_confirmation=False)
@property @property
@ -131,9 +156,9 @@ class ServerManagerApp:
) )
# Python will handle SIGINT for us (as KeyboardInterrupt) but we # Python will handle SIGINT for us (as KeyboardInterrupt) but we
# need to register a SIGTERM handler so we have a chance to clean # need to register a SIGTERM handler so we have a chance to
# up our subprocess when someone tells us to die. (and avoid # clean up our subprocess when someone tells us to die. (and
# zombie processes) # avoid zombie processes)
signal.signal(signal.SIGTERM, self._handle_term_signal) signal.signal(signal.SIGTERM, self._handle_term_signal)
# During a run, we make the assumption that cwd is the dir # 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 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._done = True
self._subprocess_thread.join() self._subprocess_thread.join()
@ -181,9 +207,10 @@ class ServerManagerApp:
# Gracefully bow out if we kill ourself via keyboard. # Gracefully bow out if we kill ourself via keyboard.
pass pass
except SystemExit: except SystemExit:
# We get this from the builtin quit(), our signal handler, etc. # We get this from the builtin quit(), our signal handler,
# Need to catch this so we can clean up, otherwise we'll be # etc. Need to catch this so we can clean up, otherwise
# left in limbo with our process thread still running. # we'll be left in limbo with our process thread still
# running.
pass pass
self._postrun() self._postrun()
@ -207,14 +234,17 @@ class ServerManagerApp:
self._enable_tab_completion(context) self._enable_tab_completion(context)
# Now just sit in an interpreter. # 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: try:
self._interpreter_start_time = time.time() self._interpreter_start_time = time.time()
code.interact(local=context, banner='', exitmsg='') code.interact(local=context, banner='', exitmsg='')
except SystemExit: except SystemExit:
# We get this from the builtin quit(), our signal handler, etc. # We get this from the builtin quit(), our signal handler,
# Need to catch this so we can clean up, otherwise we'll be # etc. Need to catch this so we can clean up, otherwise
# left in limbo with our process thread still running. # we'll be left in limbo with our process thread still
# running.
pass pass
except BaseException as exc: except BaseException as exc:
print( print(
@ -238,19 +268,21 @@ class ServerManagerApp:
self._block_for_command_completion() self._block_for_command_completion()
def _block_for_command_completion(self) -> None: def _block_for_command_completion(self) -> None:
# Ideally we'd block here until the command was run so our prompt would # Ideally we'd block here until the command was run so our
# print after it's results. We currently don't get any response from # prompt would print after it's results. We currently don't get
# the app so the best we can do is block until our bg thread has sent # any response from the app so the best we can do is block until
# it. In the future we can perhaps add a proper 'command port' # our bg thread has sent it. In the future we can perhaps add a
# interface for proper blocking two way communication. # proper 'command port' interface for proper blocking two way
# communication.
while True: while True:
with self._subprocess_commands_lock: with self._subprocess_commands_lock:
if not self._subprocess_commands: if not self._subprocess_commands:
break break
time.sleep(0.1) time.sleep(0.1)
# One last short delay so if we come out *just* as the command is sent # One last short delay so if we come out *just* as the command
# we'll hopefully still give it enough time to process/print. # is sent we'll hopefully still give it enough time to
# process/print.
time.sleep(0.1) time.sleep(0.1)
def screenmessage( def screenmessage(
@ -320,8 +352,8 @@ class ServerManagerApp:
) )
) )
# If we're asking for an immediate restart but don't get one within # If we're asking for an immediate restart but don't get one
# the grace period, bring down the hammer. # within the grace period, bring down the hammer.
if immediate: if immediate:
self._subprocess_force_kill_time = ( self._subprocess_force_kill_time = (
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
@ -340,12 +372,12 @@ class ServerManagerApp:
ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate) ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate)
) )
# An explicit shutdown means we know to bail completely once this # An explicit shutdown means we know to bail completely once
# subprocess completes. # this subprocess completes.
self._wrapper_shutdown_desired = True self._wrapper_shutdown_desired = True
# If we're asking for an immediate shutdown but don't get one within # If we're asking for an immediate shutdown but don't get one
# the grace period, bring down the hammer. # within the grace period, bring down the hammer.
if immediate: if immediate:
self._subprocess_force_kill_time = ( self._subprocess_force_kill_time = (
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
@ -378,9 +410,10 @@ class ServerManagerApp:
if i + 1 >= argc: if i + 1 >= argc:
raise CleanError('Expected a path as next arg.') raise CleanError('Expected a path as next arg.')
path = sys.argv[i + 1] path = sys.argv[i + 1]
# Unlike config_path, this one doesn't have to exist now. # Unlike config_path, this one doesn't have to exist
# We do however need an abs path because we may be in a # now. We do however need an abs path because we may be
# different cwd currently than we will be during the run. # in a different cwd currently than we will be during
# the run.
self._ba_root_path = os.path.abspath(path) self._ba_root_path = os.path.abspath(path)
i += 2 i += 2
elif arg == '--interactive': elif arg == '--interactive':
@ -538,6 +571,7 @@ class ServerManagerApp:
if not os.path.exists(self._config_path): if not os.path.exists(self._config_path):
# Special case: # Special case:
#
# If the user didn't specify a particular config file, allow # If the user didn't specify a particular config file, allow
# gracefully falling back to defaults if the default one is # gracefully falling back to defaults if the default one is
# missing. # missing.
@ -606,24 +640,26 @@ class ServerManagerApp:
"""Spin up the server subprocess and run it until exit.""" """Spin up the server subprocess and run it until exit."""
# pylint: disable=consider-using-with # pylint: disable=consider-using-with
# Reload our config, and update our overall behavior based on it. # Reload our config, and update our overall behavior based on
# We do non-strict this time to give the user repeated attempts if # it. We do non-strict this time to give the user repeated
# if they mess up while modifying the config on the fly. # attempts if if they mess up while modifying the config on the
# fly.
self.load_config(strict=False, print_confirmation=True) self.load_config(strict=False, print_confirmation=True)
self._prep_subprocess_environment() self._prep_subprocess_environment()
# Launch the binary and grab its stdin; # Launch the binary and grab its stdin; we'll use this to feed
# we'll use this to feed it commands. # it commands.
self._subprocess_launch_time = time.time() self._subprocess_launch_time = time.time()
# Set an environment var so the server process knows its being # Set an environment var so the server process knows its being
# run under us. This causes it to ignore ctrl-c presses and other # run under us. This causes it to ignore ctrl-c presses and
# slight behavior tweaks. Hmm; should this be an argument instead? # other slight behavior tweaks. Hmm; should this be an argument
# instead?
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1' os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
# Set an environment var to change the device name. # Set an environment var to change the device name. Device name
# Device name is used while making connection with master server, # is used while making connection with master server,
# cloud-console recognize us with this name. # cloud-console recognize us with this name.
os.environ['BA_DEVICE_NAME'] = self._config.party_name os.environ['BA_DEVICE_NAME'] = self._config.party_name
@ -663,9 +699,10 @@ class ServerManagerApp:
assert self._subprocess_exited_cleanly is not None assert self._subprocess_exited_cleanly is not None
# EW: it seems that if we die before the main thread has fully started # EW: it seems that if we die before the main thread has fully
# up the interpreter, its possible that it will not break out of its # started up the interpreter, its possible that it will not
# loop via the usual SystemExit that gets sent when we die. # break out of its loop via the usual SystemExit that gets sent
# when we die.
if self._interactive: if self._interactive:
while ( while (
self._interpreter_start_time is None self._interpreter_start_time is None
@ -694,8 +731,8 @@ class ServerManagerApp:
# tell the main thread to die. # tell the main thread to die.
if self._wrapper_shutdown_desired: if self._wrapper_shutdown_desired:
# Only do this if the main thread is not already waiting for # Only do this if the main thread is not already waiting for
# us to die; otherwise it can lead to deadlock. # us to die; otherwise it can lead to deadlock. (we hang in
# (we hang in os.kill while main thread is blocked in Thread.join) # os.kill while main thread is blocked in Thread.join)
if not self._done: if not self._done:
self._done = True self._done = True
@ -721,6 +758,8 @@ class ServerManagerApp:
bincfg['Auto Balance Teams'] = self._config.auto_balance_teams bincfg['Auto Balance Teams'] = self._config.auto_balance_teams
bincfg['Show Tutorial'] = self._config.show_tutorial 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: if self._config.team_names is not None:
bincfg['Custom Team Names'] = self._config.team_names bincfg['Custom Team Names'] = self._config.team_names
elif 'Custom Team Names' in bincfg: elif 'Custom Team Names' in bincfg:
@ -769,8 +808,8 @@ class ServerManagerApp:
assert current_thread() is self._subprocess_thread assert current_thread() is self._subprocess_thread
assert self._subprocess.stdin is not None assert self._subprocess.stdin is not None
# Send the initial server config which should kick things off. # Send the initial server config which should kick things off
# (but make sure its values are still valid first) # (but make sure its values are still valid first).
dataclass_validate(self._config) dataclass_validate(self._config)
self._send_server_command(StartServerModeCommand(self._config)) self._send_server_command(StartServerModeCommand(self._config))
@ -782,8 +821,8 @@ class ServerManagerApp:
# Pass along any commands to our process. # Pass along any commands to our process.
with self._subprocess_commands_lock: with self._subprocess_commands_lock:
for incmd in self._subprocess_commands: for incmd in self._subprocess_commands:
# If we're passing a raw string to exec, no need to wrap it # If we're passing a raw string to exec, no need to
# in any proper structure. # wrap it in any proper structure.
if isinstance(incmd, str): if isinstance(incmd, str):
self._subprocess.stdin.write((incmd + '\n').encode()) self._subprocess.stdin.write((incmd + '\n').encode())
self._subprocess.stdin.flush() self._subprocess.stdin.flush()
@ -794,9 +833,9 @@ class ServerManagerApp:
# Request restarts/shut-downs for various reasons. # Request restarts/shut-downs for various reasons.
self._request_shutdowns_or_restarts() self._request_shutdowns_or_restarts()
# If they want to force-kill our subprocess, simply exit this # If they want to force-kill our subprocess, simply exit
# loop; the cleanup code will kill the process if its still # this loop; the cleanup code will kill the process if its
# alive. # still alive.
if ( if (
self._subprocess_force_kill_time is not None self._subprocess_force_kill_time is not None
and time.time() > self._subprocess_force_kill_time and time.time() > self._subprocess_force_kill_time
@ -855,8 +894,8 @@ class ServerManagerApp:
self.restart(immediate=True) self.restart(immediate=True)
self._subprocess_sent_config_auto_restart = True self._subprocess_sent_config_auto_restart = True
# Attempt clean exit if our clean-exit-time passes. # Attempt clean exit if our clean-exit-time passes (and enforce
# (and enforce a 6 hour max if not provided) # a 6 hour max if not provided).
clean_exit_minutes = 360.0 clean_exit_minutes = 360.0
if self._config.clean_exit_minutes is not None: if self._config.clean_exit_minutes is not None:
clean_exit_minutes = min( clean_exit_minutes = min(
@ -881,8 +920,8 @@ class ServerManagerApp:
self.shutdown(immediate=False) self.shutdown(immediate=False)
self._subprocess_sent_clean_exit = True self._subprocess_sent_clean_exit = True
# Attempt unclean exit if our unclean-exit-time passes. # Attempt unclean exit if our unclean-exit-time passes (and
# (and enforce a 7 hour max if not provided) # enforce a 7 hour max if not provided).
unclean_exit_minutes = 420.0 unclean_exit_minutes = 420.0
if self._config.unclean_exit_minutes is not None: if self._config.unclean_exit_minutes is not None:
unclean_exit_minutes = min( unclean_exit_minutes = min(
@ -924,8 +963,8 @@ class ServerManagerApp:
print(f'{Clr.CYN}Stopping subprocess...{Clr.RST}', flush=True) print(f'{Clr.CYN}Stopping subprocess...{Clr.RST}', flush=True)
# First, ask it nicely to die and give it a moment. # First, ask it nicely to die and give it a moment. If that
# If that doesn't work, bring down the hammer. # doesn't work, bring down the hammer.
self._subprocess.terminate() self._subprocess.terminate()
try: try:
self._subprocess.wait(timeout=10) self._subprocess.wait(timeout=10)
@ -941,8 +980,9 @@ def main() -> None:
try: try:
ServerManagerApp().run() ServerManagerApp().run()
except CleanError as exc: except CleanError as exc:
# For clean errors, do a simple print and fail; no tracebacks/etc. # For clean errors, do a simple print and fail; no
# Any others will bubble up and give us the usual mess. # tracebacks/etc. Any others will bubble up and give us the
# usual mess.
exc.pretty_print() exc.pretty_print()
sys.exit(1) sys.exit(1)

View File

@ -190,13 +190,13 @@ void AppAdapterApple::SetHardwareCursorVisible(bool visible) {
assert(g_core->InMainThread()); assert(g_core->InMainThread());
#if BA_OSTYPE_MACOS #if BA_OSTYPE_MACOS
BallisticaKit::CocoaSupportSetCursorVisible(visible); BallisticaKit::CocoaFromCppSetCursorVisible(visible);
#endif #endif
} }
void AppAdapterApple::TerminateApp() { void AppAdapterApple::TerminateApp() {
#if BA_OSTYPE_MACOS #if BA_OSTYPE_MACOS
BallisticaKit::CocoaSupportTerminateApp(); BallisticaKit::CocoaFromCppTerminateApp();
#else #else
AppAdapter::TerminateApp(); AppAdapter::TerminateApp();
#endif #endif

View File

@ -59,6 +59,7 @@ void AppAdapterSDL::OnMainThreadStartApp() {
"AppAdapterSDL strict_graphics_context_ is enabled." "AppAdapterSDL strict_graphics_context_ is enabled."
" Remember to turn this off."); " Remember to turn this off.");
} }
// We may or may not want xinput on windows. // We may or may not want xinput on windows.
if (g_buildconfig.ostype_windows()) { if (g_buildconfig.ostype_windows()) {
if (!g_core->platform->GetLowLevelConfigValue("enablexinput", 1)) { 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); int result = SDL_Init(sdl_flags);
if (result < 0) { if (result < 0) {
FatalError(std::string("SDL_Init failed: ") + SDL_GetError()); 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); SDL_ShowCursor(SDL_DISABLE);
} }

View File

@ -3,6 +3,7 @@
#include "ballistica/base/app_mode/app_mode.h" #include "ballistica/base/app_mode/app_mode.h"
#include "ballistica/base/input/device/input_device_delegate.h" #include "ballistica/base/input/device/input_device_delegate.h"
#include "ballistica/base/logic/logic.h"
#include "ballistica/base/support/context.h" #include "ballistica/base/support/context.h"
namespace ballistica::base { namespace ballistica::base {
@ -43,8 +44,8 @@ void AppMode::ChangeGameSpeed(int offs) {}
void AppMode::StepDisplayTime() {} void AppMode::StepDisplayTime() {}
auto AppMode::GetHeadlessDisplayStep() -> microsecs_t { auto AppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t {
return kAppModeMaxHeadlessDisplayStep; return kHeadlessMaxDisplayTimeStep;
} }
auto AppMode::GetPartySize() const -> int { return 0; } auto AppMode::GetPartySize() const -> int { return 0; }

View File

@ -9,15 +9,6 @@
namespace ballistica::base { 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 /// 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 /// 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 /// 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 /// Called right after stepping; should return the exact microseconds
/// between the current display time and the next event the app-mode has /// between the current display time and the next event the app-mode has
/// scheduled. If no events are pending, should return /// 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. /// builds.
virtual auto GetHeadlessDisplayStep() -> microsecs_t; virtual auto GetHeadlessNextDisplayTimeStep() -> microsecs_t;
/// Create a delegate for an input-device. /// Create a delegate for an input-device.
/// Return a raw pointer allocated using Object::NewDeferred. /// Return a raw pointer allocated using Object::NewDeferred.

View File

@ -101,7 +101,7 @@ auto Audio::SourceBeginNew() -> AudioSource* {
#pragma clang diagnostic pop #pragma clang diagnostic pop
auto Audio::IsSoundPlaying(uint32_t play_id) -> bool { auto Audio::IsSoundPlaying(uint32_t play_id) -> bool {
uint32_t source_id = AudioServer::source_id_from_play_id(play_id); uint32_t source_id = AudioServer::SourceIdFromPlayId(play_id);
assert(client_sources_.size() > source_id); assert(client_sources_.size() > source_id);
client_sources_[source_id]->Lock(2); client_sources_[source_id]->Lock(2);
bool result = (client_sources_[source_id]->play_id() == play_id); bool result = (client_sources_[source_id]->play_id() == play_id);
@ -112,7 +112,7 @@ auto Audio::IsSoundPlaying(uint32_t play_id) -> bool {
auto Audio::SourceBeginExisting(uint32_t play_id, int debug_id) auto Audio::SourceBeginExisting(uint32_t play_id, int debug_id)
-> AudioSource* { -> AudioSource* {
BA_DEBUG_FUNCTION_TIMER_BEGIN(); BA_DEBUG_FUNCTION_TIMER_BEGIN();
uint32_t source_id = AudioServer::source_id_from_play_id(play_id); uint32_t source_id = AudioServer::SourceIdFromPlayId(play_id);
// Ok, the audio thread fills in this source list, // Ok, the audio thread fills in this source list,
// so theoretically a client could call this before the audio thread // so theoretically a client could call this before the audio thread

File diff suppressed because it is too large Load Diff

View File

@ -12,14 +12,14 @@
namespace ballistica::base { namespace ballistica::base {
/// A module that handles audio processing. /// Wrangles audio off in its own thread.
class AudioServer { class AudioServer {
public: public:
static auto source_id_from_play_id(uint32_t play_id) -> uint32_t { static auto SourceIdFromPlayId(uint32_t play_id) -> uint32_t {
return play_id & 0xFFFFu; return play_id & 0xFFFFu;
} }
static auto play_count_from_play_id(uint32_t play_id) -> uint32_t { static auto PlayCountFromPlayId(uint32_t play_id) -> uint32_t {
return play_id >> 16u; return play_id >> 16u;
} }
@ -28,10 +28,9 @@ class AudioServer {
void PushSetVolumesCall(float music_volume, float sound_volume); void PushSetVolumesCall(float music_volume, float sound_volume);
void PushSetSoundPitchCall(float val); void PushSetSoundPitchCall(float val);
void PushSetSuspendedCall(bool pause);
static void BeginInterruption(); // static void BeginInterruption();
static void EndInterruption(); // static void EndInterruption();
void PushSetListenerPositionCall(const Vector3f& p); void PushSetListenerPositionCall(const Vector3f& p);
void PushSetListenerOrientationCall(const Vector3f& forward, void PushSetListenerOrientationCall(const Vector3f& forward,
@ -41,10 +40,12 @@ class AudioServer {
void PushComponentUnloadCall( void PushComponentUnloadCall(
const std::vector<Object::Ref<Asset>*>& components); const std::vector<Object::Ref<Asset>*>& components);
/// For use by g_logic_module().
void ClearSoundRefDeleteList(); void ClearSoundRefDeleteList();
auto paused() const -> bool { return paused_; } auto paused() const -> bool { return suspended_; }
void Shutdown();
auto shutdown_completed() const { return shutdown_completed_; }
// Client sources use these to pass settings to the server. // Client sources use these to pass settings to the server.
void PushSourceSetIsMusicCall(uint32_t play_id, bool val); void PushSourceSetIsMusicCall(uint32_t play_id, bool val);
@ -67,37 +68,36 @@ class AudioServer {
auto event_loop() const -> EventLoop* { return event_loop_; } auto event_loop() const -> EventLoop* { return event_loop_; }
private: private:
class ThreadSource; class ThreadSource_;
struct Impl; struct Impl_;
void OnAppStartInThread(); void OnAppStartInThread_();
~AudioServer(); ~AudioServer();
void OnThreadPause(); void OnThreadSuspend_();
void OnThreadResume(); void OnThreadUnsuspend_();
void SetPaused(bool paused); void SetSuspended_(bool suspended);
void SetMusicVolume(float volume); void SetMusicVolume_(float volume);
void SetSoundVolume(float volume); void SetSoundVolume_(float volume);
void SetSoundPitch(float pitch); void SetSoundPitch_(float pitch);
auto music_volume() -> float { return music_volume_; }
auto sound_volume() -> float { return sound_volume_; } void CompleteShutdown_();
auto sound_pitch() -> float { return sound_pitch_; }
/// If a sound play id is currently playing, return the sound. /// If a sound play id is currently playing, return the sound.
auto GetPlayingSound(uint32_t play_id) -> ThreadSource*; auto GetPlayingSound_(uint32_t play_id) -> ThreadSource_*;
void Reset(); void Reset_();
void Process(); void Process_();
/// Send a component to the audio thread to delete. /// Send a component to the audio thread to delete.
void DeleteAssetComponent(Asset* c); // void DeleteAssetComponent_(Asset* c);
void UpdateTimerInterval(); void UpdateTimerInterval_();
void UpdateAvailableSources(); void UpdateAvailableSources_();
void UpdateMusicPlayState(); void UpdateMusicPlayState_();
void ProcessSoundFades(); void ProcessSoundFades_();
// Some threads such as audio hold onto allocated Media-Component-Refs to keep // Some threads such as audio hold onto allocated Media-Component-Refs to keep
// media components alive that they need. Media-Component-Refs, however, must // media components alive that they need. Media-Component-Refs, however, must
@ -107,32 +107,36 @@ class AudioServer {
// Note: should use unique_ptr for this, but build fails on raspberry pi // Note: should use unique_ptr for this, but build fails on raspberry pi
// (gcc 8.3.0). Works on Ubuntu 9.3 so should try again later. // (gcc 8.3.0). Works on Ubuntu 9.3 so should try again later.
// std::unique_ptr<Impl> impl_{}; std::unique_ptr<Impl_> impl_{};
Impl* impl_{}; // Impl* impl_{};
EventLoop* event_loop_{}; EventLoop* event_loop_{};
Timer* process_timer_{}; Timer* process_timer_{};
bool have_pending_loads_{};
bool paused_{};
millisecs_t last_sound_fade_process_time_{};
float sound_volume_{1.0f}; float sound_volume_{1.0f};
float sound_pitch_{1.0f}; float sound_pitch_{1.0f};
float music_volume_{1.0f}; float music_volume_{1.0f};
bool have_pending_loads_ : 1 {};
bool suspended_ : 1 {};
bool shutdown_completed_ : 1 {};
bool shutting_down_ : 1 {};
seconds_t shutdown_start_time_{};
millisecs_t last_sound_fade_process_time_{};
/// Indexed list of sources. /// Indexed list of sources.
std::vector<ThreadSource*> sources_; std::vector<ThreadSource_*> sources_;
std::vector<ThreadSource*> streaming_sources_; std::vector<ThreadSource_*> streaming_sources_;
millisecs_t last_stream_process_time_{}; millisecs_t last_stream_process_time_{};
millisecs_t last_sanity_check_time_{};
// Holds refs to all sources. // Holds refs to all sources.
// Use sources, not this, for faster iterating. // Use sources, not this, for faster iterating.
std::vector<Object::Ref<ThreadSource> > sound_source_refs_; std::vector<Object::Ref<ThreadSource_>> sound_source_refs_;
struct SoundFadeNode; struct SoundFadeNode_;
// NOTE: would use unordered_map here but gcc doesn't seem to allow // NOTE: would use unordered_map here but gcc doesn't seem to allow
// forward-declared template params with them. // forward-declared template params with them.
std::map<int, SoundFadeNode> sound_fade_nodes_; std::map<int, SoundFadeNode_> sound_fade_nodes_;
// This mutex controls access to our list of media component shared ptrs to // This mutex controls access to our list of media component shared ptrs to
// delete in the main thread. // delete in the main thread.
@ -141,9 +145,7 @@ class AudioServer {
// Our list of sound media components to delete via the main thread. // Our list of sound media components to delete via the main thread.
std::vector<const Object::Ref<SoundAsset>*> sound_ref_delete_list_; std::vector<const Object::Ref<SoundAsset>*> sound_ref_delete_list_;
millisecs_t last_sanity_check_time_{}; int al_source_count_{};
static int al_source_count_;
}; };
} // namespace ballistica::base } // namespace ballistica::base

View File

@ -15,7 +15,7 @@ AudioSource::AudioSource(int id_in) : id_(id_in) {}
AudioSource::~AudioSource() { assert(client_queue_size_ == 0); } AudioSource::~AudioSource() { assert(client_queue_size_ == 0); }
void AudioSource::MakeAvailable(uint32_t play_id_new) { void AudioSource::MakeAvailable(uint32_t play_id_new) {
assert(AudioServer::source_id_from_play_id(play_id_new) == id_); assert(AudioServer::SourceIdFromPlayId(play_id_new) == id_);
assert(client_queue_size_ == 0); assert(client_queue_size_ == 0);
assert(locked()); assert(locked());
play_id_ = play_id_new; play_id_ = play_id_new;

View File

@ -68,7 +68,9 @@ void AudioStreamer::Stop() {
} }
void AudioStreamer::Update() { void AudioStreamer::Update() {
if (eof_) return; if (eof_) {
return;
}
CHECK_AL_ERROR; CHECK_AL_ERROR;

View File

@ -23,17 +23,18 @@ class AudioStreamer : public Object {
auto Play() -> bool; auto Play() -> bool;
void Stop(); void Stop();
void Update(); void Update();
enum Format { INVALID_FORMAT, MONO16_FORMAT, STEREO16_FORMAT }; enum class Format : uint8_t { kInvalid, kMono16, kStereo16 };
auto al_format() const -> ALenum { auto al_format() const -> ALenum {
switch (format_) { switch (format_) {
case MONO16_FORMAT: case Format::kMono16:
return AL_FORMAT_MONO16; return AL_FORMAT_MONO16;
case STEREO16_FORMAT: case Format::kStereo16:
return AL_FORMAT_STEREO16; return AL_FORMAT_STEREO16;
default: default:
break; break;
} }
return INVALID_FORMAT; FatalError("Invalid AL format.");
return AL_FORMAT_MONO16;
} }
auto loops() const -> bool { return loops_; } auto loops() const -> bool { return loops_; }
auto file_name() const -> const std::string& { return file_name_; } auto file_name() const -> const std::string& { return file_name_; }
@ -46,13 +47,13 @@ class AudioStreamer : public Object {
void set_format(Format format) { format_ = format; } void set_format(Format format) { format_ = format; }
private: private:
Format format_ = INVALID_FORMAT; Format format_{Format::kInvalid};
bool playing_ = false; bool playing_ : 1 {};
bool loops_ : 1 {};
bool eof_ : 1 {};
ALuint buffers_[kAudioStreamBufferCount]{}; ALuint buffers_[kAudioStreamBufferCount]{};
ALuint source_ = 0; ALuint source_{};
std::string file_name_; std::string file_name_;
bool loops_ = false;
bool eof_ = false;
}; };
#endif // BA_ENABLE_AUDIO #endif // BA_ENABLE_AUDIO

View File

@ -53,9 +53,9 @@ OggStream::OggStream(const char* file_name, ALuint source, bool loop)
vorbis_info_ = ov_info(&ogg_file_, -1); vorbis_info_ = ov_info(&ogg_file_, -1);
if (vorbis_info_->channels == 1) { if (vorbis_info_->channels == 1) {
set_format(MONO16_FORMAT); set_format(Format::kMono16);
} else { } else {
set_format(STEREO16_FORMAT); set_format(Format::kStereo16);
} }
} }

View File

@ -197,9 +197,9 @@ void BaseFeatureSet::StartApp() {
assets_server->OnMainThreadStartApp(); assets_server->OnMainThreadStartApp();
app_adapter->OnMainThreadStartApp(); app_adapter->OnMainThreadStartApp();
// Take note that we're now 'running'. Various code such as anything that // Ok; we're now official 'started'. Various code such as anything that
// pushes messages to threads can watch for this state to avoid crashing // pushes messages to threads can watch for this state (via IsAppStarted()
// if called early. // to avoid crashing if called early.
app_started_ = true; app_started_ = true;
// Inform anyone who wants to know that we're done starting. // Inform anyone who wants to know that we're done starting.

View File

@ -603,14 +603,13 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// Start app systems in motion. /// Start app systems in motion.
void StartApp() override; void StartApp() override;
/// Issue a high level app quit request. Can be called from any thread. /// Issue a high level app quit request. Can be called from any thread and
/// 'soft' means the app can simply reset/hide itself instead of actually /// can be safely called repeatedly. If 'confirm' is true, a confirmation
/// exiting the process (common behavior on mobile platforms). 'back' /// dialog will be presented if the environment and situation allows;
/// means that a soft-quit should behave as if a back-button was pressed, /// otherwise the quit will be immediate. A QuitType arg can optionally be
/// which may trigger different behavior in the OS than a standard soft /// passed to influence quit behavior; on some platforms such as mobile
/// quit. If 'confirm' is true, a confirmation dialog will be presented if /// the default is for the app to recede to the background but physically
/// the current app-mode provides one and the app is in gui mode. /// remain running.
/// Otherwise the quit will be immediate.
void QuitApp(bool confirm = false, QuitType quit_type = QuitType::kSoft); void QuitApp(bool confirm = false, QuitType quit_type = QuitType::kSoft);
/// Called when app shutdown process completes. Sets app to exit. /// Called when app shutdown process completes. Sets app to exit.

View File

@ -336,6 +336,7 @@ class Graphics::ScreenMessageEntry {
float v_smoothed{}; float v_smoothed{};
bool translation_dirty{true}; bool translation_dirty{true};
bool mesh_dirty{true}; bool mesh_dirty{true};
millisecs_t smooth_time{};
private: private:
Object::Ref<TextGroup> s_mesh_; Object::Ref<TextGroup> s_mesh_;
@ -594,13 +595,21 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
{ {
auto xf = c.ScopedTransform(); auto xf = c.ScopedTransform();
if (i->v_smoothed == 0.0f) { // This logic needs to run at a fixed hz or it breaks on high frame
i->v_smoothed = v + v_extra; // rates.
} else { auto now_millisecs = pass->frame_def()->display_time_millisecs();
float smoothing = 0.8f; i->smooth_time = std::max(i->smooth_time, now_millisecs - 100);
i->v_smoothed = smoothing * i->v_smoothed while (i->smooth_time < now_millisecs) {
+ (1.0f - smoothing) * (v + v_extra); 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, c.Translate(screen_width * 0.5f, i->v_smoothed,
vr ? 60 : kScreenMessageZDepth); vr ? 60 : kScreenMessageZDepth);
if (vr) { if (vr) {
@ -767,10 +776,17 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
a = 1; a = 1;
} }
i->v_smoothed += 0.1f; // This logic needs to run at a fixed hz or it breaks on high frame
if (i->v_smoothed - last_v < min_spacing) { // rates.
i->v_smoothed += auto now_millisecs = pass->frame_def()->display_time_millisecs();
8.0f * (1.0f - ((i->v_smoothed - last_v) / min_spacing)); 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; 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); *blue = std::min(1.0f, (*blue) * s);
} }
// We may still be short of our target intensity due to clamping (ie: (10,0,0) // We may still be short of our target intensity due to clamping (ie:
// will not look any brighter than (1,0,0)) if that's the case, just convert // (10,0,0) will not look any brighter than (1,0,0)) if that's the case,
// the difference to a grey value and add that to all channels... this *still* // just convert the difference to a grey value and add that to all
// might not get us there so lets do it a few times if need be. (i'm sure // channels... this *still* might not get us there so lets do it a few times
// there's a less bone-headed way to do this) // if need be. (i'm sure there's a less bone-headed way to do this)
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
float remaining = float remaining =
(0.2989f * (*red) + 0.5870f * (*green) + 0.1140f * (*blue)) - 1.0f; (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_vel_ = tilt_smoothed_ * 3.0f;
tilt_pos_ += tilt_vel_ * timescale; tilt_pos_ += tilt_vel_ * timescale;
// Technically this will behave slightly differently at different time scales, // Technically this will behave slightly differently at different time
// but it should be close to correct.. // scales, but it should be close to correct.. tilt_pos_ *= 0.991f;
// tilt_pos_ *= 0.991f;
tilt_pos_ *= std::max(0.0f, 1.0f - 0.01f * timescale); tilt_pos_ *= std::max(0.0f, 1.0f - 0.01f * timescale);
// Some gyros seem wonky and either give us crazy big values or consistently // 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 // offset ones. Let's keep a running tally of magnitude that slowly drops
// time, and if it reaches a certain value lets just kill gyro input. // over time, and if it reaches a certain value lets just kill gyro input.
if (gyro_broken_) { if (gyro_broken_) {
tilt_pos_ *= 0.0f; tilt_pos_ *= 0.0f;
} else { } else {
@ -1485,9 +1500,9 @@ void Graphics::DrawCursor(FrameDef* frame_def) {
{ {
auto xf = c.ScopedTransform(); auto xf = c.ScopedTransform();
// Note: we don't plug in known cursor position values here; we tell the // Note: we don't plug in known cursor position values here; we tell
// renderer to insert the latest values on its end; this can lessen // the renderer to insert the latest values on its end; this can
// cursor lag substantially. // lessen cursor lag substantially.
c.CursorTranslate(); c.CursorTranslate();
c.Translate(csize * 0.40f, csize * -0.38f, kCursorZDepth); c.Translate(csize * 0.40f, csize * -0.38f, kCursorZDepth);
c.Scale(csize, csize); c.Scale(csize, csize);

View File

@ -99,7 +99,7 @@ void Logic::OnGraphicsReady() {
// Anyone dealing in display-time should be able to handle a wide // Anyone dealing in display-time should be able to handle a wide
// variety of rates anyway. NOTE: This length is currently milliseconds. // variety of rates anyway. NOTE: This length is currently milliseconds.
headless_display_time_step_timer_ = event_loop()->NewTimer( headless_display_time_step_timer_ = event_loop()->NewTimer(
kAppModeMinHeadlessDisplayStep / 1000, true, kHeadlessMinDisplayTimeStep / 1000, true,
NewLambdaRunnable([this] { StepDisplayTime_(); })); NewLambdaRunnable([this] { StepDisplayTime_(); }));
} else { } else {
// In gui mode, push an initial frame to the graphics server. From this // 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_); assert(headless_display_time_step_timer_);
// NOTE: This is currently milliseconds. // NOTE: This is currently milliseconds.
headless_display_time_step_timer_->SetLength(kAppModeMinHeadlessDisplayStep headless_display_time_step_timer_->SetLength(kHeadlessMinDisplayTimeStep
/ 1000); / 1000);
} }
} }
@ -424,9 +424,9 @@ void Logic::PostUpdateDisplayTimeForHeadlessMode_() {
// we've got until the next event. We'll plug this into our display-update // 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. // timer so we can try to sleep exactly until that point.
auto headless_display_step_microsecs = auto headless_display_step_microsecs =
std::max(std::min(g_base->app_mode()->GetHeadlessDisplayStep(), std::max(std::min(g_base->app_mode()->GetHeadlessNextDisplayTimeStep(),
kAppModeMaxHeadlessDisplayStep), kHeadlessMaxDisplayTimeStep),
kAppModeMinHeadlessDisplayStep); kHeadlessMinDisplayTimeStep);
if (debug_log_display_time_) { if (debug_log_display_time_) {
auto sleepsecs = auto sleepsecs =

View File

@ -13,6 +13,15 @@ namespace ballistica::base {
const int kDisplayTimeSampleCount{15}; 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 /// The logic subsystem of the app. This runs on a dedicated thread and is
/// where most high level app logic happens. Much app functionality /// where most high level app logic happens. Much app functionality
/// including UI calls must be run on the logic thread. /// 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. /// graphical builds we also use this opportunity to step our logic.
void Draw(); void Draw();
/// Kick off an app shutdown. Shutdown is an asynchronous process which /// Kick off a low level app shutdown. Shutdown is an asynchronous process
/// may take a bit of time to complete. Safe to call repeatedly. /// 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(); void Shutdown();
/// Should be called by the Python layer when it has completed all /// Should be called by the Python layer when it has completed all

View File

@ -4,6 +4,7 @@
#include "ballistica/base/app_adapter/app_adapter.h" #include "ballistica/base/app_adapter/app_adapter.h"
#include "ballistica/base/app_mode/app_mode_empty.h" #include "ballistica/base/app_mode/app_mode_empty.h"
#include "ballistica/base/audio/audio_server.h"
#include "ballistica/base/graphics/graphics_server.h" #include "ballistica/base/graphics/graphics_server.h"
#include "ballistica/base/logic/logic.h" #include "ballistica/base/logic/logic.h"
#include "ballistica/base/python/base_python.h" #include "ballistica/base/python/base_python.h"
@ -729,11 +730,8 @@ static auto PyEnv(PyObject* self) -> PyObject* {
"ss" // ui_scale "ss" // ui_scale
"sO" // on_tv "sO" // on_tv
"sO" // vr_mode "sO" // vr_mode
// "sO" // toolbar_test
"sO" // demo_mode "sO" // demo_mode
"sO" // arcade_mode "sO" // arcade_mode
// "sO" // iircade_mode
// "si" // protocol_version
"sO" // headless_mode "sO" // headless_mode
"sO" // python_directory_app_site "sO" // python_directory_app_site
"ss" // device_name "ss" // device_name
@ -757,8 +755,6 @@ static auto PyEnv(PyObject* self) -> PyObject* {
"vr_mode", g_core->IsVRMode() ? Py_True : Py_False, "vr_mode", g_core->IsVRMode() ? Py_True : Py_False,
"demo_mode", g_buildconfig.demo_build() ? Py_True : Py_False, "demo_mode", g_buildconfig.demo_build() ? Py_True : Py_False,
"arcade_mode", g_buildconfig.arcade_build() ? Py_True : Py_False, "arcade_mode", g_buildconfig.arcade_build() ? Py_True : Py_False,
// "iircade_mode", g_buildconfig.iircade_build() ? Py_True: Py_False,
// "protocol_version", kProtocolVersion,
"headless_mode", g_core->HeadlessMode() ? Py_True : Py_False, "headless_mode", g_core->HeadlessMode() ? Py_True : Py_False,
"python_directory_app_site", "python_directory_app_site",
site_py_dir ? *PythonRef::FromString(*site_py_dir) : Py_None, site_py_dir ? *PythonRef::FromString(*site_py_dir) : Py_None,
@ -1603,6 +1599,52 @@ static PyMethodDef PyDevConsoleInputAdapterFinishDef = {
"(internal)\n", "(internal)\n",
}; };
// -------------------------- audio_shutdown_begin -----------------------------
static auto PyAudioShutdownBegin(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
auto* audio_event_loop = g_base->audio_server->event_loop();
BA_PRECONDITION(audio_event_loop);
audio_event_loop->PushCall([] { g_base->audio_server->Shutdown(); });
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
static PyMethodDef PyAudioShutdownBeginDef = {
"audio_shutdown_begin", // name
(PyCFunction)PyAudioShutdownBegin, // method
METH_NOARGS, // flags
"audio_shutdown_begin() -> None\n"
"\n"
"(internal)\n",
};
// ----------------------- audio_shutdown_is_complete --------------------------
static auto PyAudioShutdownIsComplete(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
if (g_base->audio_server->shutdown_completed()) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
BA_PYTHON_CATCH;
}
static PyMethodDef PyAudioShutdownIsCompleteDef = {
"audio_shutdown_is_complete", // name
(PyCFunction)PyAudioShutdownIsComplete, // method
METH_NOARGS, // flags
"audio_shutdown_is_complete() -> bool\n"
"\n"
"(internal)\n",
};
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> { auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
@ -1658,6 +1700,8 @@ auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
PyGetDevConsoleInputTextDef, PyGetDevConsoleInputTextDef,
PySetDevConsoleInputTextDef, PySetDevConsoleInputTextDef,
PyDevConsoleInputAdapterFinishDef, PyDevConsoleInputAdapterFinishDef,
PyAudioShutdownBeginDef,
PyAudioShutdownIsCompleteDef,
}; };
} }

View File

@ -208,6 +208,8 @@ void AppConfig::SetupEntries() {
int_entries_[IntID::kPort] = IntEntry("Port", kDefaultPort); int_entries_[IntID::kPort] = IntEntry("Port", kDefaultPort);
int_entries_[IntID::kMaxFPS] = IntEntry("Max FPS", 60); int_entries_[IntID::kMaxFPS] = IntEntry("Max FPS", 60);
int_entries_[IntID::kSceneV1HostProtocol] =
IntEntry("SceneV1 Host Protocol", 33);
bool_entries_[BoolID::kTouchControlsSwipeHidden] = bool_entries_[BoolID::kTouchControlsSwipeHidden] =
BoolEntry("Touch Controls Swipe Hidden", false); BoolEntry("Touch Controls Swipe Hidden", false);

View File

@ -55,6 +55,7 @@ class AppConfig {
enum class IntID { enum class IntID {
kPort, kPort,
kMaxFPS, kMaxFPS,
kSceneV1HostProtocol,
kLast // Sentinel. kLast // Sentinel.
}; };

View File

@ -42,7 +42,7 @@ UI::UI() {
// VR and TV modes always use medium. // VR and TV modes always use medium.
scale_ = UIScale::kMedium; scale_ = UIScale::kMedium;
} else { } else {
scale_ = g_core->platform->GetUIScale(); scale_ = g_core->platform->GetDefaultUIScale();
} }
} }
} }

View File

@ -4,8 +4,10 @@
#include "ballistica/core/platform/apple/core_platform_apple.h" #include "ballistica/core/platform/apple/core_platform_apple.h"
#if BA_XCODE_BUILD #if BA_XCODE_BUILD
#include <BallisticaKit-Swift.h>
#include <unistd.h> #include <unistd.h>
#endif #endif
#include <uuid/uuid.h> #include <uuid/uuid.h>
#if BA_XCODE_BUILD #if BA_XCODE_BUILD
@ -102,7 +104,7 @@ auto CorePlatformApple::DoGetConfigDirectoryMonolithicDefault()
auto CorePlatformApple::GetLocale() -> std::string { auto CorePlatformApple::GetLocale() -> std::string {
#if BA_XCODE_BUILD #if BA_XCODE_BUILD
return base::AppleUtils::GetLocaleString(); return BallisticaKit::FromCppGetLocaleString();
#else #else
return CorePlatform::GetLocale(); return CorePlatform::GetLocale();
#endif #endif
@ -124,7 +126,7 @@ auto CorePlatformApple::DoHasTouchScreen() -> bool {
#endif #endif
} }
auto CorePlatformApple::GetUIScale() -> UIScale { auto CorePlatformApple::GetDefaultUIScale() -> UIScale {
#if BA_OSTYPE_IOS #if BA_OSTYPE_IOS
if (base::AppleUtils::IsTablet()) { if (base::AppleUtils::IsTablet()) {
return UIScale::kMedium; return UIScale::kMedium;
@ -132,8 +134,8 @@ auto CorePlatformApple::GetUIScale() -> UIScale {
return UIScale::kSmall; return UIScale::kSmall;
} }
#else #else
// default case handles mac/tvos // Default case handles mac & tvos.
return CorePlatform::GetUIScale(); return CorePlatform::GetDefaultUIScale();
#endif #endif
} }
@ -160,9 +162,8 @@ void CorePlatformApple::DisplayLog(const std::string& name, LogLevel level,
} }
auto CorePlatformApple::DoGetDataDirectoryMonolithicDefault() -> std::string { auto CorePlatformApple::DoGetDataDirectoryMonolithicDefault() -> std::string {
#if BA_XCODE_BUILD && !BA_HEADLESS_BUILD #if BA_XCODE_BUILD
// On Apple package-y builds use our resources dir. return BallisticaKit::FromCppGetResourcesPath();
return base::AppleUtils::GetResourcesPath();
#else #else
// Fall back to default. // Fall back to default.
return CorePlatform::DoGetDataDirectoryMonolithicDefault(); return CorePlatform::DoGetDataDirectoryMonolithicDefault();
@ -292,13 +293,8 @@ void CorePlatformApple::OpenFileExternally(const std::string& path) {
} }
void CorePlatformApple::OpenDirExternally(const std::string& path) { void CorePlatformApple::OpenDirExternally(const std::string& path) {
#if BA_OSTYPE_MACOS #if BA_OSTYPE_MACOS && BA_XCODE_BUILD
std::string cmd = std::string("open \"") + path + "\""; BallisticaKit::CocoaFromCppOpenDirExternally(path);
int result = system(cmd.c_str());
if (result != 0) {
Log(LogLevel::kError, "Got return value " + std::to_string(result)
+ " on open cmd '" + cmd + "'");
}
#else #else
CorePlatform::OpenDirExternally(path); CorePlatform::OpenDirExternally(path);
#endif #endif
@ -380,7 +376,7 @@ auto CorePlatformApple::DoClipboardIsSupported() -> bool {
return base::AppleUtils::ClipboardIsSupported(); return base::AppleUtils::ClipboardIsSupported();
#else #else
return CorePlatform::DoClipboardIsSupported(); return CorePlatform::DoClipboardIsSupported();
#endif // BA_XCODE_BUILD #endif
} }
auto CorePlatformApple::DoClipboardHasText() -> bool { auto CorePlatformApple::DoClipboardHasText() -> bool {
@ -388,7 +384,7 @@ auto CorePlatformApple::DoClipboardHasText() -> bool {
return base::AppleUtils::ClipboardHasText(); return base::AppleUtils::ClipboardHasText();
#else #else
return CorePlatform::DoClipboardHasText(); return CorePlatform::DoClipboardHasText();
#endif // BA_XCODE_BUILD #endif
} }
void CorePlatformApple::DoClipboardSetText(const std::string& text) { void CorePlatformApple::DoClipboardSetText(const std::string& text) {
@ -396,7 +392,7 @@ void CorePlatformApple::DoClipboardSetText(const std::string& text) {
base::AppleUtils::ClipboardSetText(text); base::AppleUtils::ClipboardSetText(text);
#else #else
CorePlatform::DoClipboardSetText(text); CorePlatform::DoClipboardSetText(text);
#endif // BA_XCODE_BUILD #endif
} }
auto CorePlatformApple::DoClipboardGetText() -> std::string { auto CorePlatformApple::DoClipboardGetText() -> std::string {
@ -404,7 +400,7 @@ auto CorePlatformApple::DoClipboardGetText() -> std::string {
return base::AppleUtils::ClipboardGetText(); return base::AppleUtils::ClipboardGetText();
#else #else
return CorePlatform::DoClipboardGetText(); return CorePlatform::DoClipboardGetText();
#endif // BA_XCODE_BUILD #endif
} }
} // namespace ballistica::core } // namespace ballistica::core

View File

@ -24,7 +24,7 @@ class CorePlatformApple : public CorePlatform {
auto GetLocale() -> std::string override; auto GetLocale() -> std::string override;
auto DoGetDeviceName() -> std::string override; auto DoGetDeviceName() -> std::string override;
auto DoHasTouchScreen() -> bool override; auto DoHasTouchScreen() -> bool override;
auto GetUIScale() -> UIScale override; auto GetDefaultUIScale() -> UIScale override;
auto IsRunningOnDesktop() -> bool override; auto IsRunningOnDesktop() -> bool override;
void DisplayLog(const std::string& name, LogLevel level, void DisplayLog(const std::string& name, LogLevel level,
const std::string& msg) override; const std::string& msg) override;

View File

@ -476,7 +476,7 @@ void CorePlatform::SleepMicrosecs(millisecs_t ms) {
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma ide diagnostic ignored "NullDereferences" #pragma ide diagnostic ignored "NullDereferences"
auto CorePlatform::GetUIScale() -> UIScale { auto CorePlatform::GetDefaultUIScale() -> UIScale {
// Handles mac/pc/linux cases. // Handles mac/pc/linux cases.
return UIScale::kLarge; return UIScale::kLarge;
} }

View File

@ -97,26 +97,22 @@ class CorePlatform {
#pragma mark PRINTING/LOGGING -------------------------------------------------- #pragma mark PRINTING/LOGGING --------------------------------------------------
/// Display a message to any default log for the platform (android log, /// Display a message to any default log for the platform (android log,
/// etc.) Note that this can be called from any thread. /// etc.) Note that this can be called from any thread. Default
/// implementation does nothing.
virtual void DisplayLog(const std::string& name, LogLevel level, virtual void DisplayLog(const std::string& name, LogLevel level,
const std::string& msg); const std::string& msg);
#pragma mark ENVIRONMENT ------------------------------------------------------- #pragma mark ENVIRONMENT -------------------------------------------------------
// Return a simple name for the platform: 'mac', 'windows', 'linux', etc. /// Return a simple name for the platform: 'mac', 'windows', 'linux', etc.
virtual auto GetPlatformName() -> std::string; virtual auto GetPlatformName() -> std::string;
// Return a simple name for the subplatform: 'amazon', 'google', etc. /// Return a simple name for the subplatform: 'amazon', 'google', etc.
virtual auto GetSubplatformName() -> std::string; virtual auto GetSubplatformName() -> std::string;
// Are we running in event-push-mode? With this on, we return from Main()
// and the system handles the event loop. With it off, we loop in Main()
// ourself.
// virtual auto IsEventPushMode() -> bool;
/// Return the interface type based on the environment (phone, tablet, /// Return the interface type based on the environment (phone, tablet,
/// etc). /// etc).
virtual auto GetUIScale() -> UIScale; virtual auto GetDefaultUIScale() -> UIScale;
/// Return default DataDirectory value for monolithic builds. /// Return default DataDirectory value for monolithic builds.
auto GetDataDirectoryMonolithicDefault() -> std::string; auto GetDataDirectoryMonolithicDefault() -> std::string;
@ -387,7 +383,7 @@ class CorePlatform {
static void SleepMillisecs(millisecs_t ms); static void SleepMillisecs(millisecs_t ms);
static void SleepMicrosecs(millisecs_t ms); static void SleepMicrosecs(microsecs_t ms);
/// Given a C++ symbol, attempt to return a pretty one. /// Given a C++ symbol, attempt to return a pretty one.
virtual auto DemangleCXXSymbol(const std::string& s) -> std::string; virtual auto DemangleCXXSymbol(const std::string& s) -> std::string;

View File

@ -24,7 +24,10 @@ namespace ballistica::scene_v1 {
// How long new clients have to wait before starting a kick vote. // How long new clients have to wait before starting a kick vote.
const int kNewClientKickVoteDelay = 60000; 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 // We calc this once just in case it changes on our end
// (the client uses it for their verification hash so we need to // (the client uses it for their verification hash so we need to
// ensure it stays consistent). // ensure it stays consistent).
@ -33,7 +36,7 @@ ConnectionToClient::ConnectionToClient(int id) : id_(id) {
// On newer protocols we include an extra salt value // On newer protocols we include an extra salt value
// to ensure the hash the client generates can't be recycled. // 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 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 // In newer protocols we embed a json dict as the second part of the
// handshake packet; this way we can evolve the protocol more // handshake packet; this way we can evolve the protocol more
// easily in the future. // 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. // Construct a json dict with our player-spec-string as one element.
JsonDict dict; JsonDict dict;
dict.AddString("s", our_handshake_player_spec_str_); dict.AddString("s", our_handshake_player_spec_str_);
@ -106,17 +109,17 @@ void ConnectionToClient::Update() {
std::string out = dict.PrintUnformatted(); std::string out = dict.PrintUnformatted();
std::vector<uint8_t> data(3 + out.size()); std::vector<uint8_t> data(3 + out.size());
data[0] = BA_SCENEPACKET_HANDSHAKE; data[0] = BA_SCENEPACKET_HANDSHAKE;
uint16_t val = kProtocolVersion; uint16_t val = protocol_version();
memcpy(data.data() + 1, &val, sizeof(val)); memcpy(data.data() + 1, &val, sizeof(val));
memcpy(data.data() + 3, out.c_str(), out.size()); memcpy(data.data() + 3, out.c_str(), out.size());
SendGamePacket(data); SendGamePacket(data);
} else { } else {
// (KILL THIS WHEN kProtocolVersionMin >= 33) // (KILL THIS WHEN kProtocolVersionClientMin >= 33)
// on older protocols, we simply embedded our spec-string as the second // on older protocols, we simply embedded our spec-string as the second
// part of the handshake packet // part of the handshake packet
std::vector<uint8_t> data(3 + our_handshake_player_spec_str_.size()); std::vector<uint8_t> data(3 + our_handshake_player_spec_str_.size());
data[0] = BA_SCENEPACKET_HANDSHAKE; data[0] = BA_SCENEPACKET_HANDSHAKE;
uint16_t val = kProtocolVersion; uint16_t val = protocol_version();
memcpy(data.data() + 1, &val, sizeof(val)); memcpy(data.data() + 1, &val, sizeof(val));
memcpy(data.data() + 3, our_handshake_player_spec_str_.c_str(), memcpy(data.data() + 3, our_handshake_player_spec_str_.c_str(),
our_handshake_player_spec_str_.size()); our_handshake_player_spec_str_.size());
@ -155,7 +158,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector<uint8_t>& data) {
// In newer builds we expect to be sent a json dict here; // In newer builds we expect to be sent a json dict here;
// pull client's spec from that. // pull client's spec from that.
if (explicit_bool(kProtocolVersion >= 33)) { if (protocol_version() >= 33) {
std::vector<char> string_buffer(data.size() - 3 + 1); std::vector<char> string_buffer(data.size() - 3 + 1);
memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3); memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3);
string_buffer[string_buffer.size() - 1] = 0; string_buffer[string_buffer.size() - 1] = 0;
@ -173,7 +176,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector<uint8_t>& data) {
cJSON_Delete(handshake); cJSON_Delete(handshake);
} }
} else { } else {
// (KILL THIS WHEN kProtocolVersionMin >= 33) // (KILL THIS WHEN kProtocolVersionClientMin >= 33)
// older versions only contained the client spec // older versions only contained the client spec
// pull client's spec from the handshake packet.. // pull client's spec from the handshake packet..
std::vector<char> string_buffer(data.size() - 3 + 1); std::vector<char> string_buffer(data.size() - 3 + 1);
@ -195,7 +198,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector<uint8_t>& data) {
// Bytes 2 and 3 are their protocol version. // Bytes 2 and 3 are their protocol version.
uint16_t val; uint16_t val;
memcpy(&val, data.data() + 1, sizeof(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 // 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 or not. (If we invited them it'd be good to know about the
// failure). // failure).

View File

@ -55,10 +55,17 @@ class ConnectionToClient : public Connection {
// or their peer name if they have no players. // or their peer name if they have no players.
auto GetCombinedSpec() -> PlayerSpec; auto GetCombinedSpec() -> PlayerSpec;
auto protocol_version() const {
assert(protocol_version_ != -1);
return protocol_version_;
}
private: private:
virtual auto ShouldPrintIncompatibleClientErrors() const -> bool; virtual auto ShouldPrintIncompatibleClientErrors() const -> bool;
auto GetClientInputDevice(int remote_id) -> ClientInputDevice*; auto GetClientInputDevice(int remote_id) -> ClientInputDevice*;
void Error(const std::string& error_msg) override; void Error(const std::string& error_msg) override;
int protocol_version_;
std::string our_handshake_player_spec_str_; std::string our_handshake_player_spec_str_;
std::string our_handshake_salt_; std::string our_handshake_salt_;
std::string peer_public_account_id_; std::string peer_public_account_id_;

View File

@ -22,7 +22,9 @@ namespace ballistica::scene_v1 {
// How long to go between sending out null packets for pings. // How long to go between sending out null packets for pings.
const int kPingSendInterval = 2000; const int kPingSendInterval = 2000;
ConnectionToHost::ConnectionToHost() = default; ConnectionToHost::ConnectionToHost()
: protocol_version_{
SceneV1AppMode::GetSingleton()->host_protocol_version()} {}
auto ConnectionToHost::GetAsUDP() -> ConnectionToHostUDP* { return nullptr; } auto ConnectionToHost::GetAsUDP() -> ConnectionToHostUDP* { return nullptr; }
@ -103,8 +105,8 @@ void ConnectionToHost::HandleGamePacket(const std::vector<uint8_t>& data) {
uint16_t their_protocol_version; uint16_t their_protocol_version;
memcpy(&their_protocol_version, data.data() + 1, memcpy(&their_protocol_version, data.data() + 1,
sizeof(their_protocol_version)); sizeof(their_protocol_version));
if (their_protocol_version >= kProtocolVersionMin if (their_protocol_version >= kProtocolVersionClientMin
&& their_protocol_version <= kProtocolVersion) { && their_protocol_version <= kProtocolVersionMax) {
compatible = true; compatible = true;
// If we are compatible, set our protocol version to match // If we are compatible, set our protocol version to match
@ -136,7 +138,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector<uint8_t>& data) {
memcpy(data2.data() + 3, out.c_str(), out.size()); memcpy(data2.data() + 3, out.c_str(), out.size());
SendGamePacket(data2); SendGamePacket(data2);
} else { } else {
// (KILL THIS WHEN kProtocolVersionMin >= 33) // (KILL THIS WHEN kProtocolVersionClientMin >= 33)
std::string our_spec_str = std::string our_spec_str =
PlayerSpec::GetAccountPlayerSpec().GetSpecString(); PlayerSpec::GetAccountPlayerSpec().GetSpecString();
std::vector<uint8_t> response(3 + our_spec_str.size()); std::vector<uint8_t> response(3 + our_spec_str.size());
@ -148,7 +150,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector<uint8_t>& data) {
} }
if (!compatible) { if (!compatible) {
if (their_protocol_version > kProtocolVersion) { if (their_protocol_version > protocol_version()) {
Error(g_base->assets->GetResourceString( Error(g_base->assets->GetResourceString(
"incompatibleNewerVersionHostText")); "incompatibleNewerVersionHostText"));
} else { } else {
@ -183,7 +185,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector<uint8_t>& data) {
cJSON_Delete(handshake); cJSON_Delete(handshake);
} }
} else { } else {
// (KILL THIS WHEN kProtocolVersionMin >= 33) // (KILL THIS WHEN kProtocolVersionClientMin >= 33)
// In older protocols, handshake simply contained a // In older protocols, handshake simply contained a
// player-spec for the host. // player-spec for the host.

View File

@ -33,12 +33,12 @@ class ConnectionToHost : public Connection {
std::string party_name_; std::string party_name_;
std::string peer_hash_input_; std::string peer_hash_input_;
std::string peer_hash_; std::string peer_hash_;
bool printed_connect_message_ = false; // Can remove once back-compat protocol is > 29
int protocol_version_ = kProtocolVersion; bool ignore_old_attach_remote_player_packets_ : 1 {};
int build_number_ = 0; bool printed_connect_message_ : 1 {};
bool got_host_info_ = false; bool got_host_info_ : 1 {};
// can remove once back-compat protocol is > 29 int protocol_version_{-1};
bool ignore_old_attach_remote_player_packets_ = false; int build_number_{};
millisecs_t last_ping_send_time_{}; millisecs_t last_ping_send_time_{};
// the client-session that we're driving // the client-session that we're driving
Object::WeakRef<ClientSession> client_session_; Object::WeakRef<ClientSession> client_session_;

View File

@ -13,7 +13,7 @@
namespace ballistica::scene_v1 { namespace ballistica::scene_v1 {
auto ConnectionToHostUDP::SwitchProtocol() -> bool { auto ConnectionToHostUDP::SwitchProtocol() -> bool {
if (protocol_version() > kProtocolVersionMin) { if (protocol_version() > kProtocolVersionClientMin) {
set_protocol_version(protocol_version() - 1); set_protocol_version(protocol_version() - 1);
// Need a new request id so we ignore further responses to our previous // Need a new request id so we ignore further responses to our previous

View File

@ -72,7 +72,7 @@ class ImageNode : public Node {
void set_front(bool val) { front_ = val; } void set_front(bool val) { front_ = val; }
private: private:
enum class Attach { enum class Attach : uint8_t {
CENTER, CENTER,
TOP_LEFT, TOP_LEFT,
TOP_CENTER, TOP_CENTER,
@ -83,32 +83,24 @@ class ImageNode : public Node {
BOTTOM_LEFT, BOTTOM_LEFT,
CENTER_LEFT CENTER_LEFT
}; };
bool host_only_{};
bool front_{}; bool host_only_ : 1 {};
float vr_depth_{}; bool front_ : 1 {};
std::vector<float> scale_{1.0f, 1.0f}; bool absolute_scale_ : 1 {true};
std::vector<float> position_{0.0f, 0.0f}; bool premultiplied_ : 1 {};
std::vector<float> color_{1.0f, 1.0f, 1.0f}; bool fill_screen_ : 1 {};
std::vector<float> tint_color_{1.0f, 1.0f, 1.0f}; bool has_alpha_channel_ : 1 {true};
std::vector<float> tint2_color_{1.0f, 1.0f, 1.0f}; bool dirty_ : 1 {true};
Object::Ref<SceneTexture> texture_;
Object::Ref<SceneTexture> tint_texture_;
Object::Ref<SceneTexture> mask_texture_;
Object::Ref<SceneMesh> mesh_opaque_;
Object::Ref<SceneMesh> mesh_transparent_;
bool fill_screen_{};
bool has_alpha_channel_{true};
bool dirty_{true};
float opacity_{1.0f};
Attach attach_{Attach::CENTER}; Attach attach_{Attach::CENTER};
bool absolute_scale_{true};
float vr_depth_{};
float opacity_{1.0f};
float center_x_{}; float center_x_{};
float center_y_{}; float center_y_{};
float width_{}; float width_{};
float height_{}; float height_{};
float tilt_translate_{}; float tilt_translate_{};
float rotate_{}; float rotate_{};
bool premultiplied_{};
float red_{1.0f}; float red_{1.0f};
float green_{1.0f}; float green_{1.0f};
float blue_{1.0f}; float blue_{1.0f};
@ -119,6 +111,16 @@ class ImageNode : public Node {
float tint2_red_{1.0f}; float tint2_red_{1.0f};
float tint2_green_{1.0f}; float tint2_green_{1.0f};
float tint2_blue_{1.0f}; float tint2_blue_{1.0f};
std::vector<float> scale_{1.0f, 1.0f};
std::vector<float> position_{0.0f, 0.0f};
std::vector<float> color_{1.0f, 1.0f, 1.0f};
std::vector<float> tint_color_{1.0f, 1.0f, 1.0f};
std::vector<float> tint2_color_{1.0f, 1.0f, 1.0f};
Object::Ref<SceneTexture> texture_;
Object::Ref<SceneTexture> tint_texture_;
Object::Ref<SceneTexture> mask_texture_;
Object::Ref<SceneMesh> mesh_opaque_;
Object::Ref<SceneMesh> mesh_transparent_;
}; };
} // namespace ballistica::scene_v1 } // namespace ballistica::scene_v1

View File

@ -1046,7 +1046,24 @@ static auto PyCameraShake(PyObject* self, PyObject* args, PyObject* keywds)
const_cast<char**>(kwlist), &intensity)) { const_cast<char**>(kwlist), &intensity)) {
return nullptr; 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; Py_RETURN_NONE;
BA_PYTHON_CATCH; BA_PYTHON_CATCH;
} }
@ -1172,9 +1189,13 @@ static auto PyEmitFx(PyObject* self, PyObject* args, PyObject* keywds)
e.spread = spread; e.spread = spread;
e.chunk_type = chunk_type; e.chunk_type = chunk_type;
e.tendril_type = tendril_type; e.tendril_type = tendril_type;
// Send to clients/replays.
if (SessionStream* output_stream = scene->GetSceneStream()) { if (SessionStream* output_stream = scene->GetSceneStream()) {
output_stream->EmitBGDynamics(e); output_stream->EmitBGDynamics(e);
} }
// Depict locally.
if (!g_core->HeadlessMode()) { if (!g_core->HeadlessMode()) {
g_base->bg_dynamics->Emit(e); g_base->bg_dynamics->Emit(e);
} }
@ -1722,7 +1743,8 @@ static PyMethodDef PyHandleAppIntentExecDef = {
static auto PyProtocolVersion(PyObject* self) -> PyObject* { static auto PyProtocolVersion(PyObject* self) -> PyObject* {
BA_PYTHON_TRY; BA_PYTHON_TRY;
return PyLong_FromLong(kProtocolVersion); return PyLong_FromLong(
SceneV1AppMode::GetSingleton()->host_protocol_version());
BA_PYTHON_CATCH; BA_PYTHON_CATCH;
} }

View File

@ -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, // 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, // F is 32 bit float, f is 16 bit float,
// s is string, b is bool. // s is string, b is bool.
SetupNodeMessageType("flash", NodeMessageType::kFlash, ""); SetupNodeMessageType_("flash", NodeMessageType::kFlash, "");
SetupNodeMessageType("footing", NodeMessageType::kFooting, "c"); SetupNodeMessageType_("footing", NodeMessageType::kFooting, "c");
SetupNodeMessageType("impulse", NodeMessageType::kImpulse, "fffffffffifff"); SetupNodeMessageType_("impulse", NodeMessageType::kImpulse, "fffffffffifff");
SetupNodeMessageType("kick_back", NodeMessageType::kKickback, "fffffff"); SetupNodeMessageType_("kick_back", NodeMessageType::kKickback, "fffffff");
SetupNodeMessageType("celebrate", NodeMessageType::kCelebrate, "i"); SetupNodeMessageType_("celebrate", NodeMessageType::kCelebrate, "i");
SetupNodeMessageType("celebrate_l", NodeMessageType::kCelebrateL, "i"); SetupNodeMessageType_("celebrate_l", NodeMessageType::kCelebrateL, "i");
SetupNodeMessageType("celebrate_r", NodeMessageType::kCelebrateR, "i"); SetupNodeMessageType_("celebrate_r", NodeMessageType::kCelebrateR, "i");
SetupNodeMessageType("knockout", NodeMessageType::kKnockout, "f"); SetupNodeMessageType_("knockout", NodeMessageType::kKnockout, "f");
SetupNodeMessageType("hurt_sound", NodeMessageType::kHurtSound, ""); SetupNodeMessageType_("hurt_sound", NodeMessageType::kHurtSound, "");
SetupNodeMessageType("picked_up", NodeMessageType::kPickedUp, ""); SetupNodeMessageType_("picked_up", NodeMessageType::kPickedUp, "");
SetupNodeMessageType("jump_sound", NodeMessageType::kJumpSound, ""); SetupNodeMessageType_("jump_sound", NodeMessageType::kJumpSound, "");
SetupNodeMessageType("attack_sound", NodeMessageType::kAttackSound, ""); SetupNodeMessageType_("attack_sound", NodeMessageType::kAttackSound, "");
SetupNodeMessageType("scream_sound", NodeMessageType::kScreamSound, ""); SetupNodeMessageType_("scream_sound", NodeMessageType::kScreamSound, "");
SetupNodeMessageType("stand", NodeMessageType::kStand, "ffff"); 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<SceneV1FeatureSet>("_bascenev1");
} }
void SceneV1FeatureSet::Reset() { void SceneV1FeatureSet::Reset() {
@ -132,13 +139,6 @@ void SceneV1FeatureSet::ResetRandomNames() {
random_name_registry_->clear(); 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<SceneV1FeatureSet>("_bascenev1");
}
auto SceneV1FeatureSet::GetRandomName(const std::string& full_name) auto SceneV1FeatureSet::GetRandomName(const std::string& full_name)
-> std::string { -> std::string {
assert(g_base->InLogicThread()); assert(g_base->InLogicThread());
@ -172,9 +172,9 @@ auto SceneV1FeatureSet::GetRandomName(const std::string& full_name)
return (*random_name_registry_)[full_name]; return (*random_name_registry_)[full_name];
} }
void SceneV1FeatureSet::SetupNodeMessageType(const std::string& name, void SceneV1FeatureSet::SetupNodeMessageType_(const std::string& name,
NodeMessageType val, NodeMessageType val,
const std::string& format) { const std::string& format) {
node_message_types_[name] = val; node_message_types_[name] = val;
assert(static_cast<int>(val) >= 0); assert(static_cast<int>(val) >= 0);
if (node_message_formats_.size() <= static_cast<size_t>(val)) { if (node_message_formats_.size() <= static_cast<size_t>(val)) {

View File

@ -27,39 +27,54 @@ namespace ballistica::scene_v1 {
// Protocol version we host games with and write replays to. This should be // Protocol version we host games with and write replays to. This should be
// incremented whenever there are changes made to the session-commands layer // 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 // Note that the packet/gamepacket/message layer can vary more organically
// based on build-numbers of connected clients/servers since none of that // 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 // data is stored; these protocol versions just need to be observed by
// that goes into replays since a single stream can get played/replayed on // anything emitting or ingesting scene streams.
// different builds (as long as they support that protocol version).
const int kProtocolVersion = 33; // 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 // 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 // left as-is as long as only new nodes/attrs/commands are added and old
// existing stuff is unchanged. // behavior remains the same when not using the new stuff.
const int kProtocolVersionMin = 24; const int kProtocolVersionClientMin = 24;
// FIXME: We should separate out connection protocol from scene protocol. We // Newest protocol version we can act as a client OR host for.
// want to be able to watch really old replays if possible but being able const int kProtocolVersionMax = 35;
// to connect to old clients is much less important (and slows progress).
// Protocol additions: // The protocol version we actually host is now read as a setting; see
// 25: added a few new achievement graphics and new node attrs for displaying // kSceneV1HostProtocol in ballistica/base/support/app_config.h.
// stuff in front of the UI
// 26: added penguin // Protocol changes:
// 27: added templates for LOTS of characters //
// 28: added cyborg and enabled fallback sounds and textures // 25: Added a few new achievement graphics and new node attrs for displaying
// 29: added bunny and eggs // stuff in front of the UI.
// 30: added support for resource-strings in text-nodes and screen-messages //
// 31: added support for short-form resource-strings, time-display-node, and // 26: Added penguin.
// string-to-string attr connections //
// 32: added json based player profiles message, added shield // 27: Added templates for LOTS of characters.
// alwaysShowHealthBar attr //
// 33: handshake/handshake-response now send json dicts instead of // 28: Added cyborg and enabled fallback sounds and textures.
// just player-specs //
// 34: new image_node enums, data assets. // 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. // Sim step size in milliseconds.
const int kGameStepMilliseconds = 8; const int kGameStepMilliseconds = 8;
@ -215,7 +230,8 @@ enum class SessionCommand {
kScreenMessageBottom, kScreenMessageBottom,
kScreenMessageTop, kScreenMessageTop,
kAddData, kAddData,
kRemoveData kRemoveData,
kCameraShake
}; };
enum class NodeCollideAttr { enum class NodeCollideAttr {
@ -373,8 +389,8 @@ class SceneV1FeatureSet : public FeatureSetNativeComponent {
SceneV1Python* const python; SceneV1Python* const python;
private: private:
void SetupNodeMessageType(const std::string& name, NodeMessageType val, void SetupNodeMessageType_(const std::string& name, NodeMessageType val,
const std::string& format); const std::string& format);
SceneV1FeatureSet(); SceneV1FeatureSet();
std::unordered_map<std::string, NodeType*> node_types_; std::unordered_map<std::string, NodeType*> node_types_;

View File

@ -835,6 +835,11 @@ void ClientSession::Update(int time_advance_millisecs, double time_advance) {
y, z); y, z);
break; break;
} }
case SessionCommand::kCameraShake: {
auto intensity = ReadFloat();
g_base->graphics->LocalCameraShake(intensity);
break;
}
case SessionCommand::kEmitBGDynamics: { case SessionCommand::kEmitBGDynamics: {
int cmdvals[4]; int cmdvals[4];
ReadInt32_4(cmdvals); ReadInt32_4(cmdvals);

View File

@ -17,7 +17,9 @@ ClientSessionNet::ClientSessionNet() {
"g_replay_open true at netclient start; shouldn't happen."); "g_replay_open true at netclient start; shouldn't happen.");
} }
assert(g_base->assets_server); 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; writing_replay_ = true;
g_core->replay_open = true; g_core->replay_open = true;
} }

View File

@ -26,8 +26,6 @@ class ClientSessionNet : public ClientSession {
private: private:
struct SampleBucket { struct SampleBucket {
// int least_buffered_count{};
// int most_buffered_count{};
int max_delay_from_projection{}; int max_delay_from_projection{};
}; };

View File

@ -253,7 +253,7 @@ void ClientSessionReplay::OnReset(bool rewind) {
Error("error reading version"); Error("error reading version");
return; return;
} }
if (version > kProtocolVersion || version < kProtocolVersionMin) { if (version > kProtocolVersionMax || version < kProtocolVersionClientMin) {
ScreenMessage(g_base->assets->GetResourceString("replayVersionErrorText"), ScreenMessage(g_base->assets->GetResourceString("replayVersionErrorText"),
{1, 0, 0}); {1, 0, 0});
End(); End();

View File

@ -46,7 +46,7 @@ const int kKickVoteFailRetryDelayInitiatorExtra = 120000;
// to kick). // to kick).
const int kKickVoteMinimumClients = (g_buildconfig.headless_build() ? 3 : 4); const int kKickVoteMinimumClients = (g_buildconfig.headless_build() ? 3 : 4);
struct SceneV1AppMode::ScanResultsEntryPriv { struct SceneV1AppMode::ScanResultsEntryPriv_ {
scene_v1::PlayerSpec player_spec; scene_v1::PlayerSpec player_spec;
std::string address; std::string address;
uint32_t last_query_id{}; uint32_t last_query_id{};
@ -78,7 +78,16 @@ static SceneV1AppMode* g_scene_v1_app_mode{};
void SceneV1AppMode::OnActivate() { void SceneV1AppMode::OnActivate() {
assert(g_base->InLogicThread()); 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. // We use UIV1.
if (!g_core->HeadlessMode()) { if (!g_core->HeadlessMode()) {
@ -108,9 +117,10 @@ void SceneV1AppMode::OnAppPause() {
void SceneV1AppMode::OnAppResume() { assert(g_base->InLogicThread()); } void SceneV1AppMode::OnAppResume() { assert(g_base->InLogicThread()); }
// Note: for now we're making our host-scan network calls directly from the // 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 // logic thread. This is generally not a good idea since it appears that even
// non-blocking mode they're still blocking for 3-4ms sometimes. But for now // in non-blocking mode they're still blocking for 3-4ms sometimes. But for
// since this is only used minimally and only while in the UI I guess it's ok. // now since this is only used minimally and only while in the UI I guess it's
// ok.
void SceneV1AppMode::HostScanCycle() { void SceneV1AppMode::HostScanCycle() {
assert(g_base->InLogicThread()); assert(g_base->InLogicThread());
@ -258,7 +268,7 @@ void SceneV1AppMode::HostScanCycle() {
bool do_update_entry = (i == scan_results_.end() bool do_update_entry = (i == scan_results_.end()
|| i->second.last_query_id != query_id); || i->second.last_query_id != query_id);
if (do_update_entry) { if (do_update_entry) {
ScanResultsEntryPriv& entry(scan_results_[key]); ScanResultsEntryPriv_& entry(scan_results_[key]);
entry.player_spec = scene_v1::PlayerSpec(player_spec_str); entry.player_spec = scene_v1::PlayerSpec(player_spec_str);
char buffer2[256]; char buffer2[256];
entry.address = inet_ntop( entry.address = inet_ntop(
@ -269,7 +279,7 @@ void SceneV1AppMode::HostScanCycle() {
entry.last_contact_time = g_core->GetAppTimeMillisecs(); entry.last_contact_time = g_core->GetAppTimeMillisecs();
} }
} }
PruneScanResults(); PruneScanResults_();
} }
} else { } else {
Log(LogLevel::kError, Log(LogLevel::kError,
@ -290,7 +300,7 @@ void SceneV1AppMode::EndHostScanning() {
} }
} }
void SceneV1AppMode::PruneScanResults() { void SceneV1AppMode::PruneScanResults_() {
millisecs_t t = g_core->GetAppTimeMillisecs(); millisecs_t t = g_core->GetAppTimeMillisecs();
auto i = scan_results_.begin(); auto i = scan_results_.begin();
while (i != scan_results_.end()) { while (i != scan_results_.end()) {
@ -311,13 +321,13 @@ auto SceneV1AppMode::GetScanResults()
std::scoped_lock lock(scan_results_mutex_); std::scoped_lock lock(scan_results_mutex_);
int out_num = 0; int out_num = 0;
for (auto&& i : scan_results_) { for (auto&& i : scan_results_) {
ScanResultsEntryPriv& in(i.second); ScanResultsEntryPriv_& in(i.second);
ScanResultsEntry& out(results[out_num]); ScanResultsEntry& out(results[out_num]);
out.display_string = in.player_spec.GetDisplayString(); out.display_string = in.player_spec.GetDisplayString();
out.address = in.address; out.address = in.address;
out_num++; out_num++;
} }
PruneScanResults(); PruneScanResults_();
} }
return results; return results;
} }
@ -399,8 +409,8 @@ auto SceneV1AppMode::HandleJSONPing(const std::string& data_str)
} }
cJSON_Delete(data); cJSON_Delete(data);
// Ok lets include some basic info that might be pertinent to someone pinging // Ok lets include some basic info that might be pertinent to someone
// us. Currently that includes our current/max connection count. // pinging us. Currently that includes our current/max connection count.
char buffer[256]; char buffer[256];
snprintf(buffer, sizeof(buffer), R"({"b":%d,"ps":%d,"psmx":%d})", snprintf(buffer, sizeof(buffer), R"({"b":%d,"ps":%d,"psmx":%d})",
kEngineBuildNumber, public_party_size(), public_party_max_size()); kEngineBuildNumber, public_party_size(), public_party_max_size());
@ -420,7 +430,7 @@ auto SceneV1AppMode::GetPartySize() const -> int {
return cJSON_GetArraySize(game_roster_); return cJSON_GetArraySize(game_roster_);
} }
auto SceneV1AppMode::GetHeadlessDisplayStep() -> microsecs_t { auto SceneV1AppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t {
std::optional<microsecs_t> min_time_to_next; std::optional<microsecs_t> min_time_to_next;
for (auto&& i : sessions_) { for (auto&& i : sessions_) {
if (!i.Exists()) { if (!i.Exists()) {
@ -436,7 +446,7 @@ auto SceneV1AppMode::GetHeadlessDisplayStep() -> microsecs_t {
} }
} }
return min_time_to_next.has_value() ? *min_time_to_next return min_time_to_next.has_value() ? *min_time_to_next
: base::kAppModeMaxHeadlessDisplayStep; : base::kHeadlessMaxDisplayTimeStep;
} }
void SceneV1AppMode::StepDisplayTime() { void SceneV1AppMode::StepDisplayTime() {
@ -468,15 +478,15 @@ void SceneV1AppMode::StepDisplayTime() {
} }
legacy_display_time_millisecs_prev_ = legacy_display_time_millisecs_; 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. // Send the game roster to our clients if it's changed recently.
if (game_roster_dirty_) { if (game_roster_dirty_) {
if (app_time > last_game_roster_send_time_ + 2500) { if (app_time > last_game_roster_send_time_ + 2500) {
// Now send it to all connected clients. // Now send it to all connected clients.
std::vector<uint8_t> msg = GetGameRosterMessage(); std::vector<uint8_t> msg = GetGameRosterMessage_();
for (auto&& c : connections()->GetConnectionsToClients()) { for (auto&& c : connections()->GetConnectionsToClients()) {
c->SendReliableMessage(msg); c->SendReliableMessage(msg);
} }
@ -501,7 +511,7 @@ void SceneV1AppMode::StepDisplayTime() {
} }
// Go ahead and prune dead ones. // Go ahead and prune dead ones.
PruneSessions(); PruneSessions_();
in_update_ = false; in_update_ = false;
@ -521,7 +531,7 @@ void SceneV1AppMode::StepDisplayTime() {
} }
} }
auto SceneV1AppMode::GetGameRosterMessage() -> std::vector<uint8_t> { auto SceneV1AppMode::GetGameRosterMessage_() -> std::vector<uint8_t> {
// This message is simply a flattened json string of our roster (including // This message is simply a flattened json string of our roster (including
// terminating char). // terminating char).
char* s = cJSON_PrintUnformatted(game_roster_); char* s = cJSON_PrintUnformatted(game_roster_);
@ -661,7 +671,7 @@ void SceneV1AppMode::UpdateGameRoster() {
game_roster_dirty_ = true; game_roster_dirty_ = true;
} }
void SceneV1AppMode::UpdateKickVote() { void SceneV1AppMode::UpdateKickVote_() {
if (!kick_vote_in_progress_) { if (!kick_vote_in_progress_) {
return; return;
} }
@ -973,7 +983,7 @@ void SceneV1AppMode::LaunchHostSession(PyObject* session_type_obj,
base::ScopedSetContext ssc(nullptr); base::ScopedSetContext ssc(nullptr);
// This should kill any current session and get us back to a blank slate. // This should kill any current session and get us back to a blank slate.
Reset(); Reset_();
Object::WeakRef<Session> old_foreground_session(foreground_session_); Object::WeakRef<Session> old_foreground_session(foreground_session_);
try { try {
@ -1004,7 +1014,7 @@ void SceneV1AppMode::LaunchReplaySession(const std::string& file_name) {
base::ScopedSetContext ssc(nullptr); base::ScopedSetContext ssc(nullptr);
// This should kill any current session and get us back to a blank slate. // This should kill any current session and get us back to a blank slate.
Reset(); Reset_();
// Create the new session. // Create the new session.
Object::WeakRef<Session> old_foreground_session(foreground_session_); Object::WeakRef<Session> old_foreground_session(foreground_session_);
@ -1034,7 +1044,7 @@ void SceneV1AppMode::LaunchClientSession() {
base::ScopedSetContext ssc(nullptr); base::ScopedSetContext ssc(nullptr);
// This should kill any current session and get us back to a blank slate. // This should kill any current session and get us back to a blank slate.
Reset(); Reset_();
// Create the new session. // Create the new session.
Object::WeakRef<Session> old_foreground_session(foreground_session_); Object::WeakRef<Session> old_foreground_session(foreground_session_);
@ -1052,13 +1062,13 @@ void SceneV1AppMode::LaunchClientSession() {
} }
// Reset to a blank slate. // Reset to a blank slate.
void SceneV1AppMode::Reset() { void SceneV1AppMode::Reset_() {
assert(g_base); assert(g_base);
assert(g_base->InLogicThread()); assert(g_base->InLogicThread());
// Tear down our existing session. // Tear down our existing session.
foreground_session_.Clear(); foreground_session_.Clear();
PruneSessions(); PruneSessions_();
// If all is well our sessions should all be dead. // If all is well our sessions should all be dead.
if (g_core->session_count != 0) { if (g_core->session_count != 0) {
@ -1174,7 +1184,7 @@ void SceneV1AppMode::DoApplyAppConfig() {
base::AppConfig::OptionalFloatID::kIdleExitMinutes); base::AppConfig::OptionalFloatID::kIdleExitMinutes);
} }
void SceneV1AppMode::PruneSessions() { void SceneV1AppMode::PruneSessions_() {
bool have_dead_session = false; bool have_dead_session = false;
for (auto&& i : sessions_) { for (auto&& i : sessions_) {
if (i.Exists()) { 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); banned_players_.emplace_back(g_core->GetAppTimeMillisecs() + duration, spec);
} }
void SceneV1AppMode::HandleQuitOnIdle() { void SceneV1AppMode::HandleQuitOnIdle_() {
if (idle_exit_minutes_) { if (idle_exit_minutes_) {
auto idle_seconds{static_cast<float>(g_base->input->input_idle_time()) auto idle_seconds{static_cast<float>(g_base->input->input_idle_time())
* 0.001f}; * 0.001f};
@ -1443,7 +1453,7 @@ void SceneV1AppMode::HandleGameQuery(const char* buffer, size_t size,
msg[0] = BA_PACKET_HOST_QUERY_RESPONSE; msg[0] = BA_PACKET_HOST_QUERY_RESPONSE;
memcpy(msg + 1, &query_id, 4); memcpy(msg + 1, &query_id, 4);
uint32_t protocol_version = kProtocolVersion; uint32_t protocol_version = host_protocol_version();
memcpy(msg + 5, &protocol_version, 4); memcpy(msg + 5, &protocol_version, 4);
msg[9] = static_cast<char>(usid.size()); msg[9] = static_cast<char>(usid.size());
msg[10] = static_cast<char>(player_spec_string.size()); msg[10] = static_cast<char>(player_spec_string.size());

View File

@ -182,41 +182,56 @@ class SceneV1AppMode : public base::AppMode {
auto buffer_time() const { return buffer_time_; } auto buffer_time() const { return buffer_time_; }
void set_buffer_time(int val) { buffer_time_ = val; } void set_buffer_time(int val) { buffer_time_ = val; }
void OnActivate() override; 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: private:
void PruneScanResults();
void UpdateKickVote();
SceneV1AppMode(); SceneV1AppMode();
auto GetGameRosterMessage() -> std::vector<uint8_t>; void PruneScanResults_();
void Reset(); void UpdateKickVote_();
void PruneSessions(); auto GetGameRosterMessage_() -> std::vector<uint8_t>;
void HandleQuitOnIdle(); void Reset_();
struct ScanResultsEntryPriv; void PruneSessions_();
void HandleQuitOnIdle_();
struct ScanResultsEntryPriv_;
// Note: would use an unordered_map here but gcc doesn't seem to allow // Note: would use an unordered_map here but gcc doesn't seem to allow
// forward declarations of their template params. // forward declarations of their template params.
std::map<std::string, ScanResultsEntryPriv> scan_results_; std::map<std::string, ScanResultsEntryPriv_> scan_results_;
std::mutex scan_results_mutex_; std::mutex scan_results_mutex_;
uint32_t next_scan_query_id_{}; uint32_t next_scan_query_id_{};
int scan_socket_{-1}; int scan_socket_{-1};
int host_protocol_version_{-1};
std::list<std::string> chat_messages_; std::list<std::string> chat_messages_;
bool chat_muted_{};
// *All* existing sessions (including old ones waiting to shut down). // *All* existing sessions (including old ones waiting to shut down).
std::vector<Object::Ref<Session> > sessions_; std::vector<Object::Ref<Session> > sessions_;
Object::WeakRef<Scene> foreground_scene_; Object::WeakRef<Scene> foreground_scene_;
Object::WeakRef<Session> foreground_session_; Object::WeakRef<Session> 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_{}; millisecs_t last_game_roster_send_time_{};
std::unique_ptr<ConnectionSet> connections_; std::unique_ptr<ConnectionSet> connections_;
cJSON* game_roster_{};
Object::WeakRef<ConnectionToClient> kick_vote_starter_; Object::WeakRef<ConnectionToClient> kick_vote_starter_;
Object::WeakRef<ConnectionToClient> kick_vote_target_; Object::WeakRef<ConnectionToClient> kick_vote_target_;
millisecs_t kick_vote_end_time_{}; millisecs_t kick_vote_end_time_{};
bool kick_vote_in_progress_{};
int last_kick_votes_needed_{-1}; int last_kick_votes_needed_{-1};
bool kick_voting_enabled_{true};
millisecs_t legacy_display_time_millisecs_{}; millisecs_t legacy_display_time_millisecs_{};
millisecs_t legacy_display_time_millisecs_prev_{-1}; millisecs_t legacy_display_time_millisecs_prev_{-1};
@ -229,28 +244,22 @@ class SceneV1AppMode : public base::AppMode {
// it over the network. // it over the network.
int buffer_time_{}; int buffer_time_{};
bool in_update_{};
millisecs_t next_long_update_report_time_{}; millisecs_t next_long_update_report_time_{};
int debug_speed_exponent_{}; int debug_speed_exponent_{};
float debug_speed_mult_{1.0f};
int replay_speed_exponent_{}; int replay_speed_exponent_{};
float replay_speed_mult_{1.0f};
bool kick_idle_players_{};
std::set<std::string> 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_size_{1}; // Always count ourself (is that what we want?).
int public_party_max_size_{8}; int public_party_max_size_{8};
bool public_party_queue_enabled_{true};
int public_party_player_count_{0}; int public_party_player_count_{0};
int public_party_max_player_count_{8}; int public_party_max_player_count_{8};
float debug_speed_mult_{1.0f};
float replay_speed_mult_{1.0f};
std::set<std::string> admin_public_ids_;
millisecs_t last_connection_to_client_join_time_{};
std::string public_party_name_; std::string public_party_name_;
std::string public_party_min_league_; std::string public_party_min_league_;
std::string public_party_stats_url_; std::string public_party_stats_url_;
bool require_client_authentication_{};
std::list<std::pair<millisecs_t, PlayerSpec> > banned_players_; std::list<std::pair<millisecs_t, PlayerSpec> > banned_players_;
std::optional<float> idle_exit_minutes_{}; std::optional<float> idle_exit_minutes_{};
bool idle_exiting_{};
std::optional<uint32_t> internal_music_play_id_{}; std::optional<uint32_t> internal_music_play_id_{};
}; };

View File

@ -31,8 +31,9 @@ SessionStream::SessionStream(HostSession* host_session, bool save_replay)
Log(LogLevel::kError, Log(LogLevel::kError,
"g_replay_open true at replay start; shouldn't happen."); "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); assert(g_base->assets_server);
g_base->assets_server->PushBeginWriteReplayCall(kProtocolVersion); g_base->assets_server->PushBeginWriteReplayCall(kProtocolVersionMax);
writing_replay_ = true; writing_replay_ = true;
g_core->replay_open = 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. // Writes just a command.
void SessionStream::WriteCommand(SessionCommand cmd) { void SessionStream::WriteCommand(SessionCommand cmd) {
assert(out_command_.empty()); assert(out_command_.empty());
@ -219,8 +217,6 @@ void SessionStream::WriteCommand(SessionCommand cmd) {
*ptr = static_cast<uint8_t>(cmd); *ptr = static_cast<uint8_t>(cmd);
} }
#pragma clang diagnostic pop
// Writes a command plus an int to the stream, using whatever size is optimal. // Writes a command plus an int to the stream, using whatever size is optimal.
void SessionStream::WriteCommandInt32(SessionCommand cmd, int32_t value) { void SessionStream::WriteCommandInt32(SessionCommand cmd, int32_t value) {
assert(out_command_.empty()); assert(out_command_.empty());
@ -1123,6 +1119,13 @@ void SessionStream::EmitBGDynamics(const base::BGDynamicsEmission& e) {
EndCommand(); 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) { void SessionStream::PlaySound(SceneSound* sound, float volume) {
assert(IsValidSound(sound)); assert(IsValidSound(sound));
assert(IsValidScene(sound->scene())); assert(IsValidScene(sound->scene()));

View File

@ -69,6 +69,7 @@ class SessionStream : public Object, public ClientControllerInterface {
float z); float z);
void PlaySound(SceneSound* sound, float volume); void PlaySound(SceneSound* sound, float volume);
void EmitBGDynamics(const base::BGDynamicsEmission& e); void EmitBGDynamics(const base::BGDynamicsEmission& e);
void EmitCameraShake(float intensity);
auto GetSoundID(SceneSound* s) -> int64_t; auto GetSoundID(SceneSound* s) -> int64_t;
auto GetMaterialID(Material* m) -> int64_t; auto GetMaterialID(Material* m) -> int64_t;
void ScreenMessageBottom(const std::string& val, float r, float g, float b); void ScreenMessageBottom(const std::string& val, float r, float g, float b);

View File

@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
namespace ballistica { namespace ballistica {
// These are set automatically via script; don't modify them here. // These are set automatically via script; don't modify them here.
const int kEngineBuildNumber = 21447; const int kEngineBuildNumber = 21465;
const char* kEngineVersion = "1.7.28"; const char* kEngineVersion = "1.7.28";
const int kEngineApiVersion = 8; const int kEngineApiVersion = 8;

View File

@ -248,6 +248,9 @@ void EventLoop::WaitForNextEvent_(bool single_cycle) {
} }
} }
// Note to self (Oct '23): can probably kill this at some point,
// but am still using some non-ARC objc stuff from logic thread
// so should keep it around just a bit longer just in case.
void EventLoop::LoopUpkeep_(bool single_cycle) { void EventLoop::LoopUpkeep_(bool single_cycle) {
assert(g_core); assert(g_core);
// Keep our autorelease pool clean on mac/ios // Keep our autorelease pool clean on mac/ios

View File

@ -118,7 +118,6 @@ class EventLoop {
auto CheckPushRunnableSafety_() -> bool; auto CheckPushRunnableSafety_() -> bool;
void SetInternalThreadName_(const std::string& name); void SetInternalThreadName_(const std::string& name);
void WaitForNextEvent_(bool single_cycle); void WaitForNextEvent_(bool single_cycle);
void LoopUpkeep_(bool single_cycle);
void LogThreadMessageTally_( void LogThreadMessageTally_(
std::vector<std::pair<LogLevel, std::string>>* log_entries); std::vector<std::pair<LogLevel, std::string>>* log_entries);
void PushLocalRunnable_(Runnable* runnable, bool* completion_flag); void PushLocalRunnable_(Runnable* runnable, bool* completion_flag);
@ -155,18 +154,7 @@ class EventLoop {
void BootstrapThread_(); void BootstrapThread_();
bool writing_tally_{}; void LoopUpkeep_(bool single_cycle);
bool suspended_{};
millisecs_t last_suspend_time_{};
int messages_since_suspended_{};
millisecs_t last_suspended_message_report_time_{};
bool done_{};
ThreadSource source_;
int listen_sd_{};
std::thread::id thread_id_{};
EventLoopID identifier_{EventLoopID::kInvalid};
millisecs_t last_complaint_time_{};
bool acquires_python_gil_{};
// FIXME: Should generalize this to some sort of PlatformThreadData class. // FIXME: Should generalize this to some sort of PlatformThreadData class.
#if BA_XCODE_BUILD #if BA_XCODE_BUILD
@ -174,13 +162,25 @@ class EventLoop {
#endif #endif
bool bootstrapped_{}; bool bootstrapped_{};
bool writing_tally_{};
bool suspended_{};
bool done_{};
bool acquires_python_gil_{};
EventLoopID identifier_{EventLoopID::kInvalid};
millisecs_t last_suspend_time_{};
int listen_sd_{};
int messages_since_suspended_{};
millisecs_t last_suspended_message_report_time_{};
millisecs_t last_complaint_time_{};
ThreadSource source_;
std::thread::id thread_id_{};
std::list<std::pair<Runnable*, bool*>> runnables_; std::list<std::pair<Runnable*, bool*>> runnables_;
std::list<Runnable*> suspend_callbacks_; std::list<Runnable*> suspend_callbacks_;
std::list<Runnable*> unsuspend_callbacks_; std::list<Runnable*> unsuspend_callbacks_;
std::condition_variable thread_message_cv_; std::condition_variable thread_message_cv_;
std::condition_variable client_listener_cv_;
std::mutex thread_message_mutex_; std::mutex thread_message_mutex_;
std::list<ThreadMessage_> thread_messages_; std::list<ThreadMessage_> thread_messages_;
std::condition_variable client_listener_cv_;
std::mutex client_listener_mutex_; std::mutex client_listener_mutex_;
std::list<std::vector<char>> data_to_client_; std::list<std::vector<char>> data_to_client_;
PyThreadState* py_thread_state_{}; PyThreadState* py_thread_state_{};

View File

@ -88,8 +88,7 @@ template <typename OUT_TYPE, typename IN_TYPE>
auto static_cast_check_type(IN_TYPE in) -> OUT_TYPE { auto static_cast_check_type(IN_TYPE in) -> OUT_TYPE {
auto out_static = static_cast<OUT_TYPE>(in); auto out_static = static_cast<OUT_TYPE>(in);
if (g_buildconfig.debug_build()) { if (g_buildconfig.debug_build()) {
auto out_dynamic = dynamic_cast<OUT_TYPE>(in); assert(out_static == dynamic_cast<OUT_TYPE>(in));
assert(out_static == out_dynamic);
} }
return out_static; return out_static;
} }

View File

@ -30,6 +30,7 @@ typedef struct _SDL_Joystick SDL_Joystick;
namespace ballistica { namespace ballistica {
// Used internally for time values. // Used internally for time values.
typedef double seconds_t;
typedef int64_t millisecs_t; typedef int64_t millisecs_t;
typedef int64_t microsecs_t; typedef int64_t microsecs_t;
@ -68,7 +69,7 @@ class Graphics;
/// ///
/// Category: Enums /// Category: Enums
/// ///
enum class InputType { enum class InputType : uint8_t {
kUpDown = 2, kUpDown = 2,
kLeftRight, kLeftRight,
kJumpPress, kJumpPress,
@ -111,7 +112,7 @@ enum class InputType {
/// ///
/// 'hard' leads to the process exiting. This generally should be avoided /// 'hard' leads to the process exiting. This generally should be avoided
/// on platforms such as mobile. /// on platforms such as mobile.
enum class QuitType { enum class QuitType : uint8_t {
kSoft, kSoft,
kBack, kBack,
kHard, kHard,
@ -139,7 +140,7 @@ typedef int64_t TimerMedium;
/// 'small' is used primarily for phones or other small devices where /// 'small' is used primarily for phones or other small devices where
/// content needs to be presented as large and clear in order to remain /// content needs to be presented as large and clear in order to remain
/// readable from an average distance. /// readable from an average distance.
enum class UIScale { enum class UIScale : uint8_t {
kLarge, kLarge,
kMedium, kMedium,
kSmall, kSmall,
@ -162,7 +163,7 @@ enum class UIScale {
/// 'real' time is mostly based on clock time, with a few exceptions. It may /// '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 /// not advance while the app is backgrounded for instance. (the engine
/// attempts to prevent single large time jumps from occurring) /// attempts to prevent single large time jumps from occurring)
enum class TimeType { enum class TimeType : uint8_t {
kSim, kSim,
kBase, kBase,
kReal, kReal,
@ -173,7 +174,7 @@ enum class TimeType {
/// Specifies the format time values are provided in. /// Specifies the format time values are provided in.
/// ///
/// Category: Enums /// Category: Enums
enum class TimeFormat { enum class TimeFormat : uint8_t {
kSeconds, kSeconds,
kMilliseconds, kMilliseconds,
kLast // Sentinel. kLast // Sentinel.
@ -183,7 +184,7 @@ enum class TimeFormat {
/// Permissions that can be requested from the OS. /// Permissions that can be requested from the OS.
/// ///
/// Category: Enums /// Category: Enums
enum class Permission { enum class Permission : uint8_t {
kStorage, kStorage,
kLast // Sentinel. kLast // Sentinel.
}; };
@ -192,7 +193,7 @@ enum class Permission {
/// Special characters the game can print. /// Special characters the game can print.
/// ///
/// Category: Enums /// Category: Enums
enum class SpecialChar { enum class SpecialChar : uint8_t {
kDownArrow, kDownArrow,
kUpArrow, kUpArrow,
kLeftArrow, kLeftArrow,
@ -288,7 +289,7 @@ enum class SpecialChar {
}; };
/// Python exception types we can raise from our own exceptions. /// Python exception types we can raise from our own exceptions.
enum class PyExcType { enum class PyExcType : uint8_t {
kRuntime, kRuntime,
kAttribute, kAttribute,
kIndex, kIndex,
@ -306,7 +307,7 @@ enum class PyExcType {
kWidgetNotFound kWidgetNotFound
}; };
enum class LogLevel { enum class LogLevel : uint8_t {
kDebug, kDebug,
kInfo, kInfo,
kWarning, kWarning,
@ -314,7 +315,7 @@ enum class LogLevel {
kCritical, kCritical,
}; };
enum class ThreadSource { enum class ThreadSource : uint8_t {
/// Spin up a new thread for the event loop. /// Spin up a new thread for the event loop.
kCreate, kCreate,
/// Wrap the event loop around the current thread. /// Wrap the event loop around the current thread.
@ -323,7 +324,7 @@ enum class ThreadSource {
/// Used for thread identification. /// Used for thread identification.
/// Mostly just for debugging. /// Mostly just for debugging.
enum class EventLoopID { enum class EventLoopID : uint8_t {
kInvalid, kInvalid,
kLogic, kLogic,
kAssets, kAssets,

View File

@ -23,10 +23,6 @@
#include "ballistica/ui_v1/widget/row_widget.h" #include "ballistica/ui_v1/widget/row_widget.h"
#include "ballistica/ui_v1/widget/scroll_widget.h" #include "ballistica/ui_v1/widget/scroll_widget.h"
// #if !BA_HEADLESS_BUILD && !BA_XCODE_NEW_PROJECT
// extern "C" void SDL_ericf_focus(void);
// #endif
namespace ballistica::ui_v1 { namespace ballistica::ui_v1 {
// Ignore signed bitwise stuff; python macros do it quite a bit. // Ignore signed bitwise stuff; python macros do it quite a bit.

View File

@ -143,6 +143,12 @@ class ServerConfig:
# queue spamming attacks. # queue spamming attacks.
enable_queue: bool = True 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. # (internal) stress-testing mode.
stress_test_players: int | None = None stress_test_players: int | None = None

View File

@ -601,6 +601,7 @@ def _get_server_config_raw_contents(projroot: str) -> str:
def _get_server_config_template_yaml(projroot: str) -> str: def _get_server_config_template_yaml(projroot: str) -> str:
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-statements
import yaml import yaml
lines_in = _get_server_config_raw_contents(projroot).splitlines() 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}' vval = 'https://mystatssite.com/showstats?player=${ACCOUNT}'
elif vname == 'admins': elif vname == 'admins':
vval = ['pb-yOuRAccOuNtIdHErE', 'pb-aNdMayBeAnotherHeRE'] vval = ['pb-yOuRAccOuNtIdHErE', 'pb-aNdMayBeAnotherHeRE']
elif vname == 'protocol_version':
vval = 35
lines_out += [ lines_out += [
'#' + l for l in yaml.dump({vname: vval}).strip().splitlines() '#' + l for l in yaml.dump({vname: vval}).strip().splitlines()
] ]

View File

@ -59,6 +59,10 @@ def _gen_enums(infilename: str) -> str:
def _parse_name(lines: list[str], lnum: int) -> str: def _parse_name(lines: list[str], lnum: int) -> str:
bits = lines[lnum].split(' ') 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 ( if (
len(bits) != 4 len(bits) != 4
or bits[0] != 'enum' or bits[0] != 'enum'

View File

@ -29,7 +29,7 @@ from batools.spinoff._state import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Iterable from typing import Callable, Iterable, Any
from batools.project import ProjectUpdater from batools.project import ProjectUpdater
@ -54,6 +54,7 @@ class SpinoffContext:
OVERRIDE = 'override' OVERRIDE = 'override'
DIFF = 'diff' DIFF = 'diff'
BACKPORT = 'backport' BACKPORT = 'backport'
DESCRIBE_PATH = 'describe_path'
def __init__( def __init__(
self, self,
@ -66,6 +67,7 @@ class SpinoffContext:
override_paths: list[str] | None = None, override_paths: list[str] | None = None,
backport_file: str | None = None, backport_file: str | None = None,
auto_backport: bool = False, auto_backport: bool = False,
describe_path: str | None = None,
) -> None: ) -> None:
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
@ -82,6 +84,7 @@ class SpinoffContext:
self._override_paths = override_paths self._override_paths = override_paths
self._backport_file = backport_file self._backport_file = backport_file
self._auto_backport = auto_backport self._auto_backport = auto_backport
self._describe_path = describe_path
self._project_updater: ProjectUpdater | None = None self._project_updater: ProjectUpdater | None = None
@ -278,10 +281,10 @@ class SpinoffContext:
self._src_omit_feature_sets, self._src_omit_feature_sets,
) = self._calc_src_retain_omit_feature_sets() ) = self._calc_src_retain_omit_feature_sets()
# Generate a version of src_omit_paths that includes our feature-set # Generate a version of src_omit_paths that includes some extra values
# omissions. Basically, omitting a feature set simply omits
# particular names at a few particular places.
self._src_omit_paths_expanded = self.src_omit_paths.copy() 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) self._add_feature_set_omit_paths(self._src_omit_paths_expanded)
# Create a version of dst-write-paths that also includes filtered # Create a version of dst-write-paths that also includes filtered
@ -362,6 +365,7 @@ class SpinoffContext:
def run(self) -> None: def run(self) -> None:
"""Do the thing.""" """Do the thing."""
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-statements
self._read_state() self._read_state()
@ -400,6 +404,20 @@ class SpinoffContext:
# Ignore anything under omitted paths/names. # Ignore anything under omitted paths/names.
self._filter_src_git_file_list() 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. # Now map whatever is left to paths in dst.
self._dst_git_files = set( self._dst_git_files = set(
self._filter_path(s) for s in self._src_git_files self._filter_path(s) for s in self._src_git_files
@ -454,8 +472,10 @@ class SpinoffContext:
) )
raise self.BackportInProgressError 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 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() self._print_error_entities()
else: else:
if ( if (
@ -511,6 +531,76 @@ class SpinoffContext:
if self._mode is self.Mode.UPDATE or self._mode is self.Mode.OVERRIDE: if self._mode is self.Mode.UPDATE or self._mode is self.Mode.OVERRIDE:
self._write_gitignore() 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: def _apply_project_configs(self) -> None:
# pylint: disable=exec-used # pylint: disable=exec-used
try: try:
@ -744,9 +834,6 @@ class SpinoffContext:
# Strip out any sections frames by our strip-begin/end tags. # 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( def _first_index_containing_string(
items: list[str], substring: str items: list[str], substring: str
) -> int | None: ) -> int | None:
@ -795,7 +882,7 @@ class SpinoffContext:
'make any edits in source project)' 'make any edits in source project)'
) )
lines = self.default_filter_text(text).splitlines() 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: if 'Jenkinsfile' in src_path:
blurb = ( blurb = (
'// THIS FILE IS AUTOGENERATED BY SPINOFF;' '// THIS FILE IS AUTOGENERATED BY SPINOFF;'
@ -1028,8 +1115,10 @@ class SpinoffContext:
"""Print info about entity errors encountered.""" """Print info about entity errors encountered."""
print( print(
'\nSpinoff Error(s) Found:\n' '\nSpinoff Error(s) Found:\n'
" Tip: to resolve 'spinoff-managed file modified' errors,\n" " Tips: To resolve 'spinoff-managed file modified' errors,\n"
" use the 'backport' subcommand.\n", " use the 'backport' subcommand.\n"
" To debug other issues, try the 'describe-path'"
' subcommand.\n',
file=sys.stderr, file=sys.stderr,
) )
for key, val in sorted(self._src_error_entities.items()): for key, val in sorted(self._src_error_entities.items()):
@ -1046,7 +1135,18 @@ class SpinoffContext:
print('') print('')
def _validate_final_lists(self) -> None: 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(): for ent in self._dst_purge_entities.copy():
if _any_path_contains(self.git_mirrored_paths, ent): if _any_path_contains(self.git_mirrored_paths, ent):
@ -1806,7 +1906,8 @@ class SpinoffContext:
) )
def _filter_src_git_file_list(self) -> None: 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]() out = set[str]()
assert self._src_git_files is not None assert self._src_git_files is not None
for gitpath in self._src_git_files: 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 # 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 # 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 # Note that we need to ignore git-mirrored-paths because git might
# be mucking with modtimes itself. # be mucking with modtimes itself.
if ( if (

View File

@ -32,6 +32,7 @@ class Command(Enum):
CLEAN_CHECK = 'cleancheck' CLEAN_CHECK = 'cleancheck'
OVERRIDE = 'override' OVERRIDE = 'override'
DIFF = 'diff' DIFF = 'diff'
DESCRIBE_PATH = 'describe-path'
BACKPORT = 'backport' BACKPORT = 'backport'
CREATE = 'create' CREATE = 'create'
ADD_SUBMODULE_PARENT = 'add-submodule-parent' ADD_SUBMODULE_PARENT = 'add-submodule-parent'
@ -94,6 +95,8 @@ def _main() -> None:
single_run_mode = SpinoffContext.Mode.CLEAN_CHECK single_run_mode = SpinoffContext.Mode.CLEAN_CHECK
elif cmd is Command.DIFF: elif cmd is Command.DIFF:
single_run_mode = SpinoffContext.Mode.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: elif cmd is Command.OVERRIDE:
_do_override(src_root, dst_root) _do_override(src_root, dst_root)
elif cmd is Command.BACKPORT: elif cmd is Command.BACKPORT:
@ -115,6 +118,12 @@ def _main() -> None:
assert_never(cmd) assert_never(cmd)
if single_run_mode is not None: 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 src_root is None:
if '--soft' in sys.argv: if '--soft' in sys.argv:
return return
@ -123,6 +132,15 @@ def _main() -> None:
' you appear to be in a src project.' ' you appear to be in a src project.'
" To silently no-op in this case, pass '--soft'." " 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 # SpinoffContext should never be relying on relative paths, so let's
# keep ourself honest by making sure. # keep ourself honest by making sure.
os.chdir('/') os.chdir('/')
@ -130,9 +148,10 @@ def _main() -> None:
src_root, src_root,
dst_root, dst_root,
single_run_mode, single_run_mode,
force='--force' in sys.argv, force=force,
verbose='--verbose' in sys.argv, verbose=verbose,
print_full_lists='--full' in sys.argv, print_full_lists=print_full_lists,
describe_path=describe_path,
).run() ).run()
@ -581,6 +600,8 @@ def _print_available_commands() -> None:
'Remove files from spinoff, leaving local copies in place.\n' 'Remove files from spinoff, leaving local copies in place.\n'
f' {bgn}backport [file]{end} ' f' {bgn}backport [file]{end} '
'Help get changes to spinoff dst files back to src.\n' '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} ' f' {bgn}create [name, path]{end} '
'Create a new spinoff project based on this src one.\n' 'Create a new spinoff project based on this src one.\n'
' Name should be passed in CamelCase form.\n' ' Name should be passed in CamelCase form.\n'

View File

@ -165,7 +165,6 @@ class _Outputter:
) )
return value if self._create else None return value if self._create else None
# noinspection PyPep8
if origin is typing.Union or origin is types.UnionType: if origin is typing.Union or origin is types.UnionType:
# Currently, the only unions we support are None/Value # Currently, the only unions we support are None/Value
# (translated from Optional), which we verified on prep. # (translated from Optional), which we verified on prep.