Merge branch 'efroemling:master' into master

This commit is contained in:
Vishal 2023-12-15 16:37:19 +05:30 committed by GitHub
commit add619b080
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
162 changed files with 2780 additions and 1285 deletions

108
.efrocachemap generated
View File

@ -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"
}

178
.github/workflows/cd.yml vendored Normal file
View File

@ -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

View File

@ -21,7 +21,6 @@
<excludeFolder url="file://$MODULE_DIR$/ballisticakit-android" />
<excludeFolder url="file://$MODULE_DIR$/ballisticakit-cmake" />
<excludeFolder url="file://$MODULE_DIR$/ballisticakit-ios.xcodeproj" />
<excludeFolder url="file://$MODULE_DIR$/ballisticakit-mac.xcodeproj" />
<excludeFolder url="file://$MODULE_DIR$/ballisticakit-windows" />
<excludeFolder url="file://$MODULE_DIR$/ballisticakit-windows-oculus" />
<excludeFolder url="file://$MODULE_DIR$/ballisticakit-xcode" />

View File

@ -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)

View File

@ -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/badge.svg)](https://github.com/efroemling/ballistica/actions/workflows/ci.yml)
[![](https://github.com/efroemling/ballistica/actions/workflows/ci.yml/badge.svg)](https://github.com/efroemling/ballistica/actions/workflows/ci.yml) [![](https://github.com/efroemling/ballistica/actions/workflows/cd.yml/badge.svg)](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.

View File

@ -14,7 +14,6 @@
<file path="$PROJECT_DIR$/../ballisticakit-android" />
<file path="$PROJECT_DIR$/.idea" />
<file path="$PROJECT_DIR$/../ballisticakit-ios.xcodeproj" />
<file path="$PROJECT_DIR$/../ballisticakit-mac.xcodeproj" />
<file path="$PROJECT_DIR$/../ballisticakit-windows" />
<file path="$PROJECT_DIR$/../ballisticakit-xcode" />
<file path="$PROJECT_DIR$/../build" />

View File

@ -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',

View File

@ -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",

View File

@ -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 \

View File

@ -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',

View File

@ -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(

View File

@ -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.

View File

@ -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

View File

@ -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')

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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.
)

View File

@ -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.
)

View File

@ -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

View File

@ -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()

View File

@ -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',

View File

@ -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

View File

@ -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')

View File

@ -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.

View File

@ -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',

View File

@ -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,
)

View File

@ -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)"""

View File

@ -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()

View File

@ -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:

View File

@ -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:

View File

@ -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,

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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'

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -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,
)

View File

@ -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'

View File

@ -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
)

View File

@ -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,
)

View File

@ -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:

View File

@ -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

View File

@ -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(

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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,
)

View File

@ -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(

View File

@ -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,
)

View File

@ -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:

View File

@ -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.

View File

@ -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

View File

@ -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(
{

View File

@ -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,
)

View File

@ -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:

View File

@ -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:

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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:

View File

@ -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,
)

View File

@ -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')

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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
)

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -66,7 +66,6 @@ class AppAdapterApple : public AppAdapter {
private:
class ScopedAllowGraphics_;
// void UpdateScreenSizes_();
void ReloadRenderer_(const GraphicsSettings* settings);
std::thread::id graphics_thread_{};

View File

@ -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;
}

View File

@ -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() {}

View File

@ -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();

View File

@ -17,7 +17,8 @@
#include <alc.h>
#endif
#if BA_OSTYPE_ANDROID
#if BA_OPENAL_IS_SOFT
#define AL_ALEXT_PROTOTYPES
#include <alext.h>
#endif

View File

@ -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()); }

View File

@ -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();

View File

@ -4,6 +4,7 @@
#include <algorithm>
#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<AudioSource> client_source_;
float fade_{1.0f};
float gain_{1.0f};
AudioServer* audio_thread_{};
bool valid_{};
const Object::Ref<SoundAsset>* 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<AudioSource> client_source_;
AudioServer* audio_server_{};
const Object::Ref<SoundAsset>* source_sound_{};
#if BA_ENABLE_AUDIO
ALuint source_{};
Object::Ref<AudioStreamer> 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<int>(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<LPALCDEVICEPAUSESOFT>(
alcGetProcAddress(device, "alcDevicePauseSOFT"));
BA_PRECONDITION_FATAL(alcDevicePauseSOFT != nullptr);
alcDeviceResumeSOFT = reinterpret_cast<LPALCDEVICERESUMESOFT>(
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<LPALCDEVICEPAUSESOFT>(
alcGetProcAddress(device, "alcDevicePauseSOFT"));
BA_PRECONDITION_FATAL(alcDevicePauseSOFT != nullptr);
alcDeviceResumeSOFT = reinterpret_cast<LPALCDEVICERESUMESOFT>(
alcGetProcAddress(device, "alcDeviceResumeSOFT"));
BA_PRECONDITION_FATAL(alcDeviceResumeSOFT != nullptr);
alcResetDeviceSOFT = reinterpret_cast<LPALCRESETDEVICESOFT>(
alcGetProcAddress(device, "alcResetDeviceSOFT"));
BA_PRECONDITION_FATAL(alcResetDeviceSOFT != nullptr);
alEventCallbackSOFT = reinterpret_cast<LPALEVENTCALLBACKSOFT>(
alcGetProcAddress(device, "alEventCallbackSOFT"));
BA_PRECONDITION_FATAL(alEventCallbackSOFT != nullptr);
alEventControlSOFT = reinterpret_cast<LPALEVENTCONTROLSOFT>(
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<Object::Ref<Asset>*>& components) {
event_loop()->PushCall([components] {

View File

@ -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<const Object::Ref<SoundAsset>*> sound_ref_delete_list_;
int al_source_count_{};
};
} // namespace ballistica::base

View File

@ -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<EventLoop*> 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<int>(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

View File

@ -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<std::string>;
/// 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

View File

@ -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();
}
}

View File

@ -56,8 +56,8 @@ class Graphics {
Graphics();
void OnAppStart();
void OnAppPause();
void OnAppResume();
void OnAppSuspend();
void OnAppUnsuspend();
void OnAppShutdown();
void OnAppShutdownComplete();
void OnScreenSizeChange();

View File

@ -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;
}

View File

@ -87,7 +87,7 @@ class TextGroup : public Object {
Object::Ref<TextureAsset> os_texture_;
std::vector<std::unique_ptr<TextMeshEntry>> entries_;
std::string text_;
bool big_;
bool big_{};
};
} // namespace ballistica::base

View File

@ -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); });
}

View File

@ -21,8 +21,8 @@ class Input {
Input();
void OnAppStart();
void OnAppPause();
void OnAppResume();
void OnAppSuspend();
void OnAppUnsuspend();
void OnAppShutdown();
void OnAppShutdownComplete();
void StepDisplayTime();

View File

@ -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<JoystickInput>(
-1, // not an sdl joystick

View File

@ -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() {

View File

@ -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();

View File

@ -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_);

Some files were not shown because too many files have changed in this diff Show More