diff --git a/.efrocachemap b/.efrocachemap
index a48f4efd..b656e348 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -421,21 +421,21 @@
"build/assets/ba_data/audio/zoeOw.ogg": "74befe45a8417e95b6a2233c51992a26",
"build/assets/ba_data/audio/zoePickup01.ogg": "48ab8cddfcde36a750856f3f81dd20c8",
"build/assets/ba_data/audio/zoeScream01.ogg": "2b468aedfa8741090247f04eb9e6df55",
- "build/assets/ba_data/data/langdata.json": "c6f94f9c1dc833c537d16672d9018b94",
+ "build/assets/ba_data/data/langdata.json": "f200cdf431b9494d8b96cdd47e950dd1",
"build/assets/ba_data/data/languages/arabic.json": "00ba700de6c672a56658a6bd1ad27523",
- "build/assets/ba_data/data/languages/belarussian.json": "7fe38341815ca6ff4d95224196e7a67e",
+ "build/assets/ba_data/data/languages/belarussian.json": "40883823367f04c5a2403a96525bfcc3",
"build/assets/ba_data/data/languages/chinese.json": "5761468d25f2bd4e79921826cebd572b",
"build/assets/ba_data/data/languages/chinesetraditional.json": "f858da49be0a5374157c627857751078",
"build/assets/ba_data/data/languages/croatian.json": "766532c67af5bd0144c2d63cab0516fa",
- "build/assets/ba_data/data/languages/czech.json": "93c5fe0d884d95435da6c675f64e30e0",
+ "build/assets/ba_data/data/languages/czech.json": "cd21ad8c6b8e9ed700284cf1e1aecbf8",
"build/assets/ba_data/data/languages/danish.json": "3fd69080783d5c9dcc0af737f02b6f1e",
"build/assets/ba_data/data/languages/dutch.json": "22b44a33bf81142ba2befad14eb5746e",
- "build/assets/ba_data/data/languages/english.json": "bd43b77b1ccca059573acbde148b4767",
+ "build/assets/ba_data/data/languages/english.json": "6a3fab4fb8b2879e00ed9877709bf504",
"build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880",
- "build/assets/ba_data/data/languages/filipino.json": "afbda3adf14555e1567ee63c32e340e7",
+ "build/assets/ba_data/data/languages/filipino.json": "6f4051ce78861a4666f4978d6f9a0ed2",
"build/assets/ba_data/data/languages/french.json": "49ff6d211537b8003b8241438dca661d",
"build/assets/ba_data/data/languages/german.json": "450fa41ae264f29a5d1af22143d0d0ad",
- "build/assets/ba_data/data/languages/gibberish.json": "9aae526303a22372fe9b4cf1781520ef",
+ "build/assets/ba_data/data/languages/gibberish.json": "25fcb5130fae56985bee175aa19f86a2",
"build/assets/ba_data/data/languages/greek.json": "287c0ec437b38772284ef9d3e4fb2fc3",
"build/assets/ba_data/data/languages/hindi.json": "8848f6b0caec0fcf9d85bc6e683809ec",
"build/assets/ba_data/data/languages/hungarian.json": "796a290a8c44a1e7635208c2ff5fdc6e",
@@ -445,12 +445,12 @@
"build/assets/ba_data/data/languages/malay.json": "832562ce997fc70704b9234c95fb2e38",
"build/assets/ba_data/data/languages/persian.json": "d742f4a6d3c3555031102b21abdcbb5b",
"build/assets/ba_data/data/languages/polish.json": "b9a58b70ed5e99d8b7fa2392b2eb0cda",
- "build/assets/ba_data/data/languages/portuguese.json": "556af4e8170356ad239412e1743e20d5",
+ "build/assets/ba_data/data/languages/portuguese.json": "e3adc6c04486d21e84019a0b03ce11b1",
"build/assets/ba_data/data/languages/romanian.json": "aeebdd54f65939c2facc6ac50c117826",
"build/assets/ba_data/data/languages/russian.json": "e120993371f52edd2d99f2236188933c",
"build/assets/ba_data/data/languages/serbian.json": "d7452dd72ac0e51680cb39b5ebaa1c69",
"build/assets/ba_data/data/languages/slovak.json": "27962d53dc3f7dd4e877cd40faafeeef",
- "build/assets/ba_data/data/languages/spanish.json": "80ea58bd3295a0252b7fdac9154aa22f",
+ "build/assets/ba_data/data/languages/spanish.json": "1d14210b4eefb48130608bd0495b7900",
"build/assets/ba_data/data/languages/swedish.json": "5142a96597d17d8344be96a603da64ac",
"build/assets/ba_data/data/languages/tamil.json": "b4de1a2851afe4869c82e9acd94cd89c",
"build/assets/ba_data/data/languages/thai.json": "77755219bbf5fb7eea0d6b226684f403",
@@ -4060,50 +4060,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": "9fe23e06319e4e256b9fa88814a14afa",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "4306acae21ce88235f9d1589086866e7",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "75e4f7d3a3df67dedd079ec3f4441094",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "bd5eda13f239b81886ac80596d6ade73",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "0805235a92dd91f96d43ea54575eecac",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "07589a61b11cbc5fca0bbc8b7fc1c955",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "f28629761060c8152168b6792b71adae",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "1cfd1a33474cdb31834994f626385ed0",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "d50879a92d9d344c376f6f196d78d1be",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "78cd0edf2698f197f2acd80ca364fae7",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "d656f47118ebc3af57c40423cb258bc8",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "75df540b27779342a7c696e1bdbe593f",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "76f0dfacaa9ea67e45e8ccf3bb3bc1c6",
- "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "2acc754bed825a9265e0621dc09899e0",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "62c2b6190de8784ea8750ea50e6a2304",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "e57358fd9a948a8ce82a54cdd5c766fc",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "d36e3303e13049eae5e7ec19861d300e",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "46971a2ca1e3021e52ea5d0f4938d2ff",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "19ebd36613cf62c4bd50e70b93371368",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "49ef5905b6e9e1a9caaed3d1c1da4ea5",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "c22901e06e88a55cce0b4e08bbf41a4c",
- "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "a27963487e346338e4c216bd4fbb9e2a",
- "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "c22901e06e88a55cce0b4e08bbf41a4c",
- "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "a27963487e346338e4c216bd4fbb9e2a",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "2663c888aec894656bd8c49932bd7729",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "5e57d12a3cfcfbc47b0293c3cb9fdca9",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "2663c888aec894656bd8c49932bd7729",
- "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "5e57d12a3cfcfbc47b0293c3cb9fdca9",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "0f7dbe6fb3e28a51904aa822b509da0f",
- "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "081b766945b52460a4f1afc01faa0652",
- "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "0f7dbe6fb3e28a51904aa822b509da0f",
- "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "081b766945b52460a4f1afc01faa0652",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "ad609c63f68417d5211bbfb23ce4affe",
- "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "852fe46c736082611a831a618923c241",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "36fbda7829ed5c2862c34feb09b03402",
- "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "852fe46c736082611a831a618923c241",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "38b4b5b85a9bafdb76222d0f0c962b06",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "9a8af3d217bcb0bacfaed4c30dd5f42e",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "6d10ca306f60d66efb4942636e4955d6",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "1c65d36e4420ed79380dc8c041c94a8b",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "9b1b72f3d41c89a6b06288be63e8f40a",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "e0f2eb8ea024bc88e999b9dc16317fd4",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "42be1225757328f432d91de950444ba0",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "05bc2832cc0c9fba308668fc1a6d3b0f",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "6135aeb242afaf9d1114810a67c89cec",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "bbbbb14d42ed6eb0c5eb56867b7fb870",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "cd28f9cc4652736a31c677fc4e5dbaf1",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "239c608cc52c0320210e56ad6abe57a5",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "e76d67cacf1393d33796d6b6b1bf1413",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "a7eaa8dc4d859ef7a735483b04ccec4a",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "7a2eef42da34a35ddcc2fd7c66843b1b",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "694599ac6a967b2ed383b27bf8093e5b",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "c91cbab6a07affa22e0612210f8b807c",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "d460f7a3909f92d5dbf752e4521a9fbc",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "0a0abfe75bc987e7b65a3cfa106e8353",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "8f21405b29f2b2ab01323d711492cca0",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "96dc73e819f41f99a1b2dbb45f79d551",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "c79ac51cd2deabb1c2d0acddeaf81c30",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "f06ec14e8c3106be9df91af7da621dc9",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "f389f9a7b1afc81f76787722340cfa9c",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "c7dab78aac11cb1430d8456d5d48107a",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "67e29852dfee2e63e179cfebf608ef26",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "9778f8faf91c9993fbf3015bd4554a87",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "73477bd15b9e3834314fd878c9e108d4",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "fb9b8443c1b4cccad749df7d6328220f",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "384fb7fd55ad5a6cdbb662da1ec402ab",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "fb9b8443c1b4cccad749df7d6328220f",
+ "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "384fb7fd55ad5a6cdbb662da1ec402ab",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "bc7d0811bcd87156ebf5292a38a1c350",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "bb32f45054b6999300bf8b41d6a4b402",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "bc7d0811bcd87156ebf5292a38a1c350",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "bb32f45054b6999300bf8b41d6a4b402",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "8d9a1505bf397f4902baabed7c1cf438",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "f4d9c115e22dd81e36d1c5baeac8d848",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "8d9a1505bf397f4902baabed7c1cf438",
+ "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "f4d9c115e22dd81e36d1c5baeac8d848",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "fb72c92ec6ec0e1c8f4ced32abd86505",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "131aab20cfe77fe89c3f452a855f1e68",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "ee10cdc9f9a861e2be0f1a208c0ca0fe",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "131aab20cfe77fe89c3f452a855f1e68",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "678fabc6dfd6f401ee8942d088ee9181",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "e092d2aed8464a61a623d79ca25308d8",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "6b658f49be396ad645c5e57464739a3b",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "9d79a56403a6d806ff131a7de664dfa7",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "e831a26d2c28e862d51e24393d158c99",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "46fe1c89bcc75c781729ec9e5491c610",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "9c6278d7df3ce4db2ffe7794a0fd35b7",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "110c35a17b462864075800756b5e541a",
"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": "72bfed2cce8ff19741989dec28302f3f",
@@ -4112,7 +4112,7 @@
"src/ballistica/core/mgen/pyembed/binding_core.inc": "9d0a3c9636138e35284923e0c8311c69",
"src/ballistica/core/mgen/pyembed/env.inc": "8be46e5818f360d10b7b0224a9e91d07",
"src/ballistica/core/mgen/python_modules_monolithic.h": "fb967ed1c7db0c77d8deb4f00a7103c5",
- "src/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc": "d80f970053099b3044204bfe29ddefce",
+ "src/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc": "c25b263f2a31fb5ebe057db07d144879",
"src/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc": "44a45492db057bf7f7158c3b0fa11f0f",
- "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "8f4c2070174bdc2fbf735180394d7b3a"
+ "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "f5f054050d2b2fcd3763a4833fb32269"
}
\ No newline at end of file
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
new file mode 100644
index 00000000..ebeea7ef
--- /dev/null
+++ b/.github/workflows/cd.yml
@@ -0,0 +1,178 @@
+name: CD
+
+on:
+ # Run on pushes and pull-requests
+ push:
+ pull_request:
+
+jobs:
+ make_linux_x86_64_gui_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-gui-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: linux_x86_64_gui_(debug)
+ path: build/prefab/full/linux_x86_64_gui
+ make_linux_x86_64_server_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-server-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: linux_x86_64_server_(debug)
+ path: build/prefab/full/linux_x86_64_server
+ make_linux_arm64_gui_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-linux-arm64-gui-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: linux_arm64_gui_(debug)
+ path: build/prefab/full/linux_arm64_gui
+ make_linux_arm64_server_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-linux-arm64-server-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: linux_arm64_server_(debug)
+ path: build/prefab/full/linux_arm64_server
+ make_mac_x86_64_gui_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-mac-x86-64-gui-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: mac_x86_64_gui_(debug)
+ path: build/prefab/full/mac_x86_64_gui
+ make_mac_x86_64_server_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-mac-x86-64-server-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: mac_x86_64_server_(debug)
+ path: build/prefab/full/mac_x86_64_server
+ make_mac_arm64_gui_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-mac-arm64-gui-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: mac_arm64_gui_(debug)
+ path: build/prefab/full/mac_arm64_gui
+ make_mac_arm64_server_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-mac-arm64-server-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: mac_arm64_server_(debug)
+ path: build/prefab/full/mac_arm64_server
+ make_windows_x86_gui_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-windows-x86-gui-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: windows_x86_gui_(debug)
+ path: build/prefab/full/windows_x86_gui
+ make_windows_x86_server_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-windows-x86-server-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: windows_x86_server_(debug)
+ path: build/prefab/full/windows_x86_server
diff --git a/.idea/ballisticakit.iml b/.idea/ballisticakit.iml
index 4c2844c3..6d3ca9f0 100644
--- a/.idea/ballisticakit.iml
+++ b/.idea/ballisticakit.iml
@@ -21,7 +21,6 @@
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f3ac4a8..96ff9ebd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,10 @@
-### 1.7.30 (build 21636, api 8, 2023-11-30)
+### 1.7.31 (build 21707, api 8, 2023-12-13)
+- Added `bascenev1.get_connection_to_host_info_2()` which is an improved
+ type-safe version of `bascenev1.get_connection_to_host_info()`.
+- There is now a link to the official Discord server in the About section
+ (thanks EraOSBeta!).
+
+### 1.7.30 (build 21697, api 8, 2023-12-08)
- Continued work on the big 1.7.28 update.
- Got the Android version back up and running. There's been lots of cleanup and
simplification on the Android layer, cleaning out years of cruft. This should
@@ -15,15 +21,34 @@
- Bundled Android Python has been bumped to version 3.11.6.
- Android app suspend behavior has been revamped. The app should stay running
more often and be quicker to respond when dialogs or other activities
- temporarily pop up in front of it. Please holler if you run into strange side
+ temporarily pop up in front of it. This also allows it to continue playing
+ music over other activities such as Google Play Games
+ Achievements/Leaderboards screens. Please holler if you run into strange side
effects such as the app continuing to play audio when it should not be.
+- Modernized the Android fullscreen setup code when running in Android 11 or
+ newer. The game should now use the whole screen area, including the area
+ around notches or camera cutouts. Please holler if you are seeing any problems
+ related to this.
- (build 21626) Fixed a bug where click/tap locations were incorrect on some
builds when tv-border was on (Thanks for the heads-up Loup(Dliwk's fan)!).
- (build 21631) Fixes an issue where '^^^^^^^^^^^^^' lines in stack traces could
get chopped into tiny bits each on their own line in the dev console.
-- Fixed a longstanding issue where multiple key presses simultaneously could
- cause multiple windows to pop up where only one is expected. Please holler if
- you still see this problem happening anywhere.
+- Hopefully finally fixed a longstanding issue where obscure cases such as
+ multiple key presses simultaneously could cause multiple main menu windows to
+ pop up. Please holler if you still see this problem happening anywhere. Also
+ added a few related safety checks and warnings to help ensure UI code is free
+ from such problems going forward. To make sure your custom UIs are behaving
+ well in this system, do the following two things: 1) any time you call
+ `set_main_menu_window()`, pass your existing main menu window root widget as
+ `from_window`. 2) In any call that can lead to you switching the main menu
+ window, check if your root widget is dead or transitioning out first and abort
+ if it is. See any window in `ui_v1_lib` for examples.
+- (build 21691) Fixed a bug causing touches to not register in some cases on
+ newer Android devices. (Huge thanks to JESWIN A J for helping me track that
+ down!).
+- Temporarily removed the pause-the-game-when-backgrounded behavior for locally
+ hosted games, mainly due to the code being hacky. Will try to restore this
+ functionality in a cleaner way soon.
### 1.7.29 (build 21619, api 8, 2023-11-21)
diff --git a/README.md b/README.md
index 4550a942..20fddc2a 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ height="50" alt="logo">
***-ica***: collection of things relating to a specific theme.
-[](https://github.com/efroemling/ballistica/actions/workflows/ci.yml)
+[](https://github.com/efroemling/ballistica/actions/workflows/ci.yml) [](https://github.com/efroemling/ballistica/actions/workflows/cd.yml)
The Ballistica project is the foundation for
[BombSquad](https://www.froemling.net/apps/bombsquad) and potentially other
@@ -52,7 +52,7 @@ want to keep that spirit alive as the Ballistica project moves forward. Whether
this means making it easier to share mods, organize tournaments, join up with
friends, teach each other some Python, or whatever else. Life is short; let's
play some games. Or make them. Maybe both.
-
+
### Frequently Asked Questions
* **Q: What's with this name? Is it BombSquad or Ballistica?**
@@ -86,4 +86,4 @@ Playstation / My Toaster??**
for more details or the [Ballistica
Downloads](https://ballistica.net/downloads) page for early test builds on
some platforms.
-
+
diff --git a/ballisticakit-cmake/.idea/misc.xml b/ballisticakit-cmake/.idea/misc.xml
index 17581663..f01f08b8 100644
--- a/ballisticakit-cmake/.idea/misc.xml
+++ b/ballisticakit-cmake/.idea/misc.xml
@@ -14,7 +14,6 @@
-
diff --git a/config/spinoffconfig.py b/config/spinoffconfig.py
index 21b06551..26e97c36 100644
--- a/config/spinoffconfig.py
+++ b/config/spinoffconfig.py
@@ -152,7 +152,6 @@ ctx.filter_dirs = {
'ballisticakit-cmake',
'ballisticakit-xcode/BallisticaKit.xcodeproj',
'ballisticakit-ios.xcodeproj',
- 'ballisticakit-mac.xcodeproj',
'config',
'src/assets/pdoc',
}
@@ -195,6 +194,7 @@ ctx.filter_file_names = {
'.projectile',
'.editorconfig',
'ci.yml',
+ 'cd.yml',
'LICENSE',
'cloudtool',
'bacloud',
diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json
index 55998cf7..8a62973f 100644
--- a/src/assets/.asset_manifest_public.json
+++ b/src/assets/.asset_manifest_public.json
@@ -20,7 +20,6 @@
"ba_data/python/babase/__pycache__/_error.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_general.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_hooks.cpython-311.opt-1.pyc",
- "ba_data/python/babase/__pycache__/_keyboard.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_language.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_login.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_math.cpython-311.opt-1.pyc",
@@ -50,7 +49,6 @@
"ba_data/python/babase/_error.py",
"ba_data/python/babase/_general.py",
"ba_data/python/babase/_hooks.py",
- "ba_data/python/babase/_keyboard.py",
"ba_data/python/babase/_language.py",
"ba_data/python/babase/_login.py",
"ba_data/python/babase/_math.py",
@@ -152,6 +150,7 @@
"ba_data/python/bascenev1/__pycache__/_messages.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_multiteamsession.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_music.cpython-311.opt-1.pyc",
+ "ba_data/python/bascenev1/__pycache__/_net.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_nodeactor.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_player.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_playlist.cpython-311.opt-1.pyc",
@@ -186,6 +185,7 @@
"ba_data/python/bascenev1/_messages.py",
"ba_data/python/bascenev1/_multiteamsession.py",
"ba_data/python/bascenev1/_music.py",
+ "ba_data/python/bascenev1/_net.py",
"ba_data/python/bascenev1/_nodeactor.py",
"ba_data/python/bascenev1/_player.py",
"ba_data/python/bascenev1/_playlist.py",
@@ -352,10 +352,12 @@
"ba_data/python/bauiv1/__init__.py",
"ba_data/python/bauiv1/__pycache__/__init__.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1/__pycache__/_hooks.cpython-311.opt-1.pyc",
+ "ba_data/python/bauiv1/__pycache__/_keyboard.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1/__pycache__/_subsystem.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1/__pycache__/_uitypes.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1/__pycache__/onscreenkeyboard.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1/_hooks.py",
+ "ba_data/python/bauiv1/_keyboard.py",
"ba_data/python/bauiv1/_subsystem.py",
"ba_data/python/bauiv1/_uitypes.py",
"ba_data/python/bauiv1/onscreenkeyboard.py",
diff --git a/src/assets/Makefile b/src/assets/Makefile
index a52dda49..1ee71109 100644
--- a/src/assets/Makefile
+++ b/src/assets/Makefile
@@ -178,7 +178,6 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/babase/_error.py \
$(BUILD_DIR)/ba_data/python/babase/_general.py \
$(BUILD_DIR)/ba_data/python/babase/_hooks.py \
- $(BUILD_DIR)/ba_data/python/babase/_keyboard.py \
$(BUILD_DIR)/ba_data/python/babase/_language.py \
$(BUILD_DIR)/ba_data/python/babase/_login.py \
$(BUILD_DIR)/ba_data/python/babase/_math.py \
@@ -237,6 +236,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bascenev1/_messages.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_multiteamsession.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_music.py \
+ $(BUILD_DIR)/ba_data/python/bascenev1/_net.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_nodeactor.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_player.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_playlist.py \
@@ -326,6 +326,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/batemplatefs/_subsystem.py \
$(BUILD_DIR)/ba_data/python/bauiv1/__init__.py \
$(BUILD_DIR)/ba_data/python/bauiv1/_hooks.py \
+ $(BUILD_DIR)/ba_data/python/bauiv1/_keyboard.py \
$(BUILD_DIR)/ba_data/python/bauiv1/_subsystem.py \
$(BUILD_DIR)/ba_data/python/bauiv1/_uitypes.py \
$(BUILD_DIR)/ba_data/python/bauiv1/onscreenkeyboard.py \
@@ -452,7 +453,6 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_error.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_general.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_hooks.cpython-311.opt-1.pyc \
- $(BUILD_DIR)/ba_data/python/babase/__pycache__/_keyboard.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_language.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_login.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_math.cpython-311.opt-1.pyc \
@@ -511,6 +511,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_messages.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_multiteamsession.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_music.cpython-311.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_net.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_nodeactor.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_player.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_playlist.cpython-311.opt-1.pyc \
@@ -600,6 +601,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/batemplatefs/__pycache__/_subsystem.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/__init__.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_hooks.cpython-311.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_keyboard.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_subsystem.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_uitypes.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/onscreenkeyboard.cpython-311.opt-1.pyc \
diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py
index b2a12abe..91b05f35 100644
--- a/src/assets/ba_data/python/babase/__init__.py
+++ b/src/assets/ba_data/python/babase/__init__.py
@@ -154,7 +154,6 @@ from babase._general import (
getclass,
get_type_name,
)
-from babase._keyboard import Keyboard
from babase._language import Lstr, LanguageSubsystem
from babase._login import LoginAdapter, LoginInfo
@@ -261,7 +260,6 @@ __all__ = [
'is_point_in_box',
'is_running_on_fire_tv',
'is_xcode_build',
- 'Keyboard',
'LanguageSubsystem',
'lock_all_input',
'LoginAdapter',
diff --git a/src/assets/ba_data/python/babase/_accountv2.py b/src/assets/ba_data/python/babase/_accountv2.py
index f9617fcb..37914bce 100644
--- a/src/assets/ba_data/python/babase/_accountv2.py
+++ b/src/assets/ba_data/python/babase/_accountv2.py
@@ -186,9 +186,10 @@ class AccountV2Subsystem:
cfgkey = 'ImplicitLoginStates'
cfgdict = _babase.app.config.setdefault(cfgkey, {})
- # Store which (if any) adapter is currently implicitly signed in.
- # Making the assumption there will only ever be one implicit
- # adapter at a time; may need to update this if that changes.
+ # Store which (if any) adapter is currently implicitly signed
+ # in. Making the assumption there will only ever be one implicit
+ # adapter at a time; may need to revisit this logic if that
+ # changes.
prev_state = cfgdict.get(login_type.value)
if state is None:
self._implicit_signed_in_adapter = None
@@ -296,9 +297,8 @@ class AccountV2Subsystem:
# Consider this an 'explicit' sign in because the
# implicit-login state change presumably was triggered
# by some user action (signing in, signing out, or
- # switching accounts via the back-end).
- # NOTE: should test case where we don't have
- # connectivity here.
+ # switching accounts via the back-end). NOTE: should
+ # test case where we don't have connectivity here.
if plus.cloud.is_connected():
if DEBUG_LOG:
logging.debug(
diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py
index b014d677..07bacfa6 100644
--- a/src/assets/ba_data/python/babase/_app.py
+++ b/src/assets/ba_data/python/babase/_app.py
@@ -229,6 +229,15 @@ class App:
self.lang = LanguageSubsystem()
self.plugins = PluginSubsystem()
+ @property
+ def active(self) -> bool:
+ """Whether the app is currently front and center.
+
+ This will be False when the app is hidden, other activities
+ are covering it, etc. (depending on the platform).
+ """
+ return _babase.app_is_active()
+
@property
def aioloop(self) -> asyncio.AbstractEventLoop:
"""The logic thread's asyncio event loop.
diff --git a/src/assets/ba_data/python/babase/_appmode.py b/src/assets/ba_data/python/babase/_appmode.py
index fcaa77a9..1da43144 100644
--- a/src/assets/ba_data/python/babase/_appmode.py
+++ b/src/assets/ba_data/python/babase/_appmode.py
@@ -31,6 +31,7 @@ class AppMode:
AppExperience associated with the AppMode must be supported by
the current app and runtime environment.
"""
+ # FIXME: check AppExperience.
return cls._supports_intent(intent)
@classmethod
diff --git a/src/assets/ba_data/python/babase/_apputils.py b/src/assets/ba_data/python/babase/_apputils.py
index 655bc8b3..59d76481 100644
--- a/src/assets/ba_data/python/babase/_apputils.py
+++ b/src/assets/ba_data/python/babase/_apputils.py
@@ -325,7 +325,7 @@ def dump_app_state(
)
-def log_dumped_app_state() -> None:
+def log_dumped_app_state(from_previous_run: bool = False) -> None:
"""If an app-state dump exists, log it and clear it. No-op otherwise."""
try:
@@ -352,8 +352,13 @@ def log_dumped_app_state() -> None:
metadata = dataclass_from_json(DumpedAppStateMetadata, appstatedata)
+ header = (
+ 'Found app state dump from previous app run'
+ if from_previous_run
+ else 'App state dump'
+ )
out += (
- f'App state dump:\nReason: {metadata.reason}\n'
+ f'{header}:\nReason: {metadata.reason}\n'
f'Time: {metadata.app_time:.2f}'
)
tbpath = os.path.join(
@@ -383,7 +388,7 @@ class AppHealthMonitor(AppSubsystem):
def on_app_loading(self) -> None:
# If any traceback dumps happened last run, log and clear them.
- log_dumped_app_state()
+ log_dumped_app_state(from_previous_run=True)
def _app_monitor_thread_main(self) -> None:
_babase.set_thread_name('ballistica app-monitor')
diff --git a/src/assets/ba_data/python/babase/_login.py b/src/assets/ba_data/python/babase/_login.py
index e8884034..39cfa0ed 100644
--- a/src/assets/ba_data/python/babase/_login.py
+++ b/src/assets/ba_data/python/babase/_login.py
@@ -145,7 +145,7 @@ class LoginAdapter:
is actually being used by the app. It should therefore register
unlocked achievements, leaderboard scores, allow viewing native
UIs, etc. When not active it should ignore everything and behave
- as if logged out, even if it technically is still logged in.
+ as if signed out, even if it technically is still signed in.
"""
assert _babase.in_logic_thread()
del active # Unused.
diff --git a/src/assets/ba_data/python/babase/_meta.py b/src/assets/ba_data/python/babase/_meta.py
index 5e2a0ec5..76f88b76 100644
--- a/src/assets/ba_data/python/babase/_meta.py
+++ b/src/assets/ba_data/python/babase/_meta.py
@@ -24,6 +24,8 @@ if TYPE_CHECKING:
# instead of these or to make the meta system aware of arbitrary classes.
EXPORT_CLASS_NAME_SHORTCUTS: dict[str, str] = {
'plugin': 'babase.Plugin',
+ # DEPRECATED as of 12/2023. Currently am warning if finding these
+ # but should take this out eventually.
'keyboard': 'babase.Keyboard',
}
@@ -414,30 +416,27 @@ class DirectoryScan:
if export_class_name is not None:
classname = modulename + '.' + export_class_name
- # Since we'll soon have multiple versions of 'game'
- # classes we need to migrate people to using base
- # class names for them.
- if exporttypestr == 'game':
+ # Migrating away from the 'keyboard' name shortcut
+ # since it's specific to bauiv1; warn if we find it.
+ if exporttypestr == 'keyboard':
logging.warning(
"metascan: %s:%d: '# ba_meta export"
- " game' tag should be replaced by '# ba_meta"
- " export bascenev1.GameActivity'.",
+ " keyboard' tag should be replaced by '# ba_meta"
+ " export bauiv1.Keyboard'.",
subpath,
lindex + 1,
)
self.results.announce_errors_occurred = True
- else:
- # If export type is one of our shortcuts, sub in the
- # actual class path. Otherwise assume its a classpath
- # itself.
- exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(
- exporttypestr
- )
- if exporttype is None:
- exporttype = exporttypestr
- self.results.exports.setdefault(exporttype, []).append(
- classname
- )
+
+ # If export type is one of our shortcuts, sub in the
+ # actual class path. Otherwise assume its a classpath
+ # itself.
+ exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(exporttypestr)
+ if exporttype is None:
+ exporttype = exporttypestr
+ self.results.exports.setdefault(exporttype, []).append(
+ classname
+ )
def _get_export_class_name(
self, subpath: Path, lines: list[str], lindex: int
diff --git a/src/assets/ba_data/python/baclassic/_ads.py b/src/assets/ba_data/python/baclassic/_ads.py
index 0617a6ea..fdb34de4 100644
--- a/src/assets/ba_data/python/baclassic/_ads.py
+++ b/src/assets/ba_data/python/baclassic/_ads.py
@@ -4,6 +4,8 @@
from __future__ import annotations
import time
+import asyncio
+import logging
from typing import TYPE_CHECKING
import babase
@@ -31,6 +33,7 @@ class AdsSubsystem:
self.last_in_game_ad_remove_message_show_time: float | None = None
self.last_ad_completion_time: float | None = None
self.last_ad_was_short = False
+ self._fallback_task: asyncio.Task | None = None
def do_remove_in_game_ads_message(self) -> None:
"""(internal)"""
@@ -94,7 +97,7 @@ class AdsSubsystem:
show = True
# No ads without net-connections, etc.
- if not bauiv1.can_show_ad():
+ if not plus.can_show_ad():
show = False
if classic.accounts.have_pro():
show = False # Pro disables interstitials.
@@ -132,7 +135,7 @@ class AdsSubsystem:
# ad-show-threshold and see if we should *actually* show
# (we reach our threshold faster the longer we've been
# playing).
- base = 'ads' if bauiv1.has_video_ads() else 'ads2'
+ base = 'ads' if plus.has_video_ads() else 'ads2'
min_lc = plus.get_v1_account_misc_read_val(base + '.minLC', 0.0)
max_lc = plus.get_v1_account_misc_read_val(base + '.maxLC', 5.0)
min_lc_scale = plus.get_v1_account_misc_read_val(
@@ -181,36 +184,53 @@ class AdsSubsystem:
# If we're *still* cleared to show, actually tell the system to show.
if show:
- # As a safety-check, set up an object that will run
- # the completion callback if we've returned and sat for 10 seconds
- # (in case some random ad network doesn't properly deliver its
- # completion callback).
+ # As a safety-check, we set up an object that will run the
+ # completion callback if we've returned and sat for several
+ # seconds (in case some random ad network doesn't properly
+ # deliver its completion callback).
class _Payload:
def __init__(self, pcall: Callable[[], Any]):
self._call = pcall
self._ran = False
def run(self, fallback: bool = False) -> None:
- """Run fallback call (and issue a warning about it)."""
+ """Run the payload."""
assert app.classic is not None
if not self._ran:
if fallback:
lanst = app.classic.ads.last_ad_network_set_time
- print(
- 'ERROR: relying on fallback ad-callback! '
- 'last network: '
- + app.classic.ads.last_ad_network
- + ' (set '
- + str(int(time.time() - lanst))
- + 's ago); purpose='
- + app.classic.ads.last_ad_purpose
+ logging.error(
+ 'Relying on fallback ad-callback! '
+ 'last network: %s (set %s seconds ago);'
+ ' purpose=%s.',
+ app.classic.ads.last_ad_network,
+ time.time() - lanst,
+ app.classic.ads.last_ad_purpose,
)
babase.pushcall(self._call)
self._ran = True
payload = _Payload(call)
+
+ # Set up our backup.
with babase.ContextRef.empty():
- babase.apptimer(5.0, lambda: payload.run(fallback=True))
+ # Note to self: Previously this was a simple 5 second
+ # timer because the app got totally suspended while ads
+ # were showing (which delayed the timer), but these days
+ # the app may continue to run, so we need to be more
+ # careful and only fire the fallback after we see that
+ # the app has been front-and-center for several seconds.
+ async def add_fallback_task() -> None:
+ activesecs = 5
+ while activesecs > 0:
+ if babase.app.active:
+ activesecs -= 1
+ await asyncio.sleep(1.0)
+ payload.run(fallback=True)
+
+ _fallback_task = babase.app.aioloop.create_task(
+ add_fallback_task()
+ )
self.show_ad('between_game', on_completion_call=payload.run)
else:
babase.pushcall(call) # Just run the callback without the ad.
diff --git a/src/assets/ba_data/python/baclassic/_appdelegate.py b/src/assets/ba_data/python/baclassic/_appdelegate.py
index 4595bd63..1dd1a6b0 100644
--- a/src/assets/ba_data/python/baclassic/_appdelegate.py
+++ b/src/assets/ba_data/python/baclassic/_appdelegate.py
@@ -41,5 +41,6 @@ class AppDelegate:
sessiontype,
settings,
completion_call=completion_call,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=False, # Disable check since we don't know.
)
diff --git a/src/assets/ba_data/python/baclassic/_subsystem.py b/src/assets/ba_data/python/baclassic/_subsystem.py
index f8aba3bc..6746251c 100644
--- a/src/assets/ba_data/python/baclassic/_subsystem.py
+++ b/src/assets/ba_data/python/baclassic/_subsystem.py
@@ -800,5 +800,6 @@ class ClassicSubsystem(babase.AppSubsystem):
bauiv1.getsound('swish').play()
babase.app.ui_v1.set_main_menu_window(
- MainMenuWindow().get_root_widget()
+ MainMenuWindow().get_root_widget(),
+ from_window=False, # Disable check here.
)
diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py
index 82c27c87..35416b81 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -52,8 +52,8 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 21636
-TARGET_BALLISTICA_VERSION = '1.7.30'
+TARGET_BALLISTICA_BUILD = 21707
+TARGET_BALLISTICA_VERSION = '1.7.31'
@dataclass
diff --git a/src/assets/ba_data/python/baplus/_subsystem.py b/src/assets/ba_data/python/baplus/_subsystem.py
index 97a9cdb5..0b77c20f 100644
--- a/src/assets/ba_data/python/baplus/_subsystem.py
+++ b/src/assets/ba_data/python/baplus/_subsystem.py
@@ -249,3 +249,18 @@ class PlusSubsystem(AppSubsystem):
) -> None:
"""(internal)"""
return _baplus.tournament_query(callback, args)
+
+ @staticmethod
+ def have_incentivized_ad() -> bool:
+ """Is an incentivized ad available?"""
+ return _baplus.have_incentivized_ad()
+
+ @staticmethod
+ def has_video_ads() -> bool:
+ """Are video ads available?"""
+ return _baplus.has_video_ads()
+
+ @staticmethod
+ def can_show_ad() -> bool:
+ """Can we show an ad?"""
+ return _baplus.can_show_ad()
diff --git a/src/assets/ba_data/python/bascenev1/__init__.py b/src/assets/ba_data/python/bascenev1/__init__.py
index 605b363c..e6cc6a5a 100644
--- a/src/assets/ba_data/python/bascenev1/__init__.py
+++ b/src/assets/ba_data/python/bascenev1/__init__.py
@@ -78,6 +78,7 @@ from _bascenev1 import (
end_host_scanning,
get_chat_messages,
get_connection_to_host_info,
+ get_connection_to_host_info_2,
get_foreground_host_activity,
get_foreground_host_session,
get_game_port,
@@ -202,6 +203,7 @@ from bascenev1._multiteamsession import (
DEFAULT_TEAM_NAMES,
)
from bascenev1._music import MusicType, setmusic
+from bascenev1._net import HostInfo
from bascenev1._nodeactor import NodeActor
from bascenev1._powerup import get_default_powerup_distribution
from bascenev1._profile import (
@@ -303,6 +305,7 @@ __all__ = [
'GameTip',
'get_chat_messages',
'get_connection_to_host_info',
+ 'get_connection_to_host_info_2',
'get_default_free_for_all_playlist',
'get_default_teams_playlist',
'get_default_powerup_distribution',
@@ -338,6 +341,7 @@ __all__ = [
'have_connected_clients',
'have_touchscreen_input',
'HitMessage',
+ 'HostInfo',
'host_scan_cycle',
'ImpactDamageMessage',
'increment_analytics_count',
diff --git a/src/assets/ba_data/python/bascenev1/_net.py b/src/assets/ba_data/python/bascenev1/_net.py
new file mode 100644
index 00000000..279c329d
--- /dev/null
+++ b/src/assets/ba_data/python/bascenev1/_net.py
@@ -0,0 +1,24 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Functionality related to net play."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from dataclasses import dataclass
+
+if TYPE_CHECKING:
+ pass
+
+
+@dataclass
+class HostInfo:
+ """Info about a host."""
+
+ name: str
+ build_number: int
+
+ # Note this can be None for non-ip hosts such as bluetooth.
+ address: str | None
+
+ # Note this can be None for non-ip hosts such as bluetooth.
+ port: int | None
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
index 47e8d2ef..2aff578f 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
@@ -190,7 +190,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
super().__del__()
# If our UI is still up, kill it.
- if self._root_ui:
+ if self._root_ui and not self._root_ui.transitioning_out:
with bui.ContextRef.empty():
bui.containerwidget(edit=self._root_ui, transition='out_left')
diff --git a/src/assets/ba_data/python/bascenev1lib/mainmenu.py b/src/assets/ba_data/python/bascenev1lib/mainmenu.py
index 88b85249..6092f0ce 100644
--- a/src/assets/ba_data/python/bascenev1lib/mainmenu.py
+++ b/src/assets/ba_data/python/bascenev1lib/mainmenu.py
@@ -317,7 +317,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
from bauiv1lib.kiosk import KioskWindow
bs.app.ui_v1.set_main_menu_window(
- KioskWindow().get_root_widget()
+ KioskWindow().get_root_widget(),
+ from_window=False, # Disable check here.
)
# ..or in normal cases go back to the main menu
else:
@@ -326,14 +327,16 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
from bauiv1lib.gather import GatherWindow
bs.app.ui_v1.set_main_menu_window(
- GatherWindow(transition=None).get_root_widget()
+ GatherWindow(transition=None).get_root_widget(),
+ from_window=False, # Disable check here.
)
elif main_menu_location == 'Watch':
# pylint: disable=cyclic-import
from bauiv1lib.watch import WatchWindow
bs.app.ui_v1.set_main_menu_window(
- WatchWindow(transition=None).get_root_widget()
+ WatchWindow(transition=None).get_root_widget(),
+ from_window=False, # Disable check here.
)
elif main_menu_location == 'Team Game Select':
# pylint: disable=cyclic-import
@@ -344,7 +347,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
bs.app.ui_v1.set_main_menu_window(
PlaylistBrowserWindow(
sessiontype=bs.DualTeamSession, transition=None
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=False, # Disable check here.
)
elif main_menu_location == 'Free-for-All Game Select':
# pylint: disable=cyclic-import
@@ -356,28 +360,34 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
PlaylistBrowserWindow(
sessiontype=bs.FreeForAllSession,
transition=None,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=False, # Disable check here.
)
elif main_menu_location == 'Coop Select':
# pylint: disable=cyclic-import
from bauiv1lib.coop.browser import CoopBrowserWindow
bs.app.ui_v1.set_main_menu_window(
- CoopBrowserWindow(transition=None).get_root_widget()
+ CoopBrowserWindow(
+ transition=None
+ ).get_root_widget(),
+ from_window=False, # Disable check here.
)
elif main_menu_location == 'Benchmarks & Stress Tests':
# pylint: disable=cyclic-import
from bauiv1lib.debug import DebugWindow
bs.app.ui_v1.set_main_menu_window(
- DebugWindow(transition=None).get_root_widget()
+ DebugWindow(transition=None).get_root_widget(),
+ from_window=False, # Disable check here.
)
else:
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
bs.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition=None).get_root_widget()
+ MainMenuWindow(transition=None).get_root_widget(),
+ from_window=None,
)
# attempt to show any pending offers immediately.
diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py
index fa97bb6f..5e738646 100644
--- a/src/assets/ba_data/python/bauiv1/__init__.py
+++ b/src/assets/ba_data/python/bauiv1/__init__.py
@@ -62,7 +62,6 @@ from babase import (
is_browser_likely_available,
is_running_on_fire_tv,
is_xcode_build,
- Keyboard,
lock_all_input,
LoginAdapter,
LoginInfo,
@@ -94,7 +93,6 @@ from babase import (
from _bauiv1 import (
buttonwidget,
- can_show_ad,
checkboxwidget,
columnwidget,
containerwidget,
@@ -103,8 +101,6 @@ from _bauiv1 import (
getmesh,
getsound,
gettexture,
- has_video_ads,
- have_incentivized_ad,
hscrollwidget,
imagewidget,
is_party_icon_visible,
@@ -125,6 +121,7 @@ from _bauiv1 import (
Widget,
widget,
)
+from bauiv1._keyboard import Keyboard
from bauiv1._uitypes import Window, uicleanupcheck
from bauiv1._subsystem import UIV1Subsystem
@@ -144,7 +141,6 @@ __all__ = [
'AppTimer',
'buttonwidget',
'Call',
- 'can_show_ad',
'fullscreen_control_available',
'fullscreen_control_get',
'fullscreen_control_key_shortcut',
@@ -178,8 +174,6 @@ __all__ = [
'getmesh',
'getsound',
'gettexture',
- 'has_video_ads',
- 'have_incentivized_ad',
'have_permission',
'hscrollwidget',
'imagewidget',
diff --git a/src/assets/ba_data/python/bauiv1/_hooks.py b/src/assets/ba_data/python/bauiv1/_hooks.py
index c7b6072a..30903564 100644
--- a/src/assets/ba_data/python/bauiv1/_hooks.py
+++ b/src/assets/ba_data/python/bauiv1/_hooks.py
@@ -6,6 +6,7 @@
from __future__ import annotations
import logging
+import inspect
from typing import TYPE_CHECKING
import _bauiv1
@@ -87,3 +88,19 @@ def show_url_window(address: str) -> None:
return
app.classic.show_url_window(address)
+
+
+def double_transition_out_warning() -> None:
+ """Called if a widget is set to transition out twice."""
+ caller_frame = inspect.stack()[1]
+ caller_filename = caller_frame.filename
+ caller_line_number = caller_frame.lineno
+ logging.warning(
+ 'ContainerWidget was set to transition out twice;'
+ ' this often implies buggy code (%s line %s).\n'
+ ' Generally you should check the value of'
+ ' _root_widget.transitioning_out and only kick off transitions'
+ ' when that is False.',
+ caller_filename,
+ caller_line_number,
+ )
diff --git a/src/assets/ba_data/python/babase/_keyboard.py b/src/assets/ba_data/python/bauiv1/_keyboard.py
similarity index 100%
rename from src/assets/ba_data/python/babase/_keyboard.py
rename to src/assets/ba_data/python/bauiv1/_keyboard.py
diff --git a/src/assets/ba_data/python/bauiv1/_subsystem.py b/src/assets/ba_data/python/bauiv1/_subsystem.py
index 83d88d87..3c6b1e77 100644
--- a/src/assets/ba_data/python/bauiv1/_subsystem.py
+++ b/src/assets/ba_data/python/bauiv1/_subsystem.py
@@ -5,6 +5,7 @@
from __future__ import annotations
import logging
+import inspect
from typing import TYPE_CHECKING
import babase
@@ -116,21 +117,69 @@ class UIV1Subsystem(babase.AppSubsystem):
# FIXME: Can probably kill this if we do immediate UI death checks.
self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True)
- def set_main_menu_window(self, window: bauiv1.Widget) -> None:
- """Set the current 'main' window, replacing any existing."""
+ def set_main_menu_window(
+ self,
+ window: bauiv1.Widget,
+ from_window: bauiv1.Widget | None | bool = True,
+ ) -> None:
+ """Set the current 'main' window, replacing any existing.
+
+ If 'from_window' is passed as a bauiv1.Widget or None, a warning
+ will be issued if it that value does not match the current main
+ window. This can help clean up flawed code that can lead to bad
+ UI states. A value of False will disable the check.
+ """
+
existing = self._main_menu_window
- from inspect import currentframe, getframeinfo
+
+ try:
+ if isinstance(from_window, bool):
+ # For default val True we warn that the arg wasn't
+ # passed. False can be explicitly passed to disable this
+ # check.
+ if from_window is True:
+ caller_frame = inspect.stack()[1]
+ caller_filename = caller_frame.filename
+ caller_line_number = caller_frame.lineno
+ logging.warning(
+ 'set_main_menu_window() should be passed a'
+ " 'from_window' value to help ensure proper UI behavior"
+ ' (%s line %i).',
+ caller_filename,
+ caller_line_number,
+ )
+ else:
+ # For everything else, warn if what they passed wasn't
+ # the previous main menu widget.
+ if from_window is not existing:
+ caller_frame = inspect.stack()[1]
+ caller_filename = caller_frame.filename
+ caller_line_number = caller_frame.lineno
+ logging.warning(
+ "set_main_menu_window() was passed 'from_window' %s"
+ ' but existing main-menu-window is %s. (%s line %i).',
+ from_window,
+ existing,
+ caller_filename,
+ caller_line_number,
+ )
+ except Exception:
+ # Prevent any bugs in these checks from causing problems.
+ logging.exception('Error checking from_window')
+
+ # Once the above code leads to us fixing all leftover window bugs
+ # at the source, we can kill the code below.
# Let's grab the location where we were called from to report
# if we have to force-kill the existing window (which normally
# should not happen).
frameline = None
try:
- frame = currentframe()
+ frame = inspect.currentframe()
if frame is not None:
frame = frame.f_back
if frame is not None:
- frameinfo = getframeinfo(frame)
+ frameinfo = inspect.getframeinfo(frame)
frameline = f'{frameinfo.filename} {frameinfo.lineno}'
except Exception:
logging.exception('Error calcing line for set_main_menu_window')
@@ -160,13 +209,18 @@ class UIV1Subsystem(babase.AppSubsystem):
def clear_main_menu_window(self, transition: str | None = None) -> None:
"""Clear any existing 'main' window with the provided transition."""
+ assert transition is None or not transition.endswith('_in')
if self._main_menu_window:
- if transition is not None:
+ if (
+ transition is not None
+ and not self._main_menu_window.transitioning_out
+ ):
_bauiv1.containerwidget(
edit=self._main_menu_window, transition=transition
)
else:
self._main_menu_window.delete()
+ self._main_menu_window = None
def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None:
"""(internal)"""
diff --git a/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py
index 7dc42b0e..425a78e6 100644
--- a/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py
+++ b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
import babase
import _bauiv1
+from bauiv1._keyboard import Keyboard
from bauiv1._uitypes import Window
if TYPE_CHECKING:
@@ -252,9 +253,7 @@ class OnScreenKeyboardWindow(Window):
# Show change instructions only if we have more than one
# keyboard option.
keyboards = (
- babase.app.meta.scanresults.exports_of_class(
- babase.Keyboard
- )
+ babase.app.meta.scanresults.exports_of_class(Keyboard)
if babase.app.meta.scanresults is not None
else []
)
@@ -286,10 +285,10 @@ class OnScreenKeyboardWindow(Window):
def _get_keyboard(self) -> bui.Keyboard:
assert babase.app.meta.scanresults is not None
- classname = babase.app.meta.scanresults.exports_of_class(
- babase.Keyboard
- )[self._keyboard_index]
- kbclass = babase.getclass(classname, babase.Keyboard)
+ classname = babase.app.meta.scanresults.exports_of_class(Keyboard)[
+ self._keyboard_index
+ ]
+ kbclass = babase.getclass(classname, Keyboard)
return kbclass()
def _refresh(self) -> None:
@@ -384,9 +383,7 @@ class OnScreenKeyboardWindow(Window):
def _next_keyboard(self) -> None:
assert babase.app.meta.scanresults is not None
- kbexports = babase.app.meta.scanresults.exports_of_class(
- babase.Keyboard
- )
+ kbexports = babase.app.meta.scanresults.exports_of_class(Keyboard)
self._keyboard_index = (self._keyboard_index + 1) % len(kbexports)
self._load_keyboard()
diff --git a/src/assets/ba_data/python/bauiv1lib/account/settings.py b/src/assets/ba_data/python/bauiv1lib/account/settings.py
index 8c29aee5..63937ef6 100644
--- a/src/assets/ba_data/python/bauiv1lib/account/settings.py
+++ b/src/assets/ba_data/python/bauiv1lib/account/settings.py
@@ -1507,9 +1507,18 @@ class AccountSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.profile.browser import ProfileBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
- ProfileBrowserWindow(origin_widget=self._player_profiles_button)
+ bui.app.ui_v1.set_main_menu_window(
+ ProfileBrowserWindow(
+ origin_widget=self._player_profiles_button
+ ).get_root_widget(),
+ from_window=self._root_widget,
+ )
def _cancel_sign_in_press(self) -> None:
# If we're waiting on an adapter to give us credentials, abort.
@@ -1670,6 +1679,10 @@ class AccountSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -1678,7 +1691,8 @@ class AccountSettingsWindow(bui.Window):
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/coop/browser.py b/src/assets/ba_data/python/bauiv1lib/coop/browser.py
index 20760381..c46c552d 100644
--- a/src/assets/ba_data/python/bauiv1lib/coop/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/coop/browser.py
@@ -415,7 +415,7 @@ class CoopBrowserWindow(bui.Window):
)
# Decrement time on our tournament buttons.
- ads_enabled = bui.have_incentivized_ad()
+ ads_enabled = plus.have_incentivized_ad()
for tbtn in self._tournament_buttons:
tbtn.time_remaining = max(0, tbtn.time_remaining - 1)
if tbtn.time_remaining_value_text is not None:
@@ -430,7 +430,7 @@ class CoopBrowserWindow(bui.Window):
)
# Also adjust the ad icon visibility.
- if tbtn.allow_ads and bui.has_video_ads():
+ if tbtn.allow_ads and plus.has_video_ads():
bui.imagewidget(
edit=tbtn.entry_fee_ad_image,
opacity=1.0 if ads_enabled else 0.25,
@@ -1019,6 +1019,10 @@ class CoopBrowserWindow(bui.Window):
from bauiv1lib.account import show_sign_in_prompt
from bauiv1lib.league.rankwindow import LeagueRankWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -1032,7 +1036,8 @@ class CoopBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
LeagueRankWindow(
origin_widget=self._league_rank_button.get_button()
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _switch_to_score(
@@ -1043,6 +1048,10 @@ class CoopBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.account import show_sign_in_prompt
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -1058,7 +1067,8 @@ class CoopBrowserWindow(bui.Window):
origin_widget=self._store_button.get_button(),
show_tab=show_tab,
back_location='CoopBrowserWindow',
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def is_tourney_data_up_to_date(self) -> bool:
@@ -1218,6 +1228,10 @@ class CoopBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# If something is selected, store it.
self._save_state()
bui.containerwidget(
@@ -1225,7 +1239,8 @@ class CoopBrowserWindow(bui.Window):
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- PlayWindow(transition='in_left').get_root_widget()
+ PlayWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py b/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py
index 72d604e6..4b766379 100644
--- a/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py
+++ b/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py
@@ -638,8 +638,8 @@ class TournamentButton:
# Now, if this fee allows ads and we support video ads, show
# the 'or ad' version.
- if allow_ads and bui.has_video_ads():
- ads_enabled = bui.have_incentivized_ad()
+ if allow_ads and plus.has_video_ads():
+ ads_enabled = plus.have_incentivized_ad()
bui.imagewidget(
edit=self.entry_fee_ad_image,
opacity=1.0 if ads_enabled else 0.25,
diff --git a/src/assets/ba_data/python/bauiv1lib/creditslist.py b/src/assets/ba_data/python/bauiv1lib/creditslist.py
index e6a0769f..c087dfbf 100644
--- a/src/assets/ba_data/python/bauiv1lib/creditslist.py
+++ b/src/assets/ba_data/python/bauiv1lib/creditslist.py
@@ -359,10 +359,15 @@ class CreditsListWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/debug.py b/src/assets/ba_data/python/bauiv1lib/debug.py
index 7cd5e5d2..b397610f 100644
--- a/src/assets/ba_data/python/bauiv1lib/debug.py
+++ b/src/assets/ba_data/python/bauiv1lib/debug.py
@@ -379,8 +379,13 @@ class DebugWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AdvancedSettingsWindow(transition='in_left').get_root_widget()
+ AdvancedSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/discord.py b/src/assets/ba_data/python/bauiv1lib/discord.py
index 6021d273..2e2eb520 100644
--- a/src/assets/ba_data/python/bauiv1lib/discord.py
+++ b/src/assets/ba_data/python/bauiv1lib/discord.py
@@ -73,6 +73,7 @@ class DiscordWindow(bui.Window):
edit=self._root_widget, cancel_button=self._back_button
)
+ # Do we need to translate 'Discord'? Or is that always the name?
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(0, self._height - 52),
@@ -91,6 +92,9 @@ class DiscordWindow(bui.Window):
texture=bui.gettexture('discordServer'),
)
+ # Hmm should we translate this? The discord server is mostly
+ # English so being able to read this might be a good screening
+ # process?..
bui.textwidget(
parent=self._root_widget,
position=(self._width / 2 - 60, self._height - 100),
@@ -110,7 +114,7 @@ class DiscordWindow(bui.Window):
position=(self._width / 2 - 30, 20),
size=(self._width / 2 - 60, 60),
autoselect=True,
- label='Join The Discord',
+ label=bui.Lstr(resource='discordJoinText'),
text_scale=1.0,
on_activate_call=bui.Call(
bui.open_url, 'https://ballistica.net/discord'
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/__init__.py b/src/assets/ba_data/python/bauiv1lib/gather/__init__.py
index 3b5f02d6..8fc2e841 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/__init__.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/__init__.py
@@ -270,12 +270,17 @@ class GatherWindow(bui.Window):
"""Called by the private-hosting tab to select a playlist."""
from bauiv1lib.play import PlayWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.selecting_private_party_playlist = True
bui.app.ui_v1.set_main_menu_window(
- PlayWindow(origin_widget=origin_widget).get_root_widget()
+ PlayWindow(origin_widget=origin_widget).get_root_widget(),
+ from_window=self._root_widget,
)
def _set_tab(self, tab_id: TabID) -> None:
@@ -383,11 +388,16 @@ class GatherWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py b/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py
index 840ab400..b61ed339 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py
@@ -16,10 +16,6 @@ if TYPE_CHECKING:
class AboutGatherTab(GatherTab):
"""The about tab in the gather UI"""
- def __init__(self, window: GatherWindow) -> None:
- super().__init__(window)
- self._container: bui.Widget | None = None
-
def on_activate(
self,
parent_widget: bui.Widget,
@@ -34,6 +30,40 @@ class AboutGatherTab(GatherTab):
plus = bui.app.plus
assert plus is not None
+ try_tickets = plus.get_v1_account_misc_read_val(
+ 'friendTryTickets', None
+ )
+
+ show_message = True
+ # Squish message as needed to get things to fit nicely at
+ # various scales.
+ uiscale = bui.app.ui_v1.uiscale
+ message_height = (
+ 210
+ if uiscale is bui.UIScale.SMALL
+ else 305
+ if uiscale is bui.UIScale.MEDIUM
+ else 370
+ )
+ # Let's not talk about sharing in vr-mode; its tricky to fit more
+ # than one head in a VR-headset.
+ show_message_extra = not bui.app.env.vr
+ message_extra_height = 60
+ show_invite = try_tickets is not None
+ invite_height = 80
+ show_discord = True
+ discord_height = 80
+
+ c_height = 0
+ if show_message:
+ c_height += message_height
+ if show_message_extra:
+ c_height += message_extra_height
+ if show_invite:
+ c_height += invite_height
+ if show_discord:
+ c_height += discord_height
+
party_button_label = bui.charstr(bui.SpecialChar.TOP_BUTTON)
message = bui.Lstr(
resource='gatherWindow.aboutDescriptionText',
@@ -43,9 +73,7 @@ class AboutGatherTab(GatherTab):
],
)
- # Let's not talk about sharing in vr-mode; its tricky to fit more
- # than one head in a VR-headset ;-)
- if not bui.app.env.vr:
+ if show_message_extra:
message = bui.Lstr(
value='${A}\n\n${B}',
subs=[
@@ -59,46 +87,52 @@ class AboutGatherTab(GatherTab):
),
],
)
- string_height = 400
- include_invite = True
- include_discord = False # Need to fix spacing on small first.
- msc_scale = 1.1
- c_height_2 = min(region_height, string_height * msc_scale + 100)
- try_tickets = plus.get_v1_account_misc_read_val(
- 'friendTryTickets', None
- )
- if try_tickets is None:
- include_invite = False
- self._container = bui.containerwidget(
+ scroll_widget = bui.scrollwidget(
parent=parent_widget,
+ position=(region_left, region_bottom),
+ size=(region_width, region_height),
+ highlight=False,
+ border_opacity=0,
+ )
+ msc_scale = 1.1
+
+ container = bui.containerwidget(
+ parent=scroll_widget,
position=(
region_left,
- region_bottom + (region_height - c_height_2) * 0.5,
+ region_bottom + (region_height - c_height) * 0.5,
),
- size=(region_width, c_height_2),
+ size=(region_width, c_height),
background=False,
- selectable=include_invite or include_discord,
+ selectable=show_invite or show_discord,
)
- bui.widget(edit=self._container, up_widget=tab_button)
+ # Allows escaping if we select the container somehow (though
+ # shouldn't be possible when buttons are present).
+ bui.widget(edit=container, up_widget=tab_button)
- bui.textwidget(
- parent=self._container,
- position=(region_width * 0.5, c_height_2 * 0.58),
- color=(0.6, 1.0, 0.6),
- scale=msc_scale,
- size=(0, 0),
- maxwidth=region_width * 0.9,
- max_height=c_height_2 * 0.7,
- h_align='center',
- v_align='center',
- text=message,
- )
-
- if include_invite:
+ y = c_height - 30
+ if show_message:
bui.textwidget(
- parent=self._container,
- position=(region_width * 0.57, 35),
+ parent=container,
+ position=(region_width * 0.5, y),
+ color=(0.6, 1.0, 0.6),
+ scale=msc_scale,
+ size=(0, 0),
+ maxwidth=region_width * 0.9,
+ max_height=message_height,
+ h_align='center',
+ v_align='top',
+ text=message,
+ )
+ y -= message_height
+ if show_message_extra:
+ y -= message_extra_height
+
+ if show_invite:
+ bui.textwidget(
+ parent=container,
+ position=(region_width * 0.57, y),
color=(0, 1, 0),
scale=0.6,
size=(0, 0),
@@ -112,8 +146,8 @@ class AboutGatherTab(GatherTab):
),
)
invite_button = bui.buttonwidget(
- parent=self._container,
- position=(region_width * 0.59, 10),
+ parent=container,
+ position=(region_width * 0.59, y - 25),
size=(230, 50),
color=(0.54, 0.42, 0.56),
textcolor=(0, 1, 0),
@@ -125,13 +159,14 @@ class AboutGatherTab(GatherTab):
on_activate_call=bui.WeakCall(self._invite_to_try_press),
up_widget=tab_button,
)
+ y -= invite_height
else:
invite_button = None
- if include_discord:
+ if show_discord:
bui.textwidget(
- parent=self._container,
- position=(region_width * 0.57, 15 if include_invite else 75),
+ parent=container,
+ position=(region_width * 0.57, y),
color=(0.6, 0.6, 1),
scale=0.6,
size=(0, 0),
@@ -139,26 +174,29 @@ class AboutGatherTab(GatherTab):
h_align='right',
v_align='center',
flatness=1.0,
- text=(
- 'Want to look for new people to play with?\n'
- 'Join our Discord and find new friends!'
- ),
+ text=bui.Lstr(resource='discordFriendsText'),
)
- bui.buttonwidget(
- parent=self._container,
- position=(region_width * 0.59, -10 if include_invite else 50),
+ discord_button = bui.buttonwidget(
+ parent=container,
+ position=(region_width * 0.59, y - 25),
size=(230, 50),
color=(0.54, 0.42, 0.56),
textcolor=(0.6, 0.6, 1),
- label='Join The Discord',
+ label=bui.Lstr(resource='discordJoinText'),
autoselect=True,
on_activate_call=bui.WeakCall(self._join_the_discord_press),
up_widget=(
invite_button if invite_button is not None else tab_button
),
)
+ y -= discord_height
+ else:
+ discord_button = None
- return self._container
+ if discord_button is not None:
+ pass
+
+ return scroll_widget
def _invite_to_try_press(self) -> None:
from bauiv1lib.account import show_sign_in_prompt
diff --git a/src/assets/ba_data/python/bauiv1lib/getcurrency.py b/src/assets/ba_data/python/bauiv1lib/getcurrency.py
index a87ec18f..6e355d42 100644
--- a/src/assets/ba_data/python/bauiv1lib/getcurrency.py
+++ b/src/assets/ba_data/python/bauiv1lib/getcurrency.py
@@ -334,7 +334,7 @@ class GetCurrencyWindow(bui.Window):
tex_scale=1.2,
) # 19.99-ish
- self._enable_ad_button = bui.has_video_ads()
+ self._enable_ad_button = plus.has_video_ads()
h = self._width * 0.5 + 110.0
v = self._height - b_size[1] - 115.0
@@ -561,7 +561,7 @@ class GetCurrencyWindow(bui.Window):
next_reward_ad_time
)
now = datetime.datetime.utcnow()
- if bui.have_incentivized_ad() and (
+ if plus.have_incentivized_ad() and (
next_reward_ad_time is None or next_reward_ad_time <= now
):
self._ad_button_greyed = False
@@ -732,8 +732,13 @@ class GetCurrencyWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.store import browser
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if self._transitioning_out:
return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
@@ -745,7 +750,9 @@ class GetCurrencyWindow(bui.Window):
).get_root_widget()
if not self._from_modal_store:
assert bui.app.classic is not None
- bui.app.ui_v1.set_main_menu_window(window)
+ bui.app.ui_v1.set_main_menu_window(
+ window, from_window=self._root_widget
+ )
self._transitioning_out = True
diff --git a/src/assets/ba_data/python/bauiv1lib/helpui.py b/src/assets/ba_data/python/bauiv1lib/helpui.py
index 6a7ed437..e2fc64ea 100644
--- a/src/assets/ba_data/python/bauiv1lib/helpui.py
+++ b/src/assets/ba_data/python/bauiv1lib/helpui.py
@@ -645,11 +645,16 @@ class HelpWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if self._main_menu:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/keyboard/englishkeyboard.py b/src/assets/ba_data/python/bauiv1lib/keyboard/englishkeyboard.py
index ae94d938..3de74f87 100644
--- a/src/assets/ba_data/python/bauiv1lib/keyboard/englishkeyboard.py
+++ b/src/assets/ba_data/python/bauiv1lib/keyboard/englishkeyboard.py
@@ -9,7 +9,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
-import babase
+import bauiv1 as bui
if TYPE_CHECKING:
from typing import Iterable
@@ -33,15 +33,15 @@ def split(chars: Iterable[str], maxlen: int) -> list[list[str]]:
def generate_emojis(maxlen: int) -> list[list[str]]:
- """Generates a lot of UTF8 emojis prepared for babase.Keyboard pages"""
+ """Generates a lot of UTF8 emojis prepared for bui.Keyboard pages"""
all_emojis = split([chr(i) for i in range(0x1F601, 0x1F650)], maxlen)
all_emojis += split([chr(i) for i in range(0x2702, 0x27B1)], maxlen)
all_emojis += split([chr(i) for i in range(0x1F680, 0x1F6C1)], maxlen)
return all_emojis
-# ba_meta export keyboard
-class EnglishKeyboard(babase.Keyboard):
+# ba_meta export bauiv1.Keyboard
+class EnglishKeyboard(bui.Keyboard):
"""Default English keyboard."""
name = 'English'
diff --git a/src/assets/ba_data/python/bauiv1lib/kiosk.py b/src/assets/ba_data/python/bauiv1lib/kiosk.py
index 377a86d0..ab1ca87e 100644
--- a/src/assets/ba_data/python/bauiv1lib/kiosk.py
+++ b/src/assets/ba_data/python/bauiv1lib/kiosk.py
@@ -501,9 +501,15 @@ class KioskWindow(bui.Window):
def _do_full_menu(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
bui.app.classic.did_menu_intro = True # prevent delayed transition-in
- bui.app.ui_v1.set_main_menu_window(MainMenuWindow().get_root_widget())
+ bui.app.ui_v1.set_main_menu_window(
+ MainMenuWindow().get_root_widget(), from_window=self._root_widget
+ )
diff --git a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py
index d108cbb3..725bb318 100644
--- a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py
+++ b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py
@@ -1142,6 +1142,10 @@ class LeagueRankWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.coop.browser import CoopBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -1149,5 +1153,6 @@ class LeagueRankWindow(bui.Window):
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- CoopBrowserWindow(transition='in_left').get_root_widget()
+ CoopBrowserWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py
index 7e6ca3d4..16f318ae 100644
--- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py
+++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py
@@ -1038,6 +1038,10 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.confirm import QuitWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# Note: Normally we should go through bui.quit(confirm=True) but
# invoking the window directly lets us scale it up from the
# button.
@@ -1047,24 +1051,34 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.kiosk import KioskWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- KioskWindow(transition='in_left').get_root_widget()
+ KioskWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _show_account_window(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.account.settings import AccountSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AccountSettingsWindow(
origin_widget=self._account_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_store_pressed(self) -> None:
@@ -1072,6 +1086,10 @@ class MainMenuWindow(bui.Window):
from bauiv1lib.store.browser import StoreBrowserWindow
from bauiv1lib.account import show_sign_in_prompt
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -1084,7 +1102,8 @@ class MainMenuWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
StoreBrowserWindow(
origin_widget=self._store_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _is_benchmark(self) -> bool:
@@ -1149,8 +1168,11 @@ class MainMenuWindow(bui.Window):
def _end_game(self) -> None:
assert bui.app.classic is not None
- if not self._root_widget:
+
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
return
+
bui.containerwidget(edit=self._root_widget, transition='out_left')
bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False)
@@ -1166,39 +1188,54 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.creditslist import CreditsListWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
CreditsListWindow(
origin_widget=self._credits_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _howtoplay(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.helpui import HelpWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
HelpWindow(
main_menu=True, origin_widget=self._how_to_play_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _settings(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.allsettings import AllSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AllSettingsWindow(
origin_widget=self._settings_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _resume_and_call(self, call: Callable[[], Any]) -> None:
@@ -1281,35 +1318,50 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.gather import GatherWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- GatherWindow(origin_widget=self._gather_button).get_root_widget()
+ GatherWindow(origin_widget=self._gather_button).get_root_widget(),
+ from_window=self._root_widget,
)
def _watch_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.watch import WatchWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- WatchWindow(origin_widget=self._watch_button).get_root_widget()
+ WatchWindow(origin_widget=self._watch_button).get_root_widget(),
+ from_window=self._root_widget,
)
def _play_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.selecting_private_party_playlist = False
bui.app.ui_v1.set_main_menu_window(
- PlayWindow(origin_widget=self._start_button).get_root_widget()
+ PlayWindow(origin_widget=self._start_button).get_root_widget(),
+ from_window=self._root_widget,
)
def _resume(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/party.py b/src/assets/ba_data/python/bauiv1lib/party.py
index 5fa2bddd..b4905e1b 100644
--- a/src/assets/ba_data/python/bauiv1lib/party.py
+++ b/src/assets/ba_data/python/bauiv1lib/party.py
@@ -93,9 +93,10 @@ class PartyWindow(bui.Window):
iconscale=1.2,
)
- info = bs.get_connection_to_host_info()
- if info.get('name', '') != '':
- title = bui.Lstr(value=info['name'])
+ info = bs.get_connection_to_host_info_2()
+
+ if info is not None and info.name != '':
+ title = bui.Lstr(value=info.name)
else:
title = bui.Lstr(resource=self._r + '.titleText')
@@ -483,7 +484,8 @@ class PartyWindow(bui.Window):
kick_str = bui.Lstr(resource='kickText')
else:
# kick-votes appeared in build 14248
- if bs.get_connection_to_host_info().get('build_number', 0) < 14248:
+ info = bs.get_connection_to_host_info_2()
+ if info is None or info.build_number < 14248:
return
kick_str = bui.Lstr(resource='kickVoteText')
assert bui.app.classic is not None
diff --git a/src/assets/ba_data/python/bauiv1lib/play.py b/src/assets/ba_data/python/bauiv1lib/play.py
index ef1ca896..f06a9d59 100644
--- a/src/assets/ba_data/python/bauiv1lib/play.py
+++ b/src/assets/ba_data/python/bauiv1lib/play.py
@@ -521,13 +521,19 @@ class PlayWindow(bui.Window):
def _back(self) -> None:
# pylint: disable=cyclic-import
+
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if self._is_main_menu:
from bauiv1lib.mainmenu import MainMenuWindow
self._save_state()
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -538,7 +544,8 @@ class PlayWindow(bui.Window):
self._save_state()
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- GatherWindow(transition='in_left').get_root_widget()
+ GatherWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -549,6 +556,10 @@ class PlayWindow(bui.Window):
from bauiv1lib.account import show_sign_in_prompt
from bauiv1lib.coop.browser import CoopBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -559,26 +570,38 @@ class PlayWindow(bui.Window):
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- CoopBrowserWindow(origin_widget=self._coop_button).get_root_widget()
+ CoopBrowserWindow(
+ origin_widget=self._coop_button
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _team_tourney(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.playlist.browser import PlaylistBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PlaylistBrowserWindow(
origin_widget=self._teams_button, sessiontype=bs.DualTeamSession
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _free_for_all(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.playlist.browser import PlaylistBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
@@ -586,7 +609,8 @@ class PlayWindow(bui.Window):
PlaylistBrowserWindow(
origin_widget=self._free_for_all_button,
sessiontype=bs.FreeForAllSession,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _draw_dude(
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/browser.py b/src/assets/ba_data/python/bauiv1lib/playlist/browser.py
index 287496c2..8a61e91c 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/browser.py
@@ -684,6 +684,10 @@ class PlaylistBrowserWindow(bui.Window):
PlaylistCustomizeBrowserWindow,
)
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
@@ -691,13 +695,18 @@ class PlaylistBrowserWindow(bui.Window):
PlaylistCustomizeBrowserWindow(
origin_widget=self._customize_button,
sessiontype=self._sessiontype,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_back_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# Store our selected playlist if that's changed.
if self._selected_playlist is not None:
prev_sel = bui.app.config.get(
@@ -716,7 +725,8 @@ class PlaylistBrowserWindow(bui.Window):
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- PlayWindow(transition='in_left').get_root_widget()
+ PlayWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py b/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py
index 789e7f2e..f0a433b5 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py
@@ -323,6 +323,10 @@ class PlaylistCustomizeBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.playlist import browser
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if self._selected_playlist_name is not None:
cfg = bui.app.config
cfg[
@@ -337,7 +341,8 @@ class PlaylistCustomizeBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
browser.PlaylistBrowserWindow(
transition='in_left', sessiontype=self._sessiontype
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _select(self, name: str, index: int) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/edit.py b/src/assets/ba_data/python/bauiv1lib/playlist/edit.py
index 1b971514..07f7adae 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/edit.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/edit.py
@@ -283,6 +283,10 @@ class PlaylistEditWindow(bui.Window):
PlaylistCustomizeBrowserWindow,
)
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.getsound('powerdown01').play()
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
@@ -293,7 +297,8 @@ class PlaylistEditWindow(bui.Window):
select_playlist=(
self._editcontroller.get_existing_playlist_name()
),
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _add(self) -> None:
@@ -315,6 +320,10 @@ class PlaylistEditWindow(bui.Window):
PlaylistCustomizeBrowserWindow,
)
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -380,7 +389,8 @@ class PlaylistEditWindow(bui.Window):
transition='in_left',
sessiontype=self._editcontroller.get_session_type(),
select_playlist=new_name,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _save_press_with_sound(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py b/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py
index 26e53b27..7ed9a92a 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py
@@ -92,7 +92,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition=transition
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=False, # Disable this check.
)
def get_config_name(self) -> str:
@@ -150,7 +151,8 @@ class PlaylistEditController:
assert bui.app.classic is not None
bui.app.ui_v1.clear_main_menu_window(transition='out_left')
bui.app.ui_v1.set_main_menu_window(
- PlaylistAddGameWindow(editcontroller=self).get_root_widget()
+ PlaylistAddGameWindow(editcontroller=self).get_root_widget(),
+ from_window=None,
)
def edit_game_pressed(self) -> None:
@@ -175,7 +177,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=None,
)
def _show_edit_ui(
@@ -205,7 +208,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=None,
)
# Otherwise we were adding; go back to the add type choice list.
@@ -214,7 +218,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistAddGameWindow(
editcontroller=self, transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=None,
)
else:
# Make sure type is in there.
@@ -236,5 +241,6 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=None,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py b/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py
index 4e7c3d84..cb951624 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py
@@ -514,6 +514,10 @@ class PlaylistEditGameWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# Replace ourself with the map-select UI.
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
@@ -524,7 +528,8 @@ class PlaylistEditGameWindow(bui.Window):
copy.deepcopy(self._getconfig()),
self._edit_info,
self._completion_call,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _choice_inc(
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py b/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py
index c86854d6..4d351379 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py
@@ -273,6 +273,10 @@ class PlaylistMapSelectWindow(bui.Window):
def _select(self, map_name: str) -> None:
from bauiv1lib.playlist.editgame import PlaylistEditGameWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._config['settings']['map'] = map_name
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
@@ -285,7 +289,8 @@ class PlaylistMapSelectWindow(bui.Window):
default_selection='map',
transition='in_left',
edit_info=self._edit_info,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _select_with_delay(self, map_name: str) -> None:
@@ -296,6 +301,10 @@ class PlaylistMapSelectWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.playlist.editgame import PlaylistEditGameWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
@@ -307,5 +316,6 @@ class PlaylistMapSelectWindow(bui.Window):
default_selection='map',
transition='in_left',
edit_info=self._edit_info,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/playoptions.py b/src/assets/ba_data/python/bauiv1lib/playoptions.py
index fdc79e74..ea58e4d6 100644
--- a/src/assets/ba_data/python/bauiv1lib/playoptions.py
+++ b/src/assets/ba_data/python/bauiv1lib/playoptions.py
@@ -140,7 +140,6 @@ class PlayOptionsWindow(PopupWindow):
if show_shuffle_check_box:
self._height += 40
- # Creates our _root_widget.
uiscale = bui.app.ui_v1.uiscale
scale = (
1.69
@@ -149,6 +148,7 @@ class PlayOptionsWindow(PopupWindow):
if uiscale is bui.UIScale.MEDIUM
else 0.85
)
+ # Creates our _root_widget.
super().__init__(
position=scale_origin, size=(self._width, self._height), scale=scale
)
@@ -448,6 +448,10 @@ class PlayOptionsWindow(PopupWindow):
self._transition_out()
def _on_ok_press(self) -> None:
+ # no-op if our underlying widget is dead or on its way out.
+ if not self.root_widget or self.root_widget.transitioning_out:
+ return
+
# Disallow if our playlist has disappeared.
if not self._does_target_playlist_exist():
return
@@ -478,8 +482,12 @@ class PlayOptionsWindow(PopupWindow):
cfg['Private Party Host Session Type'] = typename
bui.getsound('gunCocking').play()
assert bui.app.classic is not None
+ # Note: this is a wonky situation where we aren't actually
+ # the main window but we set it on behalf of the main window
+ # that popped us up.
bui.app.ui_v1.set_main_menu_window(
- GatherWindow(transition='in_right').get_root_widget()
+ GatherWindow(transition='in_right').get_root_widget(),
+ from_window=False, # Disable this test.
)
self._transition_out(transition='out_left')
if self._delegate is not None:
diff --git a/src/assets/ba_data/python/bauiv1lib/profile/browser.py b/src/assets/ba_data/python/bauiv1lib/profile/browser.py
index 694504a7..097d3d66 100644
--- a/src/assets/ba_data/python/bauiv1lib/profile/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/profile/browser.py
@@ -214,6 +214,10 @@ class ProfileBrowserWindow(bui.Window):
from bauiv1lib.profile.edit import EditProfileWindow
from bauiv1lib.purchase import PurchaseWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -254,7 +258,8 @@ class ProfileBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
EditProfileWindow(
existing_profile=None, in_main_menu=self._in_main_menu
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget if self._in_main_menu else False,
)
def _delete_profile(self) -> None:
@@ -303,6 +308,10 @@ class ProfileBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.profile.edit import EditProfileWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if self._selected_profile is None:
bui.getsound('error').play()
bui.screenmessage(
@@ -315,7 +324,8 @@ class ProfileBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
EditProfileWindow(
self._selected_profile, in_main_menu=self._in_main_menu
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget if self._in_main_menu else False,
)
def _select(self, name: str, index: int) -> None:
@@ -326,6 +336,10 @@ class ProfileBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.account.settings import AccountSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
self._save_state()
@@ -335,7 +349,8 @@ class ProfileBrowserWindow(bui.Window):
if self._in_main_menu:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AccountSettingsWindow(transition='in_left').get_root_widget()
+ AccountSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
# If we're being called up standalone, handle pause/resume ourself.
diff --git a/src/assets/ba_data/python/bauiv1lib/profile/edit.py b/src/assets/ba_data/python/bauiv1lib/profile/edit.py
index 5870a484..0c27d78c 100644
--- a/src/assets/ba_data/python/bauiv1lib/profile/edit.py
+++ b/src/assets/ba_data/python/bauiv1lib/profile/edit.py
@@ -18,12 +18,18 @@ class EditProfileWindow(bui.Window):
# FIXME: WILL NEED TO CHANGE THIS FOR UILOCATION.
def reload_window(self) -> None:
"""Transitions out and recreates ourself."""
+
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
EditProfileWindow(
self.getname(), self._in_main_menu
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def __init__(
@@ -672,6 +678,10 @@ class EditProfileWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.profile.browser import ProfileBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
@@ -679,7 +689,8 @@ class EditProfileWindow(bui.Window):
'in_left',
selected_profile=self._existing_profile,
in_main_menu=self._in_main_menu,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _set_color(self, color: tuple[float, float, float]) -> None:
@@ -778,6 +789,10 @@ class EditProfileWindow(bui.Window):
"""Save has been selected."""
from bauiv1lib.profile.browser import ProfileBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return False
+
plus = bui.app.plus
assert plus is not None
@@ -827,6 +842,7 @@ class EditProfileWindow(bui.Window):
'in_left',
selected_profile=new_name,
in_main_menu=self._in_main_menu,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
return True
diff --git a/src/assets/ba_data/python/bauiv1lib/promocode.py b/src/assets/ba_data/python/bauiv1lib/promocode.py
index 3e7f035c..3cf745b5 100644
--- a/src/assets/ba_data/python/bauiv1lib/promocode.py
+++ b/src/assets/ba_data/python/bauiv1lib/promocode.py
@@ -142,13 +142,18 @@ class PromoCodeWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AdvancedSettingsWindow(transition='in_left').get_root_widget()
+ AdvancedSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _activate_enter_button(self) -> None:
@@ -158,6 +163,10 @@ class PromoCodeWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -167,7 +176,8 @@ class PromoCodeWindow(bui.Window):
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AdvancedSettingsWindow(transition='in_left').get_root_widget()
+ AdvancedSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
plus.add_v1_account_transaction(
{
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py
index e7724815..0db77056 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py
@@ -682,11 +682,16 @@ class AdvancedSettingsWindow(bui.Window):
def _on_vr_test_press(self) -> None:
from bauiv1lib.settings.vrtesting import VRTestingWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- VRTestingWindow(transition='in_right').get_root_widget()
+ VRTestingWindow(transition='in_right').get_root_widget(),
+ from_window=self._root_widget,
)
def _on_net_test_press(self) -> None:
@@ -694,6 +699,10 @@ class AdvancedSettingsWindow(bui.Window):
assert plus is not None
from bauiv1lib.settings.nettesting import NetTestingWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# Net-testing requires a signed in v1 account.
if plus.get_v1_account_state() != 'signed_in':
bui.screenmessage(
@@ -706,7 +715,8 @@ class AdvancedSettingsWindow(bui.Window):
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- NetTestingWindow(transition='in_right').get_root_widget()
+ NetTestingWindow(transition='in_right').get_root_widget(),
+ from_window=self._root_widget,
)
def _on_friend_promo_code_press(self) -> None:
@@ -724,17 +734,26 @@ class AdvancedSettingsWindow(bui.Window):
def _on_plugins_button_press(self) -> None:
from bauiv1lib.settings.plugins import PluginWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- PluginWindow(origin_widget=self._plugins_button).get_root_widget()
+ PluginWindow(origin_widget=self._plugins_button).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_promo_code_press(self) -> None:
from bauiv1lib.promocode import PromoCodeWindow
from bauiv1lib.account import show_sign_in_prompt
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -742,23 +761,30 @@ class AdvancedSettingsWindow(bui.Window):
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PromoCodeWindow(
origin_widget=self._promo_code_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_benchmark_press(self) -> None:
from bauiv1lib.debug import DebugWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- DebugWindow(transition='in_right').get_root_widget()
+ DebugWindow(transition='in_right').get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
@@ -908,11 +934,16 @@ class AdvancedSettingsWindow(bui.Window):
def _do_back(self) -> None:
from bauiv1lib.settings.allsettings import AllSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AllSettingsWindow(transition='in_left').get_root_widget()
+ AllSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py
index d6d18b5f..75e0f633 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py
@@ -235,65 +235,90 @@ class AllSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _do_controllers(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.controls import ControlsSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ControlsSettingsWindow(
origin_widget=self._controllers_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _do_graphics(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.graphics import GraphicsSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
GraphicsSettingsWindow(
origin_widget=self._graphics_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _do_audio(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.audio import AudioSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AudioSettingsWindow(
origin_widget=self._audio_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _do_advanced(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AdvancedSettingsWindow(
origin_widget=self._advanced_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/audio.py b/src/assets/ba_data/python/bauiv1lib/settings/audio.py
index 643e23fa..fc39b719 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/audio.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/audio.py
@@ -237,6 +237,10 @@ class AudioSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.soundtrack import browser as stb
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# We require disk access for soundtracks;
# if we don't have it, request it.
if not bui.have_permission(bui.Permission.STORAGE):
@@ -256,13 +260,18 @@ class AudioSettingsWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
stb.SoundtrackBrowserWindow(
origin_widget=self._soundtrack_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _back(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings import allsettings
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -271,7 +280,8 @@ class AudioSettingsWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
allsettings.AllSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/controls.py b/src/assets/ba_data/python/bauiv1lib/settings/controls.py
index 168df15c..108657f5 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/controls.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/controls.py
@@ -367,59 +367,84 @@ class ControlsSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.keyboard import ConfigKeyboardWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ConfigKeyboardWindow(
bs.getinputdevice('Keyboard', '#1')
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _config_keyboard2(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.keyboard import ConfigKeyboardWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ConfigKeyboardWindow(
bs.getinputdevice('Keyboard', '#2')
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _do_mobile_devices(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.remoteapp import RemoteAppSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- RemoteAppSettingsWindow().get_root_widget()
+ RemoteAppSettingsWindow().get_root_widget(),
+ from_window=self._root_widget,
)
def _do_gamepads(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.gamepadselect import GamepadSelectWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- GamepadSelectWindow().get_root_widget()
+ GamepadSelectWindow().get_root_widget(),
+ from_window=self._root_widget,
)
def _do_touchscreen(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.touchscreen import TouchscreenSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- TouchscreenSettingsWindow().get_root_widget()
+ TouchscreenSettingsWindow().get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
@@ -466,11 +491,16 @@ class ControlsSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.allsettings import AllSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AllSettingsWindow(transition='in_left').get_root_widget()
+ AllSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py
index 3c67b23f..a63847d4 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py
@@ -795,19 +795,28 @@ class GamepadSettingsWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.settings.controls import ControlsSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if self._is_main_menu:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- ControlsSettingsWindow(transition='in_left').get_root_widget()
+ ControlsSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _save(self) -> None:
classic = bui.app.classic
assert classic is not None
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
@@ -852,7 +861,8 @@ class GamepadSettingsWindow(bui.Window):
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- ControlsSettingsWindow(transition='in_left').get_root_widget()
+ ControlsSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py
index b576d422..d0dba0ac 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py
@@ -33,7 +33,8 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
assert isinstance(device, bs.InputDevice)
if device.allows_configuring:
bui.app.ui_v1.set_main_menu_window(
- gamepad.GamepadSettingsWindow(device).get_root_widget()
+ gamepad.GamepadSettingsWindow(device).get_root_widget(),
+ from_window=None,
)
else:
width = 700
@@ -51,7 +52,7 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
size=(width, height),
transition='in_right',
)
- bui.app.ui_v1.set_main_menu_window(dlg)
+ bui.app.ui_v1.set_main_menu_window(dlg, from_window=None)
if device.allows_configuring_in_system_settings:
msg = bui.Lstr(
@@ -81,12 +82,17 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
def _ok() -> None:
from bauiv1lib.settings import controls
+ # no-op if our underlying widget is dead or on its way out.
+ if not dlg or dlg.transitioning_out:
+ return
+
bui.containerwidget(edit=dlg, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=dlg,
)
bui.buttonwidget(
@@ -191,11 +197,16 @@ class GamepadSelectWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import controls
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bs.release_gamepad_input()
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py
index 7e20a537..f441826c 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py
@@ -436,6 +436,10 @@ class GraphicsSettingsWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import allsettings
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# Applying max-fps takes a few moments. Apply if it hasn't been
# yet.
self._apply_max_fps()
@@ -447,7 +451,8 @@ class GraphicsSettingsWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
allsettings.AllSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _set_quality(self, quality: str) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py
index 60aab600..1e564378 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py
@@ -271,15 +271,24 @@ class ConfigKeyboardWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.settings.controls import ControlsSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- ControlsSettingsWindow(transition='in_left').get_root_widget()
+ ControlsSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _save(self) -> None:
from bauiv1lib.settings.controls import ControlsSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
bui.containerwidget(edit=self._root_widget, transition='out_right')
bui.getsound('gunCocking').play()
@@ -314,7 +323,8 @@ class ConfigKeyboardWindow(bui.Window):
)
bui.app.config.apply_and_commit()
bui.app.ui_v1.set_main_menu_window(
- ControlsSettingsWindow(transition='in_left').get_root_widget()
+ ControlsSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py b/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py
index dd295108..e4e6e996 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py
@@ -135,8 +135,14 @@ class NetTestingWindow(bui.Window):
def _show_val_testing(self) -> None:
assert bui.app.classic is not None
+
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.app.ui_v1.set_main_menu_window(
- NetValTestingWindow().get_root_widget()
+ NetValTestingWindow().get_root_widget(),
+ from_window=self._root_widget,
)
bui.containerwidget(edit=self._root_widget, transition='out_left')
@@ -144,9 +150,14 @@ class NetTestingWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AdvancedSettingsWindow(transition='in_left').get_root_widget()
+ AdvancedSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
bui.containerwidget(edit=self._root_widget, transition='out_right')
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/plugins.py b/src/assets/ba_data/python/bauiv1lib/settings/plugins.py
index d035f11b..f5c106b5 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/plugins.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/plugins.py
@@ -232,11 +232,16 @@ class PluginWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.pluginsettings import PluginSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- PluginSettingsWindow(transition='in_right').get_root_widget()
+ PluginSettingsWindow(transition='in_right').get_root_widget(),
+ from_window=self._root_widget,
)
def _show_category_options(self) -> None:
@@ -449,11 +454,16 @@ class PluginWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AdvancedSettingsWindow(transition='in_left').get_root_widget()
+ AdvancedSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py b/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py
index 03e9e9e8..1474f5cb 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py
@@ -161,10 +161,15 @@ class PluginSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.plugins import PluginWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- PluginWindow(transition='in_left').get_root_widget()
+ PluginWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py b/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py
index 03b1c611..3542f992 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py
@@ -138,10 +138,15 @@ class RemoteAppSettingsWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import controls
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/testing.py b/src/assets/ba_data/python/bauiv1lib/settings/testing.py
index c4bcd58a..30f11e28 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/testing.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/testing.py
@@ -217,6 +217,10 @@ class TestingWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
backwin = (
self._back_call()
@@ -224,4 +228,6 @@ class TestingWindow(bui.Window):
else AdvancedSettingsWindow(transition='in_left')
)
assert bui.app.classic is not None
- bui.app.ui_v1.set_main_menu_window(backwin.get_root_widget())
+ bui.app.ui_v1.set_main_menu_window(
+ backwin.get_root_widget(), from_window=self._root_widget
+ )
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py b/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py
index 61041bbd..d77a16a2 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py
@@ -276,11 +276,16 @@ class TouchscreenSettingsWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import controls
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
bs.set_touchscreen_editing(False)
diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py
index 021942e6..a3b561b4 100644
--- a/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py
@@ -394,13 +394,18 @@ class SoundtrackBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings import audio
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- audio.AudioSettingsWindow(transition='in_left').get_root_widget()
+ audio.AudioSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _edit_soundtrack_with_sound(self) -> None:
@@ -421,6 +426,10 @@ class SoundtrackBrowserWindow(bui.Window):
from bauiv1lib.purchase import PurchaseWindow
from bauiv1lib.soundtrack.edit import SoundtrackEditWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if (
bui.app.classic is not None
and not bui.app.classic.accounts.have_pro_options()
@@ -443,7 +452,8 @@ class SoundtrackBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
SoundtrackEditWindow(
existing_soundtrack=self._selected_soundtrack
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _get_soundtrack_display_name(self, soundtrack: str) -> bui.Lstr:
diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py
index 0e1088b4..8c3887eb 100644
--- a/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py
+++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py
@@ -351,7 +351,8 @@ class SoundtrackEditWindow(bui.Window):
soundtrack[musictype] = entry
bui.app.ui_v1.set_main_menu_window(
- cls(state, transition='in_left').get_root_widget()
+ cls(state, transition='in_left').get_root_widget(),
+ from_window=False, # Disable check here.
)
def _get_entry(
@@ -359,6 +360,11 @@ class SoundtrackEditWindow(bui.Window):
) -> None:
assert bui.app.classic is not None
music = bui.app.classic.music
+
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if selection_target_name != '':
selection_target_name = "'" + selection_target_name + "'"
state = {
@@ -375,7 +381,8 @@ class SoundtrackEditWindow(bui.Window):
entry,
selection_target_name,
)
- .get_root_widget()
+ .get_root_widget(),
+ from_window=self._root_widget,
)
def _test(self, song_type: bs.MusicType) -> None:
@@ -422,6 +429,10 @@ class SoundtrackEditWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.soundtrack import browser as stb
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
music = bui.app.classic.music
@@ -429,12 +440,17 @@ class SoundtrackEditWindow(bui.Window):
music.set_music_play_mode(bui.app.classic.MusicPlayMode.REGULAR)
bui.containerwidget(edit=self._root_widget, transition='out_right')
bui.app.ui_v1.set_main_menu_window(
- stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget()
+ stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _do_it(self) -> None:
from bauiv1lib.soundtrack import browser as stb
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
music = bui.app.classic.music
cfg = bui.app.config
@@ -483,7 +499,8 @@ class SoundtrackEditWindow(bui.Window):
)
bui.app.ui_v1.set_main_menu_window(
- stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget()
+ stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _do_it_with_sound(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py
index 2adaa721..583855a8 100644
--- a/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py
+++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py
@@ -166,6 +166,10 @@ class SoundtrackEntryTypeSelectWindow(bui.Window):
MacMusicAppPlaylistSelectWindow,
)
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_left')
current_playlist_entry: str | None
@@ -181,7 +185,8 @@ class SoundtrackEntryTypeSelectWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
MacMusicAppPlaylistSelectWindow(
self._callback, current_playlist_entry, self._current_entry
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_music_file_press(self) -> None:
@@ -189,6 +194,10 @@ class SoundtrackEntryTypeSelectWindow(bui.Window):
from baclassic.osmusic import OSMusicPlayer
from bauiv1lib.fileselector import FileSelectorWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_left')
base_path = android_get_external_files_dir()
assert bui.app.classic is not None
@@ -201,13 +210,18 @@ class SoundtrackEntryTypeSelectWindow(bui.Window):
OSMusicPlayer.get_valid_music_file_extensions()
),
allow_folders=False,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_music_folder_press(self) -> None:
from bauiv1lib.fileselector import FileSelectorWindow
from babase import android_get_external_files_dir
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_left')
base_path = android_get_external_files_dir()
assert bui.app.classic is not None
@@ -218,7 +232,8 @@ class SoundtrackEntryTypeSelectWindow(bui.Window):
show_base_path=False,
valid_file_extensions=[],
allow_folders=True,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _music_file_selector_cb(self, result: str | None) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/specialoffer.py b/src/assets/ba_data/python/bauiv1lib/specialoffer.py
index af6621a8..6e4a463b 100644
--- a/src/assets/ba_data/python/bauiv1lib/specialoffer.py
+++ b/src/assets/ba_data/python/bauiv1lib/specialoffer.py
@@ -551,9 +551,11 @@ def show_offer() -> bool:
if bui.native_review_request_supported():
bui.native_review_request()
else:
- feedback.ask_for_rating()
+ if app.ui_v1.available:
+ feedback.ask_for_rating()
else:
- SpecialOfferWindow(app.classic.special_offer)
+ if app.ui_v1.available:
+ SpecialOfferWindow(app.classic.special_offer)
app.classic.special_offer = None
return True
diff --git a/src/assets/ba_data/python/bauiv1lib/store/browser.py b/src/assets/ba_data/python/bauiv1lib/store/browser.py
index 03af613a..201643f3 100644
--- a/src/assets/ba_data/python/bauiv1lib/store/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/store/browser.py
@@ -1329,6 +1329,10 @@ class StoreBrowserWindow(bui.Window):
from bauiv1lib.account import show_sign_in_prompt
from bauiv1lib.getcurrency import GetCurrencyWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -1343,13 +1347,19 @@ class StoreBrowserWindow(bui.Window):
).get_root_widget()
if not self._modal:
assert bui.app.classic is not None
- bui.app.ui_v1.set_main_menu_window(window)
+ bui.app.ui_v1.set_main_menu_window(
+ window, from_window=self._root_widget
+ )
def _back(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.coop.browser import CoopBrowserWindow
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -1358,11 +1368,13 @@ class StoreBrowserWindow(bui.Window):
assert bui.app.classic is not None
if self._back_location == 'CoopBrowserWindow':
bui.app.ui_v1.set_main_menu_window(
- CoopBrowserWindow(transition='in_left').get_root_widget()
+ CoopBrowserWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
else:
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
if self._on_close_call is not None:
self._on_close_call()
diff --git a/src/assets/ba_data/python/bauiv1lib/tournamententry.py b/src/assets/ba_data/python/bauiv1lib/tournamententry.py
index fcba3e99..d00c37dd 100644
--- a/src/assets/ba_data/python/bauiv1lib/tournamententry.py
+++ b/src/assets/ba_data/python/bauiv1lib/tournamententry.py
@@ -34,6 +34,7 @@ class TournamentEntryWindow(PopupWindow):
# pylint: disable=too-many-statements
assert bui.app.classic is not None
+ assert bui.app.plus
bui.set_analytics_screen('Tournament Entry Window')
self._tournament_id = tournament_id
@@ -100,7 +101,7 @@ class TournamentEntryWindow(PopupWindow):
self._launched = False
# Show the ad button only if we support ads *and* it has a level 1 fee.
- self._do_ad_btn = bui.has_video_ads() and self._allow_ads
+ self._do_ad_btn = bui.app.plus.has_video_ads() and self._allow_ads
x_offs = 0 if self._do_ad_btn else 85
@@ -477,7 +478,7 @@ class TournamentEntryWindow(PopupWindow):
)
if self._do_ad_btn:
- enabled = bui.have_incentivized_ad()
+ enabled = plus.have_incentivized_ad()
have_ad_tries_remaining = (
self._tournament_info['adTriesRemaining'] is not None
and self._tournament_info['adTriesRemaining'] > 0
diff --git a/src/assets/ba_data/python/bauiv1lib/watch.py b/src/assets/ba_data/python/bauiv1lib/watch.py
index 1bdd4fb3..c2e50e4c 100644
--- a/src/assets/ba_data/python/bauiv1lib/watch.py
+++ b/src/assets/ba_data/python/bauiv1lib/watch.py
@@ -663,11 +663,16 @@ class WatchWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/ballistica/base/app_adapter/app_adapter.cc b/src/ballistica/base/app_adapter/app_adapter.cc
index e140e1a2..2c288168 100644
--- a/src/ballistica/base/app_adapter/app_adapter.cc
+++ b/src/ballistica/base/app_adapter/app_adapter.cc
@@ -25,138 +25,13 @@ void AppAdapter::OnMainThreadStartApp() {
}
void AppAdapter::OnAppStart() { assert(g_base->InLogicThread()); }
-void AppAdapter::OnAppPause() { assert(g_base->InLogicThread()); }
-void AppAdapter::OnAppResume() { assert(g_base->InLogicThread()); }
+void AppAdapter::OnAppSuspend() { assert(g_base->InLogicThread()); }
+void AppAdapter::OnAppUnsuspend() { assert(g_base->InLogicThread()); }
void AppAdapter::OnAppShutdown() { assert(g_base->InLogicThread()); }
void AppAdapter::OnAppShutdownComplete() { assert(g_base->InLogicThread()); }
void AppAdapter::OnScreenSizeChange() { assert(g_base->InLogicThread()); }
void AppAdapter::DoApplyAppConfig() { assert(g_base->InLogicThread()); }
-void AppAdapter::OnAppSuspend_() {
- assert(g_core->InMainThread());
-
- // IMPORTANT: Any pause related stuff that event-loop-threads need to do
- // should be done from their registered pause-callbacks. If we instead
- // push runnables to them from here they may or may not be called before
- // their event-loop is actually paused.
-
- // Pause all event loops.
- EventLoop::SetEventLoopsSuspended(true);
-
- if (g_base->network_reader) {
- g_base->network_reader->OnAppPause();
- }
- g_base->networking->OnAppPause();
-}
-
-void AppAdapter::OnAppUnsuspend_() {
- assert(g_core->InMainThread());
-
- // Spin all event-loops back up.
- EventLoop::SetEventLoopsSuspended(false);
-
- // Run resumes that expect to happen in the main thread.
- g_base->network_reader->OnAppResume();
- g_base->networking->OnAppResume();
-
- // When resuming from a suspended state, we may want to pause whatever
- // game was running when we last were active.
- //
- // TODO(efro): we should make this smarter so it doesn't happen if we're
- // in a network game or something that we can't pause; bringing up the
- // menu doesn't really accomplish anything there.
- //
- // In general this probably should be handled at a higher level.
- // if (g_core->should_pause_active_game) {
- // g_core->should_pause_active_game = false;
-
- // // If we've been completely backgrounded, send a menu-press command to
- // // the game; this will bring up a pause menu if we're in the game/etc.
- // if (!g_base->ui->MainMenuVisible()) {
- // g_base->ui->PushMainMenuPressCall(nullptr);
- // }
- // }
-}
-
-void AppAdapter::SuspendApp() {
- assert(g_core);
- assert(g_core->InMainThread());
-
- if (app_suspended_) {
- Log(LogLevel::kWarning,
- "AppAdapter::SuspendApp() called with app already suspended.");
- return;
- }
-
- millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()};
-
- // Apple mentioned 5 seconds to run stuff once backgrounded or they bring
- // down the hammer. Let's aim to stay under 2.
- millisecs_t max_duration{2000};
-
- g_core->platform->DebugLog(
- "SuspendApp@"
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
- app_suspended_ = true;
- OnAppSuspend_();
-
- // We assume that the OS will completely suspend our process the moment we
- // return from this call (though this is not technically true on all
- // platforms). So we want to spin and wait for threads to actually process
- // the pause message.
- size_t running_thread_count{};
- while (std::abs(core::CorePlatform::GetCurrentMillisecs() - start_time)
- < max_duration) {
- // If/when we get to a point with no threads waiting to be paused, we're
- // good to go.
- auto threads{EventLoop::GetStillSuspendingEventLoops()};
- running_thread_count = threads.size();
- if (running_thread_count == 0) {
- if (g_buildconfig.debug_build()) {
- Log(LogLevel::kDebug,
- "SuspendApp() completed in "
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()
- - start_time)
- + "ms.");
- }
- return;
- }
- }
-
- // If we made it here, we timed out. Complain.
- Log(LogLevel::kError,
- std::string("SuspendApp() took too long; ")
- + std::to_string(running_thread_count)
- + " threads not yet paused after "
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()
- - start_time)
- + " ms.");
-}
-
-void AppAdapter::UnsuspendApp() {
- assert(g_core);
- assert(g_core->InMainThread());
-
- if (!app_suspended_) {
- Log(LogLevel::kWarning,
- "AppAdapter::UnsuspendApp() called with app not in suspendedstate.");
- return;
- }
- millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()};
- g_core->platform->DebugLog(
- "UnsuspendApp@"
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
- app_suspended_ = false;
- OnAppUnsuspend_();
- if (g_buildconfig.debug_build()) {
- Log(LogLevel::kDebug,
- "UnsuspendApp() completed in "
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()
- - start_time)
- + "ms.");
- }
-}
-
void AppAdapter::RunMainThreadEventLoopToCompletion() {
FatalError("RunMainThreadEventLoopToCompletion is not implemented here.");
}
@@ -242,41 +117,6 @@ auto AppAdapter::GetGraphicsClientContext() -> GraphicsClientContext* {
auto AppAdapter::GetKeyRepeatDelay() -> float { return 0.3f; }
auto AppAdapter::GetKeyRepeatInterval() -> float { return 0.08f; }
-auto AppAdapter::ClipboardIsSupported() -> bool {
- // We only call our actual virtual function once.
- if (!have_clipboard_is_supported_) {
- clipboard_is_supported_ = DoClipboardIsSupported();
- have_clipboard_is_supported_ = true;
- }
- return clipboard_is_supported_;
-}
-
-auto AppAdapter::ClipboardHasText() -> bool {
- // If subplatform says they don't support clipboards, don't even ask.
- if (!ClipboardIsSupported()) {
- return false;
- }
- return DoClipboardHasText();
-}
-
-void AppAdapter::ClipboardSetText(const std::string& text) {
- // If subplatform says they don't support clipboards, this is an error.
- if (!ClipboardIsSupported()) {
- throw Exception("ClipboardSetText called with no clipboard support.",
- PyExcType::kRuntime);
- }
- DoClipboardSetText(text);
-}
-
-auto AppAdapter::ClipboardGetText() -> std::string {
- // If subplatform says they don't support clipboards, this is an error.
- if (!ClipboardIsSupported()) {
- throw Exception("ClipboardGetText called with no clipboard support.",
- PyExcType::kRuntime);
- }
- return DoClipboardGetText();
-}
-
auto AppAdapter::DoClipboardIsSupported() -> bool { return false; }
auto AppAdapter::DoClipboardHasText() -> bool {
@@ -311,4 +151,6 @@ void AppAdapter::NativeReviewRequest() {
void AppAdapter::DoNativeReviewRequest() { FatalError("Fixme unimplemented."); }
+auto AppAdapter::ShouldSilenceAudioForInactive() -> bool const { return false; }
+
} // namespace ballistica::base
diff --git a/src/ballistica/base/app_adapter/app_adapter.h b/src/ballistica/base/app_adapter/app_adapter.h
index f62ae114..8db58c7e 100644
--- a/src/ballistica/base/app_adapter/app_adapter.h
+++ b/src/ballistica/base/app_adapter/app_adapter.h
@@ -22,8 +22,8 @@ class AppAdapter {
// Logic thread callbacks.
virtual void OnAppStart();
- virtual void OnAppPause();
- virtual void OnAppResume();
+ virtual void OnAppSuspend();
+ virtual void OnAppUnsuspend();
virtual void OnAppShutdown();
virtual void OnAppShutdownComplete();
virtual void OnScreenSizeChange();
@@ -88,9 +88,9 @@ class AppAdapter {
/// plugged in or unplugged/etc. Default implementation returns true.
virtual auto ShouldUseCursor() -> bool;
- /// Return whether the app-adapter is having the OS show a cursor.
- /// If this returns false, the engine will take care of drawing a cursor
- /// when necessary. If true, SetHardwareCursorVisible will be called
+ /// Return whether the app-adapter is having the OS show a cursor. If this
+ /// returns false, the engine will take care of drawing a cursor when
+ /// necessary. If true, SetHardwareCursorVisible will be called
/// periodically to inform the adapter what the cursor state should be.
/// The default implementation returns false;
virtual auto HasHardwareCursor() -> bool;
@@ -106,21 +106,6 @@ class AppAdapter {
/// values.
virtual void CursorPositionForDraw(float* x, float* y);
- /// Put the app into a suspended state. Should be called from the main
- /// thread. Pauses work, closes network sockets, etc. May correspond to
- /// being backgrounded on mobile, being minimized on desktop, etc. It is
- /// assumed that, as soon as this call returns, all work is finished and
- /// all threads can be suspended by the OS without any negative side
- /// effects.
- void SuspendApp();
-
- /// Return the app to a running state from a suspended one. Can correspond
- /// to foregrounding on mobile, unminimizing on desktop, etc. Spins
- /// threads back up, re-opens network sockets, etc.
- void UnsuspendApp();
-
- auto app_suspended() const { return app_suspended_; }
-
/// Return whether this AppAdapter supports a 'fullscreen' toggle for its
/// display. This will affect whether that option is available in display
/// settings or via a hotkey. Must be called from the logic thread.
@@ -150,6 +135,15 @@ class AppAdapter {
/// Return whether this AppAdapter supports max-fps controls for its display.
virtual auto SupportsMaxFPS() -> bool const;
+ /// Return whether audio should be silenced when the app goes inactive. On
+ /// Desktop systems it is generally normal to continue to hear things even
+ /// if their windows are hidden, but on mobile we probably want to silence
+ /// our audio when phone calls, ads, etc. pop up over it. Note that this
+ /// is called each time the app goes inactive, so the adapter may choose
+ /// to selectively silence audio depending on what caused the inactive
+ /// switch.
+ virtual auto ShouldSilenceAudioForInactive() -> bool const;
+
/// Return whether this platform supports soft-quit. A soft quit is
/// when the app is reset/backgrounded/etc. but remains running in case
/// needed again. Generally this is the behavior on mobile apps.
@@ -206,22 +200,6 @@ class AppAdapter {
virtual auto GetKeyRepeatDelay() -> float;
virtual auto GetKeyRepeatInterval() -> float;
- /// Return whether clipboard operations are supported at all. This gets
- /// called when determining whether to display clipboard related UI
- /// elements/etc.
- auto ClipboardIsSupported() -> bool;
-
- /// Return whether there is currently text on the clipboard.
- auto ClipboardHasText() -> bool;
-
- /// Set current clipboard text. Raises an Exception if clipboard is
- /// unsupported.
- void ClipboardSetText(const std::string& text);
-
- /// Return current text from the clipboard. Raises an Exception if
- /// clipboard is unsupported or if there's no text on the clipboard.
- auto ClipboardGetText() -> std::string;
-
/// Push a raw pointer Runnable to the platform's 'main' thread. The main
/// thread should call its RunAndLogErrors() method and then delete it.
virtual void DoPushMainThreadRunnable(Runnable* runnable) = 0;
@@ -239,25 +217,19 @@ class AppAdapter {
/// Asynchronously kick off a native review request.
void NativeReviewRequest();
- protected:
- virtual ~AppAdapter();
-
virtual auto DoClipboardIsSupported() -> bool;
virtual auto DoClipboardHasText() -> bool;
virtual void DoClipboardSetText(const std::string& text);
virtual auto DoClipboardGetText() -> std::string;
+ protected:
+ virtual ~AppAdapter();
+
/// Override to implement native review requests. Will be called in the
/// main thread.
virtual void DoNativeReviewRequest();
private:
- void OnAppSuspend_();
- void OnAppUnsuspend_();
-
- bool app_suspended_{};
- bool have_clipboard_is_supported_{};
- bool clipboard_is_supported_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/app_adapter/app_adapter_apple.h b/src/ballistica/base/app_adapter/app_adapter_apple.h
index fa0ee058..6a51b51e 100644
--- a/src/ballistica/base/app_adapter/app_adapter_apple.h
+++ b/src/ballistica/base/app_adapter/app_adapter_apple.h
@@ -66,7 +66,6 @@ class AppAdapterApple : public AppAdapter {
private:
class ScopedAllowGraphics_;
- // void UpdateScreenSizes_();
void ReloadRenderer_(const GraphicsSettings* settings);
std::thread::id graphics_thread_{};
diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.cc b/src/ballistica/base/app_adapter/app_adapter_sdl.cc
index 13b2b698..5bc2ed08 100644
--- a/src/ballistica/base/app_adapter/app_adapter_sdl.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_sdl.cc
@@ -471,6 +471,9 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
// it to the config so that UIs can poll for it and pick up the
// change. We don't do this on other platforms where a maximized
// window is more distinctly different than a fullscreen one.
+ // Though I guess some Linux window managers have a fullscreen
+ // function so theoretically we should there. Le sigh. Maybe SDL
+ // 3 will tidy up this situation.
fullscreen_ = true;
g_base->logic->event_loop()->PushCall([] {
g_base->python->objs()
@@ -497,18 +500,22 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
break;
case SDL_WINDOWEVENT_HIDDEN: {
- // Let's keep track of when we're hidden so we can stop drawing
- // and sleep more. Theoretically we could put the app into a full
- // suspended state like we do on mobile (pausing event loops/etc.)
- // but that would be more involved; we'd need to ignore most SDL
- // events while sleeping (except for SDL_WINDOWEVENT_SHOWN) and
- // would need to rebuild our controller lists/etc when we resume.
- // For now just gonna keep things simple and keep running.
+ // We plug this into the app's overall 'Active' state so it can
+ // pause stuff or throttle down processing or whatever else.
+ if (!hidden_) {
+ g_base->SetAppActive(false);
+ }
+ // Also note that we are *completely* hidden, so we can totally
+ // stop drawing ('Inactive' app state does not imply this in and
+ // of itself).
hidden_ = true;
break;
}
case SDL_WINDOWEVENT_SHOWN: {
+ if (hidden_) {
+ g_base->SetAppActive(true);
+ }
hidden_ = false;
break;
}
diff --git a/src/ballistica/base/app_mode/app_mode.cc b/src/ballistica/base/app_mode/app_mode.cc
index e87ab5a6..4f1650fc 100644
--- a/src/ballistica/base/app_mode/app_mode.cc
+++ b/src/ballistica/base/app_mode/app_mode.cc
@@ -14,8 +14,8 @@ void AppMode::OnActivate() {}
void AppMode::OnDeactivate() {}
void AppMode::OnAppStart() {}
-void AppMode::OnAppPause() {}
-void AppMode::OnAppResume() {}
+void AppMode::OnAppSuspend() {}
+void AppMode::OnAppUnsuspend() {}
void AppMode::OnAppShutdown() {}
void AppMode::OnAppShutdownComplete() {}
diff --git a/src/ballistica/base/app_mode/app_mode.h b/src/ballistica/base/app_mode/app_mode.h
index 5a12fa0d..7f981b5a 100644
--- a/src/ballistica/base/app_mode/app_mode.h
+++ b/src/ballistica/base/app_mode/app_mode.h
@@ -26,8 +26,8 @@ class AppMode {
/// Logic thread callbacks that run while the app-mode is active.
virtual void OnAppStart();
- virtual void OnAppPause();
- virtual void OnAppResume();
+ virtual void OnAppSuspend();
+ virtual void OnAppUnsuspend();
virtual void OnAppShutdown();
virtual void OnAppShutdownComplete();
virtual void DoApplyAppConfig();
diff --git a/src/ballistica/base/audio/al_sys.h b/src/ballistica/base/audio/al_sys.h
index 475af2db..53b5ef07 100644
--- a/src/ballistica/base/audio/al_sys.h
+++ b/src/ballistica/base/audio/al_sys.h
@@ -17,7 +17,8 @@
#include
#endif
-#if BA_OSTYPE_ANDROID
+#if BA_OPENAL_IS_SOFT
+#define AL_ALEXT_PROTOTYPES
#include
#endif
diff --git a/src/ballistica/base/audio/audio.cc b/src/ballistica/base/audio/audio.cc
index dc1823f2..6e039ead 100644
--- a/src/ballistica/base/audio/audio.cc
+++ b/src/ballistica/base/audio/audio.cc
@@ -33,9 +33,9 @@ void Audio::Reset() {
void Audio::OnAppStart() { assert(g_base->InLogicThread()); }
-void Audio::OnAppPause() { assert(g_base->InLogicThread()); }
+void Audio::OnAppSuspend() { assert(g_base->InLogicThread()); }
-void Audio::OnAppResume() { assert(g_base->InLogicThread()); }
+void Audio::OnAppUnsuspend() { assert(g_base->InLogicThread()); }
void Audio::OnAppShutdown() { assert(g_base->InLogicThread()); }
diff --git a/src/ballistica/base/audio/audio.h b/src/ballistica/base/audio/audio.h
index 9c87daec..af0d646d 100644
--- a/src/ballistica/base/audio/audio.h
+++ b/src/ballistica/base/audio/audio.h
@@ -21,8 +21,8 @@ class Audio {
void Reset();
virtual void OnAppStart();
- virtual void OnAppPause();
- virtual void OnAppResume();
+ virtual void OnAppSuspend();
+ virtual void OnAppUnsuspend();
virtual void OnAppShutdown();
virtual void OnAppShutdownComplete();
virtual void DoApplyAppConfig();
diff --git a/src/ballistica/base/audio/audio_server.cc b/src/ballistica/base/audio/audio_server.cc
index fade988f..90e8e4ee 100644
--- a/src/ballistica/base/audio/audio_server.cc
+++ b/src/ballistica/base/audio/audio_server.cc
@@ -4,6 +4,7 @@
#include
+#include "ballistica/base/app_adapter/app_adapter.h"
#include "ballistica/base/assets/assets.h"
#include "ballistica/base/assets/sound_asset.h"
#include "ballistica/base/audio/al_sys.h"
@@ -27,9 +28,12 @@ namespace ballistica::base {
extern std::string g_rift_audio_device_name;
#endif
-#if BA_OSTYPE_ANDROID
+#if BA_OPENAL_IS_SOFT
LPALCDEVICEPAUSESOFT alcDevicePauseSOFT{};
LPALCDEVICERESUMESOFT alcDeviceResumeSOFT{};
+LPALCRESETDEVICESOFT alcResetDeviceSOFT{};
+LPALEVENTCALLBACKSOFT alEventCallbackSOFT{};
+LPALEVENTCONTROLSOFT alEventControlSOFT{};
#endif
const int kAudioProcessIntervalNormal{500 * 1000};
@@ -107,29 +111,24 @@ class AudioServer::ThreadSource_ : public Object {
}
private:
- bool looping_{};
- std::unique_ptr client_source_;
- float fade_{1.0f};
- float gain_{1.0f};
- AudioServer* audio_thread_{};
- bool valid_{};
- const Object::Ref* source_sound_{};
int id_{};
- uint32_t play_count_{};
+ bool looping_{};
+ bool valid_{};
bool is_actually_playing_{};
bool want_to_play_{};
-#if BA_ENABLE_AUDIO
- ALuint source_{};
-#endif
bool is_streamed_{};
-
/// Whether we should be designated as "music" next time we play.
bool is_music_{};
-
/// Whether currently playing as music.
bool current_is_music_{};
-
+ uint32_t play_count_{};
+ float fade_{1.0f};
+ float gain_{1.0f};
+ std::unique_ptr client_source_;
+ AudioServer* audio_server_{};
+ const Object::Ref* source_sound_{};
#if BA_ENABLE_AUDIO
+ ALuint source_{};
Object::Ref streamer_;
#endif
}; // ThreadSource
@@ -155,6 +154,22 @@ void AudioServer::OnMainThreadStartApp() {
event_loop_->PushCallSynchronous([this] { OnAppStartInThread_(); });
}
+#if BA_OPENAL_IS_SOFT
+static void ALEventCallback_(ALenum eventType, ALuint object, ALuint param,
+ ALsizei length, const ALchar* message,
+ ALvoid* userParam) noexcept {
+ if (eventType == AL_EVENT_TYPE_DISCONNECTED_SOFT) {
+ if (g_base->audio_server) {
+ g_base->audio_server->event_loop()->PushCall(
+ [] { g_base->audio_server->OnDeviceDisconnected(); });
+ }
+ } else {
+ Log(LogLevel::kWarning, "Got unexpected OpenAL callback event "
+ + std::to_string(static_cast(eventType)));
+ }
+}
+#endif // BA_OPENAL_IS_SOFT
+
void AudioServer::OnAppStartInThread_() {
assert(g_base->InAudioThread());
@@ -167,7 +182,7 @@ void AudioServer::OnAppStartInThread_() {
// Bring up OpenAL stuff.
{
- const char* al_device_name = nullptr;
+ const char* al_device_name{};
// On the rift build in vr mode we need to make sure we open the rift audio
// device.
@@ -211,21 +226,42 @@ void AudioServer::OnAppStartInThread_() {
"connected?");
}
impl_->alc_context = alcCreateContext(device, nullptr);
- BA_PRECONDITION(impl_->alc_context);
- BA_PRECONDITION(alcMakeContextCurrent(impl_->alc_context));
+ if (!impl_->alc_context) {
+ FatalError(
+ "Unable to init audio. Do you have speakers/headphones/etc. "
+ "connected?");
+ }
+ BA_PRECONDITION_FATAL(impl_->alc_context);
+ BA_PRECONDITION_FATAL(alcMakeContextCurrent(impl_->alc_context));
CHECK_AL_ERROR;
-#if BA_OSTYPE_ANDROID
- if (alcIsExtensionPresent(device, "ALC_SOFT_pause_device")) {
- alcDevicePauseSOFT = reinterpret_cast(
- alcGetProcAddress(device, "alcDevicePauseSOFT"));
- BA_PRECONDITION_FATAL(alcDevicePauseSOFT != nullptr);
- alcDeviceResumeSOFT = reinterpret_cast(
- alcGetProcAddress(device, "alcDeviceResumeSOFT"));
- BA_PRECONDITION_FATAL(alcDeviceResumeSOFT != nullptr);
- } else {
- FatalError("ALC_SOFT pause/resume functionality not found.");
- }
+#if BA_OPENAL_IS_SOFT
+ // Currently assuming the pause/resume and reset extensions are present.
+ // if (alcIsExtensionPresent(device, "ALC_SOFT_pause_device")) {
+ alcDevicePauseSOFT = reinterpret_cast(
+ alcGetProcAddress(device, "alcDevicePauseSOFT"));
+ BA_PRECONDITION_FATAL(alcDevicePauseSOFT != nullptr);
+ alcDeviceResumeSOFT = reinterpret_cast(
+ alcGetProcAddress(device, "alcDeviceResumeSOFT"));
+ BA_PRECONDITION_FATAL(alcDeviceResumeSOFT != nullptr);
+ alcResetDeviceSOFT = reinterpret_cast(
+ alcGetProcAddress(device, "alcResetDeviceSOFT"));
+ BA_PRECONDITION_FATAL(alcResetDeviceSOFT != nullptr);
+ alEventCallbackSOFT = reinterpret_cast(
+ alcGetProcAddress(device, "alEventCallbackSOFT"));
+ BA_PRECONDITION_FATAL(alEventCallbackSOFT != nullptr);
+ alEventControlSOFT = reinterpret_cast(
+ alcGetProcAddress(device, "alEventControlSOFT"));
+ BA_PRECONDITION_FATAL(alEventControlSOFT != nullptr);
+
+ // Ask to be notified when a device is disconnected.
+ alEventCallbackSOFT(ALEventCallback_, nullptr);
+ CHECK_AL_ERROR;
+ ALenum types[] = {AL_EVENT_TYPE_DISCONNECTED_SOFT};
+ alEventControlSOFT(1, types, AL_TRUE);
+ // } else {
+ // FatalError("ALC_SOFT pause/resume functionality not found.");
+ // }
#endif
}
@@ -259,6 +295,7 @@ void AudioServer::OnAppStartInThread_() {
// Now make available any stopped sources (should be all of them).
UpdateAvailableSources_();
+ last_started_playing_time_ = g_core->GetAppTimeSeconds();
#endif // BA_ENABLE_AUDIO
}
@@ -334,23 +371,27 @@ void AudioServer::SetSuspended_(bool suspend) {
#endif
// Pause OpenALSoft.
-#if BA_OSTYPE_ANDROID
+#if BA_OPENAL_IS_SOFT
BA_PRECONDITION_FATAL(alcDevicePauseSOFT != nullptr);
BA_PRECONDITION_FATAL(impl_ != nullptr && impl_->alc_context != nullptr);
auto* device = alcGetContextsDevice(impl_->alc_context);
BA_PRECONDITION_FATAL(device != nullptr);
try {
+ g_core->platform->LowLevelDebugLog(
+ "Calling alcDevicePauseSOFT at "
+ + std::to_string(g_core->GetAppTimeSeconds()));
alcDevicePauseSOFT(device);
} catch (const std::exception& e) {
- g_core->platform->DebugLog(
- std::string("EXC pausing alcDevice: ")
- + g_core->platform->DemangleCXXSymbol(typeid(e).name()) + " "
- + e.what());
- throw;
+ Log(LogLevel::kError,
+ "Error in alcDevicePauseSOFT at time "
+ + std::to_string(g_core->GetAppTimeSeconds())
+ + "( playing since "
+ + std::to_string(last_started_playing_time_)
+ + "): " + g_core->platform->DemangleCXXSymbol(typeid(e).name())
+ + " " + e.what());
} catch (...) {
- g_core->platform->DebugLog("UNKNOWN EXC pausing alcDevice");
- throw;
+ Log(LogLevel::kError, "Unknown error in alcDevicePauseSOFT");
}
#endif
@@ -372,25 +413,28 @@ void AudioServer::SetSuspended_(bool suspend) {
#endif
#endif
-// On android lets tell openal-soft to stop processing.
-#if BA_OSTYPE_ANDROID
+// With OpenALSoft lets tell openal-soft to resume processing.
+#if BA_OPENAL_IS_SOFT
BA_PRECONDITION_FATAL(alcDeviceResumeSOFT != nullptr);
BA_PRECONDITION_FATAL(impl_ != nullptr && impl_->alc_context != nullptr);
auto* device = alcGetContextsDevice(impl_->alc_context);
BA_PRECONDITION_FATAL(device != nullptr);
try {
+ g_core->platform->LowLevelDebugLog(
+ "Calling alcDeviceResumeSOFT at "
+ + std::to_string(g_core->GetAppTimeSeconds()));
alcDeviceResumeSOFT(device);
} catch (const std::exception& e) {
- g_core->platform->DebugLog(
- std::string("EXC resuming alcDevice: ")
- + g_core->platform->DemangleCXXSymbol(typeid(e).name()) + " "
- + e.what());
- throw;
+ Log(LogLevel::kError,
+ "Error in alcDeviceResumeSOFT at time "
+ + std::to_string(g_core->GetAppTimeSeconds()) + ": "
+ + g_core->platform->DemangleCXXSymbol(typeid(e).name()) + " "
+ + e.what());
} catch (...) {
- g_core->platform->DebugLog("UNKNOWN EXC resuming alcDevice");
- throw;
+ Log(LogLevel::kError, "Unknown error in alcDeviceResumeSOFT");
}
#endif
+ last_started_playing_time_ = g_core->GetAppTimeSeconds();
suspended_ = false;
#if BA_ENABLE_AUDIO
CHECK_AL_ERROR;
@@ -477,8 +521,8 @@ void AudioServer::PushSourcePlayCall(uint32_t play_id,
// Let's take this opportunity to pass on newly available sources.
// This way the more things clients are playing, the more
- // tight our source availability checking gets (instead of solely relying on
- // our periodic process() calls).
+ // tight our source availability checking gets (instead of solely relying
+ // on our periodic process() calls).
UpdateAvailableSources_();
});
}
@@ -685,12 +729,58 @@ void AudioServer::UpdateMusicPlayState_() {
}
}
+void AudioServer::ProcessDeviceDisconnects_(seconds_t real_time_seconds) {
+#if BA_OPENAL_IS_SOFT
+ // If our device has been disconnected, try to reconnect it
+ // periodically.
+ auto* device = alcGetContextsDevice(impl_->alc_context);
+ BA_PRECONDITION_FATAL(device != nullptr);
+ ALCint connected{-1};
+ alcGetIntegerv(device, ALC_CONNECTED, sizeof(connected), &connected);
+ CHECK_AL_ERROR;
+ if (connected == 0 && real_time_seconds - last_reset_attempt_time_ > 10.0) {
+ Log(LogLevel::kInfo, "OpenAL device disconnected; resetting...");
+ last_reset_attempt_time_ = real_time_seconds;
+ BA_PRECONDITION_FATAL(alcResetDeviceSOFT != nullptr);
+ alcResetDeviceSOFT(device, nullptr);
+ CHECK_AL_ERROR;
+
+ // Make noise if this ever fails to bring the device back.
+ ALCint connected{-1};
+ alcGetIntegerv(device, ALC_CONNECTED, sizeof(connected), &connected);
+ CHECK_AL_ERROR;
+
+ // If we were successful, don't require a wait for the next reset.
+ // (otherwise plugging in headphones and then unplugging will stay quiet
+ // for 10 seconds).
+ if (connected == 1) {
+ last_reset_attempt_time_ = -999.0;
+ }
+
+ if (connected == 0 && !reported_reset_fail_) {
+ reported_reset_fail_ = true;
+ Log(LogLevel::kError, "alcResetDeviceSOFT failed to reconnect device.");
+ }
+ }
+#endif // BA_OPENAL_IS_SOFT
+}
+
+void AudioServer::OnDeviceDisconnected() {
+ assert(g_base->InAudioThread());
+ // All we do here is run an explicit Process_. This only saves us a half
+ // second or so over letting the timer do it, but hey we'll take it.
+ Process_();
+}
+
void AudioServer::Process_() {
assert(g_base->InAudioThread());
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+ seconds_t real_time_seconds = g_core->GetAppTimeSeconds();
+ millisecs_t real_time_millisecs = real_time_seconds * 1000;
- // If we're suspended we don't do nothin'.
+ // Only do real work if we're in normal running mode.
if (!suspended_ && !shutting_down_) {
+ ProcessDeviceDisconnects_(real_time_seconds);
+
// Do some loading...
have_pending_loads_ = g_base->assets->RunPendingAudioLoads();
@@ -698,19 +788,33 @@ void AudioServer::Process_() {
UpdateAvailableSources_();
// Update our fading sound volumes.
- if (real_time - last_sound_fade_process_time_ > 50) {
+ if (real_time_millisecs - last_sound_fade_process_time_ > 50) {
ProcessSoundFades_();
- last_sound_fade_process_time_ = real_time;
+ last_sound_fade_process_time_ = real_time_millisecs;
}
// Update streaming sources.
- if (real_time - last_stream_process_time_ > 100) {
- last_stream_process_time_ = real_time;
+ if (real_time_millisecs - last_stream_process_time_ > 100) {
+ last_stream_process_time_ = real_time_millisecs;
for (auto&& i : streaming_sources_) {
i->Update();
}
}
+ // If the app has switched active/inactive state, update our volumes (we
+ // may silence our audio in these cases).
+ auto app_active = g_base->app_active();
+ if (app_active != app_active_) {
+ app_active_ = app_active;
+ app_active_volume_ =
+ (!app_active && g_base->app_adapter->ShouldSilenceAudioForInactive())
+ ? 0.0f
+ : 1.0f;
+ for (auto&& i : sources_) {
+ i->UpdateVolume();
+ }
+ }
+
#if BA_ENABLE_AUDIO
CHECK_AL_ERROR;
#endif
@@ -781,7 +885,8 @@ void AudioServer::ProcessSoundFades_() {
}
void AudioServer::FadeSoundOut(uint32_t play_id, uint32_t time) {
- // Pop a new node on the list (this won't overwrite the old if there is one).
+ // Pop a new node on the list (this won't overwrite the old if there is
+ // one).
sound_fade_nodes_.insert(
std::make_pair(play_id, SoundFadeNode_(play_id, time, true)));
}
@@ -792,9 +897,9 @@ void AudioServer::FadeSoundOut(uint32_t play_id, uint32_t time) {
// delete c;
// }
-AudioServer::ThreadSource_::ThreadSource_(AudioServer* audio_thread_in,
+AudioServer::ThreadSource_::ThreadSource_(AudioServer* audio_server_in,
int id_in, bool* valid_out)
- : id_(id_in), audio_thread_(audio_thread_in) {
+ : id_(id_in), audio_server_(audio_server_in) {
#if BA_ENABLE_AUDIO
assert(g_core);
assert(valid_out != nullptr);
@@ -839,10 +944,10 @@ AudioServer::ThreadSource_::~ThreadSource_() {
Stop();
// Remove us from sources list.
- for (auto i = audio_thread_->sources_.begin();
- i != audio_thread_->sources_.end(); ++i) {
+ for (auto i = audio_server_->sources_.begin();
+ i != audio_server_->sources_.end(); ++i) {
if (*i == this) {
- audio_thread_->sources_.erase(i);
+ audio_server_->sources_.erase(i);
break;
}
}
@@ -866,8 +971,8 @@ void AudioServer::ThreadSource_::UpdateAvailability() {
assert(g_base->InAudioThread());
- // If it's waiting to be picked up by a client or has pending client commands,
- // skip.
+ // If it's waiting to be picked up by a client or has pending client
+ // commands, skip.
if (!client_source_->TryLock(6)) {
return;
}
@@ -879,10 +984,9 @@ void AudioServer::ThreadSource_::UpdateAvailability() {
}
// We consider ourselves busy if there's an active looping play command
- // (regardless of its actual physical play state - music could be turned off,
- // stuttering, etc.).
- // If it's non-looping, we check its play state and snatch it if it's not
- // playing.
+ // (regardless of its actual physical play state - music could be turned
+ // off, stuttering, etc.). If it's non-looping, we check its play state and
+ // snatch it if it's not playing.
bool busy;
if (looping_ || (is_streamed_ && streamer_.Exists() && streamer_->loops())) {
busy = want_to_play_;
@@ -1079,12 +1183,12 @@ void AudioServer::ThreadSource_::ExecPlay() {
looping_ = false;
// Push us on the list of streaming sources if we're not on it.
- for (auto&& i : audio_thread_->streaming_sources_) {
+ for (auto&& i : audio_server_->streaming_sources_) {
if (i == this) {
throw Exception();
}
}
- audio_thread_->streaming_sources_.push_back(this);
+ audio_server_->streaming_sources_.push_back(this);
// Make sure stereo sounds aren't positional.
// This is default behavior on Mac/Win, but we enforce it for linux.
@@ -1162,10 +1266,10 @@ void AudioServer::ThreadSource_::ExecStop() {
if (streamer_.Exists()) {
assert(is_streamed_);
streamer_->Stop();
- for (auto i = audio_thread_->streaming_sources_.begin();
- i != audio_thread_->streaming_sources_.end(); ++i) {
+ for (auto i = audio_server_->streaming_sources_.begin();
+ i != audio_server_->streaming_sources_.end(); ++i) {
if (*i == this) {
- audio_thread_->streaming_sources_.erase(i);
+ audio_server_->streaming_sources_.erase(i);
break;
}
}
@@ -1182,15 +1286,16 @@ void AudioServer::ThreadSource_::ExecStop() {
void AudioServer::ThreadSource_::UpdateVolume() {
#if BA_ENABLE_AUDIO
assert(g_base->InAudioThread());
- if (g_base->audio_server->suspended_
- || g_base->audio_server->shutting_down_) {
+ if (audio_server_->suspended_ || audio_server_->shutting_down_) {
return;
}
float val = gain_ * fade_;
+ val *= audio_server_->app_active_volume_;
+
if (current_is_music()) {
- val *= audio_thread_->music_volume_ / 7.0f;
+ val *= audio_server_->music_volume_ / 7.0f;
} else {
- val *= audio_thread_->sound_volume_;
+ val *= audio_server_->sound_volume_;
}
alSourcef(source_, AL_GAIN, std::max(0.0f, val));
CHECK_AL_ERROR;
@@ -1208,7 +1313,7 @@ void AudioServer::ThreadSource_::UpdatePitch() {
float val = 1.0f;
if (current_is_music()) {
} else {
- val *= audio_thread_->sound_pitch_;
+ val *= audio_server_->sound_pitch_;
}
alSourcef(source_, AL_PITCH, val);
CHECK_AL_ERROR;
@@ -1227,16 +1332,6 @@ void AudioServer::PushSetSoundPitchCall(float val) {
event_loop()->PushCall([this, val] { SetSoundPitch_(val); });
}
-// void AudioServer::PushSetSuspendedCall(bool suspend) {
-// event_loop()->PushCall([this, suspend] {
-// if (g_buildconfig.ostype_android()) {
-// Log(LogLevel::kError, "Shouldn't be getting SetSuspendedCall on
-// android.");
-// }
-// SetSuspended_(suspend);
-// });
-// }
-
void AudioServer::PushComponentUnloadCall(
const std::vector*>& components) {
event_loop()->PushCall([components] {
diff --git a/src/ballistica/base/audio/audio_server.h b/src/ballistica/base/audio/audio_server.h
index 610b53bd..d9919c0a 100644
--- a/src/ballistica/base/audio/audio_server.h
+++ b/src/ballistica/base/audio/audio_server.h
@@ -67,6 +67,8 @@ class AudioServer {
auto event_loop() const -> EventLoop* { return event_loop_; }
+ void OnDeviceDisconnected();
+
private:
class ThreadSource_;
struct Impl_;
@@ -90,6 +92,7 @@ class AudioServer {
void Reset_();
void Process_();
+ void ProcessDeviceDisconnects_(seconds_t real_time_seconds);
/// Send a component to the audio thread to delete.
// void DeleteAssetComponent_(Asset* c);
@@ -115,12 +118,18 @@ class AudioServer {
float sound_volume_{1.0f};
float sound_pitch_{1.0f};
float music_volume_{1.0f};
+ float app_active_volume_{1.0f};
bool have_pending_loads_{};
+ bool app_active_{true};
bool suspended_{};
bool shutdown_completed_{};
bool shutting_down_{};
+ bool reported_reset_fail_{};
+ int al_source_count_{};
+ seconds_t last_reset_attempt_time_{-999.0};
seconds_t shutdown_start_time_{};
+ seconds_t last_started_playing_time_{};
millisecs_t last_sound_fade_process_time_{};
/// Indexed list of sources.
@@ -144,8 +153,6 @@ class AudioServer {
// Our list of sound media components to delete via the main thread.
std::vector*> sound_ref_delete_list_;
-
- int al_source_count_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc
index dc696467..a174c575 100644
--- a/src/ballistica/base/base.cc
+++ b/src/ballistica/base/base.cc
@@ -195,9 +195,17 @@ void BaseFeatureSet::OnAssetsAvailable() {
}
void BaseFeatureSet::StartApp() {
+ // {
+ // // TEST - recreate the ID python dumps in its thread tracebacks.
+ // auto val = PyThread_get_thread_ident();
+ // printf("MAIN THREAD IS %#018lx\n", val);
+ // }
+
BA_PRECONDITION(g_core->InMainThread());
BA_PRECONDITION(g_base);
+ auto start_time = g_core->GetAppTimeSeconds();
+
// Currently limiting this to once per process.
BA_PRECONDITION(!called_start_app_);
called_start_app_ = true;
@@ -248,6 +256,177 @@ void BaseFeatureSet::StartApp() {
}
g_core->LifecycleLog("start-app end (main thread)");
+
+ // Make some noise if this takes more than a few seconds. If we pass 5
+ // seconds or so we start to trigger App-Not-Responding reports which
+ // isn't good.
+ auto duration = g_core->GetAppTimeSeconds() - start_time;
+ if (duration > 3.0) {
+ char buffer[128];
+ snprintf(buffer, sizeof(buffer),
+ "StartApp() took too long (%.2lf seconds).", duration);
+ Log(LogLevel::kWarning, buffer);
+ }
+}
+
+void BaseFeatureSet::SuspendApp() {
+ assert(g_core);
+ assert(g_core->InMainThread());
+
+ if (app_suspended_) {
+ Log(LogLevel::kWarning,
+ "AppAdapter::SuspendApp() called with app already suspended.");
+ return;
+ }
+
+ millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()};
+
+ // Apple mentioned 5 seconds to run stuff once backgrounded or they bring
+ // down the hammer. Let's aim to stay under 2.
+ millisecs_t max_duration{2000};
+
+ g_core->platform->LowLevelDebugLog(
+ "SuspendApp@"
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
+ app_suspended_ = true;
+
+ // IMPORTANT: Any pause related stuff that event-loop-threads need to do
+ // should be done from their registered pause-callbacks. If we instead
+ // push runnables to them from here they may or may not be called before
+ // their event-loop is actually paused.
+
+ // Pause all event loops.
+ EventLoop::SetEventLoopsSuspended(true);
+
+ if (g_base->network_reader) {
+ g_base->network_reader->OnAppSuspend();
+ }
+ g_base->networking->OnAppSuspend();
+
+ // We assume that the OS will completely suspend our process the moment we
+ // return from this call (though this is not technically true on all
+ // platforms). So we want to spin here and give our various event loop
+ // threads time to park themselves.
+ std::vector running_loops;
+ do {
+ // If/when we get to a point with no threads waiting to be paused, we're
+ // good to go.
+ // auto loops{EventLoop::GetStillSuspendingEventLoops()};
+ running_loops = EventLoop::GetStillSuspendingEventLoops();
+ // running_loop_count = loops.size();
+ if (running_loops.empty()) {
+ if (g_buildconfig.debug_build()) {
+ Log(LogLevel::kDebug,
+ "SuspendApp() completed in "
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs()
+ - start_time)
+ + "ms.");
+ }
+ return;
+ }
+ } while (std::abs(core::CorePlatform::GetCurrentMillisecs() - start_time)
+ < max_duration);
+
+ // If we made it here, we timed out. Complain.
+ std::string msg =
+ std::string("SuspendApp() took too long; ")
+ + std::to_string(running_loops.size())
+ + " event-loops not yet suspended after "
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs() - start_time)
+ + " ms: (";
+ bool first = true;
+ for (auto* loop : running_loops) {
+ if (!first) {
+ msg += ", ";
+ }
+ // Note: not adding a default here so compiler complains if we
+ // add/change something.
+ switch (loop->identifier()) {
+ case EventLoopID::kInvalid:
+ msg += "invalid";
+ break;
+ case EventLoopID::kLogic:
+ msg += "logic";
+ break;
+ case EventLoopID::kAssets:
+ msg += "assets";
+ break;
+ case EventLoopID::kFileOut:
+ msg += "fileout";
+ break;
+ case EventLoopID::kMain:
+ msg += "main";
+ break;
+ case EventLoopID::kAudio:
+ msg += "audio";
+ break;
+ case EventLoopID::kNetworkWrite:
+ msg += "networkwrite";
+ break;
+ case EventLoopID::kSuicide:
+ msg += "suicide";
+ break;
+ case EventLoopID::kStdin:
+ msg += "stdin";
+ break;
+ case EventLoopID::kBGDynamics:
+ msg += "bgdynamics";
+ break;
+ }
+ first = false;
+ }
+ msg += ").";
+
+ Log(LogLevel::kError, msg);
+}
+
+void BaseFeatureSet::UnsuspendApp() {
+ assert(g_core);
+ assert(g_core->InMainThread());
+
+ if (!app_suspended_) {
+ Log(LogLevel::kWarning,
+ "AppAdapter::UnsuspendApp() called with app not in suspendedstate.");
+ return;
+ }
+ millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()};
+ g_core->platform->LowLevelDebugLog(
+ "UnsuspendApp@"
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
+ app_suspended_ = false;
+
+ // Spin all event-loops back up.
+ EventLoop::SetEventLoopsSuspended(false);
+
+ // Run resumes that expect to happen in the main thread.
+ g_base->network_reader->OnAppUnsuspend();
+ g_base->networking->OnAppUnsuspend();
+
+ // When resuming from a suspended state, we may want to pause whatever
+ // game was running when we last were active.
+ //
+ // TODO(efro): we should make this smarter so it doesn't happen if we're
+ // in a network game or something that we can't pause; bringing up the
+ // menu doesn't really accomplish anything there.
+ //
+ // In general this probably should be handled at a higher level.
+ // if (g_core->should_pause_active_game) {
+ // g_core->should_pause_active_game = false;
+
+ // // If we've been completely backgrounded, send a menu-press command to
+ // // the game; this will bring up a pause menu if we're in the game/etc.
+ // if (!g_base->ui->MainMenuVisible()) {
+ // g_base->ui->PushMainMenuPressCall(nullptr);
+ // }
+ // }
+
+ if (g_buildconfig.debug_build()) {
+ Log(LogLevel::kDebug,
+ "UnsuspendApp() completed in "
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs()
+ - start_time)
+ + "ms.");
+ }
}
void BaseFeatureSet::OnAppShutdownComplete() {
@@ -730,8 +909,6 @@ void BaseFeatureSet::ShutdownSuppressDisallow() {
shutdown_suppress_disallowed_ = true;
}
-// auto BaseFeatureSet::GetReturnValue() const -> int { return return_value(); }
-
void BaseFeatureSet::QuitApp(bool confirm, QuitType quit_type) {
// If they want a confirm dialog and we're able to present one, do that.
if (confirm && !g_core->HeadlessMode() && !g_base->input->IsInputLocked()
@@ -760,4 +937,57 @@ void BaseFeatureSet::PushMainThreadRunnable(Runnable* runnable) {
app_adapter->DoPushMainThreadRunnable(runnable);
}
+auto BaseFeatureSet::ClipboardIsSupported() -> bool {
+ // We only call our actual virtual function once.
+ if (!have_clipboard_is_supported_) {
+ clipboard_is_supported_ = app_adapter->DoClipboardIsSupported();
+ have_clipboard_is_supported_ = true;
+ }
+ return clipboard_is_supported_;
+}
+
+auto BaseFeatureSet::ClipboardHasText() -> bool {
+ // If subplatform says they don't support clipboards, don't even ask.
+ if (!ClipboardIsSupported()) {
+ return false;
+ }
+ return app_adapter->DoClipboardHasText();
+}
+
+void BaseFeatureSet::ClipboardSetText(const std::string& text) {
+ // If subplatform says they don't support clipboards, this is an error.
+ if (!ClipboardIsSupported()) {
+ throw Exception("ClipboardSetText called with no clipboard support.",
+ PyExcType::kRuntime);
+ }
+ app_adapter->DoClipboardSetText(text);
+}
+
+auto BaseFeatureSet::ClipboardGetText() -> std::string {
+ // If subplatform says they don't support clipboards, this is an error.
+ if (!ClipboardIsSupported()) {
+ throw Exception("ClipboardGetText called with no clipboard support.",
+ PyExcType::kRuntime);
+ }
+ return app_adapter->DoClipboardGetText();
+}
+
+void BaseFeatureSet::SetAppActive(bool active) {
+ assert(InMainThread());
+ g_core->platform->LowLevelDebugLog(
+ "SetAppActive(" + std::to_string(active) + ")@"
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
+
+ printf("APP ACTIVE %d\n", static_cast(active));
+
+ // Issue a gentle warning if they are feeding us the same state twice in a
+ // row; might imply faulty logic.
+ if (app_active_set_ && app_active_ == active) {
+ Log(LogLevel::kWarning, "SetAppActive called with state "
+ + std::to_string(active) + " twice in a row.");
+ }
+ app_active_set_ = true;
+ app_active_ = active;
+}
+
} // namespace ballistica::base
diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h
index a14ddee1..5bea0e93 100644
--- a/src/ballistica/base/base.h
+++ b/src/ballistica/base/base.h
@@ -609,6 +609,31 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// Start app systems in motion.
void StartApp() override;
+ /// Set the app's active state. Should be called from the main thread.
+ /// Generally called by the AppAdapter. Being inactive means the app
+ /// experience is not front and center and thus it may want to throttle
+ /// down its rendering rate, pause single play gameplay, etc. This does
+ /// not, however, cause any extreme action such as halting event loops;
+ /// use Suspend/Resume for that. And note that the app may still be
+ /// visible while inactive, so it should not *completely* stop
+ /// drawing/etc.
+ void SetAppActive(bool active);
+
+ /// Put the app into a suspended state. Should be called from the main
+ /// thread. Generally called by the AppAdapter. Suspends event loops,
+ /// closes network sockets, etc. Generally corresponds to being
+ /// backgrounded on mobile platforms. It is assumed that, as soon as this
+ /// call returns, all engine work is finished and all threads can be
+ /// immediately suspended by the OS without any problems.
+ void SuspendApp();
+
+ /// Return the app to a running state from a suspended one. Can correspond
+ /// to foregrounding on mobile, unminimizing on desktop, etc. Spins
+ /// threads back up, re-opens network sockets, etc.
+ void UnsuspendApp();
+
+ auto app_suspended() const { return app_suspended_; }
+
/// 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;
@@ -738,6 +763,22 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// reported by the Python layer.
auto GetV2AccountID() -> std::optional;
+ /// Return whether clipboard operations are supported at all. This gets
+ /// called when determining whether to display clipboard related UI
+ /// elements/etc.
+ auto ClipboardIsSupported() -> bool;
+
+ /// Return whether there is currently text on the clipboard.
+ auto ClipboardHasText() -> bool;
+
+ /// Set current clipboard text. Raises an Exception if clipboard is
+ /// unsupported.
+ void ClipboardSetText(const std::string& text);
+
+ /// Return current text from the clipboard. Raises an Exception if
+ /// clipboard is unsupported or if there's no text on the clipboard.
+ auto ClipboardGetText() -> std::string;
+
// Const subsystems.
AppAdapter* const app_adapter;
AppConfig* const app_config;
@@ -774,10 +815,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
// Non-const bits (fixme: clean up access to these).
TouchInput* touch_input{};
- // auto return_value() const { return return_value_; }
- // void set_return_value(int val) { return_value_ = val; }
-
- // auto GetReturnValue() const -> int override;
+ auto app_active() const { return app_active_; }
private:
BaseFeatureSet();
@@ -789,8 +827,12 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
AppMode* app_mode_;
PlusSoftInterface* plus_soft_{};
ClassicSoftInterface* classic_soft_{};
-
std::mutex shutdown_suppress_lock_;
+ bool have_clipboard_is_supported_{};
+ bool clipboard_is_supported_{};
+ bool app_active_set_{};
+ bool app_active_{true};
+ bool app_suspended_{};
bool shutdown_suppress_disallowed_{};
bool tried_importing_plus_{};
bool tried_importing_classic_{};
@@ -803,7 +845,6 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
bool basn_log_behavior_{};
bool server_wrapper_managed_{};
int shutdown_suppress_count_{};
- // int return_value_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc
index c5d920d3..53871876 100644
--- a/src/ballistica/base/graphics/graphics.cc
+++ b/src/ballistica/base/graphics/graphics.cc
@@ -83,12 +83,12 @@ Graphics::~Graphics() = default;
void Graphics::OnAppStart() { assert(g_base->InLogicThread()); }
-void Graphics::OnAppPause() {
+void Graphics::OnAppSuspend() {
assert(g_base->InLogicThread());
SetGyroEnabled(false);
}
-void Graphics::OnAppResume() {
+void Graphics::OnAppUnsuspend() {
assert(g_base->InLogicThread());
g_base->graphics->SetGyroEnabled(true);
}
@@ -615,7 +615,7 @@ void Graphics::FadeScreen(bool to, millisecs_t time, PyObject* endcall) {
Log(LogLevel::kWarning,
"2 fades overlapping; running first fade-end-call early.");
}
- fade_end_call_->ScheduleOnce();
+ fade_end_call_->Schedule();
fade_end_call_.Clear();
}
set_fade_start_on_next_draw_ = true;
@@ -993,7 +993,10 @@ void Graphics::DrawFades(FrameDef* frame_def) {
// Guard against accidental fades that never fade back in.
if (fade_ <= 0.0f && fade_out_) {
millisecs_t faded_time = real_time - (fade_start_ + fade_time_);
- if (faded_time > 15000) {
+
+ // TEMP HACK - don't trigger this while inactive.
+ // Need to make overall fade logic smarter.
+ if (faded_time > 15000 && g_base->app_active()) {
Log(LogLevel::kError, "FORCE-ENDING STUCK FADE");
fade_out_ = false;
fade_ = 1.0f;
@@ -1021,7 +1024,7 @@ void Graphics::DrawFades(FrameDef* frame_def) {
} else {
fade_ = 0;
if (!was_done && fade_end_call_.Exists()) {
- fade_end_call_->ScheduleOnce();
+ fade_end_call_->Schedule();
fade_end_call_.Clear();
}
}
diff --git a/src/ballistica/base/graphics/graphics.h b/src/ballistica/base/graphics/graphics.h
index c56ae98f..93ef8310 100644
--- a/src/ballistica/base/graphics/graphics.h
+++ b/src/ballistica/base/graphics/graphics.h
@@ -56,8 +56,8 @@ class Graphics {
Graphics();
void OnAppStart();
- void OnAppPause();
- void OnAppResume();
+ void OnAppSuspend();
+ void OnAppUnsuspend();
void OnAppShutdown();
void OnAppShutdownComplete();
void OnScreenSizeChange();
diff --git a/src/ballistica/base/graphics/graphics_server.cc b/src/ballistica/base/graphics/graphics_server.cc
index 4e076291..63220332 100644
--- a/src/ballistica/base/graphics/graphics_server.cc
+++ b/src/ballistica/base/graphics/graphics_server.cc
@@ -143,7 +143,7 @@ auto GraphicsServer::WaitForRenderFrameDef_() -> FrameDef* {
// Spin and wait for a short bit for a frame_def to appear.
while (true) {
// Stop waiting if we can't/shouldn't render anyway.
- if (!renderer_ || shutting_down_ || g_base->app_adapter->app_suspended()) {
+ if (!renderer_ || shutting_down_ || g_base->app_suspended()) {
return nullptr;
}
diff --git a/src/ballistica/base/graphics/text/text_group.h b/src/ballistica/base/graphics/text/text_group.h
index 86f9121b..7a65a8ee 100644
--- a/src/ballistica/base/graphics/text/text_group.h
+++ b/src/ballistica/base/graphics/text/text_group.h
@@ -87,7 +87,7 @@ class TextGroup : public Object {
Object::Ref os_texture_;
std::vector> entries_;
std::string text_;
- bool big_;
+ bool big_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/input/input.cc b/src/ballistica/base/input/input.cc
index e741a7f3..939e1c59 100644
--- a/src/ballistica/base/input/input.cc
+++ b/src/ballistica/base/input/input.cc
@@ -155,27 +155,24 @@ void Input::AnnounceConnects_() {
if (first_print && g_core->GetAppTimeSeconds() < 3.0) {
first_print = false;
- // Disabling this completely on Android for now; we often get large
- // numbers of devices there that aren't actually devices.
- bool do_print_initial_counts{!g_buildconfig.ostype_android()};
-
// If there's been several connected, just give a number.
- if (explicit_bool(do_print_initial_counts)) {
- if (newly_connected_controllers_.size() > 1) {
- std::string s =
- g_base->assets->GetResourceString("controllersDetectedText");
- Utils::StringReplaceOne(
- &s, "${COUNT}",
- std::to_string(newly_connected_controllers_.size()));
- ScreenMessage(s);
- } else {
- ScreenMessage(
- g_base->assets->GetResourceString("controllerDetectedText"));
- }
+ if (newly_connected_controllers_.size() > 1) {
+ std::string s =
+ g_base->assets->GetResourceString("controllersDetectedText");
+ Utils::StringReplaceOne(
+ &s, "${COUNT}", std::to_string(newly_connected_controllers_.size()));
+ ScreenMessage(s);
+ } else {
+ ScreenMessage(
+ g_base->assets->GetResourceString("controllerDetectedText"));
}
+
} else {
// If there's been several connected, just give a number.
if (newly_connected_controllers_.size() > 1) {
+ for (auto&& s : newly_connected_controllers_) {
+ Log(LogLevel::kInfo, "GOT CONTROLLER " + s);
+ }
std::string s =
g_base->assets->GetResourceString("controllersConnectedText");
Utils::StringReplaceOne(
@@ -193,7 +190,6 @@ void Input::AnnounceConnects_() {
g_base->audio->PlaySound(g_base->assets->SysSound(SysSoundID::kGunCock));
}
}
-
newly_connected_controllers_.clear();
}
@@ -222,6 +218,14 @@ void Input::AnnounceDisconnects_() {
void Input::ShowStandardInputDeviceConnectedMessage_(InputDevice* j) {
assert(g_base->InLogicThread());
+
+ // On Android we never show messages for initial input-devices; we often
+ // get large numbers of strange virtual devices that aren't actually
+ // controllers so this is more confusing than helpful.
+ if (g_buildconfig.ostype_android() && g_core->GetAppTimeSeconds() < 3.0) {
+ return;
+ }
+
std::string suffix;
suffix += j->GetPersistentIdentifier();
suffix += j->GetDeviceExtraDescription();
@@ -516,9 +520,9 @@ void Input::OnAppStart() {
}
}
-void Input::OnAppPause() { assert(g_base->InLogicThread()); }
+void Input::OnAppSuspend() { assert(g_base->InLogicThread()); }
-void Input::OnAppResume() { assert(g_base->InLogicThread()); }
+void Input::OnAppUnsuspend() { assert(g_base->InLogicThread()); }
void Input::OnAppShutdown() { assert(g_base->InLogicThread()); }
@@ -1239,7 +1243,14 @@ void Input::HandleSmoothMouseScroll_(const Vector2f& velocity, bool momentum) {
}
void Input::PushMouseMotionEvent(const Vector2f& position) {
- assert(g_base->logic->event_loop());
+ auto* loop = g_base->logic->event_loop();
+ assert(loop);
+
+ // Don't overload it with events if it's stuck.
+ if (!loop->CheckPushSafety()) {
+ return;
+ }
+
g_base->logic->event_loop()->PushCall(
[this, position] { HandleMouseMotion_(position); });
}
diff --git a/src/ballistica/base/input/input.h b/src/ballistica/base/input/input.h
index e4819753..a20a619d 100644
--- a/src/ballistica/base/input/input.h
+++ b/src/ballistica/base/input/input.h
@@ -21,8 +21,8 @@ class Input {
Input();
void OnAppStart();
- void OnAppPause();
- void OnAppResume();
+ void OnAppSuspend();
+ void OnAppUnsuspend();
void OnAppShutdown();
void OnAppShutdownComplete();
void StepDisplayTime();
diff --git a/src/ballistica/base/input/support/remote_app_server.cc b/src/ballistica/base/input/support/remote_app_server.cc
index d5f78b95..268cb848 100644
--- a/src/ballistica/base/input/support/remote_app_server.cc
+++ b/src/ballistica/base/input/support/remote_app_server.cc
@@ -376,8 +376,10 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr,
Vector3f(1, 1, 1));
});
g_base->logic->event_loop()->PushCall([] {
- g_base->audio->PlaySound(
- g_base->assets->SysSound(SysSoundID::kGunCock));
+ if (g_base->assets->asset_loads_allowed()) {
+ g_base->audio->PlaySound(
+ g_base->assets->SysSound(SysSoundID::kGunCock));
+ }
});
}
clients_[i].in_use = true;
@@ -426,9 +428,12 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr,
});
g_base->logic->event_loop()->PushCall([] {
- g_base->audio->PlaySound(
- g_base->assets->SysSound(SysSoundID::kGunCock));
+ if (g_base->assets->asset_loads_allowed()) {
+ g_base->audio->PlaySound(
+ g_base->assets->SysSound(SysSoundID::kGunCock));
+ }
});
+
std::string utf8 = Utils::GetValidUTF8(clients_[i].display_name, "rsgc1");
clients_[i].joystick_ = Object::NewDeferred(
-1, // not an sdl joystick
diff --git a/src/ballistica/base/logic/logic.cc b/src/ballistica/base/logic/logic.cc
index 909a8ddd..13dfef5b 100644
--- a/src/ballistica/base/logic/logic.cc
+++ b/src/ballistica/base/logic/logic.cc
@@ -48,9 +48,9 @@ void Logic::OnAppStart() {
// Stay informed when our event loop is pausing/unpausing.
event_loop_->AddSuspendCallback(
- NewLambdaRunnableUnmanaged([this] { OnAppPause(); }));
+ NewLambdaRunnableUnmanaged([this] { OnAppSuspend(); }));
event_loop_->AddUnsuspendCallback(
- NewLambdaRunnableUnmanaged([this] { OnAppResume(); }));
+ NewLambdaRunnableUnmanaged([this] { OnAppUnsuspend(); }));
// Running in a specific order here and should try to stick to it in
// other OnAppXXX callbacks so any subsystem interdependencies behave
@@ -179,40 +179,40 @@ void Logic::OnInitialAppModeSet() {
}
}
-void Logic::OnAppPause() {
+void Logic::OnAppSuspend() {
assert(g_base->InLogicThread());
assert(g_base->CurrentContext().IsEmpty());
// Note: keep these in opposite order of OnAppStart.
- g_base->python->OnAppPause();
+ g_base->python->OnAppSuspend();
if (g_base->HavePlus()) {
- g_base->plus()->OnAppPause();
+ g_base->plus()->OnAppSuspend();
}
- g_base->app_mode()->OnAppPause();
- g_base->ui->OnAppPause();
- g_base->input->OnAppPause();
- g_base->audio->OnAppPause();
- g_base->graphics->OnAppPause();
- g_base->platform->OnAppPause();
- g_base->app_adapter->OnAppPause();
+ g_base->app_mode()->OnAppSuspend();
+ g_base->ui->OnAppSuspend();
+ g_base->input->OnAppSuspend();
+ g_base->audio->OnAppSuspend();
+ g_base->graphics->OnAppSuspend();
+ g_base->platform->OnAppSuspend();
+ g_base->app_adapter->OnAppSuspend();
}
-void Logic::OnAppResume() {
+void Logic::OnAppUnsuspend() {
assert(g_base->InLogicThread());
assert(g_base->CurrentContext().IsEmpty());
// Note: keep these in the same order as OnAppStart.
- g_base->app_adapter->OnAppResume();
- g_base->platform->OnAppResume();
- g_base->graphics->OnAppResume();
- g_base->audio->OnAppResume();
- g_base->input->OnAppResume();
- g_base->ui->OnAppResume();
- g_base->app_mode()->OnAppResume();
+ g_base->app_adapter->OnAppUnsuspend();
+ g_base->platform->OnAppUnsuspend();
+ g_base->graphics->OnAppUnsuspend();
+ g_base->audio->OnAppUnsuspend();
+ g_base->input->OnAppUnsuspend();
+ g_base->ui->OnAppUnsuspend();
+ g_base->app_mode()->OnAppUnsuspend();
if (g_base->HavePlus()) {
- g_base->plus()->OnAppResume();
+ g_base->plus()->OnAppUnsuspend();
}
- g_base->python->OnAppResume();
+ g_base->python->OnAppUnsuspend();
}
void Logic::Shutdown() {
diff --git a/src/ballistica/base/logic/logic.h b/src/ballistica/base/logic/logic.h
index 06e29f69..32872e63 100644
--- a/src/ballistica/base/logic/logic.h
+++ b/src/ballistica/base/logic/logic.h
@@ -52,11 +52,11 @@ class Logic {
/// Called when our event-loop pauses. Informs Python and other
/// subsystems.
- void OnAppPause();
+ void OnAppSuspend();
/// Called when our event-loop resumes. Informs Python and other
/// subsystems.
- void OnAppResume();
+ void OnAppUnsuspend();
void OnAppShutdown();
void OnAppShutdownComplete();
diff --git a/src/ballistica/base/networking/network_reader.cc b/src/ballistica/base/networking/network_reader.cc
index 7c3e0392..0c44e9e1 100644
--- a/src/ballistica/base/networking/network_reader.cc
+++ b/src/ballistica/base/networking/network_reader.cc
@@ -25,7 +25,7 @@ void NetworkReader::SetPort(int port) {
thread_ = new std::thread(RunThreadStatic_, this);
}
-void NetworkReader::OnAppPause() {
+void NetworkReader::OnAppSuspend() {
assert(g_core->InMainThread());
assert(!paused_);
{
@@ -33,16 +33,14 @@ void NetworkReader::OnAppPause() {
paused_ = true;
}
- // Ok now attempt to send a quick ping to ourself to wake us up so we can kill
- // our socket.
+ // It's possible that we get suspended before port is set, so this could
+ // still be -1.
if (port4_ != -1) {
PokeSelf_();
- } else {
- Log(LogLevel::kError, "NetworkReader port is -1 on pause");
}
}
-void NetworkReader::OnAppResume() {
+void NetworkReader::OnAppUnsuspend() {
assert(g_core->InMainThread());
assert(paused_);
diff --git a/src/ballistica/base/networking/network_reader.h b/src/ballistica/base/networking/network_reader.h
index d0494161..7b1798c6 100644
--- a/src/ballistica/base/networking/network_reader.h
+++ b/src/ballistica/base/networking/network_reader.h
@@ -24,8 +24,8 @@ class NetworkReader {
public:
NetworkReader();
void SetPort(int port);
- void OnAppPause();
- void OnAppResume();
+ void OnAppSuspend();
+ void OnAppUnsuspend();
auto port4() const { return port4_; }
auto port6() const { return port6_; }
auto sd_mutex() -> std::mutex& { return sd_mutex_; }
diff --git a/src/ballistica/base/networking/networking.cc b/src/ballistica/base/networking/networking.cc
index cf672073..3b83f698 100644
--- a/src/ballistica/base/networking/networking.cc
+++ b/src/ballistica/base/networking/networking.cc
@@ -31,9 +31,9 @@ void Networking::DoApplyAppConfig() {
}
}
-void Networking::OnAppPause() {}
+void Networking::OnAppSuspend() {}
-void Networking::OnAppResume() {}
+void Networking::OnAppUnsuspend() {}
void Networking::SendTo(const std::vector& buffer,
const SockAddr& addr) {
@@ -50,7 +50,7 @@ void Networking::SendTo(const std::vector& buffer,
if (sd != -1) {
sendto(sd, (const char*)&buffer[0],
static_cast_check_fit(buffer.size()), 0,
- addr.GetSockAddr(), addr.GetSockAddrLen());
+ addr.AsSockAddr(), addr.GetSockAddrLen());
}
}
diff --git a/src/ballistica/base/networking/networking.h b/src/ballistica/base/networking/networking.h
index 70aea41c..833b732f 100644
--- a/src/ballistica/base/networking/networking.h
+++ b/src/ballistica/base/networking/networking.h
@@ -127,8 +127,8 @@ class Networking {
// Called on mobile platforms when going into the background, etc
// (when all networking should be shut down)
- void OnAppPause();
- void OnAppResume();
+ void OnAppSuspend();
+ void OnAppUnsuspend();
auto remote_server_accepting_connections() -> bool {
return remote_server_accepting_connections_;
diff --git a/src/ballistica/base/platform/base_platform.cc b/src/ballistica/base/platform/base_platform.cc
index 0af9a65b..f05d2b26 100644
--- a/src/ballistica/base/platform/base_platform.cc
+++ b/src/ballistica/base/platform/base_platform.cc
@@ -58,8 +58,8 @@ auto BasePlatform::GetPublicDeviceUUID() -> std::string {
// We used to plug version in directly here, but that caused uuids to
// shuffle too rapidly during periods of rapid development. This
// keeps it more constant.
- // __last_rand_uuid_component_shuffle_date__ 2023 6 15
- auto rand_uuid_component{"JVRWZ82D4WMBO110OA0IFJV7JKMQV8W3"};
+ // __last_rand_uuid_component_shuffle_date__ 2023 12 13
+ auto rand_uuid_component{"7YM96RZHN6ZCPZGTQONULZO1JU5NMMC7"};
inputs.emplace_back(rand_uuid_component);
auto gil{Python::ScopedInterpreterLock()};
@@ -166,8 +166,8 @@ void BasePlatform::SetupInterruptHandling() {
}
void BasePlatform::OnAppStart() { assert(g_base->InLogicThread()); }
-void BasePlatform::OnAppPause() { assert(g_base->InLogicThread()); }
-void BasePlatform::OnAppResume() { assert(g_base->InLogicThread()); }
+void BasePlatform::OnAppSuspend() { assert(g_base->InLogicThread()); }
+void BasePlatform::OnAppUnsuspend() { assert(g_base->InLogicThread()); }
void BasePlatform::OnAppShutdown() { assert(g_base->InLogicThread()); }
void BasePlatform::OnAppShutdownComplete() { assert(g_base->InLogicThread()); }
void BasePlatform::OnScreenSizeChange() { assert(g_base->InLogicThread()); }
diff --git a/src/ballistica/base/platform/base_platform.h b/src/ballistica/base/platform/base_platform.h
index 3609e9cb..b38716ff 100644
--- a/src/ballistica/base/platform/base_platform.h
+++ b/src/ballistica/base/platform/base_platform.h
@@ -26,8 +26,8 @@ class BasePlatform {
// Logic thread callbacks.
virtual void OnAppStart();
- virtual void OnAppPause();
- virtual void OnAppResume();
+ virtual void OnAppSuspend();
+ virtual void OnAppUnsuspend();
virtual void OnAppShutdown();
virtual void OnAppShutdownComplete();
virtual void OnScreenSizeChange();
diff --git a/src/ballistica/base/platform/support/min_sdl_key_names.h b/src/ballistica/base/platform/support/min_sdl_key_names.h
index 57eae886..64c57704 100644
--- a/src/ballistica/base/platform/support/min_sdl_key_names.h
+++ b/src/ballistica/base/platform/support/min_sdl_key_names.h
@@ -82,7 +82,7 @@ static const char* const scancode_names[SDL_NUM_SCANCODES] = {
"F12",
"PrintScreen",
"ScrollLock",
- "OnAppPause",
+ "Pause",
"Insert",
"Home",
"PageUp",
diff --git a/src/ballistica/base/python/base_python.cc b/src/ballistica/base/python/base_python.cc
index 78545b12..24b9c182 100644
--- a/src/ballistica/base/python/base_python.cc
+++ b/src/ballistica/base/python/base_python.cc
@@ -149,12 +149,12 @@ void BasePython::OnAppStart() {
objs().Get(BasePython::ObjID::kAppOnNativeStartCall).Call();
}
-void BasePython::OnAppPause() {
+void BasePython::OnAppSuspend() {
assert(g_base->InLogicThread());
objs().Get(BasePython::ObjID::kAppOnNativeSuspendCall).Call();
}
-void BasePython::OnAppResume() {
+void BasePython::OnAppUnsuspend() {
assert(g_base->InLogicThread());
objs().Get(BasePython::ObjID::kAppOnNativeUnsuspendCall).Call();
}
diff --git a/src/ballistica/base/python/base_python.h b/src/ballistica/base/python/base_python.h
index 5646cabe..870da85a 100644
--- a/src/ballistica/base/python/base_python.h
+++ b/src/ballistica/base/python/base_python.h
@@ -15,8 +15,8 @@ class BasePython {
void OnMainThreadStartApp();
void OnAppStart();
- void OnAppPause();
- void OnAppResume();
+ void OnAppSuspend();
+ void OnAppUnsuspend();
void OnAppShutdown();
void OnAppShutdownComplete();
void DoApplyAppConfig();
diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc
index 8815316c..3ce2f238 100644
--- a/src/ballistica/base/python/methods/python_methods_app.cc
+++ b/src/ballistica/base/python/methods/python_methods_app.cc
@@ -44,6 +44,27 @@ static PyMethodDef PyAppNameDef = {
"(internal)\n",
};
+// ------------------------------ app_is_active --------------------------------
+
+static auto PyAppIsActive(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+
+ if (g_base->app_active()) {
+ Py_RETURN_TRUE;
+ }
+ Py_RETURN_FALSE;
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyAppIsActiveDef = {
+ "app_is_active", // name
+ (PyCFunction)PyAppIsActive, // method
+ METH_NOARGS, // flags
+
+ "app_is_active() -> bool\n"
+ "\n"
+ "(internal)\n",
+};
// --------------------------------- run_app -----------------------------------
static auto PyRunApp(PyObject* self) -> PyObject* {
@@ -279,7 +300,7 @@ static auto PyPushCall(PyObject* self, PyObject* args, PyObject* keywds)
if (!g_base->InLogicThread()) {
throw Exception("You must use from_other_thread mode.");
}
- Object::New(call_obj)->ScheduleOnce();
+ Object::New(call_obj)->Schedule();
}
Py_RETURN_NONE;
BA_PYTHON_CATCH;
@@ -1650,6 +1671,7 @@ static PyMethodDef PyGraphicsShutdownIsCompleteDef = {
auto PythonMethodsApp::GetMethods() -> std::vector {
return {
PyAppNameDef,
+ PyAppIsActiveDef,
PyRunAppDef,
PyAppNameUpperDef,
PyIsXCodeBuildDef,
diff --git a/src/ballistica/base/python/methods/python_methods_misc.cc b/src/ballistica/base/python/methods/python_methods_misc.cc
index d8e59e45..caa96c5f 100644
--- a/src/ballistica/base/python/methods/python_methods_misc.cc
+++ b/src/ballistica/base/python/methods/python_methods_misc.cc
@@ -129,7 +129,7 @@ static PyMethodDef PyHasTouchScreenDef = {
static auto PyClipboardIsSupported(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
- if (g_base->app_adapter->ClipboardIsSupported()) {
+ if (g_base->ClipboardIsSupported()) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
@@ -155,7 +155,7 @@ static PyMethodDef PyClipboardIsSupportedDef = {
static auto PyClipboardHasText(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
- if (g_base->app_adapter->ClipboardHasText()) {
+ if (g_base->ClipboardHasText()) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
@@ -188,7 +188,7 @@ static auto PyClipboardSetText(PyObject* self, PyObject* args, PyObject* keywds)
const_cast(kwlist), &value)) {
return nullptr;
}
- g_base->app_adapter->ClipboardSetText(value);
+ g_base->ClipboardSetText(value);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
@@ -212,7 +212,7 @@ static PyMethodDef PyClipboardSetTextDef = {
static auto PyClipboardGetText(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
- return PyUnicode_FromString(g_base->app_adapter->ClipboardGetText().c_str());
+ return PyUnicode_FromString(g_base->ClipboardGetText().c_str());
Py_RETURN_FALSE;
BA_PYTHON_CATCH;
}
@@ -1788,6 +1788,32 @@ static PyMethodDef PyNativeReviewRequestDef = {
"\n"
"(internal)",
};
+
+// ------------------------------- temp_testing --------------------------------
+
+static auto PyTempTesting(PyObject* self) -> PyObject* {
+ BA_PYTHON_TRY;
+
+ std::string devstr = g_core->platform->GetDeviceName() + " "
+ + g_core->platform->GetOSVersionString();
+ if (devstr == "samsung SM-N950F 7.1.1") {
+ Py_RETURN_TRUE;
+ }
+ Py_RETURN_FALSE;
+
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyTempTestingDef = {
+ "temp_testing", // name
+ (PyCFunction)PyTempTesting, // method
+ METH_NOARGS, // flags
+
+ "temp_testing() -> bool\n"
+ "\n"
+ "(internal)",
+};
+
// -----------------------------------------------------------------------------
auto PythonMethodsMisc::GetMethods() -> std::vector {
@@ -1856,6 +1882,7 @@ auto PythonMethodsMisc::GetMethods() -> std::vector {
PyUsingGameCenterDef,
PyNativeReviewRequestSupportedDef,
PyNativeReviewRequestDef,
+ PyTempTestingDef,
};
}
diff --git a/src/ballistica/base/python/support/python_context_call.cc b/src/ballistica/base/python/support/python_context_call.cc
index 4be03012..a37ddffb 100644
--- a/src/ballistica/base/python/support/python_context_call.cc
+++ b/src/ballistica/base/python/support/python_context_call.cc
@@ -125,83 +125,45 @@ void PythonContextCall::Schedule() {
Object::Ref ref(this);
assert(base::g_base);
- schedule_count_++;
base::g_base->logic->event_loop()->PushCall([ref] {
assert(ref.Exists());
- ref->schedule_count_--;
- assert(ref->schedule_count_ >= 0);
ref->Run();
});
}
-void PythonContextCall::ScheduleOnce() {
- if (schedule_count_ > 0) {
- return;
- }
- Schedule();
-}
void PythonContextCall::Schedule(const PythonRef& args) {
// Since we're mucking with Object::Refs, need to limit to logic thread.
BA_PRECONDITION(g_base->InLogicThread());
Object::Ref ref(this);
assert(base::g_base);
- schedule_count_++;
base::g_base->logic->event_loop()->PushCall([ref, args] {
assert(ref.Exists());
- ref->schedule_count_--;
- assert(ref->schedule_count_ >= 0);
ref->Run(args);
});
}
-void PythonContextCall::ScheduleOnce(const PythonRef& args) {
- if (schedule_count_ > 0) {
- return;
- }
- Schedule(args);
-}
void PythonContextCall::ScheduleWeak() {
// Since we're mucking with Object::WeakRefs, need to limit to logic thread.
BA_PRECONDITION(g_base->InLogicThread());
Object::WeakRef ref(this);
assert(base::g_base);
- schedule_count_++;
base::g_base->logic->event_loop()->PushCall([ref] {
if (auto* call = ref.Get()) {
- call->schedule_count_--;
- assert(call->schedule_count_ >= 0);
call->Run();
}
});
}
-void PythonContextCall::ScheduleWeakOnce() {
- if (schedule_count_ > 0) {
- return;
- }
- ScheduleWeak();
-}
-
void PythonContextCall::ScheduleWeak(const PythonRef& args) {
// Since we're mucking with Object::WeakRefs, need to limit to logic thread.
BA_PRECONDITION(g_base->InLogicThread());
Object::WeakRef ref(this);
assert(base::g_base);
- schedule_count_++;
base::g_base->logic->event_loop()->PushCall([ref, args] {
if (auto* call = ref.Get()) {
- call->schedule_count_--;
- assert(call->schedule_count_ >= 0);
call->Run(args);
}
});
}
-void PythonContextCall::ScheduleWeakOnce(const PythonRef& args) {
- if (schedule_count_ > 0) {
- return;
- }
- ScheduleWeak(args);
-}
-
} // namespace ballistica::base
diff --git a/src/ballistica/base/python/support/python_context_call.h b/src/ballistica/base/python/support/python_context_call.h
index 1c99d3bc..1c7d5d01 100644
--- a/src/ballistica/base/python/support/python_context_call.h
+++ b/src/ballistica/base/python/support/python_context_call.h
@@ -42,56 +42,26 @@ class PythonContextCall : public Object {
/// context_ref-call is guaranteed to exist until run.
void Schedule();
- /// Schedule only if this instance is not already scheduled. Generally a
- /// good idea unless you know you need multiple runs scheduled. Avoids
- /// problems such as UIs expecting to be activated only once getting
- /// activated twice due to two simultenous key presses.
- void ScheduleOnce();
-
/// Run in an upcoming cycle of the logic thread with provided args. Must
/// be called from the logic thread. This form creates a strong-reference
/// so the context_ref-call is guaranteed to exist until run.
void Schedule(const PythonRef& args);
- /// Schedule only if this instance is not already scheduled. Generally a
- /// good idea unless you know you need multiple runs scheduled. Avoids
- /// problems such as UIs expecting to be activated only once getting
- /// activated twice due to two simultenous key presses.
- void ScheduleOnce(const PythonRef& args);
-
/// Run in an upcoming cycle of the logic thread. Must be called from the
/// logic thread. This form creates a weak-reference and is a no-op if the
/// context_ref-call is destroyed before its scheduled run.
void ScheduleWeak();
- /// Schedule weakly only if this instance is not already scheduled.
- /// Generally a good idea unless you know you need multiple runs
- /// scheduled. Avoids problems such as UIs expecting to be activated only
- /// once getting activated twice due to two simultenous key presses.
- void ScheduleWeakOnce();
-
/// Run in an upcoming cycle of the logic thread with provided args. Must
/// be called from the logic thread. This form creates a weak-reference
/// and is a no-op if the context_ref-call is destroyed before its
/// scheduled run.
void ScheduleWeak(const PythonRef& args);
- /// Schedule weakly only if this instance is not already scheduled.
- /// Generally a good idea unless you know you need multiple runs
- /// scheduled. Avoids problems such as UIs expecting to be activated only
- /// once getting activated twice due to two simultenous key presses.
- void ScheduleWeakOnce(const PythonRef& args);
-
- auto IsScheduled() const {
- assert(g_base->InLogicThread());
- return schedule_count_ > 0;
- }
-
private:
void GetTrace(); // we try to grab basic trace info
int line_{};
- int schedule_count_{};
bool dead_{};
std::string file_loc_;
PythonRef object_;
diff --git a/src/ballistica/base/support/plus_soft.h b/src/ballistica/base/support/plus_soft.h
index 00994d3b..32ce3684 100644
--- a/src/ballistica/base/support/plus_soft.h
+++ b/src/ballistica/base/support/plus_soft.h
@@ -19,8 +19,8 @@ namespace ballistica::base {
class PlusSoftInterface {
public:
virtual void OnAppStart() = 0;
- virtual void OnAppPause() = 0;
- virtual void OnAppResume() = 0;
+ virtual void OnAppSuspend() = 0;
+ virtual void OnAppUnsuspend() = 0;
virtual void OnAppShutdown() = 0;
virtual void OnAppShutdownComplete() = 0;
virtual void DoApplyAppConfig() = 0;
diff --git a/src/ballistica/base/ui/dev_console.cc b/src/ballistica/base/ui/dev_console.cc
index 7f31eae3..7360a9a0 100644
--- a/src/ballistica/base/ui/dev_console.cc
+++ b/src/ballistica/base/ui/dev_console.cc
@@ -1441,9 +1441,9 @@ void DevConsole::StepDisplayTime() {
auto DevConsole::PasteFromClipboard() -> bool {
if (state_ != State_::kInactive) {
if (python_terminal_visible_) {
- if (g_base->app_adapter->ClipboardIsSupported()) {
- if (g_base->app_adapter->ClipboardHasText()) {
- auto text = g_base->app_adapter->ClipboardGetText();
+ if (g_base->ClipboardIsSupported()) {
+ if (g_base->ClipboardHasText()) {
+ auto text = g_base->ClipboardGetText();
if (strstr(text.c_str(), "\n") || strstr(text.c_str(), "\r")) {
g_base->audio->PlaySound(
g_base->assets->SysSound(SysSoundID::kErrorBeep));
diff --git a/src/ballistica/base/ui/ui.cc b/src/ballistica/base/ui/ui.cc
index dda9bd0d..8d306e6d 100644
--- a/src/ballistica/base/ui/ui.cc
+++ b/src/ballistica/base/ui/ui.cc
@@ -81,9 +81,9 @@ void UI::OnAppStart() {
}
}
-void UI::OnAppPause() { assert(g_base->InLogicThread()); }
+void UI::OnAppSuspend() { assert(g_base->InLogicThread()); }
-void UI::OnAppResume() {
+void UI::OnAppUnsuspend() {
assert(g_base->InLogicThread());
SetUIInputDevice(nullptr);
}
@@ -236,9 +236,6 @@ void UI::MainMenuPress_(InputDevice* device) {
assert(g_base->InLogicThread());
if (auto* ui_delegate = g_base->ui->delegate()) {
ui_delegate->DoHandleDeviceMenuPress(device);
- } else {
- Log(LogLevel::kWarning,
- "UI::MainMenuPress called without ui_v1 present; unexpected.");
}
}
diff --git a/src/ballistica/base/ui/ui.h b/src/ballistica/base/ui/ui.h
index 7ed5dd92..2bbf0917 100644
--- a/src/ballistica/base/ui/ui.h
+++ b/src/ballistica/base/ui/ui.h
@@ -32,8 +32,8 @@ class UI {
UI();
void OnAppStart();
- void OnAppPause();
- void OnAppResume();
+ void OnAppSuspend();
+ void OnAppUnsuspend();
void OnAppShutdown();
void OnAppShutdownComplete();
void DoApplyAppConfig();
diff --git a/src/ballistica/core/core.cc b/src/ballistica/core/core.cc
index 002a4388..f7bdc1ca 100644
--- a/src/ballistica/core/core.cc
+++ b/src/ballistica/core/core.cc
@@ -368,12 +368,64 @@ void CoreFeatureSet::StartSuicideTimer(const std::string& action,
}
}
-// auto CoreFeatureSet::InMainThread() -> bool {
-// return std::this_thread::get_id() == main_thread_id;
-// // if (main_event_loop_) {
-// // return main_event_loop_->ThreadIsCurrent();
-// // }
-// // return false;
-// }
+void CoreFeatureSet::RegisterThread(const std::string& name) {
+ {
+ std::scoped_lock lock(thread_info_map_mutex_);
+
+ // Should be registering each thread just once.
+ assert(thread_info_map_.find(std::this_thread::get_id())
+ == thread_info_map_.end());
+ thread_info_map_[std::this_thread::get_id()] = name;
+ }
+
+ // Also set the name at the OS leve when possible. Prepend 'ballistica'
+ // since there's generally lots of other random threads in the mix.
+ //
+ // Note that we currently don't do this for our main thread because (on
+ // Linux at least) that changes the process name we see in top/etc. On
+ // other platforms we could reconsider, but its generally clear what the
+ // main thread is anyway in most scenarios.
+ if (!InMainThread()) {
+ g_core->platform->SetCurrentThreadName("ballistica " + name);
+ }
+}
+
+void CoreFeatureSet::UnregisterThread() {
+ std::scoped_lock lock(thread_info_map_mutex_);
+ auto i = thread_info_map_.find(std::this_thread::get_id());
+ assert(i != thread_info_map_.end());
+ if (i != thread_info_map_.end()) {
+ thread_info_map_.erase(i);
+ }
+}
+
+auto CoreFeatureSet::CurrentThreadName() -> std::string {
+ if (g_core == nullptr) {
+ return "unknown(not-yet-inited)";
+ }
+ {
+ std::scoped_lock lock(g_core->thread_info_map_mutex_);
+ auto i = g_core->thread_info_map_.find(std::this_thread::get_id());
+ if (i != g_core->thread_info_map_.end()) {
+ return i->second;
+ }
+ }
+
+ // Ask pthread for the thread name if we don't have one.
+ // FIXME - move this to platform.
+#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS || BA_OSTYPE_LINUX
+ std::string name = "unknown (sys-name=";
+ char buffer[256];
+ int result = pthread_getname_np(pthread_self(), buffer, sizeof(buffer));
+ if (result == 0) {
+ name += std::string("\"") + buffer + "\")";
+ } else {
+ name += "";
+ }
+ return name;
+#else
+ return "unknown";
+#endif
+}
} // namespace ballistica::core
diff --git a/src/ballistica/core/core.h b/src/ballistica/core/core.h
index 4c0cb1ca..d31d0e8d 100644
--- a/src/ballistica/core/core.h
+++ b/src/ballistica/core/core.h
@@ -144,6 +144,12 @@ class CoreFeatureSet {
return using_custom_app_python_dir_;
}
+ /// Register various info about the current thread.
+ void RegisterThread(const std::string& name);
+
+ /// Should be called by a thread before it exits.
+ void UnregisterThread();
+
// Subsystems.
CorePython* const python;
CorePlatform* const platform;
@@ -160,8 +166,6 @@ class CoreFeatureSet {
std::vector suspendable_event_loops;
std::mutex v1_cloud_log_mutex;
std::string v1_cloud_log;
- std::mutex thread_name_map_mutex;
- std::unordered_map thread_name_map;
#if BA_DEBUG_BUILD
std::mutex object_list_mutex;
@@ -173,6 +177,7 @@ class CoreFeatureSet {
auto vr_mode() const { return vr_mode_; }
auto event_loops_suspended() const { return event_loops_suspended_; }
void set_event_loops_suspended(bool val) { event_loops_suspended_ = val; }
+ static auto CurrentThreadName() -> std::string;
private:
explicit CoreFeatureSet(CoreConfig config);
@@ -204,6 +209,8 @@ class CoreFeatureSet {
std::optional ba_env_user_python_dir_;
std::optional ba_env_site_python_dir_;
std::string ba_env_data_dir_;
+ std::mutex thread_info_map_mutex_;
+ std::unordered_map thread_info_map_;
};
} // namespace ballistica::core
diff --git a/src/ballistica/core/platform/apple/core_platform_apple.cc b/src/ballistica/core/platform/apple/core_platform_apple.cc
index aa9f72d0..df8281f6 100644
--- a/src/ballistica/core/platform/apple/core_platform_apple.cc
+++ b/src/ballistica/core/platform/apple/core_platform_apple.cc
@@ -466,7 +466,7 @@ auto CorePlatformApple::CanShowBlockingFatalErrorDialog() -> bool {
if (g_buildconfig.xcode_build() && g_buildconfig.ostype_macos()) {
return true;
}
- return false;
+ return CorePlatform::CanShowBlockingFatalErrorDialog();
}
void CorePlatformApple::BlockingFatalErrorDialog(const std::string& message) {
diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc
index 0c31a5d7..274c0996 100644
--- a/src/ballistica/core/platform/core_platform.cc
+++ b/src/ballistica/core/platform/core_platform.cc
@@ -103,7 +103,9 @@ auto CorePlatform::Create() -> CorePlatform* {
return platform;
}
-void CorePlatform::DebugLog(const std::string& msg) { HandleDebugLog(msg); }
+void CorePlatform::LowLevelDebugLog(const std::string& msg) {
+ HandleLowLevelDebugLog(msg);
+}
CorePlatform::CorePlatform() : start_time_millisecs_(GetCurrentMillisecs()) {}
@@ -824,11 +826,9 @@ auto CorePlatform::MacMusicAppGetPlaylists() -> std::list {
}
void CorePlatform::SetCurrentThreadName(const std::string& name) {
- // Currently we leave the main thread alone, otherwise we show up as
- // "BallisticaMainThread" under "top" on linux (should check other platforms).
- if (g_core->InMainThread()) {
- return;
- }
+ // We should never be doing this for the main thread.
+ BA_PRECONDITION_FATAL(!g_core->InMainThread());
+
#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS
pthread_setname_np(name.c_str());
#elif BA_OSTYPE_LINUX || BA_OSTYPE_ANDROID
@@ -1021,7 +1021,7 @@ auto CorePlatform::HavePermission(Permission p) -> bool {
void CorePlatform::SetDebugKey(const std::string& key,
const std::string& value) {}
-void CorePlatform::HandleDebugLog(const std::string& msg) {}
+void CorePlatform::HandleLowLevelDebugLog(const std::string& msg) {}
auto CorePlatform::GetCurrentMillisecs() -> millisecs_t {
return std::chrono::time_point_cast(
diff --git a/src/ballistica/core/platform/core_platform.h b/src/ballistica/core/platform/core_platform.h
index 1d1e1e73..6bcd0627 100644
--- a/src/ballistica/core/platform/core_platform.h
+++ b/src/ballistica/core/platform/core_platform.h
@@ -54,9 +54,8 @@ class CorePlatform {
/// fopen() supporting UTF8 strings.
virtual auto FOpen(const char* path, const char* mode) -> FILE*;
- /// rename() supporting UTF8 strings.
- /// For cross-platform consistency, this should also remove any file that
- /// exists at the target location first.
+ /// rename() supporting UTF8 strings. For cross-platform consistency, this
+ /// should also remove any file that exists at the target location first.
virtual auto Rename(const char* oldname, const char* newname) -> int;
/// Simple cross-platform check for existence of a file.
@@ -332,26 +331,30 @@ class CorePlatform {
/// This is expected to be lightweight as it may be called often.
virtual void SetDebugKey(const std::string& key, const std::string& value);
- void DebugLog(const std::string& msg);
+ /// Print a log message to be included in crash logs or other debug
+ /// mechanisms (example: Crashlytics). V1-cloud-log messages get forwarded
+ /// to here as well. It can be useful to call this directly to report extra
+ /// details that may help in debugging, as these calls are not considered
+ /// 'noteworthy' or presented to the user as standard Log() calls are.
+ void LowLevelDebugLog(const std::string& msg);
#pragma mark MISC --------------------------------------------------------------
- /// Return a time measurement in milliseconds since launch.
- /// It *should* be monotonic.
- /// For most purposes, AppTime values are preferable since their progression
- /// pauses during app suspension and they are 100% guaranteed to not go
- /// backwards.
+ /// Return a time measurement in milliseconds since launch. It *should* be
+ /// monotonic. For most purposes, AppTime values are preferable since
+ /// their progression pauses during app suspension and they are 100%
+ /// guaranteed to not go backwards.
auto GetTicks() const -> millisecs_t;
- /// Return a raw current milliseconds value. It *should* be monotonic.
- /// It is relative to an undefined start point; only use it for time
+ /// Return a raw current milliseconds value. It *should* be monotonic. It
+ /// is relative to an undefined start point; only use it for time
/// differences. Generally the AppTime values are preferable since their
/// progression pauses during app suspension and they are 100% guaranteed
/// to not go backwards.
static auto GetCurrentMillisecs() -> millisecs_t;
- /// Return a raw current microseconds value. It *should* be monotonic.
- /// It is relative to an undefined start point; only use it for time
+ /// Return a raw current microseconds value. It *should* be monotonic. It
+ /// is relative to an undefined start point; only use it for time
/// differences. Generally the AppTime values are preferable since their
/// progression pauses during app suspension and they are 100% guaranteed
/// to not go backwards.
@@ -378,14 +381,15 @@ class CorePlatform {
/// Is the OS currently playing music? (so we can avoid doing so).
virtual auto IsOSPlayingMusic() -> bool;
- /// Pass platform-specific misc-read-vals along to the OS (as a json string).
+ /// Pass platform-specific misc-read-vals along to the OS (as a json
+ /// string).
virtual void SetPlatformMiscReadVals(const std::string& vals);
/// Set the name of the current thread (for debugging).
virtual void SetCurrentThreadName(const std::string& name);
- // If display-resolution can be directly set on this platform,
- // return true and set the native full res here. Otherwise return false;
+ // If display-resolution can be directly set on this platform, return true
+ // and set the native full res here. Otherwise return false;
virtual auto GetDisplayResolution(int* x, int* y) -> bool;
/// Are we being run from a terminal? (should we show prompts, etc?).
@@ -412,44 +416,39 @@ class CorePlatform {
/// device; something like "iPhone 12 Pro".
virtual auto DoGetDeviceDescription() -> std::string;
- /// Attempt to actually create a directory.
- /// Should *not* raise Exceptions if it already exists or if quiet is true.
+ /// Attempt to actually create a directory. Should *not* raise Exceptions
+ /// if it already exists or if quiet is true.
virtual void DoMakeDir(const std::string& dir, bool quiet);
- /// Attempt to actually get an abs path. This will only be called if
- /// the path is valid and exists.
+ /// Attempt to actually get an abs path. This will only be called if the
+ /// path is valid and exists.
virtual auto DoAbsPath(const std::string& path, std::string* outpath) -> bool;
- /// Calc the user scripts dir path for this platform.
- /// This will be called once and the path cached.
+ /// Calc the user scripts dir path for this platform. This will be called
+ /// once and the path cached.
virtual auto DoGetUserPythonDirectoryMonolithicDefault()
-> std::optional;
- /// Return the default config directory for this platform.
- /// This will be used as the config dir if not overridden via command
- /// line options, etc.
+ /// Return the default config directory for this platform. This will be
+ /// used as the config dir if not overridden via command line options,
+ /// etc.
virtual auto DoGetConfigDirectoryMonolithicDefault()
-> std::optional;
- /// Return the default data directory for this platform.
- /// This will be used as the data dir if not overridden by core-config, etc.
- /// This is the one monolithic-default value that is not optional.
+ /// Return the default data directory for this platform. This will be used
+ /// as the data dir if not overridden by core-config, etc. This is the one
+ /// monolithic-default value that is not optional.
virtual auto DoGetDataDirectoryMonolithicDefault() -> std::string;
- /// Return the default Volatile data dir for this platform.
- /// This will be used as the volatile-data-dir if not overridden via command
- /// line options/etc.
+ /// Return the default Volatile data dir for this platform. This will be
+ /// used as the volatile-data-dir if not overridden via command line
+ /// options/etc.
virtual auto GetDefaultVolatileDataDirectory() -> std::string;
/// Generate a random UUID string.
virtual auto GenerateUUID() -> std::string;
- /// Print a log message to be included in crash logs or other debug
- /// mechanisms (example: Crashlytics). V1-cloud-log messages get forwarded
- /// to here as well. It can be useful to call this directly to report extra
- /// details that may help in debugging, as these calls are not considered
- /// 'noteworthy' or presented to the user as standard Log() calls are.
- virtual void HandleDebugLog(const std::string& msg);
+ virtual void HandleLowLevelDebugLog(const std::string& msg);
CorePlatform();
virtual ~CorePlatform();
diff --git a/src/ballistica/core/python/core_python.cc b/src/ballistica/core/python/core_python.cc
index dcebb4a6..3c981e77 100644
--- a/src/ballistica/core/python/core_python.cc
+++ b/src/ballistica/core/python/core_python.cc
@@ -10,9 +10,9 @@
namespace ballistica::core {
-void LowLevelPythonDebugLog(const char* msg) {
+static void PythonLowLevelDebugLog_(const char* msg) {
assert(g_core);
- g_core->platform->DebugLog(msg);
+ g_core->platform->LowLevelDebugLog(msg);
}
static void CheckPyInitStatus(const char* where, const PyStatus& status) {
@@ -28,7 +28,7 @@ void CorePython::InitPython() {
// Install our low level logger in our custom Python builds.
#ifdef PY_HAVE_BALLISTICA_LOW_LEVEL_DEBUG_LOG
- Py_BallisticaLowLevelDebugLog = LowLevelPythonDebugLog;
+ Py_BallisticaLowLevelDebugLog = PythonLowLevelDebugLog_;
#endif
// Flip on some extra runtime debugging options in debug builds.
diff --git a/src/ballistica/scene_v1/connection/connection_to_host_udp.cc b/src/ballistica/scene_v1/connection/connection_to_host_udp.cc
index 97bf9271..3ae741eb 100644
--- a/src/ballistica/scene_v1/connection/connection_to_host_udp.cc
+++ b/src/ballistica/scene_v1/connection/connection_to_host_udp.cc
@@ -18,7 +18,7 @@ auto ConnectionToHostUDP::SwitchProtocol() -> bool {
// Need a new request id so we ignore further responses to our previous
// requests.
- GetRequestID();
+ GetRequestID_();
return true;
}
return false;
@@ -32,7 +32,7 @@ ConnectionToHostUDP::ConnectionToHostUDP(const SockAddr& addr)
did_die_(false),
last_host_response_time_millisecs_(
static_cast(g_base->logic->display_time() * 1000.0)) {
- GetRequestID();
+ GetRequestID_();
if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) {
if (appmode->connections()->GetPrintUDPConnectProgress()) {
ScreenMessage(g_base->assets->GetResourceString("connectingToPartyText"));
@@ -46,11 +46,11 @@ ConnectionToHostUDP::~ConnectionToHostUDP() {
set_connection_dying(true);
}
-void ConnectionToHostUDP::GetRequestID() {
+void ConnectionToHostUDP::GetRequestID_() {
// We store a unique-ish request ID to minimize the chance that data for
- // previous connections/etc will muck with us.
- // Try to start this value at something that won't be common in packets to
- // minimize chance of garbage packets causing trouble.
+ // previous connections/etc will muck with us. Try to start this value at
+ // something that won't be common in packets to minimize chance of garbage
+ // packets causing trouble.
static auto next_request_id =
static_cast(71 + (rand() % 151)); // NOLINT
request_id_ = next_request_id++;
@@ -95,13 +95,14 @@ void ConnectionToHostUDP::Update() {
{1, 0, 0});
}
- // Die immediately in this case; no use trying to wait for a disconnect-ack
- // since we've already given up hope of hearing from them.
+ // Die immediately in this case; no use trying to wait for a
+ // disconnect-ack since we've already given up hope of hearing from
+ // them.
Die();
return;
} else if (errored()) {
- // If we've errored, keep sending disconnect-requests periodically.
- // Once we get a response (or time out in the above code) we'll die.
+ // If we've errored, keep sending disconnect-requests periodically. Once
+ // we get a response (or time out in the above code) we'll die.
if (current_time_millisecs - last_disconnect_request_time_ > 1000) {
last_disconnect_request_time_ = current_time_millisecs;
@@ -189,8 +190,8 @@ void ConnectionToHostUDP::Error(const std::string& msg) {
auto ConnectionToHostUDP::GetAsUDP() -> ConnectionToHostUDP* { return this; }
void ConnectionToHostUDP::RequestDisconnect() {
- // Mark us as errored so all future communication results in more disconnect
- // requests.
+ // Mark us as errored so all future communication results in more
+ // disconnect requests.
set_errored(true);
if (client_id_ != -1) {
SendDisconnectRequest();
diff --git a/src/ballistica/scene_v1/connection/connection_to_host_udp.h b/src/ballistica/scene_v1/connection/connection_to_host_udp.h
index b085b759..58cb4a04 100644
--- a/src/ballistica/scene_v1/connection/connection_to_host_udp.h
+++ b/src/ballistica/scene_v1/connection/connection_to_host_udp.h
@@ -23,8 +23,8 @@ class ConnectionToHostUDP : public ConnectionToHost {
void set_client_id(int val) { client_id_ = val; }
auto client_id() const -> int { return client_id_; }
- // Attempt connecting via a different protocol. If none are left to try,
- // returns false.
+ /// Attempt connecting via a different protocol. If none are left to try,
+ /// returns false.
auto SwitchProtocol() -> bool;
void RequestDisconnect() override;
@@ -32,16 +32,18 @@ class ConnectionToHostUDP : public ConnectionToHost {
void Error(const std::string& error_msg) override;
void Die();
void SendDisconnectRequest();
+ const auto& addr() const { return *addr_; }
private:
- void GetRequestID();
- uint8_t request_id_{};
- std::unique_ptr addr_;
+ void GetRequestID_();
+
bool did_die_{};
+ uint8_t request_id_{};
+ int client_id_{};
millisecs_t last_client_id_request_time_{};
millisecs_t last_disconnect_request_time_{};
- int client_id_{};
millisecs_t last_host_response_time_millisecs_{};
+ std::unique_ptr addr_;
};
} // namespace ballistica::scene_v1
diff --git a/src/ballistica/scene_v1/python/class/python_class_session_player.cc b/src/ballistica/scene_v1/python/class/python_class_session_player.cc
index 76b7c1c8..5431aff9 100644
--- a/src/ballistica/scene_v1/python/class/python_class_session_player.cc
+++ b/src/ballistica/scene_v1/python/class/python_class_session_player.cc
@@ -208,7 +208,7 @@ auto PythonClassSessionPlayer::tp_getattro(PythonClassSessionPlayer* self,
PyObject* attr) -> PyObject* {
BA_PYTHON_TRY;
- assert(g_base->InLogicThread());
+ BA_PRECONDITION(g_base->InLogicThread());
// Assuming this will always be a str?
assert(PyUnicode_Check(attr));
@@ -327,6 +327,9 @@ auto PythonClassSessionPlayer::tp_setattro(PythonClassSessionPlayer* self,
PyObject* attr, PyObject* val)
-> int {
BA_PYTHON_TRY;
+
+ BA_PRECONDITION(g_base->InLogicThread());
+
// Assuming this will always be a str?
assert(PyUnicode_Check(attr));
const char* s = PyUnicode_AsUTF8(attr);
diff --git a/src/ballistica/scene_v1/python/methods/python_methods_networking.cc b/src/ballistica/scene_v1/python/methods/python_methods_networking.cc
index b254752b..72b824d1 100644
--- a/src/ballistica/scene_v1/python/methods/python_methods_networking.cc
+++ b/src/ballistica/scene_v1/python/methods/python_methods_networking.cc
@@ -5,13 +5,17 @@
#include "ballistica/base/assets/assets.h"
#include "ballistica/base/networking/network_reader.h"
#include "ballistica/base/python/base_python.h"
+#include "ballistica/core/python/core_python.h"
#include "ballistica/scene_v1/connection/connection_set.h"
#include "ballistica/scene_v1/connection/connection_to_client.h"
#include "ballistica/scene_v1/connection/connection_to_host.h"
+#include "ballistica/scene_v1/connection/connection_to_host_udp.h"
+#include "ballistica/scene_v1/python/scene_v1_python.h"
#include "ballistica/scene_v1/support/scene_v1_app_mode.h"
#include "ballistica/shared/math/vector3f.h"
#include "ballistica/shared/networking/sockaddr.h"
#include "ballistica/shared/python/python.h"
+#include "ballistica/shared/python/python_ref.h"
#include "ballistica/shared/python/python_sys.h"
namespace ballistica::scene_v1 {
@@ -20,8 +24,7 @@ namespace ballistica::scene_v1 {
#pragma clang diagnostic push
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
-// ------------------------- get_public_party_enabled
-// ---------------------------
+// ----------------------- get_public_party_enabled ---------------------------
static auto PyGetPublicPartyEnabled(PyObject* self, PyObject* args,
PyObject* keywds) -> PyObject* {
@@ -411,7 +414,10 @@ static auto PyGetConnectionToHostInfo(PyObject* self, PyObject* args,
const_cast(kwlist))) {
return nullptr;
}
- // Error if we're not in our app-mode.
+ BA_LOG_ONCE(LogLevel::kWarning,
+ "bascenev1.get_connection_to_host_info() is deprecated; use "
+ "bascenev1.get_connection_to_host_info_2().");
+ BA_PRECONDITION(g_base->InLogicThread());
auto* appmode = SceneV1AppMode::GetActiveOrThrow();
ConnectionToHost* hc = appmode->connections()->connection_to_host();
@@ -435,6 +441,57 @@ static PyMethodDef PyGetConnectionToHostInfoDef = {
"(internal)",
};
+// --------------------- get_connection_to_host_info_2 -------------------------
+
+static auto PyGetConnectionToHostInfo2(PyObject* self, PyObject* args,
+ PyObject* keywds) -> PyObject* {
+ BA_PYTHON_TRY;
+ static const char* kwlist[] = {nullptr};
+ if (!PyArg_ParseTupleAndKeywords(args, keywds, "",
+ const_cast(kwlist))) {
+ return nullptr;
+ }
+ BA_PRECONDITION(g_base->InLogicThread());
+ auto* appmode = SceneV1AppMode::GetActiveOrThrow();
+
+ ConnectionToHost* hc = appmode->connections()->connection_to_host();
+ if (hc) {
+ PythonRef addr_obj;
+ PythonRef port_obj;
+ if (ConnectionToHostUDP* hcu = dynamic_cast(hc)) {
+ addr_obj.Steal(PyUnicode_FromString(hcu->addr().AddressString().c_str()));
+ port_obj.Steal(PyLong_FromLong(hcu->addr().Port()));
+ } else {
+ addr_obj.Acquire(Py_None);
+ port_obj.Acquire(Py_None);
+ }
+ auto args =
+ g_core->python->objs().Get(core::CorePython::ObjID::kEmptyTuple);
+ auto keywds = PythonRef::Stolen(Py_BuildValue(
+ "{sssisOsO}", "name", hc->party_name().c_str(), "build_number",
+ hc->build_number(), "address", addr_obj.Get(), "port", port_obj.Get()));
+ auto result = g_scene_v1->python->objs()
+ .Get(SceneV1Python::ObjID::kHostInfoClass)
+ .Call(args, keywds);
+ if (!result.Exists()) {
+ throw Exception("Failed to instantiate HostInfo.", PyExcType::kRuntime);
+ }
+ return result.HandOver();
+ }
+ Py_RETURN_NONE;
+ BA_PYTHON_CATCH;
+}
+
+static PyMethodDef PyGetConnectionToHostInfo2Def = {
+ "get_connection_to_host_info_2", // name
+ (PyCFunction)PyGetConnectionToHostInfo2, // method
+ METH_VARARGS | METH_KEYWORDS, // flags
+
+ "get_connection_to_host_info_2() -> bascenev1.HostInfo | None\n"
+ "\n"
+ "Return info about the host we are currently connected to.",
+};
+
// --------------------------- disconnect_from_host ----------------------------
static auto PyDisconnectFromHost(PyObject* self, PyObject* args,
@@ -701,6 +758,7 @@ static auto PyChatMessage(PyObject* self, PyObject* args, PyObject* keywds)
&clients_obj, &sender_override_obj)) {
return nullptr;
}
+ BA_PRECONDITION(g_base->InLogicThread());
auto* appmode = SceneV1AppMode::GetActiveOrThrow();
message = g_base->python->GetPyLString(message_obj);
@@ -775,6 +833,7 @@ auto PythonMethodsNetworking::GetMethods() -> std::vector {
PyDisconnectClientDef,
PyGetClientPublicDeviceUUIDDef,
PyGetConnectionToHostInfoDef,
+ PyGetConnectionToHostInfo2Def,
PyClientInfoQueryResponseDef,
PyConnectToPartyDef,
PySetAuthenticateClientsDef,
diff --git a/src/ballistica/scene_v1/python/scene_v1_python.h b/src/ballistica/scene_v1/python/scene_v1_python.h
index 404e72f7..afa67832 100644
--- a/src/ballistica/scene_v1/python/scene_v1_python.h
+++ b/src/ballistica/scene_v1/python/scene_v1_python.h
@@ -91,6 +91,7 @@ class SceneV1Python {
kGetPlayerIconCall,
kFilterChatMessageCall,
kHandleLocalChatMessageCall,
+ kHostInfoClass,
kLast // Sentinel; must be at end.
};
diff --git a/src/ballistica/scene_v1/support/client_session_net.h b/src/ballistica/scene_v1/support/client_session_net.h
index 075dda57..7a6a213f 100644
--- a/src/ballistica/scene_v1/support/client_session_net.h
+++ b/src/ballistica/scene_v1/support/client_session_net.h
@@ -37,21 +37,16 @@ class ClientSessionNet : public ClientSession {
auto GetBucketNum() -> int;
bool writing_replay_{};
+ int delay_sample_counter_{};
+ float max_delay_smoothed_{};
+ float last_bucket_max_delay_{};
+ float current_delay_{};
millisecs_t base_time_received_{};
millisecs_t last_base_time_receive_time_{};
millisecs_t leading_base_time_received_{};
millisecs_t leading_base_time_receive_time_{};
Object::WeakRef connection_to_host_;
std::vector buckets_{5};
-
- // float bucket_max_smoothed_{};
- // float bucket_min_smoothed_{};
- float max_delay_smoothed_{};
- float last_bucket_max_delay_{};
- float current_delay_{};
-
- int delay_sample_counter_{};
- // int adjust_counter_{};
};
} // namespace ballistica::scene_v1
diff --git a/src/ballistica/scene_v1/support/scene_v1_app_mode.cc b/src/ballistica/scene_v1/support/scene_v1_app_mode.cc
index 5daf266c..72cfdbd2 100644
--- a/src/ballistica/scene_v1/support/scene_v1_app_mode.cc
+++ b/src/ballistica/scene_v1/support/scene_v1_app_mode.cc
@@ -106,14 +106,14 @@ void SceneV1AppMode::OnAppShutdown() {
connections_->Shutdown();
}
-void SceneV1AppMode::OnAppPause() {
+void SceneV1AppMode::OnAppSuspend() {
assert(g_base->InLogicThread());
// App is going into background or whatnot. Kill any sockets/etc.
EndHostScanning();
}
-void SceneV1AppMode::OnAppResume() { assert(g_base->InLogicThread()); }
+void SceneV1AppMode::OnAppUnsuspend() { 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
diff --git a/src/ballistica/scene_v1/support/scene_v1_app_mode.h b/src/ballistica/scene_v1/support/scene_v1_app_mode.h
index 0d9bd6f7..a3310535 100644
--- a/src/ballistica/scene_v1/support/scene_v1_app_mode.h
+++ b/src/ballistica/scene_v1/support/scene_v1_app_mode.h
@@ -148,8 +148,8 @@ class SceneV1AppMode : public base::AppMode {
auto IsPlayerBanned(const PlayerSpec& spec) -> bool;
void BanPlayer(const PlayerSpec& spec, millisecs_t duration);
void OnAppStart() override;
- void OnAppPause() override;
- void OnAppResume() override;
+ void OnAppSuspend() override;
+ void OnAppUnsuspend() override;
auto InClassicMainMenuSession() const -> bool override;
auto CreateInputDeviceDelegate(base::InputDevice* device)
-> base::InputDeviceDelegate* override;
diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc
index 38e7096c..27f3b924 100644
--- a/src/ballistica/shared/ballistica.cc
+++ b/src/ballistica/shared/ballistica.cc
@@ -39,8 +39,8 @@ auto main(int argc, char** argv) -> int {
namespace ballistica {
// These are set automatically via script; don't modify them here.
-const int kEngineBuildNumber = 21636;
-const char* kEngineVersion = "1.7.30";
+const int kEngineBuildNumber = 21707;
+const char* kEngineVersion = "1.7.31";
const int kEngineApiVersion = 8;
#if BA_MONOLITHIC_BUILD
@@ -53,6 +53,8 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
core::BaseSoftInterface* l_base{};
try {
+ auto time1 = core::CorePlatform::GetCurrentMillisecs();
+
// Even at the absolute start of execution we should be able to
// reasonably log errors. Set env var BA_CRASH_TEST=1 to test this.
if (const char* crashenv = getenv("BA_CRASH_TEST")) {
@@ -66,6 +68,8 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// import it first thing even if we don't explicitly use it.
l_core = core::CoreFeatureSet::Import(&core_config);
+ auto time2 = core::CorePlatform::GetCurrentMillisecs();
+
// If a command was passed, simply run it and exit. We want to act
// simply as a Python interpreter in that case; we don't do any
// environment setup (aside from the bits core does automatically such
@@ -90,6 +94,8 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// those modules get loaded from in the first place.
l_core->python->MonolithicModeBaEnvConfigure();
+ auto time3 = core::CorePlatform::GetCurrentMillisecs();
+
// We need the base feature-set to run a full app but we don't have a hard
// dependency to it. Let's see if it's available.
l_base = l_core->SoftImportBase();
@@ -97,6 +103,8 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
FatalError("Base module unavailable; can't run app.");
}
+ auto time4 = core::CorePlatform::GetCurrentMillisecs();
+
// -------------------------------------------------------------------------
// Phase 2: "The pieces are moving."
// -------------------------------------------------------------------------
@@ -113,6 +121,23 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
// until the app exits (or we return from this function and let the
// environment do that part).
+ // Make noise if it takes us too long to get to this point.
+ auto time5 = core::CorePlatform::GetCurrentMillisecs();
+ auto total_duration = time5 - time1;
+ if (total_duration > 5000) {
+ auto core_import_duration = time2 - time1;
+ auto env_config_duration = time3 - time2;
+ auto base_import_duration = time4 - time3;
+ auto start_app_duration = time5 - time4;
+ Log(LogLevel::kWarning,
+ "MonolithicMain took too long (" + std::to_string(total_duration)
+ + " ms; " + std::to_string(core_import_duration)
+ + " core-import, " + std::to_string(env_config_duration)
+ + " env-config, " + std::to_string(base_import_duration)
+ + " base-import, " + std::to_string(start_app_duration)
+ + " start-app).");
+ }
+
if (l_base->AppManagesMainThreadEventLoop()) {
// In environments where we control the event loop, do that.
l_base->RunAppToCompletion();
@@ -130,20 +155,20 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
std::string error_msg =
std::string("Unhandled exception in MonolithicMain(): ") + exc.what();
- // Let the user and/or master-server know we're dying.
+ // Let the user and/or master-server know what killed us.
FatalError::ReportFatalError(error_msg, true);
- // Exiting the app via an exception leads to crash reports on various
- // platforms. If it seems we're not on an official live build then we'd
- // rather just exit cleanly with an error code and avoid polluting crash
- // report logs with reports from dev builds.
+ // Exiting the app via an exception tends to lead to crash reports. If
+ // it seems we're not on an official live build then we'd rather just
+ // exit cleanly with an error code and avoid polluting crash report logs
+ // with reports from dev builds.
bool try_to_exit_cleanly = !(l_base && l_base->IsUnmodifiedBlessedBuild());
- // If this is true it means the app is handling things (showing a fatal
- // error dialog, etc.) and it's out of our hands.
+ // If this returns true, it means the app is handling things (showing a
+ // fatal error dialog, etc.) and it's out of our hands.
bool handled = FatalError::HandleFatalError(try_to_exit_cleanly, true);
- // Do the default thing if it's not been handled.
+ // If it's not been handled, take the app down ourself.
if (!handled) {
if (try_to_exit_cleanly) {
exit(1);
@@ -155,22 +180,95 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int {
return 0;
}
+// A way to do the same as above except in an incremental manner. This can
+// be used to avoid app-not-responding reports on slow devices by
+// interleaving engine init steps with other event processing.
+class IncrementalInitRunner_ {
+ public:
+ explicit IncrementalInitRunner_(const core::CoreConfig* config)
+ : config_(*config) {}
+ auto Process() -> bool {
+ if (zombie_) {
+ return false;
+ }
+ try {
+ switch (step_) {
+ case 0:
+ core_ = core::CoreFeatureSet::Import(&config_);
+ step_++;
+ return false;
+ case 1:
+ core_->python->MonolithicModeBaEnvConfigure();
+ step_++;
+ return false;
+ case 2:
+ base_ = core_->SoftImportBase();
+ if (!base_) {
+ FatalError("Base module unavailable; can't run app.");
+ }
+ step_++;
+ return false;
+ case 3:
+ base_->StartApp();
+ Python::PermanentlyReleaseGIL();
+ step_++;
+ return false;
+ default:
+ return true;
+ }
+ } catch (const std::exception& exc) {
+ std::string error_msg =
+ std::string("Unhandled exception in MonolithicMain(): ") + exc.what();
+
+ // Let the user and/or master-server know what killed us.
+ FatalError::ReportFatalError(error_msg, true);
+
+ // Exiting the app via an exception tends to lead to crash reports. If
+ // it seems we're not on an official live build then we'd rather just
+ // exit cleanly with an error code and avoid polluting crash report logs
+ // with reports from dev builds.
+ bool try_to_exit_cleanly = !(base_ && base_->IsUnmodifiedBlessedBuild());
+
+ // If this returns true, it means the app is handling things (showing a
+ // fatal error dialog, etc.) and it's out of our hands.
+ bool handled = FatalError::HandleFatalError(try_to_exit_cleanly, true);
+
+ // If it's not been handled, take the app down ourself.
+ if (!handled) {
+ if (try_to_exit_cleanly) {
+ exit(1);
+ } else {
+ throw; // Crash report here we come!
+ }
+ }
+ // Just go into vegetable mode so hopefully the handler can do its
+ // thing.
+ zombie_ = true;
+ return false;
+ }
+ }
+
+ private:
+ int step_{};
+ bool zombie_{};
+ core::CoreConfig config_;
+ core::CoreFeatureSet* core_{};
+ core::BaseSoftInterface* base_{};
+};
+
+static IncrementalInitRunner_* g_incremental_init_runner_{};
+
+auto MonolithicMainIncremental(const core::CoreConfig* config) -> bool {
+ if (g_incremental_init_runner_ == nullptr) {
+ g_incremental_init_runner_ = new IncrementalInitRunner_(config);
+ }
+ return g_incremental_init_runner_->Process();
+}
+
#endif // BA_MONOLITHIC_BUILD
void FatalError(const std::string& message) {
- // Let the user and/or master-server know we're dying.
- FatalError::ReportFatalError(message, false);
-
- // Exiting the app via an exception leads to crash reports on various
- // platforms. If it seems we're not on an official live build then we'd
- // rather just exit cleanly with an error code and avoid polluting crash
- // report logs with reports from dev builds.
- bool try_to_exit_cleanly =
- !(core::g_base_soft && core::g_base_soft->IsUnmodifiedBlessedBuild());
- bool handled = FatalError::HandleFatalError(try_to_exit_cleanly, false);
- if (!handled) {
- throw Exception("A fatal error occurred.");
- }
+ FatalError::DoFatalError(message);
}
void Log(LogLevel level, const std::string& msg) { Logging::Log(level, msg); }
@@ -192,7 +290,7 @@ void ScreenMessage(const std::string& msg) {
auto CurrentThreadName() -> std::string {
// Currently just ask event-loop for this but perhaps should be talking
// more directly to the OS/etc. to cover more cases.
- return EventLoop::CurrentThreadName();
+ return core::CoreFeatureSet::CurrentThreadName();
}
} // namespace ballistica
diff --git a/src/ballistica/shared/ballistica.h b/src/ballistica/shared/ballistica.h
index 83734735..17831283 100644
--- a/src/ballistica/shared/ballistica.h
+++ b/src/ballistica/shared/ballistica.h
@@ -68,6 +68,11 @@ class CoreConfig;
/// Entry point for standard monolithic builds. Handles all initing and
/// running.
auto MonolithicMain(const core::CoreConfig& config) -> int;
+
+/// Special alternate version of MonolithicMain which breaks its work into
+/// pieces; used to reduce app-not-responding reports from slow Android
+/// devices. Call this repeatedly until it returns true;
+auto MonolithicMainIncremental(const core::CoreConfig* config) -> bool;
#endif // BA_MONOLITHIC_BUILD
// Print a momentary message on the screen.
diff --git a/src/ballistica/shared/foundation/event_loop.cc b/src/ballistica/shared/foundation/event_loop.cc
index 24c5ca92..b3711495 100644
--- a/src/ballistica/shared/foundation/event_loop.cc
+++ b/src/ballistica/shared/foundation/event_loop.cc
@@ -98,21 +98,6 @@ EventLoop::EventLoop(EventLoopID identifier_in, ThreadSource source)
}
}
-void EventLoop::SetInternalThreadName_(const std::string& name) {
- assert(g_core);
- std::scoped_lock lock(g_core->thread_name_map_mutex);
- g_core->thread_name_map[std::this_thread::get_id()] = name;
-}
-
-void EventLoop::ClearCurrentThreadName() {
- assert(g_core);
- std::scoped_lock lock(g_core->thread_name_map_mutex);
- auto i = g_core->thread_name_map.find(std::this_thread::get_id());
- if (i != g_core->thread_name_map.end()) {
- g_core->thread_name_map.erase(i);
- }
-}
-
// These are all exactly the same; its just a way to try and clarify
// in stack traces which thread is running in case it is not otherwise
// evident.
@@ -341,54 +326,40 @@ void EventLoop::GetThreadMessages_(std::list* messages) {
void EventLoop::BootstrapThread_() {
assert(!bootstrapped_);
+ assert(g_core);
thread_id_ = std::this_thread::get_id();
- const char* name;
const char* id_string;
switch (identifier_) {
case EventLoopID::kLogic:
- name = "logic";
- id_string = "ballistica logic";
+ name_ = "logic";
break;
case EventLoopID::kStdin:
- name = "stdin";
- id_string = "ballistica stdin";
+ name_ = "stdin";
break;
case EventLoopID::kAssets:
- name = "assets";
- id_string = "ballistica assets";
+ name_ = "assets";
break;
case EventLoopID::kFileOut:
- name = "fileout";
- id_string = "ballistica file-out";
+ name_ = "fileout";
break;
case EventLoopID::kMain:
- name = "main";
- id_string = "ballistica main";
+ name_ = "main";
break;
case EventLoopID::kAudio:
- name = "audio";
- id_string = "ballistica audio";
+ name_ = "audio";
break;
case EventLoopID::kBGDynamics:
- name = "bgdynamics";
- id_string = "ballistica bg-dynamics";
+ name_ = "bgdynamics";
break;
case EventLoopID::kNetworkWrite:
- name = "networkwrite";
- id_string = "ballistica network-write";
+ name_ = "networkwrite";
break;
default:
throw Exception();
}
- assert(name && id_string);
- SetInternalThreadName_(name);
-
- // Note: we currently don't do this for our main thread because it
- // changes the process name we see in top/etc. Should look into that.
- if (identifier_ != EventLoopID::kMain) {
- g_core->platform->SetCurrentThreadName(id_string);
- }
+ assert(!name_.empty());
+ g_core->RegisterThread(name_);
bootstrapped_ = true;
}
@@ -411,7 +382,7 @@ auto EventLoop::ThreadMain_() -> int {
RunToCompletion();
- ClearCurrentThreadName();
+ g_core->UnregisterThread();
return 0;
} catch (const std::exception& e) {
auto error_msg = std::string("Unhandled exception in ")
@@ -552,8 +523,7 @@ void EventLoop::PushThreadMessage_(const ThreadMessage_& t) {
if (!sent_error) {
sent_error = true;
log_entries.emplace_back(
- LogLevel::kError,
- "ThreadMessage list > 1000 in thread: " + CurrentThreadName());
+ LogLevel::kError, "ThreadMessage list > 1000 in thread: " + name_);
LogThreadMessageTally_(&log_entries);
}
@@ -561,8 +531,7 @@ void EventLoop::PushThreadMessage_(const ThreadMessage_& t) {
// Prevent runaway mem usage if the list gets out of control.
if (thread_messages_.size() > 10000) {
- FatalError("ThreadMessage list > 10000 in thread: "
- + CurrentThreadName());
+ FatalError("ThreadMessage list > 10000 in thread: " + name_);
}
// Unlock thread-message list and inform thread that there's something
@@ -625,35 +594,6 @@ void EventLoop::DeleteTimer(int id) {
timers_.DeleteTimer(id);
}
-auto EventLoop::CurrentThreadName() -> std::string {
- if (g_core == nullptr) {
- return "unknown(not-yet-inited)";
- }
- {
- std::scoped_lock lock(g_core->thread_name_map_mutex);
- auto i = g_core->thread_name_map.find(std::this_thread::get_id());
- if (i != g_core->thread_name_map.end()) {
- return i->second;
- }
- }
-
- // Ask pthread for the thread name if we don't have one.
- // FIXME - move this to platform.
-#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS || BA_OSTYPE_LINUX
- std::string name = "unknown (sys-name=";
- char buffer[256];
- int result = pthread_getname_np(pthread_self(), buffer, sizeof(buffer));
- if (result == 0) {
- name += std::string("\"") + buffer + "\")";
- } else {
- name += "";
- }
- return name;
-#else
- return "unknown";
-#endif
-}
-
void EventLoop::RunPendingRunnables_() {
// Pull all runnables off the list first (its possible for one of these
// runnables to add more) and then process them.
diff --git a/src/ballistica/shared/foundation/event_loop.h b/src/ballistica/shared/foundation/event_loop.h
index 6b68b51d..8bb761a6 100644
--- a/src/ballistica/shared/foundation/event_loop.h
+++ b/src/ballistica/shared/foundation/event_loop.h
@@ -25,10 +25,6 @@ class EventLoop {
ThreadSource source = ThreadSource::kCreate);
virtual ~EventLoop();
- void ClearCurrentThreadName();
-
- static auto CurrentThreadName() -> std::string;
-
static void SetEventLoopsSuspended(bool enable);
static auto AreEventLoopsSuspended() -> bool;
@@ -97,6 +93,8 @@ class EventLoop {
auto suspended() { return suspended_; }
auto done() -> bool { return done_; }
+ auto name() const { return name_; }
+
private:
struct ThreadMessage_ {
enum class Type { kShutdown = 999, kRunnable, kSuspend, kUnsuspend };
@@ -111,7 +109,6 @@ class EventLoop {
: type(type), runnable(runnable), completion_flag{completion_flag} {}
};
auto CheckPushRunnableSafety_() -> bool;
- void SetInternalThreadName_(const std::string& name);
void WaitForNextEvent_(bool single_cycle);
void LogThreadMessageTally_(
std::vector>* log_entries);
@@ -149,13 +146,6 @@ class EventLoop {
void BootstrapThread_();
- // void LoopUpkeep_(bool single_cycle);
-
- // FIXME: Should generalize this to some sort of PlatformThreadData class.
-#if BA_XCODE_BUILD
- // void* auto_release_pool_{};
-#endif
-
EventLoopID identifier_{EventLoopID::kInvalid};
ThreadSource source_{};
bool bootstrapped_{};
@@ -173,6 +163,7 @@ class EventLoop {
std::mutex thread_message_mutex_;
std::mutex client_listener_mutex_;
std::list> data_to_client_;
+ std::string name_;
PyThreadState* py_thread_state_{};
TimerList timers_;
};
diff --git a/src/ballistica/shared/foundation/fatal_error.cc b/src/ballistica/shared/foundation/fatal_error.cc
index 967e6a30..4bed5b6a 100644
--- a/src/ballistica/shared/foundation/fatal_error.cc
+++ b/src/ballistica/shared/foundation/fatal_error.cc
@@ -15,15 +15,31 @@ namespace ballistica {
using core::g_base_soft;
using core::g_core;
+bool FatalError::reported_{};
+
+void FatalError::DoFatalError(const std::string& message) {
+ // Let the user and/or master-server know we're dying.
+ ReportFatalError(message, false);
+
+ // In some cases we prefer to cleanly exit the app with an error code
+ // in a way that won't wind up as a crash report; this avoids polluting
+ // our crash reports list with stuff from dev builds.
+ bool try_to_exit_cleanly =
+ !(core::g_base_soft && core::g_base_soft->IsUnmodifiedBlessedBuild());
+ bool handled = HandleFatalError(try_to_exit_cleanly, false);
+ if (!handled) {
+ abort();
+ }
+}
+
void FatalError::ReportFatalError(const std::string& message,
bool in_top_level_exception_handler) {
- // We want to report the first fatal error that happens; if further ones
- // happen they are probably red herrings.
- static bool ran = false;
- if (ran) {
+ // We want to report only the first fatal error that happens; if further
+ // ones happen they are likely red herrings triggered by the first.
+ if (reported_) {
return;
}
- ran = true;
+ reported_ = true;
// Our main goal here varies based off whether we are an unmodified
// blessed build. If we are, our main goal is to communicate as much info
@@ -75,12 +91,16 @@ void FatalError::ReportFatalError(const std::string& message,
if (trace) {
std::string tracestr = trace->FormatForDisplay();
if (!tracestr.empty()) {
- logmsg += ("\nCPP-STACK-TRACE-BEGIN:\n" + tracestr
- + "\nCPP-STACK-TRACE-END");
+ logmsg +=
+ (("\n----------------------- BALLISTICA-NATIVE-STACK-TRACE-BEGIN "
+ "--------------------\n")
+ + tracestr
+ + ("\n----------------------- BALLISTICA-NATIVE-STACK-TRACE-END "
+ "----------------------"));
}
delete trace;
} else {
- logmsg += "\n(CPP-STACK-TRACE-UNAVAILABLE)";
+ logmsg += "\n(BALLISTICA-NATIVE-STACK-TRACE-UNAVAILABLE)";
}
}
}
@@ -139,11 +159,10 @@ void FatalError::DoBlockingFatalErrorDialog(const std::string& message) {
bool* startedptr{&started};
bool* finishedptr{&finished};
- // If our thread is holding the GIL, release it to give the main
- // thread a better chance to get to the point of displaying the fatal error.
- if (Python::HaveGIL()) {
- Python::PermanentlyReleaseGIL();
- }
+ // If our thread is holding the GIL, release it while we spin; otherwise
+ // we can wind up in deadlock if the main thread wants it.
+ Python::ScopedInterpreterLockRelease gil_release;
+
g_base_soft->PushMainThreadRunnable(
NewLambdaRunnableUnmanaged([message, startedptr, finishedptr] {
*startedptr = true;
@@ -152,7 +171,7 @@ void FatalError::DoBlockingFatalErrorDialog(const std::string& message) {
}));
// Wait a short amount of time for the main thread to take action.
- // There's a chance that it can't (if threads are paused, if it is
+ // There's a chance that it can't (if threads are suspended, if it is
// blocked on a synchronous call to another thread, etc.) so if we don't
// see something happening soon, just give up on showing a dialog.
auto starttime = core::CorePlatform::GetCurrentMillisecs();
@@ -192,7 +211,7 @@ auto FatalError::HandleFatalError(bool exit_cleanly,
}
// Otherwise its up to who called us (they might let the caught exception
- // bubble up)
+ // bubble up).
return false;
}
diff --git a/src/ballistica/shared/foundation/fatal_error.h b/src/ballistica/shared/foundation/fatal_error.h
index e98fa651..f2f16dae 100644
--- a/src/ballistica/shared/foundation/fatal_error.h
+++ b/src/ballistica/shared/foundation/fatal_error.h
@@ -9,24 +9,31 @@ namespace ballistica {
class FatalError {
public:
+ /// Complete high level level fatal error call; does both reporting and
+ /// handling. ballistica::FatalError() simply calls this.
+ static void DoFatalError(const std::string& message);
+
/// Report a fatal error to the master-server/user/etc. Note that reporting
/// only happens for the first invocation of this call; additional calls
- /// are no-ops.
+ /// are no-ops. This is because the process of tearing down the app may
+ /// trigger additional errors which are red herrings.
static void ReportFatalError(const std::string& message,
bool in_top_level_exception_handler);
- /// Handle a fatal error. This can involve calling exit(), abort(), setting
- /// up an asynchronous quit, etc. Returns true if the fatal-error has been
- /// handled; otherwise it is up to the caller (this should only be the case
- /// when in_top_level_exception_handler is true).
- /// Unlike ReportFatalError, the logic in this call can be invoked repeatedly
- /// and should be prepared for that possibility in the case of recursive
- /// fatal errors/etc.
+ /// Handle a fatal error. This can involve calling exit(), abort(),
+ /// setting up an asynchronous quit, etc. Returns true if the fatal-error
+ /// has been handled; otherwise it is up to the caller (this should only
+ /// be the case when in_top_level_exception_handler is true).
+ ///
+ /// Unlike ReportFatalError, the logic in this call can be invoked
+ /// repeatedly and should be prepared for that possibility in the case of
+ /// recursive fatal errors/etc.
static auto HandleFatalError(bool clean_exit,
bool in_top_level_exception_handler) -> bool;
private:
static void DoBlockingFatalErrorDialog(const std::string& message);
+ static bool reported_;
};
} // namespace ballistica
diff --git a/src/ballistica/shared/foundation/logging.cc b/src/ballistica/shared/foundation/logging.cc
index 9e84b713..64ccfc74 100644
--- a/src/ballistica/shared/foundation/logging.cc
+++ b/src/ballistica/shared/foundation/logging.cc
@@ -38,7 +38,7 @@ void Logging::V1CloudLog(const std::string& msg) {
if (g_core) {
// (ship to things like Crashlytics crash-logging)
- g_core->platform->DebugLog(msg);
+ g_core->platform->LowLevelDebugLog(msg);
// Add to our complete v1-cloud-log.
std::scoped_lock lock(g_core->v1_cloud_log_mutex);
diff --git a/src/ballistica/shared/generic/runnable.cc b/src/ballistica/shared/generic/runnable.cc
index 3f7faa91..f5ebb16f 100644
--- a/src/ballistica/shared/generic/runnable.cc
+++ b/src/ballistica/shared/generic/runnable.cc
@@ -2,8 +2,13 @@
#include "ballistica/shared/generic/runnable.h"
+#include "ballistica/core/core.h"
+#include "ballistica/core/platform/core_platform.h"
+
namespace ballistica {
+using core::g_core;
+
auto Runnable::GetThreadOwnership() const -> Object::ThreadOwnership {
return ThreadOwnership::kNextReferencing;
}
@@ -12,7 +17,14 @@ void Runnable::RunAndLogErrors() {
try {
Run();
} catch (const std::exception& exc) {
- Log(LogLevel::kError, std::string("Error in Runnable: ") + exc.what());
+ std::string type_name;
+ if (g_core != nullptr) {
+ type_name = g_core->platform->DemangleCXXSymbol(typeid(exc).name());
+ } else {
+ type_name = "";
+ }
+ Log(LogLevel::kError,
+ std::string("Error in Runnable: " + type_name + ": ") + exc.what());
}
}
diff --git a/src/ballistica/shared/networking/sockaddr.cc b/src/ballistica/shared/networking/sockaddr.cc
index 68fdf993..6dd5b190 100644
--- a/src/ballistica/shared/networking/sockaddr.cc
+++ b/src/ballistica/shared/networking/sockaddr.cc
@@ -14,22 +14,48 @@ SockAddr::SockAddr(const std::string& addr, int port) {
if (result == 1) {
auto* a = reinterpret_cast(&addr_);
a->sin_family = AF_INET;
- a->sin_port = htons(port); // NOLINT
+ a->sin_port = htons(port);
a->sin_addr = addr_out;
return;
} else {
- struct in6_addr addr6Out {};
- result = inet_pton(AF_INET6, addr.c_str(), &addr6Out);
+ struct in6_addr addr6_out {};
+ result = inet_pton(AF_INET6, addr.c_str(), &addr6_out);
if (result == 1) {
auto* a = reinterpret_cast(&addr_);
a->sin6_family = AF_INET6;
- a->sin6_port = htons(port); // NOLINT
- a->sin6_addr = addr6Out;
+ a->sin6_port = htons(port);
+ a->sin6_addr = addr6_out;
return;
}
}
}
- throw Exception("Invalid address: '" + addr + "'.");
+ throw Exception("Invalid address: '" + addr + "'.", PyExcType::kValue);
+}
+
+auto SockAddr::AddressString() const -> std::string {
+ if (IsV6()) {
+ char ip_str[INET6_ADDRSTRLEN];
+ if (inet_ntop(AF_INET6, &(AsSockAddrIn6()->sin6_addr), ip_str,
+ INET6_ADDRSTRLEN)
+ == nullptr) {
+ throw Exception("inet_ntop failed for v6 addr", PyExcType::kValue);
+ }
+ return ip_str;
+ }
+ char ip_str[INET_ADDRSTRLEN];
+ if (inet_ntop(AF_INET, &(AsSockAddrIn()->sin_addr), ip_str, INET_ADDRSTRLEN)
+ == nullptr) {
+ throw Exception("inet_ntop failed for v4 addr", PyExcType::kValue);
+ }
+ return ip_str;
+}
+
+auto SockAddr::Port() const -> int {
+ if (IsV6()) {
+ return ntohs(AsSockAddrIn6()->sin6_port);
+ } else {
+ return ntohs(AsSockAddrIn()->sin_port);
+ }
}
} // namespace ballistica
diff --git a/src/ballistica/shared/networking/sockaddr.h b/src/ballistica/shared/networking/sockaddr.h
index f372bf70..3e880c56 100644
--- a/src/ballistica/shared/networking/sockaddr.h
+++ b/src/ballistica/shared/networking/sockaddr.h
@@ -15,16 +15,33 @@ class SockAddr {
public:
SockAddr() { memset(&addr_, 0, sizeof(addr_)); }
- // Creates from an ipv4 or ipv6 address string;
- // throws an exception on error.
+ // Creates from an ipv4 or ipv6 address string; throws an exception on
+ // error.
SockAddr(const std::string& addr, int port);
+
explicit SockAddr(const sockaddr_storage& addr_in) {
addr_ = addr_in;
assert(addr_.ss_family == AF_INET || addr_.ss_family == AF_INET6);
}
- auto GetSockAddr() const -> const sockaddr* {
+
+ auto AsSockAddr() const -> const sockaddr* {
return reinterpret_cast(&addr_);
}
+
+ auto AsSockAddrIn() const -> const sockaddr_in* {
+ assert(!IsV6());
+ return reinterpret_cast(&addr_);
+ }
+
+ auto AsSockAddrIn6() const -> const sockaddr_in6* {
+ assert(IsV6());
+ return reinterpret_cast(&addr_);
+ }
+
+ auto AddressString() const -> std::string;
+
+ auto Port() const -> int;
+
auto GetSockAddrLen() const -> socklen_t {
switch (addr_.ss_family) {
case AF_INET:
@@ -32,9 +49,10 @@ class SockAddr {
case AF_INET6:
return sizeof(sockaddr_in6);
default:
- throw Exception();
+ throw Exception(PyExcType::kValue);
}
}
+
auto IsV6() const -> bool {
switch (addr_.ss_family) {
case AF_INET:
@@ -45,25 +63,22 @@ class SockAddr {
throw Exception();
}
}
+
auto operator==(const SockAddr& other) const -> bool {
if (addr_.ss_family != other.addr_.ss_family) return false;
if (addr_.ss_family == AF_INET) {
- return (reinterpret_cast(addr_).sin_addr.s_addr
- == reinterpret_cast(other.addr_)
- .sin_addr.s_addr)
- && (reinterpret_cast(addr_).sin_port
- == reinterpret_cast(other.addr_).sin_port);
+ auto* a1 = AsSockAddrIn();
+ auto* a2 = other.AsSockAddrIn();
+ return !memcmp(&(a1->sin_addr), &(a2->sin_addr), sizeof(in_addr))
+ && a1->sin_port == a2->sin_port;
}
if (addr_.ss_family == AF_INET6) {
- return !memcmp(&(reinterpret_cast(addr_).sin6_addr),
- &(reinterpret_cast(other.addr_)
- .sin6_addr),
- sizeof(in6_addr))
- && (reinterpret_cast(addr_).sin6_port
- == reinterpret_cast(other.addr_)
- .sin6_port);
+ auto* a1 = AsSockAddrIn6();
+ auto* a2 = other.AsSockAddrIn6();
+ return !memcmp(&(a1->sin6_addr), &(a2->sin6_addr), sizeof(in6_addr))
+ && a1->sin6_port == a2->sin6_port;
}
- throw Exception();
+ throw Exception(PyExcType::kValue);
}
private:
diff --git a/src/ballistica/shared/python/python.cc b/src/ballistica/shared/python/python.cc
index 567a9831..0068077b 100644
--- a/src/ballistica/shared/python/python.cc
+++ b/src/ballistica/shared/python/python.cc
@@ -416,9 +416,7 @@ class Python::ScopedInterpreterLock::Impl {
};
Python::ScopedInterpreterLock::ScopedInterpreterLock()
- : impl_{new Python::ScopedInterpreterLock::Impl()}
-// impl_{std::make_unique()}
-{}
+ : impl_{new Python::ScopedInterpreterLock::Impl()} {}
Python::ScopedInterpreterLock::~ScopedInterpreterLock() { delete impl_; }
diff --git a/src/ballistica/shared/python/python.h b/src/ballistica/shared/python/python.h
index 2a94c9e3..4d2e1615 100644
--- a/src/ballistica/shared/python/python.h
+++ b/src/ballistica/shared/python/python.h
@@ -41,7 +41,8 @@ class Python {
/// Use this to protect Python code that may be run in cases where we
/// don't hold the Global Interpreter Lock (GIL). (Basically anything
- /// outside of the logic thread).
+ /// outside of the logic thread). This will release and then restore
+ /// the GIL if it is held initially; otherwise it is a no-op.
class ScopedInterpreterLock {
public:
ScopedInterpreterLock();
@@ -49,9 +50,6 @@ class Python {
private:
class Impl;
- // Note: should use unique_ptr for this, but build fails on raspberry pi
- // (gcc 8.3.0). Works on Ubuntu 9.3 so should try again later.
- // std::unique_ptr impl_{};
Impl* impl_{};
};
@@ -64,9 +62,6 @@ class Python {
private:
class Impl;
- // Note: should use unique_ptr for this, but build fails on raspberry pi
- // (gcc 8.3.0). Works on Ubuntu 9.3 so should try again later.
- // std::unique_ptr impl_{};
Impl* impl_{};
};
diff --git a/src/ballistica/shared/python/python_ref.h b/src/ballistica/shared/python/python_ref.h
index 141aeebf..b2455d26 100644
--- a/src/ballistica/shared/python/python_ref.h
+++ b/src/ballistica/shared/python/python_ref.h
@@ -118,7 +118,9 @@ class PythonRef {
/// Release the held reference (if one is held).
void Release();
- /// Clear the ref without decrementing its count and return the raw PyObject*
+ /// Clear the ref without decrementing its count and return the raw
+ /// PyObject*. Useful for functions that are expected to return a new
+ /// Python ref.
auto HandOver() -> PyObject* {
assert(obj_);
PyObject* obj = obj_;
@@ -151,8 +153,9 @@ class PythonRef {
/// Throws Exception if an error occurs.
auto DictGetItem(const char* name) const -> PythonRef;
- /// The equivalent of calling Python str() on the contained PyObject.
- /// Gracefully handles invalid refs.
+ /// The equivalent of calling Python str() on the contained PyObject, and
+ /// gracefully handles invalid refs. To throw exceptions on invalid refs,
+ /// use ValueAsString();
auto Str() const -> std::string;
/// The equivalent of calling repr() on the contained PyObject.
diff --git a/src/ballistica/ui_v1/python/class/python_class_widget.cc b/src/ballistica/ui_v1/python/class/python_class_widget.cc
index 7a31bb13..57853ee0 100644
--- a/src/ballistica/ui_v1/python/class/python_class_widget.cc
+++ b/src/ballistica/ui_v1/python/class/python_class_widget.cc
@@ -17,6 +17,12 @@ auto PythonClassWidget::nb_bool(PythonClassWidget* self) -> int {
PyNumberMethods PythonClassWidget::as_number_;
+// Attrs we expose through our custom getattr/setattr.
+#define ATTR_TRANSITIONING_OUT "transitioning_out"
+
+// The set we expose via dir().
+static const char* extra_dir_attrs[] = {ATTR_TRANSITIONING_OUT, nullptr};
+
auto PythonClassWidget::type_name() -> const char* { return "Widget"; }
void PythonClassWidget::SetupType(PyTypeObject* cls) {
@@ -24,6 +30,9 @@ void PythonClassWidget::SetupType(PyTypeObject* cls) {
// Fully qualified type path we will be exposed as:
cls->tp_name = "bauiv1.Widget";
cls->tp_basicsize = sizeof(PythonClassWidget);
+
+ // clang-format off
+
cls->tp_doc =
"Internal type for low level UI elements; buttons, windows, etc.\n"
"\n"
@@ -31,11 +40,22 @@ void PythonClassWidget::SetupType(PyTypeObject* cls) {
"\n"
"This class represents a weak reference to a widget object\n"
"in the internal C++ layer. Currently, functions such as\n"
- "babase.buttonwidget() must be used to instantiate or edit these.";
+ "babase.buttonwidget() must be used to instantiate or edit these.\n"
+ "Attributes:\n"
+ " " ATTR_TRANSITIONING_OUT " (bool):\n"
+ " Whether this widget is in the process of dying (read only).\n"
+ "\n"
+ " It can be useful to check this on a window's root widget to\n"
+ " prevent multiple window actions from firing simultaneously,\n"
+ " potentially leaving the UI in a broken state.\n";
+
+ // clang-format on
+
cls->tp_new = tp_new;
cls->tp_dealloc = (destructor)tp_dealloc;
cls->tp_repr = (reprfunc)tp_repr;
cls->tp_methods = tp_methods;
+ cls->tp_getattro = (getattrofunc)tp_getattro;
// we provide number methods only for bool functionality
memset(&as_number_, 0, sizeof(as_number_));
@@ -44,7 +64,7 @@ void PythonClassWidget::SetupType(PyTypeObject* cls) {
}
auto PythonClassWidget::Create(Widget* widget) -> PyObject* {
- // Make sure we only have one python ref per widget.
+ // Make sure we only have one Python ref per Widget.
if (widget) {
assert(!widget->has_py_ref());
}
@@ -62,10 +82,56 @@ auto PythonClassWidget::Create(Widget* widget) -> PyObject* {
auto PythonClassWidget::GetWidget() const -> Widget* {
Widget* w = widget_->Get();
- if (!w) throw Exception("Invalid widget");
+ if (!w) {
+ throw Exception("Invalid Widget", PyExcType::kReference);
+ }
return w;
}
+auto PythonClassWidget::tp_getattro(PythonClassWidget* self, PyObject* attr)
+ -> PyObject* {
+ BA_PYTHON_TRY;
+
+ BA_PRECONDITION(g_base->InLogicThread());
+
+ // Assuming this will always be a str?
+ assert(PyUnicode_Check(attr));
+
+ const char* s = PyUnicode_AsUTF8(attr);
+ if (!strcmp(s, ATTR_TRANSITIONING_OUT)) {
+ Widget* w = self->widget_->Get();
+ if (!w) {
+ throw Exception("Invalid Widget", PyExcType::kReference);
+ }
+ if (w->IsTransitioningOut()) {
+ Py_RETURN_TRUE;
+ }
+ Py_RETURN_FALSE;
+ }
+
+ // Fall back to generic behavior.
+ PyObject* val;
+ val = PyObject_GenericGetAttr(reinterpret_cast(self), attr);
+ return val;
+ BA_PYTHON_CATCH;
+}
+
+auto PythonClassWidget::tp_setattro(PythonClassWidget* self, PyObject* attr,
+ PyObject* val) -> int {
+ BA_PYTHON_TRY;
+
+ BA_PRECONDITION(g_base->InLogicThread());
+
+ // Assuming this will always be a str?
+ assert(PyUnicode_Check(attr));
+ const char* s = PyUnicode_AsUTF8(attr);
+
+ throw Exception("Attr '" + std::string(PyUnicode_AsUTF8(attr))
+ + "' is not settable on SessionPlayer objects.",
+ PyExcType::kAttribute);
+ BA_PYTHON_INT_CATCH;
+}
+
auto PythonClassWidget::tp_repr(PythonClassWidget* self) -> PyObject* {
BA_PYTHON_TRY;
Widget* w = self->widget_->Get();
@@ -96,8 +162,8 @@ auto PythonClassWidget::tp_new(PyTypeObject* type, PyObject* args,
void PythonClassWidget::tp_dealloc(PythonClassWidget* self) {
BA_PYTHON_TRY;
- // these have to be destructed in the logic thread - send them along to it if
- // need be
+ // these have to be destructed in the logic thread - send them along to it
+ // if need be
if (!g_base->InLogicThread()) {
Object::WeakRef* w = self->widget_;
g_base->logic->event_loop()->PushCall([w] { delete w; });
diff --git a/src/ballistica/ui_v1/python/class/python_class_widget.h b/src/ballistica/ui_v1/python/class/python_class_widget.h
index 012612da..b4e98409 100644
--- a/src/ballistica/ui_v1/python/class/python_class_widget.h
+++ b/src/ballistica/ui_v1/python/class/python_class_widget.h
@@ -26,6 +26,9 @@ class PythonClassWidget : public PythonClass {
static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds)
-> PyObject*;
static void tp_dealloc(PythonClassWidget* self);
+ static auto tp_getattro(PythonClassWidget* self, PyObject* attr) -> PyObject*;
+ static auto tp_setattro(PythonClassWidget* self, PyObject* attr,
+ PyObject* val) -> int;
static auto Exists(PythonClassWidget* self) -> PyObject*;
static auto GetWidgetType(PythonClassWidget* self) -> PyObject*;
static auto Activate(PythonClassWidget* self) -> PyObject*;
diff --git a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc
index ce6f13dd..8c0aefe5 100644
--- a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc
+++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc
@@ -1219,17 +1219,17 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds)
if (transition_obj != Py_None) {
std::string t = Python::GetPyString(transition_obj);
if (t == "in_left")
- widget->SetTransition(ContainerWidget::TRANSITION_IN_LEFT);
+ widget->SetTransition(ContainerWidget::TransitionType::kInLeft);
else if (t == "in_right")
- widget->SetTransition(ContainerWidget::TRANSITION_IN_RIGHT);
+ widget->SetTransition(ContainerWidget::TransitionType::kInRight);
else if (t == "out_left")
- widget->SetTransition(ContainerWidget::TRANSITION_OUT_LEFT);
+ widget->SetTransition(ContainerWidget::TransitionType::kOutLeft);
else if (t == "out_right")
- widget->SetTransition(ContainerWidget::TRANSITION_OUT_RIGHT);
+ widget->SetTransition(ContainerWidget::TransitionType::kOutRight);
else if (t == "in_scale")
- widget->SetTransition(ContainerWidget::TRANSITION_IN_SCALE);
+ widget->SetTransition(ContainerWidget::TransitionType::kInScale);
else if (t == "out_scale")
- widget->SetTransition(ContainerWidget::TRANSITION_OUT_SCALE);
+ widget->SetTransition(ContainerWidget::TransitionType::kOutScale);
}
if (cancel_button_obj != Py_None) {
@@ -2416,9 +2416,9 @@ static auto PyShowAd(PyObject* self, PyObject* args, PyObject* keywds)
static_cast(pass_actually_showed));
// In cases where we support ads, store our callback and kick one off.
- // We'll then fire our callback once its done.
- // If we *don't* support ads, just store our callback and then kick off
- // an ad-view-complete message ourself so the event flow is similar..
+ // We'll then fire our callback once its done. If we *don't* support ads,
+ // just store our callback and then kick off an ad-view-complete message
+ // ourself so the event flow is similar..
if (g_core->platform->GetHasAds()) {
g_core->platform->ShowAd(purpose);
} else {
@@ -2589,90 +2589,6 @@ static PyMethodDef PyGetSpecialWidgetDef = {
"(internal)",
};
-// -------------------------- have_incentivized_ad -----------------------------
-
-// returns an extra hash value that can be incorporated into security checks;
-// this contains things like whether console commands have been run, etc.
-static auto PyHaveIncentivizedAd(PyObject* self, PyObject* args,
- PyObject* keywds) -> PyObject* {
- BA_PYTHON_TRY;
- static const char* kwlist[] = {nullptr};
- if (!PyArg_ParseTupleAndKeywords(args, keywds, "",
- const_cast(kwlist))) {
- return nullptr;
- }
- if (g_core->have_incentivized_ad) {
- Py_RETURN_TRUE;
- } else {
- Py_RETURN_FALSE;
- }
- BA_PYTHON_CATCH;
-}
-
-static PyMethodDef PyHaveIncentivizedAdDef = {
- "have_incentivized_ad", // name
- (PyCFunction)PyHaveIncentivizedAd, // method
- METH_VARARGS | METH_KEYWORDS, // flags
-
- "have_incentivized_ad() -> bool\n"
- "\n"
- "(internal)",
-};
-
-// ----------------------------- can_show_ad -----------------------------------
-
-// this returns whether it makes sense to show an currently
-static auto PyCanShowAd(PyObject* self, PyObject* args, PyObject* keywds)
- -> PyObject* {
- BA_PYTHON_TRY;
-
- BA_PRECONDITION(g_base->InLogicThread());
- // if we've got any network connections, no ads.
- // (don't want to make someone on the other end wait or risk disconnecting
- // them or whatnot). Also disallow ads if remote apps are connected; at least
- // on Android, ads pause our activity which disconnects the remote app.
- // (need to fix this).
- if (g_base->app_mode()->HasConnectionToHost()
- || g_base->app_mode()->HasConnectionToClients()
- || g_base->input->HaveRemoteAppController()) {
- Py_RETURN_FALSE;
- }
- Py_RETURN_TRUE; // all systems go..
- BA_PYTHON_CATCH;
-}
-
-static PyMethodDef PyCanShowAdDef = {
- "can_show_ad", // name
- (PyCFunction)PyCanShowAd, // method
- METH_VARARGS | METH_KEYWORDS, // flags
- "can_show_ad() -> bool\n"
- "\n"
- "(internal)",
-};
-
-// ---------------------------- has_video_ads ----------------------------------
-
-static auto PyHasVideoAds(PyObject* self, PyObject* args, PyObject* keywds)
- -> PyObject* {
- BA_PYTHON_TRY;
- if (g_core->platform->GetHasVideoAds()) {
- Py_RETURN_TRUE;
- } else {
- Py_RETURN_FALSE;
- }
- BA_PYTHON_CATCH;
-}
-
-static PyMethodDef PyHasVideoAdsDef = {
- "has_video_ads", // name
- (PyCFunction)PyHasVideoAds, // method
- METH_VARARGS | METH_KEYWORDS, // flags
-
- "has_video_ads() -> bool\n"
- "\n"
- "(internal)",
-};
-
// ------------------------------ back_press -----------------------------------
static auto PyBackPress(PyObject* self, PyObject* args, PyObject* keywds)
@@ -2893,9 +2809,6 @@ auto PythonMethodsUIV1::GetMethods() -> std::vector {
PyOpenFileExternallyDef,
PyOpenURLDef,
PyBackPressDef,
- PyHasVideoAdsDef,
- PyCanShowAdDef,
- PyHaveIncentivizedAdDef,
PyGetSpecialWidgetDef,
PySetPartyWindowOpenDef,
PySetPartyIconAlwaysVisibleDef,
diff --git a/src/ballistica/ui_v1/python/ui_v1_python.cc b/src/ballistica/ui_v1/python/ui_v1_python.cc
index f4157594..f12321b0 100644
--- a/src/ballistica/ui_v1/python/ui_v1_python.cc
+++ b/src/ballistica/ui_v1/python/ui_v1_python.cc
@@ -115,7 +115,7 @@ void UIV1Python::InvokeStringEditor(PyObject* string_edit_adapter_instance) {
PythonRef::kSteal);
Object::New(
objs().Get(ObjID::kOnScreenKeyboardClass))
- ->ScheduleOnce(args);
+ ->Schedule(args);
}
void UIV1Python::LaunchStringEditOld(TextWidget* w) {
@@ -131,7 +131,7 @@ void UIV1Python::LaunchStringEditOld(TextWidget* w) {
PythonRef::kSteal);
Object::New(
objs().Get(ObjID::kOnScreenKeyboardClass))
- ->ScheduleOnce(args);
+ ->Schedule(args);
}
void UIV1Python::InvokeQuitWindow(QuitType quit_type) {
diff --git a/src/ballistica/ui_v1/python/ui_v1_python.h b/src/ballistica/ui_v1/python/ui_v1_python.h
index 8e10e9a1..734cd476 100644
--- a/src/ballistica/ui_v1/python/ui_v1_python.h
+++ b/src/ballistica/ui_v1/python/ui_v1_python.h
@@ -39,6 +39,7 @@ class UIV1Python {
kQuitWindowCall,
kDeviceMenuPressCall,
kShowURLWindowCall,
+ kDoubleTransitionOutWarningCall,
kTextWidgetStringEditAdapterClass,
kLast // Sentinel; must be at end.
};
diff --git a/src/ballistica/ui_v1/widget/button_widget.cc b/src/ballistica/ui_v1/widget/button_widget.cc
index 7b824db1..29c6db6a 100644
--- a/src/ballistica/ui_v1/widget/button_widget.cc
+++ b/src/ballistica/ui_v1/widget/button_widget.cc
@@ -562,7 +562,7 @@ void ButtonWidget::DoActivate(bool is_repeat) {
if (auto* call = on_activate_call_.Get()) {
// Call this in the next cycle (don't want to risk mucking with UI from
// within a UI loop.)
- call->ScheduleWeakOnce();
+ call->ScheduleWeak();
return;
}
}
diff --git a/src/ballistica/ui_v1/widget/check_box_widget.cc b/src/ballistica/ui_v1/widget/check_box_widget.cc
index f252870a..a6e47b3e 100644
--- a/src/ballistica/ui_v1/widget/check_box_widget.cc
+++ b/src/ballistica/ui_v1/widget/check_box_widget.cc
@@ -247,7 +247,7 @@ void CheckBoxWidget::Activate() {
// Call this in the next cycle (don't want to risk mucking with UI from
// within a UI loop)
- call->ScheduleWeakOnce(args);
+ call->ScheduleWeak(args);
}
}
@@ -271,12 +271,13 @@ auto CheckBoxWidget::HandleMessage(const base::WidgetMessage& m) -> bool {
float x = m.fval1;
float y = m.fval2;
bool claimed = (m.fval3 > 0.0f);
- if (claimed)
+ if (claimed) {
mouse_over_ = false;
- else
+ } else {
mouse_over_ =
((x >= (-left_overlap)) && (x < (width_ + right_overlap))
&& (y >= (-bottom_overlap)) && (y < (height_ + top_overlap)));
+ }
return mouse_over_;
}
case base::WidgetMessage::Type::kMouseDown: {
diff --git a/src/ballistica/ui_v1/widget/container_widget.cc b/src/ballistica/ui_v1/widget/container_widget.cc
index e47bbb17..651c9866 100644
--- a/src/ballistica/ui_v1/widget/container_widget.cc
+++ b/src/ballistica/ui_v1/widget/container_widget.cc
@@ -11,6 +11,7 @@
#include "ballistica/shared/generic/utils.h"
#include "ballistica/shared/math/random.h"
#include "ballistica/shared/python/python.h"
+#include "ballistica/ui_v1/python/ui_v1_python.h"
#include "ballistica/ui_v1/widget/button_widget.h"
#include "ballistica/ui_v1/widget/root_widget.h"
#include "ballistica/ui_v1/widget/stack_widget.h"
@@ -347,7 +348,7 @@ auto ContainerWidget::HandleMessage(const base::WidgetMessage& m) -> bool {
// Call this in the next cycle (don't wanna risk mucking with UI from
// within a UI loop).
- call->ScheduleWeakOnce();
+ call->ScheduleWeak();
} else {
OnCancelCustom();
}
@@ -631,7 +632,7 @@ auto ContainerWidget::HandleMessage(const base::WidgetMessage& m) -> bool {
if (!claimed && on_outside_click_call_.Exists()) {
// Call this in the next cycle (don't wanna risk mucking with UI from
// within a UI loop).
- on_outside_click_call_->ScheduleWeakOnce();
+ on_outside_click_call_->ScheduleWeak();
}
// Always claim if they want.
@@ -793,7 +794,7 @@ void ContainerWidget::Draw(base::RenderPass* pass, bool draw_transparent) {
bg_dirty_ = true;
if (!draw_transparent) {
- if (transition_type_ == TRANSITION_IN_SCALE) {
+ if (transition_type_ == TransitionType::kInScale) {
if (net_time - dynamics_update_time_millisecs_ > 1000)
dynamics_update_time_millisecs_ = net_time - 1000;
while (net_time - dynamics_update_time_millisecs_ > 5) {
@@ -808,7 +809,7 @@ void ContainerWidget::Draw(base::RenderPass* pass, bool draw_transparent) {
transitioning_ = false;
}
}
- } else if (transition_type_ == TRANSITION_OUT_SCALE) {
+ } else if (transition_type_ == TransitionType::kOutScale) {
if (net_time - dynamics_update_time_millisecs_ > 1000)
dynamics_update_time_millisecs_ = net_time - 1000;
while (net_time - dynamics_update_time_millisecs_ > 5) {
@@ -908,8 +909,8 @@ void ContainerWidget::Draw(base::RenderPass* pass, bool draw_transparent) {
// If we're scaling in or out, update our transition offset
// (so we can zoom from a point somewhere else on screen).
- if (transition_type_ == TRANSITION_IN_SCALE
- || transition_type_ == TRANSITION_OUT_SCALE) {
+ if (transition_type_ == TransitionType::kInScale
+ || transition_type_ == TransitionType::kOutScale) {
// Add a fudge factor since our scale point isn't exactly in our center.
// :-(
float xdiff = scale_origin_stack_offset_x_ - stack_offset_x()
@@ -1071,7 +1072,7 @@ void ContainerWidget::Activate() {
if (auto* call = on_activate_call_.Get()) {
// Call this in the next cycle (don't wanna risk mucking with UI from within
// a UI loop).
- call->ScheduleWeakOnce();
+ call->ScheduleWeak();
}
}
@@ -1156,8 +1157,29 @@ void ContainerWidget::SetStartButton(ButtonWidget* button) {
button->set_icon_type(ButtonWidget::IconType::kStart);
}
+static auto _IsTransitionOut(ContainerWidget::TransitionType type) {
+ // Note: framing this without a 'default:' so we get compiler warnings
+ // when enums are added/removed.
+ bool val = false;
+ switch (type) {
+ case ContainerWidget::TransitionType::kUnset:
+ case ContainerWidget::TransitionType::kInLeft:
+ case ContainerWidget::TransitionType::kInRight:
+ case ContainerWidget::TransitionType::kInScale:
+ val = false;
+ break;
+ case ContainerWidget::TransitionType::kOutLeft:
+ case ContainerWidget::TransitionType::kOutRight:
+ case ContainerWidget::TransitionType::kOutScale:
+ val = true;
+ break;
+ }
+ return val;
+}
+
void ContainerWidget::SetTransition(TransitionType t) {
BA_DEBUG_UI_READ_LOCK;
+ assert(g_base->InLogicThread());
bg_dirty_ = glow_dirty_ = true;
ContainerWidget* parent = parent_widget();
@@ -1167,17 +1189,26 @@ void ContainerWidget::SetTransition(TransitionType t) {
parent->CheckLayout();
auto display_time_millisecs =
static_cast(g_base->logic->display_time() * 1000.0);
+
+ // Warn if setting out-transition twice. This likely means a window is
+ // switching to another window twice which can leave the UI broken.
+ if (_IsTransitionOut(transition_type_) && _IsTransitionOut(t)) {
+ g_ui_v1->python->objs()
+ .Get(UIV1Python::ObjID::kDoubleTransitionOutWarningCall)
+ .Call();
+ }
+
transition_type_ = t;
// Scale transitions are simpler.
- if (t == TRANSITION_IN_SCALE) {
+ if (t == TransitionType::kInScale) {
transition_start_time_ = display_time_millisecs;
dynamics_update_time_millisecs_ = display_time_millisecs;
transitioning_ = true;
transitioning_out_ = false;
transition_scale_ = 0.0f;
d_transition_scale_ = 0.0f;
- } else if (t == TRANSITION_OUT_SCALE) {
+ } else if (t == TransitionType::kOutScale) {
transition_start_time_ = display_time_millisecs;
dynamics_update_time_millisecs_ = display_time_millisecs;
transitioning_ = true;
@@ -1195,7 +1226,7 @@ void ContainerWidget::SetTransition(TransitionType t) {
// In case we're mid-transition, this avoids hitches.
float y_offs = 2.0f;
- if (t == TRANSITION_IN_LEFT) {
+ if (t == TransitionType::kInLeft) {
transition_start_time_ = display_time_millisecs;
transition_start_offset_ = screen_min_x - width_ - 100;
transition_offset_x_smoothed_ = transition_start_offset_;
@@ -1204,7 +1235,7 @@ void ContainerWidget::SetTransition(TransitionType t) {
transitioning_ = true;
dynamics_update_time_millisecs_ = display_time_millisecs;
transitioning_out_ = false;
- } else if (t == TRANSITION_IN_RIGHT) {
+ } else if (t == TransitionType::kInRight) {
transition_start_time_ = display_time_millisecs;
transition_start_offset_ = screen_max_x + 100;
transition_offset_x_smoothed_ = transition_start_offset_;
@@ -1213,7 +1244,7 @@ void ContainerWidget::SetTransition(TransitionType t) {
transitioning_ = true;
dynamics_update_time_millisecs_ = display_time_millisecs;
transitioning_out_ = false;
- } else if (t == TRANSITION_OUT_LEFT) {
+ } else if (t == TransitionType::kOutLeft) {
transition_start_time_ = display_time_millisecs;
transition_start_offset_ = transition_offset_x_;
transition_target_offset_ = -2.0f * (screen_max_x - screen_min_x);
@@ -1223,7 +1254,7 @@ void ContainerWidget::SetTransition(TransitionType t) {
dynamics_update_time_millisecs_ = display_time_millisecs;
transitioning_out_ = true;
ignore_input_ = true;
- } else if (t == TRANSITION_OUT_RIGHT) {
+ } else if (t == TransitionType::kOutRight) {
transition_start_time_ = display_time_millisecs;
transition_start_offset_ = transition_offset_x_;
transition_target_offset_ = 2.0f * (screen_max_x - screen_min_x);
@@ -1279,8 +1310,9 @@ void ContainerWidget::DeleteWidget(Widget* w) {
assert(found);
- // Special case: if we're the overlay stack and we've deleted our last widget,
- // try to reselect whatever was last selected before the overlay stack.
+ // Special case: if we're the overlay stack and we've deleted our last
+ // widget, try to reselect whatever was last selected before the overlay
+ // stack.
if (is_overlay_window_stack_) {
if (widgets_.empty()) {
// Eww this logic should be in some sort of controller.
@@ -1298,8 +1330,9 @@ void ContainerWidget::DeleteWidget(Widget* w) {
if ((**i).IsSelectable()) {
// A change on the main or overlay window stack changes the global
// selection (unless its on the main window stack and there's already
- // something on the overlay stack) in all other cases we just shift our
- // direct selected child (which may not affect the global selection).
+ // something on the overlay stack) in all other cases we just shift
+ // our direct selected child (which may not affect the global
+ // selection).
if (is_window_stack_
&& (is_overlay_window_stack_
|| !g_ui_v1->root_widget()
@@ -1322,8 +1355,8 @@ void ContainerWidget::DeleteWidget(Widget* w) {
}
auto ContainerWidget::GetTopmostToolbarInfluencingWidget() -> Widget* {
- // Look for the first window that is accepting input (filters out windows that
- // are transitioning out) and also set to affect the toolbar state.
+ // Look for the first window that is accepting input (filters out windows
+ // that are transitioning out) and also set to affect the toolbar state.
for (auto w = widgets_.rbegin(); w != widgets_.rend(); ++w) {
if ((**w).IsAcceptingInput()
&& (**w).toolbar_visibility() != ToolbarVisibility::kInherit) {
@@ -1438,9 +1471,8 @@ void ContainerWidget::SetSelected(bool s, SelectionCause cause) {
}
}
} else {
- // if we're being deselected and we have a selected child, tell them they're
- // deselected
- // if (selected_widget_) {
+ // if we're being deselected and we have a selected child, tell them
+ // they're deselected if (selected_widget_) {
// }
}
}
@@ -1583,8 +1615,8 @@ void ContainerWidget::SelectDownWidget() {
selected_widget_->GetCenter(&our_x, &our_y);
w = GetClosestDownWidget(our_x, our_y, selected_widget_);
if (!w) {
- // If we found no viable children and we're under the main window stack,
- // see if we should pass focus to a toolbar widget.
+ // If we found no viable children and we're under the main window
+ // stack, see if we should pass focus to a toolbar widget.
if (IsInMainStack()) {
float x = our_x;
float y = our_y;
@@ -1712,7 +1744,8 @@ void ContainerWidget::SelectLeftWidget() {
float our_x, our_y;
selected_widget_->GetCenter(&our_x, &our_y);
w = GetClosestLeftWidget(our_x, our_y, selected_widget_);
- // When we find no viable targets for an autoselect widget we do nothing.
+ // When we find no viable targets for an autoselect widget we do
+ // nothing.
if (!w) {
return;
}
@@ -1843,8 +1876,8 @@ void ContainerWidget::SelectNextWidget() {
return;
} else if (selected_widget_
== nullptr) { // NOLINT(bugprone-branch-clone)
- // We've got no selection and we've scanned the whole list to no avail,
- // fail.
+ // We've got no selection and we've scanned the whole list to no
+ // avail, fail.
PrintExitListInstructions(old_last_prev_next_time);
return;
} else if (selection_loops()) {
@@ -1997,4 +2030,8 @@ void ContainerWidget::OnLanguageChange() {
}
}
+auto ContainerWidget::IsTransitioningOut() const -> bool {
+ return transitioning_out_;
+}
+
} // namespace ballistica::ui_v1
diff --git a/src/ballistica/ui_v1/widget/container_widget.h b/src/ballistica/ui_v1/widget/container_widget.h
index 30946207..5f55bd7e 100644
--- a/src/ballistica/ui_v1/widget/container_widget.h
+++ b/src/ballistica/ui_v1/widget/container_widget.h
@@ -14,20 +14,21 @@ namespace ballistica::ui_v1 {
// Base class for widgets that contain other widgets.
class ContainerWidget : public Widget {
public:
- explicit ContainerWidget(float width = 0, float height = 0);
+ explicit ContainerWidget(float width = 0.0f, float height = 0.0f);
~ContainerWidget() override;
void Draw(base::RenderPass* pass, bool transparent) override;
auto HandleMessage(const base::WidgetMessage& m) -> bool override;
- enum TransitionType {
- TRANSITION_OUT_LEFT,
- TRANSITION_OUT_RIGHT,
- TRANSITION_IN_LEFT,
- TRANSITION_IN_RIGHT,
- TRANSITION_IN_SCALE,
- TRANSITION_OUT_SCALE
+ enum class TransitionType {
+ kUnset,
+ kOutLeft,
+ kOutRight,
+ kInLeft,
+ kInRight,
+ kInScale,
+ kOutScale
};
void SetTransition(TransitionType t);
@@ -49,6 +50,7 @@ class ContainerWidget : public Widget {
width_ = w;
MarkForUpdate();
}
+
virtual void SetHeight(float h) {
bg_dirty_ = glow_dirty_ = true;
height_ = h;
@@ -67,6 +69,7 @@ class ContainerWidget : public Widget {
CheckLayout();
return width_;
}
+
auto GetHeight() -> float override {
CheckLayout();
return height_;
@@ -76,8 +79,8 @@ class ContainerWidget : public Widget {
auto HasKeySelectableChild() const -> bool;
- void set_is_window_stack(bool a) { is_window_stack_ = a; }
auto is_window_stack() const -> bool { return is_window_stack_; }
+ void set_is_window_stack(bool a) { is_window_stack_ = a; }
auto GetChildCount() const -> int {
assert(g_base->InLogicThread());
@@ -167,6 +170,8 @@ class ContainerWidget : public Widget {
// if the topmost one is transitioning out, etc.)
auto GetTopmostToolbarInfluencingWidget() -> Widget*;
+ auto IsTransitioningOut() const -> bool override;
+
protected:
virtual void OnCancelCustom() {}
void set_single_depth_root(bool s) { single_depth_root_ = s; }
diff --git a/src/ballistica/ui_v1/widget/text_widget.cc b/src/ballistica/ui_v1/widget/text_widget.cc
index 687c1689..c6c393a7 100644
--- a/src/ballistica/ui_v1/widget/text_widget.cc
+++ b/src/ballistica/ui_v1/widget/text_widget.cc
@@ -592,7 +592,7 @@ void TextWidget::Activate() {
if (auto* call = on_activate_call_.Get()) {
// Call this in the next cycle (don't wanna risk mucking with UI from
// within a UI loop).
- call->ScheduleWeakOnce();
+ call->ScheduleWeak();
}
// Bring up an editor if applicable.
@@ -688,13 +688,14 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool {
// If we're doing inline editing, handle clipboard paste.
if (editable() && !ShouldUseStringEditor_()
&& m.type == base::WidgetMessage::Type::kPaste) {
- if (g_base->app_adapter->ClipboardIsSupported()) {
- if (g_base->app_adapter->ClipboardHasText()) {
+ if (g_base->ClipboardIsSupported()) {
+ if (g_base->ClipboardHasText()) {
// Just enter it char by char as if we had typed it...
- AddCharsToText_(g_base->app_adapter->ClipboardGetText());
+ AddCharsToText_(g_base->ClipboardGetText());
}
}
}
+
// If we're doing inline editing, handle some key events.
if (editable() && m.has_keysym && !ShouldUseStringEditor_()) {
last_carat_change_time_millisecs_ =
@@ -720,7 +721,7 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool {
claimed = true;
// Call this in the next cycle (don't wanna risk mucking with UI
// from within a UI loop)
- call->ScheduleWeakOnce();
+ call->ScheduleWeak();
}
}
break;
diff --git a/src/ballistica/ui_v1/widget/widget.cc b/src/ballistica/ui_v1/widget/widget.cc
index 257d2198..c94910a7 100644
--- a/src/ballistica/ui_v1/widget/widget.cc
+++ b/src/ballistica/ui_v1/widget/widget.cc
@@ -88,7 +88,7 @@ void Widget::SetSelected(bool s, SelectionCause cause) {
if (selected_ && on_select_call_.Exists()) {
// Call this in the next cycle (don't wanna risk mucking
// with UI from within a UI loop).
- on_select_call_->ScheduleWeakOnce();
+ on_select_call_->ScheduleWeak();
}
}
@@ -238,4 +238,6 @@ auto Widget::IsAcceptingInput() const -> bool { return true; }
void Widget::Activate() {}
+auto Widget::IsTransitioningOut() const -> bool { return false; }
+
} // namespace ballistica::ui_v1
diff --git a/src/ballistica/ui_v1/widget/widget.h b/src/ballistica/ui_v1/widget/widget.h
index 56e7e18b..e6953c99 100644
--- a/src/ballistica/ui_v1/widget/widget.h
+++ b/src/ballistica/ui_v1/widget/widget.h
@@ -46,7 +46,8 @@ class Widget : public Object {
// Whether the widget (or its children) is selectable in any way.
virtual auto IsSelectable() -> bool;
- // Whether the widget can be selected by default with direction/tab presses.
+ // Whether the widget can be selected by default with direction/tab
+ // presses.
virtual auto IsSelectableViaKeys() -> bool;
// Is the widget currently accepting input?
@@ -83,8 +84,8 @@ class Widget : public Object {
// If this widget is in a container, return it.
auto parent_widget() const -> ContainerWidget* { return parent_widget_; }
- // Return the container_widget containing this widget, or the owner-widget if
- // there is no parent.
+ // Return the container_widget containing this widget, or the owner-widget
+ // if there is no parent.
auto GetOwnerWidget() const -> Widget*;
auto down_widget() const -> Widget* { return down_widget_.Get(); }
@@ -116,25 +117,23 @@ class Widget : public Object {
// redirecting them to transient per-window stuff).
void set_neighbors_locked(bool locked) { neighbors_locked_ = locked; }
- // Widgets normally draw with a local depth range of 0-1.
- // It can be useful to limit drawing to a subsection of that region however
- // (for manually resolving overlap issues with widgets at the same depth,
- // etc).
+ // Widgets normally draw with a local depth range of 0-1. It can be useful
+ // to limit drawing to a subsection of that region however (for manually
+ // resolving overlap issues with widgets at the same depth, etc).
void SetDepthRange(float minDepth, float maxDepth);
auto depth_range_min() const -> float { return depth_range_min_; }
auto depth_range_max() const -> float { return depth_range_max_; }
- // For use by ContainerWidgets.
- // (we probably should just this functionality to all widgets)
+ // For use by ContainerWidgets (we probably should just add this
+ // functionality to all widgets).
void set_parent_widget(ContainerWidget* c) { parent_widget_ = c; }
auto IsInMainStack() const -> bool;
auto IsInOverlayStack() const -> bool;
- // For use when embedding widgets inside others manually.
- // This will allow proper selection states/etc to trickle down to the
- // lowest-level child.
+ // For use when embedding widgets inside others manually. This will allow
+ // proper selection states/etc to trickle down to the lowest-level child.
void set_owner_widget(Widget* o) { owner_widget_ = o; }
virtual auto GetWidgetTypeName() -> std::string { return "widget"; }
virtual auto HasChildren() const -> bool { return false; }
@@ -167,22 +166,25 @@ class Widget : public Object {
void ScreenPointToWidget(float* x, float* y) const;
void WidgetPointToScreen(float* x, float* y) const;
- // Draw-control parents are used to give one widget some basic visual control
- // over others, allowing them to inherit things like draw-brightness and tilt
- // shift (for cases such as images drawn over buttons).
- // Ideally we'd probably want to extend the parent mechanism for this, but
- // this works for now.
+ // Draw-control parents are used to give one widget some basic visual
+ // control over others, allowing them to inherit things like
+ // draw-brightness and tilt shift (for cases such as images drawn over
+ // buttons). Ideally we'd probably want to extend the parent mechanism for
+ // this, but this works for now.
auto draw_control_parent() const -> Widget* {
return draw_control_parent_.Get();
}
void set_draw_control_parent(Widget* w) { draw_control_parent_ = w; }
- // Can be used to ask link-parents how bright to draw.
- // Note: make sure the value returned here does not get changed when draw()
- // is run, since parts of draw-controlled children may query this before
- // draw() and parts after. (and they need to line up visually)
+ // Can be used to ask link-parents how bright to draw. Note: make sure the
+ // value returned here does not get changed when draw() is run, since
+ // parts of draw-controlled children may query this before draw() and
+ // parts after. (and they need to line up visually)
virtual auto GetDrawBrightness(millisecs_t current_time) const -> float;
+ /// Is this widget in the process of transitioning out before dying?
+ virtual auto IsTransitioningOut() const -> bool;
+
// Extra buffer added around widgets when they are centered-on.
void set_show_buffer_top(float b) { show_buffer_top_ = b; }
void set_show_buffer_bottom(float b) { show_buffer_bottom_ = b; }
@@ -206,8 +208,8 @@ class Widget : public Object {
virtual void OnLanguageChange() {}
- // Primitive janktastic child culling for use by containers.
- // (should really implement something more proper...)
+ // Primitive janktastic child culling for use by containers (should really
+ // implement something more proper).
auto simple_culling_v() const -> float { return simple_culling_v_; }
auto simple_culling_h() const -> float { return simple_culling_h_; }
auto simple_culling_bottom() const -> float { return simple_culling_bottom_; }
@@ -224,14 +226,17 @@ class Widget : public Object {
private:
auto GetPyWidget(bool new_ref) -> PyObject*;
virtual void SetSelected(bool s, SelectionCause cause);
+ bool selected_{};
+ bool visible_in_container_{true};
+ bool neighbors_locked_{};
+ bool auto_select_{};
+ ToolbarVisibility toolbar_visibility_{ToolbarVisibility::kMenuMinimalNoBack};
float simple_culling_h_{-1.0f};
float simple_culling_v_{-1.0f};
float simple_culling_left_{};
float simple_culling_right_{};
float simple_culling_bottom_{};
float simple_culling_top_{};
- ToolbarVisibility toolbar_visibility_{ToolbarVisibility::kMenuMinimalNoBack};
- PyObject* py_ref_{};
float show_buffer_top_{20.0f};
float show_buffer_bottom_{20.0f};
float show_buffer_left_{20.0f};
@@ -241,12 +246,9 @@ class Widget : public Object {
Object::WeakRef up_widget_;
Object::WeakRef left_widget_;
Object::WeakRef right_widget_;
- bool neighbors_locked_{};
- bool auto_select_{};
ContainerWidget* parent_widget_{};
+ PyObject* py_ref_{};
Widget* owner_widget_{};
- bool selected_{};
- bool visible_in_container_{true};
float tx_{};
float ty_{};
float stack_offset_x_{};
diff --git a/src/meta/bascenev1meta/pyembed/binding_scene_v1.py b/src/meta/bascenev1meta/pyembed/binding_scene_v1.py
index c4a19066..4757e713 100644
--- a/src/meta/bascenev1meta/pyembed/binding_scene_v1.py
+++ b/src/meta/bascenev1meta/pyembed/binding_scene_v1.py
@@ -10,6 +10,7 @@ from bascenev1._player import Player
from bascenev1._dependency import AssetPackage
from bascenev1._activity import Activity
from bascenev1._session import Session
+from bascenev1._net import HostInfo
import _bascenev1
# The C++ layer looks for this variable:
@@ -30,4 +31,5 @@ values = [
AssetPackage, # kAssetPackageClass
Activity, # kActivityClass
Session, # kSceneV1SessionClass
+ HostInfo, # kHostInfoClass
]
diff --git a/src/meta/bauiv1meta/pyembed/binding_ui_v1.py b/src/meta/bauiv1meta/pyembed/binding_ui_v1.py
index 015d75ea..dc7f8c13 100644
--- a/src/meta/bauiv1meta/pyembed/binding_ui_v1.py
+++ b/src/meta/bauiv1meta/pyembed/binding_ui_v1.py
@@ -22,5 +22,6 @@ values = [
_hooks.quit_window, # kQuitWindowCall
_hooks.device_menu_press, # kDeviceMenuPressCall
_hooks.show_url_window, # kShowURLWindowCall
+ _hooks.double_transition_out_warning, # kDoubleTransitionOutWarningCall
TextWidgetStringEditAdapter, # kTextWidgetStringEditAdapterClass
]
diff --git a/tools/bacommon/app.py b/tools/bacommon/app.py
index d1b50fda..2b8abdc6 100644
--- a/tools/bacommon/app.py
+++ b/tools/bacommon/app.py
@@ -5,22 +5,49 @@
from __future__ import annotations
from enum import Enum
-from typing import TYPE_CHECKING
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Annotated
+
+from efro.dataclassio import ioprepped, IOAttrs
if TYPE_CHECKING:
pass
-class AppExperience(Enum):
- """Overall experience that can be provided by a Ballistica app.
+class AppInterfaceIdiom(Enum):
+ """A general form-factor or way of experiencing a Ballistica app.
- This corresponds generally, but not exactly, to distinct apps built
- with Ballistica. However, a single app may support multiple experiences,
- or there may be multiple apps targeting one experience. Cloud components
- such as leagues are generally associated with an AppExperience.
+ Note that it is possible for a running app to switch idioms (for
+ instance if a mobile device or computer is connected to a TV).
"""
- # A special experience category that is supported everywhere. Used
+ PHONE = 'phone'
+ TABLET = 'tablet'
+ DESKTOP = 'desktop'
+ TV = 'tv'
+ XR = 'xr'
+
+
+class AppExperience(Enum):
+ """A particular experience that can be provided by a Ballistica app.
+
+ This is one metric used to isolate different playerbases from
+ eachother where there might be no technical barriers doing so.
+ For example, a casual one-hand-playable phone game and an augmented
+ reality tabletop game may both use the same scene-versions and
+ networking-protocols and whatnot, but it would make no sense to
+ allow players of one join servers for the other. AppExperience can
+ be used to keep these player bases separate.
+
+ Generally a single Ballistica app targets a single AppExperience.
+ This is not a technical requirement, however. A single app may
+ support multiple experiences, or there may be multiple apps
+ targeting one experience. Cloud components such as leagues are
+ generally associated with an AppExperience so that they are only
+ visible to client apps designed for that play style.
+ """
+
+ # An experience that is supported everywhere. Used
# for the default empty AppMode when starting the app, etc.
EMPTY = 'empty'
@@ -33,3 +60,79 @@ class AppExperience(Enum):
# touch-screen allowing a mobile device to be used as a game
# controller.
REMOTE = 'remote'
+
+
+class AppArchitecture(Enum):
+ """Processor architecture the App is running on."""
+
+ ARM = 'arm'
+ ARM64 = 'arm64'
+ X86 = 'x86'
+ X86_64 = 'x86_64'
+
+
+class AppPlatform(Enum):
+ """Overall platform a Ballistica build can be targeting.
+
+ Each distinct flavor of an app has a unique combination
+ of AppPlatform and AppVariant. Generally platform describes
+ a set of hardware, while variant describes a destination or
+ purpose for the build.
+ """
+
+ MAC = 'mac'
+ WINDOWS = 'windows'
+ LINUX = 'linux'
+ ANDROID = 'android'
+ IOS = 'ios'
+ TVOS = 'tvos'
+
+
+class AppVariant(Enum):
+ """A unique Ballistica build type within a single platform.
+
+ Each distinct flavor of an app has a unique combination
+ of AppPlatform and AppVariant. Generally platform describes
+ a set of hardware, while variant describes a destination or
+ purpose for the build.
+ """
+
+ # Default builds.
+ GENERIC = 'generic'
+
+ # Builds intended for public testing (may have some extra checks
+ # or logging enabled).
+ TEST = 'test'
+
+ # Various stores.
+ AMAZON_APPSTORE = 'amazon_appstore'
+ GOOGLE_PLAY = 'google_play'
+ APP_STORE = 'app_store'
+ WINDOWS_STORE = 'windows_store'
+ STEAM = 'steam'
+ META = 'meta'
+ EPIC_GAMES_STORE = 'epic_games_store'
+
+ # Other.
+ ARCADE = 'arcade'
+ DEMO = 'demo'
+
+
+@ioprepped
+@dataclass
+class AppInstanceInfo:
+ """General info about an individual running app."""
+
+ name = Annotated[str, IOAttrs('n')]
+ version = Annotated[str, IOAttrs('v')]
+ build = Annotated[int, IOAttrs('b')]
+
+ platform = Annotated[AppPlatform, IOAttrs('p')]
+ variant = Annotated[AppVariant, IOAttrs('va')]
+ architecture = Annotated[AppArchitecture, IOAttrs('a')]
+ os_version = Annotated[str | None, IOAttrs('o')]
+
+ interface_idiom: Annotated[AppInterfaceIdiom, IOAttrs('i')]
+ locale: Annotated[str, IOAttrs('l')]
+
+ device: Annotated[str | None, IOAttrs('d')]
diff --git a/tools/batools/dummymodule.py b/tools/batools/dummymodule.py
index 02bd6746..7fbf618e 100755
--- a/tools/batools/dummymodule.py
+++ b/tools/batools/dummymodule.py
@@ -216,6 +216,12 @@ def _writefuncs(
'import bascenev1 # pylint: disable=cyclic-import\n'
'return bascenev1.Time(0.0)'
)
+ elif returns == 'bascenev1.HostInfo | None':
+ returnstr = (
+ 'import bascenev1 # pylint: disable=cyclic-import\n'
+ 'return bascenev1.HostInfo(\'dummyname\', -1,'
+ ' \'dummy_addr\', -1)'
+ )
elif returns == 'babase.DisplayTime':
returnstr = (
'import babase # pylint: disable=cyclic-import\n'
diff --git a/tools/batools/pcommands.py b/tools/batools/pcommands.py
index 3e5031f8..8fa3e42e 100644
--- a/tools/batools/pcommands.py
+++ b/tools/batools/pcommands.py
@@ -718,7 +718,6 @@ def logcat() -> None:
raise CleanError('Expected 2 args')
adb = sys.argv[2]
plat = sys.argv[3]
- print('plat is', plat)
# My amazon tablet chokes on the color format.
if plat == 'amazon':
diff --git a/tools/batools/project/_updater.py b/tools/batools/project/_updater.py
index db616708..5d820d92 100755
--- a/tools/batools/project/_updater.py
+++ b/tools/batools/project/_updater.py
@@ -426,7 +426,6 @@ class ProjectUpdater:
# from batools.xcode import update_xcode_project
for projpath in [
- # 'ballisticakit-mac.xcodeproj/project.pbxproj',
# 'ballisticakit-ios.xcodeproj/project.pbxproj',
'ballisticakit-xcode/BallisticaKit.xcodeproj/project.pbxproj',
]:
diff --git a/tools/efrotools/openalbuild.py b/tools/efrotools/openalbuild.py
index 7d00ed80..a53555b3 100644
--- a/tools/efrotools/openalbuild.py
+++ b/tools/efrotools/openalbuild.py
@@ -39,7 +39,7 @@ def build_openal(arch: str, mode: str) -> None:
if mode not in MODES:
raise CleanError(f"Invalid mode '{mode}'.")
- enable_oboe = True
+ # enable_oboe = True
# Get ndk path.
ndk_path = (
@@ -51,9 +51,8 @@ def build_openal(arch: str, mode: str) -> None:
.stdout.decode()
.strip()
)
- # os.environ['NDK_ROOT'] = ndk_path
- # Grab from git and build.
+ # Grab OpenALSoft
builddir = _build_dir(arch, mode)
subprocess.run(['rm', '-rf', builddir], check=True)
subprocess.run(['mkdir', '-p', os.path.dirname(builddir)], check=True)
@@ -61,27 +60,27 @@ def build_openal(arch: str, mode: str) -> None:
['git', 'clone', 'https://github.com/kcat/openal-soft.git', builddir],
check=True,
)
- subprocess.run(['git', 'checkout', '1.23.1'], check=True, cwd=builddir)
+ # subprocess.run(['git', 'checkout', '1.23.1'], check=True, cwd=builddir)
+ subprocess.run(
+ ['git', 'checkout', 'b81a270f6c1e795ca70d7684e0ccf35a19f247e2'],
+ check=True,
+ cwd=builddir,
+ )
- if enable_oboe:
- builddir_oboe = f'{builddir}_oboe'
- subprocess.run(['rm', '-rf', builddir_oboe], check=True)
- subprocess.run(
- ['mkdir', '-p', os.path.dirname(builddir_oboe)], check=True
- )
- subprocess.run(
- [
- 'git',
- 'clone',
- 'https://github.com/google/oboe',
- builddir_oboe,
- ],
- check=True,
- )
- subprocess.run(
- ['git', 'checkout', '1.8.0'], check=True, cwd=builddir_oboe
- )
- print(f'FULLY GOT {builddir_oboe}')
+ # Grab Oboe
+ builddir_oboe = f'{builddir}_oboe'
+ subprocess.run(['rm', '-rf', builddir_oboe], check=True)
+ subprocess.run(['mkdir', '-p', os.path.dirname(builddir_oboe)], check=True)
+ subprocess.run(
+ [
+ 'git',
+ 'clone',
+ 'https://github.com/google/oboe',
+ builddir_oboe,
+ ],
+ check=True,
+ )
+ subprocess.run(['git', 'checkout', '1.8.0'], check=True, cwd=builddir_oboe)
# One bit of filtering: by default, openalsoft sends all sorts of
# log messages to the android log. This is reasonable since its
@@ -106,6 +105,67 @@ def build_openal(arch: str, mode: str) -> None:
with open(loggingpath, 'w', encoding='utf-8') as outfile:
outfile.write(txt)
+ # Add a function to set a logging function so we can gather info
+ # on AL fatal errors/etc.
+ # fpath = f'{builddir}/alc/alc.cpp'
+ # with open(fpath, encoding='utf-8') as infile:
+ # txt = infile.read()
+ # txt = replace_exact(
+ # txt,
+ # 'ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device)\n',
+ # (
+ # 'void (*alcDebugLogger)(const char*) = nullptr;\n'
+ # '\n'
+ # 'ALC_API void ALC_APIENTRY'
+ # ' alcSetDebugLogger(void (*fn)(const char*)) {\n'
+ # ' alcDebugLogger = fn;\n'
+ # '}\n'
+ # '\n'
+ # 'ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device)\n'
+ # ),
+ # )
+ # with open(fpath, 'w', encoding='utf-8') as outfile:
+ # outfile.write(txt)
+
+ # fpath = f'{builddir}/include/AL/alc.h'
+ # with open(fpath, encoding='utf-8') as infile:
+ # txt = infile.read()
+ # txt = replace_exact(
+ # txt,
+ # 'ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device);\n',
+ # 'ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device);\n'
+ # 'ALC_API void ALC_APIENTRY alcSetDebugLogger('
+ # 'void (*fn)(const char*));\n',
+ # )
+ # with open(fpath, 'w', encoding='utf-8') as outfile:
+ # outfile.write(txt)
+
+ fpath = f'{builddir}/core/except.h'
+ with open(fpath, encoding='utf-8') as infile:
+ txt = infile.read()
+ txt = replace_exact(
+ txt,
+ '#define END_API_FUNC catch(...) { std::terminate(); }\n',
+ '#define END_API_FUNC\n',
+ )
+ txt = replace_exact(
+ txt, '#define START_API_FUNC try\n', '#define START_API_FUNC\n'
+ )
+ # txt = replace_exact(
+ # txt,
+ # '#define END_API_FUNC catch(...) { std::terminate(); }\n',
+ # 'extern void (*alcDebugLogger)(const char*);\n'
+ # '\n'
+ # '#define END_API_FUNC catch(...) { \\\n'
+ # ' if (alcDebugLogger != nullptr) { \\\n'
+ # ' alcDebugLogger("UNKNOWN OpenALSoft fatal exception."); \\\n'
+ # ' } \\\n'
+ # ' std::terminate(); \\\n'
+ # '}\n'
+ # )
+ with open(fpath, 'w', encoding='utf-8') as outfile:
+ outfile.write(txt)
+
android_platform = 23
subprocess.run(