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)