mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-19 13:25:31 +08:00
various fixes and polishing
This commit is contained in:
parent
d775eefdde
commit
baa09a775d
74
.efrocachemap
generated
74
.efrocachemap
generated
@ -1,5 +1,5 @@
|
||||
{
|
||||
"ballisticakit-windows/Generic/BallisticaKit.ico": "be1b956dcd7f7a261b1afe5bce2a0336",
|
||||
"ballisticakit-windows/Generic/BallisticaKit.ico": "6f33e74cb282f070871413f092983fcd",
|
||||
"build/assets/ba_data/audio/achievement.ogg": "079a366ce183b25a63550ef7072af605",
|
||||
"build/assets/ba_data/audio/actionHero1.ogg": "f0f986f268f036a5ac2f940e07f2f27e",
|
||||
"build/assets/ba_data/audio/actionHero2.ogg": "204a6735dc655f0975cf8308b585f2fd",
|
||||
@ -4056,50 +4056,50 @@
|
||||
"build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1",
|
||||
"build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae",
|
||||
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
|
||||
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "4129fd84c3c64b770c4343d347bff97a",
|
||||
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "76b293b2d942716c2606c56e13483e66",
|
||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "9cc2a5d464da1e0822ecf5493ac28b64",
|
||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "40e605e595f00286805cf15ffc096813",
|
||||
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "bdc47e8dd94bcfc2a7b8268ea366f9b5",
|
||||
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "6b57b2de2d4aefcb3a5f7df6cef53a9d",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "34c65f2a9f280420387af135d5bc6a2d",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "79e4d857fbd0871940c7fd6985d11af1",
|
||||
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "ce179db11df98c7dc53bd2fd2a710909",
|
||||
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "d47c86f2654185ab0690949c3b8f2913",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "a33fb01bdbeb7be34046d7b64673991c",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "5f4cb90415aed9415231e95394384d2a",
|
||||
"build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "26540ab81b0ad717818e077efcb9029d",
|
||||
"build/prefab/full/mac_x86_64_gui/release/ballisticakit": "11fc4a0cdf83c362b2f5c0c62a53014e",
|
||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "ba10cfebc435f211be18dbdc7416097d",
|
||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "551b8dba96bfc65c5166cde6bec37539",
|
||||
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "bc18f0765c9d96731a1b1a7acf5151db",
|
||||
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "1f1a8227190894db122fb43691371a92",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "9d546413ee365044d77e0e9c39ed77bb",
|
||||
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "470b427c4e51254538291ac56298c212",
|
||||
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "a61cf6ac9afd43081df7f63ff81f4036",
|
||||
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "af600306cf8085f909e40a7d2129a73b",
|
||||
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "54f7cdbdbc16601ff1f841e48cecf05c",
|
||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "cc6387a5d3a8f36b7bf667f9b7e719df",
|
||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "0de15cf59051ba17ee91e3d3ac25be93",
|
||||
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "345c4646377145cf88b30fcba122f42b",
|
||||
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "4dae703893e19bed1e8a16029a646104",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "573adb2913309a6c375a6d3b92a20208",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "3db3c7e41182fd05f5f9731fee3002d0",
|
||||
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "1e3e5ee34645928ec3bdc3c52de98839",
|
||||
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "41354efa8cdbb83a3b2e4cd7fce6b308",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "a304c1a0d2d2f5bdbc31a27ec899a95b",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "6b236e3246ded0460fcc7ea50fc51b36",
|
||||
"build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "e26b4e3b5e2bca5606d2ac674fcc7496",
|
||||
"build/prefab/full/mac_x86_64_gui/release/ballisticakit": "ae954a12775213c64d45b535b289ebff",
|
||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "960c3e2fd9c3e19de75fd92546447890",
|
||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "e0c05531b48ec5e36da10783280957fe",
|
||||
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "e1c137fdefcf34a642c0962999973eff",
|
||||
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "bdf75b68e6b0e1c8dcfa76ac04306fbb",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "5f01ab596c3d389c95761db531b1766b",
|
||||
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "4826ca9c757dbf38749710bc94844aca",
|
||||
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "20cc128ff9d44f9d74e4301c6d49f48f",
|
||||
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "f93bc8f98ee31f39b54ab46264eccb22",
|
||||
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "a61cf6ac9afd43081df7f63ff81f4036",
|
||||
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "20cc128ff9d44f9d74e4301c6d49f48f",
|
||||
"build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "f93bc8f98ee31f39b54ab46264eccb22",
|
||||
"build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "117f4fc8b60372763ecae00d332947a8",
|
||||
"build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "1565f3b227843827d692cb3ef65847b6",
|
||||
"build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "d8d74c6c40db43054ccc7d27920cfbfe",
|
||||
"build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "117f4fc8b60372763ecae00d332947a8",
|
||||
"build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "1565f3b227843827d692cb3ef65847b6",
|
||||
"build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "d8d74c6c40db43054ccc7d27920cfbfe",
|
||||
"build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "ba3a67e11c268a5b81b3416bc90cc297",
|
||||
"build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "43bdfa8acd84e9cf2e443ce8e923c229",
|
||||
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "d935dc21becdfd65bec51c5f5b2fd770",
|
||||
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "ba3a67e11c268a5b81b3416bc90cc297",
|
||||
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "43bdfa8acd84e9cf2e443ce8e923c229",
|
||||
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "d935dc21becdfd65bec51c5f5b2fd770",
|
||||
"build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "c5fb4b7f765cfd68ff09bcef3c5f84e6",
|
||||
"build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "6d49ad39f194480da458b431405f5a2b",
|
||||
"build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "c8715c85010ea431d7346f40f5421819",
|
||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "737778fc5e6a6ed5ca154c7953fcb377",
|
||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "49775819d4ba9af15061080d17377a18",
|
||||
"build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "c8715c85010ea431d7346f40f5421819",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "dfd5dca061d6eacaccc38c12d391cc82",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "96ab39f16820a39fa7c42af6779c3556",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "d87f20b0a0192b90687b936b6c2ad103",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "4a336c3e976924b70b39d18af6040e41",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "77d10f03566f29816de9b2ff806cc905",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "d90ef473768f7ba232ae3ca58c5c8c04",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "743d872f2c848a84e4e5d49ca6b426e9",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "36272a00b45729022167baa81749a37e",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "99e44969ef0f9421f21cb32554464bc7",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "a14441d4ff9adae6e015a9f0d107a61a",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "90b5c353d5296be7e3b06e8c823564bb",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "84409ce44260a641295c1459e4d3af8f",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "35c58dd0a7482bb5f1ef5d6edb647c46",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "a7c2895f59f75b767258277c766e9ed4",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "e0430eeeb324ccfca8ace41d743e069c",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "d594057005fc56c8576f555f892b3dc4",
|
||||
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
|
||||
"src/assets/ba_data/python/babase/_mgen/enums.py": "28323912b56ec07701eda3d41a6a4101",
|
||||
"src/ballistica/base/mgen/pyembed/binding_base.inc": "ba8ce3ca3858b4c2d20db68f99b788b2",
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -120,10 +120,10 @@ xcuserdata/
|
||||
/ballisticakit-android/BallisticaKit/src/main/res/mipmap-*/ic_launcher*.png
|
||||
/ballisticakit-android/BallisticaKit/src/cardboard/res/mipmap-*/ic_launcher*.png
|
||||
BallisticaKit.ico
|
||||
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/Cursor macOS.appiconset/cursor_*.png
|
||||
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/AppIcon iOS.appiconset/icon_*.png
|
||||
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/AppIcon macOS.appiconset/icon_*.png
|
||||
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Layer*.imagestacklayer/Content.imageset/*.png
|
||||
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Layer*.imagestacklayer/Content.imageset/*.png
|
||||
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/*.png
|
||||
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/*.png
|
||||
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/Cursor macOS.imageset/cursor_*.png
|
||||
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -4,7 +4,7 @@
|
||||
<option name="cmdArguments" value="--line-length 80 --skip-string-normalization" />
|
||||
<option name="enabledOnReformat" value="true" />
|
||||
<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 name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
|
||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@ -1,4 +1,4 @@
|
||||
### 1.7.28 (build 21447, api 8, 2023-10-12)
|
||||
### 1.7.28 (build 21453, api 8, 2023-10-13)
|
||||
|
||||
- Massively cleaned up code related to rendering and window systems (OpenGL,
|
||||
SDL, etc). This code had been growing into a nasty tangle for 15 years
|
||||
@ -12,8 +12,10 @@
|
||||
newer on mobile. This means we're cutting off a few percent of old devices on
|
||||
Android that only support ES 2, but ES 3 has been out for 10 years now so I
|
||||
feel it is time. As mentioned above, this allows massively cleaning up the
|
||||
graphics code which means we can start to improve it.
|
||||
- Removed gamma controls. These were only active on the old Mac version anyway
|
||||
graphics code which means we can start to improve it. Ideally now the GL
|
||||
renderer can be abstracted a bit more which will make the process of writing
|
||||
other renderers easier.
|
||||
- Removed gamma controls. These were only active on the old Mac builds anyway
|
||||
and are being removed from the upcoming SDL3, so if we want this sort of thing
|
||||
we should do it through shading in the renderer now.
|
||||
- Implemented both vsync and max-fps for the SDL build of the game. This means
|
||||
@ -129,7 +131,20 @@
|
||||
before. It also takes a `confirm` bool arg which allows it to be used to bring
|
||||
up a confirm dialog.
|
||||
- Clicking on a window close button to quit no longer brings up a confirm dialog
|
||||
and instead quits immediately (though with a proper graceful shutdown).
|
||||
and instead quits immediately (though with a proper graceful shutdown and a
|
||||
lovely little fade).
|
||||
- Camera shake is now supported in network games and replays. Somehow I didn't
|
||||
notice that was missing for years. The downside is this requires a server to
|
||||
be hosting protocol 35, which cuts off support for 1.4 clients. So for now I
|
||||
am keeping the default at 33. Once there a fewer 1.4 clients around we can
|
||||
consider changing this (if everything hasn't moved to SceneV2 by then).
|
||||
- Added a server option to set the hosting protocol for servers who might want
|
||||
to allow camera shake (or other minor features/fixes) that don't work in the
|
||||
default protocol 33. See `protocol_version` in `config.yaml`. Just remember
|
||||
that you will be cutting off support for older clients if you use 35.
|
||||
- Fixed a bug with screen-messages animating off screen too fast when frame
|
||||
rates are high.
|
||||
|
||||
|
||||
### 1.7.27 (build 21282, api 8, 2023-08-30)
|
||||
|
||||
|
||||
9
Makefile
9
Makefile
@ -779,13 +779,12 @@ check-full: py_check_prereqs
|
||||
|
||||
# Same as 'check' plus optional/slow extra checks.
|
||||
check2: py_check_prereqs
|
||||
@$(DMAKE) -j$(CPUS) update-check cpplint pylint mypy pycharm
|
||||
@$(DMAKE) -j$(CPUS) update-check cpplint pylint mypy
|
||||
@$(PCOMMANDBATCH) echo SGRN BLD ALL CHECKS PASSED!
|
||||
|
||||
# Same as check2 but no caching (all files are checked).
|
||||
check2-full: py_check_prereqs
|
||||
@$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full \
|
||||
pycharm-full
|
||||
@$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full
|
||||
@$(PCOMMANDBATCH) echo SGRN BLD ALL CHECKS PASSED!
|
||||
|
||||
# Run Cpplint checks on all C/C++ code.
|
||||
@ -924,14 +923,14 @@ preflight-full:
|
||||
preflight2:
|
||||
@$(MAKE) format
|
||||
@$(MAKE) update
|
||||
@$(MAKE) -j$(CPUS) cpplint pylint mypy pycharm test
|
||||
@$(MAKE) -j$(CPUS) cpplint pylint mypy test
|
||||
@$(PCOMMANDBATCH) echo SGRN BLD PREFLIGHT SUCCESSFUL!
|
||||
|
||||
# Same as 'preflight2' but without caching (all files visited).
|
||||
preflight2-full:
|
||||
@$(MAKE) format-full
|
||||
@$(MAKE) update
|
||||
@$(MAKE) -j$(CPUS) cpplint-full pylint-full mypy-full pycharm-full test-full
|
||||
@$(MAKE) -j$(CPUS) cpplint-full pylint-full mypy-full test-full
|
||||
@$(PCOMMANDBATCH) echo SGRN BLD PREFLIGHT SUCCESSFUL!
|
||||
|
||||
# Tell make which of these targets don't represent files.
|
||||
|
||||
@ -53,47 +53,58 @@ ctx.src_omit_paths = {
|
||||
'src/assets/workspace',
|
||||
}
|
||||
|
||||
# Use this to 'carve out' directories or exact file paths which will be
|
||||
# git-managed on dst. By default, spinoff will consider dirs containing
|
||||
# the files it syncs from src as 'spinoff-managed'; it will set them as
|
||||
# git-ignored and will complain if any files appear in them that it does
|
||||
# not manage itself (to prevent accidentally doing work in such places).
|
||||
# Note that adding a dir to src_write_paths does not prevent files
|
||||
# within it from being synced by spinoff; it just means that each of
|
||||
# those individual spinoff-managed files will have their own gitignore
|
||||
# entry since there is no longer one covering the whole dir. So to keep
|
||||
# things tidy, carve out the minimal set of exact file/dir paths that
|
||||
# you need.
|
||||
# Use this to 'carve out' files or directories which will be git-managed
|
||||
# on dst.
|
||||
#
|
||||
# By default, spinoff will consider dirs containing the files it syncs
|
||||
# from src as 'spinoff-managed'; it will set them as git-ignored and
|
||||
# will complain if any files appear in them that it does not manage
|
||||
# itself (to prevent accidentally doing work in such places). Note that
|
||||
# adding a dir to src_write_paths does not prevent files within it from
|
||||
# being synced by spinoff; it just means that each of those individual
|
||||
# spinoff-managed files will have their own gitignore entry since there
|
||||
# can't be a single one covering the whole dir. So to keep things tidy,
|
||||
# carve out the minimal set of exact file/dir paths that you need.
|
||||
ctx.src_write_paths = {
|
||||
'tools/spinoff',
|
||||
'config/spinoffconfig.py',
|
||||
}
|
||||
|
||||
# Normally spinoff errors if it finds any files in its managed dirs that
|
||||
# it did not put there. This is to prevent accidentally working in these
|
||||
# parts of a dst project; since these sections are git-ignored, git
|
||||
# itself won't raise any warnings in such cases and it would be easy to
|
||||
# accidentally lose work otherwise.
|
||||
# Use this to 'carve out' files or directories under spinoff managed
|
||||
# dirs which will be completely ignored by spinoff (but *not* placed
|
||||
# under git control).
|
||||
#
|
||||
# This list can be used to suppress spinoff's errors for specific
|
||||
# locations. This is generally used to allow build output or other
|
||||
# dynamically generated files to exist within spinoff-managed
|
||||
# directories. It is possible to use src_write_paths for such purposes,
|
||||
# but this has the side-effect of greatly complicating the dst project's
|
||||
# gitignore list; selectively marking a few dirs as unchecked makes for
|
||||
# a cleaner setup. Just be careful to not set excessively broad regions
|
||||
# as unchecked; you don't want to mask actual useful error messages.
|
||||
# Normally spinoff will error if it finds any files under its managed
|
||||
# dirs that it did not put there. This is to prevent accidentally
|
||||
# working in these parts of a dst project; since spinoff-controlled
|
||||
# stuff is git-ignored, git itself won't raise any warnings in such
|
||||
# cases and it would be easy to accidentally blow away changes if
|
||||
# spinoff didn't raise a stink.
|
||||
#
|
||||
# This list is used to suppress raising of said stink for specific
|
||||
# locations. This allows build output or other dynamically generated
|
||||
# files to exist under spinoff-managed directories. It is also possible
|
||||
# to use src_write_paths for such carve-outs, but that can have the
|
||||
# negative side-effect of greatly complicating the dst project's
|
||||
# .gitignore file. Selectively marking a few specific files or dirs as
|
||||
# unchecked instead can keep things tidier and more understandable.
|
||||
#
|
||||
# Note that files and paths marked as unchecked cannot be the
|
||||
# destination for synced files, as that would be ambiguous (We can
|
||||
# either sync the file ourself or expect someone else to write it, but
|
||||
# not both).
|
||||
ctx.src_unchecked_paths = {
|
||||
'src/ballistica/mgen',
|
||||
'src/ballistica/*/mgen',
|
||||
'src/assets/ba_data/python/*/_mgen',
|
||||
'src/meta/*/mgen',
|
||||
'ballisticakit-cmake/.clang-format',
|
||||
'ballisticakit-android/BallisticaKit/src/cardboard/res',
|
||||
'ballisticakit-windows/*/BallisticaKit.ico',
|
||||
'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets',
|
||||
'ballisticakit-android/BallisticaKit/src/*/res',
|
||||
'ballisticakit-android/BallisticaKit/src/*/assets',
|
||||
'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*.png',
|
||||
'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*/*.png',
|
||||
'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*/*/*/*.png',
|
||||
'ballisticakit-android/BallisticaKit/src/*/res/*/*.png',
|
||||
'ballisticakit-android/BallisticaKit/src/*/assets/ballistica_files',
|
||||
'ballisticakit-android/local.properties',
|
||||
'ballisticakit-android/.gradle',
|
||||
'ballisticakit-android/build',
|
||||
|
||||
@ -56,6 +56,8 @@ class App:
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
# A few things defined as non-optional values but not actually
|
||||
# available until the app starts.
|
||||
plugins: PluginSubsystem
|
||||
lang: LanguageSubsystem
|
||||
health_monitor: AppHealthMonitor
|
||||
@ -92,7 +94,7 @@ class App:
|
||||
|
||||
# Used on platforms such as mobile where the app basically needs
|
||||
# to shut down while backgrounded. In this state, all event
|
||||
# loops are suspended and all graphics and audio should cease
|
||||
# loops are suspended and all graphics and audio must cease
|
||||
# completely. Be aware that the suspended state can be entered
|
||||
# from any other state including NATIVE_BOOTSTRAPPING and
|
||||
# SHUTTING_DOWN.
|
||||
@ -149,9 +151,9 @@ class App:
|
||||
def __init__(self) -> None:
|
||||
"""(internal)
|
||||
|
||||
Do not instantiate this class; access the single shared instance
|
||||
of it as 'app' which is available in various Ballistica
|
||||
feature-set modules such as babase.
|
||||
Do not instantiate this class. You can access the single shared
|
||||
instance of it through various high level packages: 'babase.app',
|
||||
'bascenev1.app', 'bauiv1.app', etc.
|
||||
"""
|
||||
|
||||
# Hack for docs-generation: we can be imported with dummy modules
|
||||
@ -508,7 +510,7 @@ class App:
|
||||
except Exception:
|
||||
logging.exception('Error setting app intent to %s.', intent)
|
||||
_babase.pushcall(
|
||||
tpartial(self._apply_intent_error, intent),
|
||||
tpartial(self._display_set_intent_error, intent),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
||||
@ -553,10 +555,11 @@ class App:
|
||||
'Error handling intent %s in app-mode %s.', intent, mode
|
||||
)
|
||||
|
||||
def _apply_intent_error(self, intent: AppIntent) -> None:
|
||||
def _display_set_intent_error(self, intent: AppIntent) -> None:
|
||||
"""Show the *user* something went wrong setting an intent."""
|
||||
from babase._language import Lstr
|
||||
|
||||
del intent # Unused.
|
||||
del intent
|
||||
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
_babase.getsimplesound('error').play()
|
||||
|
||||
|
||||
@ -302,6 +302,11 @@ class AccountV1Subsystem:
|
||||
"""(internal)"""
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
import logging
|
||||
|
||||
logging.warning(
|
||||
'Error adding pending promo code; plus not present.'
|
||||
)
|
||||
babase.screenmessage(
|
||||
babase.Lstr(resource='errorText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
@ -52,7 +52,7 @@ if TYPE_CHECKING:
|
||||
|
||||
# Build number and version of the ballistica binary we expect to be
|
||||
# using.
|
||||
TARGET_BALLISTICA_BUILD = 21447
|
||||
TARGET_BALLISTICA_BUILD = 21453
|
||||
TARGET_BALLISTICA_VERSION = '1.7.28'
|
||||
|
||||
|
||||
|
||||
@ -1184,6 +1184,9 @@ class AccountSettingsWindow(bui.Window):
|
||||
self, response: bacommon.cloud.ManageAccountResponse | Exception
|
||||
) -> None:
|
||||
if isinstance(response, Exception) or response.url is None:
|
||||
logging.warning(
|
||||
'Got error in manage-account-response: %s.', response
|
||||
)
|
||||
bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
bui.getsound('error').play()
|
||||
return
|
||||
@ -1466,6 +1469,7 @@ class AccountSettingsWindow(bui.Window):
|
||||
if isinstance(result, Exception):
|
||||
# For now just make a bit of noise if anything went wrong;
|
||||
# can get more specific as needed later.
|
||||
logging.warning('Got error in v2 sign-in result: %s.', result)
|
||||
bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
bui.getsound('error').play()
|
||||
else:
|
||||
|
||||
@ -15,8 +15,8 @@ from threading import Lock, Thread, current_thread
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# We make use of the bacommon and efro packages as well as site-packages
|
||||
# included with our bundled Ballistica dist, so we need to add those paths
|
||||
# before we import them.
|
||||
# included with our bundled Ballistica dist, so we need to add those
|
||||
# paths before we import them.
|
||||
sys.path += [
|
||||
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')),
|
||||
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')),
|
||||
@ -34,31 +34,55 @@ if TYPE_CHECKING:
|
||||
VERSION_STR = '1.3.1'
|
||||
|
||||
# Version history:
|
||||
#
|
||||
# 1.3.1
|
||||
# Windows binary is now named BallisticaKitHeadless.exe
|
||||
#
|
||||
# - Windows binary is now named 'BallisticaKitHeadless.exe'.
|
||||
#
|
||||
# 1.3:
|
||||
# Added show_tutorial config option
|
||||
# Added team_names config option
|
||||
# Added team_colors config option
|
||||
# Added playlist_inline config option
|
||||
#
|
||||
# - Added show_tutorial config option.
|
||||
#
|
||||
# - Added team_names config option.
|
||||
#
|
||||
# - Added team_colors config option.
|
||||
#
|
||||
# - Added playlist_inline config option.
|
||||
#
|
||||
# 1.2:
|
||||
# Added optional --help arg
|
||||
# Added --config arg for specifying config file and --root for ba_root path
|
||||
# Added noninteractive mode and --interactive/--noninteractive args to
|
||||
# explicitly enable/disable it (it is autodetected by default)
|
||||
# Added explicit control for auto-restart: --no-auto-restart
|
||||
# Config file is now reloaded each time server binary is restarted; no more
|
||||
# need to bring down server wrapper to pick up changes
|
||||
# Now automatically restarts server binary when config file is modified
|
||||
# (use --no-config-auto-restart to disable that behavior)
|
||||
#
|
||||
# - Added optional --help arg.
|
||||
#
|
||||
# - Added --config arg for specifying config file and --root for
|
||||
# ba_root path.
|
||||
#
|
||||
# - Added noninteractive mode and --interactive/--noninteractive args
|
||||
# to explicitly enable/disable it (it is autodetected by default).
|
||||
#
|
||||
# - Added explicit control for auto-restart: --no-auto-restart.
|
||||
#
|
||||
# - Config file is now reloaded each time server binary is restarted;
|
||||
# no more need to bring down server wrapper to pick up changes.
|
||||
#
|
||||
# - Now automatically restarts server binary when config file is
|
||||
# modified (use --no-config-auto-restart to disable that behavior).
|
||||
#
|
||||
# 1.1.1:
|
||||
# Switched config reading to use efro.dataclasses.dataclass_from_dict()
|
||||
#
|
||||
# - Switched config reading to use
|
||||
# efro.dataclasses.dataclass_from_dict().
|
||||
#
|
||||
# 1.1.0:
|
||||
# Added shutdown command
|
||||
# Changed restart to default to immediate=True
|
||||
# Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes
|
||||
#
|
||||
# - Added shutdown command.
|
||||
#
|
||||
# - Changed restart to default to immediate=True.
|
||||
#
|
||||
# - Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes.
|
||||
#
|
||||
# 1.0.0:
|
||||
# Initial release
|
||||
#
|
||||
# - Initial release.
|
||||
|
||||
|
||||
class ServerManagerApp:
|
||||
@ -101,8 +125,9 @@ class ServerManagerApp:
|
||||
# This may override the above defaults.
|
||||
self._parse_command_line_args()
|
||||
|
||||
# Do an initial config-load. If the config is invalid at this point
|
||||
# we can cleanly die (we're more lenient later on reloads).
|
||||
# Do an initial config-load. If the config is invalid at this
|
||||
# point we can cleanly die (we're more lenient later on
|
||||
# reloads).
|
||||
self.load_config(strict=True, print_confirmation=False)
|
||||
|
||||
@property
|
||||
@ -131,9 +156,9 @@ class ServerManagerApp:
|
||||
)
|
||||
|
||||
# Python will handle SIGINT for us (as KeyboardInterrupt) but we
|
||||
# need to register a SIGTERM handler so we have a chance to clean
|
||||
# up our subprocess when someone tells us to die. (and avoid
|
||||
# zombie processes)
|
||||
# need to register a SIGTERM handler so we have a chance to
|
||||
# clean up our subprocess when someone tells us to die. (and
|
||||
# avoid zombie processes)
|
||||
signal.signal(signal.SIGTERM, self._handle_term_signal)
|
||||
|
||||
# During a run, we make the assumption that cwd is the dir
|
||||
@ -155,7 +180,8 @@ class ServerManagerApp:
|
||||
f'{Clr.CYN}Waiting for subprocess exit...{Clr.RST}', flush=True
|
||||
)
|
||||
|
||||
# Mark ourselves as shutting down and wait for the process to wrap up.
|
||||
# Mark ourselves as shutting down and wait for the process to
|
||||
# wrap up.
|
||||
self._done = True
|
||||
self._subprocess_thread.join()
|
||||
|
||||
@ -181,9 +207,10 @@ class ServerManagerApp:
|
||||
# Gracefully bow out if we kill ourself via keyboard.
|
||||
pass
|
||||
except SystemExit:
|
||||
# We get this from the builtin quit(), our signal handler, etc.
|
||||
# Need to catch this so we can clean up, otherwise we'll be
|
||||
# left in limbo with our process thread still running.
|
||||
# We get this from the builtin quit(), our signal handler,
|
||||
# etc. Need to catch this so we can clean up, otherwise
|
||||
# we'll be left in limbo with our process thread still
|
||||
# running.
|
||||
pass
|
||||
self._postrun()
|
||||
|
||||
@ -207,14 +234,17 @@ class ServerManagerApp:
|
||||
self._enable_tab_completion(context)
|
||||
|
||||
# Now just sit in an interpreter.
|
||||
# TODO: make it possible to use IPython if the user has it available.
|
||||
#
|
||||
# TODO: make it possible to use IPython if the user has it
|
||||
# available.
|
||||
try:
|
||||
self._interpreter_start_time = time.time()
|
||||
code.interact(local=context, banner='', exitmsg='')
|
||||
except SystemExit:
|
||||
# We get this from the builtin quit(), our signal handler, etc.
|
||||
# Need to catch this so we can clean up, otherwise we'll be
|
||||
# left in limbo with our process thread still running.
|
||||
# We get this from the builtin quit(), our signal handler,
|
||||
# etc. Need to catch this so we can clean up, otherwise
|
||||
# we'll be left in limbo with our process thread still
|
||||
# running.
|
||||
pass
|
||||
except BaseException as exc:
|
||||
print(
|
||||
@ -238,19 +268,21 @@ class ServerManagerApp:
|
||||
self._block_for_command_completion()
|
||||
|
||||
def _block_for_command_completion(self) -> None:
|
||||
# Ideally we'd block here until the command was run so our prompt would
|
||||
# print after it's results. We currently don't get any response from
|
||||
# the app so the best we can do is block until our bg thread has sent
|
||||
# it. In the future we can perhaps add a proper 'command port'
|
||||
# interface for proper blocking two way communication.
|
||||
# Ideally we'd block here until the command was run so our
|
||||
# prompt would print after it's results. We currently don't get
|
||||
# any response from the app so the best we can do is block until
|
||||
# our bg thread has sent it. In the future we can perhaps add a
|
||||
# proper 'command port' interface for proper blocking two way
|
||||
# communication.
|
||||
while True:
|
||||
with self._subprocess_commands_lock:
|
||||
if not self._subprocess_commands:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
# One last short delay so if we come out *just* as the command is sent
|
||||
# we'll hopefully still give it enough time to process/print.
|
||||
# One last short delay so if we come out *just* as the command
|
||||
# is sent we'll hopefully still give it enough time to
|
||||
# process/print.
|
||||
time.sleep(0.1)
|
||||
|
||||
def screenmessage(
|
||||
@ -320,8 +352,8 @@ class ServerManagerApp:
|
||||
)
|
||||
)
|
||||
|
||||
# If we're asking for an immediate restart but don't get one within
|
||||
# the grace period, bring down the hammer.
|
||||
# If we're asking for an immediate restart but don't get one
|
||||
# within the grace period, bring down the hammer.
|
||||
if immediate:
|
||||
self._subprocess_force_kill_time = (
|
||||
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
|
||||
@ -340,12 +372,12 @@ class ServerManagerApp:
|
||||
ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate)
|
||||
)
|
||||
|
||||
# An explicit shutdown means we know to bail completely once this
|
||||
# subprocess completes.
|
||||
# An explicit shutdown means we know to bail completely once
|
||||
# this subprocess completes.
|
||||
self._wrapper_shutdown_desired = True
|
||||
|
||||
# If we're asking for an immediate shutdown but don't get one within
|
||||
# the grace period, bring down the hammer.
|
||||
# If we're asking for an immediate shutdown but don't get one
|
||||
# within the grace period, bring down the hammer.
|
||||
if immediate:
|
||||
self._subprocess_force_kill_time = (
|
||||
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
|
||||
@ -378,9 +410,10 @@ class ServerManagerApp:
|
||||
if i + 1 >= argc:
|
||||
raise CleanError('Expected a path as next arg.')
|
||||
path = sys.argv[i + 1]
|
||||
# Unlike config_path, this one doesn't have to exist now.
|
||||
# We do however need an abs path because we may be in a
|
||||
# different cwd currently than we will be during the run.
|
||||
# Unlike config_path, this one doesn't have to exist
|
||||
# now. We do however need an abs path because we may be
|
||||
# in a different cwd currently than we will be during
|
||||
# the run.
|
||||
self._ba_root_path = os.path.abspath(path)
|
||||
i += 2
|
||||
elif arg == '--interactive':
|
||||
@ -538,6 +571,7 @@ class ServerManagerApp:
|
||||
|
||||
if not os.path.exists(self._config_path):
|
||||
# Special case:
|
||||
#
|
||||
# If the user didn't specify a particular config file, allow
|
||||
# gracefully falling back to defaults if the default one is
|
||||
# missing.
|
||||
@ -606,24 +640,26 @@ class ServerManagerApp:
|
||||
"""Spin up the server subprocess and run it until exit."""
|
||||
# pylint: disable=consider-using-with
|
||||
|
||||
# Reload our config, and update our overall behavior based on it.
|
||||
# We do non-strict this time to give the user repeated attempts if
|
||||
# if they mess up while modifying the config on the fly.
|
||||
# Reload our config, and update our overall behavior based on
|
||||
# it. We do non-strict this time to give the user repeated
|
||||
# attempts if if they mess up while modifying the config on the
|
||||
# fly.
|
||||
self.load_config(strict=False, print_confirmation=True)
|
||||
|
||||
self._prep_subprocess_environment()
|
||||
|
||||
# Launch the binary and grab its stdin;
|
||||
# we'll use this to feed it commands.
|
||||
# Launch the binary and grab its stdin; we'll use this to feed
|
||||
# it commands.
|
||||
self._subprocess_launch_time = time.time()
|
||||
|
||||
# Set an environment var so the server process knows its being
|
||||
# run under us. This causes it to ignore ctrl-c presses and other
|
||||
# slight behavior tweaks. Hmm; should this be an argument instead?
|
||||
# run under us. This causes it to ignore ctrl-c presses and
|
||||
# other slight behavior tweaks. Hmm; should this be an argument
|
||||
# instead?
|
||||
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
|
||||
|
||||
# Set an environment var to change the device name.
|
||||
# Device name is used while making connection with master server,
|
||||
# Set an environment var to change the device name. Device name
|
||||
# is used while making connection with master server,
|
||||
# cloud-console recognize us with this name.
|
||||
os.environ['BA_DEVICE_NAME'] = self._config.party_name
|
||||
|
||||
@ -663,9 +699,10 @@ class ServerManagerApp:
|
||||
|
||||
assert self._subprocess_exited_cleanly is not None
|
||||
|
||||
# EW: it seems that if we die before the main thread has fully started
|
||||
# up the interpreter, its possible that it will not break out of its
|
||||
# loop via the usual SystemExit that gets sent when we die.
|
||||
# EW: it seems that if we die before the main thread has fully
|
||||
# started up the interpreter, its possible that it will not
|
||||
# break out of its loop via the usual SystemExit that gets sent
|
||||
# when we die.
|
||||
if self._interactive:
|
||||
while (
|
||||
self._interpreter_start_time is None
|
||||
@ -694,8 +731,8 @@ class ServerManagerApp:
|
||||
# tell the main thread to die.
|
||||
if self._wrapper_shutdown_desired:
|
||||
# Only do this if the main thread is not already waiting for
|
||||
# us to die; otherwise it can lead to deadlock.
|
||||
# (we hang in os.kill while main thread is blocked in Thread.join)
|
||||
# us to die; otherwise it can lead to deadlock. (we hang in
|
||||
# os.kill while main thread is blocked in Thread.join)
|
||||
if not self._done:
|
||||
self._done = True
|
||||
|
||||
@ -721,6 +758,8 @@ class ServerManagerApp:
|
||||
bincfg['Auto Balance Teams'] = self._config.auto_balance_teams
|
||||
bincfg['Show Tutorial'] = self._config.show_tutorial
|
||||
|
||||
if self._config.protocol_version is not None:
|
||||
bincfg['SceneV1 Host Protocol'] = self._config.protocol_version
|
||||
if self._config.team_names is not None:
|
||||
bincfg['Custom Team Names'] = self._config.team_names
|
||||
elif 'Custom Team Names' in bincfg:
|
||||
@ -769,8 +808,8 @@ class ServerManagerApp:
|
||||
assert current_thread() is self._subprocess_thread
|
||||
assert self._subprocess.stdin is not None
|
||||
|
||||
# Send the initial server config which should kick things off.
|
||||
# (but make sure its values are still valid first)
|
||||
# Send the initial server config which should kick things off
|
||||
# (but make sure its values are still valid first).
|
||||
dataclass_validate(self._config)
|
||||
self._send_server_command(StartServerModeCommand(self._config))
|
||||
|
||||
@ -782,8 +821,8 @@ class ServerManagerApp:
|
||||
# Pass along any commands to our process.
|
||||
with self._subprocess_commands_lock:
|
||||
for incmd in self._subprocess_commands:
|
||||
# If we're passing a raw string to exec, no need to wrap it
|
||||
# in any proper structure.
|
||||
# If we're passing a raw string to exec, no need to
|
||||
# wrap it in any proper structure.
|
||||
if isinstance(incmd, str):
|
||||
self._subprocess.stdin.write((incmd + '\n').encode())
|
||||
self._subprocess.stdin.flush()
|
||||
@ -794,9 +833,9 @@ class ServerManagerApp:
|
||||
# Request restarts/shut-downs for various reasons.
|
||||
self._request_shutdowns_or_restarts()
|
||||
|
||||
# If they want to force-kill our subprocess, simply exit this
|
||||
# loop; the cleanup code will kill the process if its still
|
||||
# alive.
|
||||
# If they want to force-kill our subprocess, simply exit
|
||||
# this loop; the cleanup code will kill the process if its
|
||||
# still alive.
|
||||
if (
|
||||
self._subprocess_force_kill_time is not None
|
||||
and time.time() > self._subprocess_force_kill_time
|
||||
@ -855,8 +894,8 @@ class ServerManagerApp:
|
||||
self.restart(immediate=True)
|
||||
self._subprocess_sent_config_auto_restart = True
|
||||
|
||||
# Attempt clean exit if our clean-exit-time passes.
|
||||
# (and enforce a 6 hour max if not provided)
|
||||
# Attempt clean exit if our clean-exit-time passes (and enforce
|
||||
# a 6 hour max if not provided).
|
||||
clean_exit_minutes = 360.0
|
||||
if self._config.clean_exit_minutes is not None:
|
||||
clean_exit_minutes = min(
|
||||
@ -881,8 +920,8 @@ class ServerManagerApp:
|
||||
self.shutdown(immediate=False)
|
||||
self._subprocess_sent_clean_exit = True
|
||||
|
||||
# Attempt unclean exit if our unclean-exit-time passes.
|
||||
# (and enforce a 7 hour max if not provided)
|
||||
# Attempt unclean exit if our unclean-exit-time passes (and
|
||||
# enforce a 7 hour max if not provided).
|
||||
unclean_exit_minutes = 420.0
|
||||
if self._config.unclean_exit_minutes is not None:
|
||||
unclean_exit_minutes = min(
|
||||
@ -924,8 +963,8 @@ class ServerManagerApp:
|
||||
|
||||
print(f'{Clr.CYN}Stopping subprocess...{Clr.RST}', flush=True)
|
||||
|
||||
# First, ask it nicely to die and give it a moment.
|
||||
# If that doesn't work, bring down the hammer.
|
||||
# First, ask it nicely to die and give it a moment. If that
|
||||
# doesn't work, bring down the hammer.
|
||||
self._subprocess.terminate()
|
||||
try:
|
||||
self._subprocess.wait(timeout=10)
|
||||
@ -941,8 +980,9 @@ def main() -> None:
|
||||
try:
|
||||
ServerManagerApp().run()
|
||||
except CleanError as exc:
|
||||
# For clean errors, do a simple print and fail; no tracebacks/etc.
|
||||
# Any others will bubble up and give us the usual mess.
|
||||
# For clean errors, do a simple print and fail; no
|
||||
# tracebacks/etc. Any others will bubble up and give us the
|
||||
# usual mess.
|
||||
exc.pretty_print()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@ -59,6 +59,7 @@ void AppAdapterSDL::OnMainThreadStartApp() {
|
||||
"AppAdapterSDL strict_graphics_context_ is enabled."
|
||||
" Remember to turn this off.");
|
||||
}
|
||||
|
||||
// We may or may not want xinput on windows.
|
||||
if (g_buildconfig.ostype_windows()) {
|
||||
if (!g_core->platform->GetLowLevelConfigValue("enablexinput", 1)) {
|
||||
@ -66,6 +67,9 @@ void AppAdapterSDL::OnMainThreadStartApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// We wrangle our own signal handling; don't bring SDL into it.
|
||||
SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1");
|
||||
|
||||
int result = SDL_Init(sdl_flags);
|
||||
if (result < 0) {
|
||||
FatalError(std::string("SDL_Init failed: ") + SDL_GetError());
|
||||
@ -96,7 +100,7 @@ void AppAdapterSDL::OnMainThreadStartApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// We currently use a software cursor, so hide the system one.
|
||||
// This adapter draws a software cursor; hide the actual OS one.
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include "ballistica/base/app_mode/app_mode.h"
|
||||
|
||||
#include "ballistica/base/input/device/input_device_delegate.h"
|
||||
#include "ballistica/base/logic/logic.h"
|
||||
#include "ballistica/base/support/context.h"
|
||||
|
||||
namespace ballistica::base {
|
||||
@ -43,8 +44,8 @@ void AppMode::ChangeGameSpeed(int offs) {}
|
||||
|
||||
void AppMode::StepDisplayTime() {}
|
||||
|
||||
auto AppMode::GetHeadlessDisplayStep() -> microsecs_t {
|
||||
return kAppModeMaxHeadlessDisplayStep;
|
||||
auto AppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t {
|
||||
return kHeadlessMaxDisplayTimeStep;
|
||||
}
|
||||
|
||||
auto AppMode::GetPartySize() const -> int { return 0; }
|
||||
|
||||
@ -9,15 +9,6 @@
|
||||
|
||||
namespace ballistica::base {
|
||||
|
||||
/// The max amount of time a headless app can sleep if no events are pending.
|
||||
/// This should not be *too* high or it might cause delays when going from
|
||||
/// no events present to events present.
|
||||
const microsecs_t kAppModeMaxHeadlessDisplayStep{500000};
|
||||
|
||||
/// The min amount of time a headless app can sleep. This provides an upper
|
||||
/// limit on stepping overhead in cases where events are densely packed.
|
||||
const microsecs_t kAppModeMinHeadlessDisplayStep{1000};
|
||||
|
||||
/// Represents 'what the app is doing'. The global app-mode can be switched
|
||||
/// as the app is running. The Python layer has its own Python AppMode
|
||||
/// classes, and generally when one of them becomes active it calls down
|
||||
@ -51,9 +42,9 @@ class AppMode {
|
||||
/// Called right after stepping; should return the exact microseconds
|
||||
/// between the current display time and the next event the app-mode has
|
||||
/// scheduled. If no events are pending, should return
|
||||
/// kAppModeMaxHeadlessDisplayStep. This will only be called on headless
|
||||
/// kHeadlessMaxDisplayTimeStep. This will only be called on headless
|
||||
/// builds.
|
||||
virtual auto GetHeadlessDisplayStep() -> microsecs_t;
|
||||
virtual auto GetHeadlessNextDisplayTimeStep() -> microsecs_t;
|
||||
|
||||
/// Create a delegate for an input-device.
|
||||
/// Return a raw pointer allocated using Object::NewDeferred.
|
||||
|
||||
@ -197,9 +197,9 @@ void BaseFeatureSet::StartApp() {
|
||||
assets_server->OnMainThreadStartApp();
|
||||
app_adapter->OnMainThreadStartApp();
|
||||
|
||||
// Take note that we're now 'running'. Various code such as anything that
|
||||
// pushes messages to threads can watch for this state to avoid crashing
|
||||
// if called early.
|
||||
// Ok; we're now official 'started'. Various code such as anything that
|
||||
// pushes messages to threads can watch for this state (via IsAppStarted()
|
||||
// to avoid crashing if called early.
|
||||
app_started_ = true;
|
||||
|
||||
// Inform anyone who wants to know that we're done starting.
|
||||
|
||||
@ -603,14 +603,13 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
|
||||
/// Start app systems in motion.
|
||||
void StartApp() override;
|
||||
|
||||
/// Issue a high level app quit request. Can be called from any thread.
|
||||
/// 'soft' means the app can simply reset/hide itself instead of actually
|
||||
/// exiting the process (common behavior on mobile platforms). 'back'
|
||||
/// means that a soft-quit should behave as if a back-button was pressed,
|
||||
/// which may trigger different behavior in the OS than a standard soft
|
||||
/// quit. If 'confirm' is true, a confirmation dialog will be presented if
|
||||
/// the current app-mode provides one and the app is in gui mode.
|
||||
/// Otherwise the quit will be immediate.
|
||||
/// Issue a high level app quit request. Can be called from any thread and
|
||||
/// can be safely called repeatedly. If 'confirm' is true, a confirmation
|
||||
/// dialog will be presented if the environment and situation allows;
|
||||
/// otherwise the quit will be immediate. A QuitType arg can optionally be
|
||||
/// passed to influence quit behavior; on some platforms such as mobile
|
||||
/// the default is for the app to recede to the background but physically
|
||||
/// remain running.
|
||||
void QuitApp(bool confirm = false, QuitType quit_type = QuitType::kSoft);
|
||||
|
||||
/// Called when app shutdown process completes. Sets app to exit.
|
||||
|
||||
@ -336,6 +336,7 @@ class Graphics::ScreenMessageEntry {
|
||||
float v_smoothed{};
|
||||
bool translation_dirty{true};
|
||||
bool mesh_dirty{true};
|
||||
millisecs_t smooth_time{};
|
||||
|
||||
private:
|
||||
Object::Ref<TextGroup> s_mesh_;
|
||||
@ -594,13 +595,21 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
|
||||
{
|
||||
auto xf = c.ScopedTransform();
|
||||
|
||||
if (i->v_smoothed == 0.0f) {
|
||||
i->v_smoothed = v + v_extra;
|
||||
} else {
|
||||
float smoothing = 0.8f;
|
||||
i->v_smoothed = smoothing * i->v_smoothed
|
||||
+ (1.0f - smoothing) * (v + v_extra);
|
||||
// This logic needs to run at a fixed hz or it breaks on high frame
|
||||
// rates.
|
||||
auto now_millisecs = pass->frame_def()->display_time_millisecs();
|
||||
i->smooth_time = std::max(i->smooth_time, now_millisecs - 100);
|
||||
while (i->smooth_time < now_millisecs) {
|
||||
i->smooth_time += 1000 / 60;
|
||||
if (i->v_smoothed == 0.0f) {
|
||||
i->v_smoothed = v + v_extra;
|
||||
} else {
|
||||
float smoothing = 0.8f;
|
||||
i->v_smoothed = smoothing * i->v_smoothed
|
||||
+ (1.0f - smoothing) * (v + v_extra);
|
||||
}
|
||||
}
|
||||
|
||||
c.Translate(screen_width * 0.5f, i->v_smoothed,
|
||||
vr ? 60 : kScreenMessageZDepth);
|
||||
if (vr) {
|
||||
@ -767,10 +776,17 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
|
||||
a = 1;
|
||||
}
|
||||
|
||||
i->v_smoothed += 0.1f;
|
||||
if (i->v_smoothed - last_v < min_spacing) {
|
||||
i->v_smoothed +=
|
||||
8.0f * (1.0f - ((i->v_smoothed - last_v) / min_spacing));
|
||||
// This logic needs to run at a fixed hz or it breaks on high frame
|
||||
// rates.
|
||||
auto now_millisecs = pass->frame_def()->display_time_millisecs();
|
||||
i->smooth_time = std::max(i->smooth_time, now_millisecs - 100);
|
||||
while (i->smooth_time < now_millisecs) {
|
||||
i->smooth_time += 1000 / 60;
|
||||
i->v_smoothed += 0.1f;
|
||||
if (i->v_smoothed - last_v < min_spacing) {
|
||||
i->v_smoothed +=
|
||||
8.0f * (1.0f - ((i->v_smoothed - last_v) / min_spacing));
|
||||
}
|
||||
}
|
||||
last_v = i->v_smoothed;
|
||||
|
||||
@ -864,11 +880,11 @@ void Graphics::GetSafeColor(float* red, float* green, float* blue,
|
||||
*blue = std::min(1.0f, (*blue) * s);
|
||||
}
|
||||
|
||||
// We may still be short of our target intensity due to clamping (ie: (10,0,0)
|
||||
// will not look any brighter than (1,0,0)) if that's the case, just convert
|
||||
// the difference to a grey value and add that to all channels... this *still*
|
||||
// might not get us there so lets do it a few times if need be. (i'm sure
|
||||
// there's a less bone-headed way to do this)
|
||||
// We may still be short of our target intensity due to clamping (ie:
|
||||
// (10,0,0) will not look any brighter than (1,0,0)) if that's the case,
|
||||
// just convert the difference to a grey value and add that to all
|
||||
// channels... this *still* might not get us there so lets do it a few times
|
||||
// if need be. (i'm sure there's a less bone-headed way to do this)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float remaining =
|
||||
(0.2989f * (*red) + 0.5870f * (*green) + 0.1140f * (*blue)) - 1.0f;
|
||||
@ -1050,14 +1066,13 @@ void Graphics::UpdateGyro(microsecs_t time_microsecs,
|
||||
tilt_vel_ = tilt_smoothed_ * 3.0f;
|
||||
tilt_pos_ += tilt_vel_ * timescale;
|
||||
|
||||
// Technically this will behave slightly differently at different time scales,
|
||||
// but it should be close to correct..
|
||||
// tilt_pos_ *= 0.991f;
|
||||
// Technically this will behave slightly differently at different time
|
||||
// scales, but it should be close to correct.. tilt_pos_ *= 0.991f;
|
||||
tilt_pos_ *= std::max(0.0f, 1.0f - 0.01f * timescale);
|
||||
|
||||
// Some gyros seem wonky and either give us crazy big values or consistently
|
||||
// offset ones. Let's keep a running tally of magnitude that slowly drops over
|
||||
// time, and if it reaches a certain value lets just kill gyro input.
|
||||
// offset ones. Let's keep a running tally of magnitude that slowly drops
|
||||
// over time, and if it reaches a certain value lets just kill gyro input.
|
||||
if (gyro_broken_) {
|
||||
tilt_pos_ *= 0.0f;
|
||||
} else {
|
||||
@ -1485,9 +1500,9 @@ void Graphics::DrawCursor(FrameDef* frame_def) {
|
||||
{
|
||||
auto xf = c.ScopedTransform();
|
||||
|
||||
// Note: we don't plug in known cursor position values here; we tell the
|
||||
// renderer to insert the latest values on its end; this can lessen
|
||||
// cursor lag substantially.
|
||||
// Note: we don't plug in known cursor position values here; we tell
|
||||
// the renderer to insert the latest values on its end; this can
|
||||
// lessen cursor lag substantially.
|
||||
c.CursorTranslate();
|
||||
c.Translate(csize * 0.40f, csize * -0.38f, kCursorZDepth);
|
||||
c.Scale(csize, csize);
|
||||
|
||||
@ -99,7 +99,7 @@ void Logic::OnGraphicsReady() {
|
||||
// Anyone dealing in display-time should be able to handle a wide
|
||||
// variety of rates anyway. NOTE: This length is currently milliseconds.
|
||||
headless_display_time_step_timer_ = event_loop()->NewTimer(
|
||||
kAppModeMinHeadlessDisplayStep / 1000, true,
|
||||
kHeadlessMinDisplayTimeStep / 1000, true,
|
||||
NewLambdaRunnable([this] { StepDisplayTime_(); }));
|
||||
} else {
|
||||
// In gui mode, push an initial frame to the graphics server. From this
|
||||
@ -382,7 +382,7 @@ void Logic::OnAppModeChanged() {
|
||||
}
|
||||
assert(headless_display_time_step_timer_);
|
||||
// NOTE: This is currently milliseconds.
|
||||
headless_display_time_step_timer_->SetLength(kAppModeMinHeadlessDisplayStep
|
||||
headless_display_time_step_timer_->SetLength(kHeadlessMinDisplayTimeStep
|
||||
/ 1000);
|
||||
}
|
||||
}
|
||||
@ -424,9 +424,9 @@ void Logic::PostUpdateDisplayTimeForHeadlessMode_() {
|
||||
// we've got until the next event. We'll plug this into our display-update
|
||||
// timer so we can try to sleep exactly until that point.
|
||||
auto headless_display_step_microsecs =
|
||||
std::max(std::min(g_base->app_mode()->GetHeadlessDisplayStep(),
|
||||
kAppModeMaxHeadlessDisplayStep),
|
||||
kAppModeMinHeadlessDisplayStep);
|
||||
std::max(std::min(g_base->app_mode()->GetHeadlessNextDisplayTimeStep(),
|
||||
kHeadlessMaxDisplayTimeStep),
|
||||
kHeadlessMinDisplayTimeStep);
|
||||
|
||||
if (debug_log_display_time_) {
|
||||
auto sleepsecs =
|
||||
|
||||
@ -13,6 +13,15 @@ namespace ballistica::base {
|
||||
|
||||
const int kDisplayTimeSampleCount{15};
|
||||
|
||||
/// The max amount of time a headless app can sleep if no events are pending.
|
||||
/// This should not be *too* high or it might cause delays when going from
|
||||
/// no events present to events present.
|
||||
const microsecs_t kHeadlessMaxDisplayTimeStep{500000};
|
||||
|
||||
/// The min amount of time a headless app can sleep. This provides an upper
|
||||
/// limit on stepping overhead in cases where events are densely packed.
|
||||
const microsecs_t kHeadlessMinDisplayTimeStep{1000};
|
||||
|
||||
/// The logic subsystem of the app. This runs on a dedicated thread and is
|
||||
/// where most high level app logic happens. Much app functionality
|
||||
/// including UI calls must be run on the logic thread.
|
||||
@ -63,8 +72,9 @@ class Logic {
|
||||
/// graphical builds we also use this opportunity to step our logic.
|
||||
void Draw();
|
||||
|
||||
/// Kick off an app shutdown. Shutdown is an asynchronous process which
|
||||
/// may take a bit of time to complete. Safe to call repeatedly.
|
||||
/// Kick off a low level app shutdown. Shutdown is an asynchronous process
|
||||
/// which may take up to a few seconds to complete. This is safe to call
|
||||
/// repeatedly but must be called from the logic thread.
|
||||
void Shutdown();
|
||||
|
||||
/// Should be called by the Python layer when it has completed all
|
||||
|
||||
@ -208,6 +208,8 @@ void AppConfig::SetupEntries() {
|
||||
|
||||
int_entries_[IntID::kPort] = IntEntry("Port", kDefaultPort);
|
||||
int_entries_[IntID::kMaxFPS] = IntEntry("Max FPS", 60);
|
||||
int_entries_[IntID::kSceneV1HostProtocol] =
|
||||
IntEntry("SceneV1 Host Protocol", 33);
|
||||
|
||||
bool_entries_[BoolID::kTouchControlsSwipeHidden] =
|
||||
BoolEntry("Touch Controls Swipe Hidden", false);
|
||||
|
||||
@ -55,6 +55,7 @@ class AppConfig {
|
||||
enum class IntID {
|
||||
kPort,
|
||||
kMaxFPS,
|
||||
kSceneV1HostProtocol,
|
||||
kLast // Sentinel.
|
||||
};
|
||||
|
||||
|
||||
@ -24,7 +24,10 @@ namespace ballistica::scene_v1 {
|
||||
// How long new clients have to wait before starting a kick vote.
|
||||
const int kNewClientKickVoteDelay = 60000;
|
||||
|
||||
ConnectionToClient::ConnectionToClient(int id) : id_(id) {
|
||||
ConnectionToClient::ConnectionToClient(int id)
|
||||
: id_(id),
|
||||
protocol_version_{
|
||||
SceneV1AppMode::GetSingleton()->host_protocol_version()} {
|
||||
// We calc this once just in case it changes on our end
|
||||
// (the client uses it for their verification hash so we need to
|
||||
// ensure it stays consistent).
|
||||
@ -33,7 +36,7 @@ ConnectionToClient::ConnectionToClient(int id) : id_(id) {
|
||||
|
||||
// On newer protocols we include an extra salt value
|
||||
// to ensure the hash the client generates can't be recycled.
|
||||
if (explicit_bool(kProtocolVersion >= 33)) {
|
||||
if (explicit_bool(protocol_version() >= 33)) {
|
||||
our_handshake_salt_ = std::to_string(rand()); // NOLINT
|
||||
}
|
||||
}
|
||||
@ -95,7 +98,7 @@ void ConnectionToClient::Update() {
|
||||
// In newer protocols we embed a json dict as the second part of the
|
||||
// handshake packet; this way we can evolve the protocol more
|
||||
// easily in the future.
|
||||
if (explicit_bool(kProtocolVersion >= 33)) {
|
||||
if (explicit_bool(protocol_version() >= 33)) {
|
||||
// Construct a json dict with our player-spec-string as one element.
|
||||
JsonDict dict;
|
||||
dict.AddString("s", our_handshake_player_spec_str_);
|
||||
@ -106,17 +109,17 @@ void ConnectionToClient::Update() {
|
||||
std::string out = dict.PrintUnformatted();
|
||||
std::vector<uint8_t> data(3 + out.size());
|
||||
data[0] = BA_SCENEPACKET_HANDSHAKE;
|
||||
uint16_t val = kProtocolVersion;
|
||||
uint16_t val = protocol_version();
|
||||
memcpy(data.data() + 1, &val, sizeof(val));
|
||||
memcpy(data.data() + 3, out.c_str(), out.size());
|
||||
SendGamePacket(data);
|
||||
} else {
|
||||
// (KILL THIS WHEN kProtocolVersionMin >= 33)
|
||||
// (KILL THIS WHEN kProtocolVersionClientMin >= 33)
|
||||
// on older protocols, we simply embedded our spec-string as the second
|
||||
// part of the handshake packet
|
||||
std::vector<uint8_t> data(3 + our_handshake_player_spec_str_.size());
|
||||
data[0] = BA_SCENEPACKET_HANDSHAKE;
|
||||
uint16_t val = kProtocolVersion;
|
||||
uint16_t val = protocol_version();
|
||||
memcpy(data.data() + 1, &val, sizeof(val));
|
||||
memcpy(data.data() + 3, our_handshake_player_spec_str_.c_str(),
|
||||
our_handshake_player_spec_str_.size());
|
||||
@ -155,7 +158,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector<uint8_t>& data) {
|
||||
|
||||
// In newer builds we expect to be sent a json dict here;
|
||||
// pull client's spec from that.
|
||||
if (explicit_bool(kProtocolVersion >= 33)) {
|
||||
if (protocol_version() >= 33) {
|
||||
std::vector<char> string_buffer(data.size() - 3 + 1);
|
||||
memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3);
|
||||
string_buffer[string_buffer.size() - 1] = 0;
|
||||
@ -173,7 +176,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector<uint8_t>& data) {
|
||||
cJSON_Delete(handshake);
|
||||
}
|
||||
} else {
|
||||
// (KILL THIS WHEN kProtocolVersionMin >= 33)
|
||||
// (KILL THIS WHEN kProtocolVersionClientMin >= 33)
|
||||
// older versions only contained the client spec
|
||||
// pull client's spec from the handshake packet..
|
||||
std::vector<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.
|
||||
uint16_t val;
|
||||
memcpy(&val, data.data() + 1, sizeof(val));
|
||||
if (val != kProtocolVersion) {
|
||||
if (val != protocol_version()) {
|
||||
// Depending on the connection type we may print the connection
|
||||
// failure or not. (If we invited them it'd be good to know about the
|
||||
// failure).
|
||||
|
||||
@ -55,10 +55,17 @@ class ConnectionToClient : public Connection {
|
||||
// or their peer name if they have no players.
|
||||
auto GetCombinedSpec() -> PlayerSpec;
|
||||
|
||||
auto protocol_version() const {
|
||||
assert(protocol_version_ != -1);
|
||||
return protocol_version_;
|
||||
}
|
||||
|
||||
private:
|
||||
virtual auto ShouldPrintIncompatibleClientErrors() const -> bool;
|
||||
auto GetClientInputDevice(int remote_id) -> ClientInputDevice*;
|
||||
void Error(const std::string& error_msg) override;
|
||||
|
||||
int protocol_version_;
|
||||
std::string our_handshake_player_spec_str_;
|
||||
std::string our_handshake_salt_;
|
||||
std::string peer_public_account_id_;
|
||||
|
||||
@ -22,7 +22,9 @@ namespace ballistica::scene_v1 {
|
||||
// How long to go between sending out null packets for pings.
|
||||
const int kPingSendInterval = 2000;
|
||||
|
||||
ConnectionToHost::ConnectionToHost() = default;
|
||||
ConnectionToHost::ConnectionToHost()
|
||||
: protocol_version_{
|
||||
SceneV1AppMode::GetSingleton()->host_protocol_version()} {}
|
||||
|
||||
auto ConnectionToHost::GetAsUDP() -> ConnectionToHostUDP* { return nullptr; }
|
||||
|
||||
@ -103,8 +105,8 @@ void ConnectionToHost::HandleGamePacket(const std::vector<uint8_t>& data) {
|
||||
uint16_t their_protocol_version;
|
||||
memcpy(&their_protocol_version, data.data() + 1,
|
||||
sizeof(their_protocol_version));
|
||||
if (their_protocol_version >= kProtocolVersionMin
|
||||
&& their_protocol_version <= kProtocolVersion) {
|
||||
if (their_protocol_version >= kProtocolVersionClientMin
|
||||
&& their_protocol_version <= kProtocolVersionMax) {
|
||||
compatible = true;
|
||||
|
||||
// If we are compatible, set our protocol version to match
|
||||
@ -136,7 +138,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector<uint8_t>& data) {
|
||||
memcpy(data2.data() + 3, out.c_str(), out.size());
|
||||
SendGamePacket(data2);
|
||||
} else {
|
||||
// (KILL THIS WHEN kProtocolVersionMin >= 33)
|
||||
// (KILL THIS WHEN kProtocolVersionClientMin >= 33)
|
||||
std::string our_spec_str =
|
||||
PlayerSpec::GetAccountPlayerSpec().GetSpecString();
|
||||
std::vector<uint8_t> response(3 + our_spec_str.size());
|
||||
@ -148,7 +150,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector<uint8_t>& data) {
|
||||
}
|
||||
|
||||
if (!compatible) {
|
||||
if (their_protocol_version > kProtocolVersion) {
|
||||
if (their_protocol_version > protocol_version()) {
|
||||
Error(g_base->assets->GetResourceString(
|
||||
"incompatibleNewerVersionHostText"));
|
||||
} else {
|
||||
@ -183,7 +185,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector<uint8_t>& data) {
|
||||
cJSON_Delete(handshake);
|
||||
}
|
||||
} else {
|
||||
// (KILL THIS WHEN kProtocolVersionMin >= 33)
|
||||
// (KILL THIS WHEN kProtocolVersionClientMin >= 33)
|
||||
// In older protocols, handshake simply contained a
|
||||
// player-spec for the host.
|
||||
|
||||
|
||||
@ -33,12 +33,12 @@ class ConnectionToHost : public Connection {
|
||||
std::string party_name_;
|
||||
std::string peer_hash_input_;
|
||||
std::string peer_hash_;
|
||||
bool printed_connect_message_ = false;
|
||||
int protocol_version_ = kProtocolVersion;
|
||||
int build_number_ = 0;
|
||||
bool got_host_info_ = false;
|
||||
// can remove once back-compat protocol is > 29
|
||||
bool ignore_old_attach_remote_player_packets_ = false;
|
||||
// Can remove once back-compat protocol is > 29
|
||||
bool ignore_old_attach_remote_player_packets_ : 1 {};
|
||||
bool printed_connect_message_ : 1 {};
|
||||
bool got_host_info_ : 1 {};
|
||||
int protocol_version_{-1};
|
||||
int build_number_{};
|
||||
millisecs_t last_ping_send_time_{};
|
||||
// the client-session that we're driving
|
||||
Object::WeakRef<ClientSession> client_session_;
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
namespace ballistica::scene_v1 {
|
||||
|
||||
auto ConnectionToHostUDP::SwitchProtocol() -> bool {
|
||||
if (protocol_version() > kProtocolVersionMin) {
|
||||
if (protocol_version() > kProtocolVersionClientMin) {
|
||||
set_protocol_version(protocol_version() - 1);
|
||||
|
||||
// Need a new request id so we ignore further responses to our previous
|
||||
|
||||
@ -72,7 +72,7 @@ class ImageNode : public Node {
|
||||
void set_front(bool val) { front_ = val; }
|
||||
|
||||
private:
|
||||
enum class Attach {
|
||||
enum class Attach : uint8_t {
|
||||
CENTER,
|
||||
TOP_LEFT,
|
||||
TOP_CENTER,
|
||||
@ -83,32 +83,24 @@ class ImageNode : public Node {
|
||||
BOTTOM_LEFT,
|
||||
CENTER_LEFT
|
||||
};
|
||||
bool host_only_{};
|
||||
bool front_{};
|
||||
float vr_depth_{};
|
||||
std::vector<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_;
|
||||
bool fill_screen_{};
|
||||
bool has_alpha_channel_{true};
|
||||
bool dirty_{true};
|
||||
float opacity_{1.0f};
|
||||
|
||||
bool host_only_ : 1 {};
|
||||
bool front_ : 1 {};
|
||||
bool absolute_scale_ : 1 {true};
|
||||
bool premultiplied_ : 1 {};
|
||||
bool fill_screen_ : 1 {};
|
||||
bool has_alpha_channel_ : 1 {true};
|
||||
bool dirty_ : 1 {true};
|
||||
Attach attach_{Attach::CENTER};
|
||||
bool absolute_scale_{true};
|
||||
|
||||
float vr_depth_{};
|
||||
float opacity_{1.0f};
|
||||
float center_x_{};
|
||||
float center_y_{};
|
||||
float width_{};
|
||||
float height_{};
|
||||
float tilt_translate_{};
|
||||
float rotate_{};
|
||||
bool premultiplied_{};
|
||||
float red_{1.0f};
|
||||
float green_{1.0f};
|
||||
float blue_{1.0f};
|
||||
@ -119,6 +111,16 @@ class ImageNode : public Node {
|
||||
float tint2_red_{1.0f};
|
||||
float tint2_green_{1.0f};
|
||||
float tint2_blue_{1.0f};
|
||||
std::vector<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
|
||||
|
||||
@ -1046,7 +1046,24 @@ static auto PyCameraShake(PyObject* self, PyObject* args, PyObject* keywds)
|
||||
const_cast<char**>(kwlist), &intensity)) {
|
||||
return nullptr;
|
||||
}
|
||||
g_base->graphics->LocalCameraShake(intensity);
|
||||
|
||||
if (Scene* scene = ContextRefSceneV1::FromCurrent().GetMutableScene()) {
|
||||
// Send to clients/replays (IF we're servering protocol 35+).
|
||||
if (SceneV1AppMode::GetSingleton()->host_protocol_version() >= 35) {
|
||||
if (SessionStream* output_stream = scene->GetSceneStream()) {
|
||||
output_stream->EmitCameraShake(intensity);
|
||||
}
|
||||
}
|
||||
|
||||
// Depict locally.
|
||||
if (!g_core->HeadlessMode()) {
|
||||
g_base->graphics->LocalCameraShake(intensity);
|
||||
}
|
||||
} else {
|
||||
throw Exception("Can't shake the camera in this context_ref.",
|
||||
PyExcType::kContext);
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
@ -1172,9 +1189,13 @@ static auto PyEmitFx(PyObject* self, PyObject* args, PyObject* keywds)
|
||||
e.spread = spread;
|
||||
e.chunk_type = chunk_type;
|
||||
e.tendril_type = tendril_type;
|
||||
|
||||
// Send to clients/replays.
|
||||
if (SessionStream* output_stream = scene->GetSceneStream()) {
|
||||
output_stream->EmitBGDynamics(e);
|
||||
}
|
||||
|
||||
// Depict locally.
|
||||
if (!g_core->HeadlessMode()) {
|
||||
g_base->bg_dynamics->Emit(e);
|
||||
}
|
||||
@ -1722,7 +1743,8 @@ static PyMethodDef PyHandleAppIntentExecDef = {
|
||||
|
||||
static auto PyProtocolVersion(PyObject* self) -> PyObject* {
|
||||
BA_PYTHON_TRY;
|
||||
return PyLong_FromLong(kProtocolVersion);
|
||||
return PyLong_FromLong(
|
||||
SceneV1AppMode::GetSingleton()->host_protocol_version());
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
|
||||
|
||||
@ -103,20 +103,27 @@ SceneV1FeatureSet::SceneV1FeatureSet() : python{new SceneV1Python()} {
|
||||
// Types: I is 32 bit int, i is 16 bit int, c is 8 bit int,
|
||||
// F is 32 bit float, f is 16 bit float,
|
||||
// s is string, b is bool.
|
||||
SetupNodeMessageType("flash", NodeMessageType::kFlash, "");
|
||||
SetupNodeMessageType("footing", NodeMessageType::kFooting, "c");
|
||||
SetupNodeMessageType("impulse", NodeMessageType::kImpulse, "fffffffffifff");
|
||||
SetupNodeMessageType("kick_back", NodeMessageType::kKickback, "fffffff");
|
||||
SetupNodeMessageType("celebrate", NodeMessageType::kCelebrate, "i");
|
||||
SetupNodeMessageType("celebrate_l", NodeMessageType::kCelebrateL, "i");
|
||||
SetupNodeMessageType("celebrate_r", NodeMessageType::kCelebrateR, "i");
|
||||
SetupNodeMessageType("knockout", NodeMessageType::kKnockout, "f");
|
||||
SetupNodeMessageType("hurt_sound", NodeMessageType::kHurtSound, "");
|
||||
SetupNodeMessageType("picked_up", NodeMessageType::kPickedUp, "");
|
||||
SetupNodeMessageType("jump_sound", NodeMessageType::kJumpSound, "");
|
||||
SetupNodeMessageType("attack_sound", NodeMessageType::kAttackSound, "");
|
||||
SetupNodeMessageType("scream_sound", NodeMessageType::kScreamSound, "");
|
||||
SetupNodeMessageType("stand", NodeMessageType::kStand, "ffff");
|
||||
SetupNodeMessageType_("flash", NodeMessageType::kFlash, "");
|
||||
SetupNodeMessageType_("footing", NodeMessageType::kFooting, "c");
|
||||
SetupNodeMessageType_("impulse", NodeMessageType::kImpulse, "fffffffffifff");
|
||||
SetupNodeMessageType_("kick_back", NodeMessageType::kKickback, "fffffff");
|
||||
SetupNodeMessageType_("celebrate", NodeMessageType::kCelebrate, "i");
|
||||
SetupNodeMessageType_("celebrate_l", NodeMessageType::kCelebrateL, "i");
|
||||
SetupNodeMessageType_("celebrate_r", NodeMessageType::kCelebrateR, "i");
|
||||
SetupNodeMessageType_("knockout", NodeMessageType::kKnockout, "f");
|
||||
SetupNodeMessageType_("hurt_sound", NodeMessageType::kHurtSound, "");
|
||||
SetupNodeMessageType_("picked_up", NodeMessageType::kPickedUp, "");
|
||||
SetupNodeMessageType_("jump_sound", NodeMessageType::kJumpSound, "");
|
||||
SetupNodeMessageType_("attack_sound", NodeMessageType::kAttackSound, "");
|
||||
SetupNodeMessageType_("scream_sound", NodeMessageType::kScreamSound, "");
|
||||
SetupNodeMessageType_("stand", NodeMessageType::kStand, "ffff");
|
||||
}
|
||||
|
||||
auto SceneV1FeatureSet::Import() -> SceneV1FeatureSet* {
|
||||
// Since we provide a native Python module, we piggyback our C++ front-end
|
||||
// on top of that. This way our C++ and Python dependencies are resolved
|
||||
// consistently no matter which side we are imported from.
|
||||
return ImportThroughPythonModule<SceneV1FeatureSet>("_bascenev1");
|
||||
}
|
||||
|
||||
void SceneV1FeatureSet::Reset() {
|
||||
@ -132,13 +139,6 @@ void SceneV1FeatureSet::ResetRandomNames() {
|
||||
random_name_registry_->clear();
|
||||
}
|
||||
|
||||
auto SceneV1FeatureSet::Import() -> SceneV1FeatureSet* {
|
||||
// Since we provide a native Python module, we piggyback our C++ front-end
|
||||
// on top of that. This way our C++ and Python dependencies are resolved
|
||||
// consistently no matter which side we are imported from.
|
||||
return ImportThroughPythonModule<SceneV1FeatureSet>("_bascenev1");
|
||||
}
|
||||
|
||||
auto SceneV1FeatureSet::GetRandomName(const std::string& full_name)
|
||||
-> std::string {
|
||||
assert(g_base->InLogicThread());
|
||||
@ -172,9 +172,9 @@ auto SceneV1FeatureSet::GetRandomName(const std::string& full_name)
|
||||
return (*random_name_registry_)[full_name];
|
||||
}
|
||||
|
||||
void SceneV1FeatureSet::SetupNodeMessageType(const std::string& name,
|
||||
NodeMessageType val,
|
||||
const std::string& format) {
|
||||
void SceneV1FeatureSet::SetupNodeMessageType_(const std::string& name,
|
||||
NodeMessageType val,
|
||||
const std::string& format) {
|
||||
node_message_types_[name] = val;
|
||||
assert(static_cast<int>(val) >= 0);
|
||||
if (node_message_formats_.size() <= static_cast<size_t>(val)) {
|
||||
|
||||
@ -27,39 +27,54 @@ namespace ballistica::scene_v1 {
|
||||
|
||||
// Protocol version we host games with and write replays to. This should be
|
||||
// incremented whenever there are changes made to the session-commands layer
|
||||
// (new/removed/changed nodes, attrs, data files, behavior, etc.)
|
||||
// (new/removed/changed nodes, attrs, data files, behavior, etc.).
|
||||
|
||||
// Note that the packet/gamepacket/message layer can vary more organically
|
||||
// based on build-numbers of connected clients/servers since none of that
|
||||
// data is stored; this just needs to be observed for all the scene stuff
|
||||
// that goes into replays since a single stream can get played/replayed on
|
||||
// different builds (as long as they support that protocol version).
|
||||
const int kProtocolVersion = 33;
|
||||
// data is stored; these protocol versions just need to be observed by
|
||||
// anything emitting or ingesting scene streams.
|
||||
|
||||
// Oldest protocol version we can act as a host for.
|
||||
const int kProtocolVersionHostMin = 33;
|
||||
|
||||
// Oldest protocol version we can act as a client to. This can generally be
|
||||
// left as-is as long as only new nodes/attrs/commands are added and
|
||||
// existing stuff is unchanged.
|
||||
const int kProtocolVersionMin = 24;
|
||||
// left as-is as long as only new nodes/attrs/commands are added and old
|
||||
// behavior remains the same when not using the new stuff.
|
||||
const int kProtocolVersionClientMin = 24;
|
||||
|
||||
// FIXME: We should separate out connection protocol from scene protocol. We
|
||||
// want to be able to watch really old replays if possible but being able
|
||||
// to connect to old clients is much less important (and slows progress).
|
||||
// Newest protocol version we can act as a client OR host for.
|
||||
const int kProtocolVersionMax = 35;
|
||||
|
||||
// Protocol additions:
|
||||
// 25: added a few new achievement graphics and new node attrs for displaying
|
||||
// stuff in front of the UI
|
||||
// 26: added penguin
|
||||
// 27: added templates for LOTS of characters
|
||||
// 28: added cyborg and enabled fallback sounds and textures
|
||||
// 29: added bunny and eggs
|
||||
// 30: added support for resource-strings in text-nodes and screen-messages
|
||||
// 31: added support for short-form resource-strings, time-display-node, and
|
||||
// string-to-string attr connections
|
||||
// 32: added json based player profiles message, added shield
|
||||
// alwaysShowHealthBar attr
|
||||
// 33: handshake/handshake-response now send json dicts instead of
|
||||
// just player-specs
|
||||
// 34: new image_node enums, data assets.
|
||||
// The protocol version we actually host is now read as a setting; see
|
||||
// kSceneV1HostProtocol in ballistica/base/support/app_config.h.
|
||||
|
||||
// Protocol changes:
|
||||
//
|
||||
// 25: Added a few new achievement graphics and new node attrs for displaying
|
||||
// stuff in front of the UI.
|
||||
//
|
||||
// 26: Added penguin.
|
||||
//
|
||||
// 27: Added templates for LOTS of characters.
|
||||
//
|
||||
// 28: Added cyborg and enabled fallback sounds and textures.
|
||||
//
|
||||
// 29: Added bunny and eggs.
|
||||
//
|
||||
// 30: Added support for resource-strings in text-nodes and screen-messages.
|
||||
//
|
||||
// 31: Added support for short-form resource-strings, time-display-node, and
|
||||
// string-to-string attr connections.
|
||||
//
|
||||
// 32: Added json based player profiles message, added shield
|
||||
// always_show_health_bar attr.
|
||||
//
|
||||
// 33: Handshake/handshake-response now send json dicts instead of
|
||||
// just player-specs.
|
||||
//
|
||||
// 34: New image_node enums, data assets.
|
||||
//
|
||||
// 35: Camera shake in netplay. how did I apparently miss this for 10 years!?!
|
||||
|
||||
// Sim step size in milliseconds.
|
||||
const int kGameStepMilliseconds = 8;
|
||||
@ -215,7 +230,8 @@ enum class SessionCommand {
|
||||
kScreenMessageBottom,
|
||||
kScreenMessageTop,
|
||||
kAddData,
|
||||
kRemoveData
|
||||
kRemoveData,
|
||||
kCameraShake
|
||||
};
|
||||
|
||||
enum class NodeCollideAttr {
|
||||
@ -373,8 +389,8 @@ class SceneV1FeatureSet : public FeatureSetNativeComponent {
|
||||
SceneV1Python* const python;
|
||||
|
||||
private:
|
||||
void SetupNodeMessageType(const std::string& name, NodeMessageType val,
|
||||
const std::string& format);
|
||||
void SetupNodeMessageType_(const std::string& name, NodeMessageType val,
|
||||
const std::string& format);
|
||||
|
||||
SceneV1FeatureSet();
|
||||
std::unordered_map<std::string, NodeType*> node_types_;
|
||||
|
||||
@ -835,6 +835,11 @@ void ClientSession::Update(int time_advance_millisecs, double time_advance) {
|
||||
y, z);
|
||||
break;
|
||||
}
|
||||
case SessionCommand::kCameraShake: {
|
||||
auto intensity = ReadFloat();
|
||||
g_base->graphics->LocalCameraShake(intensity);
|
||||
break;
|
||||
}
|
||||
case SessionCommand::kEmitBGDynamics: {
|
||||
int cmdvals[4];
|
||||
ReadInt32_4(cmdvals);
|
||||
|
||||
@ -17,7 +17,9 @@ ClientSessionNet::ClientSessionNet() {
|
||||
"g_replay_open true at netclient start; shouldn't happen.");
|
||||
}
|
||||
assert(g_base->assets_server);
|
||||
g_base->assets_server->PushBeginWriteReplayCall(kProtocolVersion);
|
||||
|
||||
// We always write replays as the highest protocol version we support.
|
||||
g_base->assets_server->PushBeginWriteReplayCall(kProtocolVersionMax);
|
||||
writing_replay_ = true;
|
||||
g_core->replay_open = true;
|
||||
}
|
||||
|
||||
@ -26,8 +26,6 @@ class ClientSessionNet : public ClientSession {
|
||||
|
||||
private:
|
||||
struct SampleBucket {
|
||||
// int least_buffered_count{};
|
||||
// int most_buffered_count{};
|
||||
int max_delay_from_projection{};
|
||||
};
|
||||
|
||||
|
||||
@ -253,7 +253,7 @@ void ClientSessionReplay::OnReset(bool rewind) {
|
||||
Error("error reading version");
|
||||
return;
|
||||
}
|
||||
if (version > kProtocolVersion || version < kProtocolVersionMin) {
|
||||
if (version > kProtocolVersionMax || version < kProtocolVersionClientMin) {
|
||||
ScreenMessage(g_base->assets->GetResourceString("replayVersionErrorText"),
|
||||
{1, 0, 0});
|
||||
End();
|
||||
|
||||
@ -46,7 +46,7 @@ const int kKickVoteFailRetryDelayInitiatorExtra = 120000;
|
||||
// to kick).
|
||||
const int kKickVoteMinimumClients = (g_buildconfig.headless_build() ? 3 : 4);
|
||||
|
||||
struct SceneV1AppMode::ScanResultsEntryPriv {
|
||||
struct SceneV1AppMode::ScanResultsEntryPriv_ {
|
||||
scene_v1::PlayerSpec player_spec;
|
||||
std::string address;
|
||||
uint32_t last_query_id{};
|
||||
@ -78,7 +78,16 @@ static SceneV1AppMode* g_scene_v1_app_mode{};
|
||||
|
||||
void SceneV1AppMode::OnActivate() {
|
||||
assert(g_base->InLogicThread());
|
||||
Reset();
|
||||
|
||||
// Make sure we pull this only once when we are first active.
|
||||
if (host_protocol_version_ == -1) {
|
||||
host_protocol_version_ =
|
||||
std::clamp(g_base->app_config->Resolve(
|
||||
base::AppConfig::IntID::kSceneV1HostProtocol),
|
||||
kProtocolVersionHostMin, kProtocolVersionMax);
|
||||
}
|
||||
|
||||
Reset_();
|
||||
|
||||
// We use UIV1.
|
||||
if (!g_core->HeadlessMode()) {
|
||||
@ -108,9 +117,10 @@ void SceneV1AppMode::OnAppPause() {
|
||||
void SceneV1AppMode::OnAppResume() { assert(g_base->InLogicThread()); }
|
||||
|
||||
// Note: for now we're making our host-scan network calls directly from the
|
||||
// logic thread. This is generally not a good idea since it appears that even in
|
||||
// non-blocking mode they're still blocking for 3-4ms sometimes. But for now
|
||||
// since this is only used minimally and only while in the UI I guess it's ok.
|
||||
// logic thread. This is generally not a good idea since it appears that even
|
||||
// in non-blocking mode they're still blocking for 3-4ms sometimes. But for
|
||||
// now since this is only used minimally and only while in the UI I guess it's
|
||||
// ok.
|
||||
void SceneV1AppMode::HostScanCycle() {
|
||||
assert(g_base->InLogicThread());
|
||||
|
||||
@ -258,7 +268,7 @@ void SceneV1AppMode::HostScanCycle() {
|
||||
bool do_update_entry = (i == scan_results_.end()
|
||||
|| i->second.last_query_id != query_id);
|
||||
if (do_update_entry) {
|
||||
ScanResultsEntryPriv& entry(scan_results_[key]);
|
||||
ScanResultsEntryPriv_& entry(scan_results_[key]);
|
||||
entry.player_spec = scene_v1::PlayerSpec(player_spec_str);
|
||||
char buffer2[256];
|
||||
entry.address = inet_ntop(
|
||||
@ -269,7 +279,7 @@ void SceneV1AppMode::HostScanCycle() {
|
||||
entry.last_contact_time = g_core->GetAppTimeMillisecs();
|
||||
}
|
||||
}
|
||||
PruneScanResults();
|
||||
PruneScanResults_();
|
||||
}
|
||||
} else {
|
||||
Log(LogLevel::kError,
|
||||
@ -290,7 +300,7 @@ void SceneV1AppMode::EndHostScanning() {
|
||||
}
|
||||
}
|
||||
|
||||
void SceneV1AppMode::PruneScanResults() {
|
||||
void SceneV1AppMode::PruneScanResults_() {
|
||||
millisecs_t t = g_core->GetAppTimeMillisecs();
|
||||
auto i = scan_results_.begin();
|
||||
while (i != scan_results_.end()) {
|
||||
@ -311,13 +321,13 @@ auto SceneV1AppMode::GetScanResults()
|
||||
std::scoped_lock lock(scan_results_mutex_);
|
||||
int out_num = 0;
|
||||
for (auto&& i : scan_results_) {
|
||||
ScanResultsEntryPriv& in(i.second);
|
||||
ScanResultsEntryPriv_& in(i.second);
|
||||
ScanResultsEntry& out(results[out_num]);
|
||||
out.display_string = in.player_spec.GetDisplayString();
|
||||
out.address = in.address;
|
||||
out_num++;
|
||||
}
|
||||
PruneScanResults();
|
||||
PruneScanResults_();
|
||||
}
|
||||
return results;
|
||||
}
|
||||
@ -399,8 +409,8 @@ auto SceneV1AppMode::HandleJSONPing(const std::string& data_str)
|
||||
}
|
||||
cJSON_Delete(data);
|
||||
|
||||
// Ok lets include some basic info that might be pertinent to someone pinging
|
||||
// us. Currently that includes our current/max connection count.
|
||||
// Ok lets include some basic info that might be pertinent to someone
|
||||
// pinging us. Currently that includes our current/max connection count.
|
||||
char buffer[256];
|
||||
snprintf(buffer, sizeof(buffer), R"({"b":%d,"ps":%d,"psmx":%d})",
|
||||
kEngineBuildNumber, public_party_size(), public_party_max_size());
|
||||
@ -420,7 +430,7 @@ auto SceneV1AppMode::GetPartySize() const -> int {
|
||||
return cJSON_GetArraySize(game_roster_);
|
||||
}
|
||||
|
||||
auto SceneV1AppMode::GetHeadlessDisplayStep() -> microsecs_t {
|
||||
auto SceneV1AppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t {
|
||||
std::optional<microsecs_t> min_time_to_next;
|
||||
for (auto&& i : sessions_) {
|
||||
if (!i.Exists()) {
|
||||
@ -436,7 +446,7 @@ auto SceneV1AppMode::GetHeadlessDisplayStep() -> microsecs_t {
|
||||
}
|
||||
}
|
||||
return min_time_to_next.has_value() ? *min_time_to_next
|
||||
: base::kAppModeMaxHeadlessDisplayStep;
|
||||
: base::kHeadlessMaxDisplayTimeStep;
|
||||
}
|
||||
|
||||
void SceneV1AppMode::StepDisplayTime() {
|
||||
@ -468,15 +478,15 @@ void SceneV1AppMode::StepDisplayTime() {
|
||||
}
|
||||
legacy_display_time_millisecs_prev_ = legacy_display_time_millisecs_;
|
||||
|
||||
UpdateKickVote();
|
||||
UpdateKickVote_();
|
||||
|
||||
HandleQuitOnIdle();
|
||||
HandleQuitOnIdle_();
|
||||
|
||||
// Send the game roster to our clients if it's changed recently.
|
||||
if (game_roster_dirty_) {
|
||||
if (app_time > last_game_roster_send_time_ + 2500) {
|
||||
// Now send it to all connected clients.
|
||||
std::vector<uint8_t> msg = GetGameRosterMessage();
|
||||
std::vector<uint8_t> msg = GetGameRosterMessage_();
|
||||
for (auto&& c : connections()->GetConnectionsToClients()) {
|
||||
c->SendReliableMessage(msg);
|
||||
}
|
||||
@ -501,7 +511,7 @@ void SceneV1AppMode::StepDisplayTime() {
|
||||
}
|
||||
|
||||
// Go ahead and prune dead ones.
|
||||
PruneSessions();
|
||||
PruneSessions_();
|
||||
|
||||
in_update_ = false;
|
||||
|
||||
@ -521,7 +531,7 @@ void SceneV1AppMode::StepDisplayTime() {
|
||||
}
|
||||
}
|
||||
|
||||
auto SceneV1AppMode::GetGameRosterMessage() -> std::vector<uint8_t> {
|
||||
auto SceneV1AppMode::GetGameRosterMessage_() -> std::vector<uint8_t> {
|
||||
// This message is simply a flattened json string of our roster (including
|
||||
// terminating char).
|
||||
char* s = cJSON_PrintUnformatted(game_roster_);
|
||||
@ -661,7 +671,7 @@ void SceneV1AppMode::UpdateGameRoster() {
|
||||
game_roster_dirty_ = true;
|
||||
}
|
||||
|
||||
void SceneV1AppMode::UpdateKickVote() {
|
||||
void SceneV1AppMode::UpdateKickVote_() {
|
||||
if (!kick_vote_in_progress_) {
|
||||
return;
|
||||
}
|
||||
@ -973,7 +983,7 @@ void SceneV1AppMode::LaunchHostSession(PyObject* session_type_obj,
|
||||
base::ScopedSetContext ssc(nullptr);
|
||||
|
||||
// This should kill any current session and get us back to a blank slate.
|
||||
Reset();
|
||||
Reset_();
|
||||
|
||||
Object::WeakRef<Session> old_foreground_session(foreground_session_);
|
||||
try {
|
||||
@ -1004,7 +1014,7 @@ void SceneV1AppMode::LaunchReplaySession(const std::string& file_name) {
|
||||
base::ScopedSetContext ssc(nullptr);
|
||||
|
||||
// This should kill any current session and get us back to a blank slate.
|
||||
Reset();
|
||||
Reset_();
|
||||
|
||||
// Create the new session.
|
||||
Object::WeakRef<Session> old_foreground_session(foreground_session_);
|
||||
@ -1034,7 +1044,7 @@ void SceneV1AppMode::LaunchClientSession() {
|
||||
base::ScopedSetContext ssc(nullptr);
|
||||
|
||||
// This should kill any current session and get us back to a blank slate.
|
||||
Reset();
|
||||
Reset_();
|
||||
|
||||
// Create the new session.
|
||||
Object::WeakRef<Session> old_foreground_session(foreground_session_);
|
||||
@ -1052,13 +1062,13 @@ void SceneV1AppMode::LaunchClientSession() {
|
||||
}
|
||||
|
||||
// Reset to a blank slate.
|
||||
void SceneV1AppMode::Reset() {
|
||||
void SceneV1AppMode::Reset_() {
|
||||
assert(g_base);
|
||||
assert(g_base->InLogicThread());
|
||||
|
||||
// Tear down our existing session.
|
||||
foreground_session_.Clear();
|
||||
PruneSessions();
|
||||
PruneSessions_();
|
||||
|
||||
// If all is well our sessions should all be dead.
|
||||
if (g_core->session_count != 0) {
|
||||
@ -1174,7 +1184,7 @@ void SceneV1AppMode::DoApplyAppConfig() {
|
||||
base::AppConfig::OptionalFloatID::kIdleExitMinutes);
|
||||
}
|
||||
|
||||
void SceneV1AppMode::PruneSessions() {
|
||||
void SceneV1AppMode::PruneSessions_() {
|
||||
bool have_dead_session = false;
|
||||
for (auto&& i : sessions_) {
|
||||
if (i.Exists()) {
|
||||
@ -1368,7 +1378,7 @@ void SceneV1AppMode::BanPlayer(const PlayerSpec& spec, millisecs_t duration) {
|
||||
banned_players_.emplace_back(g_core->GetAppTimeMillisecs() + duration, spec);
|
||||
}
|
||||
|
||||
void SceneV1AppMode::HandleQuitOnIdle() {
|
||||
void SceneV1AppMode::HandleQuitOnIdle_() {
|
||||
if (idle_exit_minutes_) {
|
||||
auto idle_seconds{static_cast<float>(g_base->input->input_idle_time())
|
||||
* 0.001f};
|
||||
@ -1443,7 +1453,7 @@ void SceneV1AppMode::HandleGameQuery(const char* buffer, size_t size,
|
||||
|
||||
msg[0] = BA_PACKET_HOST_QUERY_RESPONSE;
|
||||
memcpy(msg + 1, &query_id, 4);
|
||||
uint32_t protocol_version = kProtocolVersion;
|
||||
uint32_t protocol_version = host_protocol_version();
|
||||
memcpy(msg + 5, &protocol_version, 4);
|
||||
msg[9] = static_cast<char>(usid.size());
|
||||
msg[10] = static_cast<char>(player_spec_string.size());
|
||||
|
||||
@ -182,41 +182,56 @@ class SceneV1AppMode : public base::AppMode {
|
||||
auto buffer_time() const { return buffer_time_; }
|
||||
void set_buffer_time(int val) { buffer_time_ = val; }
|
||||
void OnActivate() override;
|
||||
auto GetHeadlessDisplayStep() -> microsecs_t override;
|
||||
auto GetHeadlessNextDisplayTimeStep() -> microsecs_t override;
|
||||
|
||||
auto host_protocol_version() const {
|
||||
assert(host_protocol_version_ != -1);
|
||||
return host_protocol_version_;
|
||||
}
|
||||
|
||||
private:
|
||||
void PruneScanResults();
|
||||
void UpdateKickVote();
|
||||
SceneV1AppMode();
|
||||
auto GetGameRosterMessage() -> std::vector<uint8_t>;
|
||||
void Reset();
|
||||
void PruneSessions();
|
||||
void HandleQuitOnIdle();
|
||||
struct ScanResultsEntryPriv;
|
||||
void PruneScanResults_();
|
||||
void UpdateKickVote_();
|
||||
auto GetGameRosterMessage_() -> std::vector<uint8_t>;
|
||||
void Reset_();
|
||||
void PruneSessions_();
|
||||
void HandleQuitOnIdle_();
|
||||
|
||||
struct ScanResultsEntryPriv_;
|
||||
|
||||
// Note: would use an unordered_map here but gcc doesn't seem to allow
|
||||
// forward declarations of their template params.
|
||||
std::map<std::string, ScanResultsEntryPriv> scan_results_;
|
||||
std::map<std::string, ScanResultsEntryPriv_> scan_results_;
|
||||
std::mutex scan_results_mutex_;
|
||||
uint32_t next_scan_query_id_{};
|
||||
int scan_socket_{-1};
|
||||
int host_protocol_version_{-1};
|
||||
|
||||
std::list<std::string> chat_messages_;
|
||||
bool chat_muted_{};
|
||||
// *All* existing sessions (including old ones waiting to shut down).
|
||||
std::vector<Object::Ref<Session> > sessions_;
|
||||
Object::WeakRef<Scene> foreground_scene_;
|
||||
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_{};
|
||||
std::unique_ptr<ConnectionSet> connections_;
|
||||
cJSON* game_roster_{};
|
||||
Object::WeakRef<ConnectionToClient> kick_vote_starter_;
|
||||
Object::WeakRef<ConnectionToClient> kick_vote_target_;
|
||||
millisecs_t kick_vote_end_time_{};
|
||||
bool kick_vote_in_progress_{};
|
||||
int last_kick_votes_needed_{-1};
|
||||
bool kick_voting_enabled_{true};
|
||||
millisecs_t legacy_display_time_millisecs_{};
|
||||
millisecs_t legacy_display_time_millisecs_prev_{-1};
|
||||
|
||||
@ -229,28 +244,22 @@ class SceneV1AppMode : public base::AppMode {
|
||||
// it over the network.
|
||||
int buffer_time_{};
|
||||
|
||||
bool in_update_{};
|
||||
millisecs_t next_long_update_report_time_{};
|
||||
int debug_speed_exponent_{};
|
||||
float debug_speed_mult_{1.0f};
|
||||
int replay_speed_exponent_{};
|
||||
float replay_speed_mult_{1.0f};
|
||||
bool kick_idle_players_{};
|
||||
std::set<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_max_size_{8};
|
||||
bool public_party_queue_enabled_{true};
|
||||
int public_party_player_count_{0};
|
||||
int public_party_max_player_count_{8};
|
||||
float debug_speed_mult_{1.0f};
|
||||
float replay_speed_mult_{1.0f};
|
||||
std::set<std::string> admin_public_ids_;
|
||||
millisecs_t last_connection_to_client_join_time_{};
|
||||
std::string public_party_name_;
|
||||
std::string public_party_min_league_;
|
||||
std::string public_party_stats_url_;
|
||||
bool require_client_authentication_{};
|
||||
std::list<std::pair<millisecs_t, PlayerSpec> > banned_players_;
|
||||
std::optional<float> idle_exit_minutes_{};
|
||||
bool idle_exiting_{};
|
||||
std::optional<uint32_t> internal_music_play_id_{};
|
||||
};
|
||||
|
||||
|
||||
@ -31,8 +31,9 @@ SessionStream::SessionStream(HostSession* host_session, bool save_replay)
|
||||
Log(LogLevel::kError,
|
||||
"g_replay_open true at replay start; shouldn't happen.");
|
||||
}
|
||||
// We always write replays as the max protocol version we support.
|
||||
assert(g_base->assets_server);
|
||||
g_base->assets_server->PushBeginWriteReplayCall(kProtocolVersion);
|
||||
g_base->assets_server->PushBeginWriteReplayCall(kProtocolVersionMax);
|
||||
writing_replay_ = true;
|
||||
g_core->replay_open = true;
|
||||
}
|
||||
@ -205,9 +206,6 @@ void SessionStream::Flush() {
|
||||
}
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "ConstantParameter"
|
||||
|
||||
// Writes just a command.
|
||||
void SessionStream::WriteCommand(SessionCommand cmd) {
|
||||
assert(out_command_.empty());
|
||||
@ -219,8 +217,6 @@ void SessionStream::WriteCommand(SessionCommand cmd) {
|
||||
*ptr = static_cast<uint8_t>(cmd);
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
// Writes a command plus an int to the stream, using whatever size is optimal.
|
||||
void SessionStream::WriteCommandInt32(SessionCommand cmd, int32_t value) {
|
||||
assert(out_command_.empty());
|
||||
@ -1123,6 +1119,13 @@ void SessionStream::EmitBGDynamics(const base::BGDynamicsEmission& e) {
|
||||
EndCommand();
|
||||
}
|
||||
|
||||
void SessionStream::EmitCameraShake(float intensity) {
|
||||
WriteCommand(SessionCommand::kCameraShake);
|
||||
// FIXME: We shouldn't need to be passing all these as full floats. :-(
|
||||
WriteFloat(intensity);
|
||||
EndCommand();
|
||||
}
|
||||
|
||||
void SessionStream::PlaySound(SceneSound* sound, float volume) {
|
||||
assert(IsValidSound(sound));
|
||||
assert(IsValidScene(sound->scene()));
|
||||
|
||||
@ -69,6 +69,7 @@ class SessionStream : public Object, public ClientControllerInterface {
|
||||
float z);
|
||||
void PlaySound(SceneSound* sound, float volume);
|
||||
void EmitBGDynamics(const base::BGDynamicsEmission& e);
|
||||
void EmitCameraShake(float intensity);
|
||||
auto GetSoundID(SceneSound* s) -> int64_t;
|
||||
auto GetMaterialID(Material* m) -> int64_t;
|
||||
void ScreenMessageBottom(const std::string& val, float r, float g, float b);
|
||||
|
||||
@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
|
||||
namespace ballistica {
|
||||
|
||||
// These are set automatically via script; don't modify them here.
|
||||
const int kEngineBuildNumber = 21447;
|
||||
const int kEngineBuildNumber = 21453;
|
||||
const char* kEngineVersion = "1.7.28";
|
||||
const int kEngineApiVersion = 8;
|
||||
|
||||
|
||||
@ -248,24 +248,24 @@ void EventLoop::WaitForNextEvent_(bool single_cycle) {
|
||||
}
|
||||
}
|
||||
|
||||
void EventLoop::LoopUpkeep_(bool single_cycle) {
|
||||
assert(g_core);
|
||||
// Keep our autorelease pool clean on mac/ios
|
||||
// FIXME: Should define a CorePlatform::ThreadHelper or something
|
||||
// so we don't have platform-specific code here.
|
||||
#if BA_XCODE_BUILD
|
||||
// Let's not do autorelease pools when being called ad-hoc,
|
||||
// since in that case we're part of another run loop
|
||||
// (and its crashing on drain for some reason)
|
||||
if (!single_cycle) {
|
||||
if (auto_release_pool_) {
|
||||
g_core->platform->DrainAutoReleasePool(auto_release_pool_);
|
||||
auto_release_pool_ = nullptr;
|
||||
}
|
||||
auto_release_pool_ = g_core->platform->NewAutoReleasePool();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// void EventLoop::LoopUpkeep_(bool single_cycle) {
|
||||
// assert(g_core);
|
||||
// // Keep our autorelease pool clean on mac/ios
|
||||
// // FIXME: Should define a CorePlatform::ThreadHelper or something
|
||||
// // so we don't have platform-specific code here.
|
||||
// #if BA_XCODE_BUILD
|
||||
// // Let's not do autorelease pools when being called ad-hoc,
|
||||
// // since in that case we're part of another run loop
|
||||
// // (and its crashing on drain for some reason)
|
||||
// if (!single_cycle) {
|
||||
// if (auto_release_pool_) {
|
||||
// g_core->platform->DrainAutoReleasePool(auto_release_pool_);
|
||||
// auto_release_pool_ = nullptr;
|
||||
// }
|
||||
// auto_release_pool_ = g_core->platform->NewAutoReleasePool();
|
||||
// }
|
||||
// #endif
|
||||
// }
|
||||
|
||||
void EventLoop::RunToCompletion() { Run_(false); }
|
||||
void EventLoop::RunSingleCycle() { Run_(true); }
|
||||
@ -273,7 +273,7 @@ void EventLoop::RunSingleCycle() { Run_(true); }
|
||||
void EventLoop::Run_(bool single_cycle) {
|
||||
assert(g_core);
|
||||
while (true) {
|
||||
LoopUpkeep_(single_cycle);
|
||||
// LoopUpkeep_(single_cycle);
|
||||
|
||||
WaitForNextEvent_(single_cycle);
|
||||
|
||||
|
||||
@ -118,7 +118,7 @@ class EventLoop {
|
||||
auto CheckPushRunnableSafety_() -> bool;
|
||||
void SetInternalThreadName_(const std::string& name);
|
||||
void WaitForNextEvent_(bool single_cycle);
|
||||
void LoopUpkeep_(bool single_cycle);
|
||||
// void LoopUpkeep_(bool single_cycle);
|
||||
void LogThreadMessageTally_(
|
||||
std::vector<std::pair<LogLevel, std::string>>* log_entries);
|
||||
void PushLocalRunnable_(Runnable* runnable, bool* completion_flag);
|
||||
@ -155,32 +155,26 @@ class EventLoop {
|
||||
|
||||
void BootstrapThread_();
|
||||
|
||||
bool bootstrapped_{};
|
||||
bool writing_tally_{};
|
||||
bool suspended_{};
|
||||
bool done_{};
|
||||
bool acquires_python_gil_{};
|
||||
EventLoopID identifier_{EventLoopID::kInvalid};
|
||||
millisecs_t last_suspend_time_{};
|
||||
int listen_sd_{};
|
||||
int messages_since_suspended_{};
|
||||
millisecs_t last_suspended_message_report_time_{};
|
||||
bool done_{};
|
||||
ThreadSource source_;
|
||||
int listen_sd_{};
|
||||
std::thread::id thread_id_{};
|
||||
EventLoopID identifier_{EventLoopID::kInvalid};
|
||||
millisecs_t last_complaint_time_{};
|
||||
bool acquires_python_gil_{};
|
||||
|
||||
// FIXME: Should generalize this to some sort of PlatformThreadData class.
|
||||
#if BA_XCODE_BUILD
|
||||
void* auto_release_pool_{};
|
||||
#endif
|
||||
|
||||
bool bootstrapped_{};
|
||||
ThreadSource source_;
|
||||
std::thread::id thread_id_{};
|
||||
std::list<std::pair<Runnable*, bool*>> runnables_;
|
||||
std::list<Runnable*> suspend_callbacks_;
|
||||
std::list<Runnable*> unsuspend_callbacks_;
|
||||
std::condition_variable thread_message_cv_;
|
||||
std::condition_variable client_listener_cv_;
|
||||
std::mutex thread_message_mutex_;
|
||||
std::list<ThreadMessage_> thread_messages_;
|
||||
std::condition_variable client_listener_cv_;
|
||||
std::mutex client_listener_mutex_;
|
||||
std::list<std::vector<char>> data_to_client_;
|
||||
PyThreadState* py_thread_state_{};
|
||||
|
||||
@ -68,7 +68,7 @@ class Graphics;
|
||||
///
|
||||
/// Category: Enums
|
||||
///
|
||||
enum class InputType {
|
||||
enum class InputType : uint8_t {
|
||||
kUpDown = 2,
|
||||
kLeftRight,
|
||||
kJumpPress,
|
||||
@ -111,7 +111,7 @@ enum class InputType {
|
||||
///
|
||||
/// 'hard' leads to the process exiting. This generally should be avoided
|
||||
/// on platforms such as mobile.
|
||||
enum class QuitType {
|
||||
enum class QuitType : uint8_t {
|
||||
kSoft,
|
||||
kBack,
|
||||
kHard,
|
||||
@ -139,7 +139,7 @@ typedef int64_t TimerMedium;
|
||||
/// 'small' is used primarily for phones or other small devices where
|
||||
/// content needs to be presented as large and clear in order to remain
|
||||
/// readable from an average distance.
|
||||
enum class UIScale {
|
||||
enum class UIScale : uint8_t {
|
||||
kLarge,
|
||||
kMedium,
|
||||
kSmall,
|
||||
@ -162,7 +162,7 @@ enum class UIScale {
|
||||
/// 'real' time is mostly based on clock time, with a few exceptions. It may
|
||||
/// not advance while the app is backgrounded for instance. (the engine
|
||||
/// attempts to prevent single large time jumps from occurring)
|
||||
enum class TimeType {
|
||||
enum class TimeType : uint8_t {
|
||||
kSim,
|
||||
kBase,
|
||||
kReal,
|
||||
@ -173,7 +173,7 @@ enum class TimeType {
|
||||
/// Specifies the format time values are provided in.
|
||||
///
|
||||
/// Category: Enums
|
||||
enum class TimeFormat {
|
||||
enum class TimeFormat : uint8_t {
|
||||
kSeconds,
|
||||
kMilliseconds,
|
||||
kLast // Sentinel.
|
||||
@ -183,7 +183,7 @@ enum class TimeFormat {
|
||||
/// Permissions that can be requested from the OS.
|
||||
///
|
||||
/// Category: Enums
|
||||
enum class Permission {
|
||||
enum class Permission : uint8_t {
|
||||
kStorage,
|
||||
kLast // Sentinel.
|
||||
};
|
||||
@ -192,7 +192,7 @@ enum class Permission {
|
||||
/// Special characters the game can print.
|
||||
///
|
||||
/// Category: Enums
|
||||
enum class SpecialChar {
|
||||
enum class SpecialChar : uint8_t {
|
||||
kDownArrow,
|
||||
kUpArrow,
|
||||
kLeftArrow,
|
||||
@ -288,7 +288,7 @@ enum class SpecialChar {
|
||||
};
|
||||
|
||||
/// Python exception types we can raise from our own exceptions.
|
||||
enum class PyExcType {
|
||||
enum class PyExcType : uint8_t {
|
||||
kRuntime,
|
||||
kAttribute,
|
||||
kIndex,
|
||||
@ -306,7 +306,7 @@ enum class PyExcType {
|
||||
kWidgetNotFound
|
||||
};
|
||||
|
||||
enum class LogLevel {
|
||||
enum class LogLevel : uint8_t {
|
||||
kDebug,
|
||||
kInfo,
|
||||
kWarning,
|
||||
@ -314,7 +314,7 @@ enum class LogLevel {
|
||||
kCritical,
|
||||
};
|
||||
|
||||
enum class ThreadSource {
|
||||
enum class ThreadSource : uint8_t {
|
||||
/// Spin up a new thread for the event loop.
|
||||
kCreate,
|
||||
/// Wrap the event loop around the current thread.
|
||||
@ -323,7 +323,7 @@ enum class ThreadSource {
|
||||
|
||||
/// Used for thread identification.
|
||||
/// Mostly just for debugging.
|
||||
enum class EventLoopID {
|
||||
enum class EventLoopID : uint8_t {
|
||||
kInvalid,
|
||||
kLogic,
|
||||
kAssets,
|
||||
|
||||
@ -143,6 +143,12 @@ class ServerConfig:
|
||||
# queue spamming attacks.
|
||||
enable_queue: bool = True
|
||||
|
||||
# Protocol version we host with. Currently the default is 33 which
|
||||
# still allows older 1.4 game clients to connect. Explicitly setting
|
||||
# to 35 no longer allows those clients but adds/fixes a few things
|
||||
# such as making camera shake properly work in net games.
|
||||
protocol_version: int | None = None
|
||||
|
||||
# (internal) stress-testing mode.
|
||||
stress_test_players: int | None = None
|
||||
|
||||
|
||||
@ -601,6 +601,7 @@ def _get_server_config_raw_contents(projroot: str) -> str:
|
||||
|
||||
def _get_server_config_template_yaml(projroot: str) -> str:
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
import yaml
|
||||
|
||||
lines_in = _get_server_config_raw_contents(projroot).splitlines()
|
||||
@ -664,6 +665,8 @@ def _get_server_config_template_yaml(projroot: str) -> str:
|
||||
vval = 'https://mystatssite.com/showstats?player=${ACCOUNT}'
|
||||
elif vname == 'admins':
|
||||
vval = ['pb-yOuRAccOuNtIdHErE', 'pb-aNdMayBeAnotherHeRE']
|
||||
elif vname == 'protocol_version':
|
||||
vval = 35
|
||||
lines_out += [
|
||||
'#' + l for l in yaml.dump({vname: vval}).strip().splitlines()
|
||||
]
|
||||
|
||||
@ -59,6 +59,10 @@ def _gen_enums(infilename: str) -> str:
|
||||
|
||||
def _parse_name(lines: list[str], lnum: int) -> str:
|
||||
bits = lines[lnum].split(' ')
|
||||
|
||||
# Special case: allow for specifying underlying type.
|
||||
if len(bits) == 6 and bits[3] == ':' and bits[4] in {'uint8_t', 'uint16_t'}:
|
||||
bits = [bits[0], bits[1], bits[2], bits[5]]
|
||||
if (
|
||||
len(bits) != 4
|
||||
or bits[0] != 'enum'
|
||||
|
||||
@ -29,7 +29,7 @@ from batools.spinoff._state import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable
|
||||
from typing import Callable, Iterable, Any
|
||||
|
||||
from batools.project import ProjectUpdater
|
||||
|
||||
@ -54,6 +54,7 @@ class SpinoffContext:
|
||||
OVERRIDE = 'override'
|
||||
DIFF = 'diff'
|
||||
BACKPORT = 'backport'
|
||||
DESCRIBE_PATH = 'describe_path'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -66,6 +67,7 @@ class SpinoffContext:
|
||||
override_paths: list[str] | None = None,
|
||||
backport_file: str | None = None,
|
||||
auto_backport: bool = False,
|
||||
describe_path: str | None = None,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-statements
|
||||
|
||||
@ -82,6 +84,7 @@ class SpinoffContext:
|
||||
self._override_paths = override_paths
|
||||
self._backport_file = backport_file
|
||||
self._auto_backport = auto_backport
|
||||
self._describe_path = describe_path
|
||||
|
||||
self._project_updater: ProjectUpdater | None = None
|
||||
|
||||
@ -278,10 +281,10 @@ class SpinoffContext:
|
||||
self._src_omit_feature_sets,
|
||||
) = self._calc_src_retain_omit_feature_sets()
|
||||
|
||||
# Generate a version of src_omit_paths that includes our feature-set
|
||||
# omissions. Basically, omitting a feature set simply omits
|
||||
# particular names at a few particular places.
|
||||
# Generate a version of src_omit_paths that includes some extra values
|
||||
self._src_omit_paths_expanded = self.src_omit_paths.copy()
|
||||
# Include feature-set omissions. Basically, omitting a feature
|
||||
# set simply omits particular names at a few particular places.
|
||||
self._add_feature_set_omit_paths(self._src_omit_paths_expanded)
|
||||
|
||||
# Create a version of dst-write-paths that also includes filtered
|
||||
@ -362,6 +365,7 @@ class SpinoffContext:
|
||||
def run(self) -> None:
|
||||
"""Do the thing."""
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
|
||||
self._read_state()
|
||||
|
||||
@ -400,6 +404,20 @@ class SpinoffContext:
|
||||
# Ignore anything under omitted paths/names.
|
||||
self._filter_src_git_file_list()
|
||||
|
||||
# Go through the final set of files we're syncing to dst and
|
||||
# make sure none of them fall under our unchecked-paths list.
|
||||
# That would mean we are writing a file but we're also declaring
|
||||
# that we don't care if anyone else writes that file, which
|
||||
# could lead to ambiguous/dangerous situations where spinoff as
|
||||
# well as some command on dst write to the same file.
|
||||
for path in self._src_git_files:
|
||||
if _any_path_contains(self.src_unchecked_paths, path):
|
||||
self._src_error_entities[path] = (
|
||||
'Synced file falls under src_unchecked_paths, which'
|
||||
" is not allowed. Either don't sync the file or carve"
|
||||
' it out from src_unchecked_paths.'
|
||||
)
|
||||
|
||||
# Now map whatever is left to paths in dst.
|
||||
self._dst_git_files = set(
|
||||
self._filter_path(s) for s in self._src_git_files
|
||||
@ -454,8 +472,10 @@ class SpinoffContext:
|
||||
)
|
||||
raise self.BackportInProgressError
|
||||
|
||||
if self._mode is self.Mode.DESCRIBE_PATH:
|
||||
self._do_describe_path()
|
||||
# If anything is off, print errors; otherwise actually do the deed.
|
||||
if self._src_error_entities or self._dst_error_entities:
|
||||
elif self._src_error_entities or self._dst_error_entities:
|
||||
self._print_error_entities()
|
||||
else:
|
||||
if (
|
||||
@ -511,6 +531,76 @@ class SpinoffContext:
|
||||
if self._mode is self.Mode.UPDATE or self._mode is self.Mode.OVERRIDE:
|
||||
self._write_gitignore()
|
||||
|
||||
def _do_describe_path(self) -> None:
|
||||
assert self._describe_path is not None
|
||||
path = self._describe_path
|
||||
|
||||
# Currently operating only on dst paths.
|
||||
if path.startswith('/') and not path.startswith(self._dst_root):
|
||||
raise CleanError('Please supply a path in the dst dir.')
|
||||
|
||||
# Allow abs paths.
|
||||
path = path.removeprefix(f'{self._dst_root}/')
|
||||
|
||||
if self._src_error_entities or self._dst_error_entities:
|
||||
print(
|
||||
f'{Clr.RED}Note: Errors are present;'
|
||||
f' this info may not be fully accurate.{Clr.RST}'
|
||||
)
|
||||
print(f'{Clr.BLD}dstpath: {Clr.BLU}{path}{Clr.RST}')
|
||||
|
||||
def _printval(name: Any, val: Any) -> None:
|
||||
print(f' {name}: {Clr.BLU}{val}{Clr.RST}')
|
||||
|
||||
_printval('exists', os.path.exists(os.path.join(self._dst_root, path)))
|
||||
|
||||
# Adapted from code in _check_spinoff_managed_dirs.
|
||||
managed = False
|
||||
unchecked = False
|
||||
git_mirrored = False
|
||||
|
||||
dstrootsl = f'{self._dst_root}/'
|
||||
assert self._spinoff_managed_dirs is not None
|
||||
for rdir in self._spinoff_managed_dirs:
|
||||
for root, dirnames, fnames in os.walk(
|
||||
os.path.join(self._dst_root, rdir),
|
||||
topdown=True,
|
||||
):
|
||||
# Completely ignore ignore-names in both dirs and files
|
||||
# and cruft-file names in files.
|
||||
for dirname in dirnames.copy():
|
||||
if dirname in self.ignore_names:
|
||||
dirnames.remove(dirname)
|
||||
for fname in fnames.copy():
|
||||
if (
|
||||
fname in self.ignore_names
|
||||
or fname in self.cruft_file_names
|
||||
):
|
||||
fnames.remove(fname)
|
||||
|
||||
for fname in fnames:
|
||||
dst_path_full = os.path.join(root, fname)
|
||||
assert dst_path_full.startswith(dstrootsl)
|
||||
dst_path = dst_path_full.removeprefix(dstrootsl)
|
||||
if dst_path == path:
|
||||
managed = True
|
||||
if _any_path_contains(self._dst_unchecked_paths, dst_path):
|
||||
unchecked = True
|
||||
if _any_path_contains(self.git_mirrored_paths, dst_path):
|
||||
git_mirrored = True
|
||||
_printval(
|
||||
'spinoff-managed',
|
||||
managed,
|
||||
)
|
||||
_printval(
|
||||
'unchecked',
|
||||
unchecked,
|
||||
)
|
||||
_printval(
|
||||
'git-mirrored',
|
||||
git_mirrored,
|
||||
)
|
||||
|
||||
def _apply_project_configs(self) -> None:
|
||||
# pylint: disable=exec-used
|
||||
try:
|
||||
@ -744,9 +834,6 @@ class SpinoffContext:
|
||||
|
||||
# Strip out any sections frames by our strip-begin/end tags.
|
||||
|
||||
# strip_tag_pairs: list[tuple[str, str]] = []
|
||||
# print('HELLO WORLD')
|
||||
|
||||
def _first_index_containing_string(
|
||||
items: list[str], substring: str
|
||||
) -> int | None:
|
||||
@ -795,7 +882,7 @@ class SpinoffContext:
|
||||
'make any edits in source project)'
|
||||
)
|
||||
lines = self.default_filter_text(text).splitlines()
|
||||
return '\n'.join(lines[:1] + ['', blurb] + lines[1:])
|
||||
return '\n'.join([blurb, ' '] + lines)
|
||||
if 'Jenkinsfile' in src_path:
|
||||
blurb = (
|
||||
'// THIS FILE IS AUTOGENERATED BY SPINOFF;'
|
||||
@ -1028,8 +1115,10 @@ class SpinoffContext:
|
||||
"""Print info about entity errors encountered."""
|
||||
print(
|
||||
'\nSpinoff Error(s) Found:\n'
|
||||
" Tip: to resolve 'spinoff-managed file modified' errors,\n"
|
||||
" use the 'backport' subcommand.\n",
|
||||
" Tips: To resolve 'spinoff-managed file modified' errors,\n"
|
||||
" use the 'backport' subcommand.\n"
|
||||
" To debug other issues, try the 'describe-path'"
|
||||
' subcommand.\n',
|
||||
file=sys.stderr,
|
||||
)
|
||||
for key, val in sorted(self._src_error_entities.items()):
|
||||
@ -1046,7 +1135,18 @@ class SpinoffContext:
|
||||
print('')
|
||||
|
||||
def _validate_final_lists(self) -> None:
|
||||
"""Make sure we never delete the few files we're letting git store."""
|
||||
"""Check some last things on our entities lists before we update."""
|
||||
|
||||
# Go through the final set of files we're syncing to dst and
|
||||
# make sure none of them fall under our unchecked-paths list.
|
||||
# That would mean we are writing a file but we're also declaring
|
||||
# that we don't care if anyone else writes that file, which
|
||||
# could lead to ambiguous/dangerous situations where spinoff as
|
||||
# well as some command on dst write to the same file.
|
||||
# print('CHECKING', self._src_copy_entities)
|
||||
# for ent in self._src_copy_entities:
|
||||
# if _any_path_contains(self._dst_unchecked_paths, ent):
|
||||
# raise CleanError('FOUND BAD PATH', ent)
|
||||
|
||||
for ent in self._dst_purge_entities.copy():
|
||||
if _any_path_contains(self.git_mirrored_paths, ent):
|
||||
@ -1806,7 +1906,8 @@ class SpinoffContext:
|
||||
)
|
||||
|
||||
def _filter_src_git_file_list(self) -> None:
|
||||
# Crate a filtered version of src git files based on our omit entries.
|
||||
# Create a filtered version of src git files based on our omit
|
||||
# entries.
|
||||
out = set[str]()
|
||||
assert self._src_git_files is not None
|
||||
for gitpath in self._src_git_files:
|
||||
@ -1958,7 +2059,7 @@ class SpinoffContext:
|
||||
|
||||
# In strict mode we want it to always be an error if dst mod-time
|
||||
# varies from the version we wrote (we want to track down anyone
|
||||
# writing to our files who is not us).
|
||||
# writing to our managed files who is not us).
|
||||
# Note that we need to ignore git-mirrored-paths because git might
|
||||
# be mucking with modtimes itself.
|
||||
if (
|
||||
|
||||
@ -32,6 +32,7 @@ class Command(Enum):
|
||||
CLEAN_CHECK = 'cleancheck'
|
||||
OVERRIDE = 'override'
|
||||
DIFF = 'diff'
|
||||
DESCRIBE_PATH = 'describe-path'
|
||||
BACKPORT = 'backport'
|
||||
CREATE = 'create'
|
||||
ADD_SUBMODULE_PARENT = 'add-submodule-parent'
|
||||
@ -94,6 +95,8 @@ def _main() -> None:
|
||||
single_run_mode = SpinoffContext.Mode.CLEAN_CHECK
|
||||
elif cmd is Command.DIFF:
|
||||
single_run_mode = SpinoffContext.Mode.DIFF
|
||||
elif cmd is Command.DESCRIBE_PATH:
|
||||
single_run_mode = SpinoffContext.Mode.DESCRIBE_PATH
|
||||
elif cmd is Command.OVERRIDE:
|
||||
_do_override(src_root, dst_root)
|
||||
elif cmd is Command.BACKPORT:
|
||||
@ -115,6 +118,12 @@ def _main() -> None:
|
||||
assert_never(cmd)
|
||||
|
||||
if single_run_mode is not None:
|
||||
from efrotools import extract_flag
|
||||
|
||||
args = sys.argv[2:]
|
||||
force = extract_flag(args, '--force')
|
||||
verbose = extract_flag(args, '--verbose')
|
||||
print_full_lists = extract_flag(args, '--full')
|
||||
if src_root is None:
|
||||
if '--soft' in sys.argv:
|
||||
return
|
||||
@ -123,6 +132,15 @@ def _main() -> None:
|
||||
' you appear to be in a src project.'
|
||||
" To silently no-op in this case, pass '--soft'."
|
||||
)
|
||||
|
||||
describe_path: str | None
|
||||
if single_run_mode is SpinoffContext.Mode.DESCRIBE_PATH:
|
||||
if len(args) != 1:
|
||||
raise CleanError(f'Expected a single path arg; got {args}.')
|
||||
describe_path = args[0]
|
||||
else:
|
||||
describe_path = None
|
||||
|
||||
# SpinoffContext should never be relying on relative paths, so let's
|
||||
# keep ourself honest by making sure.
|
||||
os.chdir('/')
|
||||
@ -130,9 +148,10 @@ def _main() -> None:
|
||||
src_root,
|
||||
dst_root,
|
||||
single_run_mode,
|
||||
force='--force' in sys.argv,
|
||||
verbose='--verbose' in sys.argv,
|
||||
print_full_lists='--full' in sys.argv,
|
||||
force=force,
|
||||
verbose=verbose,
|
||||
print_full_lists=print_full_lists,
|
||||
describe_path=describe_path,
|
||||
).run()
|
||||
|
||||
|
||||
@ -581,6 +600,8 @@ def _print_available_commands() -> None:
|
||||
'Remove files from spinoff, leaving local copies in place.\n'
|
||||
f' {bgn}backport [file]{end} '
|
||||
'Help get changes to spinoff dst files back to src.\n'
|
||||
f' {bgn}describe-path [path]{end}'
|
||||
' Tells whether a path is spinoff-managed/etc.\n'
|
||||
f' {bgn}create [name, path]{end} '
|
||||
'Create a new spinoff project based on this src one.\n'
|
||||
' Name should be passed in CamelCase form.\n'
|
||||
|
||||
@ -165,7 +165,6 @@ class _Outputter:
|
||||
)
|
||||
return value if self._create else None
|
||||
|
||||
# noinspection PyPep8
|
||||
if origin is typing.Union or origin is types.UnionType:
|
||||
# Currently, the only unions we support are None/Value
|
||||
# (translated from Optional), which we verified on prep.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user