diff --git a/.efrocachemap b/.efrocachemap index b656e348..652de54a 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -421,10 +421,10 @@ "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": "f200cdf431b9494d8b96cdd47e950dd1", + "build/assets/ba_data/data/langdata.json": "7508c882bba2bbf6350cf1345c8c1347", "build/assets/ba_data/data/languages/arabic.json": "00ba700de6c672a56658a6bd1ad27523", - "build/assets/ba_data/data/languages/belarussian.json": "40883823367f04c5a2403a96525bfcc3", - "build/assets/ba_data/data/languages/chinese.json": "5761468d25f2bd4e79921826cebd572b", + "build/assets/ba_data/data/languages/belarussian.json": "5602b4e83ab78af37097a29593dff3d5", + "build/assets/ba_data/data/languages/chinese.json": "ff9a595726f0aff42a39be576d0ff037", "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": "cd21ad8c6b8e9ed700284cf1e1aecbf8", @@ -432,31 +432,31 @@ "build/assets/ba_data/data/languages/dutch.json": "22b44a33bf81142ba2befad14eb5746e", "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": "6f4051ce78861a4666f4978d6f9a0ed2", - "build/assets/ba_data/data/languages/french.json": "49ff6d211537b8003b8241438dca661d", + "build/assets/ba_data/data/languages/filipino.json": "e750fb1a95e4c5611115f9ece9ecab53", + "build/assets/ba_data/data/languages/french.json": "163362f7b33866ef069cae62d0387551", "build/assets/ba_data/data/languages/german.json": "450fa41ae264f29a5d1af22143d0d0ad", "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", "build/assets/ba_data/data/languages/indonesian.json": "408fb026e84c24a8dd7a43cb2b794541", - "build/assets/ba_data/data/languages/italian.json": "f2f5641bc924dfba37af2aac03e469e0", - "build/assets/ba_data/data/languages/korean.json": "ae179765711bead59d0de7fb15fac72f", + "build/assets/ba_data/data/languages/italian.json": "1824b50a9154594e7039883f6d35504f", + "build/assets/ba_data/data/languages/korean.json": "03fd99d5e1155e81053fc028f69df982", "build/assets/ba_data/data/languages/malay.json": "832562ce997fc70704b9234c95fb2e38", - "build/assets/ba_data/data/languages/persian.json": "d742f4a6d3c3555031102b21abdcbb5b", + "build/assets/ba_data/data/languages/persian.json": "d9de4a0b17331434859e0a6a9ae160c5", "build/assets/ba_data/data/languages/polish.json": "b9a58b70ed5e99d8b7fa2392b2eb0cda", - "build/assets/ba_data/data/languages/portuguese.json": "e3adc6c04486d21e84019a0b03ce11b1", + "build/assets/ba_data/data/languages/portuguese.json": "bc3b97a8213427ebbd1cfa43b4c5e6b6", "build/assets/ba_data/data/languages/romanian.json": "aeebdd54f65939c2facc6ac50c117826", - "build/assets/ba_data/data/languages/russian.json": "e120993371f52edd2d99f2236188933c", + "build/assets/ba_data/data/languages/russian.json": "e93713be12870dd0a01e2d1359d7c792", "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": "1d14210b4eefb48130608bd0495b7900", + "build/assets/ba_data/data/languages/spanish.json": "b4a51de7298ca2273bae804a98b3804d", "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", - "build/assets/ba_data/data/languages/turkish.json": "b1e491b70cec59cfa1057b8c5e7801b2", + "build/assets/ba_data/data/languages/turkish.json": "ab149ebbd57cf4daa3cf8f310d91519a", "build/assets/ba_data/data/languages/ukrainian.json": "e5c861187c4c6db37d1a033f4ef3dd5a", - "build/assets/ba_data/data/languages/venetian.json": "9fe1a58d9e5dfb00f31ce3b2eb9993f4", + "build/assets/ba_data/data/languages/venetian.json": "a559a5608d2e0b4708f7a4dee42ff354", "build/assets/ba_data/data/languages/vietnamese.json": "921cd1e50f60fe3e101f246e172750ba", "build/assets/ba_data/data/maps/big_g.json": "1dd301d490643088a435ce75df971054", "build/assets/ba_data/data/maps/bridgit.json": "6aea74805f4880cc11237c5734a24422", @@ -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": "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", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "4d6df65a5417a0eb23f4838b26054ce6", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "d4c5bd03eda639f73b769664fae9691b", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "8837eed81869a6d9404afc890a99aeda", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "906ca39550dc91235ebdf1c217b3116b", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "e54895522cdd27f020b66260acdcc42d", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "a293a3b7af38f265c995beba6027be93", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "380a18b86065840c44285dc8762f42c4", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "2dc321ce47a02190e51c2f6f4f807449", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "117f585f14bb816ed0ec5bac792077c0", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "124b14dbca819bfc300ec6c521641f78", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "4df0f5f8183b325f5be25f2f9d5fa17e", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "023558d9028ba3250384f9d7d9ef9a17", + "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "4990e055b064408ede929a4c57253344", + "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "439574bda6d8d320ce38968886e83181", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "d62da566c946136a746d55ec04c28020", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "def8d189ca344df21b0a5bb9619fef12", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "b75406a2f66d2cb74359b78356f89710", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "cc1dccb6c5e63acf7bc42ee4346df9c6", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "27028549259c2f3843d54c3d29fad470", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "656d50ed1514203e967dcf90a594fe66", + "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "db535f0ca1e01af825f75f204fbc8928", + "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "97d51afca996ae15b61fd9f409a00459", + "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "db535f0ca1e01af825f75f204fbc8928", + "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "97d51afca996ae15b61fd9f409a00459", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "43794f4973b09588367261faf46b652a", + "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "9bc71c9874596dd708841d84dff69b55", + "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "43794f4973b09588367261faf46b652a", + "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "9bc71c9874596dd708841d84dff69b55", + "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "87f6bf3ea1196d91cd6486785702a5b2", + "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "60336f9d664825ca08236dda311276ca", + "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "87f6bf3ea1196d91cd6486785702a5b2", + "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "60336f9d664825ca08236dda311276ca", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "4399c87b58fbe58fa67096cfa878f86a", + "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "452623f0495dd4375e5b5d9b80d643d5", + "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "ca49b32ed573feea11613d62cd89840c", + "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "452623f0495dd4375e5b5d9b80d643d5", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "dfb0839e79411869b196bc48565a36bc", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "e2beda37ec0d3efde860d0c2e248b9e0", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "2adae2522084e571c51595c7d31ac1dc", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "4ef71e752bfb318db84e88dfdd51d955", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "a123d9ecd0d53d9ad2f13abdf9842ade", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "6afeb02344f2faf2979188e54c6d2dc4", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "abedb44a923a4845fdc350fd979a1dc8", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "c728bfecc990701ce831644fb43520f1", "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", diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ff9ebd..f5010a82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ -### 1.7.31 (build 21707, api 8, 2023-12-13) +### 1.7.32 (build 21728, api 8, 2023-12-17) + +### 1.7.31 (build 21727, api 8, 2023-12-17) - 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!). +- Native stack traces now work on Android; woohoo! Should be very helpful for + debugging. +- Added the concept of 'ui-operations' in the native layer to hopefully clear + out the remaining double-window bugs. Basically, widgets used to schedule + their payload commands to a future cycle of the event loop, meaning it was + possible for commands that switched the main window to get scheduled twice + before the first one ran (due to 2 key presses, etc), which could lead to all + sorts of weirdness happening such as multiple windows popping up when one was + intended. Now, however, such commands get scheduled to a current + 'ui-operation' and then run *almost* immediately, which should prevent such + situations. Please holler if you run into any UI weirdness at this point. ### 1.7.30 (build 21697, api 8, 2023-12-08) - Continued work on the big 1.7.28 update. diff --git a/ballisticakit-cmake/CMakeLists.txt b/ballisticakit-cmake/CMakeLists.txt index 12934e4e..ab6575ab 100644 --- a/ballisticakit-cmake/CMakeLists.txt +++ b/ballisticakit-cmake/CMakeLists.txt @@ -697,6 +697,7 @@ set(BALLISTICA_SOURCES ${BA_SRC_ROOT}/ballistica/shared/generic/json.cc ${BA_SRC_ROOT}/ballistica/shared/generic/json.h ${BA_SRC_ROOT}/ballistica/shared/generic/lambda_runnable.h + ${BA_SRC_ROOT}/ballistica/shared/generic/native_stack_trace.h ${BA_SRC_ROOT}/ballistica/shared/generic/runnable.cc ${BA_SRC_ROOT}/ballistica/shared/generic/runnable.h ${BA_SRC_ROOT}/ballistica/shared/generic/snapshot.h diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj index ff64f440..97c94baf 100644 --- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj @@ -689,6 +689,7 @@ + diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters index 441851e6..92b1c44d 100644 --- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters @@ -1501,6 +1501,9 @@ ballistica\shared\generic + + ballistica\shared\generic + ballistica\shared\generic diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj index c3b0779c..6434233d 100644 --- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj @@ -684,6 +684,7 @@ + diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters index 441851e6..92b1c44d 100644 --- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters @@ -1501,6 +1501,9 @@ ballistica\shared\generic + + ballistica\shared\generic + ballistica\shared\generic diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py index 91b05f35..e1b7a32c 100644 --- a/src/assets/ba_data/python/babase/__init__.py +++ b/src/assets/ba_data/python/babase/__init__.py @@ -61,7 +61,6 @@ from _babase import ( in_logic_thread, increment_analytics_count, is_os_playing_music, - is_running_on_fire_tv, is_xcode_build, lock_all_input, mac_music_app_get_playlists, @@ -77,6 +76,7 @@ from _babase import ( native_review_request, native_review_request_supported, native_stack_trace, + open_file_externally, print_load_info, pushcall, quit, @@ -258,7 +258,6 @@ __all__ = [ 'is_browser_likely_available', 'is_os_playing_music', 'is_point_in_box', - 'is_running_on_fire_tv', 'is_xcode_build', 'LanguageSubsystem', 'lock_all_input', @@ -283,6 +282,7 @@ __all__ = [ 'NodeNotFoundError', 'normalized_color', 'NotFoundError', + 'open_file_externally', 'Permission', 'PlayerNotFoundError', 'Plugin', diff --git a/src/assets/ba_data/python/baclassic/_ads.py b/src/assets/ba_data/python/baclassic/_ads.py index fdb34de4..2373df36 100644 --- a/src/assets/ba_data/python/baclassic/_ads.py +++ b/src/assets/ba_data/python/baclassic/_ads.py @@ -9,7 +9,6 @@ import logging from typing import TYPE_CHECKING import babase -import bauiv1 import bascenev1 if TYPE_CHECKING: @@ -72,7 +71,8 @@ class AdsSubsystem: ) -> None: """(internal)""" self.last_ad_purpose = purpose - bauiv1.show_ad(purpose, on_completion_call) + assert babase.app.plus is not None + babase.app.plus.show_ad(purpose, on_completion_call) def show_ad_2( self, @@ -81,7 +81,8 @@ class AdsSubsystem: ) -> None: """(internal)""" self.last_ad_purpose = purpose - bauiv1.show_ad_2(purpose, on_completion_call) + assert babase.app.plus is not None + babase.app.plus.show_ad_2(purpose, on_completion_call) def call_after_ad(self, call: Callable[[], Any]) -> None: """Run a call after potentially showing an ad.""" diff --git a/src/assets/ba_data/python/baclassic/_input.py b/src/assets/ba_data/python/baclassic/_input.py index 6d60b50a..5c934c32 100644 --- a/src/assets/ba_data/python/baclassic/_input.py +++ b/src/assets/ba_data/python/baclassic/_input.py @@ -20,7 +20,6 @@ def get_input_device_mapped_value( This checks the user config and falls back to default values where available. """ - # pylint: disable=too-many-statements # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches @@ -83,91 +82,6 @@ def get_input_device_mapped_value( 'triggerRun1': 5, }.get(name, -1) - # Look for some exact types. - if babase.is_running_on_fire_tv(): - if devicename in ['Thunder', 'Amazon Fire Game Controller']: - return { - 'triggerRun2': 23, - 'unassignedButtonsRun': False, - 'buttonPickUp': 101, - 'buttonBomb': 98, - 'buttonJump': 97, - 'analogStickDeadZone': 0.0, - 'startButtonActivatesDefaultWidget': False, - 'buttonStart': 83, - 'buttonPunch': 100, - 'buttonRun2': 103, - 'buttonRun1': 104, - 'triggerRun1': 24, - }.get(name, -1) - if devicename == 'NYKO PLAYPAD PRO': - return { - 'triggerRun2': 23, - 'triggerRun1': 24, - 'buttonPickUp': 101, - 'buttonBomb': 98, - 'buttonJump': 97, - 'buttonUp': 20, - 'buttonLeft': 22, - 'buttonRight': 23, - 'buttonStart': 83, - 'buttonPunch': 100, - 'buttonDown': 21, - }.get(name, -1) - if devicename == 'Logitech Dual Action': - return { - 'triggerRun2': 23, - 'triggerRun1': 24, - 'buttonPickUp': 98, - 'buttonBomb': 101, - 'buttonJump': 100, - 'buttonStart': 109, - 'buttonPunch': 97, - }.get(name, -1) - if devicename == 'Xbox 360 Wireless Receiver': - return { - 'triggerRun2': 23, - 'triggerRun1': 24, - 'buttonPickUp': 101, - 'buttonBomb': 98, - 'buttonJump': 97, - 'buttonUp': 20, - 'buttonLeft': 22, - 'buttonRight': 23, - 'buttonStart': 83, - 'buttonPunch': 100, - 'buttonDown': 21, - }.get(name, -1) - if devicename == 'Microsoft X-Box 360 pad': - return { - 'triggerRun2': 23, - 'triggerRun1': 24, - 'buttonPickUp': 101, - 'buttonBomb': 98, - 'buttonJump': 97, - 'buttonStart': 83, - 'buttonPunch': 100, - }.get(name, -1) - if devicename in [ - 'Amazon Remote', - 'Amazon Bluetooth Dev', - 'Amazon Fire TV Remote', - ]: - return { - 'triggerRun2': 23, - 'triggerRun1': 24, - 'buttonPickUp': 24, - 'buttonBomb': 91, - 'buttonJump': 86, - 'buttonUp': 20, - 'buttonLeft': 22, - 'startButtonActivatesDefaultWidget': False, - 'buttonRight': 23, - 'buttonStart': 83, - 'buttonPunch': 90, - 'buttonDown': 21, - }.get(name, -1) - elif 'NVIDIA SHIELD;' in useragentstring: if 'NVIDIA Controller' in devicename: return { @@ -182,112 +96,6 @@ def get_input_device_mapped_value( 'buttonIgnored': 184, 'buttonIgnored2': 86, }.get(name, -1) - elif platform == 'mac': - if devicename == 'PLAYSTATION(R)3 Controller': - return { - 'buttonLeft': 8, - 'buttonUp': 5, - 'buttonRight': 6, - 'buttonDown': 7, - 'buttonJump': 15, - 'buttonPunch': 16, - 'buttonBomb': 14, - 'buttonPickUp': 13, - 'buttonStart': 4, - 'buttonIgnored': 17, - }.get(name, -1) - if devicename in ['Wireless 360 Controller', 'Controller']: - # Xbox360 gamepads - return { - 'analogStickDeadZone': 1.2, - 'buttonBomb': 13, - 'buttonDown': 2, - 'buttonJump': 12, - 'buttonLeft': 3, - 'buttonPickUp': 15, - 'buttonPunch': 14, - 'buttonRight': 4, - 'buttonStart': 5, - 'buttonUp': 1, - 'triggerRun1': 5, - 'triggerRun2': 6, - 'buttonIgnored': 11, - }.get(name, -1) - if devicename in [ - 'Logitech Dual Action', - 'Logitech Cordless RumblePad 2', - ]: - return { - 'buttonJump': 2, - 'buttonPunch': 1, - 'buttonBomb': 3, - 'buttonPickUp': 4, - 'buttonStart': 10, - }.get(name, -1) - - # Old gravis gamepad. - if devicename == 'GamePad Pro USB ': - return { - 'buttonJump': 2, - 'buttonPunch': 1, - 'buttonBomb': 3, - 'buttonPickUp': 4, - 'buttonStart': 10, - }.get(name, -1) - - if devicename == 'Microsoft SideWinder Plug & Play Game Pad': - return { - 'buttonJump': 1, - 'buttonPunch': 3, - 'buttonBomb': 2, - 'buttonPickUp': 4, - 'buttonStart': 6, - }.get(name, -1) - - # Saitek P2500 Rumble Force Pad.. (hopefully works for others too?..) - if devicename == 'Saitek P2500 Rumble Force Pad': - return { - 'buttonJump': 3, - 'buttonPunch': 1, - 'buttonBomb': 4, - 'buttonPickUp': 2, - 'buttonStart': 11, - }.get(name, -1) - - # Some crazy 'Senze' dual gamepad. - if devicename == 'Twin USB Joystick': - return { - 'analogStickLR': 3, - 'analogStickLR_B': 7, - 'analogStickUD': 4, - 'analogStickUD_B': 8, - 'buttonBomb': 2, - 'buttonBomb_B': 14, - 'buttonJump': 3, - 'buttonJump_B': 15, - 'buttonPickUp': 1, - 'buttonPickUp_B': 13, - 'buttonPunch': 4, - 'buttonPunch_B': 16, - 'buttonRun1': 7, - 'buttonRun1_B': 19, - 'buttonRun2': 8, - 'buttonRun2_B': 20, - 'buttonStart': 10, - 'buttonStart_B': 22, - 'enableSecondary': 1, - 'unassignedButtonsRun': False, - }.get(name, -1) - if devicename == 'USB Gamepad ': # some weird 'JITE' gamepad - return { - 'analogStickLR': 4, - 'analogStickUD': 5, - 'buttonJump': 3, - 'buttonPunch': 4, - 'buttonBomb': 2, - 'buttonPickUp': 1, - 'buttonStart': 10, - }.get(name, -1) default_android_mapping = { 'triggerRun2': 19, @@ -310,6 +118,41 @@ def get_input_device_mapped_value( # Generic android... if platform == 'android': + if devicename in ['Amazon Fire Game Controller']: + return { + 'triggerRun2': 23, + 'unassignedButtonsRun': False, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'analogStickDeadZone': 0.0, + 'startButtonActivatesDefaultWidget': False, + 'buttonStart': 83, + 'buttonPunch': 100, + 'buttonRun2': 103, + 'buttonRun1': 104, + 'triggerRun1': 24, + }.get(name, -1) + if devicename in [ + 'Amazon Remote', + 'Amazon Bluetooth Dev', + 'Amazon Fire TV Remote', + ]: + return { + 'triggerRun2': 23, + 'triggerRun1': 24, + 'buttonPickUp': 24, + 'buttonBomb': 91, + 'buttonJump': 86, + 'buttonUp': 20, + 'buttonLeft': 22, + 'startButtonActivatesDefaultWidget': False, + 'buttonRight': 23, + 'buttonStart': 83, + 'buttonPunch': 90, + 'buttonDown': 21, + }.get(name, -1) + # Steelseries stratus xl. if devicename == 'SteelSeries Stratus XL': return { @@ -387,14 +230,6 @@ def get_input_device_mapped_value( 'uiOnly': True, }.get(name, -1) - # flag particular gamepads to use exact android defaults.. - # (so they don't even ask to configure themselves) - if devicename in [ - 'Samsung Game Pad EI-GP20', - 'ASUS Gamepad', - ] or devicename.startswith('Freefly VR Glide'): - return default_android_mapping.get(name, -1) - # Nvidia controller is default, but gets some strange # keypresses we want to ignore.. touching the touchpad, # so lets ignore those. @@ -452,76 +287,11 @@ def get_input_device_mapped_value( 'buttonRight': 100, }.get(name, -1) - # Ok, this gamepad's not in our specific preset list; - # fall back to some (hopefully) reasonable defaults. - - # Leaving these in here for now but not gonna add any more now that we have - # fancy-pants config sharing across the internet. - if platform == 'mac': - if 'PLAYSTATION' in devicename: # ps3 gamepad?.. - return { - 'buttonLeft': 8, - 'buttonUp': 5, - 'buttonRight': 6, - 'buttonDown': 7, - 'buttonJump': 15, - 'buttonPunch': 16, - 'buttonBomb': 14, - 'buttonPickUp': 13, - 'buttonStart': 4, - }.get(name, -1) - - # Dual Action Config - hopefully applies to more... - if 'Logitech' in devicename: - return { - 'buttonJump': 2, - 'buttonPunch': 1, - 'buttonBomb': 3, - 'buttonPickUp': 4, - 'buttonStart': 10, - }.get(name, -1) - - # Saitek P2500 Rumble Force Pad.. (hopefully works for others too?..) - if 'Saitek' in devicename: - return { - 'buttonJump': 3, - 'buttonPunch': 1, - 'buttonBomb': 4, - 'buttonPickUp': 2, - 'buttonStart': 11, - }.get(name, -1) - - # Gravis stuff?... - if 'GamePad' in devicename: - return { - 'buttonJump': 2, - 'buttonPunch': 1, - 'buttonBomb': 3, - 'buttonPickUp': 4, - 'buttonStart': 10, - }.get(name, -1) + # Ok, this gamepad's not in our specific preset list; fall back to + # some (hopefully) reasonable defaults. # Reasonable defaults. if platform == 'android': - if babase.is_running_on_fire_tv(): - # Mostly same as default firetv controller. - return { - 'triggerRun2': 23, - 'triggerRun1': 24, - 'buttonPickUp': 101, - 'buttonBomb': 98, - 'buttonJump': 97, - 'buttonStart': 83, - 'buttonPunch': 100, - 'buttonDown': 21, - 'buttonUp': 20, - 'buttonLeft': 22, - 'buttonRight': 23, - 'startButtonActivatesDefaultWidget': False, - }.get(name, -1) - - # Mostly same as 'Gamepad' except with 'menu' for default start - # button instead of 'mode'. return default_android_mapping.get(name, -1) # Is there a point to any sort of fallbacks here?.. should check. @@ -540,9 +310,9 @@ def _gen_android_input_hash() -> str: md5 = hashlib.md5() - # Currently we just do a single hash of *all* inputs on android - # and that's it.. good enough. - # (grabbing mappings for a specific device looks to be non-trivial) + # Currently we just do a single hash of *all* inputs on android and + # that's it. Good enough. (grabbing mappings for a specific device + # looks to be non-trivial) for dirname in [ '/system/usr/keylayout', '/data/usr/keylayout', @@ -551,9 +321,9 @@ def _gen_android_input_hash() -> str: try: if os.path.isdir(dirname): for f_name in os.listdir(dirname): - # This is usually volume keys and stuff; - # assume we can skip it?.. - # (since it'll vary a lot across devices) + # This is usually volume keys and stuff; assume we + # can skip it?.. (since it'll vary a lot across + # devices) if f_name == 'gpio-keys.kl': continue try: @@ -576,8 +346,8 @@ def get_input_device_map_hash() -> str: """ app = babase.app - # Currently only using this when classic is present. - # Need to replace with a modern equivalent. + # Currently only using this when classic is present. Need to replace + # with a modern equivalent. if app.classic is not None: try: if app.classic.input_map_hash is None: diff --git a/src/assets/ba_data/python/baclassic/_subsystem.py b/src/assets/ba_data/python/baclassic/_subsystem.py index 6746251c..16299bdc 100644 --- a/src/assets/ba_data/python/baclassic/_subsystem.py +++ b/src/assets/ba_data/python/baclassic/_subsystem.py @@ -451,15 +451,6 @@ class ClassicSubsystem(babase.AppSubsystem): if playtype in val.get_play_types() ) - def show_online_score_ui( - self, - show: str = 'general', - game: str | None = None, - game_version: str | None = None, - ) -> None: - """(internal)""" - bauiv1.show_online_score_ui(show, game, game_version) - def game_begin_analytics(self) -> None: """(internal)""" from baclassic import _analytics diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index 35416b81..00089375 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -52,8 +52,8 @@ if TYPE_CHECKING: # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 21707 -TARGET_BALLISTICA_VERSION = '1.7.31' +TARGET_BALLISTICA_BUILD = 21728 +TARGET_BALLISTICA_VERSION = '1.7.32' @dataclass diff --git a/src/assets/ba_data/python/baplus/_subsystem.py b/src/assets/ba_data/python/baplus/_subsystem.py index 0b77c20f..d38ca31a 100644 --- a/src/assets/ba_data/python/baplus/_subsystem.py +++ b/src/assets/ba_data/python/baplus/_subsystem.py @@ -264,3 +264,26 @@ class PlusSubsystem(AppSubsystem): def can_show_ad() -> bool: """Can we show an ad?""" return _baplus.can_show_ad() + + @staticmethod + def show_ad( + purpose: str, on_completion_call: Callable[[], None] | None = None + ) -> None: + """Show an ad.""" + _baplus.show_ad(purpose, on_completion_call) + + @staticmethod + def show_ad_2( + purpose: str, on_completion_call: Callable[[bool], None] | None = None + ) -> None: + """Show an ad.""" + _baplus.show_ad_2(purpose, on_completion_call) + + @staticmethod + def show_game_service_ui( + show: str = 'general', + game: str | None = None, + game_version: str | None = None, + ) -> None: + """Show game-service provided UI.""" + _baplus.show_game_service_ui(show, game, game_version) diff --git a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py index 2aff578f..8b335a19 100644 --- a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py +++ b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py @@ -284,20 +284,20 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): self.end({'outcome': 'next_level'}) def _ui_gc(self) -> None: - if bs.app.classic is not None: - bs.app.classic.show_online_score_ui( + if bs.app.plus is not None: + bs.app.plus.show_game_service_ui( 'leaderboard', game=self._game_name_str, game_version=self._game_config_str, ) else: - logging.warning('show_online_score_ui requires classic') + logging.warning('show_game_service_ui requires plus feature-set') def _ui_show_achievements(self) -> None: - if bs.app.classic is not None: - bs.app.classic.show_online_score_ui('achievements') + if bs.app.plus is not None: + bs.app.plus.show_game_service_ui('achievements') else: - logging.warning('show_online_score_ui requires classic') + logging.warning('show_game_service_ui requires plus feature-set') def _ui_worlds_best(self) -> None: if self._score_link is None: diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py index 5e738646..cd49fca8 100644 --- a/src/assets/ba_data/python/bauiv1/__init__.py +++ b/src/assets/ba_data/python/bauiv1/__init__.py @@ -60,7 +60,6 @@ from babase import ( in_logic_thread, increment_analytics_count, is_browser_likely_available, - is_running_on_fire_tv, is_xcode_build, lock_all_input, LoginAdapter, @@ -69,6 +68,7 @@ from babase import ( native_review_request, native_review_request_supported, NotFoundError, + open_file_externally, Permission, Plugin, PluginSpec, @@ -105,15 +105,11 @@ from _bauiv1 import ( imagewidget, is_party_icon_visible, Mesh, - open_file_externally, open_url, rowwidget, scrollwidget, set_party_icon_always_visible, set_party_window_open, - show_ad, - show_ad_2, - show_online_score_ui, Sound, Texture, textwidget, @@ -181,7 +177,6 @@ __all__ = [ 'increment_analytics_count', 'is_browser_likely_available', 'is_party_icon_visible', - 'is_running_on_fire_tv', 'is_xcode_build', 'Keyboard', 'lock_all_input', @@ -210,9 +205,6 @@ __all__ = [ 'set_party_icon_always_visible', 'set_party_window_open', 'set_ui_input_device', - 'show_ad', - 'show_ad_2', - 'show_online_score_ui', 'Sound', 'SpecialChar', 'supports_max_fps', diff --git a/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py index 425a78e6..e91972e2 100644 --- a/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py +++ b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py @@ -58,6 +58,7 @@ class OnScreenKeyboardWindow(Window): position=(30, self._height - 55), size=(60, 60), label='', + enable_sound=False, on_activate_call=self._cancel, autoselect=True, color=(0.55, 0.5, 0.6), diff --git a/src/assets/ba_data/python/bauiv1lib/account/settings.py b/src/assets/ba_data/python/bauiv1lib/account/settings.py index 63937ef6..ddbbad33 100644 --- a/src/assets/ba_data/python/bauiv1lib/account/settings.py +++ b/src/assets/ba_data/python/bauiv1lib/account/settings.py @@ -1258,19 +1258,21 @@ class AccountSettingsWindow(bui.Window): self._needs_refresh = False def _on_game_service_button_press(self) -> None: - if bui.app.classic is not None: - bui.app.classic.show_online_score_ui() + if bui.app.plus is not None: + bui.app.plus.show_game_service_ui() else: - logging.warning('game service ui not available without classic.') + logging.warning( + 'game-service-ui not available without plus feature-set.' + ) def _on_custom_achievements_press(self) -> None: - if bui.app.classic is not None: + if bui.app.plus is not None: bui.apptimer( 0.15, - bui.Call(bui.app.classic.show_online_score_ui, 'achievements'), + bui.Call(bui.app.plus.show_game_service_ui, 'achievements'), ) else: - logging.warning('show_online_score_ui requires classic') + logging.warning('show_game_service_ui requires plus feature-set.') def _on_achievements_press(self) -> None: # pylint: disable=cyclic-import @@ -1327,13 +1329,13 @@ class AccountSettingsWindow(bui.Window): bui.open_url(response.url) def _on_leaderboards_press(self) -> None: - if bui.app.classic is not None: + if bui.app.plus is not None: bui.apptimer( 0.15, - bui.Call(bui.app.classic.show_online_score_ui, 'leaderboards'), + bui.Call(bui.app.plus.show_game_service_ui, 'leaderboards'), ) else: - logging.warning('show_online_score_ui requires classic') + logging.warning('show_game_service_ui requires classic') def _have_unlinkable_v1_accounts(self) -> bool: plus = bui.app.plus diff --git a/src/assets/ba_data/python/bauiv1lib/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py index 16f318ae..4562a7fa 100644 --- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py +++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py @@ -1244,10 +1244,12 @@ class MainMenuWindow(bui.Window): def _do_game_service_press(self) -> None: self._save_state() - if bui.app.classic is not None: - bui.app.classic.show_online_score_ui() + if bui.app.plus is not None: + bui.app.plus.show_game_service_ui() else: - logging.warning('classic is required to show game service ui') + logging.warning( + 'plus feature-set is required to show game service ui' + ) def _save_state(self) -> None: # Don't do this for the in-game menu. diff --git a/src/ballistica/base/audio/al_sys.h b/src/ballistica/base/audio/al_sys.h index 53b5ef07..80427913 100644 --- a/src/ballistica/base/audio/al_sys.h +++ b/src/ballistica/base/audio/al_sys.h @@ -20,6 +20,13 @@ #if BA_OPENAL_IS_SOFT #define AL_ALEXT_PROTOTYPES #include +// Has not been formalized into an extension yet (from alc/inprogext.h" +// typedef void(ALC_APIENTRY* LPALSOFTLOGCALLBACK)(void* userptr, char level, +// const char* message, +// int length) noexcept; +// typedef void(ALC_APIENTRY* LPALSOFTSETLOGCALLBACK)(LPALSOFTLOGCALLBACK +// callback, +// void* userptr) noexcept; #endif #define CHECK_AL_ERROR _check_al_error(__FILE__, __LINE__) diff --git a/src/ballistica/base/audio/audio_server.cc b/src/ballistica/base/audio/audio_server.cc index 90e8e4ee..16994b03 100644 --- a/src/ballistica/base/audio/audio_server.cc +++ b/src/ballistica/base/audio/audio_server.cc @@ -4,6 +4,11 @@ #include +// Ew fixme. +#if BA_OSTYPE_ANDROID +#include +#endif + #include "ballistica/base/app_adapter/app_adapter.h" #include "ballistica/base/assets/assets.h" #include "ballistica/base/assets/sound_asset.h" @@ -34,6 +39,8 @@ LPALCDEVICERESUMESOFT alcDeviceResumeSOFT{}; LPALCRESETDEVICESOFT alcResetDeviceSOFT{}; LPALEVENTCALLBACKSOFT alEventCallbackSOFT{}; LPALEVENTCONTROLSOFT alEventControlSOFT{}; +// LPALSOFTSETLOGCALLBACK alsoft_set_log_callback{}; + #endif const int kAudioProcessIntervalNormal{500 * 1000}; @@ -168,8 +175,42 @@ static void ALEventCallback_(ALenum eventType, ALuint object, ALuint param, + std::to_string(static_cast(eventType))); } } + +// FIXME: Should convert this to a generalized OpenALSoft log handler since +// we might want to wire it up on other platforms too. +#if BA_OSTYPE_ANDROID +static void ALCustomAndroidLogCallback_(int severity, const char* msg) { + // Let's log everything directly that is a warning or worse and store + // everything else (up to some size limit). We can then explicitly ship + // the full log if a serious problem occurs. + if (severity >= ANDROID_LOG_WARN) { + __android_log_print(severity, "BallisticaKit", "openal-log: %s", msg); + } + g_base->audio_server->OpenALSoftLogCallback(msg); +} +#endif // BA_OSTYPE_ANDROID + +void ALCustomLogCallback_(void* userptr, char level, const char* message, + int length) noexcept { + // Log(LogLevel::kInfo, "HELLO FROM GENERIC CUSTOM LOGGER"); +} + #endif // BA_OPENAL_IS_SOFT +void AudioServer::OpenALSoftLogCallback(const std::string& msg) { + size_t log_cap{1024 * 11}; + std::scoped_lock lock(openalsoft_log_mutex_); + + if (openalsoft_log_.size() < log_cap) { + openalsoft_log_ += "openal-log(" + + std::to_string(g_core->GetAppTimeSeconds()) + + "s): " + msg; + if (openalsoft_log_.size() >= log_cap) { + openalsoft_log_ += "\n\n"; + } + } +} + void AudioServer::OnAppStartInThread_() { assert(g_base->InAudioThread()); @@ -219,14 +260,38 @@ void AudioServer::OnAppStartInThread_() { } #endif // BA_RIFT_BUILD + // Wire up our custom log callback where applicable. +#if BA_OSTYPE_ANDROID + // alsoft_set_log_callback(ALCustomLogCallback_, nullptr); + alcSetCustomAndroidLogger(ALCustomAndroidLogCallback_); +#endif + auto* device = alcOpenDevice(al_device_name); if (!device) { + if (g_buildconfig.ostype_android()) { + std::scoped_lock lock(openalsoft_log_mutex_); + Log(LogLevel::kError, + "------------------------" + " OPENALSOFT-FATAL-ERROR-LOG-BEGIN ----------------------\n" + + openalsoft_log_ + + "\n-------------------------" + " OPENALSOFT-FATAL-ERROR-LOG-END -----------------------"); + } FatalError( "No audio devices found. Do you have speakers/headphones/etc. " "connected?"); } impl_->alc_context = alcCreateContext(device, nullptr); if (!impl_->alc_context) { + if (g_buildconfig.ostype_android()) { + std::scoped_lock lock(openalsoft_log_mutex_); + Log(LogLevel::kError, + "------------------------" + " OPENALSOFT-FATAL-ERROR-LOG-BEGIN ----------------------\n" + + openalsoft_log_ + + "\n-------------------------" + " OPENALSOFT-FATAL-ERROR-LOG-END -----------------------"); + } FatalError( "Unable to init audio. Do you have speakers/headphones/etc. " "connected?"); @@ -253,6 +318,9 @@ void AudioServer::OnAppStartInThread_() { alEventControlSOFT = reinterpret_cast( alcGetProcAddress(device, "alEventControlSOFT")); BA_PRECONDITION_FATAL(alEventControlSOFT != nullptr); + // alsoft_set_log_callback = reinterpret_cast( + // alcGetProcAddress(device, "alsoft_set_log_callback")); + // BA_PRECONDITION_FATAL(alsoft_set_log_callback != nullptr); // Ask to be notified when a device is disconnected. alEventCallbackSOFT(ALEventCallback_, nullptr); diff --git a/src/ballistica/base/audio/audio_server.h b/src/ballistica/base/audio/audio_server.h index d9919c0a..8f132eab 100644 --- a/src/ballistica/base/audio/audio_server.h +++ b/src/ballistica/base/audio/audio_server.h @@ -29,9 +29,6 @@ class AudioServer { void PushSetVolumesCall(float music_volume, float sound_volume); void PushSetSoundPitchCall(float val); - // static void BeginInterruption(); - // static void EndInterruption(); - void PushSetListenerPositionCall(const Vector3f& p); void PushSetListenerOrientationCall(const Vector3f& forward, const Vector3f& up); @@ -68,6 +65,7 @@ class AudioServer { auto event_loop() const -> EventLoop* { return event_loop_; } void OnDeviceDisconnected(); + void OpenALSoftLogCallback(const std::string& msg); private: class ThreadSource_; @@ -132,6 +130,9 @@ class AudioServer { seconds_t last_started_playing_time_{}; millisecs_t last_sound_fade_process_time_{}; + std::mutex openalsoft_log_mutex_; + std::string openalsoft_log_; + /// Indexed list of sources. std::vector sources_; std::vector streaming_sources_; diff --git a/src/ballistica/base/python/methods/python_methods_misc.cc b/src/ballistica/base/python/methods/python_methods_misc.cc index caa96c5f..7b324b24 100644 --- a/src/ballistica/base/python/methods/python_methods_misc.cc +++ b/src/ballistica/base/python/methods/python_methods_misc.cc @@ -14,6 +14,7 @@ #include "ballistica/base/support/app_config.h" #include "ballistica/base/ui/dev_console.h" #include "ballistica/base/ui/ui.h" +#include "ballistica/shared/generic/native_stack_trace.h" #include "ballistica/shared/generic/utils.h" namespace ballistica::base { @@ -232,24 +233,6 @@ static PyMethodDef PyClipboardGetTextDef = { " this function.", }; -// ---------------------------- is_running_on_ouya ----------------------------- - -auto PyIsRunningOnOuya(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyIsRunningOnOuyaDef = { - "is_running_on_ouya", // name - PyIsRunningOnOuya, // method - METH_VARARGS, // flags - - "is_running_on_ouya() -> bool\n" - "\n" - "(internal)", -}; - // ------------------------------ setup_sigint --------------------------------- static auto PySetUpSigInt(PyObject* self) -> PyObject* { @@ -273,27 +256,6 @@ static PyMethodDef PySetUpSigIntDef = { "(internal)", }; -// -------------------------- is_running_on_fire_tv ---------------------------- - -static auto PyIsRunningOnFireTV(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - if (g_core->platform->IsRunningOnFireTV()) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyIsRunningOnFireTVDef = { - "is_running_on_fire_tv", // name - PyIsRunningOnFireTV, // method - METH_VARARGS, // flags - - "is_running_on_fire_tv() -> bool\n" - "\n" - "(internal)", -}; - // ---------------------------- have_permission -------------------------------- static auto PyHavePermission(PyObject* self, PyObject* args, PyObject* keywds) @@ -1369,7 +1331,7 @@ static PyMethodDef PyUnlockAllInputDef = { static auto PyNativeStackTrace(PyObject* self) -> PyObject* { BA_PYTHON_TRY; assert(g_core); - auto* trace = g_core->platform->GetStackTrace(); + auto* trace = g_core->platform->GetNativeStackTrace(); if (!trace) { Py_RETURN_NONE; } @@ -1814,6 +1776,35 @@ static PyMethodDef PyTempTestingDef = { "(internal)", }; +// ------------------------- open_file_externally ------------------------------ + +static auto PyOpenFileExternally(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + + char* path = nullptr; + static const char* kwlist[] = {"path", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &path)) { + return nullptr; + } + g_base->platform->OpenFileExternally(path); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyOpenFileExternallyDef = { + "open_file_externally", // name + (PyCFunction)PyOpenFileExternally, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "open_file_externally(path: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Open the provided file in the default external app.", +}; + // ----------------------------------------------------------------------------- auto PythonMethodsMisc::GetMethods() -> std::vector { @@ -1859,8 +1850,6 @@ auto PythonMethodsMisc::GetMethods() -> std::vector { PyInLogicThreadDef, PyRequestPermissionDef, PyHavePermissionDef, - PyIsRunningOnFireTVDef, - PyIsRunningOnOuyaDef, PyUnlockAllInputDef, PyLockAllInputDef, PySetUpSigIntDef, @@ -1883,6 +1872,7 @@ auto PythonMethodsMisc::GetMethods() -> std::vector { PyNativeReviewRequestSupportedDef, PyNativeReviewRequestDef, PyTempTestingDef, + PyOpenFileExternallyDef, }; } diff --git a/src/ballistica/base/python/support/python_context_call.cc b/src/ballistica/base/python/support/python_context_call.cc index a37ddffb..1026b987 100644 --- a/src/ballistica/base/python/support/python_context_call.cc +++ b/src/ballistica/base/python/support/python_context_call.cc @@ -3,8 +3,10 @@ #include "ballistica/base/python/support/python_context_call.h" #include "ballistica/base/logic/logic.h" +#include "ballistica/base/ui/ui.h" #include "ballistica/core/python/core_python.h" #include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/lambda_runnable.h" #include "ballistica/shared/generic/utils.h" #include "ballistica/shared/python/python.h" #include "ballistica/shared/python/python_sys.h" @@ -166,4 +168,28 @@ void PythonContextCall::ScheduleWeak(const PythonRef& args) { }); } +void PythonContextCall::ScheduleInUIOperation() { + // Since we're mucking with Object::WeakRefs, need to limit to logic thread. + BA_PRECONDITION(g_base->InLogicThread()); + Object::Ref ref(this); + assert(base::g_base); + + g_base->ui->PushUIOperationRunnable(NewLambdaRunnableUnmanaged([ref] { + assert(ref.Exists()); + ref->Run(); + })); +} + +void PythonContextCall::ScheduleInUIOperation(const PythonRef& args) { + // Since we're mucking with Object::WeakRefs, need to limit to logic thread. + BA_PRECONDITION(g_base->InLogicThread()); + Object::Ref ref(this); + assert(base::g_base); + + g_base->ui->PushUIOperationRunnable(NewLambdaRunnableUnmanaged([ref, args] { + assert(ref.Exists()); + ref->Run(args); + })); +} + } // namespace ballistica::base diff --git a/src/ballistica/base/python/support/python_context_call.h b/src/ballistica/base/python/support/python_context_call.h index 1c7d5d01..7b6e961c 100644 --- a/src/ballistica/base/python/support/python_context_call.h +++ b/src/ballistica/base/python/support/python_context_call.h @@ -58,6 +58,28 @@ class PythonContextCall : public Object { /// scheduled run. void ScheduleWeak(const PythonRef& args); + /// Schedule a call to run as part of a current UI interaction such as a + /// button being clicked. Must be called from the logic thread. Calls + /// scheduled this way will be run as part of the handling of the event + /// that triggered them, though safely outside of any UI traversal. This + /// avoids pitfalls that can arise with regular Schedule() where calls + /// that run some action and then disable further UI interaction can get + /// run twice due to interaction not actually being disabled until the + /// next event loop cycle, potentially allowing multiple calls to be + /// scheduled before the disable happens. + void ScheduleInUIOperation(); + + /// Schedule a call to run as part of a current UI interaction such as a + /// button being clicked. Must be called from the logic thread. Calls + /// scheduled this way will be run as part of the handling of the event + /// that triggered them, though safely outside of any UI traversal. This + /// avoids pitfalls that can arise with regular Schedule() where calls + /// that run some action and then disable further UI interaction can get + /// run twice due to interaction not actually being disabled until the + /// next event loop cycle, potentially allowing multiple calls to be + /// scheduled before the disable happens. + void ScheduleInUIOperation(const PythonRef& args); + private: void GetTrace(); // we try to grab basic trace info diff --git a/src/ballistica/base/ui/ui.cc b/src/ballistica/base/ui/ui.cc index 8d306e6d..eef389b3 100644 --- a/src/ballistica/base/ui/ui.cc +++ b/src/ballistica/base/ui/ui.cc @@ -2,6 +2,8 @@ #include "ballistica/base/ui/ui.h" +#include + #include "ballistica/base/app_adapter/app_adapter.h" #include "ballistica/base/audio/audio.h" #include "ballistica/base/graphics/component/simple_component.h" @@ -12,12 +14,100 @@ #include "ballistica/base/ui/dev_console.h" #include "ballistica/base/ui/ui_delegate.h" #include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/foundation/macros.h" +#include "ballistica/shared/generic/native_stack_trace.h" #include "ballistica/shared/generic/utils.h" namespace ballistica::base { static const int kUIOwnerTimeoutSeconds = 30; +/// We use this to gather up runnables triggered by UI elements in response +/// to stuff happening (mouse clicks, elements being added or removed, +/// etc.). It's a bad idea to run such runnables immediately because they +/// might modify UI lists we're in the process of traversing. It's also a +/// bad idea to schedule such runnables in the event loop, because a +/// runnable may wish to modify the UI to prevent further runs from +/// happening and that won't work if multiple runnables can be scheduled +/// before the first runs. So our goldilocks approach here is to gather +/// all runnables that get scheduled as part of each operation and then +/// run them explicitly once we are safely out of any UI list traversal. +UI::OperationContext::OperationContext() { + assert(g_base->InLogicThread()); + + // Register ourself as current only if there is none. + parent_ = g_base->ui->operation_context_; + if (parent_ == nullptr) { + g_base->ui->operation_context_ = this; + } +} + +UI::OperationContext::~OperationContext() { + assert(g_base->InLogicThread()); + + // If we registered ourself as the top level context, unregister. + if (parent_ == nullptr) { + assert(g_base->ui->operation_context_ == this); + g_base->ui->operation_context_ = nullptr; + } else { + // If a context was set when we came into existence, it should + // still be that same context when we go out of existence. + assert(g_base->ui->operation_context_ == parent_); + assert(runnables_.empty()); + } + + // Complain if our Finish() call was never run (unless we're being torn + // down due to an exception). + if (!ran_finish_ && !std::current_exception()) { + BA_LOG_ERROR_NATIVE_TRACE_ONCE( + "UI::InteractionContext_ being torn down without Complete called."); + } + + // Our runnables are raw unmanaged pointers; need to explicitly kill them. + // Finish generally clears these out as it goes, but there might be some + // left in the case of exceptions or infinite loop breakouts. + for (auto* ptr : runnables_) { + delete ptr; + } +} + +void UI::OperationContext::AddRunnable(Runnable* runnable) { + // This should only be getting called when we installed ourself as top + // level context. + assert(parent_ == nullptr); + assert(Object::IsValidUnmanagedObject(runnable)); + runnables_.push_back(runnable); +} + +/// Should be explicitly called at the end of the operation. +void UI::OperationContext::Finish() { + assert(g_base->InLogicThread()); + assert(!ran_finish_); + ran_finish_ = true; + + // Run pent up runnaables. It's possible that the payload of something + // scheduled here will itself schedule something here, so we need to do + // this in a loop (and watch for infinite ones). + int cycle_count{}; + while (!runnables_.empty()) { + std::vector runnables; + runnables.swap(runnables_); + for (auto* runnable : runnables) { + runnable->RunAndLogErrors(); + // Our runnables are raw unmanaged pointers; need to explicitly kill + // them. + delete runnable; + } + cycle_count += 1; + if (cycle_count >= 10) { + BA_LOG_ERROR_NATIVE_TRACE( + "UIOperationCount cycle-count hit max; you probably have an infinite " + "loop."); + break; + } + } +} + UI::UI() { assert(g_core); @@ -58,12 +148,6 @@ void UI::StepDisplayTime() { void UI::OnAppStart() { assert(g_base->InLogicThread()); - // if (auto* ui_delegate = g_base->ui->delegate()) { - // printf("HAVE DEL %d\n", - // static_cast(g_base->ui->delegate() != nullptr)); - // g_base->ui_v1()->OnAppStart(); - // } - // Make sure user knows when forced-ui-scale is enabled. if (force_scale_) { if (scale_ == UIScale::kSmall) { @@ -129,6 +213,8 @@ auto UI::PartyWindowOpen() -> bool { auto UI::HandleMouseDown(int button, float x, float y, bool double_click) -> bool { + assert(g_base->InLogicThread()); + bool handled{}; // Dev console button. @@ -265,11 +351,20 @@ auto UI::ShouldShowButtonShortcuts() const -> bool { return g_base->input->have_non_touch_inputs(); } -auto UI::SendWidgetMessage(const WidgetMessage& m) -> int { +auto UI::SendWidgetMessage(const WidgetMessage& m) -> bool { + OperationContext operation_context; + + bool result; if (auto* ui_delegate = g_base->ui->delegate()) { - return ui_delegate->SendWidgetMessage(m); + result = ui_delegate->SendWidgetMessage(m); + } else { + result = false; } - return false; + + // Run anything we triggered. + operation_context.Finish(); + + return result; } void UI::OnScreenSizeChange() { @@ -541,4 +636,28 @@ void UI::OnAssetsAvailable() { } } +void UI::PushUIOperationRunnable(Runnable* runnable) { + assert(g_base->InLogicThread()); + + if (operation_context_ != nullptr) { + operation_context_->AddRunnable(runnable); + return; + } + + // For now, gracefully fall back to pushing an event if there's no current + // operation. Once we've got any bugs cleared out, can leave this as just + // an error log. + + auto trace = g_core->platform->GetNativeStackTrace(); + BA_LOG_ERROR_NATIVE_TRACE_ONCE( + "UI::PushUIOperationRunnable() called outside of UI operation."); + + g_base->logic->event_loop()->PushRunnable(runnable); +} + +auto UI::InUIOperation() -> bool { + assert(g_base->InLogicThread()); + return operation_context_ != nullptr; +} + } // namespace ballistica::base diff --git a/src/ballistica/base/ui/ui.h b/src/ballistica/base/ui/ui.h index 2bbf0917..f30440d7 100644 --- a/src/ballistica/base/ui/ui.h +++ b/src/ballistica/base/ui/ui.h @@ -77,13 +77,22 @@ class UI { /// Draw dev UI on top. void DrawDev(FrameDef* frame_def); + /// Add a runnable to be run as part of the currently-being-processed UI + /// operation. Pass a Runnable that has been allocated with + /// NewUnmanaged(). It will be owned and disposed of by the UI from this + /// point. Must be called from the logic thread. + void PushUIOperationRunnable(Runnable* runnable); + + auto InUIOperation() -> bool; + /// Return the widget an input-device should send commands to, if any. /// Potentially assigns UI control to the provide device, so only call /// this if you intend on actually sending a message to that widget. auto GetWidgetForInput(InputDevice* input_device) -> ui_v1::Widget*; - /// Send a message to the active widget. - auto SendWidgetMessage(const WidgetMessage& msg) -> int; + /// Send a message to the active widget. This is a high level call that + /// should only be used by top level event handling/etc. + auto SendWidgetMessage(const WidgetMessage& msg) -> bool; /// Set the device controlling the UI. void SetUIInputDevice(InputDevice* input_device); @@ -123,12 +132,28 @@ class UI { auto* delegate() const { return delegate_; } + class OperationContext { + public: + OperationContext(); + ~OperationContext(); + /// Should be called before returning from the high level event handling + /// call. + void Finish(); + void AddRunnable(Runnable* runnable); + + private: + bool ran_finish_{}; + OperationContext* parent_{}; + std::vector runnables_; + }; + private: void MainMenuPress_(InputDevice* device); auto DevConsoleButtonSize_() const -> float; auto InDevConsoleButton_(float x, float y) const -> bool; void DrawDevConsoleButton_(FrameDef* frame_def); + OperationContext* operation_context_{}; base::UIDelegateInterface* delegate_{}; DevConsole* dev_console_{}; std::string dev_console_startup_messages_; diff --git a/src/ballistica/core/platform/apple/core_platform_apple.cc b/src/ballistica/core/platform/apple/core_platform_apple.cc index df8281f6..a0a6df03 100644 --- a/src/ballistica/core/platform/apple/core_platform_apple.cc +++ b/src/ballistica/core/platform/apple/core_platform_apple.cc @@ -323,14 +323,14 @@ auto CorePlatformApple::HaveLeaderboard(const std::string& game, #endif } -void CorePlatformApple::ShowOnlineScoreUI(const std::string& show, +void CorePlatformApple::ShowGameServiceUI(const std::string& show, const std::string& game, const std::string& game_version) { #if BA_USE_GAME_CENTER - BallisticaKit::GameCenterContext::showOnlineScoreUI(show, game, game_version); - // base::AppleUtils::ShowOnlineScoreUI(show, game, game_version); + BallisticaKit::GameCenterContext::showGameServiceUI(show, game, game_version); + // base::AppleUtils::ShowGameServiceUI(show, game, game_version); #else - CorePlatform::ShowOnlineScoreUI(show, game, game_version); + CorePlatform::ShowGameServiceUI(show, game, game_version); #endif } diff --git a/src/ballistica/core/platform/apple/core_platform_apple.h b/src/ballistica/core/platform/apple/core_platform_apple.h index 2fd3894f..982fb8f9 100644 --- a/src/ballistica/core/platform/apple/core_platform_apple.h +++ b/src/ballistica/core/platform/apple/core_platform_apple.h @@ -41,7 +41,7 @@ class CorePlatformApple : public CorePlatform { void ReportAchievement(const std::string& achievement) override; auto HaveLeaderboard(const std::string& game, const std::string& config) -> bool override; - void ShowOnlineScoreUI(const std::string& show, const std::string& game, + void ShowGameServiceUI(const std::string& show, const std::string& game, const std::string& game_version) override; void ResetAchievements() override; auto IsOSPlayingMusic() -> bool override; diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc index 274c0996..2050f32e 100644 --- a/src/ballistica/core/platform/core_platform.cc +++ b/src/ballistica/core/platform/core_platform.cc @@ -9,10 +9,15 @@ // Trying to avoid platform-specific headers here except for // a few mostly-cross-platform bits where its worth the mess. -#if !BA_OSTYPE_WINDOWS #if BA_ENABLE_EXECINFO_BACKTRACES +#if BA_OSTYPE_ANDROID +#include "ballistica/core/platform/android/execinfo.h" +#else #include -#endif +#endif // BA_OSTYPE_ANDROID +#endif // BA_ENABLE_EXECINFO_BACKTRACES + +#if !BA_OSTYPE_WINDOWS #include #include #endif @@ -20,6 +25,7 @@ #include "ballistica/core/platform/support/min_sdl.h" #include "ballistica/core/support/base_soft.h" #include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/native_stack_trace.h" #include "ballistica/shared/generic/utils.h" #include "ballistica/shared/networking/networking_sys.h" #include "ballistica/shared/python/python.h" @@ -689,10 +695,10 @@ auto CorePlatform::HaveLeaderboard(const std::string& game, return false; } -void CorePlatform::ShowOnlineScoreUI(const std::string& show, +void CorePlatform::ShowGameServiceUI(const std::string& show, const std::string& game, const std::string& game_version) { - Log(LogLevel::kError, "FIXME: ShowOnlineScoreUI() unimplemented"); + Log(LogLevel::kError, "FIXME: ShowGameServiceUI() unimplemented"); } void CorePlatform::AndroidSetResString(const std::string& res) { @@ -952,7 +958,7 @@ auto CorePlatform::GetSubplatformName() -> std::string { #if BA_ENABLE_EXECINFO_BACKTRACES // Stack traces using the functionality in execinfo.h -class PlatformStackTraceExecInfo : public PlatformStackTrace { +class NativeStackTraceExecInfo : public NativeStackTrace { public: static constexpr int kMaxStackLevels = 64; @@ -960,14 +966,23 @@ class PlatformStackTraceExecInfo : public PlatformStackTrace { // construction but should do the bare minimum amount of work to store it. Any // expensive operations such as symbolification should be deferred until // FormatForDisplay(). - PlatformStackTraceExecInfo() { nsize_ = backtrace(array_, kMaxStackLevels); } + NativeStackTraceExecInfo() { nsize_ = backtrace(array_, kMaxStackLevels); } auto FormatForDisplay() noexcept -> std::string override { try { std::string s; char** symbols = backtrace_symbols(array_, nsize_); for (int i = 0; i < nsize_; i++) { - s += std::string(symbols[i]); + const char* symbol = symbols[i]; + // Special case for Android: there's usually a horrific mess of a + // pathname leading up to libmain.so, which we should never really + // care about, so let's strip that out if possible. + if (g_buildconfig.ostype_android()) { + if (const char* s2 = strstr(symbol, "/libmain.so")) { + symbol = s2 + 1; + } + } + s += std::string(symbol); if (i < nsize_ - 1) { s += "\n"; } @@ -979,9 +994,9 @@ class PlatformStackTraceExecInfo : public PlatformStackTrace { } } - auto Copy() const noexcept -> PlatformStackTrace* override { + auto Copy() const noexcept -> NativeStackTrace* override { try { - auto s = new PlatformStackTraceExecInfo(*this); + auto s = new NativeStackTraceExecInfo(*this); // Vanilla copy constructor should do the right thing here. assert(s->nsize_ == nsize_ @@ -999,11 +1014,11 @@ class PlatformStackTraceExecInfo : public PlatformStackTrace { }; #endif -auto CorePlatform::GetStackTrace() -> PlatformStackTrace* { +auto CorePlatform::GetNativeStackTrace() -> NativeStackTrace* { // Our default handler here supports execinfo backtraces where available // and gives nothing elsewhere. #if BA_ENABLE_EXECINFO_BACKTRACES - return new PlatformStackTraceExecInfo(); + return new NativeStackTraceExecInfo(); #else return nullptr; #endif diff --git a/src/ballistica/core/platform/core_platform.h b/src/ballistica/core/platform/core_platform.h index 6bcd0627..cd84f478 100644 --- a/src/ballistica/core/platform/core_platform.h +++ b/src/ballistica/core/platform/core_platform.h @@ -281,7 +281,7 @@ class CorePlatform { virtual auto HaveLeaderboard(const std::string& game, const std::string& config) -> bool; - virtual void ShowOnlineScoreUI(const std::string& show, + virtual void ShowGameServiceUI(const std::string& show, const std::string& game, const std::string& game_version); virtual void ResetAchievements(); @@ -294,11 +294,11 @@ class CorePlatform { #pragma mark ERRORS & DEBUGGING ------------------------------------------------ - /// Should return a subclass of PlatformStackTrace allocated via new. It + /// Should return a subclass of NativeStackTrace allocated via new. It /// is up to the caller to call delete on the returned trace when done /// with it. Platforms with no meaningful stack trace functionality can /// return nullptr. - virtual auto GetStackTrace() -> PlatformStackTrace*; + virtual auto GetNativeStackTrace() -> NativeStackTrace*; /// Optionally override fatal error reporting. If true is returned, default /// fatal error reporting will not run. @@ -472,25 +472,6 @@ class CorePlatform { std::list mac_music_app_playlists_; }; -/// For capturing and printing stack-traces and related errors. Platforms -/// should subclass this and return instances in GetStackTrace(). Stack -/// trace classes should capture the stack state immediately upon -/// construction but should do the bare minimum amount of work to store it. -/// Any expensive operations such as symbolification should be deferred -/// until FormatForDisplay(). -class PlatformStackTrace { - public: - virtual ~PlatformStackTrace() = default; - - // Return a human readable version of the trace (with symbolification if - // available). - virtual auto FormatForDisplay() noexcept -> std::string = 0; - - // Should return a copy of itself allocated via new() (or nullptr if not - // possible). - virtual auto Copy() const noexcept -> PlatformStackTrace* = 0; -}; - } // namespace ballistica::core #endif // BALLISTICA_CORE_PLATFORM_CORE_PLATFORM_H_ diff --git a/src/ballistica/core/platform/windows/core_platform_windows.cc b/src/ballistica/core/platform/windows/core_platform_windows.cc index cfcff5e6..7bf234c7 100644 --- a/src/ballistica/core/platform/windows/core_platform_windows.cc +++ b/src/ballistica/core/platform/windows/core_platform_windows.cc @@ -40,6 +40,7 @@ #endif #include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/native_stack_trace.h" #include "ballistica/shared/generic/utils.h" #include "ballistica/shared/networking/networking_sys.h" @@ -52,7 +53,7 @@ namespace ballistica::core { static const int kTraceMaxStackFrames{256}; static const int kTraceMaxFunctionNameLength{1024}; -class WinStackTrace : public PlatformStackTrace { +class WinStackTrace : public NativeStackTrace { public: explicit WinStackTrace(CorePlatformWindows* platform) : platform_{platform} { number_of_frames_ = @@ -67,7 +68,7 @@ class WinStackTrace : public PlatformStackTrace { // Should return a copy of itself allocated via new() (or nullptr if not // possible). - auto Copy() const noexcept -> PlatformStackTrace* override { + auto Copy() const noexcept -> NativeStackTrace* override { try { auto s = new WinStackTrace(*this); @@ -169,7 +170,7 @@ auto CorePlatformWindows::FormatWinStackTraceForDisplay( } } -auto CorePlatformWindows::GetStackTrace() -> PlatformStackTrace* { +auto CorePlatformWindows::GetNativeStackTrace() -> NativeStackTrace* { return new WinStackTrace(this); } diff --git a/src/ballistica/core/platform/windows/core_platform_windows.h b/src/ballistica/core/platform/windows/core_platform_windows.h index fce89580..4943e85e 100644 --- a/src/ballistica/core/platform/windows/core_platform_windows.h +++ b/src/ballistica/core/platform/windows/core_platform_windows.h @@ -21,7 +21,7 @@ class CorePlatformWindows : public CorePlatform { static auto UTF8Encode(const std::wstring& wstr) -> std::string; static auto UTF8Decode(const std::string& str) -> std::wstring; - auto GetStackTrace() -> PlatformStackTrace* override; + auto GetNativeStackTrace() -> NativeStackTrace* override; auto GetDeviceV1AccountUUIDPrefix() -> std::string override { return "w"; } auto GetDeviceUUIDInputs() -> std::list override; auto GenerateUUID() -> std::string override; diff --git a/src/ballistica/scene_v1/support/host_session.cc b/src/ballistica/scene_v1/support/host_session.cc index 9c726733..9537d3ea 100644 --- a/src/ballistica/scene_v1/support/host_session.cc +++ b/src/ballistica/scene_v1/support/host_session.cc @@ -287,7 +287,7 @@ void HostSession::RemovePlayer(Player* player) { return; } } - BA_LOG_ERROR_TRACE("Player not found in HostSession::RemovePlayer()"); + BA_LOG_ERROR_PYTHON_TRACE("Player not found in HostSession::RemovePlayer()"); } void HostSession::IssuePlayerLeft(Player* player) { diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index 27f3b924..1fb955d8 100644 --- a/src/ballistica/shared/ballistica.cc +++ b/src/ballistica/shared/ballistica.cc @@ -39,8 +39,8 @@ auto main(int argc, char** argv) -> int { namespace ballistica { // These are set automatically via script; don't modify them here. -const int kEngineBuildNumber = 21707; -const char* kEngineVersion = "1.7.31"; +const int kEngineBuildNumber = 21728; +const char* kEngineVersion = "1.7.32"; const int kEngineApiVersion = 8; #if BA_MONOLITHIC_BUILD @@ -164,8 +164,9 @@ auto MonolithicMain(const core::CoreConfig& core_config) -> int { // with reports from dev builds. bool try_to_exit_cleanly = !(l_base && l_base->IsUnmodifiedBlessedBuild()); - // If this returns true, it means the app is handling things (showing a - // fatal error dialog, etc.) and it's out of our hands. + // If this returns true, it means the platform/app-adapter is handling + // things (showing a fatal error dialog, etc.) and it's out of our + // hands. bool handled = FatalError::HandleFatalError(try_to_exit_cleanly, true); // If it's not been handled, take the app down ourself. @@ -229,8 +230,9 @@ class IncrementalInitRunner_ { // with reports from dev builds. bool try_to_exit_cleanly = !(base_ && base_->IsUnmodifiedBlessedBuild()); - // If this returns true, it means the app is handling things (showing a - // fatal error dialog, etc.) and it's out of our hands. + // If this returns true, it means the platform/app-adapter is handling + // things (showing a fatal error dialog, etc.) and it's out of our + // hands. bool handled = FatalError::HandleFatalError(try_to_exit_cleanly, true); // If it's not been handled, take the app down ourself. diff --git a/src/ballistica/shared/foundation/event_loop.cc b/src/ballistica/shared/foundation/event_loop.cc index b3711495..58153d94 100644 --- a/src/ballistica/shared/foundation/event_loop.cc +++ b/src/ballistica/shared/foundation/event_loop.cc @@ -396,6 +396,7 @@ auto EventLoop::ThreadMain_() -> int { // report logs with reports from dev builds. bool try_to_exit_cleanly = !(g_base_soft && g_base_soft->IsUnmodifiedBlessedBuild()); + bool handled = FatalError::HandleFatalError(try_to_exit_cleanly, true); // Do the default thing if platform didn't handle it. diff --git a/src/ballistica/shared/foundation/event_loop.h b/src/ballistica/shared/foundation/event_loop.h index 8bb761a6..f7391e58 100644 --- a/src/ballistica/shared/foundation/event_loop.h +++ b/src/ballistica/shared/foundation/event_loop.h @@ -52,9 +52,9 @@ class EventLoop { Timer* GetTimer(int id); void DeleteTimer(int id); - /// Add a runnable to this thread's event-loop. - /// Pass a Runnable that has been allocated with NewUnmanaged(). - /// It will be owned and disposed of by the thread. + /// Add a runnable to this thread's event-loop. Pass a Runnable that has + /// been allocated with NewUnmanaged(). It will be owned and disposed of + /// by the thread. void PushRunnable(Runnable* runnable); /// Convenience function to push a lambda as a runnable. @@ -63,7 +63,8 @@ class EventLoop { PushRunnable(NewLambdaRunnableUnmanaged(lambda)); } - /// Add a runnable to this thread's event-loop and wait until it completes. + /// Add a runnable to this thread's event-loop and wait until it + /// completes. void PushRunnableSynchronous(Runnable* runnable); /// Convenience function to push a lambda as a runnable. diff --git a/src/ballistica/shared/foundation/exception.cc b/src/ballistica/shared/foundation/exception.cc index 8e54fc6b..04f27288 100644 --- a/src/ballistica/shared/foundation/exception.cc +++ b/src/ballistica/shared/foundation/exception.cc @@ -4,6 +4,7 @@ #include "ballistica/core/core.h" #include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/generic/native_stack_trace.h" namespace ballistica { @@ -21,7 +22,7 @@ Exception::Exception(std::string message_in, PyExcType python_type) // If core has been inited, attempt to capture a stack-trace here we // can print out later if desired. if (core::g_core) { - stack_trace_ = core::g_core->platform->GetStackTrace(); + stack_trace_ = core::g_core->platform->GetNativeStackTrace(); } } @@ -31,7 +32,7 @@ Exception::Exception(PyExcType python_type) : python_type_(python_type) { // If core has been inited, attempt to capture a stack-trace here we // can print out later if desired. if (core::g_core) { - stack_trace_ = core::g_core->platform->GetStackTrace(); + stack_trace_ = core::g_core->platform->GetNativeStackTrace(); } } diff --git a/src/ballistica/shared/foundation/exception.h b/src/ballistica/shared/foundation/exception.h index 6e384151..b0a0d544 100644 --- a/src/ballistica/shared/foundation/exception.h +++ b/src/ballistica/shared/foundation/exception.h @@ -36,10 +36,6 @@ namespace ballistica { // std::runtime_error at a later time and also catches exceptions coming from // std itself. -namespace core { -class PlatformStackTrace; -} - /// Get a short description for an exception. /// By default, our Exception classes provide what() values that may include /// backtraces of the throw location or other extended info that can be useful @@ -77,7 +73,7 @@ class Exception : public std::exception { std::string message_; std::string full_description_; PyExcType python_type_; - core::PlatformStackTrace* stack_trace_{}; + NativeStackTrace* stack_trace_{}; }; } // namespace ballistica diff --git a/src/ballistica/shared/foundation/fatal_error.cc b/src/ballistica/shared/foundation/fatal_error.cc index 4bed5b6a..3fb5c7ea 100644 --- a/src/ballistica/shared/foundation/fatal_error.cc +++ b/src/ballistica/shared/foundation/fatal_error.cc @@ -6,6 +6,7 @@ #include "ballistica/core/support/base_soft.h" #include "ballistica/shared/foundation/logging.h" #include "ballistica/shared/generic/lambda_runnable.h" +#include "ballistica/shared/generic/native_stack_trace.h" #include "ballistica/shared/python/python.h" namespace ballistica { @@ -87,7 +88,7 @@ void FatalError::ReportFatalError(const std::string& message, // since we know where those are anyway. if (!in_top_level_exception_handler) { if (g_core && g_core->platform) { - core::PlatformStackTrace* trace{g_core->platform->GetStackTrace()}; + NativeStackTrace* trace{g_core->platform->GetNativeStackTrace()}; if (trace) { std::string tracestr = trace->FormatForDisplay(); if (!tracestr.empty()) { diff --git a/src/ballistica/shared/foundation/macros.cc b/src/ballistica/shared/foundation/macros.cc index 97734ce9..c2ae788d 100644 --- a/src/ballistica/shared/foundation/macros.cc +++ b/src/ballistica/shared/foundation/macros.cc @@ -6,6 +6,7 @@ #include "ballistica/core/core.h" #include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/generic/native_stack_trace.h" #include "ballistica/shared/python/python.h" // Snippets of compiled functionality used by our evil macros. @@ -35,7 +36,6 @@ void MacroFunctionTimerEndThread(core::CoreFeatureSet* corefs, if (g_buildconfig.test_build()) { return; } - // EW: Using core's internal globals directly; shouldn't do this. assert(corefs); millisecs_t endtime = corefs->platform->GetTicks(); if (endtime - starttime > time) { @@ -53,7 +53,6 @@ void MacroFunctionTimerEndEx(core::CoreFeatureSet* corefs, if (g_buildconfig.test_build()) { return; } - // EW: Using core's internal globals directly; shouldn't do this. assert(corefs); millisecs_t endtime = corefs->platform->GetTicks(); if (endtime - starttime > time) { @@ -72,7 +71,6 @@ void MacroFunctionTimerEndThreadEx(core::CoreFeatureSet* corefs, if (g_buildconfig.test_build()) { return; } - // EW: Using core's internal globals directly; shouldn't do this. assert(corefs); millisecs_t endtime = corefs->platform->GetTicks(); if (endtime - starttime > time) { @@ -91,7 +89,6 @@ void MacroTimeCheckEnd(core::CoreFeatureSet* corefs, millisecs_t starttime, if (g_buildconfig.test_build()) { return; } - // EW: Using core's internal globals directly; shouldn't do this. assert(corefs); millisecs_t e = corefs->platform->GetTicks(); if (e - starttime > time) { @@ -102,12 +99,28 @@ void MacroTimeCheckEnd(core::CoreFeatureSet* corefs, millisecs_t starttime, } } -void MacroLogErrorTrace(core::CoreFeatureSet* corefs, const std::string& msg, - const char* fname, int line) { +void MacroLogErrorNativeTrace(core::CoreFeatureSet* corefs, + const std::string& msg, const char* fname, + int line) { char buffer[2048]; snprintf(buffer, sizeof(buffer), "%s:%d:", MacroPathFilter(corefs, fname), line); - buffer[sizeof(buffer) - 1] = 0; + auto trace = corefs->platform->GetNativeStackTrace(); + auto trace_s = + trace ? trace->FormatForDisplay() : ""; + Log(LogLevel::kError, + std::string(buffer) + " error: " + msg + "\n" + trace_s); +} + +void MacroLogErrorPythonTrace(core::CoreFeatureSet* corefs, + const std::string& msg, const char* fname, + int line) { + char buffer[2048]; + snprintf(buffer, sizeof(buffer), "%s:%d:", MacroPathFilter(corefs, fname), + line); + // FIXME: Should have the trace be part of the log; not a separate print. + // Since our logging goes through Python anyway, we should just ask + // Python do include the trace in our log call. Python::PrintStackTrace(); Log(LogLevel::kError, std::string(buffer) + " error: " + msg); } @@ -117,7 +130,6 @@ void MacroLogError(core::CoreFeatureSet* corefs, const std::string& msg, char e_buffer[2048]; snprintf(e_buffer, sizeof(e_buffer), "%s:%d:", MacroPathFilter(corefs, fname), line); - e_buffer[sizeof(e_buffer) - 1] = 0; Log(LogLevel::kError, std::string(e_buffer) + " error: " + msg); } @@ -128,7 +140,8 @@ void MacroLogPythonTrace(core::CoreFeatureSet* corefs, const std::string& msg) { auto MacroPathFilter(core::CoreFeatureSet* corefs, const char* filename) -> const char* { - // If we've got a build_src_dir set and filename starts with it, skip past it. + // If we've got a build_src_dir set and filename starts with it, skip past + // it. assert(corefs); if (corefs && !corefs->build_src_dir().empty() && strstr(filename, core::g_core->build_src_dir().c_str()) == filename) { diff --git a/src/ballistica/shared/foundation/macros.h b/src/ballistica/shared/foundation/macros.h index 1a445200..f8526b8c 100644 --- a/src/ballistica/shared/foundation/macros.h +++ b/src/ballistica/shared/foundation/macros.h @@ -75,19 +75,34 @@ type(const type& foo) = delete; \ type& operator=(const type& src) = delete; /* NOLINT (macro parens) */ -// Call this for errors which are non-fatal but should be noted so they can be -// fixed. -#define BA_LOG_ERROR_TRACE(msg) \ - ::ballistica::MacroLogErrorTrace(g_core, msg, __FILE__, __LINE__) +// Call this for errors which are non-fatal but should be noted so they can +// be fixed. +#define BA_LOG_ERROR_NATIVE_TRACE(msg) \ + ::ballistica::MacroLogErrorNativeTrace(g_core, msg, __FILE__, __LINE__) -#define BA_LOG_ERROR_TRACE_ONCE(msg) \ - { \ - static bool did_log_error_trace_here = false; \ - if (!did_log_error_trace_here) { \ - ::ballistica::MacroLogErrorTrace(g_core, msg, __FILE__, __LINE__); \ - did_log_error_trace_here = true; \ - } \ - } \ +#define BA_LOG_ERROR_NATIVE_TRACE_ONCE(msg) \ + { \ + static bool did_log_error_trace_here = false; \ + if (!did_log_error_trace_here) { \ + ::ballistica::MacroLogErrorNativeTrace(g_core, msg, __FILE__, __LINE__); \ + did_log_error_trace_here = true; \ + } \ + } \ + ((void)0) // (see 'Trailing-semicolon note' at top) + +// Call this for errors which are non-fatal but should be noted so they can +// be fixed. +#define BA_LOG_ERROR_PYTHON_TRACE(msg) \ + ::ballistica::MacroLogErrorPythonTrace(g_core, msg, __FILE__, __LINE__) + +#define BA_LOG_ERROR_PYTHON_TRACE_ONCE(msg) \ + { \ + static bool did_log_error_trace_here = false; \ + if (!did_log_error_trace_here) { \ + ::ballistica::MacroLogErrorPythonTrace(g_core, msg, __FILE__, __LINE__); \ + did_log_error_trace_here = true; \ + } \ + } \ ((void)0) // (see 'Trailing-semicolon note' at top) #define BA_LOG_ONCE(lvl, msg) \ @@ -175,8 +190,12 @@ void MacroFunctionTimerEndThreadEx(core::CoreFeatureSet* corefs, void MacroTimeCheckEnd(core::CoreFeatureSet* corefs, millisecs_t starttime, millisecs_t time, const char* name, const char* file, int line); -void MacroLogErrorTrace(core::CoreFeatureSet* corefs, const std::string& msg, - const char* fname, int line); +void MacroLogErrorNativeTrace(core::CoreFeatureSet* corefs, + const std::string& msg, const char* fname, + int line); +void MacroLogErrorPythonTrace(core::CoreFeatureSet* corefs, + const std::string& msg, const char* fname, + int line); void MacroLogError(core::CoreFeatureSet* corefs, const std::string& msg, const char* fname, int line); void MacroLogPythonTrace(core::CoreFeatureSet* corefs, const std::string& msg); diff --git a/src/ballistica/shared/foundation/types.h b/src/ballistica/shared/foundation/types.h index fd35d729..971e5f52 100644 --- a/src/ballistica/shared/foundation/types.h +++ b/src/ballistica/shared/foundation/types.h @@ -42,6 +42,7 @@ class EventLoop; class FeatureSetNativeComponent; class JsonDict; class Matrix44f; +class NativeStackTrace; class Object; class Python; class PythonRef; diff --git a/src/ballistica/shared/generic/native_stack_trace.h b/src/ballistica/shared/generic/native_stack_trace.h new file mode 100644 index 00000000..added5fe --- /dev/null +++ b/src/ballistica/shared/generic/native_stack_trace.h @@ -0,0 +1,31 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_GENERIC_NATIVE_STACK_TRACE_H_ +#define BALLISTICA_SHARED_GENERIC_NATIVE_STACK_TRACE_H_ + +#include + +namespace ballistica { + +/// For capturing and printing stack-traces and related errors. Platforms +/// should subclass this and return instances in GetNativeStackTrace(). Stack +/// trace classes should capture the stack state immediately upon +/// construction but should do the bare minimum amount of work to store it. +/// Any expensive operations such as symbolification should be deferred +/// until FormatForDisplay(). +class NativeStackTrace { + public: + virtual ~NativeStackTrace() = default; + + // Return a human readable version of the trace (with symbolification if + // available). + virtual auto FormatForDisplay() noexcept -> std::string = 0; + + // Should return a copy of itself allocated via new() (or nullptr if not + // possible). + virtual auto Copy() const noexcept -> NativeStackTrace* = 0; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SHARED_GENERIC_NATIVE_STACK_TRACE_H_ diff --git a/src/ballistica/ui_v1/python/class/python_class_widget.cc b/src/ballistica/ui_v1/python/class/python_class_widget.cc index 57853ee0..62b879be 100644 --- a/src/ballistica/ui_v1/python/class/python_class_widget.cc +++ b/src/ballistica/ui_v1/python/class/python_class_widget.cc @@ -40,7 +40,7 @@ void PythonClassWidget::SetupType(PyTypeObject* cls) { "\n" "This class represents a weak reference to a widget object\n" "in the internal C++ layer. Currently, functions such as\n" - "babase.buttonwidget() must be used to instantiate or edit these.\n" + "bauiv1.buttonwidget() must be used to instantiate or edit these.\n" "Attributes:\n" " " ATTR_TRANSITIONING_OUT " (bool):\n" " Whether this widget is in the process of dying (read only).\n" @@ -72,7 +72,9 @@ auto PythonClassWidget::Create(Widget* widget) -> PyObject* { assert(TypeIsSetUp(&type_obj)); auto* py_widget = reinterpret_cast( PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); - if (!py_widget) throw Exception("babase.Widget creation failed"); + if (!py_widget) { + throw Exception("bauiv1.Widget creation failed"); + } *py_widget->widget_ = widget; @@ -285,6 +287,10 @@ auto PythonClassWidget::Delete(PythonClassWidget* self, PyObject* args, args, keywds, "|i", const_cast(kwlist), &ignore_missing)) { return nullptr; } + + // Defer any user code triggered by selects/etc until the end. + base::UI::OperationContext ui_op_context; + Widget* w = self->widget_->Get(); if (!w) { if (!ignore_missing) { @@ -298,6 +304,9 @@ auto PythonClassWidget::Delete(PythonClassWidget* self, PyObject* args, Log(LogLevel::kError, "Can't delete widget: no parent."); } } + + // Run any user code that got triggered. + ui_op_context.Finish(); Py_RETURN_NONE; BA_PYTHON_CATCH; } diff --git a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc index 8c0aefe5..2ff37cf6 100644 --- a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc +++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc @@ -87,35 +87,6 @@ static PyMethodDef PyGetTextureDef = { "Load a texture for use in the ui.", }; -// ------------------------------- getmesh ------------------------------------- - -static auto PyGetMesh(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* name; - static const char* kwlist[] = {"name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &name)) { - return nullptr; - } - { - base::Assets::AssetListLock lock; - return PythonClassUIMesh::Create(g_base->assets->GetMesh(name)); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyGetMeshDef = { - "getmesh", // name - (PyCFunction)PyGetMesh, // method - METH_VARARGS | METH_KEYWORDS, // flags - - "getmesh(name: str) -> bauiv1.Mesh\n" - "\n" - "Load a mesh for use solely in the local user interface.", -}; - // -------------------------- get_qrcode_texture ------------------------------- static auto PyGetQRCodeTexture(PyObject* self, PyObject* args, PyObject* keywds) @@ -146,6 +117,35 @@ static PyMethodDef PyGetQRCodeTextureDef = { "The provided url must be 64 bytes or less.", }; +// ------------------------------- getmesh ------------------------------------- + +static auto PyGetMesh(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + { + base::Assets::AssetListLock lock; + return PythonClassUIMesh::Create(g_base->assets->GetMesh(name)); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetMeshDef = { + "getmesh", // name + (PyCFunction)PyGetMesh, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getmesh(name: str) -> bauiv1.Mesh\n" + "\n" + "Load a mesh for use solely in the local user interface.", +}; + // ----------------------------- buttonwidget ---------------------------------- static auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) @@ -250,6 +250,10 @@ static auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("UI functions must be called with no context set."); } + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + // Grab the edited widget or create a new one. Object::Ref b; if (edit_obj != Py_None) { @@ -444,6 +448,9 @@ static auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) g_ui_v1->AddWidget(b.Get(), parent_widget); } + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + return b->NewPyRef(); BA_PYTHON_CATCH; @@ -552,6 +559,10 @@ static auto PyCheckBoxWidget(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("UI functions must be called with no context set."); } + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { @@ -633,6 +644,9 @@ static auto PyCheckBoxWidget(PyObject* self, PyObject* args, PyObject* keywds) g_ui_v1->AddWidget(widget.Get(), parent_widget); } + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + return widget->NewPyRef(); BA_PYTHON_CATCH; @@ -725,6 +739,10 @@ static auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("UI functions must be called with no context set."); } + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + // Grab the edited widget or create a new one. Object::Ref b; if (edit_obj != Py_None) { @@ -821,6 +839,9 @@ static auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) g_ui_v1->AddWidget(b.Get(), parent_widget); } + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + return b->NewPyRef(); BA_PYTHON_CATCH; } @@ -914,6 +935,10 @@ static auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("UI functions must be called with no context set."); } + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { @@ -970,7 +995,13 @@ static auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) widget->set_background(Python::GetPyBool(background_obj)); } if (selected_child_obj != Py_None) { + // Need to wrap this in an operation because it can trigger user code. + base::UI::OperationContext operation_context; + widget->SelectWidget(UIV1Python::GetPyWidget(selected_child_obj)); + + // Run any user code/etc. + operation_context.Finish(); } if (visible_child_obj != Py_None) { widget->ShowWidget(UIV1Python::GetPyWidget(visible_child_obj)); @@ -991,6 +1022,9 @@ static auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) g_ui_v1->AddWidget(widget.Get(), parent_widget); } + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + return widget->NewPyRef(); BA_PYTHON_CATCH; @@ -1120,6 +1154,9 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("UI functions must be called with no context set."); } + // Defer any user code triggered by selects/etc until the end. + base::UI::OperationContext ui_op_context; + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { @@ -1207,7 +1244,7 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) widget->SetRootSelectable(Python::GetPyBool(root_selectable_obj)); } if (selected_child_obj != Py_None) { - // special case: passing 0 implies deselect + // Special case: passing 0 implies deselect. if (PyLong_Check(selected_child_obj) && (PyLong_AsLong(selected_child_obj) == 0)) { widget->SelectWidget(nullptr); @@ -1218,18 +1255,19 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) if (transition_obj != Py_None) { std::string t = Python::GetPyString(transition_obj); - if (t == "in_left") + if (t == "in_left") { widget->SetTransition(ContainerWidget::TransitionType::kInLeft); - else if (t == "in_right") + } else if (t == "in_right") { widget->SetTransition(ContainerWidget::TransitionType::kInRight); - else if (t == "out_left") + } else if (t == "out_left") { widget->SetTransition(ContainerWidget::TransitionType::kOutLeft); - else if (t == "out_right") + } else if (t == "out_right") { widget->SetTransition(ContainerWidget::TransitionType::kOutRight); - else if (t == "in_scale") + } else if (t == "in_scale") { widget->SetTransition(ContainerWidget::TransitionType::kInScale); - else if (t == "out_scale") + } else if (t == "out_scale") { widget->SetTransition(ContainerWidget::TransitionType::kOutScale); + } } if (cancel_button_obj != Py_None) { @@ -1301,6 +1339,10 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) widget->set_claims_outside_clicks( Python::GetPyBool(claim_outside_clicks_obj)); } + + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + return widget->NewPyRef(); BA_PYTHON_CATCH; } @@ -1387,6 +1429,10 @@ static auto PyRowWidget(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("UI functions must be called with no context set."); } + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { @@ -1443,6 +1489,9 @@ static auto PyRowWidget(PyObject* self, PyObject* args, PyObject* keywds) g_ui_v1->AddWidget(widget.Get(), parent_widget); } + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + return widget->NewPyRef(); BA_PYTHON_CATCH; @@ -1532,6 +1581,10 @@ static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("UI functions must be called with no context set."); } + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { @@ -1615,6 +1668,10 @@ static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) if (edit_obj == Py_None) { g_ui_v1->AddWidget(widget.Get(), parent_widget); } + + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + return widget->NewPyRef(); BA_PYTHON_CATCH; @@ -1710,6 +1767,10 @@ static auto PyHScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("UI functions must be called with no context set."); } + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { @@ -1788,6 +1849,10 @@ static auto PyHScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) if (edit_obj == Py_None) { g_ui_v1->AddWidget(widget.Get(), parent_widget); } + + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + return widget->NewPyRef(); BA_PYTHON_CATCH; @@ -1931,6 +1996,8 @@ static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) // Grab the edited widget or create a new one. Object::Ref widget; + + // Handle query special cases first. if (query_obj != Py_None) { widget = dynamic_cast(UIV1Python::GetPyWidget(query_obj)); if (!widget.Exists()) { @@ -1957,6 +2024,13 @@ static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) } return PyUnicode_FromString(widget->description().c_str()); } + + // Ok it's not a query; it's a create or edit. + + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + if (edit_obj != Py_None) { widget = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); if (!widget.Exists()) { @@ -2142,6 +2216,10 @@ static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) if (edit_obj == Py_None) { g_ui_v1->AddWidget(widget.Get(), parent_widget); } + + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + return widget->NewPyRef(); BA_PYTHON_CATCH; @@ -2241,6 +2319,10 @@ static auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("UI functions must be called with no context set."); } + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + Widget* widget = nullptr; if (edit_obj != Py_None) { widget = UIV1Python::GetPyWidget(edit_obj); @@ -2293,6 +2375,9 @@ static auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds) widget->set_auto_select(Python::GetPyBool(autoselect_obj)); } + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + Py_RETURN_NONE; BA_PYTHON_CATCH; } @@ -2356,135 +2441,6 @@ static PyMethodDef PyUIBoundsDef = { "center remains onscreen.", }; -// ------------------------ show_online_score_ui ------------------------------- - -static auto PyShowOnlineScoreUI(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - const char* show = "general"; - PyObject* game_obj = Py_None; - PyObject* game_version_obj = Py_None; - static const char* kwlist[] = {"show", "game", "game_version", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "|sOO", - const_cast(kwlist), &show, &game_obj, - &game_version_obj)) { - return nullptr; - } - std::string game; - if (game_obj != Py_None) { - game = Python::GetPyString(game_obj); - } - std::string game_version; - if (game_version_obj != Py_None) { - game_version = Python::GetPyString(game_version_obj); - } - g_base->app_adapter->PushMainThreadCall([show, game, game_version] { - assert(g_core->InMainThread()); - g_core->platform->ShowOnlineScoreUI(show, game, game_version); - }); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyShowOnlineScoreUIDef = { - "show_online_score_ui", // name - (PyCFunction)PyShowOnlineScoreUI, // method - METH_VARARGS | METH_KEYWORDS, // flags - - "show_online_score_ui(show: str = 'general', game: str | None = None,\n" - " game_version: str | None = None) -> None\n" - "\n" - "(internal)", -}; - -// -------------------------------- show_ad ------------------------------------ - -static auto PyShowAd(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - BA_PRECONDITION(g_base->InLogicThread()); - const char* purpose; - PyObject* on_completion_call_obj = Py_None; - int pass_actually_showed = false; - static const char* kwlist[] = {"purpose", "on_completion_call", nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "s|O", const_cast(kwlist), &purpose, - &on_completion_call_obj, &pass_actually_showed)) { - return nullptr; - } - g_base->plus()->SetAdCompletionCall(on_completion_call_obj, - static_cast(pass_actually_showed)); - - // In cases where we support ads, store our callback and kick one off. - // We'll then fire our callback once its done. If we *don't* support ads, - // just store our callback and then kick off an ad-view-complete message - // ourself so the event flow is similar.. - if (g_core->platform->GetHasAds()) { - g_core->platform->ShowAd(purpose); - } else { - g_base->plus()->PushAdViewComplete(purpose, false); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyShowAdDef = { - "show_ad", // name - (PyCFunction)PyShowAd, // method - METH_VARARGS | METH_KEYWORDS, // flags - - "show_ad(purpose: str,\n" - " on_completion_call: Callable[[], None] | None = None)\n" - " -> None\n" - "\n" - "(internal)", -}; - -// ------------------------------ show_ad_2 ------------------------------------ - -// (same as PyShowAd but passes actually_showed arg in callback) -static auto PyShowAd2(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - - BA_PRECONDITION(g_base->InLogicThread()); - const char* purpose; - PyObject* on_completion_call_obj = Py_None; - int pass_actually_showed = true; - static const char* kwlist[] = {"purpose", "on_completion_call", nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "s|O", const_cast(kwlist), &purpose, - &on_completion_call_obj, &pass_actually_showed)) { - return nullptr; - } - g_base->plus()->SetAdCompletionCall(on_completion_call_obj, - static_cast(pass_actually_showed)); - - // In cases where we support ads, store our callback and kick one off. - // We'll then fire our callback once its done. - // If we *don't* support ads, just store our callback and then kick off - // an ad-view-complete message ourself so the event flow is similar.. - if (g_core->platform->GetHasAds()) { - g_core->platform->ShowAd(purpose); - } else { - g_base->plus()->PushAdViewComplete(purpose, false); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyShowAd2Def = { - "show_ad_2", // name - (PyCFunction)PyShowAd2, // method - METH_VARARGS | METH_KEYWORDS, // flags - - "show_ad_2(purpose: str,\n" - " on_completion_call: Callable[[bool], None] | None = None)\n" - " -> None\n" - "\n" - "(internal)", -}; - // --------------------- set_party_icon_always_visible ------------------------- static auto PySetPartyIconAlwaysVisible(PyObject* self, PyObject* args, @@ -2658,74 +2614,6 @@ static PyMethodDef PyOpenURLDef = { "is True).", }; -// ------------------------- open_file_externally ------------------------------ - -static auto PyOpenFileExternally(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - - char* path = nullptr; - static const char* kwlist[] = {"path", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &path)) { - return nullptr; - } - g_base->platform->OpenFileExternally(path); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyOpenFileExternallyDef = { - "open_file_externally", // name - (PyCFunction)PyOpenFileExternally, // method - METH_VARARGS | METH_KEYWORDS, // flags - - "open_file_externally(path: str) -> None\n" - "\n" - "(internal)\n" - "\n" - "Open the provided file in the default external app.", -}; - -// ----------------------------- console_print --------------------------------- - -static auto PyConsolePrint(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - - if (!g_core->HeadlessMode()) { - Py_ssize_t tuple_size = PyTuple_GET_SIZE(args); - PyObject* obj; - for (Py_ssize_t i = 0; i < tuple_size; i++) { - obj = PyTuple_GET_ITEM(args, i); - PyObject* str_obj = PyObject_Str(obj); - if (!str_obj) { - PyErr_Clear(); // In case this is caught without setting the py exc. - throw Exception(); - } - const char* c = PyUnicode_AsUTF8(str_obj); - g_base->PushDevConsolePrintCall(c); - Py_DECREF(str_obj); - } - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyConsolePrintDef = { - "console_print", // name - PyConsolePrint, // method - METH_VARARGS, // flags - - "console_print(*args: Any) -> None\n" - "\n" - "(internal)\n" - "\n" - "Print the provided args to the game console (using str()).\n" - "For most debugging/info purposes you should just use Python's " - "standard\n" - "print, which will show up in the game console as well.", -}; - // ------------------------ is_party_icon_visible ------------------------------ static auto PyIsPartyIconVisible(PyObject* self) -> PyObject* { @@ -2803,18 +2691,12 @@ static PyMethodDef PyIsAvailableDef = { auto PythonMethodsUIV1::GetMethods() -> std::vector { return { - PyGetQRCodeTextureDef, PyIsPartyIconVisibleDef, - PyConsolePrintDef, - PyOpenFileExternallyDef, PyOpenURLDef, PyBackPressDef, PyGetSpecialWidgetDef, PySetPartyWindowOpenDef, PySetPartyIconAlwaysVisibleDef, - PyShowAdDef, - PyShowAd2Def, - PyShowOnlineScoreUIDef, PyButtonWidgetDef, PyCheckBoxWidgetDef, PyImageWidgetDef, @@ -2828,6 +2710,7 @@ auto PythonMethodsUIV1::GetMethods() -> std::vector { PyUIBoundsDef, PyGetSoundDef, PyGetTextureDef, + PyGetQRCodeTextureDef, PyGetMeshDef, PyToolbarTestDef, PyIsAvailableDef, diff --git a/src/ballistica/ui_v1/python/ui_v1_python.cc b/src/ballistica/ui_v1/python/ui_v1_python.cc index f12321b0..c1a014d7 100644 --- a/src/ballistica/ui_v1/python/ui_v1_python.cc +++ b/src/ballistica/ui_v1/python/ui_v1_python.cc @@ -110,28 +110,23 @@ void UIV1Python::InvokeStringEditor(PyObject* string_edit_adapter_instance) { base::ScopedSetContext ssc(nullptr); g_base->audio->PlaySound(g_base->assets->SysSound(base::SysSoundID::kSwish)); - // Schedule this in the next cycle to be safe. PythonRef args(Py_BuildValue("(O)", string_edit_adapter_instance), PythonRef::kSteal); - Object::New( - objs().Get(ObjID::kOnScreenKeyboardClass)) - ->Schedule(args); -} -void UIV1Python::LaunchStringEditOld(TextWidget* w) { - assert(g_base->InLogicThread()); - BA_PRECONDITION(w); + auto context_call = Object::New( + objs().Get(ObjID::kOnScreenKeyboardClass)); - base::ScopedSetContext ssc(nullptr); - g_base->audio->PlaySound(g_base->assets->SysSound(base::SysSoundID::kSwish)); - - // Gotta run this in the next cycle. - PythonRef args(Py_BuildValue("(Osi)", w->BorrowPyRef(), - w->description().c_str(), w->max_chars()), - PythonRef::kSteal); - Object::New( - objs().Get(ObjID::kOnScreenKeyboardClass)) - ->Schedule(args); + // This is probably getting called from within UI handling, so we + // need to schedule things to run post-ui-traversal in that case. + if (g_base->ui->InUIOperation()) { + context_call->ScheduleInUIOperation(args); + } else { + // Otherwise just run immediately. + Log(LogLevel::kWarning, + "UIV1Python::InvokeStringEditor running outside of UIInteraction; " + "unexpected."); + context_call->Run(args); + } } void UIV1Python::InvokeQuitWindow(QuitType quit_type) { diff --git a/src/ballistica/ui_v1/python/ui_v1_python.h b/src/ballistica/ui_v1/python/ui_v1_python.h index 734cd476..dadc4524 100644 --- a/src/ballistica/ui_v1/python/ui_v1_python.h +++ b/src/ballistica/ui_v1/python/ui_v1_python.h @@ -17,7 +17,6 @@ class UIV1Python { void AddPythonClasses(PyObject* module); void ImportPythonObjs(); - void LaunchStringEditOld(TextWidget* w); void InvokeStringEditor(PyObject* string_edit_adapter_instance); void HandleDeviceMenuPress(base::InputDevice* device); void ShowURL(const std::string& url); diff --git a/src/ballistica/ui_v1/ui_v1.cc b/src/ballistica/ui_v1/ui_v1.cc index cf8ffc9d..c7a3006f 100644 --- a/src/ballistica/ui_v1/ui_v1.cc +++ b/src/ballistica/ui_v1/ui_v1.cc @@ -6,6 +6,7 @@ #include "ballistica/base/graphics/component/empty_component.h" #include "ballistica/base/input/input.h" #include "ballistica/base/support/app_config.h" +#include "ballistica/shared/generic/native_stack_trace.h" #include "ballistica/ui_v1/python/ui_v1_python.h" #include "ballistica/ui_v1/support/root_ui.h" #include "ballistica/ui_v1/widget/root_widget.h" @@ -71,10 +72,6 @@ void UIV1FeatureSet::DoHandleDeviceMenuPress(base::InputDevice* device) { void UIV1FeatureSet::DoShowURL(const std::string& url) { python->ShowURL(url); } -// void UIV1FeatureSet::DoQuitWindow() { -// g_ui_v1->python->objs().Get(UIV1Python::ObjID::kQuitWindowCall).Call(); -// } - bool UIV1FeatureSet::MainMenuVisible() { // We consider anything on our screen or overlay stacks to be a 'main menu'. // Probably need a better name than 'main menu' though. @@ -296,7 +293,7 @@ UIV1FeatureSet::UILock::UILock(bool write) { assert(g_base->InLogicThread()); if (write && g_ui_v1->ui_lock_count_ != 0) { - BA_LOG_ERROR_TRACE_ONCE("Illegal operation: UI is locked"); + BA_LOG_ERROR_NATIVE_TRACE_ONCE("Illegal operation: UI is locked."); } g_ui_v1->ui_lock_count_++; } @@ -304,7 +301,7 @@ UIV1FeatureSet::UILock::UILock(bool write) { UIV1FeatureSet::UILock::~UILock() { g_ui_v1->ui_lock_count_--; if (g_ui_v1->ui_lock_count_ < 0) { - BA_LOG_ERROR_TRACE_ONCE("ui_lock_count_ < 0"); + BA_LOG_ERROR_NATIVE_TRACE_ONCE("ui_lock_count_ < 0"); g_ui_v1->ui_lock_count_ = 0; } } diff --git a/src/ballistica/ui_v1/widget/button_widget.cc b/src/ballistica/ui_v1/widget/button_widget.cc index 29c6db6a..e992098c 100644 --- a/src/ballistica/ui_v1/widget/button_widget.cc +++ b/src/ballistica/ui_v1/widget/button_widget.cc @@ -54,10 +54,18 @@ void ButtonWidget::SetIcon(base::TextureAsset* val) { icon_ = val; } void ButtonWidget::OnRepeatTimerExpired() { // Repeat our action unless we somehow lost focus but didn't get a mouse-up. if (IsHierarchySelected() && pressed_) { + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + DoActivate(true); // Speed up repeats after the first. repeat_timer_->SetLength(0.150); + + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); + } else { repeat_timer_.Clear(); } @@ -560,9 +568,8 @@ void ButtonWidget::DoActivate(bool is_repeat) { } } if (auto* call = on_activate_call_.Get()) { - // Call this in the next cycle (don't want to risk mucking with UI from - // within a UI loop.) - call->ScheduleWeak(); + // Schedule this to run immediately after any current UI traversal. + call->ScheduleInUIOperation(); return; } } diff --git a/src/ballistica/ui_v1/widget/button_widget.h b/src/ballistica/ui_v1/widget/button_widget.h index 116a6bcd..1928c41d 100644 --- a/src/ballistica/ui_v1/widget/button_widget.h +++ b/src/ballistica/ui_v1/widget/button_widget.h @@ -48,9 +48,9 @@ class ButtonWidget : public Widget { icon_color_alpha_ = a; } auto set_text_flatness(float f) { text_flatness_ = f; } - enum class Style { kRegular, kBack, kBackSmall, kTab, kSquare }; + enum class Style : uint8_t { kRegular, kBack, kBackSmall, kTab, kSquare }; auto set_style(Style s) { style_ = s; } - enum class IconType { kNone, kCancel, kStart }; + enum class IconType : uint8_t { kNone, kCancel, kStart }; void set_text(const std::string& text); auto text() const -> std::string { return text_->text_raw(); } auto set_icon_type(IconType i) { icon_type_ = i; } @@ -91,37 +91,32 @@ class ButtonWidget : public Widget { bool color_set_ = false; void DoActivate(bool is_repeat = false); auto GetMult(millisecs_t current_time) const -> float; - IconType icon_type_ = IconType::kNone; - bool enabled_ = true; - bool selectable_ = true; - float icon_tint_ = 0.0f; - Style style_ = Style::kRegular; - bool sound_enabled_ = true; - bool mouse_over_ = false; - bool repeat_ = false; - bool pressed_ = false; - float extra_touch_border_scale_ = 1.0f; - float width_ = 50.0f; - float height_ = 30.0f; - float text_scale_ = 1.0f; - float text_width_ = 0.0f; - float color_red_ = 0.5f; - float color_green_ = 0.7f; - float color_blue_ = 0.2f; - float icon_color_red_ = 1.0f; - float icon_color_green_ = 1.0f; - float icon_color_blue_ = 1.0f; - float icon_color_alpha_ = 1.0f; - Object::Ref texture_; - Object::Ref icon_; - Object::Ref tint_texture_; - Object::Ref mask_texture_; - Object::Ref mesh_transparent_; - Object::Ref mesh_opaque_; - float icon_scale_{1.0f}; + + IconType icon_type_{}; + Style style_{}; + bool enabled_{true}; + bool selectable_{true}; + bool sound_enabled_{true}; + bool mouse_over_{}; + bool repeat_{}; + bool pressed_{}; millisecs_t last_activate_time_millisecs_{}; millisecs_t birth_time_millisecs_{}; millisecs_t transition_delay_{}; + float icon_tint_{}; + float extra_touch_border_scale_{1.0f}; + float width_{50.0f}; + float height_{30.0f}; + float text_scale_{1.0f}; + float text_width_{0.0f}; + float color_red_{0.5f}; + float color_green_{0.7f}; + float color_blue_{0.2f}; + float icon_color_red_{1.0f}; + float icon_color_green_{1.0f}; + float icon_color_blue_{1.0f}; + float icon_color_alpha_{1.0f}; + float icon_scale_{1.0f}; float opacity_{1.0f}; float text_flatness_{0.5f}; float text_color_r_{0.75f}; @@ -134,11 +129,17 @@ class ButtonWidget : public Widget { float tint2_color_red_{1.0f}; float tint2_color_green_{1.0f}; float tint2_color_blue_{1.0f}; + Object::Ref texture_; + Object::Ref icon_; + Object::Ref tint_texture_; + Object::Ref mask_texture_; + Object::Ref mesh_transparent_; + Object::Ref mesh_opaque_; - // Keep these at the bottom, so they're torn down first. + // Keep these at the bottom so they're torn down first (this was a problem + // at some point though I don't remember details). Object::Ref text_; Object::Ref on_activate_call_; - // Object::Ref > repeat_timer_; Object::Ref repeat_timer_; }; diff --git a/src/ballistica/ui_v1/widget/check_box_widget.cc b/src/ballistica/ui_v1/widget/check_box_widget.cc index a6e47b3e..2159d048 100644 --- a/src/ballistica/ui_v1/widget/check_box_widget.cc +++ b/src/ballistica/ui_v1/widget/check_box_widget.cc @@ -245,9 +245,8 @@ void CheckBoxWidget::Activate() { PythonRef args(Py_BuildValue("(O)", checked_ ? Py_True : Py_False), PythonRef::kSteal); - // Call this in the next cycle (don't want to risk mucking with UI from - // within a UI loop) - call->ScheduleWeak(args); + // Schedule this to run immediately after any current UI traversal. + call->ScheduleInUIOperation(args); } } diff --git a/src/ballistica/ui_v1/widget/container_widget.cc b/src/ballistica/ui_v1/widget/container_widget.cc index 651c9866..8d315738 100644 --- a/src/ballistica/ui_v1/widget/container_widget.cc +++ b/src/ballistica/ui_v1/widget/container_widget.cc @@ -346,9 +346,8 @@ auto ContainerWidget::HandleMessage(const base::WidgetMessage& m) -> bool { } else if (auto* call = on_cancel_call_.Get()) { claimed = true; - // Call this in the next cycle (don't wanna risk mucking with UI from - // within a UI loop). - call->ScheduleWeak(); + // Schedule this to run immediately after any current UI traversal. + call->ScheduleInUIOperation(); } else { OnCancelCustom(); } @@ -630,9 +629,8 @@ auto ContainerWidget::HandleMessage(const base::WidgetMessage& m) -> bool { // Call our outside-click callback if unclaimed. if (!claimed && on_outside_click_call_.Exists()) { - // Call this in the next cycle (don't wanna risk mucking with UI from - // within a UI loop). - on_outside_click_call_->ScheduleWeak(); + // Schedule this to run immediately after any current UI traversal. + on_outside_click_call_->ScheduleInUIOperation(); } // Always claim if they want. @@ -1070,9 +1068,8 @@ void ContainerWidget::Activate() { last_activate_time_millisecs_ = static_cast(g_base->logic->display_time() * 1000.0); if (auto* call = on_activate_call_.Get()) { - // Call this in the next cycle (don't wanna risk mucking with UI from within - // a UI loop). - call->ScheduleWeak(); + // Schedule this to run immediately after any current UI traversal. + call->ScheduleInUIOperation(); } } diff --git a/src/ballistica/ui_v1/widget/h_scroll_widget.cc b/src/ballistica/ui_v1/widget/h_scroll_widget.cc index 40b832fc..7f0984d1 100644 --- a/src/ballistica/ui_v1/widget/h_scroll_widget.cc +++ b/src/ballistica/ui_v1/widget/h_scroll_widget.cc @@ -23,11 +23,17 @@ void HScrollWidget::OnTouchDelayTimerExpired() { if (touch_held_) { // Pass a mouse-down event if we haven't moved. if (!touch_is_scrolling_ && !touch_down_sent_) { + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + ContainerWidget::HandleMessage(base::WidgetMessage( base::WidgetMessage::Type::kMouseDown, nullptr, touch_x_, touch_y_, static_cast(touch_held_click_count_))); touch_down_sent_ = true; - } else { + + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); } } diff --git a/src/ballistica/ui_v1/widget/scroll_widget.cc b/src/ballistica/ui_v1/widget/scroll_widget.cc index 67ce6d2c..82d04bce 100644 --- a/src/ballistica/ui_v1/widget/scroll_widget.cc +++ b/src/ballistica/ui_v1/widget/scroll_widget.cc @@ -24,10 +24,17 @@ void ScrollWidget::OnTouchDelayTimerExpired() { if (touch_held_) { // Pass a mouse-down event if we haven't moved. if (!touch_is_scrolling_ && !touch_down_sent_) { + // Gather up any user code triggered by this stuff and run it at the end + // before we return. + base::UI::OperationContext ui_op_context; + ContainerWidget::HandleMessage(base::WidgetMessage( base::WidgetMessage::Type::kMouseDown, nullptr, touch_x_, touch_y_, static_cast(touch_held_click_count_))); touch_down_sent_ = true; + + // Run any calls built up by UI callbacks. + ui_op_context.Finish(); } } diff --git a/src/ballistica/ui_v1/widget/text_widget.cc b/src/ballistica/ui_v1/widget/text_widget.cc index c6c393a7..cd291550 100644 --- a/src/ballistica/ui_v1/widget/text_widget.cc +++ b/src/ballistica/ui_v1/widget/text_widget.cc @@ -590,9 +590,8 @@ void TextWidget::Activate() { static_cast(g_base->logic->display_time() * 1000.0); if (auto* call = on_activate_call_.Get()) { - // Call this in the next cycle (don't wanna risk mucking with UI from - // within a UI loop). - call->ScheduleWeak(); + // Schedule this to run immediately after any current UI traversal. + call->ScheduleInUIOperation(); } // Bring up an editor if applicable. @@ -719,9 +718,8 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { } else { if (auto* call = on_return_press_call_.Get()) { claimed = true; - // Call this in the next cycle (don't wanna risk mucking with UI - // from within a UI loop) - call->ScheduleWeak(); + // Schedule this to run immediately after any current UI traversal. + call->ScheduleInUIOperation(); } } break; diff --git a/src/ballistica/ui_v1/widget/widget.cc b/src/ballistica/ui_v1/widget/widget.cc index c94910a7..628af05e 100644 --- a/src/ballistica/ui_v1/widget/widget.cc +++ b/src/ballistica/ui_v1/widget/widget.cc @@ -19,12 +19,10 @@ Widget::~Widget() { Py_DECREF(py_ref_); } - // The very last thing we do is call our on-delete calls. - // We need to be prepared for anything happening as a result of this, - // so let's work off a copy of our callback list in case it gets mucked with. auto on_delete_calls = on_delete_calls_; for (auto&& i : on_delete_calls) { - i->Run(); + i->ScheduleInUIOperation(); + // i->Run(); } } @@ -83,12 +81,13 @@ auto Widget::IsInOverlayStack() const -> bool { } void Widget::SetSelected(bool s, SelectionCause cause) { - if (selected_ == s) return; + if (selected_ == s) { + return; + } selected_ = s; if (selected_ && on_select_call_.Exists()) { - // Call this in the next cycle (don't wanna risk mucking - // with UI from within a UI loop). - on_select_call_->ScheduleWeak(); + // Schedule this to run immediately after any current UI traversal. + on_select_call_->ScheduleInUIOperation(); } } diff --git a/tools/efrotools/openalbuild.py b/tools/efrotools/openalbuild.py index a53555b3..cfa7f225 100644 --- a/tools/efrotools/openalbuild.py +++ b/tools/efrotools/openalbuild.py @@ -31,6 +31,8 @@ def _build_dir(arch: str, mode: str) -> str: def build_openal(arch: str, mode: str) -> None: """Do the thing.""" + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals from efrotools import replace_exact if arch not in ARCHS: @@ -39,7 +41,11 @@ def build_openal(arch: str, mode: str) -> None: if mode not in MODES: raise CleanError(f"Invalid mode '{mode}'.") - # enable_oboe = True + # If true, we suppress most OpenAL logs to keep logcat tidier. + reduce_logs = False + + # Inject a function to reroute OpenAL logs to ourself. + reroute_logs = True # Get ndk path. ndk_path = ( @@ -60,9 +66,13 @@ def build_openal(arch: str, mode: str) -> None: ['git', 'clone', 'https://github.com/kcat/openal-soft.git', builddir], check=True, ) - # subprocess.run(['git', 'checkout', '1.23.1'], check=True, cwd=builddir) subprocess.run( - ['git', 'checkout', 'b81a270f6c1e795ca70d7684e0ccf35a19f247e2'], + [ + 'git', + 'checkout', + # '1.23.1', + '1381a951bea78c67281a2e844e6db1dedbd5ed7c', + ], check=True, cwd=builddir, ) @@ -87,59 +97,102 @@ def build_openal(arch: str, mode: str) -> None: # possible to filter by tag/level. However I'd prefer it to send # only the ones that it would send to stderr so I don't always have # to worry about filtering. - loggingpath = f'{builddir}/core/logging.cpp' - with open(loggingpath, encoding='utf-8') as infile: - txt = infile.read() + if reduce_logs or reroute_logs: + loggingpath = f'{builddir}/core/logging.cpp' + with open(loggingpath, encoding='utf-8') as infile: + txt = infile.read() + logcall = ( + '__android_log_print(android_severity(level),' + ' "openal", "%s", str);' + ) + condition = 'gLogLevel >= level' if reduce_logs else 'true' + if reroute_logs: + logcall = ( + 'if (alcCustomAndroidLogger) {\n' + ' alcCustomAndroidLogger(android_severity(level), str);\n' + '} else {\n' + f' {logcall}\n' + '}' + ) + txt = replace_exact( + txt, + ( + ' __android_log_print(android_severity(level),' + ' "openal", "%s", str);' + ), + ( + ' // ericf tweak; only send logs that meet some condition.\n' + f' if ({condition}) {{\n' + f' {logcall}\n' + ' }' + ), + ) + + # Note to self: looks like there's actually already a log + # redirect callback function in OpenALSoft, but it's not an + # official extension yet and I haven't been able to get it + # working in its current form. Ideally should adopt that + # eventually though. + if reroute_logs: + txt = replace_exact( + txt, + 'namespace {\n', + ( + 'extern void (*alcCustomAndroidLogger)(int, const char*);\n' + '\n' + 'namespace {\n' + ), + ) + with open(loggingpath, 'w', encoding='utf-8') as outfile: + outfile.write(txt) + + # Add a function to set a logging function so we can capture OpenAL + # logging. + fpath = f'{builddir}/alc/alc.cpp' + with open(fpath, encoding='utf-8') as infile: + txt = infile.read() txt = replace_exact( txt, - ' __android_log_print(android_severity(level),' - ' "openal", "%s", str);', - ' // ericf tweak; only send logs to' - ' android that we\'d send to stderr.\n' - ' if (gLogLevel >= level) {\n' - ' __android_log_print(android_severity(level),' - ' "openal", "%s", str);\n' - ' }', + 'ALC_API ALCenum ALC_APIENTRY' + ' alcGetError(ALCdevice *device) noexcept\n', + ( + 'void (*alcCustomAndroidLogger)(int, const char*) = nullptr;\n' + '\n' + 'ALC_API void ALC_APIENTRY' + ' alcSetCustomAndroidLogger(void (*fn)(int, const char*)) {\n' + ' alcCustomAndroidLogger = fn;\n' + '}\n' + '\n' + 'ALC_API ALCenum ALC_APIENTRY' + ' alcGetError(ALCdevice *device) noexcept\n' + ), ) - with open(loggingpath, 'w', encoding='utf-8') as outfile: + with open(fpath, 'w', encoding='utf-8') as outfile: outfile.write(txt) - # Add a function to set a logging function so we can gather info - # on AL fatal errors/etc. - # fpath = f'{builddir}/alc/alc.cpp' - # with open(fpath, encoding='utf-8') as infile: - # txt = infile.read() - # txt = replace_exact( - # txt, - # 'ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device)\n', - # ( - # 'void (*alcDebugLogger)(const char*) = nullptr;\n' - # '\n' - # 'ALC_API void ALC_APIENTRY' - # ' alcSetDebugLogger(void (*fn)(const char*)) {\n' - # ' alcDebugLogger = fn;\n' - # '}\n' - # '\n' - # 'ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device)\n' - # ), - # ) - # with open(fpath, 'w', encoding='utf-8') as outfile: - # outfile.write(txt) - - # fpath = f'{builddir}/include/AL/alc.h' - # with open(fpath, encoding='utf-8') as infile: - # txt = infile.read() - # txt = replace_exact( - # txt, - # 'ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device);\n', - # 'ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device);\n' - # 'ALC_API void ALC_APIENTRY alcSetDebugLogger(' - # 'void (*fn)(const char*));\n', - # ) - # with open(fpath, 'w', encoding='utf-8') as outfile: - # outfile.write(txt) + fpath = f'{builddir}/include/AL/alc.h' + with open(fpath, encoding='utf-8') as infile: + txt = infile.read() + txt = replace_exact( + txt, + ( + 'ALC_API ALCenum ALC_APIENTRY' + ' alcGetError(ALCdevice *device) ALC_API_NOEXCEPT;\n' + ), + ( + 'ALC_API ALCenum ALC_APIENTRY' + ' alcGetError(ALCdevice *device) ALC_API_NOEXCEPT;\n' + 'ALC_API void ALC_APIENTRY alcSetCustomAndroidLogger(' + 'void (*fn)(int, const char*));\n' + ), + ) + with open(fpath, 'w', encoding='utf-8') as outfile: + outfile.write(txt) + # Let's modify the try/catch around api calls so that we can catch + # and inspect exceptions thrown by them instead of it just resulting + # in an insta-terminate. fpath = f'{builddir}/core/except.h' with open(fpath, encoding='utf-8') as infile: txt = infile.read() @@ -151,18 +204,6 @@ def build_openal(arch: str, mode: str) -> None: txt = replace_exact( txt, '#define START_API_FUNC try\n', '#define START_API_FUNC\n' ) - # txt = replace_exact( - # txt, - # '#define END_API_FUNC catch(...) { std::terminate(); }\n', - # 'extern void (*alcDebugLogger)(const char*);\n' - # '\n' - # '#define END_API_FUNC catch(...) { \\\n' - # ' if (alcDebugLogger != nullptr) { \\\n' - # ' alcDebugLogger("UNKNOWN OpenALSoft fatal exception."); \\\n' - # ' } \\\n' - # ' std::terminate(); \\\n' - # '}\n' - # ) with open(fpath, 'w', encoding='utf-8') as outfile: outfile.write(txt)