diff --git a/.efrocachemap b/.efrocachemap
index 2559c814..4008800b 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -1,5 +1,5 @@
{
- "ballisticakit-windows/Generic/BallisticaKit.ico": "be1b956dcd7f7a261b1afe5bce2a0336",
+ "ballisticakit-windows/Generic/BallisticaKit.ico": "6f33e74cb282f070871413f092983fcd",
"build/assets/ba_data/audio/achievement.ogg": "079a366ce183b25a63550ef7072af605",
"build/assets/ba_data/audio/actionHero1.ogg": "f0f986f268f036a5ac2f940e07f2f27e",
"build/assets/ba_data/audio/actionHero2.ogg": "204a6735dc655f0975cf8308b585f2fd",
@@ -421,42 +421,42 @@
"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": "992c5c5ce292132c4f011f39e0d13de8",
- "build/assets/ba_data/data/languages/arabic.json": "d1f900ab5aa2433d402bd46ed1149cc7",
- "build/assets/ba_data/data/languages/belarussian.json": "e151808b6b4f6dc159cf55ee62adad3c",
- "build/assets/ba_data/data/languages/chinese.json": "8d889accdd49334591209bdaf6eaf02f",
+ "build/assets/ba_data/data/langdata.json": "fae88cbb2a5b9c24096f2e43452114a2",
+ "build/assets/ba_data/data/languages/arabic.json": "0db32e21b6d5337ccca478381744aa88",
+ "build/assets/ba_data/data/languages/belarussian.json": "a112dfca3e188387516788bd8229c5b0",
+ "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": "93c5fe0d884d95435da6c675f64e30e0",
+ "build/assets/ba_data/data/languages/czech.json": "c9d518a324870066b987b8f412881dd3",
"build/assets/ba_data/data/languages/danish.json": "3fd69080783d5c9dcc0af737f02b6f1e",
"build/assets/ba_data/data/languages/dutch.json": "22b44a33bf81142ba2befad14eb5746e",
- "build/assets/ba_data/data/languages/english.json": "b38d54aecf3ac47b8d8ca97d8bab3006",
+ "build/assets/ba_data/data/languages/english.json": "2434e127b6788e3128d3523fcb1b8994",
"build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880",
- "build/assets/ba_data/data/languages/filipino.json": "347f38524816691170d266708fe25894",
- "build/assets/ba_data/data/languages/french.json": "d8527da977a563185de25ef02bacf826",
- "build/assets/ba_data/data/languages/german.json": "549754d2a530d825200c6126be56df5c",
- "build/assets/ba_data/data/languages/gibberish.json": "837423db378b3e7679683805826aa26e",
- "build/assets/ba_data/data/languages/greek.json": "a65d78f912e9a89f98de004405167a6a",
- "build/assets/ba_data/data/languages/hindi.json": "88ee0cda537bab9ac827def5e236fe1a",
+ "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": "e24d391c9fd12f9afa92f7ff65a06d23",
+ "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": "bff88ce57744a639810b93a1d1dd79f4",
- "build/assets/ba_data/data/languages/italian.json": "338e7a03dff47f4eefc0ca3a995cd4f4",
- "build/assets/ba_data/data/languages/korean.json": "ca1122a9ee551da3f75ae632012bd0e2",
+ "build/assets/ba_data/data/languages/indonesian.json": "408fb026e84c24a8dd7a43cb2b794541",
+ "build/assets/ba_data/data/languages/italian.json": "61c5308638bed44194f0ec24f19bf3cb",
+ "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": "71cc5b33abda0f285b970b8cc4a014a8",
- "build/assets/ba_data/data/languages/polish.json": "e1a1a801851924748ad38fa68216439a",
- "build/assets/ba_data/data/languages/portuguese.json": "9fcd6b4da9e5d0dc0e337ab00b5debe2",
+ "build/assets/ba_data/data/languages/persian.json": "4c3394f6662bb6dcf55728cfe213d750",
+ "build/assets/ba_data/data/languages/polish.json": "3a90b2d9e2c59305580c96f8098fc839",
+ "build/assets/ba_data/data/languages/portuguese.json": "0274cb9a4b7d2bd49c8eb8120144a1bf",
"build/assets/ba_data/data/languages/romanian.json": "aeebdd54f65939c2facc6ac50c117826",
- "build/assets/ba_data/data/languages/russian.json": "910cf653497654a16d5c4f067d6def22",
+ "build/assets/ba_data/data/languages/russian.json": "9d0b40586301a82e532c4a250d5f6d58",
"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": "0122b0b24aa111ab259af02bbae9b7b6",
- "build/assets/ba_data/data/languages/swedish.json": "77d671f10613291ebf9c71da66f18a18",
- "build/assets/ba_data/data/languages/tamil.json": "b9d4b4e107456ea6420ee0f9d9d7a03e",
- "build/assets/ba_data/data/languages/thai.json": "33f63753c9af9a5b238d229a0bf23fbc",
- "build/assets/ba_data/data/languages/turkish.json": "9d7e58c9062dc517c3779c255a9b3142",
- "build/assets/ba_data/data/languages/ukrainian.json": "f72eb51abfbbb56e27866895d7e947d2",
- "build/assets/ba_data/data/languages/venetian.json": "88595b7ee696b4094d7874c3c4188852",
+ "build/assets/ba_data/data/languages/spanish.json": "42f857c40dbd4b637e3866849489f7d1",
+ "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": "ab149ebbd57cf4daa3cf8f310d91519a",
+ "build/assets/ba_data/data/languages/ukrainian.json": "e5c861187c4c6db37d1a033f4ef3dd5a",
+ "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",
@@ -946,11 +946,11 @@
"build/assets/ba_data/meshes/zoeUpperArm.bob": "a8a881010ac1ee9ec5ca872d5c5e853a",
"build/assets/ba_data/meshes/zoeUpperLeg.bob": "95b2502f74c70f934927f67cd505c3ad",
"build/assets/ba_data/python-site-packages/_yaml/__init__.py": "b09d1968d73a04d6cf20e4e79657a6e7",
- "build/assets/ba_data/python-site-packages/certifi/__init__.py": "6337efa17f5b457b793332df33904162",
+ "build/assets/ba_data/python-site-packages/certifi/__init__.py": "b1fb6436db400125ecbb288262d00f0f",
"build/assets/ba_data/python-site-packages/certifi/__main__.py": "ef02e73f8581609df189a9f61aca365b",
- "build/assets/ba_data/python-site-packages/certifi/cacert.pem": "6ac29a6bccca11cd2ed7e16e27dfccec",
+ "build/assets/ba_data/python-site-packages/certifi/cacert.pem": "4422aed09ab445f7290df7d72a301a47",
"build/assets/ba_data/python-site-packages/certifi/core.py": "1b505388f1475fabd1b60031f985271c",
- "build/assets/ba_data/python-site-packages/typing_extensions.py": "084d93bb609d798a3930dfb5e25eba59",
+ "build/assets/ba_data/python-site-packages/typing_extensions.py": "2d974cad17a71505d86513d1322976a5",
"build/assets/ba_data/python-site-packages/yaml/__init__.py": "2b747e5772c203377222afc888ac6b71",
"build/assets/ba_data/python-site-packages/yaml/composer.py": "cef871e1f5f99ba2a7c44941b70afb06",
"build/assets/ba_data/python-site-packages/yaml/constructor.py": "8a15e361e34b79491c81553bb3534062",
@@ -1444,6 +1444,10 @@
"build/assets/ba_data/textures/discordLogo.ktx": "d56ab6389e2770e0601d87f99375d7a5",
"build/assets/ba_data/textures/discordLogo.pvr": "bd39785cf2cbf9bc41fdde8b86ab3310",
"build/assets/ba_data/textures/discordLogo_preview.png": "90efd54d3abd371c0150a363f3f673e7",
+ "build/assets/ba_data/textures/discordServer.dds": "782f63ce8bcf0de2c338ee1fbfb5df2e",
+ "build/assets/ba_data/textures/discordServer.ktx": "09c969fd0a278b5c426344845ef07c43",
+ "build/assets/ba_data/textures/discordServer.pvr": "54216467f9527c3fc2d8c777811e97a7",
+ "build/assets/ba_data/textures/discordServer_preview.png": "99cf0a7f77b909e9079d423a687e399f",
"build/assets/ba_data/textures/doomShroomBGColor.dds": "628100f564789f57e22f2ce0dcf53b76",
"build/assets/ba_data/textures/doomShroomBGColor.ktx": "ffbb38af60ab59e6030b01cd129bfaf9",
"build/assets/ba_data/textures/doomShroomBGColor.pvr": "77d103392813e302cdaf631cef88ba36",
@@ -1524,9 +1528,9 @@
"build/assets/ba_data/textures/fontBig.ktx": "94b56c2488d6c9ebabfbbb740eca07dd",
"build/assets/ba_data/textures/fontBig.pvr": "dff3f6c04a8c7b0bb937001640b42c8d",
"build/assets/ba_data/textures/fontBig_preview.png": "f8b15cb04f0deca7774def335a72f053",
- "build/assets/ba_data/textures/fontExtras.dds": "7ab11df1b3a3daa651dfad34219b89f5",
- "build/assets/ba_data/textures/fontExtras.ktx": "30c3c8ca2cdf1209ff177017bb10f0a8",
- "build/assets/ba_data/textures/fontExtras.pvr": "fd3b0bd902c30e4b7aa5fe00e1eec4be",
+ "build/assets/ba_data/textures/fontExtras.dds": "0a5a39028853c443cd88bc2492cb6ad9",
+ "build/assets/ba_data/textures/fontExtras.ktx": "5b14075ce3d1d29c6d5635602e2176d8",
+ "build/assets/ba_data/textures/fontExtras.pvr": "8cc68ca85ba327c20c45bad73b000d8c",
"build/assets/ba_data/textures/fontExtras2.dds": "18063a12912dadc9528afd90d1cf2369",
"build/assets/ba_data/textures/fontExtras2.ktx": "36da7f6cfbfb8d32fb14371de0a8f660",
"build/assets/ba_data/textures/fontExtras2.pvr": "7a4e8e64ac05313b1782fb5b958150d0",
@@ -1539,7 +1543,7 @@
"build/assets/ba_data/textures/fontExtras4.ktx": "6d872ac15e2e874c1252f63b4584722b",
"build/assets/ba_data/textures/fontExtras4.pvr": "6a0a0a1a8bbbc3ee9d6b8b914e7aa697",
"build/assets/ba_data/textures/fontExtras4_preview.png": "363e2647621917b3821c9068267d2516",
- "build/assets/ba_data/textures/fontExtras_preview.png": "9c9c58aff612e7b6386f3522c0b4f1f6",
+ "build/assets/ba_data/textures/fontExtras_preview.png": "b6503267cc15e9e2524f41fabd94e773",
"build/assets/ba_data/textures/fontSmall0.dds": "b30bfe5f9e436be7be8b5eae6e8490c3",
"build/assets/ba_data/textures/fontSmall0.ktx": "7e6058f37e6c5a4ea628f35b5f92c227",
"build/assets/ba_data/textures/fontSmall0.pvr": "c66e3d6aa1f7def83aaacd8a6c9185e5",
@@ -2586,21 +2590,21 @@
"build/assets/pylib-android/_pyio.py": "a6e88d66fbca88b13213cdd2177390b8",
"build/assets/pylib-android/_sitebuiltins.py": "8b5e3f6e73917962fa014ad2c4a55e61",
"build/assets/pylib-android/_strptime.py": "ff699c3f7647db7621bb88c43cc282d3",
- "build/assets/pylib-android/_sysconfigdata__linux_aarch64-linux-android.py": "cb9a77b04173c8776365999b57186e36",
- "build/assets/pylib-android/_sysconfigdata__linux_arm-linux-androideabi.py": "6d50596ec7f4858a0c6a5edefde21f7a",
- "build/assets/pylib-android/_sysconfigdata__linux_i686-linux-android.py": "bf9358a2243aa7884b8e80d85c969fa5",
- "build/assets/pylib-android/_sysconfigdata__linux_x86_64-linux-android.py": "45eee0efbc2441535b94a8ad5acf4d2e",
- "build/assets/pylib-android/_sysconfigdata_d_linux_aarch64-linux-android.py": "f8ff271cf6df0b5b4d46d9c548abb84e",
- "build/assets/pylib-android/_sysconfigdata_d_linux_arm-linux-androideabi.py": "da171b290c06a34d6a5cfbb296c22c34",
- "build/assets/pylib-android/_sysconfigdata_d_linux_i686-linux-android.py": "dde2516b5ac29412dfbebaa7b3de0d0d",
- "build/assets/pylib-android/_sysconfigdata_d_linux_x86_64-linux-android.py": "7df452144c6630afb96951487c1257a0",
+ "build/assets/pylib-android/_sysconfigdata__linux_aarch64-linux-android.py": "b1a9ca985ff6a159aa5ef94abd287f46",
+ "build/assets/pylib-android/_sysconfigdata__linux_arm-linux-androideabi.py": "21a5842f39c86fccaaa0a30e0e4ab347",
+ "build/assets/pylib-android/_sysconfigdata__linux_i686-linux-android.py": "9349023049d7599da61456b3f9a9687b",
+ "build/assets/pylib-android/_sysconfigdata__linux_x86_64-linux-android.py": "4151fa62c11c32cddf538e5cc7647160",
+ "build/assets/pylib-android/_sysconfigdata_d_linux_aarch64-linux-android.py": "d9f7f1d3f5b89b08150dfa00cf243901",
+ "build/assets/pylib-android/_sysconfigdata_d_linux_arm-linux-androideabi.py": "f4b99d4501a1cf1eb20fbc8973fa0040",
+ "build/assets/pylib-android/_sysconfigdata_d_linux_i686-linux-android.py": "454094da5fe52a969b53bb46d360da84",
+ "build/assets/pylib-android/_sysconfigdata_d_linux_x86_64-linux-android.py": "1e23f45f4243c1aacc83f23ad5852390",
"build/assets/pylib-android/_threading_local.py": "4a9688e3987d7d692db46feb9214945e",
"build/assets/pylib-android/_weakrefset.py": "e4fa8532ace46dfbc35149c41ea497f7",
"build/assets/pylib-android/abc.py": "a0daa1ed187eee8690c1e8438b97da90",
"build/assets/pylib-android/aifc.py": "1b9134c72b1e542417bee5bf345a1d0a",
"build/assets/pylib-android/antigravity.py": "6d56bedf73be574cb6d7117caf5d334c",
"build/assets/pylib-android/argparse.py": "e22cac9b12c09592929d57eb982fc554",
- "build/assets/pylib-android/ast.py": "3aaa1b0e56b21b28155707c54bc225a8",
+ "build/assets/pylib-android/ast.py": "f287ccaa1cd7cb0ea256e3984fd4ce4d",
"build/assets/pylib-android/asynchat.py": "2ef3a0ce322332fabbf8fad4e133c6a3",
"build/assets/pylib-android/asyncio/__init__.py": "edf0e79e2b8b85c08f09fd14668e4822",
"build/assets/pylib-android/asyncio/__main__.py": "8e391b47f448ad922dc2614dbd93011e",
@@ -2624,10 +2628,10 @@
"build/assets/pylib-android/asyncio/selector_events.py": "a108fbd3a49f967da245f39cebf7694e",
"build/assets/pylib-android/asyncio/sslproto.py": "2ec1b21e523055147d94c8c634154aab",
"build/assets/pylib-android/asyncio/staggered.py": "f5056f0a56b73b477a9fa65e71145366",
- "build/assets/pylib-android/asyncio/streams.py": "f00ddd2b2fd74554ae1d3088bd9d2bfd",
- "build/assets/pylib-android/asyncio/subprocess.py": "edb8d98278300b6c99f36cd08643c743",
+ "build/assets/pylib-android/asyncio/streams.py": "8cc026c067fc9245568199ea659167df",
+ "build/assets/pylib-android/asyncio/subprocess.py": "46e8b0ba32b4ac7bb5f840c49c89c85a",
"build/assets/pylib-android/asyncio/taskgroups.py": "5162e5b1806d9b647383d34ba1b21b56",
- "build/assets/pylib-android/asyncio/tasks.py": "234550593cd4928e6ee2c9591b6928ca",
+ "build/assets/pylib-android/asyncio/tasks.py": "c1bc59c01792bac43b79b425bb61e10e",
"build/assets/pylib-android/asyncio/threads.py": "7bbf81d424901524510e07b5d20e4a50",
"build/assets/pylib-android/asyncio/timeouts.py": "c7cb81c7ee938bc47ff75342befc872a",
"build/assets/pylib-android/asyncio/transports.py": "04598090d813bb363cea9bf714b97c3f",
@@ -2641,13 +2645,13 @@
"build/assets/pylib-android/bisect.py": "9b70437e327d5176da41192567ad0064",
"build/assets/pylib-android/bz2.py": "cd6a5f2491bc52afd8fc180097371473",
"build/assets/pylib-android/cProfile.py": "9e9c07ac3b9e4195a62b74e4f2b9489f",
- "build/assets/pylib-android/calendar.py": "4ef3d6d85d44e36212e5d784051c80b6",
+ "build/assets/pylib-android/calendar.py": "18df862e8e3c3fcbe4ab8a0b0348e339",
"build/assets/pylib-android/cgi.py": "090c5cfc8b4b92a730beec975159bd2a",
"build/assets/pylib-android/cgitb.py": "2bcff1cec7f3a3a9c96de7a55ebb4ea3",
"build/assets/pylib-android/chunk.py": "13d7633b1ff28f5aed4eb043c65c99c5",
"build/assets/pylib-android/cmd.py": "8befee2654b0954af7886e24e2e7871f",
"build/assets/pylib-android/code.py": "5d47099984013b933c96b02ef16981b8",
- "build/assets/pylib-android/codecs.py": "6fac5e2969e98ceaba92d3b8e42cb2ec",
+ "build/assets/pylib-android/codecs.py": "e11eabe4824899dea4b26a89a568a361",
"build/assets/pylib-android/codeop.py": "d375467fb29fccd43ab94d15a2e63085",
"build/assets/pylib-android/collections/__init__.py": "dcffbb6ee2cadd0c05ad22f2ef41f89b",
"build/assets/pylib-android/collections/abc.py": "15f410d3821352033a90a04539c99060",
@@ -2656,7 +2660,7 @@
"build/assets/pylib-android/concurrent/__init__.py": "aa990702e8f3a7af205efb5ae23a7c85",
"build/assets/pylib-android/concurrent/futures/__init__.py": "3e46fadb9de9c995c37dca4311641d6a",
"build/assets/pylib-android/concurrent/futures/_base.py": "a1cd37aea6fe0efff1bc00a39543609e",
- "build/assets/pylib-android/concurrent/futures/process.py": "a44e8618e158f8f351dafcb566a02544",
+ "build/assets/pylib-android/concurrent/futures/process.py": "1d1bb7b14e3999b383ba8bd11aa8951c",
"build/assets/pylib-android/concurrent/futures/thread.py": "e63753b8201f1392dbebc84a15054a13",
"build/assets/pylib-android/configparser.py": "914afd2b2cec90bbca0b94fd176b5176",
"build/assets/pylib-android/contextlib.py": "6f52eac914e438ef54407760def8305f",
@@ -2680,7 +2684,7 @@
"build/assets/pylib-android/curses/panel.py": "8f36fdade9588f8a4362d2cc057a6eff",
"build/assets/pylib-android/curses/textpad.py": "94aa9ebc47a6068d4461652346646dbb",
"build/assets/pylib-android/dataclasses.py": "febeea138bff21dbed88762be772514e",
- "build/assets/pylib-android/datetime.py": "5dcfd7f3b1a4db8214c1442164ac999c",
+ "build/assets/pylib-android/datetime.py": "521d6767afcfef887ac4c3719386b8fd",
"build/assets/pylib-android/decimal.py": "f57d255d45b5d1d7d8e13c41a283c3e4",
"build/assets/pylib-android/difflib.py": "6b3c8fd541b2b8d0320727025cd25275",
"build/assets/pylib-android/dis.py": "cecdc0c02aa3d70a7f550e60ebc9b3ba",
@@ -2836,7 +2840,7 @@
"build/assets/pylib-android/encodings/utf_8_sig.py": "8f3542863ef311d8b970a37c0d66b0de",
"build/assets/pylib-android/encodings/uu_codec.py": "4ef8a65413574c017a96b97fc1638ba6",
"build/assets/pylib-android/encodings/zlib_codec.py": "1388fb103fdf395451bfc8a2d60933a9",
- "build/assets/pylib-android/enum.py": "73b214a43ceef88aff7098b83623ed09",
+ "build/assets/pylib-android/enum.py": "e2a5734675e418870d7b379b5dba1ed3",
"build/assets/pylib-android/filecmp.py": "7648fdc6d0fc8bae7429d5e4081cf353",
"build/assets/pylib-android/fileinput.py": "c3def1041e6b12dd5f1906c9dbbd1101",
"build/assets/pylib-android/fnmatch.py": "a1bc67633695d4defd4c0886428c5363",
@@ -2916,7 +2920,7 @@
"build/assets/pylib-android/optparse.py": "5f65f891612b68c71a2846da86254285",
"build/assets/pylib-android/os.py": "36f9692131ffb9ba4db510de31afc651",
"build/assets/pylib-android/pathlib.py": "095ec821fec243124d0a286b4de3848a",
- "build/assets/pylib-android/pdb.py": "117b0d24ccb89edc5f183c94f6722f70",
+ "build/assets/pylib-android/pdb.py": "c44527d9e905ca3b1b45d3c158df730a",
"build/assets/pylib-android/pickle.py": "e6f9f53d29988454690ccde3279c7c38",
"build/assets/pylib-android/pickletools.py": "85b30fba86d32dfc4a588300dedf5f01",
"build/assets/pylib-android/pipes.py": "2dd796bdbb87982034234fec50d4526c",
@@ -2945,10 +2949,10 @@
"build/assets/pylib-android/runpy.py": "3a2dd98314791c7e36b6bd3585f6ad82",
"build/assets/pylib-android/sched.py": "f5579c8c711dd3e89da70ec9e1788c9c",
"build/assets/pylib-android/secrets.py": "bbf9ed672044ef3ab4b83ca2aea1644e",
- "build/assets/pylib-android/selectors.py": "98e0d83849452cbc2cc1381555bd5024",
+ "build/assets/pylib-android/selectors.py": "3c94b3b678c473543cdc7f1d2b20a6f6",
"build/assets/pylib-android/shelve.py": "3e569c07c863ecbd7f35a6c382d1785a",
"build/assets/pylib-android/shlex.py": "0873fac90a491702950816ead0e59dd0",
- "build/assets/pylib-android/shutil.py": "a5d0ee9f28244b42a06e682312d0e3fa",
+ "build/assets/pylib-android/shutil.py": "aa636d67785c2e92d34c7c5c81f9e8c5",
"build/assets/pylib-android/signal.py": "114ef47b1798fca6f56ac8a250974b3e",
"build/assets/pylib-android/site.py": "2a99f7de2702aa8411d35acbb91fe926",
"build/assets/pylib-android/smtpd.py": "0602b6a39c4e37133303bee16c3e28a4",
@@ -2958,7 +2962,7 @@
"build/assets/pylib-android/socketserver.py": "98e33643181a54765e6d0b9e01b03d53",
"build/assets/pylib-android/sqlite3/__init__.py": "8838d75ad0e465e25bb0c8dfeab7a9ab",
"build/assets/pylib-android/sqlite3/dbapi2.py": "c85f3ff9ddbd56683a8c801885dc5e53",
- "build/assets/pylib-android/sqlite3/dump.py": "8364bd18be01acf7e56e168db98c0e6f",
+ "build/assets/pylib-android/sqlite3/dump.py": "8d2085ec40031d544694759608e53178",
"build/assets/pylib-android/sre_compile.py": "a1784e9ccbea7d9963cab75b536b40c8",
"build/assets/pylib-android/sre_constants.py": "5c5be32a5334d9b0a848dad520746a63",
"build/assets/pylib-android/sre_parse.py": "cca15b9ab31509e6642f9d2fd4fb9d91",
@@ -2978,8 +2982,8 @@
"build/assets/pylib-android/tempfile.py": "436007fbe6821c864a53861bd73b4d43",
"build/assets/pylib-android/textwrap.py": "3eb16a40553205dc96be5cb9039f3c8c",
"build/assets/pylib-android/this.py": "8b0a9a1fa0a45a37e6c656eca1922277",
- "build/assets/pylib-android/threading.py": "dda98a9e1169adb496655300454ecc09",
- "build/assets/pylib-android/timeit.py": "8dc6f4245abf1d44814745e22a2f78b1",
+ "build/assets/pylib-android/threading.py": "3354bf0cad72286a0532b0754de78704",
+ "build/assets/pylib-android/timeit.py": "c918c7dee7538ff6e5a92288f55b4327",
"build/assets/pylib-android/token.py": "d8ff4e6c8eb59896891d01148f481e27",
"build/assets/pylib-android/tokenize.py": "3056f048c07e6c5a6442a5ef4f38e54c",
"build/assets/pylib-android/tomllib/__init__.py": "253ecf9dd67cb81a3e19911a4a39f930",
@@ -2987,7 +2991,7 @@
"build/assets/pylib-android/tomllib/_re.py": "0e509117e16c41c491615e06bb98861d",
"build/assets/pylib-android/tomllib/_types.py": "07be9616d6f5e401fd31fbeea619fc97",
"build/assets/pylib-android/trace.py": "3d8698a2c3ec03dc0f394a2f48c2ffbc",
- "build/assets/pylib-android/traceback.py": "91f67818e621e3b2f5bf583ed6863ef8",
+ "build/assets/pylib-android/traceback.py": "668bd36fc103a89554d2f9202a07f56d",
"build/assets/pylib-android/tracemalloc.py": "e4d10d2bee7773566e46797a939e5cbf",
"build/assets/pylib-android/tty.py": "271c7d61005a0a3c2c0952efc60dcb6d",
"build/assets/pylib-android/types.py": "78f8942c08dbfc9c582f1bb8d5206639",
@@ -4056,59 +4060,59 @@
"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": "43a2b924e39a8ec1c1c4dc29c4dd82a1",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "870c3973e3f84ca977496f79706ceab1",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "49251d9bf7583a51e7e28cf6b62ff357",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "23068b129ac54e4f24c96aa154f0bb4d",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "045814b1c6e365ca5ac73883bbd9f22d",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "52d8664a3ed40b0715fa5097005f58da",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "4f364467c2f636a83a04f4f2930b34ee",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "9b2afa754ae00d687750b243ee977312",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "182c54fa13364049339aedfa67cbfcba",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "341976d8d665a94bf20a685aace37be5",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "b60fe7cb522e9aa3969c7e92af06d14e",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "e03943051877ae0105c7526b5094c67f",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "bfc7e631e7319b0749912e40b4bdbe88",
- "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "d30e8266191cfd80f108129086f83b87",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "bcf27351436cb58df186612b82be983b",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "39f8ce569f1faed09bd4189e291a91bd",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "71b1c542f48d5abae5fb790b521a156a",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "553f71787d119cb01046f247711424b5",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "12cd08021ae0ed1ce4238c1adead61dc",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "b7e08e6b9f76def9b61e4839b2682497",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "58946f3534363d88f713c54d3d643d6d",
- "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "be356d05ecccd68043258d87b1892805",
- "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "58946f3534363d88f713c54d3d643d6d",
- "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "be356d05ecccd68043258d87b1892805",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "bf7d793d62416db7273590a796001cb6",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "b309e0cc3ec04024712c4ca938efdb92",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "bf7d793d62416db7273590a796001cb6",
- "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "b309e0cc3ec04024712c4ca938efdb92",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "a86b09c31abf0b5ec934ef28c8bd9fa3",
- "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "87be7a2f6e83c495f99024bb68660e17",
- "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "a86b09c31abf0b5ec934ef28c8bd9fa3",
- "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "87be7a2f6e83c495f99024bb68660e17",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "9fb5d3cb36dd53bd18c7ca831e7c73ee",
- "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "88332859e6e9ee70848f5252e5ee6ce0",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "55b6db8700acfc573cc3db31c6b210f7",
- "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "88332859e6e9ee70848f5252e5ee6ce0",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "cf40ba3bce2391e82978b08785405a5e",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "a8a74156e04932a2a5cc6d2d4b202acf",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "a135f9210a1c3be6b5d5d8228c8f6184",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "e412e20e4a0ac33b9f83c7750cde7109",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "cfcae11dab1c6752f821f0816706fa47",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "16daa37287a6d9d3404461da8565aadb",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "b3faf8b8925145f121b09e67d6114fb8",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "ed7ec02978df94f92168c5990cb6c78c",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "5f7e668a6a904ba8c0fe391654a8211b",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "77cd63dbc1760a3d1bb55753794701b0",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "9f674a5b767f798b350ef30d84968d44",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "84546f0452561d3f6519d296a9e9b2ca",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "3122dbce63fb248c9ebe66d545be8480",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "4004c01a3ea1a7b62544f08f14e76f9e",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "cd9b5b9ae925cef7417d2b86fd0f0489",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "35c2b307e5d228b105315b8d022d31d2",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "90993c873a2d1ca7b593992e52b1865a",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "46119cb9ff99629ca16471f51032bac7",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "8cac6eff8eb4f9e453a669b6f239b8a5",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "3e20c91bf8948354a0e167faa5c184e9",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "52ff216e1b0394a07bf178b8eea8c269",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "32d85cc58ade8e7a63dfb604ef175553",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "fdf3da5251bc0d3b8f4e5a5b2e0d4b94",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "c03afc18b7aa72ab065381be985f48ae",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "67a58d483ac028f9fc59112b59463a99",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "0800d71a41320664bacba49822e8b442",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "9019d9b48cefa45a7a16bae6bd696896",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "90eca6cc81b0a2c39fd1d41d0d029f04",
+ "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": "ff81e6eeea861f59e71db628bc64918b",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "ba89e5949d1cdf2b857089feb901285c",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "5e063f8acc0e0e9f35f82480d7dbf143",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "1f0d14fcc16dd0d4896d91c75e32be25",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "251d3dd0bc9a6418eb1cb5176bb5509c",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "96c003edb87b3f506d1b15af461487c3",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "274ebd634f05b23653719ef973119cf5",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "796edace73f874ebf46054b2a1ff0ba1",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "28323912b56ec07701eda3d41a6a4101",
- "src/ballistica/base/mgen/pyembed/binding_base.inc": "ba8ce3ca3858b4c2d20db68f99b788b2",
- "src/ballistica/base/mgen/pyembed/binding_base_app.inc": "00f81f9bd92386ec12a6e60170678a98",
+ "src/ballistica/base/mgen/pyembed/binding_base.inc": "72bfed2cce8ff19741989dec28302f3f",
+ "src/ballistica/base/mgen/pyembed/binding_base_app.inc": "97efb93f4bfd8e8b09f2db24398e29fc",
"src/ballistica/classic/mgen/pyembed/binding_classic.inc": "3ceb412513963f0818ab39c58bf292e3",
"src/ballistica/core/mgen/pyembed/binding_core.inc": "9d0a3c9636138e35284923e0c8311c69",
"src/ballistica/core/mgen/pyembed/env.inc": "8be46e5818f360d10b7b0224a9e91d07",
"src/ballistica/core/mgen/python_modules_monolithic.h": "fb967ed1c7db0c77d8deb4f00a7103c5",
- "src/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc": "d80f970053099b3044204bfe29ddefce",
+ "src/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc": "c25b263f2a31fb5ebe057db07d144879",
"src/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc": "44a45492db057bf7f7158c3b0fa11f0f",
- "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "8f4c2070174bdc2fbf735180394d7b3a"
+ "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "f5f054050d2b2fcd3763a4833fb32269"
}
\ No newline at end of file
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
new file mode 100644
index 00000000..ebeea7ef
--- /dev/null
+++ b/.github/workflows/cd.yml
@@ -0,0 +1,178 @@
+name: CD
+
+on:
+ # Run on pushes and pull-requests
+ push:
+ pull_request:
+
+jobs:
+ make_linux_x86_64_gui_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-gui-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: linux_x86_64_gui_(debug)
+ path: build/prefab/full/linux_x86_64_gui
+ make_linux_x86_64_server_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-server-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: linux_x86_64_server_(debug)
+ path: build/prefab/full/linux_x86_64_server
+ make_linux_arm64_gui_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-linux-arm64-gui-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: linux_arm64_gui_(debug)
+ path: build/prefab/full/linux_arm64_gui
+ make_linux_arm64_server_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-linux-arm64-server-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: linux_arm64_server_(debug)
+ path: build/prefab/full/linux_arm64_server
+ make_mac_x86_64_gui_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-mac-x86-64-gui-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: mac_x86_64_gui_(debug)
+ path: build/prefab/full/mac_x86_64_gui
+ make_mac_x86_64_server_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-mac-x86-64-server-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: mac_x86_64_server_(debug)
+ path: build/prefab/full/mac_x86_64_server
+ make_mac_arm64_gui_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-mac-arm64-gui-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: mac_arm64_gui_(debug)
+ path: build/prefab/full/mac_arm64_gui
+ make_mac_arm64_server_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-mac-arm64-server-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: mac_arm64_server_(debug)
+ path: build/prefab/full/mac_arm64_server
+ make_windows_x86_gui_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-windows-x86-gui-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: windows_x86_gui_(debug)
+ path: build/prefab/full/windows_x86_gui
+ make_windows_x86_server_debug_build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+ - name: Install pip requirements
+ run: tools/pcommand install_pip_reqs
+ - name: Make the build
+ run: make prefab-windows-x86-server-debug-build
+ - name: Upload the build
+ uses: actions/upload-artifact@v3
+ with:
+ name: windows_x86_server_(debug)
+ path: build/prefab/full/windows_x86_server
diff --git a/.gitignore b/.gitignore
index 1c856436..273c7b58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -120,10 +120,10 @@ xcuserdata/
/ballisticakit-android/BallisticaKit/src/main/res/mipmap-*/ic_launcher*.png
/ballisticakit-android/BallisticaKit/src/cardboard/res/mipmap-*/ic_launcher*.png
BallisticaKit.ico
-/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/Cursor macOS.appiconset/cursor_*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/AppIcon iOS.appiconset/icon_*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/AppIcon macOS.appiconset/icon_*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Layer*.imagestacklayer/Content.imageset/*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Layer*.imagestacklayer/Content.imageset/*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/*.png
/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/*.png
+/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/Cursor macOS.imageset/cursor_*.png
diff --git a/.idea/ballisticakit.iml b/.idea/ballisticakit.iml
index 45327b93..6d3ca9f0 100644
--- a/.idea/ballisticakit.iml
+++ b/.idea/ballisticakit.iml
@@ -21,7 +21,6 @@
-
@@ -74,4 +73,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 0143897a..7326dccb 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -4,10 +4,10 @@
-
+
-
\ No newline at end of file
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4327b5d4..d84b9b62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,19 +1,128 @@
-### 1.7.28 (build 21445, api 8, 2023-10-11)
+### 1.7.33 (build 21743, api 8, 2023-12-21)
+### 1.7.32 (build 21741, api 8, 2023-12-20)
+- Fixed a screen message that no one will ever see (Thanks vishal332008?...)
+- Plugins window now displays 'No Plugins Installed' when no plugins are present (Thanks vishal332008!)
+- Old messages are now displayed as soon as you press 'Unmute Chat' (Thanks vishal332008!)
+- Added an 'Add to Favorites' entry to the party menu (Thanks vishal332008!)
+- Now displays 'No Parties Added' in favorites tab if no favorites are present (Thanks vishal332008!)
+- Now shows character icons in the profiles list window (Thanks vishal332008!)
+- Added a Random button for names in the Player Profiles window (Thanks vishal332008!)
+- Fixed a bug where no server is selected by default in the favorites tab (Thanks vishal332008!)
+- Fixed a bug where no replay is selected by default in the watch tab (Thanks vishal332008!)
+- Fixed a bug where no profile is selected by default in the profile tab (Thanks vishal332008!)
+- Fixed a number of UI screens so that ugly window edges are no longer visible
+ in corners on modern ultra wide phone displays.
+- Added a `player_rejoin_cooldown` server config option. This defaults to 10
+ seconds for servers but 0 for normal gui clients. This mechanism had been
+ introduced recently to combat multiplayer fast-rejoin exploits and was set to
+ 10 seconds everywhere, but it could tend to be annoying for local single
+ player play, dev testing, etc. Hopefully this strikes a good balance now.
+- Removed the player-rejoin-cooldown mechanism from the C++ layer since it was
+ redundant with the Python level one and didn't cover as many cases.
+- Restored the behavior from before 1.7.28 where backgrounding the app would
+ bring up the main menu and pause the action. Now it is implemented more
+ cleanly however (an `on_app_active_changed()` call in the `AppMode` class).
+ This means that it also applies to other platforms when the app reaches the
+ 'inactive' state; for instance when minimizing the window on the SDL build.
+
+### 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.
+- Got the Android version back up and running. There's been lots of cleanup and
+ simplification on the Android layer, cleaning out years of cruft. This should
+ put things in a better more maintainable place, but there will probably be
+ some bugs to iron out, so please holler if you run into any.
+- Minimum supported Android version has been bumped from 5.0 to 6.0. Some
+ upcoming tech such as ASTC textures will likely not be well supported on such
+ old devices, so I think it is better to leave them running an older version
+ that performs decently instead of a newer version that performs poorly. And
+ letting go of old Android versions lets us better support new ones.
+- Android version now uses the 'Oboe' library as an audio back-end instead of
+ OpenSL. This should result in better behaving audio in general. Please holler
+ if you experience otherwise.
+- Bundled Android Python has been bumped to version 3.11.6.
+- Android app suspend behavior has been revamped. The app should stay running
+ more often and be quicker to respond when dialogs or other activities
+ temporarily pop up in front of it. This also allows it to continue playing
+ music over other activities such as Google Play Games
+ Achievements/Leaderboards screens. Please holler if you run into strange side
+ effects such as the app continuing to play audio when it should not be.
+- Modernized the Android fullscreen setup code when running in Android 11 or
+ newer. The game should now use the whole screen area, including the area
+ around notches or camera cutouts. Please holler if you are seeing any problems
+ related to this.
+- (build 21626) Fixed a bug where click/tap locations were incorrect on some
+ builds when tv-border was on (Thanks for the heads-up Loup(Dliwk's fan)!).
+- (build 21631) Fixes an issue where '^^^^^^^^^^^^^' lines in stack traces could
+ get chopped into tiny bits each on their own line in the dev console.
+- Hopefully finally fixed a longstanding issue where obscure cases such as
+ multiple key presses simultaneously could cause multiple main menu windows to
+ pop up. Please holler if you still see this problem happening anywhere. Also
+ added a few related safety checks and warnings to help ensure UI code is free
+ from such problems going forward. To make sure your custom UIs are behaving
+ well in this system, do the following two things: 1) any time you call
+ `set_main_menu_window()`, pass your existing main menu window root widget as
+ `from_window`. 2) In any call that can lead to you switching the main menu
+ window, check if your root widget is dead or transitioning out first and abort
+ if it is. See any window in `ui_v1_lib` for examples.
+- (build 21691) Fixed a bug causing touches to not register in some cases on
+ newer Android devices. (Huge thanks to JESWIN A J for helping me track that
+ down!).
+- Temporarily removed the pause-the-game-when-backgrounded behavior for locally
+ hosted games, mainly due to the code being hacky. Will try to restore this
+ functionality in a cleaner way soon.
+
+### 1.7.29 (build 21619, api 8, 2023-11-21)
+
+- Simply continued work on the big 1.7.28 update. I was able to finally start
+ updating the Mac App Store version of the game again (it had been stuck at
+ 1.4!), and it turns out that Apple AppStore submissions require the version
+ number to increase each time and not just the build number, so we may start
+ seeing more minor version number bumps for that reason.
+- Windows builds should now die with a clear error when the OpenGL version is
+ too old (OpenGL 3.0 or newer is required). Previously they could die with more
+ cryptic error messages such as "OpenGL function 'glActiveTexture2D' not
+ found".
+
+### 1.7.28 (build 21599, api 8, 2023-11-16)
+
+- Turning off ticket continues on all platforms. I'll be moving the game towards
+ a new monetization scheme mostly based on cosmetics and this has always felt a
+ bit ugly pay-to-win to me, so it's time for it to go. Note that the
+ functionality is still in there if anyone wants to support it in mods.
- Massively cleaned up code related to rendering and window systems (OpenGL,
SDL, etc). This code had been growing into a nasty tangle for 15 years
attempting to support various old/hacked versions of SDL, etc. I ripped out
huge chunks of it and put back still-relevant pieces in a much more cleanly
designed way. This should put us in a much better place for supporting various
- platforms and making graphical improvements going forward. See
- `ballistica/base/app_adapter/app_adapter_sdl.cc` for an example of the now
+ platforms and making graphical improvements going forward.
+ `ballistica/base/app_adapter/app_adapter_sdl.cc` is an example of the now
nicely implemented system.
- The engine now requires OpenGL 3.0 or newer on desktop and OpenGL ES 3.0 or
newer on mobile. This means we're cutting off a few percent of old devices on
Android that only support ES 2, but ES 3 has been out for 10 years now so I
feel it is time. As mentioned above, this allows massively cleaning up the
- graphics code which means we can start to improve it.
-- Removed gamma controls. These were only active on the old Mac version anyway
+ graphics code which means we can start to improve it. Ideally now the GL
+ renderer can be abstracted a bit more which will make the process of writing
+ other renderers easier.
+- Removed gamma controls. These were only active on the old Mac builds anyway
and are being removed from the upcoming SDL3, so if we want this sort of thing
we should do it through shading in the renderer now.
- Implemented both vsync and max-fps for the SDL build of the game. This means
@@ -129,7 +238,87 @@
before. It also takes a `confirm` bool arg which allows it to be used to bring
up a confirm dialog.
- Clicking on a window close button to quit no longer brings up a confirm dialog
- and instead quits immediately (though with a proper graceful shutdown).
+ and instead quits immediately (though with a proper graceful shutdown and a
+ lovely little fade).
+- Camera shake is now supported in network games and replays. Somehow I didn't
+ notice that was missing for years. The downside is this requires a server to
+ be hosting protocol 35, which cuts off support for 1.4 clients. So for now I
+ am keeping the default at 33. Once there a fewer 1.4 clients around we can
+ consider changing this (if everything hasn't moved to SceneV2 by then).
+- Added a server option to set the hosting protocol for servers who might want
+ to allow camera shake (or other minor features/fixes) that don't work in the
+ default protocol 33. See `protocol_version` in `config.yaml`. Just remember
+ that you will be cutting off support for older clients if you use 35.
+- Fixed a bug with screen-messages animating off screen too fast when frame
+ rates are high.
+- Added a proper graceful shutdown process for the audio server. This should
+ result in fewer ugly pops and warning messages when the app is quit.
+- Tidied up some keyboard shortcuts to be more platform-appropriate. For
+ example, toggling fullscreen on Windows is now Alt+Enter or F11.
+- Fancy rebuilt Mac build should now automatically sync its frame rate to the
+ display its running on (using CVDisplayLinks, not VSync).
+- Mac build is now relying solely on Apple's Game Controller Framework, which
+ seems pretty awesome these days. It should support most stuff SDL does and
+ with less configuring involved. Please holler if you come across something
+ that doesn't work.
+- Mac build is also now using the Game Controller Framework to handle keyboard
+ events. This should better handle things like modifier keys and also will
+ allow us to use that exact same code on the iPad/iPhone version.
+- OS key repeat events are no longer passed through the engine. This means that
+ any time we want repeating behavior, such as holding an arrow key to move
+ through UI elements, we will need to wire it up ourselves. We already do this
+ for things like game controllers however, so this is more consistent in a way.
+- Dev console no longer claims key events unless the Python tab is showing and
+ there is a hardware keyboard attached. This allows showing dev console tabs
+ above gameplay without interfering with it.
+- Added clipboard paste support to the dev console python terminal.
+- Added various text editing functionality to the dev console python terminal
+ (cursor movement, deleting chars and words, etc.)
+- Internal on-screen-keyboard now has a cancel button (thanks vishal332008!)
+- Public servers list now shows 'No servers found' if there are no servers to
+ show instead of just remaining mysteriously blank (thanks vishal332008!)
+- Players are now prevented from rejoining a session for 10 seconds after they
+ leave to prevent game exploits. Note this is different than the existing
+ system that prevents joining a *party* for 10 seconds; this covers people
+ who never leave the party (Thanks EraOSBeta!).
+- Fixes an issue where servers could be crashed by flooding them with join
+ requests (Thanks for the heads-up Era!).
+- The engine will now ignore empty device config dicts and fall back to
+ defaults; these could theoretically happen if device config code fails
+ somewhere and it previously would leave the device mysteriously inoperable.
+- The game will now show for controls with no bindings in the in-game
+ guide and controller/keyboard config screens.
+- Fixed a crash that could occur if SDL couldn't find a name for connected
+ joystick.
+- Simplified the app's handling of broken config files. Previously it would do
+ various complex things such as offering to edit the broken config on desktop
+ builds, avoiding overwriting broken configs, and automatically loading
+ previous configs. Now, if it finds a broken config, it will simply back it up
+ to a .broken file, log an error message, and then start up normally with a
+ default config. This way, things are more consistent across platforms, and
+ technical users can still fix and restore their old configs. Note that the app
+ still also writes .prev configs for extra security, though it no longer uses
+ them for anything itself.
+- Converted more internal engine time values from milliseconds to microseconds,
+ including things like the internal EventLoop timeline. Please holler if you
+ notice anything running 1000x too fast or slow. In general my strategy going
+ forward is to use microseconds for exact internal time values but to mostly
+ expose float seconds to the user, especially on the Python layer. There were
+ starting to be a few cases were integer milliseconds was not enough precision
+ for internal values. For instance, if we run with unclamped framerates and hit
+ several hundred FPS, milliseconds per frame would drop to 0 which caused some
+ problems. Note that scenev1 will be remaining on milliseconds internally for
+ compatibility reasons. Scenev2 should move to microseconds though.
+- The V2 account id for the signed in account is now available at
+ `ba*.app.plus.accounts.primary.accountid` (alongside some other existing
+ account info).
+- (build 21585) Fixed an issue where some navigation key presses were getting
+ incorrectly absorbed by text widgets. (Thanks for the heads-up Temp!)
+- (build 21585) Fixed an issue where texture quality changes would not take
+ effect until next launch.
+- Added a 'glow_type' arg to `bauiv1.textwidget()` to adjust the glow used when
+ the text is selected. The default is 'gradient' but there is now a 'uniform'
+ option which may look better in some circumstances.
### 1.7.27 (build 21282, api 8, 2023-08-30)
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index d8c74cc1..8fccc544 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -41,8 +41,12 @@
- Modder
### Era0S
+- Community Suggestions Implementer
- Bug Fixer
- Modder
### VinniTR
- Fixes
+
+### Rikko
+- Created the original "reject_recently_left_players" plugin
diff --git a/Makefile b/Makefile
index 36637945..d21ab3e5 100644
--- a/Makefile
+++ b/Makefile
@@ -779,13 +779,12 @@ check-full: py_check_prereqs
# Same as 'check' plus optional/slow extra checks.
check2: py_check_prereqs
- @$(DMAKE) -j$(CPUS) update-check cpplint pylint mypy pycharm
+ @$(DMAKE) -j$(CPUS) update-check cpplint pylint mypy
@$(PCOMMANDBATCH) echo SGRN BLD ALL CHECKS PASSED!
# Same as check2 but no caching (all files are checked).
check2-full: py_check_prereqs
- @$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full \
- pycharm-full
+ @$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full
@$(PCOMMANDBATCH) echo SGRN BLD ALL CHECKS PASSED!
# Run Cpplint checks on all C/C++ code.
@@ -924,14 +923,14 @@ preflight-full:
preflight2:
@$(MAKE) format
@$(MAKE) update
- @$(MAKE) -j$(CPUS) cpplint pylint mypy pycharm test
+ @$(MAKE) -j$(CPUS) cpplint pylint mypy test
@$(PCOMMANDBATCH) echo SGRN BLD PREFLIGHT SUCCESSFUL!
# Same as 'preflight2' but without caching (all files visited).
preflight2-full:
@$(MAKE) format-full
@$(MAKE) update
- @$(MAKE) -j$(CPUS) cpplint-full pylint-full mypy-full pycharm-full test-full
+ @$(MAKE) -j$(CPUS) cpplint-full pylint-full mypy-full test-full
@$(PCOMMANDBATCH) echo SGRN BLD PREFLIGHT SUCCESSFUL!
# Tell make which of these targets don't represent files.
diff --git a/README.md b/README.md
index 4550a942..20fddc2a 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ height="50" alt="logo">
***-ica***: collection of things relating to a specific theme.
-[](https://github.com/efroemling/ballistica/actions/workflows/ci.yml)
+[](https://github.com/efroemling/ballistica/actions/workflows/ci.yml) [](https://github.com/efroemling/ballistica/actions/workflows/cd.yml)
The Ballistica project is the foundation for
[BombSquad](https://www.froemling.net/apps/bombsquad) and potentially other
@@ -52,7 +52,7 @@ want to keep that spirit alive as the Ballistica project moves forward. Whether
this means making it easier to share mods, organize tournaments, join up with
friends, teach each other some Python, or whatever else. Life is short; let's
play some games. Or make them. Maybe both.
-
+
### Frequently Asked Questions
* **Q: What's with this name? Is it BombSquad or Ballistica?**
@@ -86,4 +86,4 @@ Playstation / My Toaster??**
for more details or the [Ballistica
Downloads](https://ballistica.net/downloads) page for early test builds on
some platforms.
-
+
diff --git a/ballisticakit-cmake/.idea/misc.xml b/ballisticakit-cmake/.idea/misc.xml
index 17581663..f01f08b8 100644
--- a/ballisticakit-cmake/.idea/misc.xml
+++ b/ballisticakit-cmake/.idea/misc.xml
@@ -14,7 +14,6 @@
-
diff --git a/ballisticakit-cmake/CMakeLists.txt b/ballisticakit-cmake/CMakeLists.txt
index b9050756..ab6575ab 100644
--- a/ballisticakit-cmake/CMakeLists.txt
+++ b/ballisticakit-cmake/CMakeLists.txt
@@ -353,9 +353,15 @@ set(BALLISTICA_SOURCES
${BA_SRC_ROOT}/ballistica/base/graphics/support/camera.h
${BA_SRC_ROOT}/ballistica/base/graphics/support/frame_def.cc
${BA_SRC_ROOT}/ballistica/base/graphics/support/frame_def.h
+ ${BA_SRC_ROOT}/ballistica/base/graphics/support/graphics_client_context.cc
+ ${BA_SRC_ROOT}/ballistica/base/graphics/support/graphics_client_context.h
+ ${BA_SRC_ROOT}/ballistica/base/graphics/support/graphics_settings.cc
+ ${BA_SRC_ROOT}/ballistica/base/graphics/support/graphics_settings.h
${BA_SRC_ROOT}/ballistica/base/graphics/support/net_graph.cc
${BA_SRC_ROOT}/ballistica/base/graphics/support/net_graph.h
${BA_SRC_ROOT}/ballistica/base/graphics/support/render_command_buffer.h
+ ${BA_SRC_ROOT}/ballistica/base/graphics/support/screen_messages.cc
+ ${BA_SRC_ROOT}/ballistica/base/graphics/support/screen_messages.h
${BA_SRC_ROOT}/ballistica/base/graphics/text/font_page_map_data.h
${BA_SRC_ROOT}/ballistica/base/graphics/text/text_graphics.cc
${BA_SRC_ROOT}/ballistica/base/graphics/text/text_graphics.h
@@ -432,16 +438,19 @@ set(BALLISTICA_SOURCES
${BA_SRC_ROOT}/ballistica/base/support/app_config.cc
${BA_SRC_ROOT}/ballistica/base/support/app_config.h
${BA_SRC_ROOT}/ballistica/base/support/app_timer.h
+ ${BA_SRC_ROOT}/ballistica/base/support/base_build_switches.cc
+ ${BA_SRC_ROOT}/ballistica/base/support/base_build_switches.h
${BA_SRC_ROOT}/ballistica/base/support/classic_soft.h
${BA_SRC_ROOT}/ballistica/base/support/context.cc
${BA_SRC_ROOT}/ballistica/base/support/context.h
+ ${BA_SRC_ROOT}/ballistica/base/support/display_timer.h
${BA_SRC_ROOT}/ballistica/base/support/huffman.cc
${BA_SRC_ROOT}/ballistica/base/support/huffman.h
${BA_SRC_ROOT}/ballistica/base/support/plus_soft.h
+ ${BA_SRC_ROOT}/ballistica/base/support/repeater.cc
+ ${BA_SRC_ROOT}/ballistica/base/support/repeater.h
${BA_SRC_ROOT}/ballistica/base/support/stdio_console.cc
${BA_SRC_ROOT}/ballistica/base/support/stdio_console.h
- ${BA_SRC_ROOT}/ballistica/base/support/stress_test.cc
- ${BA_SRC_ROOT}/ballistica/base/support/stress_test.h
${BA_SRC_ROOT}/ballistica/base/ui/dev_console.cc
${BA_SRC_ROOT}/ballistica/base/ui/dev_console.h
${BA_SRC_ROOT}/ballistica/base/ui/ui.cc
@@ -454,6 +463,8 @@ set(BALLISTICA_SOURCES
${BA_SRC_ROOT}/ballistica/classic/python/classic_python.h
${BA_SRC_ROOT}/ballistica/classic/python/methods/python_methods_classic.cc
${BA_SRC_ROOT}/ballistica/classic/python/methods/python_methods_classic.h
+ ${BA_SRC_ROOT}/ballistica/classic/support/stress_test.cc
+ ${BA_SRC_ROOT}/ballistica/classic/support/stress_test.h
${BA_SRC_ROOT}/ballistica/classic/support/v1_account.cc
${BA_SRC_ROOT}/ballistica/classic/support/v1_account.h
${BA_SRC_ROOT}/ballistica/core/core.cc
@@ -686,8 +697,10 @@ 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
${BA_SRC_ROOT}/ballistica/shared/generic/timer_list.cc
${BA_SRC_ROOT}/ballistica/shared/generic/timer_list.h
${BA_SRC_ROOT}/ballistica/shared/generic/utf8.cc
diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
index 3ec34dcc..97c94baf 100644
--- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
+++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
@@ -345,9 +345,15 @@
+
+
+
+
+
+
@@ -424,16 +430,19 @@
+
+
+
+
+
-
-
@@ -446,6 +455,8 @@
+
+
@@ -678,8 +689,10 @@
+
+
diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
index f77535ec..92b1c44d 100644
--- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
+++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
@@ -469,6 +469,18 @@
ballistica\base\graphics\support
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+ ballistica\base\graphics\support
@@ -478,6 +490,12 @@
ballistica\base\graphics\support
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+ ballistica\base\graphics\text
@@ -706,6 +724,12 @@
ballistica\base\support
+
+ ballistica\base\support
+
+
+ ballistica\base\support
+ ballistica\base\support
@@ -715,6 +739,9 @@
ballistica\base\support
+
+ ballistica\base\support
+ ballistica\base\support
@@ -724,18 +751,18 @@
ballistica\base\support
+
+ ballistica\base\support
+
+
+ ballistica\base\support
+ ballistica\base\supportballistica\base\support
-
- ballistica\base\support
-
-
- ballistica\base\support
- ballistica\base\ui
@@ -772,6 +799,12 @@
ballistica\classic\python\methods
+
+ ballistica\classic\support
+
+
+ ballistica\classic\support
+ ballistica\classic\support
@@ -1468,12 +1501,18 @@
ballistica\shared\generic
+
+ ballistica\shared\generic
+ ballistica\shared\genericballistica\shared\generic
+
+ ballistica\shared\generic
+ ballistica\shared\generic
diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
index 505b71c1..6434233d 100644
--- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
+++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
@@ -340,9 +340,15 @@
+
+
+
+
+
+
@@ -419,16 +425,19 @@
+
+
+
+
+
-
-
@@ -441,6 +450,8 @@
+
+
@@ -673,8 +684,10 @@
+
+
diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
index f77535ec..92b1c44d 100644
--- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
+++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
@@ -469,6 +469,18 @@
ballistica\base\graphics\support
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+ ballistica\base\graphics\support
@@ -478,6 +490,12 @@
ballistica\base\graphics\support
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+ ballistica\base\graphics\text
@@ -706,6 +724,12 @@
ballistica\base\support
+
+ ballistica\base\support
+
+
+ ballistica\base\support
+ ballistica\base\support
@@ -715,6 +739,9 @@
ballistica\base\support
+
+ ballistica\base\support
+ ballistica\base\support
@@ -724,18 +751,18 @@
ballistica\base\support
+
+ ballistica\base\support
+
+
+ ballistica\base\support
+ ballistica\base\supportballistica\base\support
-
- ballistica\base\support
-
-
- ballistica\base\support
- ballistica\base\ui
@@ -772,6 +799,12 @@
ballistica\classic\python\methods
+
+ ballistica\classic\support
+
+
+ ballistica\classic\support
+ ballistica\classic\support
@@ -1468,12 +1501,18 @@
ballistica\shared\generic
+
+ ballistica\shared\generic
+ ballistica\shared\genericballistica\shared\generic
+
+ ballistica\shared\generic
+ ballistica\shared\generic
diff --git a/config/projectconfig.json b/config/projectconfig.json
index a60bf91d..68dd6d90 100644
--- a/config/projectconfig.json
+++ b/config/projectconfig.json
@@ -10,11 +10,11 @@
"src/ballistica/base/graphics/texture/ktx.cc",
"src/ballistica/core/platform/android/android_gl3.h",
"src/ballistica/base/platform/apple/app_delegate.h",
- "src/ballistica/base/platform/apple/scripting_bridge_music.h",
+ "src/ballistica/base/platform/apple/MacMusicApp.h",
+ "src/ballistica/base/platform/apple/MacMusicAppScriptingBridge.h",
"src/ballistica/core/platform/android/utf8/checked.h",
"src/ballistica/core/platform/android/utf8/unchecked.h",
"src/ballistica/core/platform/android/utf8/core.h",
- "src/ballistica/base/platform/apple/sdl_main_mac.h",
"src/ballistica/base/platform/oculus/main_rift.cc",
"src/ballistica/core/platform/android/android_gl3.c"
],
diff --git a/config/spinoffconfig.py b/config/spinoffconfig.py
index 41dda47b..26e97c36 100644
--- a/config/spinoffconfig.py
+++ b/config/spinoffconfig.py
@@ -53,47 +53,60 @@ ctx.src_omit_paths = {
'src/assets/workspace',
}
-# Use this to 'carve out' directories or exact file paths which will be
-# git-managed on dst. By default, spinoff will consider dirs containing
-# the files it syncs from src as 'spinoff-managed'; it will set them as
-# git-ignored and will complain if any files appear in them that it does
-# not manage itself (to prevent accidentally doing work in such places).
-# Note that adding a dir to src_write_paths does not prevent files
-# within it from being synced by spinoff; it just means that each of
-# those individual spinoff-managed files will have their own gitignore
-# entry since there is no longer one covering the whole dir. So to keep
-# things tidy, carve out the minimal set of exact file/dir paths that
-# you need.
+# Use this to 'carve out' files or directories which will be git-managed
+# on dst.
+#
+# By default, spinoff will consider dirs containing the files it syncs
+# from src as 'spinoff-managed'; it will set them as git-ignored and
+# will complain if any files appear in them that it does not manage
+# itself (to prevent accidentally doing work in such places). Note that
+# adding a dir to src_write_paths does not prevent files within it from
+# being synced by spinoff; it just means that each of those individual
+# spinoff-managed files will have their own gitignore entry since there
+# can't be a single one covering the whole dir. So to keep things tidy,
+# carve out the minimal set of exact file/dir paths that you need.
ctx.src_write_paths = {
'tools/spinoff',
'config/spinoffconfig.py',
}
-# Normally spinoff errors if it finds any files in its managed dirs that
-# it did not put there. This is to prevent accidentally working in these
-# parts of a dst project; since these sections are git-ignored, git
-# itself won't raise any warnings in such cases and it would be easy to
-# accidentally lose work otherwise.
+# Use this to 'carve out' files or directories under spinoff managed
+# dirs which will be completely ignored by spinoff (but *not* placed
+# under git control).
#
-# This list can be used to suppress spinoff's errors for specific
-# locations. This is generally used to allow build output or other
-# dynamically generated files to exist within spinoff-managed
-# directories. It is possible to use src_write_paths for such purposes,
-# but this has the side-effect of greatly complicating the dst project's
-# gitignore list; selectively marking a few dirs as unchecked makes for
-# a cleaner setup. Just be careful to not set excessively broad regions
-# as unchecked; you don't want to mask actual useful error messages.
+# Normally spinoff will error if it finds any files under its managed
+# dirs that it did not put there. This is to prevent accidentally
+# working in these parts of a dst project; since spinoff-controlled
+# stuff is git-ignored, git itself won't raise any warnings in such
+# cases and it would be easy to accidentally blow away changes if
+# spinoff didn't raise a stink.
+#
+# This list is used to suppress raising of said stink for specific
+# locations. This allows build output or other dynamically generated
+# files to exist under spinoff-managed directories. It is also possible
+# to use src_write_paths for such carve-outs, but that can have the
+# negative side-effect of greatly complicating the dst project's
+# .gitignore file. Selectively marking a few specific files or dirs as
+# unchecked instead can keep things tidier and more understandable.
+#
+# Note that files and paths marked as unchecked cannot be the
+# destination for synced files, as that would be ambiguous (We can
+# either sync the file ourself or expect someone else to write it, but
+# not both).
ctx.src_unchecked_paths = {
'src/ballistica/mgen',
'src/ballistica/*/mgen',
'src/assets/ba_data/python/*/_mgen',
'src/meta/*/mgen',
'ballisticakit-cmake/.clang-format',
- 'ballisticakit-android/BallisticaKit/src/cardboard/res',
'ballisticakit-windows/*/BallisticaKit.ico',
- 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets',
- 'ballisticakit-android/BallisticaKit/src/*/res',
- 'ballisticakit-android/BallisticaKit/src/*/assets',
+ 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*.png',
+ 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*/*.png',
+ 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/*/*/*/*/*.png',
+ 'ballisticakit-xcode/BallisticaKit.xcodeproj/'
+ 'project.xcworkspace/xcuserdata',
+ 'ballisticakit-android/BallisticaKit/src/*/res/*/*.png',
+ 'ballisticakit-android/BallisticaKit/src/*/assets/ballistica_files',
'ballisticakit-android/local.properties',
'ballisticakit-android/.gradle',
'ballisticakit-android/build',
@@ -139,7 +152,6 @@ ctx.filter_dirs = {
'ballisticakit-cmake',
'ballisticakit-xcode/BallisticaKit.xcodeproj',
'ballisticakit-ios.xcodeproj',
- 'ballisticakit-mac.xcodeproj',
'config',
'src/assets/pdoc',
}
@@ -182,6 +194,7 @@ ctx.filter_file_names = {
'.projectile',
'.editorconfig',
'ci.yml',
+ 'cd.yml',
'LICENSE',
'cloudtool',
'bacloud',
@@ -263,6 +276,7 @@ ctx.filter_file_extensions = {
'.frag',
'.vert',
'.xcsettings',
+ '.xcstrings',
'.filters',
}
diff --git a/src/assets/.asset_manifest_private.json b/src/assets/.asset_manifest_private.json
index c383200f..6e46c89e 100644
--- a/src/assets/.asset_manifest_private.json
+++ b/src/assets/.asset_manifest_private.json
@@ -1465,6 +1465,10 @@
"ba_data/textures/discordLogo.ktx",
"ba_data/textures/discordLogo.pvr",
"ba_data/textures/discordLogo_preview.png",
+ "ba_data/textures/discordServer.dds",
+ "ba_data/textures/discordServer.ktx",
+ "ba_data/textures/discordServer.pvr",
+ "ba_data/textures/discordServer_preview.png",
"ba_data/textures/doomShroomBGColor.dds",
"ba_data/textures/doomShroomBGColor.ktx",
"ba_data/textures/doomShroomBGColor.pvr",
diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json
index 9c4f56a5..8a62973f 100644
--- a/src/assets/.asset_manifest_public.json
+++ b/src/assets/.asset_manifest_public.json
@@ -20,7 +20,6 @@
"ba_data/python/babase/__pycache__/_error.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_general.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_hooks.cpython-311.opt-1.pyc",
- "ba_data/python/babase/__pycache__/_keyboard.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_language.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_login.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_math.cpython-311.opt-1.pyc",
@@ -50,7 +49,6 @@
"ba_data/python/babase/_error.py",
"ba_data/python/babase/_general.py",
"ba_data/python/babase/_hooks.py",
- "ba_data/python/babase/_keyboard.py",
"ba_data/python/babase/_language.py",
"ba_data/python/babase/_login.py",
"ba_data/python/babase/_math.py",
@@ -152,6 +150,7 @@
"ba_data/python/bascenev1/__pycache__/_messages.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_multiteamsession.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_music.cpython-311.opt-1.pyc",
+ "ba_data/python/bascenev1/__pycache__/_net.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_nodeactor.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_player.cpython-311.opt-1.pyc",
"ba_data/python/bascenev1/__pycache__/_playlist.cpython-311.opt-1.pyc",
@@ -186,6 +185,7 @@
"ba_data/python/bascenev1/_messages.py",
"ba_data/python/bascenev1/_multiteamsession.py",
"ba_data/python/bascenev1/_music.py",
+ "ba_data/python/bascenev1/_net.py",
"ba_data/python/bascenev1/_nodeactor.py",
"ba_data/python/bascenev1/_player.py",
"ba_data/python/bascenev1/_playlist.py",
@@ -352,10 +352,12 @@
"ba_data/python/bauiv1/__init__.py",
"ba_data/python/bauiv1/__pycache__/__init__.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1/__pycache__/_hooks.cpython-311.opt-1.pyc",
+ "ba_data/python/bauiv1/__pycache__/_keyboard.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1/__pycache__/_subsystem.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1/__pycache__/_uitypes.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1/__pycache__/onscreenkeyboard.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1/_hooks.py",
+ "ba_data/python/bauiv1/_keyboard.py",
"ba_data/python/bauiv1/_subsystem.py",
"ba_data/python/bauiv1/_uitypes.py",
"ba_data/python/bauiv1/onscreenkeyboard.py",
@@ -366,11 +368,11 @@
"ba_data/python/bauiv1lib/__pycache__/characterpicker.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/colorpicker.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/config.cpython-311.opt-1.pyc",
- "ba_data/python/bauiv1lib/__pycache__/configerror.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/confirm.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/continues.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/creditslist.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/debug.cpython-311.opt-1.pyc",
+ "ba_data/python/bauiv1lib/__pycache__/discord.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/feedback.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/fileselector.cpython-311.opt-1.pyc",
"ba_data/python/bauiv1lib/__pycache__/getcurrency.cpython-311.opt-1.pyc",
@@ -417,7 +419,6 @@
"ba_data/python/bauiv1lib/characterpicker.py",
"ba_data/python/bauiv1lib/colorpicker.py",
"ba_data/python/bauiv1lib/config.py",
- "ba_data/python/bauiv1lib/configerror.py",
"ba_data/python/bauiv1lib/confirm.py",
"ba_data/python/bauiv1lib/continues.py",
"ba_data/python/bauiv1lib/coop/__init__.py",
@@ -432,6 +433,7 @@
"ba_data/python/bauiv1lib/coop/tournamentbutton.py",
"ba_data/python/bauiv1lib/creditslist.py",
"ba_data/python/bauiv1lib/debug.py",
+ "ba_data/python/bauiv1lib/discord.py",
"ba_data/python/bauiv1lib/feedback.py",
"ba_data/python/bauiv1lib/fileselector.py",
"ba_data/python/bauiv1lib/gather/__init__.py",
diff --git a/src/assets/Makefile b/src/assets/Makefile
index 5af7a60c..1ee71109 100644
--- a/src/assets/Makefile
+++ b/src/assets/Makefile
@@ -178,7 +178,6 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/babase/_error.py \
$(BUILD_DIR)/ba_data/python/babase/_general.py \
$(BUILD_DIR)/ba_data/python/babase/_hooks.py \
- $(BUILD_DIR)/ba_data/python/babase/_keyboard.py \
$(BUILD_DIR)/ba_data/python/babase/_language.py \
$(BUILD_DIR)/ba_data/python/babase/_login.py \
$(BUILD_DIR)/ba_data/python/babase/_math.py \
@@ -237,6 +236,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bascenev1/_messages.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_multiteamsession.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_music.py \
+ $(BUILD_DIR)/ba_data/python/bascenev1/_net.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_nodeactor.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_player.py \
$(BUILD_DIR)/ba_data/python/bascenev1/_playlist.py \
@@ -326,6 +326,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/batemplatefs/_subsystem.py \
$(BUILD_DIR)/ba_data/python/bauiv1/__init__.py \
$(BUILD_DIR)/ba_data/python/bauiv1/_hooks.py \
+ $(BUILD_DIR)/ba_data/python/bauiv1/_keyboard.py \
$(BUILD_DIR)/ba_data/python/bauiv1/_subsystem.py \
$(BUILD_DIR)/ba_data/python/bauiv1/_uitypes.py \
$(BUILD_DIR)/ba_data/python/bauiv1/onscreenkeyboard.py \
@@ -341,7 +342,6 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bauiv1lib/characterpicker.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/colorpicker.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/config.py \
- $(BUILD_DIR)/ba_data/python/bauiv1lib/configerror.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/confirm.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/continues.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/coop/__init__.py \
@@ -351,6 +351,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bauiv1lib/coop/tournamentbutton.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/creditslist.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/debug.py \
+ $(BUILD_DIR)/ba_data/python/bauiv1lib/discord.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/feedback.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/fileselector.py \
$(BUILD_DIR)/ba_data/python/bauiv1lib/gather/__init__.py \
@@ -452,7 +453,6 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_error.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_general.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_hooks.cpython-311.opt-1.pyc \
- $(BUILD_DIR)/ba_data/python/babase/__pycache__/_keyboard.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_language.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_login.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_math.cpython-311.opt-1.pyc \
@@ -511,6 +511,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_messages.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_multiteamsession.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_music.cpython-311.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_net.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_nodeactor.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_player.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_playlist.cpython-311.opt-1.pyc \
@@ -600,6 +601,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/batemplatefs/__pycache__/_subsystem.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/__init__.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_hooks.cpython-311.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_keyboard.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_subsystem.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_uitypes.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/onscreenkeyboard.cpython-311.opt-1.pyc \
@@ -615,7 +617,6 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/characterpicker.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/colorpicker.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/config.cpython-311.opt-1.pyc \
- $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/configerror.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/confirm.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/continues.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/coop/__pycache__/__init__.cpython-311.opt-1.pyc \
@@ -625,6 +626,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/bauiv1lib/coop/__pycache__/tournamentbutton.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/creditslist.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/debug.cpython-311.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/discord.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/feedback.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/fileselector.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bauiv1lib/gather/__pycache__/__init__.cpython-311.opt-1.pyc \
@@ -5710,6 +5712,7 @@ TEX2D_DDS_TARGETS = \
$(BUILD_DIR)/ba_data/textures/cyborgIcon.dds \
$(BUILD_DIR)/ba_data/textures/cyborgIconColorMask.dds \
$(BUILD_DIR)/ba_data/textures/discordLogo.dds \
+ $(BUILD_DIR)/ba_data/textures/discordServer.dds \
$(BUILD_DIR)/ba_data/textures/doomShroomBGColor.dds \
$(BUILD_DIR)/ba_data/textures/doomShroomLevelColor.dds \
$(BUILD_DIR)/ba_data/textures/doomShroomPreview.dds \
@@ -6113,6 +6116,7 @@ TEX2D_PVR_TARGETS = \
$(BUILD_DIR)/ba_data/textures/cyborgIcon.pvr \
$(BUILD_DIR)/ba_data/textures/cyborgIconColorMask.pvr \
$(BUILD_DIR)/ba_data/textures/discordLogo.pvr \
+ $(BUILD_DIR)/ba_data/textures/discordServer.pvr \
$(BUILD_DIR)/ba_data/textures/doomShroomBGColor.pvr \
$(BUILD_DIR)/ba_data/textures/doomShroomLevelColor.pvr \
$(BUILD_DIR)/ba_data/textures/doomShroomPreview.pvr \
@@ -6516,6 +6520,7 @@ TEX2D_KTX_TARGETS = \
$(BUILD_DIR)/ba_data/textures/cyborgIcon.ktx \
$(BUILD_DIR)/ba_data/textures/cyborgIconColorMask.ktx \
$(BUILD_DIR)/ba_data/textures/discordLogo.ktx \
+ $(BUILD_DIR)/ba_data/textures/discordServer.ktx \
$(BUILD_DIR)/ba_data/textures/doomShroomBGColor.ktx \
$(BUILD_DIR)/ba_data/textures/doomShroomLevelColor.ktx \
$(BUILD_DIR)/ba_data/textures/doomShroomPreview.ktx \
@@ -6919,6 +6924,7 @@ TEX2D_PREVIEW_PNG_TARGETS = \
$(BUILD_DIR)/ba_data/textures/cyborgIconColorMask_preview.png \
$(BUILD_DIR)/ba_data/textures/cyborgIcon_preview.png \
$(BUILD_DIR)/ba_data/textures/discordLogo_preview.png \
+ $(BUILD_DIR)/ba_data/textures/discordServer_preview.png \
$(BUILD_DIR)/ba_data/textures/doomShroomBGColor_preview.png \
$(BUILD_DIR)/ba_data/textures/doomShroomLevelColor_preview.png \
$(BUILD_DIR)/ba_data/textures/doomShroomPreview_preview.png \
diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py
index fa542c74..9d4379f3 100644
--- a/src/assets/ba_data/python/babase/__init__.py
+++ b/src/assets/ba_data/python/babase/__init__.py
@@ -27,7 +27,10 @@ from _babase import (
apptime,
apptimer,
AppTimer,
- can_toggle_fullscreen,
+ fullscreen_control_available,
+ fullscreen_control_get,
+ fullscreen_control_key_shortcut,
+ fullscreen_control_set,
charstr,
clipboard_get_text,
clipboard_has_text,
@@ -57,11 +60,10 @@ from _babase import (
have_permission,
in_logic_thread,
increment_analytics_count,
+ invoke_main_menu,
is_os_playing_music,
- is_running_on_fire_tv,
is_xcode_build,
lock_all_input,
- mac_music_app_get_library_source,
mac_music_app_get_playlists,
mac_music_app_get_volume,
mac_music_app_init,
@@ -72,7 +74,10 @@ from _babase import (
music_player_set_volume,
music_player_shutdown,
music_player_stop,
+ native_review_request,
+ native_review_request_supported,
native_stack_trace,
+ open_file_externally,
print_load_info,
pushcall,
quit,
@@ -82,7 +87,6 @@ from _babase import (
screenmessage,
set_analytics_screen,
set_low_level_config_value,
- set_stress_testing,
set_thread_name,
set_ui_input_device,
show_progress_bar,
@@ -151,9 +155,8 @@ from babase._general import (
getclass,
get_type_name,
)
-from babase._keyboard import Keyboard
from babase._language import Lstr, LanguageSubsystem
-from babase._login import LoginAdapter
+from babase._login import LoginAdapter, LoginInfo
# noinspection PyProtectedMember
# (PyCharm inspection bug?)
@@ -200,7 +203,10 @@ __all__ = [
'apptimer',
'AppTimer',
'Call',
- 'can_toggle_fullscreen',
+ 'fullscreen_control_available',
+ 'fullscreen_control_get',
+ 'fullscreen_control_key_shortcut',
+ 'fullscreen_control_set',
'charstr',
'clipboard_get_text',
'clipboard_has_text',
@@ -249,18 +255,17 @@ __all__ = [
'increment_analytics_count',
'InputDeviceNotFoundError',
'InputType',
+ 'invoke_main_menu',
'is_browser_likely_available',
'is_browser_likely_available',
'is_os_playing_music',
'is_point_in_box',
- 'is_running_on_fire_tv',
'is_xcode_build',
- 'Keyboard',
'LanguageSubsystem',
'lock_all_input',
'LoginAdapter',
+ 'LoginInfo',
'Lstr',
- 'mac_music_app_get_library_source',
'mac_music_app_get_playlists',
'mac_music_app_get_volume',
'mac_music_app_init',
@@ -273,10 +278,13 @@ __all__ = [
'music_player_set_volume',
'music_player_shutdown',
'music_player_stop',
+ 'native_review_request',
+ 'native_review_request_supported',
'native_stack_trace',
'NodeNotFoundError',
'normalized_color',
'NotFoundError',
+ 'open_file_externally',
'Permission',
'PlayerNotFoundError',
'Plugin',
@@ -297,7 +305,6 @@ __all__ = [
'SessionTeamNotFoundError',
'set_analytics_screen',
'set_low_level_config_value',
- 'set_stress_testing',
'set_thread_name',
'set_ui_input_device',
'show_progress_bar',
diff --git a/src/assets/ba_data/python/babase/_accountv2.py b/src/assets/ba_data/python/babase/_accountv2.py
index 78d138f1..37914bce 100644
--- a/src/assets/ba_data/python/babase/_accountv2.py
+++ b/src/assets/ba_data/python/babase/_accountv2.py
@@ -6,7 +6,7 @@ from __future__ import annotations
import hashlib
import logging
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, assert_never
from efro.call import tpartial
from efro.error import CommunicationError
@@ -16,7 +16,7 @@ import _babase
if TYPE_CHECKING:
from typing import Any
- from babase._login import LoginAdapter
+ from babase._login import LoginAdapter, LoginInfo
DEBUG_LOG = False
@@ -27,10 +27,12 @@ class AccountV2Subsystem:
Category: **App Classes**
- Access the single shared instance of this class at 'ba.app.accounts'.
+ Access the single shared instance of this class at 'ba.app.plus.accounts'.
"""
def __init__(self) -> None:
+ from babase._login import LoginAdapterGPGS, LoginAdapterGameCenter
+
# Whether or not everything related to an initial login
# (or lack thereof) has completed. This includes things like
# workspace syncing. Completion of this is what flips the app
@@ -45,16 +47,13 @@ class AccountV2Subsystem:
self._implicit_state_changed = False
self._can_do_auto_sign_in = True
- if _babase.app.classic is None:
- raise RuntimeError('Needs updating for no-classic case.')
-
- if (
- _babase.app.classic.platform == 'android'
- and _babase.app.classic.subplatform == 'google'
- ):
- from babase._login import LoginAdapterGPGS
-
- self.login_adapters[LoginType.GPGS] = LoginAdapterGPGS()
+ adapter: LoginAdapter
+ if _babase.using_google_play_game_services():
+ adapter = LoginAdapterGPGS()
+ self.login_adapters[adapter.login_type] = adapter
+ if _babase.using_game_center():
+ adapter = LoginAdapterGameCenter()
+ self.login_adapters[adapter.login_type] = adapter
def on_app_loading(self) -> None:
"""Should be called at standard on_app_loading time."""
@@ -62,10 +61,6 @@ class AccountV2Subsystem:
for adapter in self.login_adapters.values():
adapter.on_app_loading()
- def set_primary_credentials(self, credentials: str | None) -> None:
- """Set credentials for the primary app account."""
- raise NotImplementedError('This should be overridden.')
-
def have_primary_credentials(self) -> bool:
"""Are credentials currently set for the primary app account?
@@ -80,10 +75,6 @@ class AccountV2Subsystem:
"""The primary account for the app, or None if not logged in."""
return self.do_get_primary()
- def do_get_primary(self) -> AccountV2Handle | None:
- """Internal - should be overridden by subclass."""
- return None
-
def on_primary_account_changed(
self, account: AccountV2Handle | None
) -> None:
@@ -142,6 +133,8 @@ class AccountV2Subsystem:
"""An implicit sign-in happened (called by native layer)."""
from babase._login import LoginAdapter
+ assert _babase.in_logic_thread()
+
with _babase.ContextRef.empty():
self.login_adapters[login_type].set_implicit_login_state(
LoginAdapter.ImplicitLoginState(
@@ -151,6 +144,7 @@ class AccountV2Subsystem:
def on_implicit_sign_out(self, login_type: LoginType) -> None:
"""An implicit sign-out happened (called by native layer)."""
+ assert _babase.in_logic_thread()
with _babase.ContextRef.empty():
self.login_adapters[login_type].set_implicit_login_state(None)
@@ -192,9 +186,10 @@ class AccountV2Subsystem:
cfgkey = 'ImplicitLoginStates'
cfgdict = _babase.app.config.setdefault(cfgkey, {})
- # Store which (if any) adapter is currently implicitly signed in.
- # Making the assumption there will only ever be one implicit
- # adapter at a time; may need to update this if that changes.
+ # Store which (if any) adapter is currently implicitly signed
+ # in. Making the assumption there will only ever be one implicit
+ # adapter at a time; may need to revisit this logic if that
+ # changes.
prev_state = cfgdict.get(login_type.value)
if state is None:
self._implicit_signed_in_adapter = None
@@ -205,18 +200,26 @@ class AccountV2Subsystem:
state.login_id
)
- # Special case: if the user is already signed in but not with
- # this implicit login, we may want to let them know that the
- # 'Welcome back FOO' they likely just saw is not actually
- # accurate.
+ # Special case: if the user is already signed in but not
+ # with this implicit login, let them know that the 'Welcome
+ # back FOO' they likely just saw is not actually accurate.
if (
self.primary is not None
and not self.login_adapters[login_type].is_back_end_active()
):
+ service_str: Lstr | None
if login_type is LoginType.GPGS:
service_str = Lstr(resource='googlePlayText')
- else:
+ elif login_type is LoginType.GAME_CENTER:
+ # Note: Apparently Game Center is just called 'Game
+ # Center' in all languages. Can revisit if not true.
+ # https://developer.apple.com/forums/thread/725779
+ service_str = Lstr(value='Game Center')
+ elif login_type is LoginType.EMAIL:
+ # Not possible; just here for exhaustive coverage.
service_str = None
+ else:
+ assert_never(login_type)
if service_str is not None:
_babase.apptimer(
2.0,
@@ -259,6 +262,14 @@ class AccountV2Subsystem:
# We may want to auto-sign-in based on this new state.
self._update_auto_sign_in()
+ def do_get_primary(self) -> AccountV2Handle | None:
+ """Internal - should be overridden by subclass."""
+ raise NotImplementedError('This should be overridden.')
+
+ def set_primary_credentials(self, credentials: str | None) -> None:
+ """Set credentials for the primary app account."""
+ raise NotImplementedError('This should be overridden.')
+
def _update_auto_sign_in(self) -> None:
plus = _babase.app.plus
assert plus is not None
@@ -266,7 +277,7 @@ class AccountV2Subsystem:
# If implicit state has changed, try to respond.
if self._implicit_state_changed:
if self._implicit_signed_in_adapter is None:
- # If implicit back-end is signed out, follow suit
+ # If implicit back-end has signed out, we follow suit
# immediately; no need to wait for network connectivity.
if DEBUG_LOG:
logging.debug(
@@ -286,9 +297,8 @@ class AccountV2Subsystem:
# Consider this an 'explicit' sign in because the
# implicit-login state change presumably was triggered
# by some user action (signing in, signing out, or
- # switching accounts via the back-end).
- # NOTE: should test case where we don't have
- # connectivity here.
+ # switching accounts via the back-end). NOTE: should
+ # test case where we don't have connectivity here.
if plus.cloud.is_connected():
if DEBUG_LOG:
logging.debug(
@@ -419,14 +429,11 @@ class AccountV2Handle:
used with some operations such as cloud messaging.
"""
- def __init__(self) -> None:
- self.tag = '?'
-
- self.workspacename: str | None = None
- self.workspaceid: str | None = None
-
- # Login types and their display-names associated with this account.
- self.logins: dict[LoginType, str] = {}
+ accountid: str
+ tag: str
+ workspacename: str | None
+ workspaceid: str | None
+ logins: dict[LoginType, LoginInfo]
def __enter__(self) -> None:
"""Support for "with" statement.
diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py
index 134caf5d..4a021ab8 100644
--- a/src/assets/ba_data/python/babase/_app.py
+++ b/src/assets/ba_data/python/babase/_app.py
@@ -56,6 +56,8 @@ class App:
# pylint: disable=too-many-public-methods
+ # A few things defined as non-optional values but not actually
+ # available until the app starts.
plugins: PluginSubsystem
lang: LanguageSubsystem
health_monitor: AppHealthMonitor
@@ -92,7 +94,7 @@ class App:
# Used on platforms such as mobile where the app basically needs
# to shut down while backgrounded. In this state, all event
- # loops are suspended and all graphics and audio should cease
+ # loops are suspended and all graphics and audio must cease
# completely. Be aware that the suspended state can be entered
# from any other state including NATIVE_BOOTSTRAPPING and
# SHUTTING_DOWN.
@@ -149,9 +151,9 @@ class App:
def __init__(self) -> None:
"""(internal)
- Do not instantiate this class; access the single shared instance
- of it as 'app' which is available in various Ballistica
- feature-set modules such as babase.
+ Do not instantiate this class. You can access the single shared
+ instance of it through various high level packages: 'babase.app',
+ 'bascenev1.app', 'bauiv1.app', etc.
"""
# Hack for docs-generation: we can be imported with dummy modules
@@ -182,7 +184,6 @@ class App:
# foregrounded; can be a simple way to determine if network data
# should be refreshed/etc.
self.fg_state = 0
- self.config_file_healthy: bool = False
self._subsystems: list[AppSubsystem] = []
self._native_bootstrapping_completed = False
@@ -208,7 +209,8 @@ class App:
self._shutdown_task: asyncio.Task[None] | None = None
self._shutdown_tasks: list[Coroutine[None, None, None]] = [
self._wait_for_shutdown_suppressions(),
- self._fade_for_shutdown(),
+ self._fade_and_shutdown_graphics(),
+ self._fade_and_shutdown_audio(),
]
self._pool_thread_count = 0
@@ -227,6 +229,15 @@ class App:
self.lang = LanguageSubsystem()
self.plugins = PluginSubsystem()
+ @property
+ def active(self) -> bool:
+ """Whether the app is currently front and center.
+
+ This will be False when the app is hidden, other activities
+ are covering it, etc. (depending on the platform).
+ """
+ return _babase.app_is_active()
+
@property
def aioloop(self) -> asyncio.AbstractEventLoop:
"""The logic thread's asyncio event loop.
@@ -426,11 +437,17 @@ class App:
self._native_shutdown_complete_called = True
self._update_state()
+ def on_native_active_changed(self) -> None:
+ """Called by the native layer when the app active state changes."""
+ assert _babase.in_logic_thread()
+ if self._mode is not None:
+ self._mode.on_app_active_changed()
+
def read_config(self) -> None:
"""(internal)"""
from babase._appconfig import read_app_config
- self._config, self.config_file_healthy = read_app_config()
+ self._config = read_app_config()
def handle_deep_link(self, url: str) -> None:
"""Handle a deep link URL."""
@@ -508,7 +525,7 @@ class App:
except Exception:
logging.exception('Error setting app intent to %s.', intent)
_babase.pushcall(
- tpartial(self._apply_intent_error, intent),
+ tpartial(self._display_set_intent_error, intent),
from_other_thread=True,
)
@@ -553,10 +570,11 @@ class App:
'Error handling intent %s in app-mode %s.', intent, mode
)
- def _apply_intent_error(self, intent: AppIntent) -> None:
+ def _display_set_intent_error(self, intent: AppIntent) -> None:
+ """Show the *user* something went wrong setting an intent."""
from babase._language import Lstr
- del intent # Unused.
+ del intent
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
_babase.getsimplesound('error').play()
@@ -579,19 +597,6 @@ class App:
self._aioloop = _asyncio.setup_asyncio()
self.health_monitor = AppHealthMonitor()
- # Only proceed if our config file is healthy so we don't
- # overwrite a broken one or whatnot and wipe out data.
- if not self.config_file_healthy:
- if self.classic is not None:
- handled = self.classic.show_config_error_window()
- if handled:
- return
-
- # For now on other systems we just overwrite the bum config.
- # At this point settings are already set; lets just commit
- # them to disk.
- _appconfig.commit_app_config(force=True)
-
# __FEATURESET_APP_SUBSYSTEM_CREATE_BEGIN__
# This section generated by batools.appmodule; do not edit.
@@ -795,6 +800,7 @@ class App:
async def _shutdown(self) -> None:
import asyncio
+ _babase.lock_all_input()
try:
async with asyncio.TaskGroup() as task_group:
for task_coro in self._shutdown_tasks:
@@ -890,23 +896,45 @@ class App:
import asyncio
# Spin and wait for anything blocking shutdown to complete.
+ starttime = _babase.apptime()
_babase.lifecyclelog('shutdown-suppress wait begin')
while _babase.shutdown_suppress_count() > 0:
await asyncio.sleep(0.001)
_babase.lifecyclelog('shutdown-suppress wait end')
+ duration = _babase.apptime() - starttime
+ if duration > 1.0:
+ logging.warning(
+ 'Shutdown-suppressions lasted longer than ideal '
+ '(%.2f seconds).',
+ duration,
+ )
- async def _fade_for_shutdown(self) -> None:
+ async def _fade_and_shutdown_graphics(self) -> None:
import asyncio
- # Kick off a fade, block input, and wait for a short bit.
- # Ideally most shutdown activity completes during the fade so
- # there's no tangible wait.
- _babase.lifecyclelog('fade-for-shutdown begin')
+ # Kick off a short fade and give it time to complete.
+ _babase.lifecyclelog('fade-and-shutdown-graphics begin')
_babase.fade_screen(False, time=0.15)
- _babase.lock_all_input()
- # _babase.getsimplesound('swish2').play()
await asyncio.sleep(0.15)
- _babase.lifecyclelog('fade-for-shutdown end')
+
+ # Now tell the graphics system to go down and wait until
+ # it has done so.
+ _babase.graphics_shutdown_begin()
+ while not _babase.graphics_shutdown_is_complete():
+ await asyncio.sleep(0.01)
+ _babase.lifecyclelog('fade-and-shutdown-graphics end')
+
+ async def _fade_and_shutdown_audio(self) -> None:
+ import asyncio
+
+ # Tell the audio system to go down and give it a bit of
+ # time to do so gracefully.
+ _babase.lifecyclelog('fade-and-shutdown-audio begin')
+ _babase.audio_shutdown_begin()
+ await asyncio.sleep(0.15)
+ while not _babase.audio_shutdown_is_complete():
+ await asyncio.sleep(0.01)
+ _babase.lifecyclelog('fade-and-shutdown-audio end')
def _threadpool_no_wait_done(self, fut: Future) -> None:
try:
diff --git a/src/assets/ba_data/python/babase/_appconfig.py b/src/assets/ba_data/python/babase/_appconfig.py
index f34ff8ab..92efc73c 100644
--- a/src/assets/ba_data/python/babase/_appconfig.py
+++ b/src/assets/ba_data/python/babase/_appconfig.py
@@ -101,15 +101,13 @@ class AppConfig(dict):
self.commit()
-def read_app_config() -> tuple[AppConfig, bool]:
+def read_app_config() -> AppConfig:
"""Read the app config."""
import os
import json
- config_file_healthy = False
-
- # NOTE: it is assumed that this only gets called once and the
- # config object will not change from here on out
+ # NOTE: it is assumed that this only gets called once and the config
+ # object will not change from here on out
config_file_path = _babase.app.env.config_file_path
config_contents = ''
try:
@@ -119,20 +117,16 @@ def read_app_config() -> tuple[AppConfig, bool]:
config = AppConfig(json.loads(config_contents))
else:
config = AppConfig()
- config_file_healthy = True
except Exception:
logging.exception(
- "Error reading config file at time %.3f: '%s'.",
+ "Error reading config file '%s' at time %.3f.\n"
+ "Backing up broken config to'%s.broken'.",
+ config_file_path,
_babase.apptime(),
config_file_path,
)
- # Whenever this happens lets back up the broken one just in case it
- # gets overwritten accidentally.
- logging.info(
- "Backing up current config file to '%s.broken'", config_file_path
- )
try:
import shutil
@@ -141,23 +135,10 @@ def read_app_config() -> tuple[AppConfig, bool]:
logging.exception('Error copying broken config.')
config = AppConfig()
- # Now attempt to read one of our 'prev' backup copies.
- prev_path = config_file_path + '.prev'
- try:
- if os.path.exists(prev_path):
- with open(prev_path, encoding='utf-8') as infile:
- config_contents = infile.read()
- config = AppConfig(json.loads(config_contents))
- else:
- config = AppConfig()
- config_file_healthy = True
- logging.info('Successfully read backup config.')
- except Exception:
- logging.exception('Error reading prev backup config.')
- return config, config_file_healthy
+ return config
-def commit_app_config(force: bool = False) -> None:
+def commit_app_config() -> None:
"""Commit the config to persistent storage.
Category: **General Utility Functions**
@@ -167,10 +148,4 @@ def commit_app_config(force: bool = False) -> None:
plus = _babase.app.plus
assert plus is not None
- if not _babase.app.config_file_healthy and not force:
- logging.warning(
- 'Current config file is broken; '
- 'skipping write to avoid losing settings.'
- )
- return
plus.mark_config_dirty()
diff --git a/src/assets/ba_data/python/babase/_appmode.py b/src/assets/ba_data/python/babase/_appmode.py
index fcaa77a9..cfe01f7b 100644
--- a/src/assets/ba_data/python/babase/_appmode.py
+++ b/src/assets/ba_data/python/babase/_appmode.py
@@ -31,6 +31,7 @@ class AppMode:
AppExperience associated with the AppMode must be supported by
the current app and runtime environment.
"""
+ # FIXME: check AppExperience.
return cls._supports_intent(intent)
@classmethod
@@ -51,3 +52,10 @@ class AppMode:
def on_deactivate(self) -> None:
"""Called when the mode is being deactivated."""
+
+ def on_app_active_changed(self) -> None:
+ """Called when babase.app.active changes.
+
+ The app-mode may want to take action such as pausing a running
+ game in such cases.
+ """
diff --git a/src/assets/ba_data/python/babase/_apputils.py b/src/assets/ba_data/python/babase/_apputils.py
index 655bc8b3..59d76481 100644
--- a/src/assets/ba_data/python/babase/_apputils.py
+++ b/src/assets/ba_data/python/babase/_apputils.py
@@ -325,7 +325,7 @@ def dump_app_state(
)
-def log_dumped_app_state() -> None:
+def log_dumped_app_state(from_previous_run: bool = False) -> None:
"""If an app-state dump exists, log it and clear it. No-op otherwise."""
try:
@@ -352,8 +352,13 @@ def log_dumped_app_state() -> None:
metadata = dataclass_from_json(DumpedAppStateMetadata, appstatedata)
+ header = (
+ 'Found app state dump from previous app run'
+ if from_previous_run
+ else 'App state dump'
+ )
out += (
- f'App state dump:\nReason: {metadata.reason}\n'
+ f'{header}:\nReason: {metadata.reason}\n'
f'Time: {metadata.app_time:.2f}'
)
tbpath = os.path.join(
@@ -383,7 +388,7 @@ class AppHealthMonitor(AppSubsystem):
def on_app_loading(self) -> None:
# If any traceback dumps happened last run, log and clear them.
- log_dumped_app_state()
+ log_dumped_app_state(from_previous_run=True)
def _app_monitor_thread_main(self) -> None:
_babase.set_thread_name('ballistica app-monitor')
diff --git a/src/assets/ba_data/python/babase/_cloud.py b/src/assets/ba_data/python/babase/_cloud.py
index b380bc3b..3f5643db 100644
--- a/src/assets/ba_data/python/babase/_cloud.py
+++ b/src/assets/ba_data/python/babase/_cloud.py
@@ -26,6 +26,11 @@ DEBUG_LOG = False
class CloudSubsystem(AppSubsystem):
"""Manages communication with cloud components."""
+ @property
+ def connected(self) -> bool:
+ """Property equivalent of CloudSubsystem.is_connected()."""
+ return self.is_connected()
+
def is_connected(self) -> bool:
"""Return whether a connection to the cloud is present.
diff --git a/src/assets/ba_data/python/babase/_devconsole.py b/src/assets/ba_data/python/babase/_devconsole.py
index 9e0ca0cd..fb63c9e7 100644
--- a/src/assets/ba_data/python/babase/_devconsole.py
+++ b/src/assets/ba_data/python/babase/_devconsole.py
@@ -3,6 +3,7 @@
"""Dev-Console functionality."""
from __future__ import annotations
+import os
from typing import TYPE_CHECKING
from dataclasses import dataclass
import logging
@@ -154,9 +155,10 @@ class DevConsoleSubsystem:
# All tabs in the dev-console. Add your own stuff here via
# plugins or whatnot.
self.tabs: list[DevConsoleTabEntry] = [
- DevConsoleTabEntry('Python', DevConsoleTabPython),
- DevConsoleTabEntry('Test', DevConsoleTabTest),
+ DevConsoleTabEntry('Python', DevConsoleTabPython)
]
+ if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1':
+ self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest))
self.is_refreshing = False
def do_refresh_tab(self, tabname: str) -> None:
diff --git a/src/assets/ba_data/python/babase/_env.py b/src/assets/ba_data/python/babase/_env.py
index 1e386689..2af28c23 100644
--- a/src/assets/ba_data/python/babase/_env.py
+++ b/src/assets/ba_data/python/babase/_env.py
@@ -185,10 +185,8 @@ def _feed_logs_to_babase(log_handler: LogHandler) -> None:
def _on_log(entry: LogEntry) -> None:
# Forward this along to the engine to display in the in-app
# console, in the Android log, etc.
- _babase.display_log(
- name=entry.name,
- level=entry.level.name,
- message=entry.message,
+ _babase.emit_log(
+ name=entry.name, level=entry.level.name, message=entry.message
)
# We also want to feed some logs to the old v1-cloud-log system.
diff --git a/src/assets/ba_data/python/babase/_hooks.py b/src/assets/ba_data/python/babase/_hooks.py
index 90dde9be..535d3084 100644
--- a/src/assets/ba_data/python/babase/_hooks.py
+++ b/src/assets/ba_data/python/babase/_hooks.py
@@ -33,18 +33,47 @@ def reset_to_main_menu() -> None:
logging.warning('reset_to_main_menu: no-op due to classic not present.')
-def set_config_fullscreen_on() -> None:
+def get_v2_account_id() -> str | None:
+ """Return the current V2 account id if signed in, or None if not."""
+ try:
+ plus = _babase.app.plus
+ if plus is not None:
+ account = plus.accounts.primary
+ if account is not None:
+ accountid = account.accountid
+ # (Avoids mypy complaints when plus is not present)
+ assert isinstance(accountid, (str, type(None)))
+ return accountid
+ return None
+ except Exception:
+ logging.exception('Error fetching v2 account id.')
+ return None
+
+
+def store_config_fullscreen_on() -> None:
"""The OS has changed our fullscreen state and we should take note."""
_babase.app.config['Fullscreen'] = True
_babase.app.config.commit()
-def set_config_fullscreen_off() -> None:
+def store_config_fullscreen_off() -> None:
"""The OS has changed our fullscreen state and we should take note."""
_babase.app.config['Fullscreen'] = False
_babase.app.config.commit()
+def set_config_fullscreen_on() -> None:
+ """Set and store fullscreen state"""
+ _babase.app.config['Fullscreen'] = True
+ _babase.app.config.apply_and_commit()
+
+
+def set_config_fullscreen_off() -> None:
+ """The OS has changed our fullscreen state and we should take note."""
+ _babase.app.config['Fullscreen'] = False
+ _babase.app.config.apply_and_commit()
+
+
def not_signed_in_screen_message() -> None:
from babase._language import Lstr
@@ -111,6 +140,14 @@ def error_message() -> None:
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
+def success_message() -> None:
+ from babase._language import Lstr
+
+ if _babase.app.env.gui:
+ _babase.getsimplesound('dingSmall').play()
+ _babase.screenmessage(Lstr(resource='successText'), color=(0, 1, 0))
+
+
def purchase_not_valid_error() -> None:
from babase._language import Lstr
@@ -300,6 +337,7 @@ def implicit_sign_in(
from bacommon.login import LoginType
assert _babase.app.plus is not None
+
_babase.app.plus.accounts.on_implicit_sign_in(
login_type=LoginType(login_type_str),
login_id=login_id,
@@ -377,3 +415,17 @@ def string_edit_adapter_can_be_replaced(adapter: StringEditAdapter) -> bool:
def get_dev_console_tab_names() -> list[str]:
"""Return the current set of dev-console tab names."""
return [t.name for t in _babase.app.devconsole.tabs]
+
+
+def unsupported_controller_message(name: str) -> None:
+ """Print a message when an unsupported controller is connected."""
+ from babase._language import Lstr
+
+ # Ick; this can get called early in the bootstrapping process
+ # before we're allowed to load assets. Guard against that.
+ if _babase.asset_loads_allowed():
+ _babase.getsimplesound('error').play()
+ _babase.screenmessage(
+ Lstr(resource='unsupportedControllerText', subs=[('${NAME}', name)]),
+ color=(1, 0, 0),
+ )
diff --git a/src/assets/ba_data/python/babase/_login.py b/src/assets/ba_data/python/babase/_login.py
index f6c761c4..39cfa0ed 100644
--- a/src/assets/ba_data/python/babase/_login.py
+++ b/src/assets/ba_data/python/babase/_login.py
@@ -20,6 +20,13 @@ if TYPE_CHECKING:
DEBUG_LOG = False
+@dataclass
+class LoginInfo:
+ """Basic info about a login available in the app.plus.accounts section."""
+
+ name: str
+
+
class LoginAdapter:
"""Allows using implicit login types in an explicit way.
@@ -138,7 +145,7 @@ class LoginAdapter:
is actually being used by the app. It should therefore register
unlocked achievements, leaderboard scores, allow viewing native
UIs, etc. When not active it should ignore everything and behave
- as if logged out, even if it technically is still logged in.
+ as if signed out, even if it technically is still signed in.
"""
assert _babase.in_logic_thread()
del active # Unused.
@@ -149,7 +156,7 @@ class LoginAdapter:
result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
description: str,
) -> None:
- """Attempt an explicit sign in via this adapter.
+ """Attempt to sign in via this adapter.
This can be called even if the back-end is not implicitly signed in;
the adapter will attempt to sign in if possible. An exception will
@@ -161,7 +168,7 @@ class LoginAdapter:
# Have been seeing multiple sign-in attempts come through
# nearly simultaneously which can be problematic server-side.
# Let's error if a sign-in attempt is made within a few seconds
- # of the last one to address this.
+ # of the last one to try and address this.
now = time.monotonic()
appnow = _babase.apptime()
if self._last_sign_in_time is not None:
@@ -229,6 +236,7 @@ class LoginAdapter:
def _got_sign_in_response(
response: bacommon.cloud.SignInResponse | Exception,
) -> None:
+ # This likely means we couldn't communicate with the server.
if isinstance(response, Exception):
if DEBUG_LOG:
logging.debug(
@@ -239,20 +247,18 @@ class LoginAdapter:
)
_babase.pushcall(Call(result_cb, self, response))
else:
- if DEBUG_LOG:
- logging.debug(
- 'LoginAdapter: %s adapter got successful'
- ' sign-in response',
- self.login_type.name,
- )
+ # This means our credentials were explicitly rejected.
if response.credentials is None:
result2: LoginAdapter.SignInResult | Exception = (
- RuntimeError(
- 'No credentials returned after'
- ' submitting sign-in-token.'
- )
+ RuntimeError('Sign-in-token was rejected.')
)
else:
+ if DEBUG_LOG:
+ logging.debug(
+ 'LoginAdapter: %s adapter got successful'
+ ' sign-in response',
+ self.login_type.name,
+ )
result2 = self.SignInResult(
credentials=response.credentials
)
@@ -269,7 +275,7 @@ class LoginAdapter:
on_response=_got_sign_in_response,
)
- # Kick off the process by fetching a sign-in token.
+ # Kick off the sign-in process by fetching a sign-in token.
self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
def is_back_end_active(self) -> bool:
@@ -282,11 +288,10 @@ class LoginAdapter:
"""Get a sign-in token from the adapter back end.
This token is then passed to the master-server to complete the
- login process.
- The adapter can use this opportunity to bring up account creation
- UI, call its internal sign_in function, etc. as needed.
- The provided completion_cb should then be called with either a token
- or None if sign in failed or was cancelled.
+ sign-in process. The adapter can use this opportunity to bring
+ up account creation UI, call its internal sign_in function, etc.
+ as needed. The provided completion_cb should then be called with
+ either a token or None if sign in failed or was cancelled.
"""
from babase._general import Call
@@ -295,7 +300,7 @@ class LoginAdapter:
def _update_implicit_login_state(self) -> None:
# If we've received an implicit login state, schedule it to be
- # sent along to the app. We wait until on-app-launch has been
+ # sent along to the app. We wait until on-app-loading has been
# called so that account-client-v2 has had a chance to load
# any existing state so it can properly respond to this.
if self._implicit_login_state_dirty and self._on_app_loading_called:
@@ -340,8 +345,8 @@ class LoginAdapter:
class LoginAdapterNative(LoginAdapter):
"""A login adapter that does its work in the native layer."""
- def __init__(self) -> None:
- super().__init__(LoginType.GPGS)
+ def __init__(self, login_type: LoginType) -> None:
+ super().__init__(login_type)
# Store int ids for in-flight attempts since they may go through
# various platform layers and back.
@@ -375,3 +380,13 @@ class LoginAdapterNative(LoginAdapter):
class LoginAdapterGPGS(LoginAdapterNative):
"""Google Play Game Services adapter."""
+
+ def __init__(self) -> None:
+ super().__init__(LoginType.GPGS)
+
+
+class LoginAdapterGameCenter(LoginAdapterNative):
+ """Apple Game Center adapter."""
+
+ def __init__(self) -> None:
+ super().__init__(LoginType.GAME_CENTER)
diff --git a/src/assets/ba_data/python/babase/_meta.py b/src/assets/ba_data/python/babase/_meta.py
index 5e2a0ec5..76f88b76 100644
--- a/src/assets/ba_data/python/babase/_meta.py
+++ b/src/assets/ba_data/python/babase/_meta.py
@@ -24,6 +24,8 @@ if TYPE_CHECKING:
# instead of these or to make the meta system aware of arbitrary classes.
EXPORT_CLASS_NAME_SHORTCUTS: dict[str, str] = {
'plugin': 'babase.Plugin',
+ # DEPRECATED as of 12/2023. Currently am warning if finding these
+ # but should take this out eventually.
'keyboard': 'babase.Keyboard',
}
@@ -414,30 +416,27 @@ class DirectoryScan:
if export_class_name is not None:
classname = modulename + '.' + export_class_name
- # Since we'll soon have multiple versions of 'game'
- # classes we need to migrate people to using base
- # class names for them.
- if exporttypestr == 'game':
+ # Migrating away from the 'keyboard' name shortcut
+ # since it's specific to bauiv1; warn if we find it.
+ if exporttypestr == 'keyboard':
logging.warning(
"metascan: %s:%d: '# ba_meta export"
- " game' tag should be replaced by '# ba_meta"
- " export bascenev1.GameActivity'.",
+ " keyboard' tag should be replaced by '# ba_meta"
+ " export bauiv1.Keyboard'.",
subpath,
lindex + 1,
)
self.results.announce_errors_occurred = True
- else:
- # If export type is one of our shortcuts, sub in the
- # actual class path. Otherwise assume its a classpath
- # itself.
- exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(
- exporttypestr
- )
- if exporttype is None:
- exporttype = exporttypestr
- self.results.exports.setdefault(exporttype, []).append(
- classname
- )
+
+ # If export type is one of our shortcuts, sub in the
+ # actual class path. Otherwise assume its a classpath
+ # itself.
+ exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(exporttypestr)
+ if exporttype is None:
+ exporttype = exporttypestr
+ self.results.exports.setdefault(exporttype, []).append(
+ classname
+ )
def _get_export_class_name(
self, subpath: Path, lines: list[str], lindex: int
diff --git a/src/assets/ba_data/python/babase/modutils.py b/src/assets/ba_data/python/babase/modutils.py
index 240c5cbf..7780d1f6 100644
--- a/src/assets/ba_data/python/babase/modutils.py
+++ b/src/assets/ba_data/python/babase/modutils.py
@@ -104,8 +104,8 @@ def show_user_scripts() -> None:
_error.print_exception('error writing about_this_folder stuff')
- # On a few platforms we try to open the dir in the UI.
- if app.classic is not None and app.classic.platform in ['mac', 'windows']:
+ # On platforms that support it, open the dir in the UI.
+ if _babase.supports_open_dir_externally():
_babase.open_dir_externally(env.python_directory_user)
# Otherwise we just print a pretty version of it.
diff --git a/src/assets/ba_data/python/baclassic/_accountv1.py b/src/assets/ba_data/python/baclassic/_accountv1.py
index f4667edf..e732f135 100644
--- a/src/assets/ba_data/python/baclassic/_accountv1.py
+++ b/src/assets/ba_data/python/baclassic/_accountv1.py
@@ -302,6 +302,11 @@ class AccountV1Subsystem:
"""(internal)"""
plus = babase.app.plus
if plus is None:
+ import logging
+
+ logging.warning(
+ 'Error adding pending promo code; plus not present.'
+ )
babase.screenmessage(
babase.Lstr(resource='errorText'), color=(1, 0, 0)
)
diff --git a/src/assets/ba_data/python/baclassic/_ads.py b/src/assets/ba_data/python/baclassic/_ads.py
index 0617a6ea..2373df36 100644
--- a/src/assets/ba_data/python/baclassic/_ads.py
+++ b/src/assets/ba_data/python/baclassic/_ads.py
@@ -4,10 +4,11 @@
from __future__ import annotations
import time
+import asyncio
+import logging
from typing import TYPE_CHECKING
import babase
-import bauiv1
import bascenev1
if TYPE_CHECKING:
@@ -31,6 +32,7 @@ class AdsSubsystem:
self.last_in_game_ad_remove_message_show_time: float | None = None
self.last_ad_completion_time: float | None = None
self.last_ad_was_short = False
+ self._fallback_task: asyncio.Task | None = None
def do_remove_in_game_ads_message(self) -> None:
"""(internal)"""
@@ -69,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,
@@ -78,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."""
@@ -94,7 +98,7 @@ class AdsSubsystem:
show = True
# No ads without net-connections, etc.
- if not bauiv1.can_show_ad():
+ if not plus.can_show_ad():
show = False
if classic.accounts.have_pro():
show = False # Pro disables interstitials.
@@ -132,7 +136,7 @@ class AdsSubsystem:
# ad-show-threshold and see if we should *actually* show
# (we reach our threshold faster the longer we've been
# playing).
- base = 'ads' if bauiv1.has_video_ads() else 'ads2'
+ base = 'ads' if plus.has_video_ads() else 'ads2'
min_lc = plus.get_v1_account_misc_read_val(base + '.minLC', 0.0)
max_lc = plus.get_v1_account_misc_read_val(base + '.maxLC', 5.0)
min_lc_scale = plus.get_v1_account_misc_read_val(
@@ -181,36 +185,53 @@ class AdsSubsystem:
# If we're *still* cleared to show, actually tell the system to show.
if show:
- # As a safety-check, set up an object that will run
- # the completion callback if we've returned and sat for 10 seconds
- # (in case some random ad network doesn't properly deliver its
- # completion callback).
+ # As a safety-check, we set up an object that will run the
+ # completion callback if we've returned and sat for several
+ # seconds (in case some random ad network doesn't properly
+ # deliver its completion callback).
class _Payload:
def __init__(self, pcall: Callable[[], Any]):
self._call = pcall
self._ran = False
def run(self, fallback: bool = False) -> None:
- """Run fallback call (and issue a warning about it)."""
+ """Run the payload."""
assert app.classic is not None
if not self._ran:
if fallback:
lanst = app.classic.ads.last_ad_network_set_time
- print(
- 'ERROR: relying on fallback ad-callback! '
- 'last network: '
- + app.classic.ads.last_ad_network
- + ' (set '
- + str(int(time.time() - lanst))
- + 's ago); purpose='
- + app.classic.ads.last_ad_purpose
+ logging.error(
+ 'Relying on fallback ad-callback! '
+ 'last network: %s (set %s seconds ago);'
+ ' purpose=%s.',
+ app.classic.ads.last_ad_network,
+ time.time() - lanst,
+ app.classic.ads.last_ad_purpose,
)
babase.pushcall(self._call)
self._ran = True
payload = _Payload(call)
+
+ # Set up our backup.
with babase.ContextRef.empty():
- babase.apptimer(5.0, lambda: payload.run(fallback=True))
+ # Note to self: Previously this was a simple 5 second
+ # timer because the app got totally suspended while ads
+ # were showing (which delayed the timer), but these days
+ # the app may continue to run, so we need to be more
+ # careful and only fire the fallback after we see that
+ # the app has been front-and-center for several seconds.
+ async def add_fallback_task() -> None:
+ activesecs = 5
+ while activesecs > 0:
+ if babase.app.active:
+ activesecs -= 1
+ await asyncio.sleep(1.0)
+ payload.run(fallback=True)
+
+ _fallback_task = babase.app.aioloop.create_task(
+ add_fallback_task()
+ )
self.show_ad('between_game', on_completion_call=payload.run)
else:
babase.pushcall(call) # Just run the callback without the ad.
diff --git a/src/assets/ba_data/python/baclassic/_appdelegate.py b/src/assets/ba_data/python/baclassic/_appdelegate.py
index 4595bd63..1dd1a6b0 100644
--- a/src/assets/ba_data/python/baclassic/_appdelegate.py
+++ b/src/assets/ba_data/python/baclassic/_appdelegate.py
@@ -41,5 +41,6 @@ class AppDelegate:
sessiontype,
settings,
completion_call=completion_call,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=False, # Disable check since we don't know.
)
diff --git a/src/assets/ba_data/python/baclassic/_benchmark.py b/src/assets/ba_data/python/baclassic/_benchmark.py
index 18e04335..569f9d64 100644
--- a/src/assets/ba_data/python/baclassic/_benchmark.py
+++ b/src/assets/ba_data/python/baclassic/_benchmark.py
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
import babase
import bascenev1
+import _baclassic
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -54,7 +55,6 @@ def run_stress_test(
round_duration: int = 30,
) -> None:
"""Run a stress test."""
- from babase import modutils
babase.screenmessage(
"Beginning stress test.. use 'End Test' to stop testing.",
@@ -69,22 +69,12 @@ def run_stress_test(
'round_duration': round_duration,
}
)
- babase.apptimer(
- 7.0,
- babase.Call(
- babase.screenmessage,
- (
- 'stats will be written to '
- + modutils.get_human_readable_user_scripts_path()
- + '/stress_test_stats.csv'
- ),
- ),
- )
def stop_stress_test() -> None:
"""End a running stress test."""
- babase.set_stress_testing(False, 0)
+
+ _baclassic.set_stress_testing(False, 0)
assert babase.app.classic is not None
try:
if babase.app.classic.stress_test_reset_timer is not None:
@@ -134,14 +124,14 @@ def start_stress_test(args: dict[str, Any]) -> None:
babase.Call(bascenev1.new_host_session, FreeForAllSession),
),
)
- babase.set_stress_testing(True, args['player_count'])
+ _baclassic.set_stress_testing(True, args['player_count'])
babase.app.classic.stress_test_reset_timer = babase.AppTimer(
args['round_duration'], babase.Call(_reset_stress_test, args)
)
def _reset_stress_test(args: dict[str, Any]) -> None:
- babase.set_stress_testing(False, args['player_count'])
+ _baclassic.set_stress_testing(False, args['player_count'])
babase.screenmessage('Resetting stress test...')
session = bascenev1.get_foreground_host_session()
assert session is not None
diff --git a/src/assets/ba_data/python/baclassic/_input.py b/src/assets/ba_data/python/baclassic/_input.py
index e3cc0546..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
@@ -40,7 +39,14 @@ def get_input_device_mapped_value(
mapping = ccfgs[devicename][unique_id]
elif 'default' in ccfgs[devicename]:
mapping = ccfgs[devicename]['default']
- if mapping is not None:
+
+ # We now use the config mapping *only* if it is not empty.
+ # There have been cases of config writing code messing up
+ # and leaving empty dicts in the app config, which currently
+ # leaves the device unusable. Alternatively, we'd perhaps
+ # want to fall back to defaults for individual missing
+ # values, but that is a bigger change we can make later.
+ if isinstance(mapping, dict) and mapping:
return mapping.get(name, -1)
if platform == 'windows':
@@ -76,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 {
@@ -175,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,
@@ -303,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 {
@@ -380,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.
@@ -445,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.
@@ -533,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',
@@ -544,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:
@@ -569,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/_music.py b/src/assets/ba_data/python/baclassic/_music.py
index bdda3a3a..8f439d79 100644
--- a/src/assets/ba_data/python/baclassic/_music.py
+++ b/src/assets/ba_data/python/baclassic/_music.py
@@ -165,15 +165,16 @@ class MusicSubsystem:
def supports_soundtrack_entry_type(self, entry_type: str) -> bool:
"""Return whether provided soundtrack entry type is supported here."""
- uas = babase.env()['legacy_user_agent_string']
- assert isinstance(uas, str)
-
- # FIXME: Generalize this.
+ # Note to self; can't access babase.app.classic here because
+ # we are called during its construction.
+ env = babase.env()
+ platform = env.get('platform')
+ assert isinstance(platform, str)
if entry_type == 'iTunesPlaylist':
- return 'Mac' in uas
+ return platform == 'mac' and babase.is_xcode_build()
if entry_type in ('musicFile', 'musicFolder'):
return (
- 'android' in uas
+ platform == 'android'
and babase.android_get_external_files_dir() is not None
)
if entry_type == 'default':
diff --git a/src/assets/ba_data/python/baclassic/_servermode.py b/src/assets/ba_data/python/baclassic/_servermode.py
index 8cb9b911..45ee6eb9 100644
--- a/src/assets/ba_data/python/baclassic/_servermode.py
+++ b/src/assets/ba_data/python/baclassic/_servermode.py
@@ -423,6 +423,10 @@ class ServerController:
bascenev1.set_public_party_stats_url(self._config.stats_url)
bascenev1.set_public_party_enabled(self._config.party_is_public)
+ bascenev1.set_player_rejoin_cooldown(
+ self._config.player_rejoin_cooldown
+ )
+
# And here.. we.. go.
if self._config.stress_test_players is not None:
# Special case: run a stress test.
diff --git a/src/assets/ba_data/python/baclassic/_subsystem.py b/src/assets/ba_data/python/baclassic/_subsystem.py
index 757661fc..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
@@ -627,15 +618,6 @@ class ClassicSubsystem(babase.AppSubsystem):
"""(internal)"""
return bascenev1.get_foreground_host_activity()
- def show_config_error_window(self) -> bool:
- """(internal)"""
- if self.platform in ('mac', 'linux', 'windows'):
- from bauiv1lib.configerror import ConfigErrorWindow
-
- babase.pushcall(ConfigErrorWindow)
- return True
- return False
-
def value_test(
self,
arg: str,
@@ -809,5 +791,6 @@ class ClassicSubsystem(babase.AppSubsystem):
bauiv1.getsound('swish').play()
babase.app.ui_v1.set_main_menu_window(
- MainMenuWindow().get_root_widget()
+ MainMenuWindow().get_root_widget(),
+ from_window=False, # Disable check here.
)
diff --git a/src/assets/ba_data/python/baclassic/macmusicapp.py b/src/assets/ba_data/python/baclassic/macmusicapp.py
index 1b87fbf1..27b0e126 100644
--- a/src/assets/ba_data/python/baclassic/macmusicapp.py
+++ b/src/assets/ba_data/python/baclassic/macmusicapp.py
@@ -80,14 +80,13 @@ class _MacMusicAppThread(threading.Thread):
def run(self) -> None:
"""Run the Music.app thread."""
babase.set_thread_name('BA_MacMusicAppThread')
- babase.mac_music_app_init()
# Let's mention to the user we're launching Music.app in case
# it causes any funny business (this used to background the app
# sometimes, though I think that is fixed now)
def do_print() -> None:
babase.apptimer(
- 1.0,
+ 0.5,
babase.Call(
babase.screenmessage,
babase.Lstr(resource='usingItunesText'),
@@ -97,9 +96,8 @@ class _MacMusicAppThread(threading.Thread):
babase.pushcall(do_print, from_other_thread=True)
- # Here we grab this to force the actual launch.
- babase.mac_music_app_get_volume()
- babase.mac_music_app_get_library_source()
+ babase.mac_music_app_init()
+
done = False
while not done:
self._commands_available.wait()
diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py
index 59ccd7ff..c45c1c54 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -40,7 +40,7 @@ if TYPE_CHECKING:
# the last load. Either way, however, multiple execs will happen in some
# form.
#
-# So we need to do a few things to handle that situation gracefully.
+# To handle that situation gracefully, we need to do a few things:
#
# - First, we need to store any mutable global state in the __main__
# module; not in ourself. This way, alternate versions of ourself will
@@ -48,12 +48,12 @@ if TYPE_CHECKING:
#
# - Second, we should avoid the use of isinstance and similar calls for
# our types. An EnvConfig we create would technically be a different
-# type than that created by an alternate baenv.
+# type than an EnvConfig created by an alternate baenv.
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 21445
-TARGET_BALLISTICA_VERSION = '1.7.28'
+TARGET_BALLISTICA_BUILD = 21743
+TARGET_BALLISTICA_VERSION = '1.7.33'
@dataclass
diff --git a/src/assets/ba_data/python/baplus/_subsystem.py b/src/assets/ba_data/python/baplus/_subsystem.py
index 00d4f0bf..d38ca31a 100644
--- a/src/assets/ba_data/python/baplus/_subsystem.py
+++ b/src/assets/ba_data/python/baplus/_subsystem.py
@@ -1,6 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
-"""Provides classic app subsystem."""
+"""Provides plus app subsystem."""
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -249,3 +249,41 @@ class PlusSubsystem(AppSubsystem):
) -> None:
"""(internal)"""
return _baplus.tournament_query(callback, args)
+
+ @staticmethod
+ def have_incentivized_ad() -> bool:
+ """Is an incentivized ad available?"""
+ return _baplus.have_incentivized_ad()
+
+ @staticmethod
+ def has_video_ads() -> bool:
+ """Are video ads available?"""
+ return _baplus.has_video_ads()
+
+ @staticmethod
+ def can_show_ad() -> bool:
+ """Can we show an ad?"""
+ return _baplus.can_show_ad()
+
+ @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/bascenev1/__init__.py b/src/assets/ba_data/python/bascenev1/__init__.py
index 605b363c..e96deb0b 100644
--- a/src/assets/ba_data/python/bascenev1/__init__.py
+++ b/src/assets/ba_data/python/bascenev1/__init__.py
@@ -78,6 +78,7 @@ from _bascenev1 import (
end_host_scanning,
get_chat_messages,
get_connection_to_host_info,
+ get_connection_to_host_info_2,
get_foreground_host_activity,
get_foreground_host_session,
get_game_port,
@@ -202,6 +203,7 @@ from bascenev1._multiteamsession import (
DEFAULT_TEAM_NAMES,
)
from bascenev1._music import MusicType, setmusic
+from bascenev1._net import HostInfo
from bascenev1._nodeactor import NodeActor
from bascenev1._powerup import get_default_powerup_distribution
from bascenev1._profile import (
@@ -226,7 +228,7 @@ from bascenev1._settings import (
IntSetting,
Setting,
)
-from bascenev1._session import Session
+from bascenev1._session import Session, set_player_rejoin_cooldown
from bascenev1._stats import PlayerScoredMessage, PlayerRecord, Stats
from bascenev1._team import SessionTeam, Team, EmptyTeam
from bascenev1._teamgame import TeamGameActivity
@@ -303,6 +305,7 @@ __all__ = [
'GameTip',
'get_chat_messages',
'get_connection_to_host_info',
+ 'get_connection_to_host_info_2',
'get_default_free_for_all_playlist',
'get_default_teams_playlist',
'get_default_powerup_distribution',
@@ -338,6 +341,7 @@ __all__ = [
'have_connected_clients',
'have_touchscreen_input',
'HitMessage',
+ 'HostInfo',
'host_scan_cycle',
'ImpactDamageMessage',
'increment_analytics_count',
@@ -415,6 +419,7 @@ __all__ = [
'set_public_party_name',
'set_public_party_queue_enabled',
'set_public_party_stats_url',
+ 'set_player_rejoin_cooldown',
'set_replay_speed_exponent',
'set_touchscreen_editing',
'setmusic',
diff --git a/src/assets/ba_data/python/bascenev1/_appmode.py b/src/assets/ba_data/python/bascenev1/_appmode.py
index 10d1c320..9408865d 100644
--- a/src/assets/ba_data/python/bascenev1/_appmode.py
+++ b/src/assets/ba_data/python/bascenev1/_appmode.py
@@ -6,7 +6,13 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from bacommon.app import AppExperience
-from babase import AppMode, AppIntentExec, AppIntentDefault
+from babase import (
+ app,
+ AppMode,
+ AppIntentExec,
+ AppIntentDefault,
+ invoke_main_menu,
+)
import _bascenev1
@@ -40,3 +46,9 @@ class SceneV1AppMode(AppMode):
def on_deactivate(self) -> None:
# Let the native layer do its thing.
_bascenev1.on_app_mode_deactivate()
+
+ def on_app_active_changed(self) -> None:
+ # If we've gone inactive, bring up the main menu, which has the
+ # side effect of pausing the action (when possible).
+ if not app.active:
+ invoke_main_menu()
diff --git a/src/assets/ba_data/python/bascenev1/_gameactivity.py b/src/assets/ba_data/python/bascenev1/_gameactivity.py
index 7135b6d4..ad9bf0a5 100644
--- a/src/assets/ba_data/python/bascenev1/_gameactivity.py
+++ b/src/assets/ba_data/python/bascenev1/_gameactivity.py
@@ -438,10 +438,16 @@ class GameActivity(Activity[PlayerT, TeamT]):
assert classic is not None
continues_window = classic.continues_window
+ # Turning these off. I want to migrate towards monetization that
+ # feels less pay-to-win-ish.
+ allow_continues = False
+
plus = babase.app.plus
try:
- if plus is not None and plus.get_v1_account_misc_read_val(
- 'enableContinues', False
+ if (
+ plus is not None
+ and plus.get_v1_account_misc_read_val('enableContinues', False)
+ and allow_continues
):
session = self.session
diff --git a/src/assets/ba_data/python/bascenev1/_net.py b/src/assets/ba_data/python/bascenev1/_net.py
new file mode 100644
index 00000000..279c329d
--- /dev/null
+++ b/src/assets/ba_data/python/bascenev1/_net.py
@@ -0,0 +1,24 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Functionality related to net play."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from dataclasses import dataclass
+
+if TYPE_CHECKING:
+ pass
+
+
+@dataclass
+class HostInfo:
+ """Info about a host."""
+
+ name: str
+ build_number: int
+
+ # Note this can be None for non-ip hosts such as bluetooth.
+ address: str | None
+
+ # Note this can be None for non-ip hosts such as bluetooth.
+ port: int | None
diff --git a/src/assets/ba_data/python/bascenev1/_session.py b/src/assets/ba_data/python/bascenev1/_session.py
index ffef80cd..d561f12d 100644
--- a/src/assets/ba_data/python/bascenev1/_session.py
+++ b/src/assets/ba_data/python/bascenev1/_session.py
@@ -3,6 +3,7 @@
"""Defines base session class."""
from __future__ import annotations
+import math
import weakref
import logging
from typing import TYPE_CHECKING
@@ -17,6 +18,17 @@ if TYPE_CHECKING:
import bascenev1
+# How many seconds someone who left the session (but not the party) must
+# wait to rejoin the session again. Intended to prevent game exploits
+# such as skipping respawn waits.
+_g_player_rejoin_cooldown: float = 0.0
+
+
+def set_player_rejoin_cooldown(cooldown: float) -> None:
+ """Set the cooldown for individual players rejoining after leaving."""
+ global _g_player_rejoin_cooldown # pylint: disable=global-statement
+ _g_player_rejoin_cooldown = max(0.0, cooldown)
+
class Session:
"""Defines a high level series of bascenev1.Activity-es.
@@ -203,6 +215,11 @@ class Session:
# Instantiate our session globals node which will apply its settings.
self._sessionglobalsnode = _bascenev1.newnode('sessionglobals')
+ # Rejoin cooldown stuff.
+ self._players_on_wait: dict = {}
+ self._player_requested_identifiers: dict = {}
+ self._waitlist_timers: dict = {}
+
@property
def context(self) -> bascenev1.ContextRef:
"""A context-ref pointing at this activity."""
@@ -253,6 +270,33 @@ class Session:
)
return False
+ # Rejoin cooldown.
+ identifier = player.get_v1_account_id()
+ if identifier:
+ leave_time = self._players_on_wait.get(identifier)
+ if leave_time:
+ diff = str(
+ math.ceil(
+ _g_player_rejoin_cooldown
+ - babase.apptime()
+ + leave_time
+ )
+ )
+ _bascenev1.broadcastmessage(
+ babase.Lstr(
+ translate=(
+ 'serverResponses',
+ 'You can join in ${COUNT} seconds.',
+ ),
+ subs=[('${COUNT}', diff)],
+ ),
+ color=(1, 1, 0),
+ clients=[player.inputdevice.client_id],
+ transient=True,
+ )
+ return False
+ self._player_requested_identifiers[player.id] = identifier
+
_bascenev1.getsound('dripity').play()
return True
@@ -270,6 +314,16 @@ class Session:
activity = self._activity_weak()
+ # Rejoin cooldown.
+ identifier = self._player_requested_identifiers.get(sessionplayer.id)
+ if identifier:
+ self._players_on_wait[identifier] = babase.apptime()
+ with babase.ContextRef.empty():
+ self._waitlist_timers[identifier] = babase.AppTimer(
+ _g_player_rejoin_cooldown,
+ babase.Call(self._remove_player_from_waitlist, identifier),
+ )
+
if not sessionplayer.in_game:
# Ok, the player is still in the lobby; simply remove them.
with self.context:
@@ -770,3 +824,9 @@ class Session:
if pass_to_activity:
activity.add_player(sessionplayer)
return sessionplayer
+
+ def _remove_player_from_waitlist(self, identifier: str) -> None:
+ try:
+ self._players_on_wait.pop(identifier)
+ except KeyError:
+ pass
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
index ce226cfb..8b335a19 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
@@ -9,6 +9,7 @@ import random
import logging
from typing import TYPE_CHECKING
+from bacommon.login import LoginType
import bascenev1 as bs
import bauiv1 as bui
@@ -59,29 +60,25 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
)
)
- self._account_type = (
- plus.get_v1_account_type()
- if plus.get_v1_account_state() == 'signed_in'
- else None
- )
-
self._game_service_icon_color: Sequence[float] | None
self._game_service_achievements_texture: bui.Texture | None
self._game_service_leaderboards_texture: bui.Texture | None
- if self._account_type == 'Game Center':
+ # Tie in to specific game services if they are active.
+ adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
+ gpgs_active = adapter is not None and adapter.is_back_end_active()
+ adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
+ game_center_active = (
+ adapter is not None and adapter.is_back_end_active()
+ )
+
+ if game_center_active:
self._game_service_icon_color = (1.0, 1.0, 1.0)
icon = bui.gettexture('gameCenterIcon')
self._game_service_achievements_texture = icon
self._game_service_leaderboards_texture = icon
self._account_has_achievements = True
- elif self._account_type == 'Game Circle':
- icon = bui.gettexture('gameCircleIcon')
- self._game_service_icon_color = (1, 1, 1)
- self._game_service_achievements_texture = icon
- self._game_service_leaderboards_texture = icon
- self._account_has_achievements = True
- elif self._account_type == 'Google Play':
+ elif gpgs_active:
self._game_service_icon_color = (0.8, 1.0, 0.6)
self._game_service_achievements_texture = bui.gettexture(
'googlePlayAchievementsIcon'
@@ -193,7 +190,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
super().__del__()
# If our UI is still up, kill it.
- if self._root_ui:
+ if self._root_ui and not self._root_ui.transitioning_out:
with bui.ContextRef.empty():
bui.containerwidget(edit=self._root_ui, transition='out_left')
@@ -287,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/bascenev1lib/actor/controlsguide.py b/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py
index 18854e5d..23c4a977 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py
@@ -35,7 +35,7 @@ class ControlsGuide(bs.Actor):
delay: is the time in seconds before the overlay fades in.
lifespan: if not None, the overlay will fade back out and die after
- that long (in milliseconds).
+ that long (in seconds).
bright: if True, brighter colors will be used; handy when showing
over gameplay but may be too bright for join-screens, etc.
@@ -50,6 +50,7 @@ class ControlsGuide(bs.Actor):
offs5 = 43.0 * scale
ouya = False
maxw = 50
+ xtweak = -2.8 * scale
self._lifespan = lifespan
self._dead = False
self._bright = bright
@@ -117,7 +118,7 @@ class ControlsGuide(bs.Actor):
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
- 'position': (pos[0], pos[1] - offs5),
+ 'position': (pos[0] + xtweak, pos[1] - offs5),
'color': clr,
},
)
@@ -145,7 +146,7 @@ class ControlsGuide(bs.Actor):
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
- 'position': (pos[0], pos[1] - offs5),
+ 'position': (pos[0] + xtweak, pos[1] - offs5),
'color': clr,
},
)
@@ -173,7 +174,7 @@ class ControlsGuide(bs.Actor):
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
- 'position': (pos[0], pos[1] - offs5),
+ 'position': (pos[0] + xtweak, pos[1] - offs5),
'color': clr,
},
)
@@ -201,7 +202,7 @@ class ControlsGuide(bs.Actor):
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
- 'position': (pos[0], pos[1] - offs5),
+ 'position': (pos[0] + xtweak, pos[1] - offs5),
'color': clr,
},
)
@@ -264,10 +265,19 @@ class ControlsGuide(bs.Actor):
bs.timer(delay, bs.WeakCall(self._start_updating))
@staticmethod
- def _meaningful_button_name(device: bs.InputDevice, button: int) -> str:
+ def _meaningful_button_name(
+ device: bs.InputDevice, button_name: str
+ ) -> str:
"""Return a flattened string button name; empty for non-meaningful."""
if not device.has_meaningful_button_names:
return ''
+ assert bs.app.classic is not None
+ button = bs.app.classic.get_input_device_mapped_value(
+ device, button_name
+ )
+ # -1 means unset; let's show that.
+ if button == -1:
+ return bs.Lstr(resource='configGamepadWindow.unsetText').evaluate()
return device.get_button_name(button).evaluate()
def _start_updating(self) -> None:
@@ -289,10 +299,10 @@ class ControlsGuide(bs.Actor):
def _check_fade_in(self) -> None:
assert bs.app.classic is not None
- # If we have a touchscreen, we only fade in if we have a player with
- # an input device that is *not* the touchscreen.
- # (otherwise it is confusing to see the touchscreen buttons right
- # next to our display buttons)
+ # If we have a touchscreen, we only fade in if we have a player
+ # with an input device that is *not* the touchscreen. Otherwise
+ # it is confusing to see the touchscreen buttons right next to
+ # our display buttons.
touchscreen: bs.InputDevice | None = bs.getinputdevice(
'TouchScreen', '#1', doraise=False
)
@@ -318,15 +328,7 @@ class ControlsGuide(bs.Actor):
'buttonBomb',
'buttonPickUp',
):
- if (
- self._meaningful_button_name(
- device,
- bs.app.classic.get_input_device_mapped_value(
- device, name
- ),
- )
- != ''
- ):
+ if self._meaningful_button_name(device, name) != '':
fade_in = True
break
if fade_in:
@@ -401,58 +403,30 @@ class ControlsGuide(bs.Actor):
# We only care about movement buttons in the case of keyboards.
if all_keyboards:
right_button_names.add(
- device.get_button_name(
- classic.get_input_device_mapped_value(
- device, 'buttonRight'
- )
- )
+ self._meaningful_button_name(device, 'buttonRight')
)
left_button_names.add(
- device.get_button_name(
- classic.get_input_device_mapped_value(
- device, 'buttonLeft'
- )
- )
+ self._meaningful_button_name(device, 'buttonLeft')
)
down_button_names.add(
- device.get_button_name(
- classic.get_input_device_mapped_value(
- device, 'buttonDown'
- )
- )
+ self._meaningful_button_name(device, 'buttonDown')
)
up_button_names.add(
- device.get_button_name(
- classic.get_input_device_mapped_value(
- device, 'buttonUp'
- )
- )
+ self._meaningful_button_name(device, 'buttonUp')
)
# Ignore empty values; things like the remote app or
# wiimotes can return these.
- bname = self._meaningful_button_name(
- device,
- classic.get_input_device_mapped_value(device, 'buttonPunch'),
- )
+ bname = self._meaningful_button_name(device, 'buttonPunch')
if bname != '':
punch_button_names.add(bname)
- bname = self._meaningful_button_name(
- device,
- classic.get_input_device_mapped_value(device, 'buttonJump'),
- )
+ bname = self._meaningful_button_name(device, 'buttonJump')
if bname != '':
jump_button_names.add(bname)
- bname = self._meaningful_button_name(
- device,
- classic.get_input_device_mapped_value(device, 'buttonBomb'),
- )
+ bname = self._meaningful_button_name(device, 'buttonBomb')
if bname != '':
bomb_button_names.add(bname)
- bname = self._meaningful_button_name(
- device,
- classic.get_input_device_mapped_value(device, 'buttonPickUp'),
- )
+ bname = self._meaningful_button_name(device, 'buttonPickUp')
if bname != '':
pickup_button_names.add(bname)
@@ -582,8 +556,8 @@ class ControlsGuide(bs.Actor):
if msg.immediate:
self._die()
else:
- # If they don't need immediate,
- # fade out our nodes and die later.
+ # If they don't need immediate, fade out our nodes and
+ # die later.
for node in self._nodes:
bs.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0})
bs.timer(3.1, bs.WeakCall(self._die))
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/spaz.py b/src/assets/ba_data/python/bascenev1lib/actor/spaz.py
index e38be539..71a31a4c 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/spaz.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/spaz.py
@@ -624,7 +624,7 @@ class Spaz(bs.Actor):
1000.0 * (tval + self.curse_time)
)
self._curse_timer = bs.Timer(
- 5.0, bs.WeakCall(self.curse_explode)
+ 5.0, bs.WeakCall(self.handlemessage, CurseExplodeMessage())
)
def equip_boxing_gloves(self) -> None:
diff --git a/src/assets/ba_data/python/bascenev1lib/mainmenu.py b/src/assets/ba_data/python/bascenev1lib/mainmenu.py
index 88b85249..6092f0ce 100644
--- a/src/assets/ba_data/python/bascenev1lib/mainmenu.py
+++ b/src/assets/ba_data/python/bascenev1lib/mainmenu.py
@@ -317,7 +317,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
from bauiv1lib.kiosk import KioskWindow
bs.app.ui_v1.set_main_menu_window(
- KioskWindow().get_root_widget()
+ KioskWindow().get_root_widget(),
+ from_window=False, # Disable check here.
)
# ..or in normal cases go back to the main menu
else:
@@ -326,14 +327,16 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
from bauiv1lib.gather import GatherWindow
bs.app.ui_v1.set_main_menu_window(
- GatherWindow(transition=None).get_root_widget()
+ GatherWindow(transition=None).get_root_widget(),
+ from_window=False, # Disable check here.
)
elif main_menu_location == 'Watch':
# pylint: disable=cyclic-import
from bauiv1lib.watch import WatchWindow
bs.app.ui_v1.set_main_menu_window(
- WatchWindow(transition=None).get_root_widget()
+ WatchWindow(transition=None).get_root_widget(),
+ from_window=False, # Disable check here.
)
elif main_menu_location == 'Team Game Select':
# pylint: disable=cyclic-import
@@ -344,7 +347,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
bs.app.ui_v1.set_main_menu_window(
PlaylistBrowserWindow(
sessiontype=bs.DualTeamSession, transition=None
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=False, # Disable check here.
)
elif main_menu_location == 'Free-for-All Game Select':
# pylint: disable=cyclic-import
@@ -356,28 +360,34 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
PlaylistBrowserWindow(
sessiontype=bs.FreeForAllSession,
transition=None,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=False, # Disable check here.
)
elif main_menu_location == 'Coop Select':
# pylint: disable=cyclic-import
from bauiv1lib.coop.browser import CoopBrowserWindow
bs.app.ui_v1.set_main_menu_window(
- CoopBrowserWindow(transition=None).get_root_widget()
+ CoopBrowserWindow(
+ transition=None
+ ).get_root_widget(),
+ from_window=False, # Disable check here.
)
elif main_menu_location == 'Benchmarks & Stress Tests':
# pylint: disable=cyclic-import
from bauiv1lib.debug import DebugWindow
bs.app.ui_v1.set_main_menu_window(
- DebugWindow(transition=None).get_root_widget()
+ DebugWindow(transition=None).get_root_widget(),
+ from_window=False, # Disable check here.
)
else:
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
bs.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition=None).get_root_widget()
+ MainMenuWindow(transition=None).get_root_widget(),
+ from_window=None,
)
# attempt to show any pending offers immediately.
diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py
index 3d3a7b42..cd49fca8 100644
--- a/src/assets/ba_data/python/bauiv1/__init__.py
+++ b/src/assets/ba_data/python/bauiv1/__init__.py
@@ -31,7 +31,10 @@ from babase import (
apptimer,
AppTimer,
Call,
- can_toggle_fullscreen,
+ fullscreen_control_available,
+ fullscreen_control_get,
+ fullscreen_control_key_shortcut,
+ fullscreen_control_set,
charstr,
clipboard_is_supported,
clipboard_set_text,
@@ -57,13 +60,15 @@ from babase import (
in_logic_thread,
increment_analytics_count,
is_browser_likely_available,
- is_running_on_fire_tv,
is_xcode_build,
- Keyboard,
lock_all_input,
LoginAdapter,
+ LoginInfo,
Lstr,
+ native_review_request,
+ native_review_request_supported,
NotFoundError,
+ open_file_externally,
Permission,
Plugin,
PluginSpec,
@@ -88,7 +93,6 @@ from babase import (
from _bauiv1 import (
buttonwidget,
- can_show_ad,
checkboxwidget,
columnwidget,
containerwidget,
@@ -97,21 +101,15 @@ from _bauiv1 import (
getmesh,
getsound,
gettexture,
- has_video_ads,
- have_incentivized_ad,
hscrollwidget,
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,
@@ -119,6 +117,7 @@ from _bauiv1 import (
Widget,
widget,
)
+from bauiv1._keyboard import Keyboard
from bauiv1._uitypes import Window, uicleanupcheck
from bauiv1._subsystem import UIV1Subsystem
@@ -138,8 +137,10 @@ __all__ = [
'AppTimer',
'buttonwidget',
'Call',
- 'can_show_ad',
- 'can_toggle_fullscreen',
+ 'fullscreen_control_available',
+ 'fullscreen_control_get',
+ 'fullscreen_control_key_shortcut',
+ 'fullscreen_control_set',
'charstr',
'checkboxwidget',
'clipboard_is_supported',
@@ -169,8 +170,6 @@ __all__ = [
'getmesh',
'getsound',
'gettexture',
- 'has_video_ads',
- 'have_incentivized_ad',
'have_permission',
'hscrollwidget',
'imagewidget',
@@ -178,13 +177,15 @@ __all__ = [
'increment_analytics_count',
'is_browser_likely_available',
'is_party_icon_visible',
- 'is_running_on_fire_tv',
'is_xcode_build',
'Keyboard',
'lock_all_input',
'LoginAdapter',
+ 'LoginInfo',
'Lstr',
'Mesh',
+ 'native_review_request',
+ 'native_review_request_supported',
'NotFoundError',
'open_file_externally',
'open_url',
@@ -204,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/_hooks.py b/src/assets/ba_data/python/bauiv1/_hooks.py
index c7b6072a..b26b9fd3 100644
--- a/src/assets/ba_data/python/bauiv1/_hooks.py
+++ b/src/assets/ba_data/python/bauiv1/_hooks.py
@@ -6,6 +6,7 @@
from __future__ import annotations
import logging
+import inspect
from typing import TYPE_CHECKING
import _bauiv1
@@ -87,3 +88,19 @@ def show_url_window(address: str) -> None:
return
app.classic.show_url_window(address)
+
+
+def double_transition_out_warning() -> None:
+ """Called if a widget is set to transition out twice."""
+ caller_frame = inspect.stack()[1]
+ caller_filename = caller_frame.filename
+ caller_line_number = caller_frame.lineno
+ logging.warning(
+ 'ContainerWidget was set to transition out twice;'
+ ' this often implies buggy code (%s line %s).\n'
+ ' Generally you should check the value of'
+ ' _root_widget.transitioning_out and perform no actions if that'
+ ' is True.',
+ caller_filename,
+ caller_line_number,
+ )
diff --git a/src/assets/ba_data/python/babase/_keyboard.py b/src/assets/ba_data/python/bauiv1/_keyboard.py
similarity index 100%
rename from src/assets/ba_data/python/babase/_keyboard.py
rename to src/assets/ba_data/python/bauiv1/_keyboard.py
diff --git a/src/assets/ba_data/python/bauiv1/_subsystem.py b/src/assets/ba_data/python/bauiv1/_subsystem.py
index 311650cb..3c6b1e77 100644
--- a/src/assets/ba_data/python/bauiv1/_subsystem.py
+++ b/src/assets/ba_data/python/bauiv1/_subsystem.py
@@ -5,6 +5,7 @@
from __future__ import annotations
import logging
+import inspect
from typing import TYPE_CHECKING
import babase
@@ -66,6 +67,16 @@ class UIV1Subsystem(babase.AppSubsystem):
# a more elegant way once we revamp high level UI stuff a bit.
self.selecting_private_party_playlist: bool = False
+ @property
+ def available(self) -> bool:
+ """Can uiv1 currently be used?
+
+ Code that may run in headless mode, before the UI has been spun up,
+ while other ui systems are active, etc. can check this to avoid
+ likely erroring.
+ """
+ return _bauiv1.is_available()
+
@property
def uiscale(self) -> babase.UIScale:
"""Current ui scale for the app."""
@@ -106,21 +117,69 @@ class UIV1Subsystem(babase.AppSubsystem):
# FIXME: Can probably kill this if we do immediate UI death checks.
self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True)
- def set_main_menu_window(self, window: bauiv1.Widget) -> None:
- """Set the current 'main' window, replacing any existing."""
+ def set_main_menu_window(
+ self,
+ window: bauiv1.Widget,
+ from_window: bauiv1.Widget | None | bool = True,
+ ) -> None:
+ """Set the current 'main' window, replacing any existing.
+
+ If 'from_window' is passed as a bauiv1.Widget or None, a warning
+ will be issued if it that value does not match the current main
+ window. This can help clean up flawed code that can lead to bad
+ UI states. A value of False will disable the check.
+ """
+
existing = self._main_menu_window
- from inspect import currentframe, getframeinfo
+
+ try:
+ if isinstance(from_window, bool):
+ # For default val True we warn that the arg wasn't
+ # passed. False can be explicitly passed to disable this
+ # check.
+ if from_window is True:
+ caller_frame = inspect.stack()[1]
+ caller_filename = caller_frame.filename
+ caller_line_number = caller_frame.lineno
+ logging.warning(
+ 'set_main_menu_window() should be passed a'
+ " 'from_window' value to help ensure proper UI behavior"
+ ' (%s line %i).',
+ caller_filename,
+ caller_line_number,
+ )
+ else:
+ # For everything else, warn if what they passed wasn't
+ # the previous main menu widget.
+ if from_window is not existing:
+ caller_frame = inspect.stack()[1]
+ caller_filename = caller_frame.filename
+ caller_line_number = caller_frame.lineno
+ logging.warning(
+ "set_main_menu_window() was passed 'from_window' %s"
+ ' but existing main-menu-window is %s. (%s line %i).',
+ from_window,
+ existing,
+ caller_filename,
+ caller_line_number,
+ )
+ except Exception:
+ # Prevent any bugs in these checks from causing problems.
+ logging.exception('Error checking from_window')
+
+ # Once the above code leads to us fixing all leftover window bugs
+ # at the source, we can kill the code below.
# Let's grab the location where we were called from to report
# if we have to force-kill the existing window (which normally
# should not happen).
frameline = None
try:
- frame = currentframe()
+ frame = inspect.currentframe()
if frame is not None:
frame = frame.f_back
if frame is not None:
- frameinfo = getframeinfo(frame)
+ frameinfo = inspect.getframeinfo(frame)
frameline = f'{frameinfo.filename} {frameinfo.lineno}'
except Exception:
logging.exception('Error calcing line for set_main_menu_window')
@@ -150,13 +209,18 @@ class UIV1Subsystem(babase.AppSubsystem):
def clear_main_menu_window(self, transition: str | None = None) -> None:
"""Clear any existing 'main' window with the provided transition."""
+ assert transition is None or not transition.endswith('_in')
if self._main_menu_window:
- if transition is not None:
+ if (
+ transition is not None
+ and not self._main_menu_window.transitioning_out
+ ):
_bauiv1.containerwidget(
edit=self._main_menu_window, transition=transition
)
else:
self._main_menu_window.delete()
+ self._main_menu_window = None
def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None:
"""(internal)"""
diff --git a/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py
index 30a168c3..e91972e2 100644
--- a/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py
+++ b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
import babase
import _bauiv1
+from bauiv1._keyboard import Keyboard
from bauiv1._uitypes import Window
if TYPE_CHECKING:
@@ -51,6 +52,19 @@ class OnScreenKeyboardWindow(Window):
else (0, 0),
)
)
+ self._cancel_button = _bauiv1.buttonwidget(
+ parent=self._root_widget,
+ scale=0.5,
+ 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),
+ icon=_bauiv1.gettexture('crossOut'),
+ iconscale=1.2,
+ )
self._done_button = _bauiv1.buttonwidget(
parent=self._root_widget,
position=(self._width - 200, 44),
@@ -240,9 +254,7 @@ class OnScreenKeyboardWindow(Window):
# Show change instructions only if we have more than one
# keyboard option.
keyboards = (
- babase.app.meta.scanresults.exports_of_class(
- babase.Keyboard
- )
+ babase.app.meta.scanresults.exports_of_class(Keyboard)
if babase.app.meta.scanresults is not None
else []
)
@@ -274,10 +286,10 @@ class OnScreenKeyboardWindow(Window):
def _get_keyboard(self) -> bui.Keyboard:
assert babase.app.meta.scanresults is not None
- classname = babase.app.meta.scanresults.exports_of_class(
- babase.Keyboard
- )[self._keyboard_index]
- kbclass = babase.getclass(classname, babase.Keyboard)
+ classname = babase.app.meta.scanresults.exports_of_class(Keyboard)[
+ self._keyboard_index
+ ]
+ kbclass = babase.getclass(classname, Keyboard)
return kbclass()
def _refresh(self) -> None:
@@ -372,9 +384,7 @@ class OnScreenKeyboardWindow(Window):
def _next_keyboard(self) -> None:
assert babase.app.meta.scanresults is not None
- kbexports = babase.app.meta.scanresults.exports_of_class(
- babase.Keyboard
- )
+ kbexports = babase.app.meta.scanresults.exports_of_class(Keyboard)
self._keyboard_index = (self._keyboard_index + 1) % len(kbexports)
self._load_keyboard()
diff --git a/src/assets/ba_data/python/bauiv1lib/account/settings.py b/src/assets/ba_data/python/bauiv1lib/account/settings.py
index b0e56c1e..f9eab7c7 100644
--- a/src/assets/ba_data/python/bauiv1lib/account/settings.py
+++ b/src/assets/ba_data/python/bauiv1lib/account/settings.py
@@ -63,20 +63,14 @@ class AccountSettingsWindow(bui.Window):
1.0, bui.WeakCall(self._update), repeat=True
)
- # Currently we can only reset achievements on game-center.
- v1_account_type: str | None
- if self._v1_signed_in:
- v1_account_type = plus.get_v1_account_type()
- else:
- v1_account_type = None
- self._can_reset_achievements = v1_account_type == 'Game Center'
+ self._can_reset_achievements = False
app = bui.app
assert app.classic is not None
uiscale = app.ui_v1.uiscale
- self._width = 760 if uiscale is bui.UIScale.SMALL else 660
- x_offs = 50 if uiscale is bui.UIScale.SMALL else 0
+ self._width = 860 if uiscale is bui.UIScale.SMALL else 660
+ x_offs = 100 if uiscale is bui.UIScale.SMALL else 0
self._height = (
390
if uiscale is bui.UIScale.SMALL
@@ -98,6 +92,9 @@ class AccountSettingsWindow(bui.Window):
if LoginType.GPGS in plus.accounts.login_adapters:
self._show_sign_in_buttons.append('Google Play')
+ if LoginType.GAME_CENTER in plus.accounts.login_adapters:
+ self._show_sign_in_buttons.append('Game Center')
+
# Always want to show our web-based v2 login option.
self._show_sign_in_buttons.append('V2Proxy')
@@ -227,6 +224,8 @@ class AccountSettingsWindow(bui.Window):
plus = bui.app.plus
assert plus is not None
+ via_lines: list[str] = []
+
primary_v2_account = plus.accounts.primary
v1_state = plus.get_v1_account_state()
@@ -237,14 +236,55 @@ class AccountSettingsWindow(bui.Window):
# We expose GPGS-specific functionality only if it is 'active'
# (meaning the current GPGS player matches one of our account's
# logins).
- gpgs_adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
- is_gpgs = (
- False if gpgs_adapter is None else gpgs_adapter.is_back_end_active()
+ adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
+ gpgs_active = adapter is not None and adapter.is_back_end_active()
+
+ # Ditto for Game Center.
+ adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
+ game_center_active = (
+ adapter is not None and adapter.is_back_end_active()
)
show_signed_in_as = self._v1_signed_in
signed_in_as_space = 95.0
+ # To reduce confusion about the whole V2 account situation for
+ # people used to seeing their Google Play Games or Game Center
+ # account name and icon and whatnot, let's show those underneath
+ # the V2 tag to help communicate that they are in fact logged in
+ # through that account.
+ via_space = 25.0
+ if show_signed_in_as and bui.app.plus is not None:
+ accounts = bui.app.plus.accounts
+ if accounts.primary is not None:
+ # For these login types, we show 'via' IF there is a
+ # login of that type attached to our account AND it is
+ # currently active (We don't want to show 'via Game
+ # Center' if we're signed out of Game Center or
+ # currently running on Steam, even if there is a Game
+ # Center login attached to our account).
+ for ltype, lchar in [
+ (LoginType.GPGS, bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
+ (LoginType.GAME_CENTER, bui.SpecialChar.GAME_CENTER_LOGO),
+ ]:
+ linfo = accounts.primary.logins.get(ltype)
+ ladapter = accounts.login_adapters.get(ltype)
+ if (
+ linfo is not None
+ and ladapter is not None
+ and ladapter.is_back_end_active()
+ ):
+ via_lines.append(f'{bui.charstr(lchar)}{linfo.name}')
+
+ # TEMP TESTING
+ if bool(False):
+ icontxt = bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
+ via_lines.append(f'{icontxt}FloofDibble')
+ icontxt = bui.charstr(
+ bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO
+ )
+ via_lines.append(f'{icontxt}StinkBobble')
+
show_sign_in_benefits = not self._v1_signed_in
sign_in_benefits_space = 80.0
@@ -258,6 +298,11 @@ class AccountSettingsWindow(bui.Window):
and self._signing_in_adapter is None
and 'Google Play' in self._show_sign_in_buttons
)
+ show_game_center_sign_in_button = (
+ v1_state == 'signed_out'
+ and self._signing_in_adapter is None
+ and 'Game Center' in self._show_sign_in_buttons
+ )
show_v2_proxy_sign_in_button = (
v1_state == 'signed_out'
and self._signing_in_adapter is None
@@ -271,9 +316,8 @@ class AccountSettingsWindow(bui.Window):
sign_in_button_space = 70.0
deprecated_space = 60
- show_game_service_button = self._v1_signed_in and v1_account_type in [
- 'Game Center'
- ]
+ # Game Center currently has a single UI for everything.
+ show_game_service_button = game_center_active
game_service_button_space = 60.0
show_what_is_v2 = self._v1_signed_in and v1_account_type == 'V2'
@@ -281,11 +325,9 @@ class AccountSettingsWindow(bui.Window):
show_linked_accounts_text = self._v1_signed_in
linked_accounts_text_space = 60.0
- show_achievements_button = self._v1_signed_in and v1_account_type in (
- 'Google Play',
- 'Local',
- 'V2',
- )
+ # Always show achievements except in the game-center case where
+ # its unified UI covers them.
+ show_achievements_button = self._v1_signed_in and not game_center_active
achievements_button_space = 60.0
show_achievements_text = (
@@ -293,7 +335,7 @@ class AccountSettingsWindow(bui.Window):
)
achievements_text_space = 27.0
- show_leaderboards_button = self._v1_signed_in and is_gpgs
+ show_leaderboards_button = self._v1_signed_in and gpgs_active
leaderboards_button_space = 60.0
show_campaign_progress = self._v1_signed_in
@@ -330,7 +372,6 @@ class AccountSettingsWindow(bui.Window):
show_sign_out_button = self._v1_signed_in and v1_account_type in [
'Local',
- 'Google Play',
'V2',
]
sign_out_button_space = 70.0
@@ -349,10 +390,13 @@ class AccountSettingsWindow(bui.Window):
self._sub_height = 60.0
if show_signed_in_as:
self._sub_height += signed_in_as_space
+ self._sub_height += via_space * len(via_lines)
if show_signing_in_text:
self._sub_height += signing_in_text_space
if show_google_play_sign_in_button:
self._sub_height += sign_in_button_space
+ if show_game_center_sign_in_button:
+ self._sub_height += sign_in_button_space
if show_v2_proxy_sign_in_button:
self._sub_height += sign_in_button_space
if show_device_sign_in_button:
@@ -442,20 +486,21 @@ class AccountSettingsWindow(bui.Window):
self._account_name_what_is_text = bui.textwidget(
parent=self._subcontainer,
position=(0.0, self._account_name_what_is_y),
- size=(200.0, 60),
+ size=(220.0, 60),
text=bui.Lstr(
value='${WHAT} -->',
subs=[('${WHAT}', bui.Lstr(resource='whatIsThisText'))],
),
scale=0.6,
color=(0.3, 0.7, 0.05),
- maxwidth=200.0,
+ maxwidth=130.0,
h_align='right',
v_align='center',
autoselect=True,
selectable=True,
on_activate_call=show_what_is_v2_page,
click_activate=True,
+ glow_type='uniform',
)
if first_selectable is None:
first_selectable = self._account_name_what_is_text
@@ -466,6 +511,54 @@ class AccountSettingsWindow(bui.Window):
v -= signed_in_as_space * 0.4
+ for via in via_lines:
+ v -= via_space * 0.1
+ sscale = 0.7
+ swidth = (
+ bui.get_string_width(via, suppress_warning=True) * sscale
+ )
+ bui.textwidget(
+ parent=self._subcontainer,
+ position=(self._sub_width * 0.5, v),
+ size=(0, 0),
+ text=via,
+ scale=sscale,
+ color=(0.6, 0.6, 0.6),
+ flatness=1.0,
+ shadow=0.0,
+ h_align='center',
+ v_align='center',
+ )
+ bui.textwidget(
+ parent=self._subcontainer,
+ position=(self._sub_width * 0.5 - swidth * 0.5 - 5, v),
+ size=(0, 0),
+ text=bui.Lstr(
+ value='(${VIA}',
+ subs=[('${VIA}', bui.Lstr(resource='viaText'))],
+ ),
+ scale=0.5,
+ color=(0.4, 0.6, 0.4, 0.5),
+ flatness=1.0,
+ shadow=0.0,
+ h_align='right',
+ v_align='center',
+ )
+ bui.textwidget(
+ parent=self._subcontainer,
+ position=(self._sub_width * 0.5 + swidth * 0.5 + 10, v),
+ size=(0, 0),
+ text=')',
+ scale=0.5,
+ color=(0.4, 0.6, 0.4, 0.5),
+ flatness=1.0,
+ shadow=0.0,
+ h_align='right',
+ v_align='center',
+ )
+
+ v -= via_space * 0.9
+
else:
self._account_name_text = None
self._account_name_what_is_text = None
@@ -477,22 +570,6 @@ class AccountSettingsWindow(bui.Window):
if show_sign_in_benefits:
v -= sign_in_benefits_space
- app = bui.app
- assert app.classic is not None
- extra: str | bui.Lstr | None
- if (
- app.classic.platform in ['mac', 'ios']
- and app.classic.subplatform == 'appstore'
- ):
- extra = bui.Lstr(
- value='\n${S}',
- subs=[
- ('${S}', bui.Lstr(resource='signInWithGameCenterText'))
- ],
- )
- else:
- extra = ''
-
bui.textwidget(
parent=self._subcontainer,
position=(
@@ -500,16 +577,7 @@ class AccountSettingsWindow(bui.Window):
v + sign_in_benefits_space * 0.4,
),
size=(0, 0),
- text=bui.Lstr(
- value='${A}${B}',
- subs=[
- (
- '${A}',
- bui.Lstr(resource=self._r + '.signInInfoText'),
- ),
- ('${B}', extra),
- ],
- ),
+ text=bui.Lstr(resource=self._r + '.signInInfoText'),
max_height=sign_in_benefits_space * 0.9,
scale=0.9,
color=(0.75, 0.7, 0.8),
@@ -554,7 +622,13 @@ class AccountSettingsWindow(bui.Window):
(
'${B}',
bui.Lstr(
- resource=self._r + '.signInWithGooglePlayText'
+ resource=self._r + '.signInWithText',
+ subs=[
+ (
+ '${SERVICE}',
+ bui.Lstr(resource='googlePlayText'),
+ )
+ ],
),
),
],
@@ -572,6 +646,48 @@ class AccountSettingsWindow(bui.Window):
bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None
+ if show_game_center_sign_in_button:
+ button_width = 350
+ v -= sign_in_button_space
+ self._sign_in_google_play_button = btn = bui.buttonwidget(
+ parent=self._subcontainer,
+ position=((self._sub_width - button_width) * 0.5, v - 20),
+ autoselect=True,
+ size=(button_width, 60),
+ # Note: Apparently Game Center is just called 'Game Center'
+ # in all languages. Can revisit if not true.
+ # https://developer.apple.com/forums/thread/725779
+ label=bui.Lstr(
+ value='${A}${B}',
+ subs=[
+ (
+ '${A}',
+ bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO),
+ ),
+ (
+ '${B}',
+ bui.Lstr(
+ resource=self._r + '.signInWithText',
+ subs=[('${SERVICE}', 'Game Center')],
+ ),
+ ),
+ ],
+ ),
+ on_activate_call=lambda: self._sign_in_press(
+ LoginType.GAME_CENTER
+ ),
+ )
+ if first_selectable is None:
+ first_selectable = btn
+ if bui.app.ui_v1.use_toolbars:
+ bui.widget(
+ edit=btn,
+ right_widget=bui.get_special_widget('party_button'),
+ )
+ bui.widget(edit=btn, left_widget=bbtn)
+ bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
+ self._sign_in_text = None
+
if show_v2_proxy_sign_in_button:
button_width = 350
v -= sign_in_button_space
@@ -704,7 +820,7 @@ class AccountSettingsWindow(bui.Window):
position=((self._sub_width - button_width) * 0.5, v + 30),
autoselect=True,
size=(button_width, 60),
- label=bui.Lstr(resource=self._r + '.manageAccountText'),
+ label=bui.Lstr(resource=f'{self._r}.manageAccountText'),
color=(0.55, 0.5, 0.6),
icon=bui.gettexture('settingsIcon'),
textcolor=(0.75, 0.7, 0.8),
@@ -745,10 +861,15 @@ class AccountSettingsWindow(bui.Window):
# the button to go to OS-Specific leaderboards/high-score-lists/etc.
if show_game_service_button:
button_width = 300
- v -= game_service_button_space * 0.85
- v1_account_type = plus.get_v1_account_type()
- if v1_account_type == 'Game Center':
- v1_account_type_name = bui.Lstr(resource='gameCenterText')
+ v -= game_service_button_space * 0.6
+ if game_center_active:
+ # Note: Apparently Game Center is just called 'Game Center'
+ # in all languages. Can revisit if not true.
+ # https://developer.apple.com/forums/thread/725779
+ game_service_button_label = bui.Lstr(
+ value=bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
+ + 'Game Center'
+ )
else:
raise ValueError(
"unknown account type: '" + str(v1_account_type) + "'"
@@ -761,7 +882,7 @@ class AccountSettingsWindow(bui.Window):
autoselect=True,
on_activate_call=self._on_game_service_button_press,
size=(button_width, 50),
- label=v1_account_type_name,
+ label=game_service_button_label,
)
if first_selectable is None:
first_selectable = btn
@@ -771,7 +892,7 @@ class AccountSettingsWindow(bui.Window):
right_widget=bui.get_special_widget('party_button'),
)
bui.widget(edit=btn, left_widget=bbtn)
- v -= game_service_button_space * 0.15
+ v -= game_service_button_space * 0.4
else:
self.game_service_button = None
@@ -804,13 +925,15 @@ class AccountSettingsWindow(bui.Window):
autoselect=True,
icon=bui.gettexture(
'googlePlayAchievementsIcon'
- if is_gpgs
+ if gpgs_active
else 'achievementsIcon'
),
- icon_color=(0.8, 0.95, 0.7) if is_gpgs else (0.85, 0.8, 0.9),
+ icon_color=(0.8, 0.95, 0.7)
+ if gpgs_active
+ else (0.85, 0.8, 0.9),
on_activate_call=(
self._on_custom_achievements_press
- if is_gpgs
+ if gpgs_active
else self._on_achievements_press
),
size=(button_width, 50),
@@ -1135,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
@@ -1162,11 +1287,21 @@ class AccountSettingsWindow(bui.Window):
show_what_is_v2_page()
def _on_manage_account_press(self) -> None:
- bui.screenmessage(bui.Lstr(resource='oneMomentText'))
-
plus = bui.app.plus
assert plus is not None
+ # Preemptively fail if it looks like we won't be able to talk to
+ # the server anyway.
+ if not plus.cloud.connected:
+ bui.screenmessage(
+ bui.Lstr(resource='internal.unavailableNoConnectionText'),
+ color=(1, 0, 0),
+ )
+ bui.getsound('error').play()
+ return
+
+ bui.screenmessage(bui.Lstr(resource='oneMomentText'))
+
# We expect to have a v2 account signed in if we get here.
if plus.accounts.primary is None:
logging.exception(
@@ -1184,6 +1319,9 @@ class AccountSettingsWindow(bui.Window):
self, response: bacommon.cloud.ManageAccountResponse | Exception
) -> None:
if isinstance(response, Exception) or response.url is None:
+ logging.warning(
+ 'Got error in manage-account-response: %s.', response
+ )
bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
bui.getsound('error').play()
return
@@ -1191,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
@@ -1323,7 +1461,7 @@ class AccountSettingsWindow(bui.Window):
swidth = bui.get_string_width(name_str, suppress_warning=True)
# Eww; number-fudging. Need to recalibrate this if
# account name scaling changes.
- x = self._sub_width * 0.5 - swidth * 0.75 - 170
+ x = self._sub_width * 0.5 - swidth * 0.75 - 190
bui.textwidget(
edit=self._account_name_what_is_text,
@@ -1371,9 +1509,18 @@ class AccountSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.profile.browser import ProfileBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
- ProfileBrowserWindow(origin_widget=self._player_profiles_button)
+ bui.app.ui_v1.set_main_menu_window(
+ ProfileBrowserWindow(
+ origin_widget=self._player_profiles_button
+ ).get_root_widget(),
+ from_window=self._root_widget,
+ )
def _cancel_sign_in_press(self) -> None:
# If we're waiting on an adapter to give us credentials, abort.
@@ -1466,7 +1613,11 @@ class AccountSettingsWindow(bui.Window):
if isinstance(result, Exception):
# For now just make a bit of noise if anything went wrong;
# can get more specific as needed later.
- bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
+ logging.warning('Got error in v2 sign-in result: %s', result)
+ bui.screenmessage(
+ bui.Lstr(resource='internal.signInNoConnectionText'),
+ color=(1, 0, 0),
+ )
bui.getsound('error').play()
else:
# Success! Plug in these credentials which will begin
@@ -1530,6 +1681,10 @@ class AccountSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -1538,7 +1693,8 @@ class AccountSettingsWindow(bui.Window):
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/account/v2proxy.py b/src/assets/ba_data/python/bauiv1lib/account/v2proxy.py
index 51d48061..756ea612 100644
--- a/src/assets/ba_data/python/bauiv1lib/account/v2proxy.py
+++ b/src/assets/ba_data/python/bauiv1lib/account/v2proxy.py
@@ -62,14 +62,11 @@ class V2ProxySignInWindow(bui.Window):
label=bui.Lstr(resource='cancelText'),
on_activate_call=self._done,
autoselect=True,
- color=(0.55, 0.5, 0.6),
- textcolor=(0.75, 0.7, 0.8),
)
- if bool(False):
- bui.containerwidget(
- edit=self._root_widget, cancel_button=self._cancel_button
- )
+ bui.containerwidget(
+ edit=self._root_widget, cancel_button=self._cancel_button
+ )
self._update_timer: bui.AppTimer | None = None
@@ -242,4 +239,7 @@ class V2ProxySignInWindow(bui.Window):
)
def _done(self) -> None:
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
bui.containerwidget(edit=self._root_widget, transition='out_scale')
diff --git a/src/assets/ba_data/python/bauiv1lib/config.py b/src/assets/ba_data/python/bauiv1lib/config.py
index 15032cfa..763a3704 100644
--- a/src/assets/ba_data/python/bauiv1lib/config.py
+++ b/src/assets/ba_data/python/bauiv1lib/config.py
@@ -93,6 +93,7 @@ class ConfigNumberEdit:
displayname: str | bui.Lstr | None = None,
changesound: bool = True,
textscale: float = 1.0,
+ as_percent: bool = False,
):
if displayname is None:
displayname = configkey
@@ -103,6 +104,7 @@ class ConfigNumberEdit:
self._increment = increment
self._callback = callback
self._value = bui.app.config.resolve(configkey)
+ self._as_percent = as_percent
self.nametext = bui.textwidget(
parent=parent,
@@ -166,4 +168,8 @@ class ConfigNumberEdit:
bui.app.config.apply_and_commit()
def _update_display(self) -> None:
- bui.textwidget(edit=self.valuetext, text=f'{self._value:.1f}')
+ if self._as_percent:
+ val = f'{round(self._value*100.0)}%'
+ else:
+ val = f'{self._value:.1f}'
+ bui.textwidget(edit=self.valuetext, text=val)
diff --git a/src/assets/ba_data/python/bauiv1lib/configerror.py b/src/assets/ba_data/python/bauiv1lib/configerror.py
deleted file mode 100644
index c64a31d6..00000000
--- a/src/assets/ba_data/python/bauiv1lib/configerror.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# Released under the MIT License. See LICENSE for details.
-#
-"""UI for dealing with broken config files."""
-
-from __future__ import annotations
-
-import bauiv1 as bui
-
-
-class ConfigErrorWindow(bui.Window):
- """Window for dealing with a broken config."""
-
- def __init__(self) -> None:
- self._config_file_path = bui.app.env.config_file_path
- width = 800
- super().__init__(
- bui.containerwidget(size=(width, 400), transition='in_right')
- )
- padding = 20
- bui.textwidget(
- parent=self._root_widget,
- position=(padding, 220 + 60),
- size=(width - 2 * padding, 100 - 2 * padding),
- h_align='center',
- v_align='top',
- scale=0.73,
- text=(
- f'Error reading {bui.appnameupper()} config file'
- ':\n\n\nCheck the console'
- ' (press ~ twice) for details.\n\nWould you like to quit and'
- ' try to fix it by hand\nor overwrite it with defaults?\n\n'
- '(high scores, player profiles, etc will be lost if you'
- ' overwrite)'
- ),
- )
- bui.textwidget(
- parent=self._root_widget,
- position=(padding, 198 + 60),
- size=(width - 2 * padding, 100 - 2 * padding),
- h_align='center',
- v_align='top',
- scale=0.5,
- text=self._config_file_path,
- )
- quit_button = bui.buttonwidget(
- parent=self._root_widget,
- position=(35, 30),
- size=(240, 54),
- label='Quit and Edit',
- on_activate_call=self._quit,
- )
- bui.buttonwidget(
- parent=self._root_widget,
- position=(width - 370, 30),
- size=(330, 54),
- label='Overwrite with Defaults',
- on_activate_call=self._defaults,
- )
- bui.containerwidget(
- edit=self._root_widget,
- cancel_button=quit_button,
- selected_child=quit_button,
- )
-
- def _quit(self) -> None:
- bui.apptimer(0.001, self._edit_and_quit)
- bui.lock_all_input()
-
- def _edit_and_quit(self) -> None:
- bui.open_file_externally(self._config_file_path)
- bui.apptimer(0.1, bui.quit)
-
- def _defaults(self) -> None:
- bui.containerwidget(edit=self._root_widget, transition='out_left')
- bui.getsound('gunCocking').play()
- bui.screenmessage('settings reset.', color=(1, 1, 0))
-
- # At this point settings are already set; lets just commit them
- # to disk.
- bui.commit_app_config(force=True)
diff --git a/src/assets/ba_data/python/bauiv1lib/coop/browser.py b/src/assets/ba_data/python/bauiv1lib/coop/browser.py
index 20760381..e9f9b4af 100644
--- a/src/assets/ba_data/python/bauiv1lib/coop/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/coop/browser.py
@@ -85,8 +85,8 @@ class CoopBrowserWindow(bui.Window):
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
- self._width = 1320 if uiscale is bui.UIScale.SMALL else 1120
- self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
+ self._width = 1520 if uiscale is bui.UIScale.SMALL else 1120
+ self._x_inset = x_inset = 200 if uiscale is bui.UIScale.SMALL else 0
self._height = (
657
if uiscale is bui.UIScale.SMALL
@@ -415,7 +415,7 @@ class CoopBrowserWindow(bui.Window):
)
# Decrement time on our tournament buttons.
- ads_enabled = bui.have_incentivized_ad()
+ ads_enabled = plus.have_incentivized_ad()
for tbtn in self._tournament_buttons:
tbtn.time_remaining = max(0, tbtn.time_remaining - 1)
if tbtn.time_remaining_value_text is not None:
@@ -430,7 +430,7 @@ class CoopBrowserWindow(bui.Window):
)
# Also adjust the ad icon visibility.
- if tbtn.allow_ads and bui.has_video_ads():
+ if tbtn.allow_ads and plus.has_video_ads():
bui.imagewidget(
edit=tbtn.entry_fee_ad_image,
opacity=1.0 if ads_enabled else 0.25,
@@ -1019,6 +1019,10 @@ class CoopBrowserWindow(bui.Window):
from bauiv1lib.account import show_sign_in_prompt
from bauiv1lib.league.rankwindow import LeagueRankWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -1032,7 +1036,8 @@ class CoopBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
LeagueRankWindow(
origin_widget=self._league_rank_button.get_button()
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _switch_to_score(
@@ -1043,6 +1048,10 @@ class CoopBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.account import show_sign_in_prompt
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -1058,7 +1067,8 @@ class CoopBrowserWindow(bui.Window):
origin_widget=self._store_button.get_button(),
show_tab=show_tab,
back_location='CoopBrowserWindow',
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def is_tourney_data_up_to_date(self) -> bool:
@@ -1218,6 +1228,10 @@ class CoopBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# If something is selected, store it.
self._save_state()
bui.containerwidget(
@@ -1225,7 +1239,8 @@ class CoopBrowserWindow(bui.Window):
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- PlayWindow(transition='in_left').get_root_widget()
+ PlayWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py b/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py
index 72d604e6..4b766379 100644
--- a/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py
+++ b/src/assets/ba_data/python/bauiv1lib/coop/tournamentbutton.py
@@ -638,8 +638,8 @@ class TournamentButton:
# Now, if this fee allows ads and we support video ads, show
# the 'or ad' version.
- if allow_ads and bui.has_video_ads():
- ads_enabled = bui.have_incentivized_ad()
+ if allow_ads and plus.has_video_ads():
+ ads_enabled = plus.have_incentivized_ad()
bui.imagewidget(
edit=self.entry_fee_ad_image,
opacity=1.0 if ads_enabled else 0.25,
diff --git a/src/assets/ba_data/python/bauiv1lib/creditslist.py b/src/assets/ba_data/python/bauiv1lib/creditslist.py
index e6a0769f..c087dfbf 100644
--- a/src/assets/ba_data/python/bauiv1lib/creditslist.py
+++ b/src/assets/ba_data/python/bauiv1lib/creditslist.py
@@ -359,10 +359,15 @@ class CreditsListWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/debug.py b/src/assets/ba_data/python/bauiv1lib/debug.py
index 7cd5e5d2..b397610f 100644
--- a/src/assets/ba_data/python/bauiv1lib/debug.py
+++ b/src/assets/ba_data/python/bauiv1lib/debug.py
@@ -379,8 +379,13 @@ class DebugWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AdvancedSettingsWindow(transition='in_left').get_root_widget()
+ AdvancedSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/discord.py b/src/assets/ba_data/python/bauiv1lib/discord.py
new file mode 100644
index 00000000..2e2eb520
--- /dev/null
+++ b/src/assets/ba_data/python/bauiv1lib/discord.py
@@ -0,0 +1,133 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""UI functionality for the Discord window."""
+
+from __future__ import annotations
+
+import bauiv1 as bui
+
+
+class DiscordWindow(bui.Window):
+ """Window for joining the Discord."""
+
+ def __init__(
+ self,
+ transition: str = 'in_right',
+ origin_widget: bui.Widget | None = None,
+ ):
+ if bui.app.classic is None:
+ raise RuntimeError('This requires classic support.')
+
+ app = bui.app
+ assert app.classic is not None
+
+ # If they provided an origin-widget, scale up from that.
+ scale_origin: tuple[float, float] | None
+ if origin_widget is not None:
+ self._transition_out = 'out_scale'
+ scale_origin = origin_widget.get_screen_space_center()
+ transition = 'in_scale'
+ else:
+ self._transition_out = 'out_right'
+ scale_origin = None
+
+ uiscale = bui.app.ui_v1.uiscale
+ self._width = 800
+ x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
+ self._height = 320
+ top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
+ super().__init__(
+ root_widget=bui.containerwidget(
+ size=(self._width, self._height + top_extra),
+ transition=transition,
+ toolbar_visibility='menu_minimal',
+ scale_origin_stack_offset=scale_origin,
+ scale=(
+ 1.6
+ if uiscale is bui.UIScale.SMALL
+ else 1.3
+ if uiscale is bui.UIScale.MEDIUM
+ else 1.0
+ ),
+ stack_offset=(0, 5) if uiscale is bui.UIScale.SMALL else (0, 0),
+ )
+ )
+
+ if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
+ bui.containerwidget(
+ edit=self._root_widget, on_cancel_call=self._do_back
+ )
+ self._back_button = None
+ else:
+ self._back_button = bui.buttonwidget(
+ parent=self._root_widget,
+ position=(53 + x_inset, self._height - 60),
+ size=(140, 60),
+ scale=0.8,
+ autoselect=True,
+ label=bui.Lstr(resource='backText'),
+ button_type='back',
+ on_activate_call=self._do_back,
+ )
+ bui.containerwidget(
+ edit=self._root_widget, cancel_button=self._back_button
+ )
+
+ # Do we need to translate 'Discord'? Or is that always the name?
+ self._title_text = bui.textwidget(
+ parent=self._root_widget,
+ position=(0, self._height - 52),
+ size=(self._width, 25),
+ text='Discord',
+ color=app.ui_v1.title_color,
+ h_align='center',
+ v_align='top',
+ )
+
+ min_size = min(self._width - 25, self._height - 25)
+ bui.imagewidget(
+ parent=self._root_widget,
+ position=(40, -15),
+ size=(min_size, min_size),
+ texture=bui.gettexture('discordServer'),
+ )
+
+ # Hmm should we translate this? The discord server is mostly
+ # English so being able to read this might be a good screening
+ # process?..
+ bui.textwidget(
+ parent=self._root_widget,
+ position=(self._width / 2 - 60, self._height - 100),
+ text='We have our own Discord server where you can:\n- Find new'
+ ' friends and people to play with\n- Participate in Office'
+ ' Hours/Coffee with Eric\n- Share mods, plugins, art, and'
+ ' memes\n- Report bugs and make feature suggestions\n'
+ '- Troubleshoot issues',
+ maxwidth=(self._width - 10) / 2,
+ color=(1, 1, 1, 1),
+ h_align='left',
+ v_align='top',
+ )
+
+ bui.buttonwidget(
+ parent=self._root_widget,
+ position=(self._width / 2 - 30, 20),
+ size=(self._width / 2 - 60, 60),
+ autoselect=True,
+ label=bui.Lstr(resource='discordJoinText'),
+ text_scale=1.0,
+ on_activate_call=bui.Call(
+ bui.open_url, 'https://ballistica.net/discord'
+ ),
+ )
+
+ if self._back_button is not None:
+ bui.buttonwidget(
+ edit=self._back_button,
+ button_type='backSmall',
+ size=(60, 60),
+ label=bui.charstr(bui.SpecialChar.BACK),
+ )
+
+ def _do_back(self) -> None:
+ bui.containerwidget(edit=self._root_widget, transition='out_scale')
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/__init__.py b/src/assets/ba_data/python/bauiv1lib/gather/__init__.py
index 3b5f02d6..61efe323 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/__init__.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/__init__.py
@@ -94,8 +94,8 @@ class GatherWindow(bui.Window):
bui.app.ui_v1.set_main_menu_location('Gather')
bui.set_party_icon_always_visible(True)
uiscale = bui.app.ui_v1.uiscale
- self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040
- x_offs = 100 if uiscale is bui.UIScale.SMALL else 0
+ self._width = 1440 if uiscale is bui.UIScale.SMALL else 1040
+ x_offs = 200 if uiscale is bui.UIScale.SMALL else 0
self._height = (
582
if uiscale is bui.UIScale.SMALL
@@ -270,12 +270,17 @@ class GatherWindow(bui.Window):
"""Called by the private-hosting tab to select a playlist."""
from bauiv1lib.play import PlayWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.selecting_private_party_playlist = True
bui.app.ui_v1.set_main_menu_window(
- PlayWindow(origin_widget=origin_widget).get_root_widget()
+ PlayWindow(origin_widget=origin_widget).get_root_widget(),
+ from_window=self._root_widget,
)
def _set_tab(self, tab_id: TabID) -> None:
@@ -383,11 +388,16 @@ class GatherWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py b/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py
index 66c3f480..b61ed339 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py
@@ -16,10 +16,6 @@ if TYPE_CHECKING:
class AboutGatherTab(GatherTab):
"""The about tab in the gather UI"""
- def __init__(self, window: GatherWindow) -> None:
- super().__init__(window)
- self._container: bui.Widget | None = None
-
def on_activate(
self,
parent_widget: bui.Widget,
@@ -29,9 +25,45 @@ class AboutGatherTab(GatherTab):
region_left: float,
region_bottom: float,
) -> bui.Widget:
+ # pylint: disable=too-many-locals
+
plus = bui.app.plus
assert plus is not None
+ try_tickets = plus.get_v1_account_misc_read_val(
+ 'friendTryTickets', None
+ )
+
+ show_message = True
+ # Squish message as needed to get things to fit nicely at
+ # various scales.
+ uiscale = bui.app.ui_v1.uiscale
+ message_height = (
+ 210
+ if uiscale is bui.UIScale.SMALL
+ else 305
+ if uiscale is bui.UIScale.MEDIUM
+ else 370
+ )
+ # Let's not talk about sharing in vr-mode; its tricky to fit more
+ # than one head in a VR-headset.
+ show_message_extra = not bui.app.env.vr
+ message_extra_height = 60
+ show_invite = try_tickets is not None
+ invite_height = 80
+ show_discord = True
+ discord_height = 80
+
+ c_height = 0
+ if show_message:
+ c_height += message_height
+ if show_message_extra:
+ c_height += message_extra_height
+ if show_invite:
+ c_height += invite_height
+ if show_discord:
+ c_height += discord_height
+
party_button_label = bui.charstr(bui.SpecialChar.TOP_BUTTON)
message = bui.Lstr(
resource='gatherWindow.aboutDescriptionText',
@@ -41,9 +73,7 @@ class AboutGatherTab(GatherTab):
],
)
- # Let's not talk about sharing in vr-mode; its tricky to fit more
- # than one head in a VR-headset ;-)
- if not bui.app.env.vr:
+ if show_message_extra:
message = bui.Lstr(
value='${A}\n\n${B}',
subs=[
@@ -57,47 +87,52 @@ class AboutGatherTab(GatherTab):
),
],
)
- string_height = 400
- include_invite = True
- msc_scale = 1.1
- c_height_2 = min(region_height, string_height * msc_scale + 100)
- try_tickets = plus.get_v1_account_misc_read_val(
- 'friendTryTickets', None
- )
- if try_tickets is None:
- include_invite = False
- self._container = bui.containerwidget(
+
+ scroll_widget = bui.scrollwidget(
parent=parent_widget,
+ position=(region_left, region_bottom),
+ size=(region_width, region_height),
+ highlight=False,
+ border_opacity=0,
+ )
+ msc_scale = 1.1
+
+ container = bui.containerwidget(
+ parent=scroll_widget,
position=(
region_left,
- region_bottom + (region_height - c_height_2) * 0.5,
+ region_bottom + (region_height - c_height) * 0.5,
),
- size=(region_width, c_height_2),
+ size=(region_width, c_height),
background=False,
- selectable=include_invite,
+ selectable=show_invite or show_discord,
)
- bui.widget(edit=self._container, up_widget=tab_button)
+ # Allows escaping if we select the container somehow (though
+ # shouldn't be possible when buttons are present).
+ bui.widget(edit=container, up_widget=tab_button)
- bui.textwidget(
- parent=self._container,
- position=(
- region_width * 0.5,
- c_height_2 * (0.58 if include_invite else 0.5),
- ),
- color=(0.6, 1.0, 0.6),
- scale=msc_scale,
- size=(0, 0),
- maxwidth=region_width * 0.9,
- max_height=c_height_2 * (0.7 if include_invite else 0.9),
- h_align='center',
- v_align='center',
- text=message,
- )
-
- if include_invite:
+ y = c_height - 30
+ if show_message:
bui.textwidget(
- parent=self._container,
- position=(region_width * 0.57, 35),
+ parent=container,
+ position=(region_width * 0.5, y),
+ color=(0.6, 1.0, 0.6),
+ scale=msc_scale,
+ size=(0, 0),
+ maxwidth=region_width * 0.9,
+ max_height=message_height,
+ h_align='center',
+ v_align='top',
+ text=message,
+ )
+ y -= message_height
+ if show_message_extra:
+ y -= message_extra_height
+
+ if show_invite:
+ bui.textwidget(
+ parent=container,
+ position=(region_width * 0.57, y),
color=(0, 1, 0),
scale=0.6,
size=(0, 0),
@@ -110,9 +145,9 @@ class AboutGatherTab(GatherTab):
subs=[('${COUNT}', str(try_tickets))],
),
)
- bui.buttonwidget(
- parent=self._container,
- position=(region_width * 0.59, 10),
+ invite_button = bui.buttonwidget(
+ parent=container,
+ position=(region_width * 0.59, y - 25),
size=(230, 50),
color=(0.54, 0.42, 0.56),
textcolor=(0, 1, 0),
@@ -124,7 +159,44 @@ class AboutGatherTab(GatherTab):
on_activate_call=bui.WeakCall(self._invite_to_try_press),
up_widget=tab_button,
)
- return self._container
+ y -= invite_height
+ else:
+ invite_button = None
+
+ if show_discord:
+ bui.textwidget(
+ parent=container,
+ position=(region_width * 0.57, y),
+ color=(0.6, 0.6, 1),
+ scale=0.6,
+ size=(0, 0),
+ maxwidth=region_width * 0.5,
+ h_align='right',
+ v_align='center',
+ flatness=1.0,
+ text=bui.Lstr(resource='discordFriendsText'),
+ )
+ discord_button = bui.buttonwidget(
+ parent=container,
+ position=(region_width * 0.59, y - 25),
+ size=(230, 50),
+ color=(0.54, 0.42, 0.56),
+ textcolor=(0.6, 0.6, 1),
+ label=bui.Lstr(resource='discordJoinText'),
+ autoselect=True,
+ on_activate_call=bui.WeakCall(self._join_the_discord_press),
+ up_widget=(
+ invite_button if invite_button is not None else tab_button
+ ),
+ )
+ y -= discord_height
+ else:
+ discord_button = None
+
+ if discord_button is not None:
+ pass
+
+ return scroll_widget
def _invite_to_try_press(self) -> None:
from bauiv1lib.account import show_sign_in_prompt
@@ -137,3 +209,10 @@ class AboutGatherTab(GatherTab):
show_sign_in_prompt()
return
handle_app_invites_press()
+
+ def _join_the_discord_press(self) -> None:
+ # pylint: disable=cyclic-import
+ from bauiv1lib.discord import DiscordWindow
+
+ assert bui.app.classic is not None
+ DiscordWindow().get_root_widget()
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py b/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py
index 3202d812..e09968b1 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py
@@ -99,6 +99,7 @@ class ManualGatherTab(GatherTab):
self._party_edit_name_text: bui.Widget | None = None
self._party_edit_addr_text: bui.Widget | None = None
self._party_edit_port_text: bui.Widget | None = None
+ self._no_parties_added_text: bui.Widget | None = None
def on_activate(
self,
@@ -142,6 +143,7 @@ class ManualGatherTab(GatherTab):
playsound=True,
),
text=bui.Lstr(resource='gatherWindow.manualJoinSectionText'),
+ glow_type='uniform',
)
self._favorites_text = bui.textwidget(
parent=self._container,
@@ -162,6 +164,7 @@ class ManualGatherTab(GatherTab):
playsound=True,
),
text=bui.Lstr(resource='gatherWindow.favoritesText'),
+ glow_type='uniform',
)
bui.widget(edit=self._join_by_address_text, up_widget=tab_button)
bui.widget(
@@ -316,7 +319,7 @@ class ManualGatherTab(GatherTab):
self._check_button = bui.textwidget(
parent=self._container,
size=(250, 60),
- text=bui.Lstr(resource='gatherWindow.' 'showMyAddressText'),
+ text=bui.Lstr(resource='gatherWindow.showMyAddressText'),
v_align='center',
h_align='center',
click_activate=True,
@@ -331,6 +334,7 @@ class ManualGatherTab(GatherTab):
self._container,
c_width,
),
+ glow_type='uniform',
)
bui.widget(edit=self._check_button, up_widget=btn)
@@ -453,6 +457,24 @@ class ManualGatherTab(GatherTab):
claims_left_right=True,
)
+ self._no_parties_added_text = bui.textwidget(
+ parent=self._container,
+ size=(0, 0),
+ h_align='center',
+ v_align='center',
+ text='',
+ color=(0.6, 0.6, 0.6),
+ scale=1.2,
+ position=(
+ (
+ (190 if uiscale is bui.UIScale.SMALL else 225)
+ + sub_scroll_width * 0.5
+ ),
+ v + sub_scroll_height * 0.5,
+ ),
+ glow_type='uniform',
+ )
+
self._favorite_selected = None
self._refresh_favorites()
@@ -695,6 +717,12 @@ class ManualGatherTab(GatherTab):
assert self._favorites_scroll_width is not None
assert self._favorites_connect_button is not None
+
+ bui.textwidget(
+ edit=self._no_parties_added_text,
+ text='',
+ )
+ num_of_fav = 0
for i, server in enumerate(servers):
txt = bui.textwidget(
parent=self._columnwidget,
@@ -718,11 +746,13 @@ class ManualGatherTab(GatherTab):
)
if i == 0:
bui.widget(edit=txt, up_widget=self._favorites_text)
+ self._favorite_selected = server
bui.widget(
edit=txt,
left_widget=self._favorites_connect_button,
right_widget=txt,
)
+ num_of_fav = num_of_fav + 1
# If there's no servers, allow selecting out of the scroll area
bui.containerwidget(
@@ -735,6 +765,11 @@ class ManualGatherTab(GatherTab):
up_widget=self._favorites_text,
left_widget=self._favorites_connect_button,
)
+ if num_of_fav == 0:
+ bui.textwidget(
+ edit=self._no_parties_added_text,
+ text=bui.Lstr(resource='gatherWindow.noPartiesAddedText'),
+ )
def on_deactivate(self) -> None:
self._access_check_timer = None
@@ -800,8 +835,17 @@ class ManualGatherTab(GatherTab):
}
config.commit()
bui.getsound('gunCocking').play()
+ bui.screenmessage(
+ bui.Lstr(
+ resource='addedToFavoritesText', subs=[('${NAME}', addr)]
+ ),
+ color=(0, 1, 0),
+ )
else:
- bui.screenmessage('Invalid Address', color=(1, 0, 0))
+ bui.screenmessage(
+ bui.Lstr(resource='internal.invalidAddressErrorText'),
+ color=(1, 0, 0),
+ )
bui.getsound('error').play()
def _host_lookup_result(
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py
index 1cf4e43a..e66cd6c1 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py
@@ -120,6 +120,7 @@ class PrivateGatherTab(GatherTab):
playsound=True,
),
text=bui.Lstr(resource='gatherWindow.privatePartyJoinText'),
+ glow_type='uniform',
)
self._host_sub_tab_text = bui.textwidget(
parent=self._container,
@@ -138,6 +139,7 @@ class PrivateGatherTab(GatherTab):
playsound=True,
),
text=bui.Lstr(resource='gatherWindow.privatePartyHostText'),
+ glow_type='uniform',
)
bui.widget(edit=self._join_sub_tab_text, up_widget=tab_button)
bui.widget(
@@ -458,9 +460,9 @@ class PrivateGatherTab(GatherTab):
scale=1.5,
size=(300, 50),
editable=True,
+ max_chars=20,
description=bui.Lstr(resource='gatherWindow.partyCodeText'),
autoselect=True,
- maxwidth=250,
h_align='left',
v_align='center',
text='',
@@ -962,7 +964,7 @@ class PrivateGatherTab(GatherTab):
code = cast(str, bui.textwidget(query=self._join_party_code_text))
if not code:
bui.screenmessage(
- bui.Lstr(resource='internal.invalidAddressErrorText'),
+ bui.Lstr(translate=('serverResponses', 'Invalid code.')),
color=(1, 0, 0),
)
bui.getsound('error').play()
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py
index 0a9ef110..e1619436 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py
@@ -114,7 +114,7 @@ class UIRow:
self._name_widget = bui.textwidget(
text=bui.Lstr(value=party.name),
parent=columnwidget,
- size=(sub_scroll_width * 0.63, 20),
+ size=(sub_scroll_width * 0.46, 20),
position=(0 + hpos, 4 + vpos),
selectable=True,
on_select_call=bui.WeakCall(
@@ -248,6 +248,7 @@ class AddrFetchThread(Thread):
self._call = call
def run(self) -> None:
+ sock: socket.socket | None = None
try:
# FIXME: Update this to work with IPv6 at some point.
import socket
@@ -255,7 +256,6 @@ class AddrFetchThread(Thread):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(('8.8.8.8', 80))
val = sock.getsockname()[0]
- sock.close()
bui.pushcall(bui.Call(self._call, val), from_other_thread=True)
except Exception as exc:
from efro.error import is_udp_communication_error
@@ -265,6 +265,9 @@ class AddrFetchThread(Thread):
pass
else:
logging.exception('Error in addr-fetch-thread')
+ finally:
+ if sock is not None:
+ sock.close()
class PingThread(Thread):
@@ -361,6 +364,7 @@ class PublicGatherTab(GatherTab):
self._last_server_list_query_time: float | None = None
self._join_list_column: bui.Widget | None = None
self._join_status_text: bui.Widget | None = None
+ self._no_servers_found_text: bui.Widget | None = None
self._host_max_party_size_value: bui.Widget | None = None
self._host_max_party_size_minus_button: (bui.Widget | None) = None
self._host_max_party_size_plus_button: (bui.Widget | None) = None
@@ -431,6 +435,7 @@ class PublicGatherTab(GatherTab):
text=bui.Lstr(
resource='gatherWindow.' 'joinPublicPartyDescriptionText'
),
+ glow_type='uniform',
)
self._host_text = bui.textwidget(
parent=self._container,
@@ -453,6 +458,7 @@ class PublicGatherTab(GatherTab):
text=bui.Lstr(
resource='gatherWindow.' 'hostPublicPartyDescriptionText'
),
+ glow_type='uniform',
)
bui.widget(edit=self._join_text, up_widget=tab_button)
bui.widget(
@@ -658,6 +664,18 @@ class PublicGatherTab(GatherTab):
color=(0.6, 0.6, 0.6),
position=(c_width * 0.5, c_height * 0.5),
)
+ self._no_servers_found_text = bui.textwidget(
+ parent=self._container,
+ text='',
+ size=(0, 0),
+ scale=0.9,
+ flatness=1.0,
+ shadow=0.0,
+ h_align='center',
+ v_align='top',
+ color=(0.6, 0.6, 0.6),
+ position=(c_width * 0.5, c_height * 0.5),
+ )
def _build_host_tab(
self, region_width: float, region_height: float
@@ -950,6 +968,9 @@ class PublicGatherTab(GatherTab):
self._update_party_rows()
def _update_party_rows(self) -> None:
+ plus = bui.app.plus
+ assert plus is not None
+
columnwidget = self._join_list_column
if not columnwidget:
return
@@ -963,6 +984,7 @@ class PublicGatherTab(GatherTab):
edit=self._host_scrollwidget,
claims_up_down=(len(self._parties_displayed) > 0),
)
+ bui.textwidget(edit=self._no_servers_found_text, text='')
# Clip if we have more UI rows than parties to show.
clipcount = len(self._ui_rows) - len(self._parties_displayed)
@@ -972,6 +994,15 @@ class PublicGatherTab(GatherTab):
# If we have no parties to show, we're done.
if not self._parties_displayed:
+ text = self._join_status_text
+ if (
+ plus.get_v1_account_state() == 'signed_in'
+ and cast(str, bui.textwidget(query=text)) == ''
+ ):
+ bui.textwidget(
+ edit=self._no_servers_found_text,
+ text=bui.Lstr(resource='noServersFoundText'),
+ )
return
sub_scroll_width = 830
diff --git a/src/assets/ba_data/python/bauiv1lib/getcurrency.py b/src/assets/ba_data/python/bauiv1lib/getcurrency.py
index a87ec18f..6e355d42 100644
--- a/src/assets/ba_data/python/bauiv1lib/getcurrency.py
+++ b/src/assets/ba_data/python/bauiv1lib/getcurrency.py
@@ -334,7 +334,7 @@ class GetCurrencyWindow(bui.Window):
tex_scale=1.2,
) # 19.99-ish
- self._enable_ad_button = bui.has_video_ads()
+ self._enable_ad_button = plus.has_video_ads()
h = self._width * 0.5 + 110.0
v = self._height - b_size[1] - 115.0
@@ -561,7 +561,7 @@ class GetCurrencyWindow(bui.Window):
next_reward_ad_time
)
now = datetime.datetime.utcnow()
- if bui.have_incentivized_ad() and (
+ if plus.have_incentivized_ad() and (
next_reward_ad_time is None or next_reward_ad_time <= now
):
self._ad_button_greyed = False
@@ -732,8 +732,13 @@ class GetCurrencyWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.store import browser
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if self._transitioning_out:
return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
@@ -745,7 +750,9 @@ class GetCurrencyWindow(bui.Window):
).get_root_widget()
if not self._from_modal_store:
assert bui.app.classic is not None
- bui.app.ui_v1.set_main_menu_window(window)
+ bui.app.ui_v1.set_main_menu_window(
+ window, from_window=self._root_widget
+ )
self._transitioning_out = True
diff --git a/src/assets/ba_data/python/bauiv1lib/helpui.py b/src/assets/ba_data/python/bauiv1lib/helpui.py
index 6a7ed437..e1981aad 100644
--- a/src/assets/ba_data/python/bauiv1lib/helpui.py
+++ b/src/assets/ba_data/python/bauiv1lib/helpui.py
@@ -36,8 +36,8 @@ class HelpWindow(bui.Window):
self._main_menu = main_menu
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
- width = 950 if uiscale is bui.UIScale.SMALL else 750
- x_offs = 100 if uiscale is bui.UIScale.SMALL else 0
+ width = 1050 if uiscale is bui.UIScale.SMALL else 750
+ x_offs = 150 if uiscale is bui.UIScale.SMALL else 0
height = (
460
if uiscale is bui.UIScale.SMALL
@@ -645,11 +645,16 @@ class HelpWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if self._main_menu:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/keyboard/englishkeyboard.py b/src/assets/ba_data/python/bauiv1lib/keyboard/englishkeyboard.py
index ae94d938..3de74f87 100644
--- a/src/assets/ba_data/python/bauiv1lib/keyboard/englishkeyboard.py
+++ b/src/assets/ba_data/python/bauiv1lib/keyboard/englishkeyboard.py
@@ -9,7 +9,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
-import babase
+import bauiv1 as bui
if TYPE_CHECKING:
from typing import Iterable
@@ -33,15 +33,15 @@ def split(chars: Iterable[str], maxlen: int) -> list[list[str]]:
def generate_emojis(maxlen: int) -> list[list[str]]:
- """Generates a lot of UTF8 emojis prepared for babase.Keyboard pages"""
+ """Generates a lot of UTF8 emojis prepared for bui.Keyboard pages"""
all_emojis = split([chr(i) for i in range(0x1F601, 0x1F650)], maxlen)
all_emojis += split([chr(i) for i in range(0x2702, 0x27B1)], maxlen)
all_emojis += split([chr(i) for i in range(0x1F680, 0x1F6C1)], maxlen)
return all_emojis
-# ba_meta export keyboard
-class EnglishKeyboard(babase.Keyboard):
+# ba_meta export bauiv1.Keyboard
+class EnglishKeyboard(bui.Keyboard):
"""Default English keyboard."""
name = 'English'
diff --git a/src/assets/ba_data/python/bauiv1lib/kiosk.py b/src/assets/ba_data/python/bauiv1lib/kiosk.py
index 377a86d0..ab1ca87e 100644
--- a/src/assets/ba_data/python/bauiv1lib/kiosk.py
+++ b/src/assets/ba_data/python/bauiv1lib/kiosk.py
@@ -501,9 +501,15 @@ class KioskWindow(bui.Window):
def _do_full_menu(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
bui.app.classic.did_menu_intro = True # prevent delayed transition-in
- bui.app.ui_v1.set_main_menu_window(MainMenuWindow().get_root_widget())
+ bui.app.ui_v1.set_main_menu_window(
+ MainMenuWindow().get_root_widget(), from_window=self._root_widget
+ )
diff --git a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py
index d108cbb3..725bb318 100644
--- a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py
+++ b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py
@@ -1142,6 +1142,10 @@ class LeagueRankWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.coop.browser import CoopBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -1149,5 +1153,6 @@ class LeagueRankWindow(bui.Window):
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- CoopBrowserWindow(transition='in_left').get_root_widget()
+ CoopBrowserWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py
index d1a00332..4562a7fa 100644
--- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py
+++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py
@@ -311,8 +311,8 @@ class MainMenuWindow(bui.Window):
else self._confirm_end_game
),
)
- # Assume we're in a client-session.
else:
+ # Assume we're in a client-session.
bui.buttonwidget(
parent=self._root_widget,
position=(h - self._button_width * 0.5 * scale, v),
@@ -360,7 +360,6 @@ class MainMenuWindow(bui.Window):
tilt_scale=0.0,
draw_controller=store_button,
)
-
self._tdelay += self._t_delay_inc
else:
self._store_button = None
@@ -1039,6 +1038,10 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.confirm import QuitWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# Note: Normally we should go through bui.quit(confirm=True) but
# invoking the window directly lets us scale it up from the
# button.
@@ -1048,24 +1051,34 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.kiosk import KioskWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- KioskWindow(transition='in_left').get_root_widget()
+ KioskWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _show_account_window(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.account.settings import AccountSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AccountSettingsWindow(
origin_widget=self._account_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_store_pressed(self) -> None:
@@ -1073,6 +1086,10 @@ class MainMenuWindow(bui.Window):
from bauiv1lib.store.browser import StoreBrowserWindow
from bauiv1lib.account import show_sign_in_prompt
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -1085,7 +1102,8 @@ class MainMenuWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
StoreBrowserWindow(
origin_widget=self._store_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _is_benchmark(self) -> bool:
@@ -1150,8 +1168,11 @@ class MainMenuWindow(bui.Window):
def _end_game(self) -> None:
assert bui.app.classic is not None
- if not self._root_widget:
+
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
return
+
bui.containerwidget(edit=self._root_widget, transition='out_left')
bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False)
@@ -1167,39 +1188,54 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.creditslist import CreditsListWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
CreditsListWindow(
origin_widget=self._credits_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _howtoplay(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.helpui import HelpWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
HelpWindow(
main_menu=True, origin_widget=self._how_to_play_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _settings(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.allsettings import AllSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AllSettingsWindow(
origin_widget=self._settings_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _resume_and_call(self, call: Callable[[], Any]) -> None:
@@ -1208,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.
@@ -1282,35 +1320,50 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.gather import GatherWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- GatherWindow(origin_widget=self._gather_button).get_root_widget()
+ GatherWindow(origin_widget=self._gather_button).get_root_widget(),
+ from_window=self._root_widget,
)
def _watch_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.watch import WatchWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- WatchWindow(origin_widget=self._watch_button).get_root_widget()
+ WatchWindow(origin_widget=self._watch_button).get_root_widget(),
+ from_window=self._root_widget,
)
def _play_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.selecting_private_party_playlist = False
bui.app.ui_v1.set_main_menu_window(
- PlayWindow(origin_widget=self._start_button).get_root_widget()
+ PlayWindow(origin_widget=self._start_button).get_root_widget(),
+ from_window=self._root_widget,
)
def _resume(self) -> None:
@@ -1318,7 +1371,7 @@ class MainMenuWindow(bui.Window):
bui.app.classic.resume()
if self._root_widget:
bui.containerwidget(edit=self._root_widget, transition='out_right')
- bui.app.ui_v1.clear_main_menu_window()
+ bui.app.ui_v1.clear_main_menu_window(transition='out_right')
# If there's callbacks waiting for this window to go away, call them.
for call in bui.app.ui_v1.main_menu_resume_callbacks:
diff --git a/src/assets/ba_data/python/bauiv1lib/party.py b/src/assets/ba_data/python/bauiv1lib/party.py
index 5920c2d8..e531b860 100644
--- a/src/assets/ba_data/python/bauiv1lib/party.py
+++ b/src/assets/ba_data/python/bauiv1lib/party.py
@@ -40,6 +40,7 @@ class PartyWindow(bui.Window):
if uiscale is bui.UIScale.MEDIUM
else 600
)
+ self._display_old_msgs = True
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
@@ -92,9 +93,10 @@ class PartyWindow(bui.Window):
iconscale=1.2,
)
- info = bs.get_connection_to_host_info()
- if info.get('name', '') != '':
- title = bui.Lstr(value=info['name'])
+ info = bs.get_connection_to_host_info_2()
+
+ if info is not None and info.name != '':
+ title = bui.Lstr(value=info.name)
else:
title = bui.Lstr(resource=self._r + '.titleText')
@@ -142,12 +144,6 @@ class PartyWindow(bui.Window):
)
self._chat_texts: list[bui.Widget] = []
- # add all existing messages if chat is not muted
- if not bui.app.config.resolve('Chat Muted'):
- msgs = bs.get_chat_messages()
- for msg in msgs:
- self._add_msg(msg)
-
self._text_field = txt = bui.textwidget(
parent=self._root_widget,
editable=True,
@@ -233,6 +229,23 @@ class PartyWindow(bui.Window):
is_muted = bui.app.config.resolve('Chat Muted')
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
+
+ choices: list[str] = ['unmute' if is_muted else 'mute']
+ choices_display: list[bui.Lstr] = [
+ bui.Lstr(resource='chatUnMuteText' if is_muted else 'chatMuteText')
+ ]
+
+ # Allow the 'Add to Favorites' option only if we're actually
+ # connected to a party and if it doesn't seem to be a private
+ # party (those are dynamically assigned addresses and ports so
+ # it makes no sense to save them).
+ server_info = bs.get_connection_to_host_info_2()
+ if server_info is not None and not server_info.name.startswith(
+ 'Private Party '
+ ):
+ choices.append('add_to_favorites')
+ choices_display.append(bui.Lstr(resource='addToFavoritesText'))
+
PopupMenuWindow(
position=self._menu_button.get_screen_space_center(),
scale=(
@@ -242,12 +255,8 @@ class PartyWindow(bui.Window):
if uiscale is bui.UIScale.MEDIUM
else 1.23
),
- choices=['unmute' if is_muted else 'mute'],
- choices_display=[
- bui.Lstr(
- resource='chatUnMuteText' if is_muted else 'chatMuteText'
- )
- ],
+ choices=choices,
+ choices_display=choices_display,
current_choice='unmute' if is_muted else 'mute',
delegate=self,
)
@@ -269,6 +278,12 @@ class PartyWindow(bui.Window):
first.delete()
else:
bui.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0))
+ # add all existing messages if chat is not muted
+ if self._display_old_msgs:
+ msgs = bs.get_chat_messages()
+ for msg in msgs:
+ self._add_msg(msg)
+ self._display_old_msgs = False
# update roster section
roster = bs.get_game_roster()
@@ -466,10 +481,75 @@ class PartyWindow(bui.Window):
cfg = bui.app.config
cfg['Chat Muted'] = choice == 'mute'
cfg.apply_and_commit()
+ self._display_old_msgs = True
self._update()
+ if choice == 'add_to_favorites':
+ info = bs.get_connection_to_host_info_2()
+ if info is not None:
+ self._add_to_favorites(
+ name=info.name,
+ address=info.address,
+ port_num=info.port,
+ )
+ else:
+ # We should not allow the user to see this option
+ # if they aren't in a server; this is our bad.
+ bui.screenmessage(
+ bui.Lstr(resource='errorText'), color=(1, 0, 0)
+ )
+ bui.getsound('error').play()
else:
print(f'unhandled popup type: {self._popup_type}')
+ def _add_to_favorites(
+ self, name: str, address: str | None, port_num: int | None
+ ) -> None:
+ addr = address
+ if addr == '':
+ bui.screenmessage(
+ bui.Lstr(resource='internal.invalidAddressErrorText'),
+ color=(1, 0, 0),
+ )
+ bui.getsound('error').play()
+ return
+ port = port_num if port_num is not None else -1
+ if port > 65535 or port < 0:
+ bui.screenmessage(
+ bui.Lstr(resource='internal.invalidPortErrorText'),
+ color=(1, 0, 0),
+ )
+ bui.getsound('error').play()
+ return
+
+ # Avoid empty names.
+ if not name:
+ name = f'{addr}@{port}'
+
+ config = bui.app.config
+
+ if addr:
+ if not isinstance(config.get('Saved Servers'), dict):
+ config['Saved Servers'] = {}
+ config['Saved Servers'][f'{addr}@{port}'] = {
+ 'addr': addr,
+ 'port': port,
+ 'name': name,
+ }
+ config.commit()
+ bui.getsound('gunCocking').play()
+ bui.screenmessage(
+ bui.Lstr(
+ resource='addedToFavoritesText', subs=[('${NAME}', name)]
+ ),
+ color=(0, 1, 0),
+ )
+ else:
+ bui.screenmessage(
+ bui.Lstr(resource='internal.invalidAddressErrorText'),
+ color=(1, 0, 0),
+ )
+ bui.getsound('error').play()
+
def popup_menu_closing(self, popup_window: PopupWindow) -> None:
"""Called when the popup is closing."""
@@ -481,7 +561,8 @@ class PartyWindow(bui.Window):
kick_str = bui.Lstr(resource='kickText')
else:
# kick-votes appeared in build 14248
- if bs.get_connection_to_host_info().get('build_number', 0) < 14248:
+ info = bs.get_connection_to_host_info_2()
+ if info is None or info.build_number < 14248:
return
kick_str = bui.Lstr(resource='kickVoteText')
assert bui.app.classic is not None
@@ -510,9 +591,17 @@ class PartyWindow(bui.Window):
def close(self) -> None:
"""Close the window."""
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_scale')
def close_with_sound(self) -> None:
"""Close the window and make a lovely sound."""
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.getsound('swish').play()
self.close()
diff --git a/src/assets/ba_data/python/bauiv1lib/play.py b/src/assets/ba_data/python/bauiv1lib/play.py
index ef1ca896..ea52588c 100644
--- a/src/assets/ba_data/python/bauiv1lib/play.py
+++ b/src/assets/ba_data/python/bauiv1lib/play.py
@@ -32,8 +32,8 @@ class PlayWindow(bui.Window):
self._is_main_menu = not bui.app.ui_v1.selecting_private_party_playlist
uiscale = bui.app.ui_v1.uiscale
- width = 1000 if uiscale is bui.UIScale.SMALL else 800
- x_offs = 100 if uiscale is bui.UIScale.SMALL else 0
+ width = 1100 if uiscale is bui.UIScale.SMALL else 800
+ x_offs = 150 if uiscale is bui.UIScale.SMALL else 0
height = 550
button_width = 400
@@ -521,13 +521,19 @@ class PlayWindow(bui.Window):
def _back(self) -> None:
# pylint: disable=cyclic-import
+
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if self._is_main_menu:
from bauiv1lib.mainmenu import MainMenuWindow
self._save_state()
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -538,7 +544,8 @@ class PlayWindow(bui.Window):
self._save_state()
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- GatherWindow(transition='in_left').get_root_widget()
+ GatherWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -549,6 +556,10 @@ class PlayWindow(bui.Window):
from bauiv1lib.account import show_sign_in_prompt
from bauiv1lib.coop.browser import CoopBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -559,26 +570,38 @@ class PlayWindow(bui.Window):
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- CoopBrowserWindow(origin_widget=self._coop_button).get_root_widget()
+ CoopBrowserWindow(
+ origin_widget=self._coop_button
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _team_tourney(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.playlist.browser import PlaylistBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PlaylistBrowserWindow(
origin_widget=self._teams_button, sessiontype=bs.DualTeamSession
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _free_for_all(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.playlist.browser import PlaylistBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
@@ -586,7 +609,8 @@ class PlayWindow(bui.Window):
PlaylistBrowserWindow(
origin_widget=self._free_for_all_button,
sessiontype=bs.FreeForAllSession,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _draw_dude(
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/addgame.py b/src/assets/ba_data/python/bauiv1lib/playlist/addgame.py
index 47450726..c8acdb8a 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/addgame.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/addgame.py
@@ -198,7 +198,7 @@ class PlaylistAddGameWindow(bui.Window):
txt = bui.textwidget(
parent=self._column,
position=(0, 0),
- size=(self._width - 88, 24),
+ size=(self._scroll_width * 1.1, 24),
text=gametype.get_display_string(),
h_align='left',
v_align='center',
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/browser.py b/src/assets/ba_data/python/bauiv1lib/playlist/browser.py
index 287496c2..806ab7aa 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/browser.py
@@ -62,8 +62,8 @@ class PlaylistBrowserWindow(bui.Window):
)
uiscale = bui.app.ui_v1.uiscale
- self._width = 900.0 if uiscale is bui.UIScale.SMALL else 800.0
- x_inset = 50 if uiscale is bui.UIScale.SMALL else 0
+ self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0
+ x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
self._height = (
480
if uiscale is bui.UIScale.SMALL
@@ -684,6 +684,10 @@ class PlaylistBrowserWindow(bui.Window):
PlaylistCustomizeBrowserWindow,
)
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
@@ -691,13 +695,18 @@ class PlaylistBrowserWindow(bui.Window):
PlaylistCustomizeBrowserWindow(
origin_widget=self._customize_button,
sessiontype=self._sessiontype,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_back_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# Store our selected playlist if that's changed.
if self._selected_playlist is not None:
prev_sel = bui.app.config.get(
@@ -716,7 +725,8 @@ class PlaylistBrowserWindow(bui.Window):
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- PlayWindow(transition='in_left').get_root_widget()
+ PlayWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py b/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py
index 789e7f2e..c5bd8428 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py
@@ -47,8 +47,8 @@ class PlaylistCustomizeBrowserWindow(bui.Window):
self._r = 'gameListWindow'
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
- self._width = 750.0 if uiscale is bui.UIScale.SMALL else 650.0
- x_inset = 50.0 if uiscale is bui.UIScale.SMALL else 0.0
+ self._width = 850.0 if uiscale is bui.UIScale.SMALL else 650.0
+ x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
self._height = (
380.0
if uiscale is bui.UIScale.SMALL
@@ -323,6 +323,10 @@ class PlaylistCustomizeBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.playlist import browser
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if self._selected_playlist_name is not None:
cfg = bui.app.config
cfg[
@@ -337,7 +341,8 @@ class PlaylistCustomizeBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
browser.PlaylistBrowserWindow(
transition='in_left', sessiontype=self._sessiontype
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _select(self, name: str, index: int) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/edit.py b/src/assets/ba_data/python/bauiv1lib/playlist/edit.py
index 1b971514..11e00190 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/edit.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/edit.py
@@ -31,8 +31,8 @@ class PlaylistEditWindow(bui.Window):
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
- self._width = 770 if uiscale is bui.UIScale.SMALL else 670
- x_inset = 50 if uiscale is bui.UIScale.SMALL else 0
+ self._width = 870 if uiscale is bui.UIScale.SMALL else 670
+ x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
self._height = (
400
if uiscale is bui.UIScale.SMALL
@@ -283,6 +283,10 @@ class PlaylistEditWindow(bui.Window):
PlaylistCustomizeBrowserWindow,
)
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.getsound('powerdown01').play()
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
@@ -293,7 +297,8 @@ class PlaylistEditWindow(bui.Window):
select_playlist=(
self._editcontroller.get_existing_playlist_name()
),
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _add(self) -> None:
@@ -315,6 +320,10 @@ class PlaylistEditWindow(bui.Window):
PlaylistCustomizeBrowserWindow,
)
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -380,7 +389,8 @@ class PlaylistEditWindow(bui.Window):
transition='in_left',
sessiontype=self._editcontroller.get_session_type(),
select_playlist=new_name,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _save_press_with_sound(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py b/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py
index 26e53b27..7ed9a92a 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py
@@ -92,7 +92,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition=transition
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=False, # Disable this check.
)
def get_config_name(self) -> str:
@@ -150,7 +151,8 @@ class PlaylistEditController:
assert bui.app.classic is not None
bui.app.ui_v1.clear_main_menu_window(transition='out_left')
bui.app.ui_v1.set_main_menu_window(
- PlaylistAddGameWindow(editcontroller=self).get_root_widget()
+ PlaylistAddGameWindow(editcontroller=self).get_root_widget(),
+ from_window=None,
)
def edit_game_pressed(self) -> None:
@@ -175,7 +177,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=None,
)
def _show_edit_ui(
@@ -205,7 +208,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=None,
)
# Otherwise we were adding; go back to the add type choice list.
@@ -214,7 +218,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistAddGameWindow(
editcontroller=self, transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=None,
)
else:
# Make sure type is in there.
@@ -236,5 +241,6 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=None,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py b/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py
index 4e7c3d84..491608f9 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py
@@ -103,8 +103,8 @@ class PlaylistEditGameWindow(bui.Window):
self._choice_selections: dict[str, int] = {}
uiscale = bui.app.ui_v1.uiscale
- width = 720 if uiscale is bui.UIScale.SMALL else 620
- x_inset = 50 if uiscale is bui.UIScale.SMALL else 0
+ width = 820 if uiscale is bui.UIScale.SMALL else 620
+ x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
height = (
365
if uiscale is bui.UIScale.SMALL
@@ -514,6 +514,10 @@ class PlaylistEditGameWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# Replace ourself with the map-select UI.
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
@@ -524,7 +528,8 @@ class PlaylistEditGameWindow(bui.Window):
copy.deepcopy(self._getconfig()),
self._edit_info,
self._completion_call,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _choice_inc(
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py b/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py
index c86854d6..77200bea 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py
@@ -44,8 +44,8 @@ class PlaylistMapSelectWindow(bui.Window):
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
- width = 715 if uiscale is bui.UIScale.SMALL else 615
- x_inset = 50 if uiscale is bui.UIScale.SMALL else 0
+ width = 815 if uiscale is bui.UIScale.SMALL else 615
+ x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
height = (
400
if uiscale is bui.UIScale.SMALL
@@ -273,6 +273,10 @@ class PlaylistMapSelectWindow(bui.Window):
def _select(self, map_name: str) -> None:
from bauiv1lib.playlist.editgame import PlaylistEditGameWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._config['settings']['map'] = map_name
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
@@ -285,7 +289,8 @@ class PlaylistMapSelectWindow(bui.Window):
default_selection='map',
transition='in_left',
edit_info=self._edit_info,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _select_with_delay(self, map_name: str) -> None:
@@ -296,6 +301,10 @@ class PlaylistMapSelectWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.playlist.editgame import PlaylistEditGameWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
@@ -307,5 +316,6 @@ class PlaylistMapSelectWindow(bui.Window):
default_selection='map',
transition='in_left',
edit_info=self._edit_info,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/playoptions.py b/src/assets/ba_data/python/bauiv1lib/playoptions.py
index fdc79e74..ea58e4d6 100644
--- a/src/assets/ba_data/python/bauiv1lib/playoptions.py
+++ b/src/assets/ba_data/python/bauiv1lib/playoptions.py
@@ -140,7 +140,6 @@ class PlayOptionsWindow(PopupWindow):
if show_shuffle_check_box:
self._height += 40
- # Creates our _root_widget.
uiscale = bui.app.ui_v1.uiscale
scale = (
1.69
@@ -149,6 +148,7 @@ class PlayOptionsWindow(PopupWindow):
if uiscale is bui.UIScale.MEDIUM
else 0.85
)
+ # Creates our _root_widget.
super().__init__(
position=scale_origin, size=(self._width, self._height), scale=scale
)
@@ -448,6 +448,10 @@ class PlayOptionsWindow(PopupWindow):
self._transition_out()
def _on_ok_press(self) -> None:
+ # no-op if our underlying widget is dead or on its way out.
+ if not self.root_widget or self.root_widget.transitioning_out:
+ return
+
# Disallow if our playlist has disappeared.
if not self._does_target_playlist_exist():
return
@@ -478,8 +482,12 @@ class PlayOptionsWindow(PopupWindow):
cfg['Private Party Host Session Type'] = typename
bui.getsound('gunCocking').play()
assert bui.app.classic is not None
+ # Note: this is a wonky situation where we aren't actually
+ # the main window but we set it on behalf of the main window
+ # that popped us up.
bui.app.ui_v1.set_main_menu_window(
- GatherWindow(transition='in_right').get_root_widget()
+ GatherWindow(transition='in_right').get_root_widget(),
+ from_window=False, # Disable this test.
)
self._transition_out(transition='out_left')
if self._delegate is not None:
diff --git a/src/assets/ba_data/python/bauiv1lib/profile/browser.py b/src/assets/ba_data/python/bauiv1lib/profile/browser.py
index 55c1e121..ef9db117 100644
--- a/src/assets/ba_data/python/bauiv1lib/profile/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/profile/browser.py
@@ -33,8 +33,8 @@ class ProfileBrowserWindow(bui.Window):
back_label = bui.Lstr(resource='doneText')
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
- self._width = 700.0 if uiscale is bui.UIScale.SMALL else 600.0
- x_inset = 50.0 if uiscale is bui.UIScale.SMALL else 0.0
+ self._width = 800.0 if uiscale is bui.UIScale.SMALL else 600.0
+ x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
self._height = (
360.0
if uiscale is bui.UIScale.SMALL
@@ -197,8 +197,10 @@ class ProfileBrowserWindow(bui.Window):
bui.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
- self._columnwidget = bui.columnwidget(
- parent=self._scrollwidget, border=2, margin=0
+ self._subcontainer = bui.containerwidget(
+ parent=self._scrollwidget,
+ size=(self._scroll_width, 32),
+ background=False,
)
v -= 255
self._profiles: dict[str, dict[str, Any]] | None = None
@@ -212,6 +214,10 @@ class ProfileBrowserWindow(bui.Window):
from bauiv1lib.profile.edit import EditProfileWindow
from bauiv1lib.purchase import PurchaseWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -252,7 +258,8 @@ class ProfileBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
EditProfileWindow(
existing_profile=None, in_main_menu=self._in_main_menu
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget if self._in_main_menu else False,
)
def _delete_profile(self) -> None:
@@ -301,6 +308,10 @@ class ProfileBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.profile.edit import EditProfileWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if self._selected_profile is None:
bui.getsound('error').play()
bui.screenmessage(
@@ -313,7 +324,8 @@ class ProfileBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
EditProfileWindow(
self._selected_profile, in_main_menu=self._in_main_menu
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget if self._in_main_menu else False,
)
def _select(self, name: str, index: int) -> None:
@@ -324,6 +336,10 @@ class ProfileBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.account.settings import AccountSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
self._save_state()
@@ -333,7 +349,8 @@ class ProfileBrowserWindow(bui.Window):
if self._in_main_menu:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AccountSettingsWindow(transition='in_left').get_root_widget()
+ AccountSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
# If we're being called up standalone, handle pause/resume ourself.
@@ -342,8 +359,10 @@ class ProfileBrowserWindow(bui.Window):
def _refresh(self) -> None:
# pylint: disable=too-many-locals
+ # pylint: disable=too-many-statements
from efro.util import asserttype
from bascenev1 import PlayerProfilesChangedMessage
+ from bascenev1lib.actor import spazappearance
assert bui.app.classic is not None
@@ -359,14 +378,27 @@ class ProfileBrowserWindow(bui.Window):
assert self._profiles is not None
items = list(self._profiles.items())
items.sort(key=lambda x: asserttype(x[0], str).lower())
+ spazzes = spazappearance.get_appearances()
+ spazzes.sort()
+ icon_textures = [
+ bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture)
+ for s in spazzes
+ ]
+ icon_tint_textures = [
+ bui.gettexture(
+ bui.app.classic.spaz_appearances[s].icon_mask_texture
+ )
+ for s in spazzes
+ ]
index = 0
+ y_val = 35 * (len(self._profiles) - 1)
account_name: str | None
if plus.get_v1_account_state() == 'signed_in':
account_name = plus.get_v1_account_display_string()
else:
account_name = None
widget_to_select = None
- for p_name, _ in items:
+ for p_name, p_info in items:
if p_name == '__account__' and account_name is None:
continue
color, _highlight = bui.app.classic.get_player_profile_colors(
@@ -378,16 +410,35 @@ class ProfileBrowserWindow(bui.Window):
if p_name == '__account__'
else bui.app.classic.get_player_profile_icon(p_name) + p_name
)
+
+ try:
+ char_index = spazzes.index(p_info['character'])
+ except Exception:
+ char_index = spazzes.index('Spaz')
+
assert isinstance(tval, str)
+ character = bui.buttonwidget(
+ parent=self._subcontainer,
+ position=(0, y_val),
+ size=(28, 28),
+ label='',
+ color=(1, 1, 1),
+ mask_texture=bui.gettexture('characterIconMask'),
+ tint_color=color,
+ tint2_color=_highlight,
+ texture=icon_textures[char_index],
+ tint_texture=icon_tint_textures[char_index],
+ selectable=False,
+ )
txtw = bui.textwidget(
- parent=self._columnwidget,
- position=(0, 32),
- size=((self._width - 40) / scl, 28),
+ parent=self._subcontainer,
+ position=(35, y_val),
+ size=((self._width - 210) / scl, 28),
text=bui.Lstr(value=tval),
h_align='left',
v_align='center',
on_select_call=bui.WeakCall(self._select, p_name, index),
- maxwidth=self._scroll_width * 0.92,
+ maxwidth=self._scroll_width * 0.86,
corner_scale=scl,
color=bui.safecolor(color, 0.4),
always_highlight=True,
@@ -396,8 +447,11 @@ class ProfileBrowserWindow(bui.Window):
)
if index == 0:
bui.widget(edit=txtw, up_widget=self._back_button)
+ if self._selected_profile is None:
+ self._selected_profile = p_name
bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40)
self._profile_widgets.append(txtw)
+ self._profile_widgets.append(character)
# Select/show this one if it was previously selected
# (but defer till after this loop since our height is
@@ -406,10 +460,15 @@ class ProfileBrowserWindow(bui.Window):
widget_to_select = txtw
index += 1
+ y_val -= 35
+ bui.containerwidget(
+ edit=self._subcontainer,
+ size=(self._scroll_width, index * 35),
+ )
if widget_to_select is not None:
- bui.columnwidget(
- edit=self._columnwidget,
+ bui.containerwidget(
+ edit=self._subcontainer,
selected_child=widget_to_select,
visible_child=widget_to_select,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/profile/edit.py b/src/assets/ba_data/python/bauiv1lib/profile/edit.py
index 5c9c899d..8456c607 100644
--- a/src/assets/ba_data/python/bauiv1lib/profile/edit.py
+++ b/src/assets/ba_data/python/bauiv1lib/profile/edit.py
@@ -18,12 +18,18 @@ class EditProfileWindow(bui.Window):
# FIXME: WILL NEED TO CHANGE THIS FOR UILOCATION.
def reload_window(self) -> None:
"""Transitions out and recreates ourself."""
+
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
EditProfileWindow(
self.getname(), self._in_main_menu
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def __init__(
@@ -54,8 +60,8 @@ class EditProfileWindow(bui.Window):
self._highlight,
) = bui.app.classic.get_player_profile_colors(existing_profile)
uiscale = bui.app.ui_v1.uiscale
- self._width = width = 780.0 if uiscale is bui.UIScale.SMALL else 680.0
- self._x_inset = x_inset = 50.0 if uiscale is bui.UIScale.SMALL else 0.0
+ self._width = width = 880.0 if uiscale is bui.UIScale.SMALL else 680.0
+ self._x_inset = x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
self._height = height = (
350.0
if uiscale is bui.UIScale.SMALL
@@ -184,7 +190,7 @@ class EditProfileWindow(bui.Window):
self._clipped_name_text = bui.textwidget(
parent=self._root_widget,
text='',
- position=(540 + x_inset, v - 8),
+ position=(580 + x_inset, v - 8),
flatness=1.0,
shadow=0.0,
scale=0.55,
@@ -390,6 +396,16 @@ class EditProfileWindow(bui.Window):
autoselect=True,
on_activate_call=self.upgrade_profile,
)
+ self._random_name_button = bui.buttonwidget(
+ parent=self._root_widget,
+ label=bui.Lstr(resource='randomText'),
+ size=(30, 20),
+ position=(495 + x_inset, v - 20),
+ button_type='square',
+ color=(0.6, 0.5, 0.65),
+ autoselect=True,
+ on_activate_call=self.assign_random_name,
+ )
self._update_clipped_name()
self._clipped_name_timer = bui.AppTimer(
@@ -498,8 +514,17 @@ class EditProfileWindow(bui.Window):
)
self._update_character()
+ def assign_random_name(self) -> None:
+ """Assigning a random name to the player."""
+ names = bs.get_random_names()
+ name = names[random.randrange(len(names))]
+ bui.textwidget(
+ edit=self._text_field,
+ text=name,
+ )
+
def upgrade_profile(self) -> None:
- """Attempt to ugrade the profile to global."""
+ """Attempt to upgrade the profile to global."""
from bauiv1lib import account
from bauiv1lib.profile import upgrade as pupgrade
@@ -653,6 +678,10 @@ class EditProfileWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.profile.browser import ProfileBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
@@ -660,7 +689,8 @@ class EditProfileWindow(bui.Window):
'in_left',
selected_profile=self._existing_profile,
in_main_menu=self._in_main_menu,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _set_color(self, color: tuple[float, float, float]) -> None:
@@ -759,6 +789,10 @@ class EditProfileWindow(bui.Window):
"""Save has been selected."""
from bauiv1lib.profile.browser import ProfileBrowserWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return False
+
plus = bui.app.plus
assert plus is not None
@@ -808,6 +842,7 @@ class EditProfileWindow(bui.Window):
'in_left',
selected_profile=new_name,
in_main_menu=self._in_main_menu,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
return True
diff --git a/src/assets/ba_data/python/bauiv1lib/promocode.py b/src/assets/ba_data/python/bauiv1lib/promocode.py
index ae8d0549..3cf745b5 100644
--- a/src/assets/ba_data/python/bauiv1lib/promocode.py
+++ b/src/assets/ba_data/python/bauiv1lib/promocode.py
@@ -26,7 +26,7 @@ class PromoCodeWindow(bui.Window):
transition = 'in_right'
width = 450
- height = 230
+ height = 330
self._modal = modal
self._r = 'promoCodeWindow'
@@ -62,17 +62,50 @@ class PromoCodeWindow(bui.Window):
iconscale=1.2,
)
+ v = height - 74
+ bui.textwidget(
+ parent=self._root_widget,
+ text=bui.Lstr(resource='codesExplainText'),
+ maxwidth=width * 0.9,
+ position=(width * 0.5, v),
+ color=(0.7, 0.7, 0.7, 1.0),
+ size=(0, 0),
+ scale=0.8,
+ h_align='center',
+ v_align='center',
+ )
+ v -= 60
+
+ bui.textwidget(
+ parent=self._root_widget,
+ text=bui.Lstr(
+ resource='supportEmailText',
+ subs=[('${EMAIL}', 'support@froemling.net')],
+ ),
+ maxwidth=width * 0.9,
+ position=(width * 0.5, v),
+ color=(0.7, 0.7, 0.7, 1.0),
+ size=(0, 0),
+ scale=0.65,
+ h_align='center',
+ v_align='center',
+ )
+
+ v -= 80
+
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource=self._r + '.codeText'),
- position=(22, height - 113),
+ position=(22, v),
color=(0.8, 0.8, 0.8, 1.0),
size=(90, 30),
h_align='right',
)
+ v -= 8
+
self._text_field = bui.textwidget(
parent=self._root_widget,
- position=(125, height - 121),
+ position=(125, v),
size=(280, 46),
text='',
h_align='left',
@@ -86,10 +119,11 @@ class PromoCodeWindow(bui.Window):
)
bui.widget(edit=btn, down_widget=self._text_field)
+ v -= 79
b_width = 200
self._enter_button = btn2 = bui.buttonwidget(
parent=self._root_widget,
- position=(width * 0.5 - b_width * 0.5, height - 200),
+ position=(width * 0.5 - b_width * 0.5, v),
size=(b_width, 60),
scale=1.0,
label=bui.Lstr(
@@ -108,13 +142,18 @@ class PromoCodeWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AdvancedSettingsWindow(transition='in_left').get_root_widget()
+ AdvancedSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _activate_enter_button(self) -> None:
@@ -124,6 +163,10 @@ class PromoCodeWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -133,7 +176,8 @@ class PromoCodeWindow(bui.Window):
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AdvancedSettingsWindow(transition='in_left').get_root_widget()
+ AdvancedSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
plus.add_v1_account_transaction(
{
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py
index 930ecaa3..61c72cbb 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py
@@ -47,8 +47,8 @@ class AdvancedSettingsWindow(bui.Window):
scale_origin = None
uiscale = bui.app.ui_v1.uiscale
- self._width = 870.0 if uiscale is bui.UIScale.SMALL else 670.0
- x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
+ self._width = 970.0 if uiscale is bui.UIScale.SMALL else 670.0
+ x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
self._height = (
390.0
if uiscale is bui.UIScale.SMALL
@@ -682,11 +682,16 @@ class AdvancedSettingsWindow(bui.Window):
def _on_vr_test_press(self) -> None:
from bauiv1lib.settings.vrtesting import VRTestingWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- VRTestingWindow(transition='in_right').get_root_widget()
+ VRTestingWindow(transition='in_right').get_root_widget(),
+ from_window=self._root_widget,
)
def _on_net_test_press(self) -> None:
@@ -694,6 +699,10 @@ class AdvancedSettingsWindow(bui.Window):
assert plus is not None
from bauiv1lib.settings.nettesting import NetTestingWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# Net-testing requires a signed in v1 account.
if plus.get_v1_account_state() != 'signed_in':
bui.screenmessage(
@@ -706,7 +715,8 @@ class AdvancedSettingsWindow(bui.Window):
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- NetTestingWindow(transition='in_right').get_root_widget()
+ NetTestingWindow(transition='in_right').get_root_widget(),
+ from_window=self._root_widget,
)
def _on_friend_promo_code_press(self) -> None:
@@ -724,17 +734,26 @@ class AdvancedSettingsWindow(bui.Window):
def _on_plugins_button_press(self) -> None:
from bauiv1lib.settings.plugins import PluginWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- PluginWindow(origin_widget=self._plugins_button).get_root_widget()
+ PluginWindow(origin_widget=self._plugins_button).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_promo_code_press(self) -> None:
from bauiv1lib.promocode import PromoCodeWindow
from bauiv1lib.account import show_sign_in_prompt
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -742,23 +761,30 @@ class AdvancedSettingsWindow(bui.Window):
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PromoCodeWindow(
origin_widget=self._promo_code_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_benchmark_press(self) -> None:
from bauiv1lib.debug import DebugWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- DebugWindow(transition='in_right').get_root_widget()
+ DebugWindow(transition='in_right').get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
@@ -807,6 +833,8 @@ class AdvancedSettingsWindow(bui.Window):
sel_name = 'ModdingGuide'
elif sel == self._language_inform_checkbox:
sel_name = 'LangInform'
+ elif sel == self._show_dev_console_button_check_box.widget:
+ sel_name = 'ShowDevConsole'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
elif sel == self._back_button:
@@ -870,6 +898,8 @@ class AdvancedSettingsWindow(bui.Window):
sel = self._modding_guide_button
elif sel_name == 'LangInform':
sel = self._language_inform_checkbox
+ elif sel_name == 'ShowDevConsole':
+ sel = self._show_dev_console_button_check_box.widget
else:
sel = None
if sel is not None:
@@ -904,11 +934,16 @@ class AdvancedSettingsWindow(bui.Window):
def _do_back(self) -> None:
from bauiv1lib.settings.allsettings import AllSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AllSettingsWindow(transition='in_left').get_root_widget()
+ AllSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py
index d6d18b5f..98aedda9 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py
@@ -40,8 +40,8 @@ class AllSettingsWindow(bui.Window):
scale_origin = None
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
- width = 900 if uiscale is bui.UIScale.SMALL else 580
- x_inset = 75 if uiscale is bui.UIScale.SMALL else 0
+ width = 1000 if uiscale is bui.UIScale.SMALL else 580
+ x_inset = 125 if uiscale is bui.UIScale.SMALL else 0
height = 435
self._r = 'settingsWindow'
top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
@@ -235,65 +235,90 @@ class AllSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _do_controllers(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.controls import ControlsSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ControlsSettingsWindow(
origin_widget=self._controllers_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _do_graphics(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.graphics import GraphicsSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
GraphicsSettingsWindow(
origin_widget=self._graphics_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _do_audio(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.audio import AudioSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AudioSettingsWindow(
origin_widget=self._audio_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _do_advanced(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AdvancedSettingsWindow(
origin_widget=self._advanced_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/audio.py b/src/assets/ba_data/python/bauiv1lib/settings/audio.py
index bfe71e40..fc39b719 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/audio.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/audio.py
@@ -121,7 +121,8 @@ class AudioSettingsWindow(bui.Window):
displayname=bui.Lstr(resource=self._r + '.soundVolumeText'),
minval=0.0,
maxval=1.0,
- increment=0.1,
+ increment=0.05,
+ as_percent=True,
)
if bui.app.ui_v1.use_toolbars:
bui.widget(
@@ -137,9 +138,10 @@ class AudioSettingsWindow(bui.Window):
displayname=bui.Lstr(resource=self._r + '.musicVolumeText'),
minval=0.0,
maxval=1.0,
- increment=0.1,
+ increment=0.05,
callback=music.music_volume_changed,
changesound=False,
+ as_percent=True,
)
v -= 0.5 * spacing
@@ -235,6 +237,10 @@ class AudioSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.soundtrack import browser as stb
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# We require disk access for soundtracks;
# if we don't have it, request it.
if not bui.have_permission(bui.Permission.STORAGE):
@@ -254,13 +260,18 @@ class AudioSettingsWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
stb.SoundtrackBrowserWindow(
origin_widget=self._soundtrack_button
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _back(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings import allsettings
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -269,7 +280,8 @@ class AudioSettingsWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
allsettings.AllSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/controls.py b/src/assets/ba_data/python/bauiv1lib/settings/controls.py
index 0b9abd39..108657f5 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/controls.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/controls.py
@@ -98,9 +98,11 @@ class ControlsSettingsWindow(bui.Window):
# made-for-iOS/Mac systems
# (we can run into problems where devices register as one of each
# type otherwise)..
+ # UPDATE: We always use the apple system these days (which should
+ # support older controllers). So no need for a switch.
show_mac_controller_subsystem = False
- if platform == 'mac' and bui.is_xcode_build():
- show_mac_controller_subsystem = True
+ # if platform == 'mac' and bui.is_xcode_build():
+ # show_mac_controller_subsystem = True
if show_mac_controller_subsystem:
height += spacing * 1.5
@@ -311,6 +313,7 @@ class ControlsSettingsWindow(bui.Window):
maxwidth=width * 0.8,
)
v -= spacing
+
if show_mac_controller_subsystem:
PopupMenu(
parent=self._root_widget,
@@ -364,59 +367,84 @@ class ControlsSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.keyboard import ConfigKeyboardWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ConfigKeyboardWindow(
bs.getinputdevice('Keyboard', '#1')
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _config_keyboard2(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.keyboard import ConfigKeyboardWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ConfigKeyboardWindow(
bs.getinputdevice('Keyboard', '#2')
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _do_mobile_devices(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.remoteapp import RemoteAppSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- RemoteAppSettingsWindow().get_root_widget()
+ RemoteAppSettingsWindow().get_root_widget(),
+ from_window=self._root_widget,
)
def _do_gamepads(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.gamepadselect import GamepadSelectWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- GamepadSelectWindow().get_root_widget()
+ GamepadSelectWindow().get_root_widget(),
+ from_window=self._root_widget,
)
def _do_touchscreen(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.touchscreen import TouchscreenSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- TouchscreenSettingsWindow().get_root_widget()
+ TouchscreenSettingsWindow().get_root_widget(),
+ from_window=self._root_widget,
)
def _save_state(self) -> None:
@@ -463,11 +491,16 @@ class ControlsSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.allsettings import AllSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AllSettingsWindow(transition='in_left').get_root_widget()
+ AllSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py
index c9780d15..a63847d4 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py
@@ -545,20 +545,24 @@ class GamepadSettingsWindow(bui.Window):
if 'analogStickLR' + self._ext in self._settings
else 5
if self._is_secondary
- else 1
+ else None
)
sval2 = (
self._settings['analogStickUD' + self._ext]
if 'analogStickUD' + self._ext in self._settings
else 6
if self._is_secondary
- else 2
- )
- return (
- self._input.get_axis_name(sval1)
- + ' / '
- + self._input.get_axis_name(sval2)
+ else None
)
+ assert isinstance(sval1, (int, type(None)))
+ assert isinstance(sval2, (int, type(None)))
+ if sval1 is not None and sval2 is not None:
+ return (
+ self._input.get_axis_name(sval1)
+ + ' / '
+ + self._input.get_axis_name(sval2)
+ )
+ return bui.Lstr(resource=self._r + '.unsetText')
# If they're looking for triggers.
if control in ['triggerRun1' + self._ext, 'triggerRun2' + self._ext]:
@@ -573,7 +577,7 @@ class GamepadSettingsWindow(bui.Window):
return str(1.0)
# For dpad buttons: show individual buttons if any are set.
- # Otherwise show whichever dpad is set (defaulting to 1).
+ # Otherwise show whichever dpad is set.
dpad_buttons = [
'buttonLeft' + self._ext,
'buttonRight' + self._ext,
@@ -588,24 +592,28 @@ class GamepadSettingsWindow(bui.Window):
return bui.Lstr(resource=self._r + '.unsetText')
# No dpad buttons - show the dpad number for all 4.
- return bui.Lstr(
- value='${A} ${B}',
- subs=[
- ('${A}', bui.Lstr(resource=self._r + '.dpadText')),
- (
- '${B}',
- str(
- self._settings['dpad' + self._ext]
- if 'dpad' + self._ext in self._settings
- else 2
- if self._is_secondary
- else 1
- ),
- ),
- ],
+ dpadnum = (
+ self._settings['dpad' + self._ext]
+ if 'dpad' + self._ext in self._settings
+ else 2
+ if self._is_secondary
+ else None
)
+ assert isinstance(dpadnum, (int, type(None)))
+ if dpadnum is not None:
+ return bui.Lstr(
+ value='${A} ${B}',
+ subs=[
+ ('${A}', bui.Lstr(resource=self._r + '.dpadText')),
+ (
+ '${B}',
+ str(dpadnum),
+ ),
+ ],
+ )
+ return bui.Lstr(resource=self._r + '.unsetText')
- # other buttons..
+ # Other buttons.
if control in self._settings:
return self._input.get_button_name(self._settings[control])
return bui.Lstr(resource=self._r + '.unsetText')
@@ -616,9 +624,7 @@ class GamepadSettingsWindow(bui.Window):
event: dict[str, Any],
dialog: AwaitGamepadInputWindow,
) -> None:
- # pylint: disable=too-many-nested-blocks
# pylint: disable=too-many-branches
- # pylint: disable=too-many-statements
assert self._settings is not None
ext = self._ext
@@ -648,10 +654,6 @@ class GamepadSettingsWindow(bui.Window):
if btn in self._settings:
del self._settings[btn]
if event['hat'] == (2 if self._is_secondary else 1):
- # Exclude value in default case.
- if 'dpad' + ext in self._settings:
- del self._settings['dpad' + ext]
- else:
self._settings['dpad' + ext] = event['hat']
# Update the 4 dpad button txt widgets.
@@ -680,10 +682,6 @@ class GamepadSettingsWindow(bui.Window):
if abs(event['value']) > 0.5:
axis = event['axis']
if axis == (5 if self._is_secondary else 1):
- # Exclude value in default case.
- if 'analogStickLR' + ext in self._settings:
- del self._settings['analogStickLR' + ext]
- else:
self._settings['analogStickLR' + ext] = axis
bui.textwidget(
edit=self._textwidgets['analogStickLR' + ext],
@@ -713,10 +711,6 @@ class GamepadSettingsWindow(bui.Window):
lr_axis = 5 if self._is_secondary else 1
if axis != lr_axis:
if axis == (6 if self._is_secondary else 2):
- # Exclude value in default case.
- if 'analogStickUD' + ext in self._settings:
- del self._settings['analogStickUD' + ext]
- else:
self._settings['analogStickUD' + ext] = axis
bui.textwidget(
edit=self._textwidgets['analogStickLR' + ext],
@@ -795,25 +789,34 @@ class GamepadSettingsWindow(bui.Window):
),
)
- bui.apptimer(0, doit)
+ bui.pushcall(doit)
return btn
def _cancel(self) -> None:
from bauiv1lib.settings.controls import ControlsSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if self._is_main_menu:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- ControlsSettingsWindow(transition='in_left').get_root_widget()
+ ControlsSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _save(self) -> None:
classic = bui.app.classic
assert classic is not None
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
@@ -858,7 +861,8 @@ class GamepadSettingsWindow(bui.Window):
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- ControlsSettingsWindow(transition='in_left').get_root_widget()
+ ControlsSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepadadvanced.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepadadvanced.py
index ae50be7b..d44300f0 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/gamepadadvanced.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepadadvanced.py
@@ -452,7 +452,7 @@ class GamepadAdvancedSettingsWindow(bui.Window):
),
)
- bui.apptimer(0, doit)
+ bui.pushcall(doit)
return btn, btn2
def _inc(
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py
index 9534ec8d..d0dba0ac 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py
@@ -29,16 +29,17 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
logging.exception('Error transitioning out main_menu_window.')
bui.getsound('activateBeep').play()
bui.getsound('swish').play()
- inputdevice = event['input_device']
- assert isinstance(inputdevice, bs.InputDevice)
- if inputdevice.allows_configuring:
+ device = event['input_device']
+ assert isinstance(device, bs.InputDevice)
+ if device.allows_configuring:
bui.app.ui_v1.set_main_menu_window(
- gamepad.GamepadSettingsWindow(inputdevice).get_root_widget()
+ gamepad.GamepadSettingsWindow(device).get_root_widget(),
+ from_window=None,
)
else:
width = 700
height = 200
- button_width = 100
+ button_width = 80
uiscale = bui.app.ui_v1.uiscale
dlg = bui.containerwidget(
scale=(
@@ -51,9 +52,14 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
size=(width, height),
transition='in_right',
)
- bui.app.ui_v1.set_main_menu_window(dlg)
- device_name = inputdevice.name
- if device_name == 'iDevice':
+ bui.app.ui_v1.set_main_menu_window(dlg, from_window=None)
+
+ if device.allows_configuring_in_system_settings:
+ msg = bui.Lstr(
+ resource='configureDeviceInSystemSettingsText',
+ subs=[('${DEVICE}', device.name)],
+ )
+ elif device.is_controller_app:
msg = bui.Lstr(
resource='bsRemoteConfigureInAppText',
subs=[('${REMOTE_APP_NAME}', bui.get_remote_app_name())],
@@ -61,7 +67,7 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
else:
msg = bui.Lstr(
resource='cantConfigureDeviceText',
- subs=[('${DEVICE}', device_name)],
+ subs=[('${DEVICE}', device.name)],
)
bui.textwidget(
parent=dlg,
@@ -76,12 +82,17 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
def _ok() -> None:
from bauiv1lib.settings import controls
+ # no-op if our underlying widget is dead or on its way out.
+ if not dlg or dlg.transitioning_out:
+ return
+
bui.containerwidget(edit=dlg, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=dlg,
)
bui.buttonwidget(
@@ -186,11 +197,16 @@ class GamepadSelectWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import controls
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bs.release_gamepad_input()
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py
index ec6879b4..f441826c 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py
@@ -52,7 +52,7 @@ class GraphicsSettingsWindow(bui.Window):
self._show_fullscreen = False
fullscreen_spacing_top = spacing * 0.2
fullscreen_spacing = spacing * 1.2
- if bui.can_toggle_fullscreen():
+ if bui.fullscreen_control_available():
self._show_fullscreen = True
height += fullscreen_spacing + fullscreen_spacing_top
@@ -122,21 +122,29 @@ class GraphicsSettingsWindow(bui.Window):
self._fullscreen_checkbox: bui.Widget | None = None
if self._show_fullscreen:
v -= fullscreen_spacing_top
- self._fullscreen_checkbox = ConfigCheckBox(
+ # Fullscreen control does not necessarily talk to the
+ # app config so we have to wrangle it manually instead of
+ # using a config-checkbox.
+ label = bui.Lstr(resource=f'{self._r}.fullScreenText')
+
+ # Show keyboard shortcut alongside the control if they
+ # provide one.
+ shortcut = bui.fullscreen_control_key_shortcut()
+ if shortcut is not None:
+ label = bui.Lstr(
+ value='$(NAME) [$(SHORTCUT)]',
+ subs=[('$(NAME)', label), ('$(SHORTCUT)', shortcut)],
+ )
+ self._fullscreen_checkbox = bui.checkboxwidget(
parent=self._root_widget,
position=(100, v),
- maxwidth=200,
+ value=bui.fullscreen_control_get(),
+ on_value_change_call=bui.fullscreen_control_set,
+ maxwidth=250,
size=(300, 30),
- configkey='Fullscreen',
- displayname=bui.Lstr(
- resource=self._r
- + (
- '.fullScreenCmdText'
- if app.classic.platform == 'mac'
- else '.fullScreenCtrlText'
- )
- ),
- ).widget
+ text=label,
+ )
+
if not self._have_selected_child:
bui.containerwidget(
edit=self._root_widget,
@@ -259,9 +267,7 @@ class GraphicsSettingsWindow(bui.Window):
bui.Lstr(resource='nativeText'),
]
for res in [1440, 1080, 960, 720, 480]:
- # Nav bar is 72px so lets allow for that in what
- # choices we show.
- if native_res[1] >= res - 72:
+ if native_res[1] >= res:
res_str = f'{res}p'
choices.append(res_str)
choices_display.append(bui.Lstr(value=res_str))
@@ -430,6 +436,10 @@ class GraphicsSettingsWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import allsettings
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
# Applying max-fps takes a few moments. Apply if it hasn't been
# yet.
self._apply_max_fps()
@@ -441,7 +451,8 @@ class GraphicsSettingsWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
allsettings.AllSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _set_quality(self, quality: str) -> None:
@@ -528,8 +539,10 @@ class GraphicsSettingsWindow(bui.Window):
and bui.apptime() - self._last_max_fps_set_time > 1.0
):
self._apply_max_fps()
+
if self._show_fullscreen:
+ # Keep the fullscreen checkbox up to date with the current value.
bui.checkboxwidget(
edit=self._fullscreen_checkbox,
- value=bui.app.config.resolve('Fullscreen'),
+ value=bui.fullscreen_control_get(),
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py
index 6f37aabb..1e564378 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py
@@ -213,6 +213,12 @@ class ConfigKeyboardWindow(bui.Window):
scale=1.0,
)
+ def _pretty_button_name(self, button_name: str) -> bui.Lstr:
+ button_id = self._settings[button_name]
+ if button_id == -1:
+ return bs.Lstr(resource='configGamepadWindow.unsetText')
+ return self._input.get_button_name(button_id)
+
def _capture_button(
self,
pos: tuple[float, float],
@@ -250,7 +256,7 @@ class ConfigKeyboardWindow(bui.Window):
v_align='top',
scale=uiscale,
maxwidth=maxwidth,
- text=self._input.get_button_name(self._settings[button]),
+ text=self._pretty_button_name(button),
)
bui.buttonwidget(
edit=btn,
@@ -265,15 +271,24 @@ class ConfigKeyboardWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.settings.controls import ControlsSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- ControlsSettingsWindow(transition='in_left').get_root_widget()
+ ControlsSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _save(self) -> None:
from bauiv1lib.settings.controls import ControlsSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
bui.containerwidget(edit=self._root_widget, transition='out_right')
bui.getsound('gunCocking').play()
@@ -308,7 +323,8 @@ class ConfigKeyboardWindow(bui.Window):
)
bui.app.config.apply_and_commit()
bui.app.ui_v1.set_main_menu_window(
- ControlsSettingsWindow(transition='in_left').get_root_widget()
+ ControlsSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py b/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py
index dd295108..e4e6e996 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py
@@ -135,8 +135,14 @@ class NetTestingWindow(bui.Window):
def _show_val_testing(self) -> None:
assert bui.app.classic is not None
+
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.app.ui_v1.set_main_menu_window(
- NetValTestingWindow().get_root_widget()
+ NetValTestingWindow().get_root_widget(),
+ from_window=self._root_widget,
)
bui.containerwidget(edit=self._root_widget, transition='out_left')
@@ -144,9 +150,14 @@ class NetTestingWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AdvancedSettingsWindow(transition='in_left').get_root_widget()
+ AdvancedSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
bui.containerwidget(edit=self._root_widget, transition='out_right')
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/plugins.py b/src/assets/ba_data/python/bauiv1lib/settings/plugins.py
index 5f22847b..b5f93320 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/plugins.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/plugins.py
@@ -110,11 +110,11 @@ class PluginWindow(bui.Window):
self._title_text = bui.textwidget(
parent=self._root_widget,
- position=(self._width * 0.5, self._height - 38),
+ position=(self._width * 0.5, self._height - 41),
size=(0, 0),
text=bui.Lstr(resource='pluginsText'),
color=app.ui_v1.title_color,
- maxwidth=200,
+ maxwidth=170,
h_align='center',
v_align='center',
)
@@ -129,6 +129,15 @@ class PluginWindow(bui.Window):
settings_button_x = 670 if uiscale is bui.UIScale.SMALL else 570
+ self._num_plugins_text = bui.textwidget(
+ parent=self._root_widget,
+ position=(settings_button_x - 130, self._height - 41),
+ size=(0, 0),
+ text='',
+ h_align='center',
+ v_align='center',
+ )
+
self._category_button = bui.buttonwidget(
parent=self._root_widget,
scale=0.7,
@@ -174,6 +183,17 @@ class PluginWindow(bui.Window):
)
bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
+ self._no_plugins_installed_text = bui.textwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5, self._height * 0.5),
+ size=(0, 0),
+ text='',
+ color=(0.6, 0.6, 0.6),
+ scale=0.8,
+ h_align='center',
+ v_align='center',
+ )
+
if bui.app.meta.scanresults is None:
bui.screenmessage(
'Still scanning plugins; please try again.', color=(1, 0, 0)
@@ -212,11 +232,16 @@ class PluginWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.pluginsettings import PluginSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- PluginSettingsWindow(transition='in_right').get_root_widget()
+ PluginSettingsWindow(transition='in_right').get_root_widget(),
+ from_window=self._root_widget,
)
def _show_category_options(self) -> None:
@@ -263,6 +288,7 @@ class PluginWindow(bui.Window):
def _show_plugins(self) -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
+ # pylint: disable=too-many-statements
plugspecs = bui.app.plugins.plugin_specs
plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {})
assert isinstance(plugstates, dict)
@@ -274,6 +300,11 @@ class PluginWindow(bui.Window):
plugspecs_sorted = sorted(plugspecs.items())
+ bui.textwidget(
+ edit=self._no_plugins_installed_text,
+ text='',
+ )
+
for _classpath, plugspec in plugspecs_sorted:
# counting number of enabled and disabled plugins
# plugstate = plugstates.setdefault(plugspec[0], {})
@@ -372,6 +403,17 @@ class PluginWindow(bui.Window):
bui.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40)
num_shown += 1
+ bui.textwidget(
+ edit=self._num_plugins_text,
+ text=str(num_shown),
+ )
+
+ if num_shown == 0:
+ bui.textwidget(
+ edit=self._no_plugins_installed_text,
+ text=bui.Lstr(resource='noPluginsInstalledText'),
+ )
+
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
@@ -412,11 +454,16 @@ class PluginWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- AdvancedSettingsWindow(transition='in_left').get_root_widget()
+ AdvancedSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py b/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py
index 03e9e9e8..1474f5cb 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py
@@ -161,10 +161,15 @@ class PluginSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.plugins import PluginWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- PluginWindow(transition='in_left').get_root_widget()
+ PluginWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py b/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py
index 03b1c611..3542f992 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py
@@ -138,10 +138,15 @@ class RemoteAppSettingsWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import controls
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/testing.py b/src/assets/ba_data/python/bauiv1lib/settings/testing.py
index c4bcd58a..30f11e28 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/testing.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/testing.py
@@ -217,6 +217,10 @@ class TestingWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
backwin = (
self._back_call()
@@ -224,4 +228,6 @@ class TestingWindow(bui.Window):
else AdvancedSettingsWindow(transition='in_left')
)
assert bui.app.classic is not None
- bui.app.ui_v1.set_main_menu_window(backwin.get_root_widget())
+ bui.app.ui_v1.set_main_menu_window(
+ backwin.get_root_widget(), from_window=self._root_widget
+ )
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py b/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py
index 61041bbd..d77a16a2 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py
@@ -276,11 +276,16 @@ class TouchscreenSettingsWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import controls
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
bs.set_touchscreen_editing(False)
diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py
index 021942e6..a3b561b4 100644
--- a/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py
@@ -394,13 +394,18 @@ class SoundtrackBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings import audio
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- audio.AudioSettingsWindow(transition='in_left').get_root_widget()
+ audio.AudioSettingsWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _edit_soundtrack_with_sound(self) -> None:
@@ -421,6 +426,10 @@ class SoundtrackBrowserWindow(bui.Window):
from bauiv1lib.purchase import PurchaseWindow
from bauiv1lib.soundtrack.edit import SoundtrackEditWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if (
bui.app.classic is not None
and not bui.app.classic.accounts.have_pro_options()
@@ -443,7 +452,8 @@ class SoundtrackBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
SoundtrackEditWindow(
existing_soundtrack=self._selected_soundtrack
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _get_soundtrack_display_name(self, soundtrack: str) -> bui.Lstr:
diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py
index 0e1088b4..8c3887eb 100644
--- a/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py
+++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py
@@ -351,7 +351,8 @@ class SoundtrackEditWindow(bui.Window):
soundtrack[musictype] = entry
bui.app.ui_v1.set_main_menu_window(
- cls(state, transition='in_left').get_root_widget()
+ cls(state, transition='in_left').get_root_widget(),
+ from_window=False, # Disable check here.
)
def _get_entry(
@@ -359,6 +360,11 @@ class SoundtrackEditWindow(bui.Window):
) -> None:
assert bui.app.classic is not None
music = bui.app.classic.music
+
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
if selection_target_name != '':
selection_target_name = "'" + selection_target_name + "'"
state = {
@@ -375,7 +381,8 @@ class SoundtrackEditWindow(bui.Window):
entry,
selection_target_name,
)
- .get_root_widget()
+ .get_root_widget(),
+ from_window=self._root_widget,
)
def _test(self, song_type: bs.MusicType) -> None:
@@ -422,6 +429,10 @@ class SoundtrackEditWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.soundtrack import browser as stb
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
music = bui.app.classic.music
@@ -429,12 +440,17 @@ class SoundtrackEditWindow(bui.Window):
music.set_music_play_mode(bui.app.classic.MusicPlayMode.REGULAR)
bui.containerwidget(edit=self._root_widget, transition='out_right')
bui.app.ui_v1.set_main_menu_window(
- stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget()
+ stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _do_it(self) -> None:
from bauiv1lib.soundtrack import browser as stb
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
assert bui.app.classic is not None
music = bui.app.classic.music
cfg = bui.app.config
@@ -483,7 +499,8 @@ class SoundtrackEditWindow(bui.Window):
)
bui.app.ui_v1.set_main_menu_window(
- stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget()
+ stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
def _do_it_with_sound(self) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py
index 2adaa721..583855a8 100644
--- a/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py
+++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py
@@ -166,6 +166,10 @@ class SoundtrackEntryTypeSelectWindow(bui.Window):
MacMusicAppPlaylistSelectWindow,
)
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_left')
current_playlist_entry: str | None
@@ -181,7 +185,8 @@ class SoundtrackEntryTypeSelectWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
MacMusicAppPlaylistSelectWindow(
self._callback, current_playlist_entry, self._current_entry
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_music_file_press(self) -> None:
@@ -189,6 +194,10 @@ class SoundtrackEntryTypeSelectWindow(bui.Window):
from baclassic.osmusic import OSMusicPlayer
from bauiv1lib.fileselector import FileSelectorWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_left')
base_path = android_get_external_files_dir()
assert bui.app.classic is not None
@@ -201,13 +210,18 @@ class SoundtrackEntryTypeSelectWindow(bui.Window):
OSMusicPlayer.get_valid_music_file_extensions()
),
allow_folders=False,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _on_music_folder_press(self) -> None:
from bauiv1lib.fileselector import FileSelectorWindow
from babase import android_get_external_files_dir
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
bui.containerwidget(edit=self._root_widget, transition='out_left')
base_path = android_get_external_files_dir()
assert bui.app.classic is not None
@@ -218,7 +232,8 @@ class SoundtrackEntryTypeSelectWindow(bui.Window):
show_base_path=False,
valid_file_extensions=[],
allow_folders=True,
- ).get_root_widget()
+ ).get_root_widget(),
+ from_window=self._root_widget,
)
def _music_file_selector_cb(self, result: str | None) -> None:
diff --git a/src/assets/ba_data/python/bauiv1lib/specialoffer.py b/src/assets/ba_data/python/bauiv1lib/specialoffer.py
index 7fe7729c..6e4a463b 100644
--- a/src/assets/ba_data/python/bauiv1lib/specialoffer.py
+++ b/src/assets/ba_data/python/bauiv1lib/specialoffer.py
@@ -518,10 +518,13 @@ def show_offer() -> bool:
assert plus is not None
app = bui.app
- assert app.classic is not None
+ if app.classic is None:
+ raise RuntimeError(
+ 'Classic feature-set is required to show offers.'
+ )
- # Space things out a bit so we don't hit the poor user with an ad and
- # then an in-game offer.
+ # Space things out a bit so we don't hit the poor user with an
+ # ad and then an in-game offer.
has_been_long_enough_since_ad = True
if app.classic.ads.last_ad_completion_time is not None and (
bui.apptime() - app.classic.ads.last_ad_completion_time < 30.0
@@ -532,8 +535,9 @@ def show_offer() -> bool:
app.classic.special_offer is not None
and has_been_long_enough_since_ad
):
- # Special case: for pro offers, store this in our prefs so we
- # can re-show it if the user kills us (set phasers to 'NAG'!!!).
+ # Special case: for pro offers, store this in our prefs so
+ # we can re-show it if the user kills us (set phasers to
+ # 'NAG'!!!).
if app.classic.special_offer.get('item') == 'pro_fullprice':
cfg = app.config
cfg['pendingSpecialOffer'] = {
@@ -543,9 +547,15 @@ def show_offer() -> bool:
cfg.commit()
if app.classic.special_offer['item'] == 'rating':
- feedback.ask_for_rating()
+ # Go with a native thing if we've got one.
+ if bui.native_review_request_supported():
+ bui.native_review_request()
+ else:
+ if app.ui_v1.available:
+ feedback.ask_for_rating()
else:
- SpecialOfferWindow(app.classic.special_offer)
+ if app.ui_v1.available:
+ SpecialOfferWindow(app.classic.special_offer)
app.classic.special_offer = None
return True
diff --git a/src/assets/ba_data/python/bauiv1lib/store/browser.py b/src/assets/ba_data/python/bauiv1lib/store/browser.py
index 269a8164..555e5e1c 100644
--- a/src/assets/ba_data/python/bauiv1lib/store/browser.py
+++ b/src/assets/ba_data/python/bauiv1lib/store/browser.py
@@ -76,8 +76,8 @@ class StoreBrowserWindow(bui.Window):
self._on_close_call = on_close_call
self._show_tab = show_tab
self._modal = modal
- self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040
- self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
+ self._width = 1440 if uiscale is bui.UIScale.SMALL else 1040
+ self._x_inset = x_inset = 200 if uiscale is bui.UIScale.SMALL else 0
self._height = (
578
if uiscale is bui.UIScale.SMALL
@@ -352,7 +352,7 @@ class StoreBrowserWindow(bui.Window):
plus = bui.app.plus
assert plus is not None
- if plus.get_v1_account_state() != 'signed_in':
+ if plus.accounts.primary is None:
account.show_sign_in_prompt()
else:
plus.restore_purchases()
@@ -1329,6 +1329,10 @@ class StoreBrowserWindow(bui.Window):
from bauiv1lib.account import show_sign_in_prompt
from bauiv1lib.getcurrency import GetCurrencyWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
plus = bui.app.plus
assert plus is not None
@@ -1343,13 +1347,19 @@ class StoreBrowserWindow(bui.Window):
).get_root_widget()
if not self._modal:
assert bui.app.classic is not None
- bui.app.ui_v1.set_main_menu_window(window)
+ bui.app.ui_v1.set_main_menu_window(
+ window, from_window=self._root_widget
+ )
def _back(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.coop.browser import CoopBrowserWindow
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@@ -1358,11 +1368,13 @@ class StoreBrowserWindow(bui.Window):
assert bui.app.classic is not None
if self._back_location == 'CoopBrowserWindow':
bui.app.ui_v1.set_main_menu_window(
- CoopBrowserWindow(transition='in_left').get_root_widget()
+ CoopBrowserWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
else:
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
if self._on_close_call is not None:
self._on_close_call()
diff --git a/src/assets/ba_data/python/bauiv1lib/tournamententry.py b/src/assets/ba_data/python/bauiv1lib/tournamententry.py
index fcba3e99..d00c37dd 100644
--- a/src/assets/ba_data/python/bauiv1lib/tournamententry.py
+++ b/src/assets/ba_data/python/bauiv1lib/tournamententry.py
@@ -34,6 +34,7 @@ class TournamentEntryWindow(PopupWindow):
# pylint: disable=too-many-statements
assert bui.app.classic is not None
+ assert bui.app.plus
bui.set_analytics_screen('Tournament Entry Window')
self._tournament_id = tournament_id
@@ -100,7 +101,7 @@ class TournamentEntryWindow(PopupWindow):
self._launched = False
# Show the ad button only if we support ads *and* it has a level 1 fee.
- self._do_ad_btn = bui.has_video_ads() and self._allow_ads
+ self._do_ad_btn = bui.app.plus.has_video_ads() and self._allow_ads
x_offs = 0 if self._do_ad_btn else 85
@@ -477,7 +478,7 @@ class TournamentEntryWindow(PopupWindow):
)
if self._do_ad_btn:
- enabled = bui.have_incentivized_ad()
+ enabled = plus.have_incentivized_ad()
have_ad_tries_remaining = (
self._tournament_info['adTriesRemaining'] is not None
and self._tournament_info['adTriesRemaining'] > 0
diff --git a/src/assets/ba_data/python/bauiv1lib/watch.py b/src/assets/ba_data/python/bauiv1lib/watch.py
index 1bdd4fb3..b9636123 100644
--- a/src/assets/ba_data/python/bauiv1lib/watch.py
+++ b/src/assets/ba_data/python/bauiv1lib/watch.py
@@ -55,8 +55,8 @@ class WatchWindow(bui.Window):
self._my_replay_rename_text: bui.Widget | None = None
self._r = 'watchWindow'
uiscale = bui.app.ui_v1.uiscale
- self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040
- x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
+ self._width = 1440 if uiscale is bui.UIScale.SMALL else 1040
+ x_inset = 200 if uiscale is bui.UIScale.SMALL else 0
self._height = (
578
if uiscale is bui.UIScale.SMALL
@@ -598,6 +598,7 @@ class WatchWindow(bui.Window):
edit=txt,
up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button,
)
+ self._my_replay_selected = name
def _save_state(self) -> None:
try:
@@ -663,11 +664,16 @@ class WatchWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
+ # no-op if our underlying widget is dead or on its way out.
+ if not self._root_widget or self._root_widget.transitioning_out:
+ return
+
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
- MainMenuWindow(transition='in_left').get_root_widget()
+ MainMenuWindow(transition='in_left').get_root_widget(),
+ from_window=self._root_widget,
)
diff --git a/src/assets/server_package/ballisticakit_server.py b/src/assets/server_package/ballisticakit_server.py
index 840079dd..2ccd1152 100755
--- a/src/assets/server_package/ballisticakit_server.py
+++ b/src/assets/server_package/ballisticakit_server.py
@@ -15,8 +15,8 @@ from threading import Lock, Thread, current_thread
from typing import TYPE_CHECKING
# We make use of the bacommon and efro packages as well as site-packages
-# included with our bundled Ballistica dist, so we need to add those paths
-# before we import them.
+# included with our bundled Ballistica dist, so we need to add those
+# paths before we import them.
sys.path += [
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')),
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')),
@@ -34,31 +34,55 @@ if TYPE_CHECKING:
VERSION_STR = '1.3.1'
# Version history:
+#
# 1.3.1
-# Windows binary is now named BallisticaKitHeadless.exe
+#
+# - Windows binary is now named 'BallisticaKitHeadless.exe'.
+#
# 1.3:
-# Added show_tutorial config option
-# Added team_names config option
-# Added team_colors config option
-# Added playlist_inline config option
+#
+# - Added show_tutorial config option.
+#
+# - Added team_names config option.
+#
+# - Added team_colors config option.
+#
+# - Added playlist_inline config option.
+#
# 1.2:
-# Added optional --help arg
-# Added --config arg for specifying config file and --root for ba_root path
-# Added noninteractive mode and --interactive/--noninteractive args to
-# explicitly enable/disable it (it is autodetected by default)
-# Added explicit control for auto-restart: --no-auto-restart
-# Config file is now reloaded each time server binary is restarted; no more
-# need to bring down server wrapper to pick up changes
-# Now automatically restarts server binary when config file is modified
-# (use --no-config-auto-restart to disable that behavior)
+#
+# - Added optional --help arg.
+#
+# - Added --config arg for specifying config file and --root for
+# ba_root path.
+#
+# - Added noninteractive mode and --interactive/--noninteractive args
+# to explicitly enable/disable it (it is autodetected by default).
+#
+# - Added explicit control for auto-restart: --no-auto-restart.
+#
+# - Config file is now reloaded each time server binary is restarted;
+# no more need to bring down server wrapper to pick up changes.
+#
+# - Now automatically restarts server binary when config file is
+# modified (use --no-config-auto-restart to disable that behavior).
+#
# 1.1.1:
-# Switched config reading to use efro.dataclasses.dataclass_from_dict()
+#
+# - Switched config reading to use
+# efro.dataclasses.dataclass_from_dict().
+#
# 1.1.0:
-# Added shutdown command
-# Changed restart to default to immediate=True
-# Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes
+#
+# - Added shutdown command.
+#
+# - Changed restart to default to immediate=True.
+#
+# - Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes.
+#
# 1.0.0:
-# Initial release
+#
+# - Initial release.
class ServerManagerApp:
@@ -101,8 +125,9 @@ class ServerManagerApp:
# This may override the above defaults.
self._parse_command_line_args()
- # Do an initial config-load. If the config is invalid at this point
- # we can cleanly die (we're more lenient later on reloads).
+ # Do an initial config-load. If the config is invalid at this
+ # point we can cleanly die (we're more lenient later on
+ # reloads).
self.load_config(strict=True, print_confirmation=False)
@property
@@ -131,9 +156,9 @@ class ServerManagerApp:
)
# Python will handle SIGINT for us (as KeyboardInterrupt) but we
- # need to register a SIGTERM handler so we have a chance to clean
- # up our subprocess when someone tells us to die. (and avoid
- # zombie processes)
+ # need to register a SIGTERM handler so we have a chance to
+ # clean up our subprocess when someone tells us to die. (and
+ # avoid zombie processes)
signal.signal(signal.SIGTERM, self._handle_term_signal)
# During a run, we make the assumption that cwd is the dir
@@ -155,7 +180,8 @@ class ServerManagerApp:
f'{Clr.CYN}Waiting for subprocess exit...{Clr.RST}', flush=True
)
- # Mark ourselves as shutting down and wait for the process to wrap up.
+ # Mark ourselves as shutting down and wait for the process to
+ # wrap up.
self._done = True
self._subprocess_thread.join()
@@ -181,9 +207,10 @@ class ServerManagerApp:
# Gracefully bow out if we kill ourself via keyboard.
pass
except SystemExit:
- # We get this from the builtin quit(), our signal handler, etc.
- # Need to catch this so we can clean up, otherwise we'll be
- # left in limbo with our process thread still running.
+ # We get this from the builtin quit(), our signal handler,
+ # etc. Need to catch this so we can clean up, otherwise
+ # we'll be left in limbo with our process thread still
+ # running.
pass
self._postrun()
@@ -207,14 +234,17 @@ class ServerManagerApp:
self._enable_tab_completion(context)
# Now just sit in an interpreter.
- # TODO: make it possible to use IPython if the user has it available.
+ #
+ # TODO: make it possible to use IPython if the user has it
+ # available.
try:
self._interpreter_start_time = time.time()
code.interact(local=context, banner='', exitmsg='')
except SystemExit:
- # We get this from the builtin quit(), our signal handler, etc.
- # Need to catch this so we can clean up, otherwise we'll be
- # left in limbo with our process thread still running.
+ # We get this from the builtin quit(), our signal handler,
+ # etc. Need to catch this so we can clean up, otherwise
+ # we'll be left in limbo with our process thread still
+ # running.
pass
except BaseException as exc:
print(
@@ -238,19 +268,21 @@ class ServerManagerApp:
self._block_for_command_completion()
def _block_for_command_completion(self) -> None:
- # Ideally we'd block here until the command was run so our prompt would
- # print after it's results. We currently don't get any response from
- # the app so the best we can do is block until our bg thread has sent
- # it. In the future we can perhaps add a proper 'command port'
- # interface for proper blocking two way communication.
+ # Ideally we'd block here until the command was run so our
+ # prompt would print after it's results. We currently don't get
+ # any response from the app so the best we can do is block until
+ # our bg thread has sent it. In the future we can perhaps add a
+ # proper 'command port' interface for proper blocking two way
+ # communication.
while True:
with self._subprocess_commands_lock:
if not self._subprocess_commands:
break
time.sleep(0.1)
- # One last short delay so if we come out *just* as the command is sent
- # we'll hopefully still give it enough time to process/print.
+ # One last short delay so if we come out *just* as the command
+ # is sent we'll hopefully still give it enough time to
+ # process/print.
time.sleep(0.1)
def screenmessage(
@@ -320,8 +352,8 @@ class ServerManagerApp:
)
)
- # If we're asking for an immediate restart but don't get one within
- # the grace period, bring down the hammer.
+ # If we're asking for an immediate restart but don't get one
+ # within the grace period, bring down the hammer.
if immediate:
self._subprocess_force_kill_time = (
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
@@ -340,12 +372,12 @@ class ServerManagerApp:
ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate)
)
- # An explicit shutdown means we know to bail completely once this
- # subprocess completes.
+ # An explicit shutdown means we know to bail completely once
+ # this subprocess completes.
self._wrapper_shutdown_desired = True
- # If we're asking for an immediate shutdown but don't get one within
- # the grace period, bring down the hammer.
+ # If we're asking for an immediate shutdown but don't get one
+ # within the grace period, bring down the hammer.
if immediate:
self._subprocess_force_kill_time = (
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
@@ -378,9 +410,10 @@ class ServerManagerApp:
if i + 1 >= argc:
raise CleanError('Expected a path as next arg.')
path = sys.argv[i + 1]
- # Unlike config_path, this one doesn't have to exist now.
- # We do however need an abs path because we may be in a
- # different cwd currently than we will be during the run.
+ # Unlike config_path, this one doesn't have to exist
+ # now. We do however need an abs path because we may be
+ # in a different cwd currently than we will be during
+ # the run.
self._ba_root_path = os.path.abspath(path)
i += 2
elif arg == '--interactive':
@@ -538,6 +571,7 @@ class ServerManagerApp:
if not os.path.exists(self._config_path):
# Special case:
+ #
# If the user didn't specify a particular config file, allow
# gracefully falling back to defaults if the default one is
# missing.
@@ -606,24 +640,26 @@ class ServerManagerApp:
"""Spin up the server subprocess and run it until exit."""
# pylint: disable=consider-using-with
- # Reload our config, and update our overall behavior based on it.
- # We do non-strict this time to give the user repeated attempts if
- # if they mess up while modifying the config on the fly.
+ # Reload our config, and update our overall behavior based on
+ # it. We do non-strict this time to give the user repeated
+ # attempts if if they mess up while modifying the config on the
+ # fly.
self.load_config(strict=False, print_confirmation=True)
self._prep_subprocess_environment()
- # Launch the binary and grab its stdin;
- # we'll use this to feed it commands.
+ # Launch the binary and grab its stdin; we'll use this to feed
+ # it commands.
self._subprocess_launch_time = time.time()
# Set an environment var so the server process knows its being
- # run under us. This causes it to ignore ctrl-c presses and other
- # slight behavior tweaks. Hmm; should this be an argument instead?
+ # run under us. This causes it to ignore ctrl-c presses and
+ # other slight behavior tweaks. Hmm; should this be an argument
+ # instead?
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
- # Set an environment var to change the device name.
- # Device name is used while making connection with master server,
+ # Set an environment var to change the device name. Device name
+ # is used while making connection with master server,
# cloud-console recognize us with this name.
os.environ['BA_DEVICE_NAME'] = self._config.party_name
@@ -663,9 +699,10 @@ class ServerManagerApp:
assert self._subprocess_exited_cleanly is not None
- # EW: it seems that if we die before the main thread has fully started
- # up the interpreter, its possible that it will not break out of its
- # loop via the usual SystemExit that gets sent when we die.
+ # EW: it seems that if we die before the main thread has fully
+ # started up the interpreter, its possible that it will not
+ # break out of its loop via the usual SystemExit that gets sent
+ # when we die.
if self._interactive:
while (
self._interpreter_start_time is None
@@ -694,8 +731,8 @@ class ServerManagerApp:
# tell the main thread to die.
if self._wrapper_shutdown_desired:
# Only do this if the main thread is not already waiting for
- # us to die; otherwise it can lead to deadlock.
- # (we hang in os.kill while main thread is blocked in Thread.join)
+ # us to die; otherwise it can lead to deadlock. (we hang in
+ # os.kill while main thread is blocked in Thread.join)
if not self._done:
self._done = True
@@ -721,6 +758,8 @@ class ServerManagerApp:
bincfg['Auto Balance Teams'] = self._config.auto_balance_teams
bincfg['Show Tutorial'] = self._config.show_tutorial
+ if self._config.protocol_version is not None:
+ bincfg['SceneV1 Host Protocol'] = self._config.protocol_version
if self._config.team_names is not None:
bincfg['Custom Team Names'] = self._config.team_names
elif 'Custom Team Names' in bincfg:
@@ -769,8 +808,8 @@ class ServerManagerApp:
assert current_thread() is self._subprocess_thread
assert self._subprocess.stdin is not None
- # Send the initial server config which should kick things off.
- # (but make sure its values are still valid first)
+ # Send the initial server config which should kick things off
+ # (but make sure its values are still valid first).
dataclass_validate(self._config)
self._send_server_command(StartServerModeCommand(self._config))
@@ -782,8 +821,8 @@ class ServerManagerApp:
# Pass along any commands to our process.
with self._subprocess_commands_lock:
for incmd in self._subprocess_commands:
- # If we're passing a raw string to exec, no need to wrap it
- # in any proper structure.
+ # If we're passing a raw string to exec, no need to
+ # wrap it in any proper structure.
if isinstance(incmd, str):
self._subprocess.stdin.write((incmd + '\n').encode())
self._subprocess.stdin.flush()
@@ -794,9 +833,9 @@ class ServerManagerApp:
# Request restarts/shut-downs for various reasons.
self._request_shutdowns_or_restarts()
- # If they want to force-kill our subprocess, simply exit this
- # loop; the cleanup code will kill the process if its still
- # alive.
+ # If they want to force-kill our subprocess, simply exit
+ # this loop; the cleanup code will kill the process if its
+ # still alive.
if (
self._subprocess_force_kill_time is not None
and time.time() > self._subprocess_force_kill_time
@@ -855,8 +894,8 @@ class ServerManagerApp:
self.restart(immediate=True)
self._subprocess_sent_config_auto_restart = True
- # Attempt clean exit if our clean-exit-time passes.
- # (and enforce a 6 hour max if not provided)
+ # Attempt clean exit if our clean-exit-time passes (and enforce
+ # a 6 hour max if not provided).
clean_exit_minutes = 360.0
if self._config.clean_exit_minutes is not None:
clean_exit_minutes = min(
@@ -881,8 +920,8 @@ class ServerManagerApp:
self.shutdown(immediate=False)
self._subprocess_sent_clean_exit = True
- # Attempt unclean exit if our unclean-exit-time passes.
- # (and enforce a 7 hour max if not provided)
+ # Attempt unclean exit if our unclean-exit-time passes (and
+ # enforce a 7 hour max if not provided).
unclean_exit_minutes = 420.0
if self._config.unclean_exit_minutes is not None:
unclean_exit_minutes = min(
@@ -924,8 +963,8 @@ class ServerManagerApp:
print(f'{Clr.CYN}Stopping subprocess...{Clr.RST}', flush=True)
- # First, ask it nicely to die and give it a moment.
- # If that doesn't work, bring down the hammer.
+ # First, ask it nicely to die and give it a moment. If that
+ # doesn't work, bring down the hammer.
self._subprocess.terminate()
try:
self._subprocess.wait(timeout=10)
@@ -941,8 +980,9 @@ def main() -> None:
try:
ServerManagerApp().run()
except CleanError as exc:
- # For clean errors, do a simple print and fail; no tracebacks/etc.
- # Any others will bubble up and give us the usual mess.
+ # For clean errors, do a simple print and fail; no
+ # tracebacks/etc. Any others will bubble up and give us the
+ # usual mess.
exc.pretty_print()
sys.exit(1)
diff --git a/src/ballistica/base/app_adapter/app_adapter.cc b/src/ballistica/base/app_adapter/app_adapter.cc
index f7fb480c..2c288168 100644
--- a/src/ballistica/base/app_adapter/app_adapter.cc
+++ b/src/ballistica/base/app_adapter/app_adapter.cc
@@ -2,62 +2,17 @@
#include "ballistica/base/app_adapter/app_adapter.h"
-#if BA_OSTYPE_ANDROID
-#include "ballistica/base/app_adapter/app_adapter_android.h"
-#endif
-#include "ballistica/base/app_adapter/app_adapter_apple.h"
-#include "ballistica/base/app_adapter/app_adapter_headless.h"
-#include "ballistica/base/app_adapter/app_adapter_sdl.h"
-#include "ballistica/base/app_adapter/app_adapter_vr.h"
-#include "ballistica/base/graphics/graphics_server.h"
#include "ballistica/base/graphics/renderer/renderer.h"
#include "ballistica/base/input/input.h"
#include "ballistica/base/networking/network_reader.h"
#include "ballistica/base/networking/networking.h"
-#include "ballistica/base/platform/base_platform.h"
-#include "ballistica/base/support/stress_test.h"
+#include "ballistica/base/python/base_python.h"
+#include "ballistica/base/support/app_config.h"
#include "ballistica/base/ui/ui.h"
#include "ballistica/shared/foundation/event_loop.h"
-#include "ballistica/shared/python/python.h"
namespace ballistica::base {
-auto AppAdapter::Create() -> AppAdapter* {
- assert(g_core);
-
-// TEMP - need to init sdl on our legacy mac build even though its not
-// technically an SDL app. Kill this once the old mac build is gone.
-#if BA_LEGACY_MACOS_BUILD
- AppAdapterSDL::InitSDL();
-#endif
-
- AppAdapter* app_adapter{};
-
-#if BA_HEADLESS_BUILD
- app_adapter = new AppAdapterHeadless();
-#elif BA_OSTYPE_ANDROID
- app_adapter = new AppAdapterAndroid();
-#elif BA_XCODE_BUILD
- app_adapter = new AppAdapterApple();
-#elif BA_RIFT_BUILD
- // Rift build can spin up in either VR or regular mode.
- if (g_core->vr_mode) {
- app_adapter = new AppAdapterVR();
- } else {
- app_adapter = new AppAdapterSDL();
- }
-#elif BA_CARDBOARD_BUILD
- app_adapter = new AppAdapterVR();
-#elif BA_SDL_BUILD
- app_adapter = new AppAdapterSDL();
-#else
-#error No app adapter defined for this build.
-#endif
-
- assert(app_adapter);
- return app_adapter;
-}
-
AppAdapter::AppAdapter() = default;
AppAdapter::~AppAdapter() = default;
@@ -65,172 +20,18 @@ AppAdapter::~AppAdapter() = default;
auto AppAdapter::ManagesMainThreadEventLoop() const -> bool { return true; }
void AppAdapter::OnMainThreadStartApp() {
- assert(g_base);
assert(g_core);
assert(g_core->InMainThread());
-
- // Add some common input devices where applicable. More specific ones (SDL
- // Joysticks, etc.) get added in subclasses.
-
- // FIXME: This stuff should probably go elsewhere.
- if (!g_core->HeadlessMode()) {
- // If we've got a nice themed hardware cursor, show it. Otherwise we'll
- // render it manually, which is laggier but gets the job done.
- // g_base->platform->SetHardwareCursorVisible(g_buildconfig.hardware_cursor());
-
- // On desktop systems we just assume keyboard input exists and add it
- // immediately.
- if (g_core->platform->IsRunningOnDesktop()) {
- g_base->input->PushCreateKeyboardInputDevices();
- }
-
- // On non-tv, non-desktop, non-vr systems, create a touchscreen input.
- if (!g_core->platform->IsRunningOnTV() && !g_core->IsVRMode()
- && !g_core->platform->IsRunningOnDesktop()) {
- g_base->input->CreateTouchInput();
- }
- }
}
void AppAdapter::OnAppStart() { assert(g_base->InLogicThread()); }
-void AppAdapter::OnAppPause() { assert(g_base->InLogicThread()); }
-void AppAdapter::OnAppResume() { assert(g_base->InLogicThread()); }
+void AppAdapter::OnAppSuspend() { assert(g_base->InLogicThread()); }
+void AppAdapter::OnAppUnsuspend() { assert(g_base->InLogicThread()); }
void AppAdapter::OnAppShutdown() { assert(g_base->InLogicThread()); }
void AppAdapter::OnAppShutdownComplete() { assert(g_base->InLogicThread()); }
void AppAdapter::OnScreenSizeChange() { assert(g_base->InLogicThread()); }
void AppAdapter::DoApplyAppConfig() { assert(g_base->InLogicThread()); }
-void AppAdapter::OnAppSuspend_() {
- assert(g_core->InMainThread());
-
- // IMPORTANT: Any pause related stuff that event-loop-threads need to do
- // should be done from their registered pause-callbacks. If we instead
- // push runnables to them from here they may or may not be called before
- // their event-loop is actually paused.
-
- // Pause all event loops.
- EventLoop::SetEventLoopsSuspended(true);
-
- if (g_base->network_reader) {
- g_base->network_reader->OnAppPause();
- }
- g_base->networking->OnAppPause();
-}
-
-void AppAdapter::OnAppUnsuspend_() {
- assert(g_core->InMainThread());
-
- // Spin all event-loops back up.
- EventLoop::SetEventLoopsSuspended(false);
-
- // Run resumes that expect to happen in the main thread.
- g_base->network_reader->OnAppResume();
- g_base->networking->OnAppResume();
-
- // When resuming from a suspended state, we may want to pause whatever
- // game was running when we last were active.
- //
- // TODO(efro): we should make this smarter so it doesn't happen if we're
- // in a network game or something that we can't pause; bringing up the
- // menu doesn't really accomplish anything there.
- //
- // In general this probably should be handled at a higher level.
- if (g_core->should_pause_active_game) {
- g_core->should_pause_active_game = false;
-
- // If we've been completely backgrounded, send a menu-press command to
- // the game; this will bring up a pause menu if we're in the game/etc.
- if (!g_base->ui->MainMenuVisible()) {
- g_base->ui->PushMainMenuPressCall(nullptr);
- }
- }
-}
-
-void AppAdapter::SuspendApp() {
- assert(g_core);
- assert(g_core->InMainThread());
-
- if (app_suspended_) {
- Log(LogLevel::kWarning,
- "AppAdapter::SuspendApp() called with app already suspended.");
- return;
- }
-
- millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()};
-
- // Apple mentioned 5 seconds to run stuff once backgrounded or they bring
- // down the hammer. Let's aim to stay under 2.
- millisecs_t max_duration{2000};
-
- g_core->platform->DebugLog(
- "SuspendApp@"
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
- // assert(!app_pause_requested_);
- // app_pause_requested_ = true;
- app_suspended_ = true;
- OnAppSuspend_();
- // UpdatePauseResume_();
-
- // We assume that the OS will completely suspend our process the moment we
- // return from this call (though this is not technically true on all
- // platforms). So we want to spin and wait for threads to actually process
- // the pause message.
- size_t running_thread_count{};
- while (std::abs(core::CorePlatform::GetCurrentMillisecs() - start_time)
- < max_duration) {
- // If/when we get to a point with no threads waiting to be paused, we're
- // good to go.
- auto threads{EventLoop::GetStillSuspendingEventLoops()};
- running_thread_count = threads.size();
- if (running_thread_count == 0) {
- if (g_buildconfig.debug_build()) {
- Log(LogLevel::kDebug,
- "SuspendApp() completed in "
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()
- - start_time)
- + "ms.");
- }
- return;
- }
- }
-
- // If we made it here, we timed out. Complain.
- Log(LogLevel::kError,
- std::string("SuspendApp() took too long; ")
- + std::to_string(running_thread_count)
- + " threads not yet paused after "
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()
- - start_time)
- + " ms.");
-}
-
-void AppAdapter::UnsuspendApp() {
- assert(g_core);
- assert(g_core->InMainThread());
-
- if (!app_suspended_) {
- Log(LogLevel::kWarning,
- "AppAdapter::UnsuspendApp() called with app not in paused state.");
- return;
- }
- millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()};
- g_core->platform->DebugLog(
- "UnsuspendApp@"
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
- // assert(app_pause_requested_);
- // app_pause_requested_ = false;
- // UpdatePauseResume_();
- app_suspended_ = false;
- OnAppUnsuspend_();
- if (g_buildconfig.debug_build()) {
- Log(LogLevel::kDebug,
- "UnsuspendApp() completed in "
- + std::to_string(core::CorePlatform::GetCurrentMillisecs()
- - start_time)
- + "ms.");
- }
-}
-
void AppAdapter::RunMainThreadEventLoopToCompletion() {
FatalError("RunMainThreadEventLoopToCompletion is not implemented here.");
}
@@ -239,7 +40,7 @@ void AppAdapter::DoExitMainThreadEventLoop() {
FatalError("DoExitMainThreadEventLoop is not implemented here.");
}
-auto AppAdapter::CanToggleFullscreen() -> bool const { return false; }
+auto AppAdapter::FullscreenControlAvailable() const -> bool { return false; }
auto AppAdapter::SupportsVSync() -> bool const { return false; }
@@ -253,6 +54,29 @@ void AppAdapter::DoPushGraphicsContextRunnable(Runnable* runnable) {
DoPushMainThreadRunnable(runnable);
}
+auto AppAdapter::FullscreenControlGet() const -> bool {
+ assert(g_base->InLogicThread());
+
+ // By default, just go through config (assume we have full control over
+ // the fullscreen state ourself).
+ return g_base->app_config->Resolve(AppConfig::BoolID::kFullscreen);
+}
+
+void AppAdapter::FullscreenControlSet(bool fullscreen) {
+ assert(g_base->InLogicThread());
+ // By default, just set these in the config and apply it (assumes config
+ // changes get plugged into actual fullscreen state).
+ g_base->python->objs()
+ .Get(fullscreen ? BasePython::ObjID::kSetConfigFullscreenOnCall
+ : BasePython::ObjID::kSetConfigFullscreenOffCall)
+ .Call();
+}
+
+auto AppAdapter::FullscreenControlKeyShortcut() const
+ -> std::optional {
+ return {};
+}
+
void AppAdapter::CursorPositionForDraw(float* x, float* y) {
assert(x && y);
@@ -271,14 +95,62 @@ auto AppAdapter::ShouldUseCursor() -> bool { return true; }
auto AppAdapter::HasHardwareCursor() -> bool { return false; }
-void AppAdapter::SetHardwareCursorVisible(bool visible) {
- printf("SHOULD SET VIS %d\n", static_cast(visible));
-}
+void AppAdapter::SetHardwareCursorVisible(bool visible) {}
auto AppAdapter::CanSoftQuit() -> bool { return false; }
auto AppAdapter::CanBackQuit() -> bool { return false; }
void AppAdapter::DoBackQuit() { FatalError("Fixme unimplemented."); }
void AppAdapter::DoSoftQuit() { FatalError("Fixme unimplemented."); }
void AppAdapter::TerminateApp() { FatalError("Fixme unimplemented."); }
+auto AppAdapter::HasDirectKeyboardInput() -> bool { return false; }
+
+void AppAdapter::ApplyGraphicsSettings(const GraphicsSettings* settings) {}
+
+auto AppAdapter::GetGraphicsSettings() -> GraphicsSettings* {
+ return new GraphicsSettings();
+}
+
+auto AppAdapter::GetGraphicsClientContext() -> GraphicsClientContext* {
+ return new GraphicsClientContext();
+}
+
+auto AppAdapter::GetKeyRepeatDelay() -> float { return 0.3f; }
+auto AppAdapter::GetKeyRepeatInterval() -> float { return 0.08f; }
+
+auto AppAdapter::DoClipboardIsSupported() -> bool { return false; }
+
+auto AppAdapter::DoClipboardHasText() -> bool {
+ // Shouldn't get here since we default to no clipboard support.
+ FatalError("Shouldn't get here.");
+ return false;
+}
+
+void AppAdapter::DoClipboardSetText(const std::string& text) {
+ // Shouldn't get here since we default to no clipboard support.
+ FatalError("Shouldn't get here.");
+}
+
+auto AppAdapter::DoClipboardGetText() -> std::string {
+ // Shouldn't get here since we default to no clipboard support.
+ FatalError("Shouldn't get here.");
+ return "";
+}
+
+auto AppAdapter::GetKeyName(int keycode) -> std::string {
+ BA_LOG_ONCE(LogLevel::kWarning,
+ "CorePlatform::GetKeyName not implemented here.");
+ return "?";
+}
+
+auto AppAdapter::NativeReviewRequestSupported() -> bool { return false; }
+
+void AppAdapter::NativeReviewRequest() {
+ BA_PRECONDITION(NativeReviewRequestSupported());
+ PushMainThreadCall([this] { DoNativeReviewRequest(); });
+}
+
+void AppAdapter::DoNativeReviewRequest() { FatalError("Fixme unimplemented."); }
+
+auto AppAdapter::ShouldSilenceAudioForInactive() -> bool const { return false; }
} // namespace ballistica::base
diff --git a/src/ballistica/base/app_adapter/app_adapter.h b/src/ballistica/base/app_adapter/app_adapter.h
index 427f536c..8db58c7e 100644
--- a/src/ballistica/base/app_adapter/app_adapter.h
+++ b/src/ballistica/base/app_adapter/app_adapter.h
@@ -15,21 +15,30 @@ namespace ballistica::base {
/// all might share the same CorePlatform and BasePlatform classes.
class AppAdapter {
public:
- /// Instantiate the AppAdapter subclass for the current build.
- static auto Create() -> AppAdapter*;
+ AppAdapter();
/// Called in the main thread when the app is being started.
virtual void OnMainThreadStartApp();
// Logic thread callbacks.
virtual void OnAppStart();
- virtual void OnAppPause();
- virtual void OnAppResume();
+ virtual void OnAppSuspend();
+ virtual void OnAppUnsuspend();
virtual void OnAppShutdown();
virtual void OnAppShutdownComplete();
virtual void OnScreenSizeChange();
virtual void DoApplyAppConfig();
+ /// When called, should allocate an instance of a GraphicsSettings
+ /// subclass using 'new', fill it out, and return it. Runs in the logic
+ /// thread.
+ virtual auto GetGraphicsSettings() -> GraphicsSettings*;
+
+ /// When called, should allocate an instance of a GraphicsClientContext
+ /// subclass using 'new', fill it out, and return it. Runs in the graphics
+ /// context.
+ virtual auto GetGraphicsClientContext() -> GraphicsClientContext*;
+
/// Return whether this class manages the main thread event loop itself.
/// Default is true. If this is true, RunMainThreadEventLoopToCompletion()
/// will be called to run the app. This should return false on builds
@@ -79,9 +88,9 @@ class AppAdapter {
/// plugged in or unplugged/etc. Default implementation returns true.
virtual auto ShouldUseCursor() -> bool;
- /// Return whether the app-adapter is having the OS show a cursor.
- /// If this returns false, the engine will take care of drawing a cursor
- /// when necessary. If true, SetHardwareCursorVisible will be called
+ /// Return whether the app-adapter is having the OS show a cursor. If this
+ /// returns false, the engine will take care of drawing a cursor when
+ /// necessary. If true, SetHardwareCursorVisible will be called
/// periodically to inform the adapter what the cursor state should be.
/// The default implementation returns false;
virtual auto HasHardwareCursor() -> bool;
@@ -97,25 +106,28 @@ class AppAdapter {
/// values.
virtual void CursorPositionForDraw(float* x, float* y);
- /// Put the app into a paused state. Should be called from the main
- /// thread. Pauses work, closes network sockets, etc. May correspond to
- /// being backgrounded on mobile, being minimized on desktop, etc. It is
- /// assumed that, as soon as this call returns, all work is finished and
- /// all threads can be suspended by the OS without any negative side
- /// effects.
- void SuspendApp();
-
- /// Resume the app; can correspond to foregrounding on mobile,
- /// unminimizing on desktop, etc. Spins threads back up, re-opens network
- /// sockets, etc.
- void UnsuspendApp();
-
- auto app_suspended() const { return app_suspended_; }
-
/// Return whether this AppAdapter supports a 'fullscreen' toggle for its
- /// display. This currently will simply affect whether that option is
- /// available in display settings or via a hotkey.
- virtual auto CanToggleFullscreen() -> bool const;
+ /// display. This will affect whether that option is available in display
+ /// settings or via a hotkey. Must be called from the logic thread.
+ virtual auto FullscreenControlAvailable() const -> bool;
+
+ /// AppAdapters supporting a 'fullscreen' control should return the
+ /// current fullscreen state here. By default this simply returns the
+ /// app-config fullscreen value (so assumes the actual state is synced to
+ /// that). Must be called from the logic thread.
+ virtual auto FullscreenControlGet() const -> bool;
+
+ /// AppAdapters supporting a 'fullscreen' control should set the
+ /// current fullscreen state here. By default this simply sets the
+ /// app-config fullscreen value (so assumes the actual state is synced to
+ /// that). Must be called from the logic thread.
+ virtual void FullscreenControlSet(bool fullscreen);
+
+ /// AppAdapters supporting a 'fullscreen' control can return a key name
+ /// here to display if they support toggling via key ('ctrl-F', etc.).
+ /// Must be called from the logic thread.
+ virtual auto FullscreenControlKeyShortcut() const
+ -> std::optional;
/// Return whether this AppAdapter supports vsync controls for its display.
virtual auto SupportsVSync() -> bool const;
@@ -123,6 +135,15 @@ class AppAdapter {
/// Return whether this AppAdapter supports max-fps controls for its display.
virtual auto SupportsMaxFPS() -> bool const;
+ /// Return whether audio should be silenced when the app goes inactive. On
+ /// Desktop systems it is generally normal to continue to hear things even
+ /// if their windows are hidden, but on mobile we probably want to silence
+ /// our audio when phone calls, ads, etc. pop up over it. Note that this
+ /// is called each time the app goes inactive, so the adapter may choose
+ /// to selectively silence audio depending on what caused the inactive
+ /// switch.
+ virtual auto ShouldSilenceAudioForInactive() -> bool const;
+
/// Return whether this platform supports soft-quit. A soft quit is
/// when the app is reset/backgrounded/etc. but remains running in case
/// needed again. Generally this is the behavior on mobile apps.
@@ -153,9 +174,31 @@ class AppAdapter {
/// this point.
virtual void TerminateApp();
- protected:
- AppAdapter();
- virtual ~AppAdapter();
+ /// Should return whether there is a keyboard attached that will deliver
+ /// direct text-editing related events to the app. When this is false,
+ /// alternate entry methods such as keyboard-entry-dialogs and on-screen
+ /// keyboards will be used. This value can change based on conditions such
+ /// as a hardware keyboard getting attached or detached or the language
+ /// changing (it may be preferable to rely on dialogs for non-english
+ /// languages/etc.). Default implementation returns false. This function
+ /// should be callable from any thread.
+ ///
+ /// Note that UI elements wanting to accept direct keyboard input should
+ /// not call this directly, but instead should call
+ /// UI::UIHasDirectKeyboardInput, as that takes into account other factors
+ /// such as which device is currently controlling the UI (Someone
+ /// navigating the UI with a game controller may still get an on-screen
+ /// keyboard even if there is a physical keyboard attached).
+ virtual auto HasDirectKeyboardInput() -> bool;
+
+ /// Called in the graphics context to apply new settings coming in from
+ /// the logic subsystem. This will be called initially to jump-start the
+ /// graphics system as well as before frame draws to update any new
+ /// settings coming in.
+ virtual void ApplyGraphicsSettings(const GraphicsSettings* settings);
+
+ virtual auto GetKeyRepeatDelay() -> float;
+ virtual auto GetKeyRepeatInterval() -> float;
/// Push a raw pointer Runnable to the platform's 'main' thread. The main
/// thread should call its RunAndLogErrors() method and then delete it.
@@ -165,10 +208,28 @@ class AppAdapter {
/// context. By default this is simply the main thread.
virtual void DoPushGraphicsContextRunnable(Runnable* runnable);
+ /// Return a name for a ballistica keyboard keycode.
+ virtual auto GetKeyName(int keycode) -> std::string;
+
+ /// Return whether there is a native 'review-this-app' prompt.
+ virtual auto NativeReviewRequestSupported() -> bool;
+
+ /// Asynchronously kick off a native review request.
+ void NativeReviewRequest();
+
+ virtual auto DoClipboardIsSupported() -> bool;
+ virtual auto DoClipboardHasText() -> bool;
+ virtual void DoClipboardSetText(const std::string& text);
+ virtual auto DoClipboardGetText() -> std::string;
+
+ protected:
+ virtual ~AppAdapter();
+
+ /// Override to implement native review requests. Will be called in the
+ /// main thread.
+ virtual void DoNativeReviewRequest();
+
private:
- void OnAppSuspend_();
- void OnAppUnsuspend_();
- bool app_suspended_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/app_adapter/app_adapter_apple.cc b/src/ballistica/base/app_adapter/app_adapter_apple.cc
index 9d87088d..515eb046 100644
--- a/src/ballistica/base/app_adapter/app_adapter_apple.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_apple.cc
@@ -3,16 +3,23 @@
#include "ballistica/base/app_adapter/app_adapter_apple.h"
-#include
-
#include "ballistica/base/graphics/gl/renderer_gl.h"
#include "ballistica/base/graphics/graphics.h"
#include "ballistica/base/graphics/graphics_server.h"
#include "ballistica/base/logic/logic.h"
+#include "ballistica/base/platform/apple/apple_utils.h"
+#include "ballistica/base/platform/apple/from_swift.h"
+#include "ballistica/base/platform/support/min_sdl_key_names.h"
#include "ballistica/base/support/app_config.h"
#include "ballistica/shared/ballistica.h"
#include "ballistica/shared/foundation/event_loop.h"
+// clang-format off
+// This needs to be below ballistica headers since it relies on
+// some types in them but does not include headers itself.
+#include
+// clang-format on
+
namespace ballistica::base {
/// RAII-friendly way to mark the thread and calls we're allowed to run graphics
@@ -20,7 +27,10 @@ namespace ballistica::base {
class AppAdapterApple::ScopedAllowGraphics_ {
public:
explicit ScopedAllowGraphics_(AppAdapterApple* adapter) : adapter_{adapter} {
+ // We currently assume only one thread will be doing this at any given
+ // time; will need to add a lock if that's not always the case.
assert(!adapter_->graphics_allowed_);
+ // Keep graphics thread updated each time through since it can change.
adapter->graphics_thread_ = std::this_thread::get_id();
adapter->graphics_allowed_ = true;
}
@@ -34,68 +44,47 @@ class AppAdapterApple::ScopedAllowGraphics_ {
};
auto AppAdapterApple::ManagesMainThreadEventLoop() const -> bool {
- // Nope; we run under a standard Cocoa/UIKit environment and they call us; we
- // don't call them.
+ // Nope; we run under a standard Cocoa/UIKit environment and they call us;
+ // we don't call them.
return false;
}
void AppAdapterApple::DoPushMainThreadRunnable(Runnable* runnable) {
// Kick this along to swift.
- BallisticaKit::FromCppPushRawRunnableToMain(runnable);
+ BallisticaKit::FromCpp::pushRawRunnableToMain(runnable);
}
-void AppAdapterApple::DoApplyAppConfig() {
- assert(g_base->InLogicThread());
-
- g_base->graphics_server->PushSetScreenPixelScaleCall(
- g_base->app_config->Resolve(AppConfig::FloatID::kScreenPixelScale));
-
- auto graphics_quality_requested =
- g_base->graphics->GraphicsQualityFromAppConfig();
-
- auto texture_quality_requested =
- g_base->graphics->TextureQualityFromAppConfig();
-
- g_base->app_adapter->PushGraphicsContextCall([=] {
- SetScreen_(texture_quality_requested, graphics_quality_requested);
- });
+void AppAdapterApple::OnMainThreadStartApp() {
+ AppAdapter::OnMainThreadStartApp();
+#if BA_USE_STORE_KIT
+ BallisticaKit::StoreKitContext::onAppStart();
+#endif
+#if BA_USE_GAME_CENTER
+ BallisticaKit::GameCenterContext::onAppStart();
+#endif
}
-void AppAdapterApple::SetScreen_(
- TextureQualityRequest texture_quality_requested,
- GraphicsQualityRequest graphics_quality_requested) {
- // If we know what we support, filter our request types to what is
- // supported. This will keep us from rebuilding contexts if request type
- // is flipping between different types that we don't support.
- if (g_base->graphics->has_supports_high_quality_graphics_value()) {
- if (!g_base->graphics->supports_high_quality_graphics()
- && graphics_quality_requested > GraphicsQualityRequest::kMedium) {
- graphics_quality_requested = GraphicsQualityRequest::kMedium;
- }
- }
+void AppAdapterApple::DoApplyAppConfig() { assert(g_base->InLogicThread()); }
- auto* gs = g_base->graphics_server;
+void AppAdapterApple::ApplyGraphicsSettings(const GraphicsSettings* settings) {
+ auto* graphics_server = g_base->graphics_server;
// We need a full renderer reload if quality values have changed
- // or if we don't have one yet.
- bool need_full_reload =
- ((gs->texture_quality_requested() != texture_quality_requested)
- || (gs->graphics_quality_requested() != graphics_quality_requested)
- || !gs->texture_quality_set() || !gs->graphics_quality_set());
+ // or if we don't have a renderer yet.
+ bool need_full_reload = ((graphics_server->texture_quality_requested()
+ != settings->texture_quality)
+ || (graphics_server->graphics_quality_requested()
+ != settings->graphics_quality));
+
+ // We need a full renderer reload if quality values have changed or if we
+ // don't yet have a renderer.
if (need_full_reload) {
- ReloadRenderer_(graphics_quality_requested, texture_quality_requested);
+ ReloadRenderer_(settings);
}
-
- // Let the logic thread know we've got a graphics system up and running.
- // It may use this cue to kick off asset loads and other bootstrapping.
- g_base->logic->event_loop()->PushCall(
- [] { g_base->logic->OnGraphicsReady(); });
}
-void AppAdapterApple::ReloadRenderer_(
- GraphicsQualityRequest graphics_quality_requested,
- TextureQualityRequest texture_quality_requested) {
+void AppAdapterApple::ReloadRenderer_(const GraphicsSettings* settings) {
auto* gs = g_base->graphics_server;
if (gs->renderer() && gs->renderer_loaded()) {
@@ -105,38 +94,22 @@ void AppAdapterApple::ReloadRenderer_(
gs->set_renderer(new RendererGL());
}
- // Set a dummy screen resolution to start with.
- // The main thread will kick along the latest real resolution just before
- // each frame draw, but we need *something* here or else we'll get errors due
- // to framebuffers getting made at size 0/etc.
- g_base->graphics_server->SetScreenResolution(320.0, 240.0);
-
// Update graphics quality based on request.
- gs->set_graphics_quality_requested(graphics_quality_requested);
- gs->set_texture_quality_requested(texture_quality_requested);
+ gs->set_graphics_quality_requested(settings->graphics_quality);
+ gs->set_texture_quality_requested(settings->texture_quality);
// (Re)load stuff with these latest quality settings.
gs->LoadRenderer();
}
-void AppAdapterApple::UpdateScreenSizes_() {
- assert(g_base->app_adapter->InGraphicsContext());
-}
-
-void AppAdapterApple::SetScreenResolution(float pixel_width,
- float pixel_height) {
- auto allow = ScopedAllowGraphics_(this);
- g_base->graphics_server->SetScreenResolution(pixel_width, pixel_height);
-}
-
auto AppAdapterApple::TryRender() -> bool {
auto allow = ScopedAllowGraphics_(this);
// Run & release any pending runnables.
std::vector calls;
{
- // Pull calls off the list before running them; this way we only need
- // to grab the list lock for a moment.
+ // Pull calls off the list before running them; this way we only need to
+ // grab the list lock for a moment.
auto lock = std::scoped_lock(graphics_calls_mutex_);
if (!graphics_calls_.empty()) {
graphics_calls_.swap(calls);
@@ -146,10 +119,45 @@ auto AppAdapterApple::TryRender() -> bool {
call->RunAndLogErrors();
delete call;
}
- // Lastly render.
- return g_base->graphics_server->TryRender();
- return true;
+ // Lastly, render.
+ auto result = g_base->graphics_server->TryRender();
+
+ // A little trick to make mac resizing look a lot smoother. Because we
+ // render in a background thread, we often don't render at the most up to
+ // date window size during a window resize. Normally this makes our image
+ // jerk around in an ugly way, but if we just re-render once or twice in
+ // those cases we mostly always get the most up to date window size.
+ if (result && resize_friendly_frames_ > 0) {
+ // Leave this enabled for just a few frames every time it is set.
+ // (so just in case it breaks we won't draw each frame serveral times for
+ // eternity).
+ resize_friendly_frames_ -= 1;
+
+ // Keep on drawing until the drawn window size
+ // matches what we have (or until we try for too long or fail at drawing).
+ seconds_t start_time = g_core->GetAppTimeSeconds();
+ for (int i = 0; i < 5; ++i) {
+ bool size_differs =
+ ((std::abs(resize_target_resolution_.x
+ - g_base->graphics_server->screen_pixel_width())
+ > 0.01f)
+ || (std::abs(resize_target_resolution_.y
+ - g_base->graphics_server->screen_pixel_height())
+ > 0.01f));
+ if (size_differs && g_core->GetAppTimeSeconds() - start_time < 0.1
+ && result) {
+ result = g_base->graphics_server->TryRender();
+ }
+ }
+ }
+
+ return result;
+}
+
+void AppAdapterApple::EnableResizeFriendlyMode(int width, int height) {
+ resize_friendly_frames_ = 5;
+ resize_target_resolution_ = Vector2f(width, height);
}
auto AppAdapterApple::InGraphicsContext() -> bool {
@@ -157,7 +165,6 @@ auto AppAdapterApple::InGraphicsContext() -> bool {
}
void AppAdapterApple::DoPushGraphicsContextRunnable(Runnable* runnable) {
- // In strict mode, make sure we're in our TryRender() call.
auto lock = std::scoped_lock(graphics_calls_mutex_);
if (graphics_calls_.size() > 1000) {
BA_LOG_ONCE(LogLevel::kError, "graphics_calls_ got too big.");
@@ -171,15 +178,15 @@ auto AppAdapterApple::ShouldUseCursor() -> bool {
return true;
}
- // Anywhere else (iOS, tvOS, etc.) just say no cursor for now. The OS
- // may draw one in some cases (trackpad connected to iPad, etc.) but we
- // don't interfere and just let the OS draw its normal cursor in that
- // case. Can revisit this later if that becomes a more common scenario.
+ // Anywhere else (iOS, tvOS, etc.) just say no cursor for now. The OS may
+ // draw one in some cases (trackpad connected to iPad, etc.) but we don't
+ // interfere and just let the OS draw its normal cursor in that case. Can
+ // revisit this later if that becomes a more common scenario.
return false;
}
auto AppAdapterApple::HasHardwareCursor() -> bool {
- // (mac should be only build getting called here)
+ // Mac should be only build getting called here (see ShouldUseCursor).
assert(g_buildconfig.ostype_macos());
return true;
@@ -191,18 +198,121 @@ void AppAdapterApple::SetHardwareCursorVisible(bool visible) {
assert(g_core->InMainThread());
#if BA_OSTYPE_MACOS
- BallisticaKit::CocoaSupportSetCursorVisible(visible);
+ BallisticaKit::CocoaFromCpp::setCursorVisible(visible);
#endif
}
void AppAdapterApple::TerminateApp() {
#if BA_OSTYPE_MACOS
- BallisticaKit::CocoaSupportTerminateApp();
+ BallisticaKit::CocoaFromCpp::terminateApp();
#else
AppAdapter::TerminateApp();
#endif
}
+auto AppAdapterApple::FullscreenControlAvailable() const -> bool {
+ // Currently Mac only. Any window-management stuff elsewhere such as
+ // iPadOS is out of our hands.
+ if (g_buildconfig.ostype_macos()) {
+ return true;
+ }
+ return false;
+}
+
+auto AppAdapterApple::FullscreenControlGet() const -> bool {
+#if BA_OSTYPE_MACOS
+ return BallisticaKit::CocoaFromCpp::getMainWindowIsFullscreen();
+#else
+ return false;
+#endif
+}
+
+void AppAdapterApple::FullscreenControlSet(bool fullscreen) {
+#if BA_OSTYPE_MACOS
+ return BallisticaKit::CocoaFromCpp::setMainWindowFullscreen(fullscreen);
+#endif
+}
+
+auto AppAdapterApple::FullscreenControlKeyShortcut() const
+ -> std::optional {
+ return "fn+F";
+}
+
+auto AppAdapterApple::HasDirectKeyboardInput() -> bool { return true; };
+
+auto AppAdapterApple::GetKeyRepeatDelay() -> float {
+#if BA_OSTYPE_MACOS
+ return BallisticaKit::CocoaFromCpp::getKeyRepeatDelay();
+#else
+ return AppAdapter::GetKeyRepeatDelay();
+#endif
+}
+
+auto AppAdapterApple::GetKeyRepeatInterval() -> float {
+#if BA_OSTYPE_MACOS
+ return BallisticaKit::CocoaFromCpp::getKeyRepeatInterval();
+#else
+ return AppAdapter::GetKeyRepeatDelay();
+#endif
+}
+
+auto AppAdapterApple::DoClipboardIsSupported() -> bool {
+#if BA_OSTYPE_MACOS
+ return BallisticaKit::CocoaFromCpp::clipboardIsSupported();
+#else
+ return AppAdapter::DoClipboardIsSupported();
+#endif
+}
+
+auto AppAdapterApple::DoClipboardHasText() -> bool {
+#if BA_OSTYPE_MACOS
+ return BallisticaKit::CocoaFromCpp::clipboardHasText();
+#else
+ return AppAdapter::DoClipboardHasText();
+#endif
+}
+
+void AppAdapterApple::DoClipboardSetText(const std::string& text) {
+#if BA_OSTYPE_MACOS
+ BallisticaKit::CocoaFromCpp::clipboardSetText(text);
+#else
+ AppAdapter::DoClipboardSetText(text);
+#endif
+}
+
+auto AppAdapterApple::DoClipboardGetText() -> std::string {
+#if BA_OSTYPE_MACOS
+ auto contents = BallisticaKit::CocoaFromCpp::clipboardGetText();
+ if (contents) {
+ return std::string(contents.get());
+ }
+ throw Exception("No text on clipboard.");
+#else
+ return AppAdapter::DoClipboardGetText();
+#endif
+}
+
+auto AppAdapterApple::GetKeyName(int keycode) -> std::string {
+ return MinSDL_GetKeyName(keycode);
+}
+
+auto AppAdapterApple::NativeReviewRequestSupported() -> bool {
+ // StoreKit currently supports this everywhere except tvOS.
+ if (g_buildconfig.xcode_build() && g_buildconfig.use_store_kit()
+ && !g_buildconfig.ostype_tvos()) {
+ return true;
+ }
+ return false;
+}
+
+void AppAdapterApple::DoNativeReviewRequest() {
+#if BA_XCODE_BUILD && BA_USE_STORE_KIT && !BA_OSTYPE_TVOS
+ BallisticaKit::StoreKitContext::requestReview();
+#else
+ FatalError("This should not be getting called.");
+#endif
+}
+
} // namespace ballistica::base
#endif // BA_XCODE_BUILD
diff --git a/src/ballistica/base/app_adapter/app_adapter_apple.h b/src/ballistica/base/app_adapter/app_adapter_apple.h
index 09ccdbc2..6a51b51e 100644
--- a/src/ballistica/base/app_adapter/app_adapter_apple.h
+++ b/src/ballistica/base/app_adapter/app_adapter_apple.h
@@ -11,6 +11,7 @@
#include "ballistica/base/app_adapter/app_adapter.h"
#include "ballistica/shared/generic/runnable.h"
+#include "ballistica/shared/math/vector2f.h"
namespace ballistica::base {
@@ -25,14 +26,27 @@ class AppAdapterApple : public AppAdapter {
return val;
}
+ void OnMainThreadStartApp() override;
+
auto ManagesMainThreadEventLoop() const -> bool override;
void DoApplyAppConfig() override;
/// Called by FromSwift.
auto TryRender() -> bool;
- /// Called by FromSwift.
- void SetScreenResolution(float pixel_width, float pixel_height);
+ auto FullscreenControlAvailable() const -> bool override;
+ auto FullscreenControlGet() const -> bool override;
+ void FullscreenControlSet(bool fullscreen) override;
+ auto FullscreenControlKeyShortcut() const
+ -> std::optional override;
+
+ auto HasDirectKeyboardInput() -> bool override;
+ void EnableResizeFriendlyMode(int width, int height);
+
+ auto GetKeyRepeatDelay() -> float override;
+ auto GetKeyRepeatInterval() -> float override;
+ auto GetKeyName(int keycode) -> std::string override;
+ auto NativeReviewRequestSupported() -> bool override;
protected:
void DoPushMainThreadRunnable(Runnable* runnable) override;
@@ -42,16 +56,22 @@ class AppAdapterApple : public AppAdapter {
auto HasHardwareCursor() -> bool override;
void SetHardwareCursorVisible(bool visible) override;
void TerminateApp() override;
+ void ApplyGraphicsSettings(const GraphicsSettings* settings) override;
+ auto DoClipboardIsSupported() -> bool override;
+ auto DoClipboardHasText() -> bool override;
+ void DoClipboardSetText(const std::string& text) override;
+ auto DoClipboardGetText() -> std::string override;
+ void DoNativeReviewRequest() override;
private:
- void UpdateScreenSizes_();
class ScopedAllowGraphics_;
- void SetScreen_(TextureQualityRequest texture_quality_requested,
- GraphicsQualityRequest graphics_quality_requested);
- void ReloadRenderer_(GraphicsQualityRequest graphics_quality_requested,
- TextureQualityRequest texture_quality_requested);
+
+ void ReloadRenderer_(const GraphicsSettings* settings);
+
std::thread::id graphics_thread_{};
- bool graphics_allowed_;
+ bool graphics_allowed_{};
+ uint8_t resize_friendly_frames_{};
+ Vector2f resize_target_resolution_{-1.0f, -1.0f};
std::mutex graphics_calls_mutex_;
std::vector graphics_calls_;
};
diff --git a/src/ballistica/base/app_adapter/app_adapter_headless.cc b/src/ballistica/base/app_adapter/app_adapter_headless.cc
index 2b8339bb..bc313f33 100644
--- a/src/ballistica/base/app_adapter/app_adapter_headless.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_headless.cc
@@ -4,6 +4,7 @@
#include "ballistica/base/app_adapter/app_adapter_headless.h"
#include "ballistica/base/graphics/graphics_server.h"
+#include "ballistica/base/graphics/support/graphics_client_context.h"
#include "ballistica/shared/ballistica.h"
namespace ballistica::base {
@@ -19,12 +20,7 @@ void AppAdapterHeadless::OnMainThreadStartApp() {
new EventLoop(EventLoopID::kMain, ThreadSource::kWrapCurrent);
}
-void AppAdapterHeadless::DoApplyAppConfig() {
- // Normal graphical app-adapters kick off screen creation here
- // which then leads to remaining app bootstrapping. We create
- // a 'null' screen purely for the same effect.
- PushMainThreadCall([] { g_base->graphics_server->SetNullGraphics(); });
-}
+void AppAdapterHeadless::DoApplyAppConfig() {}
void AppAdapterHeadless::RunMainThreadEventLoopToCompletion() {
assert(g_core->InMainThread());
@@ -40,6 +36,11 @@ void AppAdapterHeadless::DoExitMainThreadEventLoop() {
main_event_loop_->Exit();
}
+auto AppAdapterHeadless::GetGraphicsClientContext() -> GraphicsClientContext* {
+ // Special dummy form.
+ return new GraphicsClientContext(0);
+}
+
} // namespace ballistica::base
#endif // BA_HEADLESS_BUILD
diff --git a/src/ballistica/base/app_adapter/app_adapter_headless.h b/src/ballistica/base/app_adapter/app_adapter_headless.h
index 6d687bb2..2df675ea 100644
--- a/src/ballistica/base/app_adapter/app_adapter_headless.h
+++ b/src/ballistica/base/app_adapter/app_adapter_headless.h
@@ -17,6 +17,8 @@ class AppAdapterHeadless : public AppAdapter {
void DoApplyAppConfig() override;
+ auto GetGraphicsClientContext() -> GraphicsClientContext* override;
+
protected:
void DoPushMainThreadRunnable(Runnable* runnable) override;
void RunMainThreadEventLoopToCompletion() override;
diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.cc b/src/ballistica/base/app_adapter/app_adapter_sdl.cc
index 09782caa..5bc2ed08 100644
--- a/src/ballistica/base/app_adapter/app_adapter_sdl.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_sdl.cc
@@ -51,6 +51,8 @@ AppAdapterSDL::AppAdapterSDL() {
}
void AppAdapterSDL::OnMainThreadStartApp() {
+ AppAdapter::OnMainThreadStartApp();
+
// App is starting. Let's fire up the ol' SDL.
uint32_t sdl_flags{SDL_INIT_VIDEO | SDL_INIT_JOYSTICK};
@@ -59,6 +61,7 @@ void AppAdapterSDL::OnMainThreadStartApp() {
"AppAdapterSDL strict_graphics_context_ is enabled."
" Remember to turn this off.");
}
+
// We may or may not want xinput on windows.
if (g_buildconfig.ostype_windows()) {
if (!g_core->platform->GetLowLevelConfigValue("enablexinput", 1)) {
@@ -66,6 +69,9 @@ void AppAdapterSDL::OnMainThreadStartApp() {
}
}
+ // We wrangle our own signal handling; don't bring SDL into it.
+ SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1");
+
int result = SDL_Init(sdl_flags);
if (result < 0) {
FatalError(std::string("SDL_Init failed: ") + SDL_GetError());
@@ -75,9 +81,8 @@ void AppAdapterSDL::OnMainThreadStartApp() {
sdl_runnable_event_id_ = SDL_RegisterEvents(1);
assert(sdl_runnable_event_id_ != (uint32_t)-1);
- // Note: parent class can add some input devices so need to bring up sdl
- // before we let it run. That code should maybe be relocated/refactored.
- AppAdapter::OnMainThreadStartApp();
+ // SDL builds just assume keyboard input is available.
+ g_base->input->PushCreateKeyboardInputDevices();
if (g_buildconfig.enable_sdl_joysticks()) {
// We want events from joysticks.
@@ -96,36 +101,111 @@ void AppAdapterSDL::OnMainThreadStartApp() {
}
}
- // We currently use a software cursor, so hide the system one.
+ // This adapter draws a software cursor; hide the actual OS one.
SDL_ShowCursor(SDL_DISABLE);
}
-void AppAdapterSDL::DoApplyAppConfig() {
- assert(g_base->InLogicThread());
-
- g_base->graphics_server->PushSetScreenPixelScaleCall(
- g_base->app_config->Resolve(AppConfig::FloatID::kScreenPixelScale));
-
- auto graphics_quality_requested =
- g_base->graphics->GraphicsQualityFromAppConfig();
-
- auto texture_quality_requested =
- g_base->graphics->TextureQualityFromAppConfig();
-
- // Android res string.
- // std::string android_res =
- // g_base->app_config->Resolve(AppConfig::StringID::kResolutionAndroid);
-
+/// Our particular flavor of graphics settings.
+struct AppAdapterSDL::GraphicsSettings_ : public GraphicsSettings {
bool fullscreen = g_base->app_config->Resolve(AppConfig::BoolID::kFullscreen);
-
- auto vsync = g_base->graphics->VSyncFromAppConfig();
+ VSyncRequest vsync = g_base->graphics->VSyncFromAppConfig();
int max_fps = g_base->app_config->Resolve(AppConfig::IntID::kMaxFPS);
+};
- // Tell the main thread to set up the screen with these settings.
- g_base->app_adapter->PushMainThreadCall([=] {
- SetScreen_(fullscreen, max_fps, vsync, texture_quality_requested,
- graphics_quality_requested);
- });
+auto AppAdapterSDL::GetGraphicsSettings() -> GraphicsSettings* {
+ assert(g_base->InLogicThread());
+ return new GraphicsSettings_();
+}
+
+void AppAdapterSDL::ApplyGraphicsSettings(
+ const GraphicsSettings* settings_base) {
+ assert(g_core->InMainThread());
+ assert(!g_core->HeadlessMode());
+
+ // In strict mode, allow graphics stuff while in here.
+ auto allow = ScopedAllowGraphics_(this);
+
+ // Settings will always be our subclass (since we created it).
+ auto* settings = static_cast(settings_base);
+
+ // Apply any changes.
+ bool do_toggle_fs{};
+ bool do_set_existing_fullscreen{};
+
+ auto* graphics_server = g_base->graphics_server;
+
+ // We need a full renderer reload if quality values have changed
+ // or if we don't have a renderer yet.
+ bool need_full_reload = ((sdl_window_ == nullptr
+ || graphics_server->texture_quality_requested()
+ != settings->texture_quality)
+ || (graphics_server->graphics_quality_requested()
+ != settings->graphics_quality));
+
+ if (need_full_reload) {
+ ReloadRenderer_(settings);
+ } else if (settings->fullscreen != fullscreen_) {
+ SDL_SetWindowFullscreen(
+ sdl_window_, settings->fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
+ fullscreen_ = settings->fullscreen;
+ }
+
+ // VSync always gets set independent of the screen (though we set it down
+ // here to make sure we have a screen when its set).
+ VSync vsync;
+ switch (settings->vsync) {
+ case VSyncRequest::kNever:
+ vsync = VSync::kNever;
+ break;
+ case VSyncRequest::kAlways:
+ vsync = VSync::kAlways;
+ break;
+ case VSyncRequest::kAuto:
+ vsync = VSync::kAdaptive;
+ break;
+ default:
+ vsync = VSync::kNever;
+ break;
+ }
+ if (vsync != vsync_) {
+ switch (vsync) {
+ case VSync::kUnset:
+ case VSync::kNever: {
+ SDL_GL_SetSwapInterval(0);
+ vsync_actually_enabled_ = false;
+ break;
+ }
+ case VSync::kAlways: {
+ SDL_GL_SetSwapInterval(1);
+ vsync_actually_enabled_ = true;
+ break;
+ }
+ case VSync::kAdaptive: {
+ // In this case, let's try setting to 'adaptive' and turn it off if
+ // that is unsupported.
+ auto result = SDL_GL_SetSwapInterval(-1);
+ if (result == 0) {
+ vsync_actually_enabled_ = true;
+ } else {
+ SDL_GL_SetSwapInterval(0);
+ vsync_actually_enabled_ = false;
+ }
+ break;
+ }
+ }
+ vsync_ = vsync;
+ }
+
+ // This we can set anytime. Probably could have just set it from the logic
+ // thread where we read it, but let's be pedantic and keep everything to
+ // the main thread.
+ max_fps_ = settings->max_fps;
+
+ // Take -1 to mean no max. Otherwise clamp to a reasonable range.
+ if (max_fps_ != -1) {
+ max_fps_ = std::max(10, max_fps_);
+ max_fps_ = std::min(99999, max_fps_);
+ }
}
void AppAdapterSDL::RunMainThreadEventLoopToCompletion() {
@@ -154,7 +234,7 @@ void AppAdapterSDL::RunMainThreadEventLoopToCompletion() {
auto AppAdapterSDL::TryRender() -> bool {
if (strict_graphics_context_) {
- // In strict mode, allow graphics stuff in here. Normally we allow it
+ // In strict mode, allow graphics stuff in here. Otherwise we allow it
// anywhere in the main thread.
auto allow = ScopedAllowGraphics_(this);
@@ -175,7 +255,7 @@ auto AppAdapterSDL::TryRender() -> bool {
// Lastly render.
return g_base->graphics_server->TryRender();
} else {
- // Simple path; just render.
+ // Simpler path; just render.
return g_base->graphics_server->TryRender();
}
}
@@ -184,7 +264,7 @@ void AppAdapterSDL::SleepUntilNextEventCycle_(microsecs_t cycle_start_time) {
// Special case: if we're hidden, we simply sleep for a long bit; no fancy
// timing.
if (hidden_) {
- g_core->platform->SleepMillisecs(100);
+ g_core->platform->SleepSeconds(0.1);
return;
}
@@ -328,7 +408,9 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
}
case SDL_KEYDOWN: {
- g_base->input->PushKeyPressEvent(event.key.keysym);
+ if (!event.key.repeat) {
+ g_base->input->PushKeyPressEvent(event.key.keysym);
+ }
break;
}
@@ -355,12 +437,14 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
break;
case SDL_QUIT:
- if (g_core->GetAppTimeMillisecs() - last_windowevent_close_time_ < 100) {
+ if (g_core->GetAppTimeSeconds() - last_windowevent_close_time_ < 0.1) {
// If they hit the window close button, skip the confirm.
g_base->QuitApp(false);
} else {
- // By default, confirm before quitting.
- g_base->QuitApp(true);
+ // For all other quits we might want to default to a confirm dialog.
+ // Update: going to try without confirm for a bit and see how that
+ // feels.
+ g_base->QuitApp(false);
}
break;
@@ -374,7 +458,7 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
case SDL_WINDOWEVENT_CLOSE: {
// Simply note that this happened. We use this to adjust our
// SDL_QUIT behavior (quit is called right after this).
- last_windowevent_close_time_ = g_core->GetAppTimeMillisecs();
+ last_windowevent_close_time_ = g_core->GetAppTimeSeconds();
break;
}
@@ -387,10 +471,13 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
// it to the config so that UIs can poll for it and pick up the
// change. We don't do this on other platforms where a maximized
// window is more distinctly different than a fullscreen one.
+ // Though I guess some Linux window managers have a fullscreen
+ // function so theoretically we should there. Le sigh. Maybe SDL
+ // 3 will tidy up this situation.
fullscreen_ = true;
g_base->logic->event_loop()->PushCall([] {
g_base->python->objs()
- .Get(BasePython::ObjID::kSetConfigFullscreenOnCall)
+ .Get(BasePython::ObjID::kStoreConfigFullscreenOnCall)
.Call();
});
}
@@ -403,7 +490,7 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
fullscreen_ = false;
g_base->logic->event_loop()->PushCall([] {
g_base->python->objs()
- .Get(BasePython::ObjID::kSetConfigFullscreenOffCall)
+ .Get(BasePython::ObjID::kStoreConfigFullscreenOffCall)
.Call();
});
}
@@ -413,18 +500,22 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
break;
case SDL_WINDOWEVENT_HIDDEN: {
- // Let's keep track of when we're hidden so we can stop drawing
- // and sleep more. Theoretically we could put the app into a full
- // suspended state like we do on mobile (pausing event loops/etc.)
- // but that would be more involved; we'd need to ignore most SDL
- // events while sleeping (except for SDL_WINDOWEVENT_SHOWN) and
- // would need to rebuild our controller lists/etc when we resume.
- // For now just gonna keep things simple and keep running.
+ // We plug this into the app's overall 'Active' state so it can
+ // pause stuff or throttle down processing or whatever else.
+ if (!hidden_) {
+ g_base->SetAppActive(false);
+ }
+ // Also note that we are *completely* hidden, so we can totally
+ // stop drawing ('Inactive' app state does not imply this in and
+ // of itself).
hidden_ = true;
break;
}
case SDL_WINDOWEVENT_SHOWN: {
+ if (hidden_) {
+ g_base->SetAppActive(true);
+ }
hidden_ = false;
break;
}
@@ -540,114 +631,7 @@ auto AppAdapterSDL::GetSDLJoystickInput_(int sdl_joystick_id) const
return nullptr; // Epic fail.
}
-void AppAdapterSDL::SetScreen_(
- bool fullscreen, int max_fps, VSyncRequest vsync_requested,
- TextureQualityRequest texture_quality_requested,
- GraphicsQualityRequest graphics_quality_requested) {
- assert(g_core->InMainThread());
- assert(!g_core->HeadlessMode());
-
- // In strict mode, allow graphics stuff in here.
- auto allow = ScopedAllowGraphics_(this);
-
- // If we know what we support, filter our request types to what is
- // supported. This will keep us from rebuilding contexts if request type
- // is flipping between different types that we don't support.
- if (g_base->graphics->has_supports_high_quality_graphics_value()) {
- if (!g_base->graphics->supports_high_quality_graphics()
- && graphics_quality_requested > GraphicsQualityRequest::kMedium) {
- graphics_quality_requested = GraphicsQualityRequest::kMedium;
- }
- }
-
- bool do_toggle_fs{};
- bool do_set_existing_fullscreen{};
-
- auto* gs = g_base->graphics_server;
-
- // We need a full renderer reload if quality values have changed
- // or if we don't have one yet.
- bool need_full_reload =
- ((sdl_window_ == nullptr
- || gs->texture_quality_requested() != texture_quality_requested)
- || (gs->graphics_quality_requested() != graphics_quality_requested)
- || !gs->texture_quality_set() || !gs->graphics_quality_set());
-
- if (need_full_reload) {
- ReloadRenderer_(fullscreen, graphics_quality_requested,
- texture_quality_requested);
- } else if (fullscreen != fullscreen_) {
- SDL_SetWindowFullscreen(sdl_window_,
- fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
- fullscreen_ = fullscreen;
- }
-
- // VSync always gets set independent of the screen (though we set it down
- // here to make sure we have a screen when its set).
- VSync vsync;
- switch (vsync_requested) {
- case VSyncRequest::kNever:
- vsync = VSync::kNever;
- break;
- case VSyncRequest::kAlways:
- vsync = VSync::kAlways;
- break;
- case VSyncRequest::kAuto:
- vsync = VSync::kAdaptive;
- break;
- default:
- vsync = VSync::kNever;
- break;
- }
- if (vsync != vsync_) {
- switch (vsync) {
- case VSync::kUnset:
- case VSync::kNever: {
- SDL_GL_SetSwapInterval(0);
- vsync_actually_enabled_ = false;
- break;
- }
- case VSync::kAlways: {
- SDL_GL_SetSwapInterval(1);
- vsync_actually_enabled_ = true;
- break;
- }
- case VSync::kAdaptive: {
- // In this case, let's try setting to 'adaptive' and turn it off if
- // that is unsupported.
- auto result = SDL_GL_SetSwapInterval(-1);
- if (result == 0) {
- vsync_actually_enabled_ = true;
- } else {
- SDL_GL_SetSwapInterval(0);
- vsync_actually_enabled_ = false;
- }
- break;
- }
- }
- vsync_ = vsync;
- }
-
- // This we can set anytime. Probably could have just set it from the logic
- // thread where we read it, but let's be pedantic and keep everything to
- // the main thread.
- max_fps_ = max_fps;
-
- // Take -1 to mean no max. Otherwise clamp to a reasonable range.
- if (max_fps_ != -1) {
- max_fps_ = std::max(10, max_fps_);
- max_fps_ = std::min(99999, max_fps_);
- }
-
- // Let the logic thread know we've got a graphics system up and running.
- // It may use this cue to kick off asset loads and other bootstrapping.
- g_base->logic->event_loop()->PushCall(
- [] { g_base->logic->OnGraphicsReady(); });
-}
-
-void AppAdapterSDL::ReloadRenderer_(
- bool fullscreen, GraphicsQualityRequest graphics_quality_requested,
- TextureQualityRequest texture_quality_requested) {
+void AppAdapterSDL::ReloadRenderer_(const GraphicsSettings_* settings) {
assert(g_base->app_adapter->InGraphicsContext());
auto* gs = g_base->graphics_server;
@@ -658,7 +642,7 @@ void AppAdapterSDL::ReloadRenderer_(
// If we don't haven't yet, create our window and renderer.
if (!sdl_window_) {
- fullscreen_ = fullscreen;
+ fullscreen_ = settings->fullscreen;
// A reasonable default window size.
auto width = static_cast(kBaseVirtualResX * 0.8f);
@@ -666,7 +650,7 @@ void AppAdapterSDL::ReloadRenderer_(
uint32_t flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
| SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE;
- if (fullscreen) {
+ if (settings->fullscreen) {
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
}
@@ -723,15 +707,16 @@ void AppAdapterSDL::ReloadRenderer_(
}
}
- // Update graphics quality based on request.
- gs->set_graphics_quality_requested(graphics_quality_requested);
- gs->set_texture_quality_requested(texture_quality_requested);
+ // Update graphics-server's qualities based on request.
+ gs->set_graphics_quality_requested(settings->graphics_quality);
+ gs->set_texture_quality_requested(settings->texture_quality);
gs->LoadRenderer();
}
void AppAdapterSDL::UpdateScreenSizes_() {
- assert(g_base->app_adapter->InGraphicsContext());
+ // This runs in the main thread in response to SDL events.
+ assert(g_core->InMainThread());
// Grab logical window dimensions (points?). This is the coordinate space
// SDL's events deal in.
@@ -743,11 +728,15 @@ void AppAdapterSDL::UpdateScreenSizes_() {
// dimensions.
int pixels_x, pixels_y;
SDL_GL_GetDrawableSize(sdl_window_, &pixels_x, &pixels_y);
- g_base->graphics_server->SetScreenResolution(static_cast(pixels_x),
- static_cast(pixels_y));
+
+ // Push this over to the logic thread which owns the canonical value
+ // for this.
+ g_base->logic->event_loop()->PushCall([pixels_x, pixels_y] {
+ g_base->graphics->SetScreenResolution(static_cast(pixels_x),
+ static_cast(pixels_y));
+ });
}
-/// As a default, allow graphics stuff in the main thread.
auto AppAdapterSDL::InGraphicsContext() -> bool {
// In strict mode, make sure we're in the right thread *and* within our
// render call.
@@ -815,10 +804,48 @@ void AppAdapterSDL::CursorPositionForDraw(float* x, float* y) {
*y = immediate_y;
}
-auto AppAdapterSDL::CanToggleFullscreen() -> bool const { return true; }
+auto AppAdapterSDL::FullscreenControlAvailable() const -> bool { return true; }
+auto AppAdapterSDL::FullscreenControlKeyShortcut() const
+ -> std::optional {
+ // On our SDL build we support F11 and Alt+Enter to toggle fullscreen.
+ // Let's mention Alt+Enter which seems like it might be more commonly used
+ return "Alt+Enter";
+};
+
auto AppAdapterSDL::SupportsVSync() -> bool const { return true; }
auto AppAdapterSDL::SupportsMaxFPS() -> bool const { return true; }
+auto AppAdapterSDL::HasDirectKeyboardInput() -> bool {
+ // We always provide direct keyboard events.
+ return true;
+}
+
+auto AppAdapterSDL::DoClipboardIsSupported() -> bool { return true; }
+
+auto AppAdapterSDL::DoClipboardHasText() -> bool {
+ return SDL_HasClipboardText();
+}
+
+void AppAdapterSDL::DoClipboardSetText(const std::string& text) {
+ SDL_SetClipboardText(text.c_str());
+}
+
+auto AppAdapterSDL::DoClipboardGetText() -> std::string {
+ // Go through SDL functionality on SDL based platforms;
+ // otherwise default to no clipboard.
+ char* out = SDL_GetClipboardText();
+ if (out == nullptr) {
+ throw Exception("Error fetching clipboard contents.", PyExcType::kRuntime);
+ }
+ std::string out_s{out};
+ SDL_free(out);
+ return out_s;
+}
+
+auto AppAdapterSDL::GetKeyName(int keycode) -> std::string {
+ return SDL_GetKeyName(static_cast(keycode));
+}
+
} // namespace ballistica::base
#endif // BA_SDL_BUILD
diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.h b/src/ballistica/base/app_adapter/app_adapter_sdl.h
index cc0f0405..2f9b6f29 100644
--- a/src/ballistica/base/app_adapter/app_adapter_sdl.h
+++ b/src/ballistica/base/app_adapter/app_adapter_sdl.h
@@ -30,14 +30,22 @@ class AppAdapterSDL : public AppAdapter {
AppAdapterSDL();
void OnMainThreadStartApp() override;
- void DoApplyAppConfig() override;
auto TryRender() -> bool;
- auto CanToggleFullscreen() -> bool const override;
+ auto FullscreenControlAvailable() const -> bool override;
+ auto FullscreenControlKeyShortcut() const
+ -> std::optional override;
auto SupportsVSync() -> bool const override;
auto SupportsMaxFPS() -> bool const override;
+ auto HasDirectKeyboardInput() -> bool override;
+ void ApplyGraphicsSettings(const GraphicsSettings* settings) override;
+
+ auto GetGraphicsSettings() -> GraphicsSettings* override;
+
+ auto GetKeyName(int keycode) -> std::string override;
+
protected:
void DoPushMainThreadRunnable(Runnable* runnable) override;
void RunMainThreadEventLoopToCompletion() override;
@@ -45,17 +53,18 @@ class AppAdapterSDL : public AppAdapter {
auto InGraphicsContext() -> bool override;
void DoPushGraphicsContextRunnable(Runnable* runnable) override;
void CursorPositionForDraw(float* x, float* y) override;
+ auto DoClipboardIsSupported() -> bool override;
+ auto DoClipboardHasText() -> bool override;
+ void DoClipboardSetText(const std::string& text) override;
+ auto DoClipboardGetText() -> std::string override;
private:
class ScopedAllowGraphics_;
- void SetScreen_(bool fullscreen, int max_fps, VSyncRequest vsync_requested,
- TextureQualityRequest texture_quality_requested,
- GraphicsQualityRequest graphics_quality_requested);
+ struct GraphicsSettings_;
+
void HandleSDLEvent_(const SDL_Event& event);
void UpdateScreenSizes_();
- void ReloadRenderer_(bool fullscreen,
- GraphicsQualityRequest graphics_quality_requested,
- TextureQualityRequest texture_quality_requested);
+ void ReloadRenderer_(const GraphicsSettings_* settings);
void OnSDLJoystickAdded_(int index);
void OnSDLJoystickRemoved_(int index);
// Given an SDL joystick ID, returns our Ballistica input for it.
@@ -66,11 +75,12 @@ class AppAdapterSDL : public AppAdapter {
void RemoveSDLInputDevice_(int index);
void SleepUntilNextEventCycle_(microsecs_t cycle_start_time);
- bool done_ : 1 {};
- bool fullscreen_ : 1 {};
- bool vsync_actually_enabled_ : 1 {};
- bool debug_log_sdl_frame_timing_ : 1 {};
- bool hidden_ : 1 {};
+ int max_fps_{60};
+ bool done_{};
+ bool fullscreen_{};
+ bool vsync_actually_enabled_{};
+ bool debug_log_sdl_frame_timing_{};
+ bool hidden_{};
/// With this off, graphics call pushes simply get pushed to the main
/// thread and graphics code is allowed to run any time in the main
@@ -79,19 +89,18 @@ class AppAdapterSDL : public AppAdapter {
/// allowed during draws. This strictness is generally not needed here but
/// can be useful to test with, as it more closely matches other platforms
/// that require such a setup.
- bool strict_graphics_context_ : 1 {};
- bool strict_graphics_allowed_ : 1 {};
- std::mutex strict_graphics_calls_mutex_;
- std::vector strict_graphics_calls_;
+ bool strict_graphics_context_{};
+ bool strict_graphics_allowed_{};
VSync vsync_{VSync::kUnset};
uint32_t sdl_runnable_event_id_{};
- int max_fps_{60};
+ std::mutex strict_graphics_calls_mutex_;
+ std::vector strict_graphics_calls_;
microsecs_t oversleep_{};
std::vector sdl_joysticks_;
Vector2f window_size_{1.0f, 1.0f};
SDL_Window* sdl_window_{};
void* sdl_gl_context_{};
- millisecs_t last_windowevent_close_time_{};
+ seconds_t last_windowevent_close_time_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/app_adapter/app_adapter_vr.cc b/src/ballistica/base/app_adapter/app_adapter_vr.cc
index bfa614cd..703dbca2 100644
--- a/src/ballistica/base/app_adapter/app_adapter_vr.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_vr.cc
@@ -40,7 +40,8 @@ void AppAdapterVR::PushVRSimpleRemoteStateCall(
}
void AppAdapterVR::VRSetDrawDimensions(int w, int h) {
- g_base->graphics_server->SetScreenResolution(w, h);
+ FatalError("FIXME UPDATE SET-SCREEN-RESOLUTION");
+ // g_base->graphics_server->SetScreenResolution(w, h);
}
void AppAdapterVR::VRPreDraw() {
diff --git a/src/ballistica/base/app_mode/app_mode.cc b/src/ballistica/base/app_mode/app_mode.cc
index 268f2f52..4f1650fc 100644
--- a/src/ballistica/base/app_mode/app_mode.cc
+++ b/src/ballistica/base/app_mode/app_mode.cc
@@ -3,6 +3,7 @@
#include "ballistica/base/app_mode/app_mode.h"
#include "ballistica/base/input/device/input_device_delegate.h"
+#include "ballistica/base/logic/logic.h"
#include "ballistica/base/support/context.h"
namespace ballistica::base {
@@ -13,8 +14,8 @@ void AppMode::OnActivate() {}
void AppMode::OnDeactivate() {}
void AppMode::OnAppStart() {}
-void AppMode::OnAppPause() {}
-void AppMode::OnAppResume() {}
+void AppMode::OnAppSuspend() {}
+void AppMode::OnAppUnsuspend() {}
void AppMode::OnAppShutdown() {}
void AppMode::OnAppShutdownComplete() {}
@@ -35,16 +36,14 @@ void AppMode::HandleGameQuery(const char* buffer, size_t size,
auto AppMode::DoesWorldFillScreen() -> bool { return false; }
-void AppMode::GraphicsQualityChanged(GraphicsQuality quality) {}
-
void AppMode::DrawWorld(FrameDef* frame_def) {}
void AppMode::ChangeGameSpeed(int offs) {}
void AppMode::StepDisplayTime() {}
-auto AppMode::GetHeadlessDisplayStep() -> microsecs_t {
- return kAppModeMaxHeadlessDisplayStep;
+auto AppMode::GetHeadlessNextDisplayTimeStep() -> microsecs_t {
+ return kHeadlessMaxDisplayTimeStep;
}
auto AppMode::GetPartySize() const -> int { return 0; }
diff --git a/src/ballistica/base/app_mode/app_mode.h b/src/ballistica/base/app_mode/app_mode.h
index b98fa2a4..7f981b5a 100644
--- a/src/ballistica/base/app_mode/app_mode.h
+++ b/src/ballistica/base/app_mode/app_mode.h
@@ -9,15 +9,6 @@
namespace ballistica::base {
-/// The max amount of time a headless app can sleep if no events are pending.
-/// This should not be *too* high or it might cause delays when going from
-/// no events present to events present.
-const microsecs_t kAppModeMaxHeadlessDisplayStep{500000};
-
-/// The min amount of time a headless app can sleep. This provides an upper
-/// limit on stepping overhead in cases where events are densely packed.
-const microsecs_t kAppModeMinHeadlessDisplayStep{1000};
-
/// Represents 'what the app is doing'. The global app-mode can be switched
/// as the app is running. The Python layer has its own Python AppMode
/// classes, and generally when one of them becomes active it calls down
@@ -35,8 +26,8 @@ class AppMode {
/// Logic thread callbacks that run while the app-mode is active.
virtual void OnAppStart();
- virtual void OnAppPause();
- virtual void OnAppResume();
+ virtual void OnAppSuspend();
+ virtual void OnAppUnsuspend();
virtual void OnAppShutdown();
virtual void OnAppShutdownComplete();
virtual void DoApplyAppConfig();
@@ -51,9 +42,9 @@ class AppMode {
/// Called right after stepping; should return the exact microseconds
/// between the current display time and the next event the app-mode has
/// scheduled. If no events are pending, should return
- /// kAppModeMaxHeadlessDisplayStep. This will only be called on headless
+ /// kHeadlessMaxDisplayTimeStep. This will only be called on headless
/// builds.
- virtual auto GetHeadlessDisplayStep() -> microsecs_t;
+ virtual auto GetHeadlessNextDisplayTimeStep() -> microsecs_t;
/// Create a delegate for an input-device.
/// Return a raw pointer allocated using Object::NewDeferred.
@@ -71,8 +62,6 @@ class AppMode {
virtual void DrawWorld(FrameDef* frame_def);
- virtual void GraphicsQualityChanged(GraphicsQuality quality);
-
/// Called whenever screen size changes.
virtual void OnScreenSizeChange();
diff --git a/src/ballistica/base/assets/assets.cc b/src/ballistica/base/assets/assets.cc
index a8da1408..1fe097d1 100644
--- a/src/ballistica/base/assets/assets.cc
+++ b/src/ballistica/base/assets/assets.cc
@@ -82,10 +82,10 @@ void Assets::StartLoading() {
assert(g_base);
assert(g_base->audio_server && g_base->assets_server
&& g_base->graphics_server);
- assert(g_base->graphics_server->texture_compression_types_are_set());
- assert(g_base->graphics_server->texture_quality_set());
+ assert(g_base->graphics->has_client_context());
- assert(!asset_loads_allowed_); // We should only be called once.
+ // We should only be called once.
+ assert(!asset_loads_allowed_);
asset_loads_allowed_ = true;
// Just grab the lock once for all this stuff for efficiency.
@@ -1102,10 +1102,13 @@ auto Assets::FindAssetFile(FileType type, const std::string& name)
}
}
- assert(g_base->graphics_server
- && g_base->graphics_server->texture_compression_types_are_set());
- assert(g_base->graphics_server
- && g_base->graphics_server->texture_quality_set());
+ // Make sure we know what compression/quality to use.
+ assert(g_base->graphics->has_client_context());
+ // assert(g_base->graphics_server
+ // &&
+ // g_base->graphics_server->texture_compression_types_are_set());
+ // assert(g_base->graphics_server
+ // && g_base->graphics_server->texture_quality_set());
prefix = "textures/";
#if BA_OSTYPE_ANDROID && !BA_ANDROID_DDS_BUILD
diff --git a/src/ballistica/base/assets/assets.h b/src/ballistica/base/assets/assets.h
index 6db00287..8fa05722 100644
--- a/src/ballistica/base/assets/assets.h
+++ b/src/ballistica/base/assets/assets.h
@@ -117,6 +117,8 @@ class Assets {
auto language_state() const { return language_state_; }
+ auto asset_loads_allowed() const { return asset_loads_allowed_; }
+
private:
static void MarkAssetForLoad(Asset* c);
void LoadSystemTexture(SysTextureID id, const char* name);
@@ -136,20 +138,21 @@ class Assets {
std::unordered_map >* c_list)
-> Object::Ref;
- std::vector asset_paths_;
+ int language_state_{};
bool have_pending_loads_[static_cast(AssetType::kLast)]{};
+
+ // Will be true while a AssetListLock exists. Good to debug-verify this
+ // during any asset list access.
+ bool asset_lists_locked_{};
+ bool asset_loads_allowed_{};
+ bool sys_assets_loaded_{};
+
+ std::vector asset_paths_;
std::unordered_map packages_;
// For use by AssetListLock; don't manually acquire.
std::mutex asset_lists_mutex_;
- // Will be true while a AssetListLock exists. Good to debug-verify this
- // during any asset list access.
- bool asset_lists_locked_{};
-
- // 'hard-wired' internal assets
- bool asset_loads_allowed_{};
- bool sys_assets_loaded_{};
std::vector > system_textures_;
std::vector > system_cube_map_textures_;
std::vector > system_sounds_;
@@ -177,7 +180,6 @@ class Assets {
// Text & Language (need to mold this into more asset-like concepts).
std::mutex language_mutex_;
std::unordered_map language_;
- int language_state_{};
std::mutex special_char_mutex_;
std::unordered_map special_char_strings_;
};
diff --git a/src/ballistica/base/assets/assets_server.cc b/src/ballistica/base/assets/assets_server.cc
index e5352ddb..aaf27ba5 100644
--- a/src/ballistica/base/assets/assets_server.cc
+++ b/src/ballistica/base/assets/assets_server.cc
@@ -4,7 +4,7 @@
#include "ballistica/base/assets/asset.h"
#include "ballistica/base/assets/assets.h"
-#include "ballistica/base/graphics/graphics_server.h"
+#include "ballistica/base/graphics/graphics.h"
#include "ballistica/base/support/huffman.h"
#include "ballistica/shared/foundation/event_loop.h"
@@ -25,7 +25,7 @@ void AssetsServer::OnAppStartInThread() {
// Ask our thread to give us periodic processing time (close to but
// not *exactly* one second; try to avoid aliasing with similar updates).
process_timer_ = event_loop()->NewTimer(
- 987, true, NewLambdaRunnable([this] { Process(); }));
+ 987 * 1000, true, NewLambdaRunnable([this] { Process(); }).Get());
}
void AssetsServer::PushPendingPreload(Object::Ref* asset_ref_ptr) {
@@ -221,12 +221,18 @@ void AssetsServer::WriteReplayMessages() {
void AssetsServer::Process() {
// Make sure we don't do any loading until we know what kind/quality of
// textures we'll be loading.
- if (!g_base->assets || !g_base->graphics_server
- || !g_base->graphics_server
- ->texture_compression_types_are_set() // NOLINT
- || !g_base->graphics_server->texture_quality_set()) {
+
+ // FIXME - we'll need to revisit this when adding support for
+ // renderer switches, since this is not especially thread-safe.
+
+ if (!g_base->graphics->has_client_context()) {
return;
}
+ // if (!g_base->assets ||
+ // || !g_base->graphics->texture_compression_types_are_set() // NOLINT
+ // || !g_base->graphics_server->texture_quality_set()) {
+ // return;
+ // }
// Process exactly 1 preload item. Empty out our non-audio list first
// (audio is less likely to cause noticeable hitches if it needs to be loaded
@@ -252,7 +258,7 @@ void AssetsServer::Process() {
// we're writing a replay.. otherwise just sleep indefinitely.
if (pending_preloads_.empty() && pending_preloads_audio_.empty()) {
if (writing_replay_) {
- process_timer_->SetLength(1000);
+ process_timer_->SetLength(1000 * 1000);
} else {
process_timer_->SetLength(-1);
}
diff --git a/src/ballistica/base/assets/texture_asset.cc b/src/ballistica/base/assets/texture_asset.cc
index b43c13bb..8198a4d9 100644
--- a/src/ballistica/base/assets/texture_asset.cc
+++ b/src/ballistica/base/assets/texture_asset.cc
@@ -15,7 +15,7 @@
namespace ballistica::base {
-static void rgba8888_unpremultiply_in_place(uint8_t* src, size_t cb) {
+static void Rgba8888UnpremultiplyInPlace_(uint8_t* src, size_t cb) {
// Compute the actual number of pixel elements in the buffer.
size_t cpel = cb / 4;
auto* psrc = src;
@@ -93,11 +93,14 @@ auto TextureAsset::GetNameFull() const -> std::string {
void TextureAsset::DoPreload() {
assert(valid_);
- assert(g_base->graphics_server
- && g_base->graphics_server->texture_compression_types_are_set());
+ // Make sure we're not loading without knowing what texture types we
+ // support.
+ // assert(g_base->graphics->has_client_context());
+ // assert(g_base->graphics_server
+ // && g_base->graphics_server->texture_compression_types_are_set());
- // We figure out which LOD should be our base level based on quality.
- TextureQuality texture_quality = g_base->graphics_server->texture_quality();
+ // Figure out which LOD should be our base level based on texture quality.
+ auto texture_quality = g_base->graphics->placeholder_texture_quality();
// If we're a text-texture.
if (packer_.Exists()) {
@@ -154,7 +157,7 @@ void TextureAsset::DoPreload() {
auto* buffer = static_cast(malloc(buffer_size));
preload_datas_[0].buffers[0] = buffer;
memcpy(buffer, pixels, buffer_size);
- rgba8888_unpremultiply_in_place(buffer, buffer_size);
+ Rgba8888UnpremultiplyInPlace_(buffer, buffer_size);
preload_datas_[0].widths[0] = width;
preload_datas_[0].heights[0] = height;
preload_datas_[0].formats[0] = TextureFormat::kRGBA_8888;
@@ -218,12 +221,14 @@ void TextureAsset::DoPreload() {
&preload_datas_[0].base_level);
// We should only be loading this if we support etc1 in hardware.
- assert(g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC1));
+ assert(g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC1));
// Decompress dxt1/dxt5 ones if we don't natively support S3TC.
- if (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kS3TC)) {
+ if (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kS3TC)) {
if ((preload_datas_[0].formats[preload_datas_[0].base_level]
== TextureFormat::kDXT5)
|| (preload_datas_[0].formats[preload_datas_[0].base_level]
@@ -241,8 +246,9 @@ void TextureAsset::DoPreload() {
&preload_datas_[0].base_level);
// Decompress dxt1/dxt5 if we don't natively support it.
- if (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kS3TC)) {
+ if (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kS3TC)) {
preload_datas_[0].ConvertToUncompressed(this);
}
} else if (!strcmp(file_name_full_.c_str() + file_name_size - 4,
@@ -264,16 +270,18 @@ void TextureAsset::DoPreload() {
== TextureFormat::kETC2_RGB)
|| (preload_datas_[0].formats[preload_datas_[0].base_level]
== TextureFormat::kETC2_RGBA))
- && (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC2))) {
+ && (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC2))) {
preload_datas_[0].ConvertToUncompressed(this);
}
// Decompress etc1 if we don't natively support it.
if ((preload_datas_[0].formats[preload_datas_[0].base_level]
== TextureFormat::kETC1)
- && (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC1))) {
+ && (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC1))) {
preload_datas_[0].ConvertToUncompressed(this);
}
@@ -287,8 +295,9 @@ void TextureAsset::DoPreload() {
&preload_datas_[0].base_level);
// We should only be loading this if we support pvr in hardware.
- assert(g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kPVR));
+ assert(
+ g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(TextureCompressionType::kPVR));
} else if (!strcmp(file_name_full_.c_str() + file_name_size - 4,
".nop")) {
// Dummy path for headless; nothing to do here.
@@ -342,12 +351,14 @@ void TextureAsset::DoPreload() {
}
// We should only be loading this if we support etc1 in hardware.
- assert(g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC1));
+ assert(g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC1));
// Decompress dxt1/dxt5 ones if we don't natively support S3TC.
- if (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kS3TC)) {
+ if (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kS3TC)) {
if ((preload_datas_[d].formats[preload_datas_[d].base_level]
== TextureFormat::kDXT5)
|| (preload_datas_[d].formats[preload_datas_[d].base_level]
@@ -365,8 +376,9 @@ void TextureAsset::DoPreload() {
&preload_datas_[d].base_level);
// Decompress dxt1/dxt5 if we don't natively support it.
- if (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kS3TC)) {
+ if (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kS3TC)) {
preload_datas_[d].ConvertToUncompressed(this);
}
} else if (!strcmp(file_name_full_.c_str() + file_name_size - 4,
@@ -383,16 +395,18 @@ void TextureAsset::DoPreload() {
== TextureFormat::kETC2_RGB)
|| (preload_datas_[d].formats[preload_datas_[d].base_level]
== TextureFormat::kETC2_RGBA))
- && (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC2))) {
+ && (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC2))) {
preload_datas_[d].ConvertToUncompressed(this);
}
// Decompress etc1 if we don't natively support it.
if ((preload_datas_[d].formats[preload_datas_[d].base_level]
== TextureFormat::kETC1)
- && (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC1))) {
+ && (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC1))) {
preload_datas_[d].ConvertToUncompressed(this);
}
diff --git a/src/ballistica/base/audio/al_sys.h b/src/ballistica/base/audio/al_sys.h
index 475af2db..80427913 100644
--- a/src/ballistica/base/audio/al_sys.h
+++ b/src/ballistica/base/audio/al_sys.h
@@ -17,8 +17,16 @@
#include
#endif
-#if BA_OSTYPE_ANDROID
+#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.cc b/src/ballistica/base/audio/audio.cc
index 8971b449..6e039ead 100644
--- a/src/ballistica/base/audio/audio.cc
+++ b/src/ballistica/base/audio/audio.cc
@@ -5,6 +5,7 @@
#include "ballistica/base/assets/sound_asset.h"
#include "ballistica/base/audio/audio_server.h"
#include "ballistica/base/audio/audio_source.h"
+#include "ballistica/base/graphics/graphics.h"
#include "ballistica/base/support/app_config.h"
#include "ballistica/shared/foundation/event_loop.h"
@@ -12,6 +13,19 @@ namespace ballistica::base {
Audio::Audio() = default;
+auto Audio::UseLowQualityAudio() -> bool {
+ assert(g_base->InLogicThread());
+ // Currently just piggybacking off graphics quality here.
+ if (g_core->HeadlessMode() || g_base->graphics->has_client_context()) {
+ return true;
+ }
+ // We don't have a frame-def to look at so need to calc this ourself; ugh.
+ auto quality = Graphics::GraphicsQualityFromRequest(
+ g_base->graphics->settings()->graphics_quality,
+ g_base->graphics->client_context()->auto_graphics_quality);
+ return quality < GraphicsQuality::kMedium;
+}
+
void Audio::Reset() {
assert(g_base->InLogicThread());
g_base->audio_server->PushResetCall();
@@ -19,9 +33,9 @@ void Audio::Reset() {
void Audio::OnAppStart() { assert(g_base->InLogicThread()); }
-void Audio::OnAppPause() { assert(g_base->InLogicThread()); }
+void Audio::OnAppSuspend() { assert(g_base->InLogicThread()); }
-void Audio::OnAppResume() { assert(g_base->InLogicThread()); }
+void Audio::OnAppUnsuspend() { assert(g_base->InLogicThread()); }
void Audio::OnAppShutdown() { assert(g_base->InLogicThread()); }
@@ -101,7 +115,7 @@ auto Audio::SourceBeginNew() -> AudioSource* {
#pragma clang diagnostic pop
auto Audio::IsSoundPlaying(uint32_t play_id) -> bool {
- uint32_t source_id = AudioServer::source_id_from_play_id(play_id);
+ uint32_t source_id = AudioServer::SourceIdFromPlayId(play_id);
assert(client_sources_.size() > source_id);
client_sources_[source_id]->Lock(2);
bool result = (client_sources_[source_id]->play_id() == play_id);
@@ -112,7 +126,7 @@ auto Audio::IsSoundPlaying(uint32_t play_id) -> bool {
auto Audio::SourceBeginExisting(uint32_t play_id, int debug_id)
-> AudioSource* {
BA_DEBUG_FUNCTION_TIMER_BEGIN();
- uint32_t source_id = AudioServer::source_id_from_play_id(play_id);
+ uint32_t source_id = AudioServer::SourceIdFromPlayId(play_id);
// Ok, the audio thread fills in this source list,
// so theoretically a client could call this before the audio thread
@@ -159,7 +173,7 @@ auto Audio::PlaySound(SoundAsset* sound, float volume)
if (s) {
// In vr mode, play non-positional sounds positionally in space roughly
// where the menu is.
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
s->SetGain(volume);
s->SetPositional(true);
float x = 0.0f;
diff --git a/src/ballistica/base/audio/audio.h b/src/ballistica/base/audio/audio.h
index d3126565..af0d646d 100644
--- a/src/ballistica/base/audio/audio.h
+++ b/src/ballistica/base/audio/audio.h
@@ -21,44 +21,49 @@ class Audio {
void Reset();
virtual void OnAppStart();
- virtual void OnAppPause();
- virtual void OnAppResume();
+ virtual void OnAppSuspend();
+ virtual void OnAppUnsuspend();
virtual void OnAppShutdown();
virtual void OnAppShutdownComplete();
virtual void DoApplyAppConfig();
virtual void OnScreenSizeChange();
virtual void StepDisplayTime();
+ /// Can be keyed off of to cut corners in audio (leaving sounds out, etc.)
+ /// Currently just piggybacks off graphics quality settings but this logic
+ /// may get fancier in the future.
+ auto UseLowQualityAudio() -> bool;
+
void SetVolumes(float music_volume, float sound_volume);
void SetListenerPosition(const Vector3f& p);
void SetListenerOrientation(const Vector3f& forward, const Vector3f& up);
void SetSoundPitch(float pitch);
- // Return a pointer to a locked sound source, or nullptr if they're all busy.
- // The sound source will be reset to standard settings (no loop, fade 1, pos
- // 0,0,0, etc.).
- // Send the source any immediate commands and then unlock it.
- // For later modifications, re-retrieve the sound with GetPlayingSound()
+ /// Return a pointer to a locked sound source, or nullptr if they're all busy.
+ /// The sound source will be reset to standard settings (no loop, fade 1, pos
+ /// 0,0,0, etc.).
+ /// Send the source any immediate commands and then unlock it.
+ /// For later modifications, re-retrieve the sound with GetPlayingSound()
auto SourceBeginNew() -> AudioSource*;
- // If a sound play id is playing, locks and returns its sound source.
- // on success, you must unlock the source once done with it.
+ /// If a sound play id is playing, locks and returns its sound source.
+ /// on success, you must unlock the source once done with it.
auto SourceBeginExisting(uint32_t play_id, int debug_id) -> AudioSource*;
- // Return true if the sound id is currently valid. This is not guaranteed
- // to be super accurate, but can be used to determine if a sound is still
- // playing.
+ /// Return true if the sound id is currently valid. This is not guaranteed
+ /// to be super accurate, but can be used to determine if a sound is still
+ /// playing.
auto IsSoundPlaying(uint32_t play_id) -> bool;
- // Simple one-shot play functions.
+ /// Simple one-shot play functions.
auto PlaySound(SoundAsset* s, float volume = 1.0f) -> std::optional;
auto PlaySoundAtPosition(SoundAsset* sound, float volume, float x, float y,
float z) -> std::optional;
- // Call this if you want to prevent repeated plays of the same sound. It'll
- // tell you if the sound has been played recently. The one-shot sound-play
- // functions use this under the hood. (PlaySound, PlaySoundAtPosition).
+ /// Call this if you want to prevent repeated plays of the same sound. It'll
+ /// tell you if the sound has been played recently. The one-shot sound-play
+ /// functions use this under the hood. (PlaySound, PlaySoundAtPosition).
auto ShouldPlay(SoundAsset* s) -> bool;
// Hmm; shouldn't these be accessed through the Source class?
@@ -73,15 +78,15 @@ class Audio {
}
private:
- // Flat list of client sources indexed by id.
+ /// Flat list of client sources indexed by id.
std::vector client_sources_;
- // List of sources that are ready to use.
- // This is kept filled by the audio thread
- // and used by the client.
+ /// List of sources that are ready to use.
+ /// This is kept filled by the audio thread
+ /// and used by the client.
std::vector available_sources_;
- // This must be locked whenever accessing the availableSources list.
+ /// This must be locked whenever accessing the availableSources list.
std::mutex available_sources_mutex_;
};
diff --git a/src/ballistica/base/audio/audio_server.cc b/src/ballistica/base/audio/audio_server.cc
index 5ba5a304..798f0d49 100644
--- a/src/ballistica/base/audio/audio_server.cc
+++ b/src/ballistica/base/audio/audio_server.cc
@@ -2,6 +2,16 @@
#include "ballistica/base/audio/audio_server.h"
+#include
+
+#include "ballistica/shared/buildconfig/buildconfig_common.h"
+
+// 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"
#include "ballistica/base/audio/al_sys.h"
@@ -25,23 +35,27 @@ namespace ballistica::base {
extern std::string g_rift_audio_device_name;
#endif
-#if BA_OSTYPE_ANDROID
-LPALCDEVICEPAUSESOFT alcDevicePauseSOFT;
-LPALCDEVICERESUMESOFT alcDeviceResumeSOFT;
+#if BA_OPENAL_IS_SOFT
+LPALCDEVICEPAUSESOFT alcDevicePauseSOFT{};
+LPALCDEVICERESUMESOFT alcDeviceResumeSOFT{};
+LPALCRESETDEVICESOFT alcResetDeviceSOFT{};
+LPALEVENTCALLBACKSOFT alEventCallbackSOFT{};
+LPALEVENTCONTROLSOFT alEventControlSOFT{};
+// LPALSOFTSETLOGCALLBACK alsoft_set_log_callback{};
+
#endif
-const int kAudioProcessIntervalNormal{500};
-const int kAudioProcessIntervalFade{50};
-const int kAudioProcessIntervalPendingLoad{1};
-#if (BA_DEBUG_BUILD || BA_TEST_BUILD)
+const int kAudioProcessIntervalNormal{500 * 1000};
+const int kAudioProcessIntervalFade{50 * 1000};
+const int kAudioProcessIntervalPendingLoad{1 * 1000};
+
+#if BA_DEBUG_BUILD || BA_TEST_BUILD
const bool kShowInUseSounds{};
#endif
-int AudioServer::al_source_count_ = 0;
-
-struct AudioServer::Impl {
- Impl() = default;
- ~Impl() = default;
+struct AudioServer::Impl_ {
+ Impl_() = default;
+ ~Impl_() = default;
#if BA_ENABLE_AUDIO
ALCcontext* alc_context{};
@@ -49,14 +63,14 @@ struct AudioServer::Impl {
};
/// Location for sound emission (server version).
-class AudioServer::ThreadSource : public Object {
+class AudioServer::ThreadSource_ : public Object {
public:
// The id is returned as the lo-word of the identifier
// returned by "play". If valid is returned as false, there are no
// hardware channels available (or another error) and the source should
// not be used.
- ThreadSource(AudioServer* audio_thread, int id, bool* valid);
- ~ThreadSource() override;
+ ThreadSource_(AudioServer* audio_thread, int id, bool* valid);
+ ~ThreadSource_() override;
void Reset() {
SetIsMusic(false);
SetPositional(true);
@@ -101,37 +115,36 @@ class AudioServer::ThreadSource : public Object {
void ExecPlay();
void Update();
+ void CreateClientSource(int id) {
+ client_source_ = std::make_unique(id);
+ }
+
private:
- bool looping_{};
- std::unique_ptr client_source_;
- float fade_{1.0f};
- float gain_{1.0f};
- AudioServer* audio_thread_{};
- bool valid_{};
- const Object::Ref* source_sound_{};
int id_{};
- uint32_t play_count_{};
+ bool looping_{};
+ bool valid_{};
bool is_actually_playing_{};
bool want_to_play_{};
-#if BA_ENABLE_AUDIO
- ALuint source_{};
-#endif
bool is_streamed_{};
-
/// Whether we should be designated as "music" next time we play.
bool is_music_{};
-
/// Whether currently playing as music.
bool current_is_music_{};
-
+ uint32_t play_count_{};
+ float fade_{1.0f};
+ float gain_{1.0f};
+ std::unique_ptr client_source_;
+ AudioServer* audio_server_{};
+ const Object::Ref* source_sound_{};
#if BA_ENABLE_AUDIO
+ ALuint source_{};
Object::Ref streamer_;
#endif
-
- friend class AudioServer;
}; // ThreadSource
-AudioServer::AudioServer() : impl_{new AudioServer::Impl()} {}
+AudioServer::AudioServer() : impl_{std::make_unique()} {}
+
+AudioServer::~AudioServer() = default;
void AudioServer::OnMainThreadStartApp() {
// Spin up our thread.
@@ -142,32 +155,102 @@ void AudioServer::OnMainThreadStartApp() {
event_loop_->PushCall([this] {
// We want to be informed when our event-loop is pausing and unpausing.
event_loop()->AddSuspendCallback(
- NewLambdaRunnableUnmanaged([this] { OnThreadPause(); }));
+ NewLambdaRunnableUnmanaged([this] { OnThreadSuspend_(); }));
event_loop()->AddUnsuspendCallback(
- NewLambdaRunnableUnmanaged([this] { OnThreadResume(); }));
+ NewLambdaRunnableUnmanaged([this] { OnThreadUnsuspend_(); }));
});
- event_loop_->PushCallSynchronous([this] { OnAppStartInThread(); });
+ event_loop_->PushCallSynchronous([this] { OnAppStartInThread_(); });
}
-void AudioServer::OnAppStartInThread() {
+#if BA_OPENAL_IS_SOFT
+static void ALEventCallback_(ALenum eventType, ALuint object, ALuint param,
+ ALsizei length, const ALchar* message,
+ ALvoid* userParam) noexcept {
+ if (eventType == AL_EVENT_TYPE_DISCONNECTED_SOFT) {
+ if (g_base->audio_server) {
+ g_base->audio_server->event_loop()->PushCall(
+ [] { g_base->audio_server->OnDeviceDisconnected(); });
+ }
+ } else {
+ Log(LogLevel::kWarning, "Got unexpected OpenAL callback event "
+ + std::to_string(static_cast(eventType)));
+ }
+}
+
+// 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_android_log_mutex_);
+
+ if (openalsoft_android_log_.size() < log_cap) {
+ openalsoft_android_log_ += "openal-log("
+ + std::to_string(g_core->GetAppTimeSeconds())
+ + "s): " + msg;
+ if (openalsoft_android_log_.size() >= log_cap) {
+ openalsoft_android_log_ +=
+ "\n\n";
+ }
+ }
+}
+
+void AudioServer::OnAppStartInThread_() {
assert(g_base->InAudioThread());
// Get our thread to give us periodic processing time.
process_timer_ =
event_loop()->NewTimer(kAudioProcessIntervalNormal, true,
- NewLambdaRunnable([this] { Process(); }));
+ NewLambdaRunnable([this] { Process_(); }).Get());
#if BA_ENABLE_AUDIO
// Bring up OpenAL stuff.
{
- const char* al_device_name = nullptr;
+ // Android-specific workaround; seeing lots of random crashes on Xiaomi
+ // Android 11 since switching from OpenALSoft's OpenSL backend to it's
+ // Oboe backend (which itself uses AAudio on newer Androids). Trying
+ // Oboe's OpenSL backend to see if it heads off the crashes.
+ {
+ std::string prefix = "Xiaomi ";
+ if (g_core->platform->GetDeviceName().compare(0, prefix.size(), prefix)
+ == 0) {
+ std::string prefix2 = "11";
+ if (g_core->platform->GetOSVersionString().compare(0, prefix2.size(),
+ prefix2)
+ == 0) {
+ Log(LogLevel::kInfo,
+ "Xiaomi Android 11 detected; using OpenSL instead of AAudio.");
+ g_core->platform->SetEnv("BA_OBOE_USE_OPENSLES", "1");
+ }
+ }
+ }
+
+ const char* al_device_name{};
// On the rift build in vr mode we need to make sure we open the rift audio
// device.
#if BA_RIFT_BUILD
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
ALboolean enumeration =
alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT");
if (enumeration == AL_FALSE) {
@@ -199,26 +282,154 @@ 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_android_log_mutex_);
+ Log(LogLevel::kError,
+ "------------------------"
+ " OPENALSOFT-FATAL-ERROR-LOG-BEGIN ----------------------\n"
+ + openalsoft_android_log_
+ + "\n-------------------------"
+ " OPENALSOFT-FATAL-ERROR-LOG-END -----------------------");
+ openalsoft_android_log_.clear();
+ }
FatalError(
"No audio devices found. Do you have speakers/headphones/etc. "
"connected?");
}
+
impl_->alc_context = alcCreateContext(device, nullptr);
- BA_PRECONDITION(impl_->alc_context);
- BA_PRECONDITION(alcMakeContextCurrent(impl_->alc_context));
+
+ // Android special case: if we fail, try again after a few seconds.
+ if (!impl_->alc_context && g_buildconfig.ostype_android()) {
+ Log(LogLevel::kError,
+ "Failed creating AL context; waiting and trying again.");
+ {
+ std::scoped_lock lock(openalsoft_android_log_mutex_);
+ Log(LogLevel::kWarning,
+ "------------------------"
+ " OPENALSOFT-ERROR-LOG-BEGIN ----------------------\n"
+ + openalsoft_android_log_
+ + "\n-------------------------"
+ " OPENALSOFT-ERROR-LOG-END -----------------------");
+ openalsoft_android_log_.clear();
+ }
+ alcCloseDevice(device);
+ g_core->platform->SleepSeconds(2.0);
+ device = alcOpenDevice(al_device_name);
+ alGetError(); // Clear any errors.
+
+ if (!device) {
+ std::scoped_lock lock(openalsoft_android_log_mutex_);
+ Log(LogLevel::kError,
+ "------------------------"
+ " OPENALSOFT-FATAL-ERROR-LOG-BEGIN ----------------------\n"
+ + openalsoft_android_log_
+ + "\n-------------------------"
+ " OPENALSOFT-FATAL-ERROR-LOG-END -----------------------");
+ openalsoft_android_log_.clear();
+ FatalError("Fallback attempt device create failed.");
+ }
+ impl_->alc_context = alcCreateContext(device, nullptr);
+ if (impl_->alc_context) {
+ // For now want to explicitly know if this works.
+ Log(LogLevel::kWarning, "Backup AL context creation successful!");
+ }
+ }
+
+ // Android special case: if we fail, try OpenSL back-end.
+ if (!impl_->alc_context && g_buildconfig.ostype_android()) {
+ Log(LogLevel::kError,
+ "Failed second time creating AL context; trying OpenSL backend.");
+ {
+ std::scoped_lock lock(openalsoft_android_log_mutex_);
+ Log(LogLevel::kWarning,
+ "------------------------"
+ " OPENALSOFT-ERROR-LOG-BEGIN ----------------------\n"
+ + openalsoft_android_log_
+ + "\n-------------------------"
+ " OPENALSOFT-ERROR-LOG-END -----------------------");
+ openalsoft_android_log_.clear();
+ }
+ alcCloseDevice(device);
+ g_core->platform->SetEnv("BA_OBOE_USE_OPENSLES", "1");
+ device = alcOpenDevice(al_device_name);
+ alGetError(); // Clear any errors.
+ if (!device) {
+ std::scoped_lock lock(openalsoft_android_log_mutex_);
+ Log(LogLevel::kError,
+ "------------------------"
+ " OPENALSOFT-FATAL-ERROR-LOG-BEGIN ----------------------\n"
+ + openalsoft_android_log_
+ + "\n-------------------------"
+ " OPENALSOFT-FATAL-ERROR-LOG-END -----------------------");
+ openalsoft_android_log_.clear();
+ FatalError("Fallback attempt 2 device create failed.");
+ }
+ impl_->alc_context = alcCreateContext(device, nullptr);
+ if (impl_->alc_context) {
+ // For now want to explicitly know if this works.
+ Log(LogLevel::kWarning, "Backup AL context creation 2 successful!");
+ }
+ }
+
+ // Fail at this point if we've got nothing.
+ if (!impl_->alc_context) {
+ if (g_buildconfig.ostype_android()) {
+ std::scoped_lock lock(openalsoft_android_log_mutex_);
+ Log(LogLevel::kError,
+ "------------------------"
+ " OPENALSOFT-FATAL-ERROR-LOG-BEGIN ----------------------\n"
+ + openalsoft_android_log_
+ + "\n-------------------------"
+ " OPENALSOFT-FATAL-ERROR-LOG-END -----------------------");
+ openalsoft_android_log_.clear();
+ }
+ FatalError(
+ "Unable to init audio. Do you have speakers/headphones/etc. "
+ "connected?");
+ }
+ BA_PRECONDITION_FATAL(impl_->alc_context);
+ BA_PRECONDITION_FATAL(alcMakeContextCurrent(impl_->alc_context));
CHECK_AL_ERROR;
-#if BA_OSTYPE_ANDROID
- if (alcIsExtensionPresent(device, "ALC_SOFT_pause_device")) {
- alcDevicePauseSOFT = reinterpret_cast(
- alcGetProcAddress(device, "alcDevicePauseSOFT"));
- alcDeviceResumeSOFT = reinterpret_cast(
- alcGetProcAddress(device, "alcDeviceResumeSOFT"));
- } else {
- FatalError("ALC_SOFT pause/resume functionality not found.");
- }
+#if BA_OPENAL_IS_SOFT
+ // Currently assuming the pause/resume and reset extensions are present.
+ // if (alcIsExtensionPresent(device, "ALC_SOFT_pause_device")) {
+ alcDevicePauseSOFT = reinterpret_cast(
+ alcGetProcAddress(device, "alcDevicePauseSOFT"));
+ BA_PRECONDITION_FATAL(alcDevicePauseSOFT != nullptr);
+ alcDeviceResumeSOFT = reinterpret_cast(
+ alcGetProcAddress(device, "alcDeviceResumeSOFT"));
+ BA_PRECONDITION_FATAL(alcDeviceResumeSOFT != nullptr);
+ alcResetDeviceSOFT = reinterpret_cast(
+ alcGetProcAddress(device, "alcResetDeviceSOFT"));
+ BA_PRECONDITION_FATAL(alcResetDeviceSOFT != nullptr);
+ alEventCallbackSOFT = reinterpret_cast(
+ alcGetProcAddress(device, "alEventCallbackSOFT"));
+ BA_PRECONDITION_FATAL(alEventCallbackSOFT != nullptr);
+ alEventControlSOFT = reinterpret_cast(
+ alcGetProcAddress(device, "alEventControlSOFT"));
+ BA_PRECONDITION_FATAL(alEventControlSOFT != nullptr);
+ // 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);
+ CHECK_AL_ERROR;
+ ALenum types[] = {AL_EVENT_TYPE_DISCONNECTED_SOFT};
+ alEventControlSOFT(1, types, AL_TRUE);
+ // } else {
+ // FatalError("ALC_SOFT pause/resume functionality not found.");
+ // }
#endif
}
@@ -235,10 +446,10 @@ void AudioServer::OnAppStartInThread() {
int target_source_count = 30;
for (int i = 0; i < target_source_count; i++) {
bool valid = false;
- auto s(Object::New(this, i, &valid));
+ auto s(Object::New(this, i, &valid));
if (valid) {
- s->client_source_ = std::make_unique(i);
- g_base->audio->AddClientSource(&(*s->client_source_));
+ s->CreateClientSource(i);
+ g_base->audio->AddClientSource(&(*s->client_source()));
sound_source_refs_.push_back(s);
sources_.push_back(&(*s));
} else {
@@ -250,47 +461,75 @@ void AudioServer::OnAppStartInThread() {
CHECK_AL_ERROR;
// Now make available any stopped sources (should be all of them).
- UpdateAvailableSources();
+ UpdateAvailableSources_();
+ last_started_playing_time_ = g_core->GetAppTimeSeconds();
#endif // BA_ENABLE_AUDIO
}
-AudioServer::~AudioServer() {
-#if BA_ENABLE_AUDIO
- sound_source_refs_.clear();
-
- // Take down AL stuff.
- {
- ALCdevice* device;
- BA_PRECONDITION_LOG(alcMakeContextCurrent(nullptr));
- device = alcGetContextsDevice(impl_->alc_context);
- alcDestroyContext(impl_->alc_context);
- assert(alcGetError(device) == ALC_NO_ERROR);
- alcCloseDevice(device);
+void AudioServer::Shutdown() {
+ BA_PRECONDITION(g_base->InAudioThread());
+ if (shutting_down_) {
+ return;
}
- assert(streaming_sources_.empty());
- assert(al_source_count_ == 0);
+ shutting_down_ = true;
+ shutdown_start_time_ = g_core->GetAppTimeSeconds();
-#endif // BA_ENABLE_AUDIO
- delete impl_;
+ // Stop all playing sounds and note the time. We'll then give everything a
+ // moment to come to a halt before we tear down the audio context to
+ // hopefully minimize errors/pops/etc.
+ for (auto&& i : sources_) {
+ i->Stop();
+ }
+ UpdateTimerInterval_();
}
-struct AudioServer::SoundFadeNode {
+void AudioServer::CompleteShutdown_() {
+ assert(g_base->InAudioThread());
+ assert(shutting_down_);
+ assert(!shutdown_completed_);
+
+#if BA_ENABLE_AUDIO
+ ALCboolean check = alcMakeContextCurrent(nullptr);
+ if (!check) {
+ Log(LogLevel::kWarning, "Error on alcMakeContextCurrent at shutdown.");
+ }
+ auto* device = alcGetContextsDevice(impl_->alc_context);
+ if (!device) {
+ Log(LogLevel::kWarning, "Unable to get ALCdevice at shutdown.");
+ } else {
+ alcDestroyContext(impl_->alc_context);
+ ALenum err = alcGetError(device);
+ if (err != ALC_NO_ERROR) {
+ Log(LogLevel::kWarning, "Error on AL shutdown.");
+ }
+ check = alcCloseDevice(device);
+ if (!check) {
+ Log(LogLevel::kWarning, "Error on alcCloseDevice at shutdown.");
+ }
+ }
+#endif
+
+ shutdown_completed_ = true;
+}
+
+struct AudioServer::SoundFadeNode_ {
uint32_t play_id;
millisecs_t starttime;
millisecs_t endtime;
bool out;
- SoundFadeNode(uint32_t play_id_in, millisecs_t duration_in, bool out_in)
+ SoundFadeNode_(uint32_t play_id_in, millisecs_t duration_in, bool out_in)
: play_id(play_id_in),
starttime(g_core->GetAppTimeMillisecs()),
endtime(g_core->GetAppTimeMillisecs() + duration_in),
out(out_in) {}
};
-void AudioServer::SetPaused(bool pause) {
- if (!paused_) {
- if (!pause) {
- Log(LogLevel::kError, "Got audio unpause request when already unpaused.");
+void AudioServer::SetSuspended_(bool suspend) {
+ if (!suspended_) {
+ if (!suspend) {
+ Log(LogLevel::kError,
+ "Got audio unsuspend request when already unsuspended.");
} else {
#if BA_OSTYPE_IOS_TVOS
// apple recommends this during audio-interruptions..
@@ -299,20 +538,41 @@ void AudioServer::SetPaused(bool pause) {
alcMakeContextCurrent(nullptr);
#endif
-// On android lets tell open-sl to stop its processing.
-#if BA_OSTYPE_ANDROID
- alcDevicePauseSOFT(alcGetContextsDevice(impl_->alc_context));
-#endif // BA_OSTYPE_ANDROID
+ // Pause OpenALSoft.
+#if BA_OPENAL_IS_SOFT
+ BA_PRECONDITION_FATAL(alcDevicePauseSOFT != nullptr);
+ BA_PRECONDITION_FATAL(impl_ != nullptr && impl_->alc_context != nullptr);
+ auto* device = alcGetContextsDevice(impl_->alc_context);
+ BA_PRECONDITION_FATAL(device != nullptr);
- paused_ = true;
+ try {
+ g_core->platform->LowLevelDebugLog(
+ "Calling alcDevicePauseSOFT at "
+ + std::to_string(g_core->GetAppTimeSeconds()));
+ alcDevicePauseSOFT(device);
+ } catch (const std::exception& e) {
+ Log(LogLevel::kError,
+ "Error in alcDevicePauseSOFT at time "
+ + std::to_string(g_core->GetAppTimeSeconds())
+ + "( playing since "
+ + std::to_string(last_started_playing_time_)
+ + "): " + g_core->platform->DemangleCXXSymbol(typeid(e).name())
+ + " " + e.what());
+ } catch (...) {
+ Log(LogLevel::kError, "Unknown error in alcDevicePauseSOFT");
+ }
+#endif
+
+ suspended_ = true;
}
} else {
- // unpause if requested..
- if (pause) {
- Log(LogLevel::kError, "Got audio pause request when already paused.");
+ // Unsuspend if requested.
+ if (suspend) {
+ Log(LogLevel::kError,
+ "Got audio suspend request when already suspended.");
} else {
#if BA_OSTYPE_IOS_TVOS
- // apple recommends this during audio-interruptions..
+ // Apple recommends this during audio-interruptions.
// http://developer.apple.com/library/ios/#documentation/Audio/
// Conceptual/AudioSessionProgrammingGuide/Cookbook/
// Cookbook.html#//apple_ref/doc/uid/TP40007875-CH6-SW38
@@ -320,18 +580,36 @@ void AudioServer::SetPaused(bool pause) {
alcMakeContextCurrent(impl_->alc_context); // hmm is this necessary?..
#endif
#endif
-// On android lets tell openal-soft to stop processing.
-#if BA_OSTYPE_ANDROID
- alcDeviceResumeSOFT(alcGetContextsDevice(impl_->alc_context));
-#endif // BA_OSTYPE_ANDROID
- paused_ = false;
+// With OpenALSoft lets tell openal-soft to resume processing.
+#if BA_OPENAL_IS_SOFT
+ BA_PRECONDITION_FATAL(alcDeviceResumeSOFT != nullptr);
+ BA_PRECONDITION_FATAL(impl_ != nullptr && impl_->alc_context != nullptr);
+ auto* device = alcGetContextsDevice(impl_->alc_context);
+ BA_PRECONDITION_FATAL(device != nullptr);
+ try {
+ g_core->platform->LowLevelDebugLog(
+ "Calling alcDeviceResumeSOFT at "
+ + std::to_string(g_core->GetAppTimeSeconds()));
+ alcDeviceResumeSOFT(device);
+ } catch (const std::exception& e) {
+ Log(LogLevel::kError,
+ "Error in alcDeviceResumeSOFT at time "
+ + std::to_string(g_core->GetAppTimeSeconds()) + ": "
+ + g_core->platform->DemangleCXXSymbol(typeid(e).name()) + " "
+ + e.what());
+ } catch (...) {
+ Log(LogLevel::kError, "Unknown error in alcDeviceResumeSOFT");
+ }
+#endif
+ last_started_playing_time_ = g_core->GetAppTimeSeconds();
+ suspended_ = false;
#if BA_ENABLE_AUDIO
CHECK_AL_ERROR;
-#endif // BA_ENABLE_AUDIO
+#endif
- // Go through all of our sources and stop any we've wanted to stop while
- // paused.
+ // Go through all of our sources and stop any we've wanted to stop
+ // while we were suspended.
for (auto&& i : sources_) {
if ((!i->want_to_play()) && (i->is_actually_playing())) {
i->ExecStop();
@@ -343,7 +621,7 @@ void AudioServer::SetPaused(bool pause) {
void AudioServer::PushSourceSetIsMusicCall(uint32_t play_id, bool val) {
event_loop()->PushCall([this, play_id, val] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetIsMusic(val);
}
@@ -352,7 +630,7 @@ void AudioServer::PushSourceSetIsMusicCall(uint32_t play_id, bool val) {
void AudioServer::PushSourceSetPositionalCall(uint32_t play_id, bool val) {
event_loop()->PushCall([this, play_id, val] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetPositional(val);
}
@@ -362,7 +640,7 @@ void AudioServer::PushSourceSetPositionalCall(uint32_t play_id, bool val) {
void AudioServer::PushSourceSetPositionCall(uint32_t play_id,
const Vector3f& p) {
event_loop()->PushCall([this, play_id, p] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetPosition(p.x, p.y, p.z);
}
@@ -371,7 +649,7 @@ void AudioServer::PushSourceSetPositionCall(uint32_t play_id,
void AudioServer::PushSourceSetGainCall(uint32_t play_id, float val) {
event_loop()->PushCall([this, play_id, val] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetGain(val);
}
@@ -380,7 +658,7 @@ void AudioServer::PushSourceSetGainCall(uint32_t play_id, float val) {
void AudioServer::PushSourceSetFadeCall(uint32_t play_id, float val) {
event_loop()->PushCall([this, play_id, val] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetFade(val);
}
@@ -389,7 +667,7 @@ void AudioServer::PushSourceSetFadeCall(uint32_t play_id, float val) {
void AudioServer::PushSourceSetLoopingCall(uint32_t play_id, bool val) {
event_loop()->PushCall([this, play_id, val] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->SetLooping(val);
}
@@ -399,7 +677,7 @@ void AudioServer::PushSourceSetLoopingCall(uint32_t play_id, bool val) {
void AudioServer::PushSourcePlayCall(uint32_t play_id,
Object::Ref* sound) {
event_loop()->PushCall([this, play_id, sound] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
// If this play command is valid, pass it along.
// Otherwise, return it immediately for deletion.
@@ -411,15 +689,15 @@ void AudioServer::PushSourcePlayCall(uint32_t play_id,
// Let's take this opportunity to pass on newly available sources.
// This way the more things clients are playing, the more
- // tight our source availability checking gets (instead of solely relying on
- // our periodic process() calls).
- UpdateAvailableSources();
+ // tight our source availability checking gets (instead of solely relying
+ // on our periodic process() calls).
+ UpdateAvailableSources_();
});
}
void AudioServer::PushSourceStopCall(uint32_t play_id) {
event_loop()->PushCall([this, play_id] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
if (s) {
s->Stop();
}
@@ -428,7 +706,7 @@ void AudioServer::PushSourceStopCall(uint32_t play_id) {
void AudioServer::PushSourceEndCall(uint32_t play_id) {
event_loop()->PushCall([this, play_id] {
- ThreadSource* s = GetPlayingSound(play_id);
+ ThreadSource_* s = GetPlayingSound_(play_id);
assert(s);
s->client_source()->Lock(5);
s->client_source()->set_client_queue_size(
@@ -439,13 +717,13 @@ void AudioServer::PushSourceEndCall(uint32_t play_id) {
}
void AudioServer::PushResetCall() {
- event_loop()->PushCall([this] { Reset(); });
+ event_loop()->PushCall([this] { Reset_(); });
}
void AudioServer::PushSetListenerPositionCall(const Vector3f& p) {
event_loop()->PushCall([this, p] {
#if BA_ENABLE_AUDIO
- if (!paused_) {
+ if (!suspended_ && !shutting_down_) {
ALfloat lpos[3] = {p.x, p.y, p.z};
alListenerfv(AL_POSITION, lpos);
CHECK_AL_ERROR;
@@ -458,7 +736,7 @@ void AudioServer::PushSetListenerOrientationCall(const Vector3f& forward,
const Vector3f& up) {
event_loop()->PushCall([this, forward, up] {
#if BA_ENABLE_AUDIO
- if (!paused_) {
+ if (!suspended_ && !shutting_down_) {
ALfloat lorient[6] = {forward.x, forward.y, forward.z, up.x, up.y, up.z};
alListenerfv(AL_ORIENTATION, lorient);
CHECK_AL_ERROR;
@@ -467,7 +745,7 @@ void AudioServer::PushSetListenerOrientationCall(const Vector3f& forward,
});
}
-void AudioServer::UpdateAvailableSources() {
+void AudioServer::UpdateAvailableSources_() {
for (auto&& i : sources_) {
i->UpdateAvailability();
}
@@ -525,17 +803,19 @@ void AudioServer::UpdateAvailableSources() {
}
void AudioServer::StopSound(uint32_t play_id) {
- uint32_t source = source_id_from_play_id(play_id);
- uint32_t count = play_count_from_play_id(play_id);
+ uint32_t source = SourceIdFromPlayId(play_id);
+ uint32_t count = PlayCountFromPlayId(play_id);
if (source < sources_.size()) {
- if (count == sources_[source]->play_count()) sources_[source]->Stop();
+ if (count == sources_[source]->play_count()) {
+ sources_[source]->Stop();
+ }
}
}
-auto AudioServer::GetPlayingSound(uint32_t play_id)
- -> AudioServer::ThreadSource* {
- uint32_t source = source_id_from_play_id(play_id);
- uint32_t count = play_count_from_play_id(play_id);
+auto AudioServer::GetPlayingSound_(uint32_t play_id)
+ -> AudioServer::ThreadSource_* {
+ uint32_t source = SourceIdFromPlayId(play_id);
+ uint32_t count = PlayCountFromPlayId(play_id);
assert(source < sources_.size());
if (source < sources_.size()) {
// If the sound has finished playing or whatnot, we
@@ -551,9 +831,10 @@ auto AudioServer::GetPlayingSound(uint32_t play_id)
return nullptr;
}
-void AudioServer::UpdateTimerInterval() {
- // If we've got pending loads, go into uber-hyperactive mode.
- if (have_pending_loads_) {
+void AudioServer::UpdateTimerInterval_() {
+ // If we've got pending loads or are shutting down, go into
+ // uber-hyperactive mode.
+ if (have_pending_loads_ || shutting_down_) {
assert(process_timer_);
process_timer_->SetLength(kAudioProcessIntervalPendingLoad);
} else {
@@ -563,48 +844,40 @@ void AudioServer::UpdateTimerInterval() {
assert(process_timer_);
process_timer_->SetLength(kAudioProcessIntervalFade);
} else {
- // Nothing but normal activity; just run enough to keep
- // buffers filled and whatnot.
+ // Nothing but normal activity; just run often enough to keep buffers
+ // filled and whatnot.
assert(process_timer_);
process_timer_->SetLength(kAudioProcessIntervalNormal);
}
}
}
-void AudioServer::SetSoundPitch(float pitch) {
- sound_pitch_ = pitch;
- if (sound_pitch_ < 0.01f) sound_pitch_ = 0.01f;
+void AudioServer::SetSoundPitch_(float pitch) {
+ sound_pitch_ = std::clamp(pitch, 0.1f, 10.0f);
for (auto&& i : sources_) {
i->UpdatePitch();
}
}
-void AudioServer::SetSoundVolume(float volume) {
- sound_volume_ = volume;
- if (sound_volume_ > 3.0f) {
- sound_volume_ = 3.0f;
- }
- if (sound_volume_ < 0) {
- sound_volume_ = 0;
- }
+void AudioServer::SetSoundVolume_(float volume) {
+ sound_volume_ = std::clamp(volume, 0.0f, 3.0f);
for (auto&& i : sources_) {
i->UpdateVolume();
}
}
-void AudioServer::SetMusicVolume(float volume) {
- music_volume_ = volume;
- if (music_volume_ > 3.0f) music_volume_ = 3.0f;
- if (music_volume_ < 0) music_volume_ = 0;
- UpdateMusicPlayState();
+void AudioServer::SetMusicVolume_(float volume) {
+ music_volume_ = std::clamp(volume, 0.0f, 3.0f);
+ UpdateMusicPlayState_();
for (auto&& i : sources_) {
i->UpdateVolume();
}
}
-// Start or stop music playback based on volume/pause-state/etc.
-void AudioServer::UpdateMusicPlayState() {
- bool should_be_playing = ((music_volume_ > 0.000001f) && !paused_);
+// Start or stop music playback based on volume/suspend-state/etc.
+void AudioServer::UpdateMusicPlayState_() {
+ bool should_be_playing =
+ (music_volume_ > 0.000001f && !suspended_ && !shutting_down_);
// Flip any playing music off.
if (!should_be_playing) {
@@ -624,39 +897,179 @@ void AudioServer::UpdateMusicPlayState() {
}
}
-void AudioServer::Process() {
- assert(g_base->InAudioThread());
- millisecs_t real_time = g_core->GetAppTimeMillisecs();
+void AudioServer::ProcessDeviceDisconnects_(seconds_t real_time_seconds) {
+#if BA_OPENAL_IS_SOFT
+ // If our device has been disconnected, try to reconnect it
+ // periodically.
+ auto* device = alcGetContextsDevice(impl_->alc_context);
+ BA_PRECONDITION_FATAL(device != nullptr);
+ ALCint connected{-1};
+ alcGetIntegerv(device, ALC_CONNECTED, sizeof(connected), &connected);
+ CHECK_AL_ERROR;
+ if (connected != 0) {
+ last_connected_time_ = real_time_seconds;
+ // reconnect_fail_count_ = 0;
+ }
+ // else {
+ // reconnect_fail_count_ = 0;
+ // }
+
+ // Retry less often once we've been failing for a while.
+ seconds_t retry_interval =
+ real_time_seconds - last_connected_time_ > 20.0 ? 10.0 : 3.0;
+
+ if (connected == 0
+ && real_time_seconds - last_reset_attempt_time_ >= retry_interval) {
+ Log(LogLevel::kInfo, "OpenAL device disconnected; resetting...");
+ if (g_buildconfig.ostype_android()) {
+ std::scoped_lock lock(openalsoft_android_log_mutex_);
+ openalsoft_android_log_ +=
+ "DEVICE DISCONNECT DETECTED; ATTEMPTING RESET\n";
+ }
+ last_reset_attempt_time_ = real_time_seconds;
+ BA_PRECONDITION_FATAL(alcResetDeviceSOFT != nullptr);
+ auto result = alcResetDeviceSOFT(device, nullptr);
+ CHECK_AL_ERROR;
+
+ // Log(LogLevel::kInfo, std::string("alcResetDeviceSOFT returned ")
+ // + (result == ALC_TRUE ? "ALC_TRUE" :
+ // "ALC_FALSE"));
+
+ // Check to see if this brought the device back.
+ // ALCint connected{-1};
+ // alcGetIntegerv(device, ALC_CONNECTED, sizeof(connected), &connected);
+ // CHECK_AL_ERROR;
+
+ // If we were successful, clear out the wait for the next reset.
+ // Otherwise plugging in headphones and then unplugging them immediately
+ // will result in 10 seconds of silence.
+ if (result == ALC_TRUE) {
+ // last_reset_attempt_time_ = -999.0;
+ if (g_buildconfig.ostype_android()) {
+ std::scoped_lock lock(openalsoft_android_log_mutex_);
+ openalsoft_android_log_ += "DEVICE RESET CALL SUCCESSFUL\n";
+ }
+ } else {
+ if (g_buildconfig.ostype_android()) {
+ std::scoped_lock lock(openalsoft_android_log_mutex_);
+ openalsoft_android_log_ += "DEVICE RESET CALL FAILED\n";
+ }
+ }
+
+ // If we're ever *not* immediately successful, flip on reporting to try
+ // and figure out what's going on. After that point we'll report subsequent
+ // if (connected == 0) {
+ // report_reset_results_ = true;
+ // }
+ // if (report_reset_results_ && reset_result_reports_remaining_ > 0) {
+ // reset_result_reports_remaining_ -= 1;
+ // if (connected != 0) {
+ // Log(LogLevel::kInfo,
+ // "alcResetDeviceSOFT successfully reconnected device.");
+ // } else {
+ // Log(LogLevel::kError, "alcResetDeviceSOFT failed to reconnect
+ // device.");
+ // }
+ // if (g_buildconfig.ostype_android()) {
+ // std::scoped_lock lock(openalsoft_android_log_mutex_);
+ // Log(LogLevel::kWarning,
+ // "------------------------"
+ // " OPENALSOFT-RECONNECT-LOG-BEGIN ----------------------\n"
+ // + openalsoft_android_log_
+ // + "\n-------------------------"
+ // " OPENALSOFT-RECONNECT-LOG-END -----------------------");
+ // openalsoft_android_log_.clear();
+ // }
+ // }
+ }
+
+ // If we've failed at reconnecting for a while, ship logs once.
+ if (real_time_seconds - last_connected_time_ > 20.0
+ && !shipped_reconnect_logs_) {
+ shipped_reconnect_logs_ = true;
+ if (g_buildconfig.ostype_android()) {
+ std::scoped_lock lock(openalsoft_android_log_mutex_);
+ Log(LogLevel::kWarning,
+ "Have been disconnected for a while; dumping OpenAL log.\n"
+ "------------------------"
+ " OPENALSOFT-RECONNECT-LOG-BEGIN ----------------------\n"
+ + openalsoft_android_log_
+ + "\n-------------------------"
+ " OPENALSOFT-RECONNECT-LOG-END -----------------------");
+ openalsoft_android_log_.clear();
+ }
+ }
+#endif // BA_OPENAL_IS_SOFT
+}
+
+void AudioServer::OnDeviceDisconnected() {
+ assert(g_base->InAudioThread());
+ // All we do here is run an explicit Process_. This only saves us a half
+ // second or so over letting the timer do it, but hey we'll take it.
+ Process_();
+}
+
+void AudioServer::Process_() {
+ assert(g_base->InAudioThread());
+ seconds_t real_time_seconds = g_core->GetAppTimeSeconds();
+ millisecs_t real_time_millisecs = real_time_seconds * 1000;
+
+ // Only do real work if we're in normal running mode.
+ if (!suspended_ && !shutting_down_) {
+ ProcessDeviceDisconnects_(real_time_seconds);
- // If we're paused we don't do nothin'.
- if (!paused_) {
// Do some loading...
have_pending_loads_ = g_base->assets->RunPendingAudioLoads();
// Keep that available-sources list filled.
- UpdateAvailableSources();
+ UpdateAvailableSources_();
// Update our fading sound volumes.
- if (real_time - last_sound_fade_process_time_ > 50) {
- ProcessSoundFades();
- last_sound_fade_process_time_ = real_time;
+ if (real_time_millisecs - last_sound_fade_process_time_ > 50) {
+ ProcessSoundFades_();
+ last_sound_fade_process_time_ = real_time_millisecs;
}
// Update streaming sources.
- if (real_time - last_stream_process_time_ > 100) {
- last_stream_process_time_ = real_time;
+ if (real_time_millisecs - last_stream_process_time_ > 100) {
+ last_stream_process_time_ = real_time_millisecs;
for (auto&& i : streaming_sources_) {
i->Update();
}
}
+
+ // If the app has switched active/inactive state, update our volumes (we
+ // may silence our audio in these cases).
+ auto app_active = g_base->app_active();
+ if (app_active != app_active_) {
+ app_active_ = app_active;
+ app_active_volume_ =
+ (!app_active && g_base->app_adapter->ShouldSilenceAudioForInactive())
+ ? 0.0f
+ : 1.0f;
+ for (auto&& i : sources_) {
+ i->UpdateVolume();
+ }
+ }
+
#if BA_ENABLE_AUDIO
CHECK_AL_ERROR;
#endif
}
- UpdateTimerInterval();
+ UpdateTimerInterval_();
+
+ // In my brief unscientific testing with my airpods, a 0.2 second delay
+ // between stopping sounds and killing the sound-system seems to be enough
+ // for the mixer to spit out some silence so we don't hear sudden cut-offs
+ // in one or both ears.
+ if (shutting_down_ && !shutdown_completed_) {
+ if (g_core->GetAppTimeSeconds() - shutdown_start_time_ > 0.2) {
+ CompleteShutdown_();
+ }
+ }
}
-void AudioServer::Reset() {
+void AudioServer::Reset_() {
// Note: up until version 1.7.20, the audio server would stop all playing
// sounds when reset. This would prevent against long sounds playing at
// the end of a game session 'bleeding' into the main menu/etc. However,
@@ -665,10 +1078,11 @@ void AudioServer::Reset() {
// growing. In particular, a 'power down' sound at launch when a plugin is
// no longer found is being cut off by the initial app-mode switch.
- // So disabling the stop behavior for now and hoping that doesn't bite us.
- // Ideally we should have sounds contexts so that we can stop sounds for
- // a particular scene when that scene ends/etc. This would also fix our
- // current problem where epic mode screws up the pitch on our UI sounds.
+ // So I'm disabling the stop behavior for now and hoping that doesn't bite
+ // us. Ideally we should have sounds contexts so that we can stop sounds
+ // for a particular scene when that scene ends/etc. This could also
+ // address our current problem where epic mode screws up the pitch on our
+ // UI sounds.
if (explicit_bool(false)) {
// Stop all playing sounds.
@@ -677,17 +1091,17 @@ void AudioServer::Reset() {
}
}
// Still need to reset this though or epic-mode will screw us up.
- SetSoundPitch(1.0f);
+ SetSoundPitch_(1.0f);
}
-void AudioServer::ProcessSoundFades() {
+void AudioServer::ProcessSoundFades_() {
auto i = sound_fade_nodes_.begin();
decltype(i) i_next;
while (i != sound_fade_nodes_.end()) {
i_next = i;
i_next++;
- AudioServer::ThreadSource* s = GetPlayingSound(i->second.play_id);
+ AudioServer::ThreadSource_* s = GetPlayingSound_(i->second.play_id);
if (s) {
if (g_core->GetAppTimeMillisecs() > i->second.endtime) {
StopSound(i->second.play_id);
@@ -708,20 +1122,21 @@ void AudioServer::ProcessSoundFades() {
}
void AudioServer::FadeSoundOut(uint32_t play_id, uint32_t time) {
- // Pop a new node on the list (this won't overwrite the old if there is one).
+ // Pop a new node on the list (this won't overwrite the old if there is
+ // one).
sound_fade_nodes_.insert(
- std::make_pair(play_id, SoundFadeNode(play_id, time, true)));
+ std::make_pair(play_id, SoundFadeNode_(play_id, time, true)));
}
-void AudioServer::DeleteAssetComponent(Asset* c) {
- assert(g_base->InAudioThread());
- c->Unload();
- delete c;
-}
+// void AudioServer::DeleteAssetComponent_(Asset* c) {
+// assert(g_base->InAudioThread());
+// c->Unload();
+// delete c;
+// }
-AudioServer::ThreadSource::ThreadSource(AudioServer* audio_thread_in, int id_in,
- bool* valid_out)
- : id_(id_in), audio_thread_(audio_thread_in) {
+AudioServer::ThreadSource_::ThreadSource_(AudioServer* audio_server_in,
+ int id_in, bool* valid_out)
+ : id_(id_in), audio_server_(audio_server_in) {
#if BA_ENABLE_AUDIO
assert(g_core);
assert(valid_out != nullptr);
@@ -737,7 +1152,7 @@ AudioServer::ThreadSource::ThreadSource(AudioServer* audio_thread_in, int id_in,
} else {
// In vr mode we keep the microphone a bit closer to the camera
// for realism purposes, so we need stuff louder in general.
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
alSourcef(source_, AL_MAX_DISTANCE, 100);
alSourcef(source_, AL_REFERENCE_DISTANCE, 7.5f);
} else {
@@ -751,13 +1166,13 @@ AudioServer::ThreadSource::ThreadSource(AudioServer* audio_thread_in, int id_in,
}
*valid_out = valid_;
if (valid_) {
- al_source_count_++;
+ g_base->audio_server->al_source_count_++;
}
#endif // BA_ENABLE_AUDIO
}
-AudioServer::ThreadSource::~ThreadSource() {
+AudioServer::ThreadSource_::~ThreadSource_() {
#if BA_ENABLE_AUDIO
if (!valid_) {
@@ -766,10 +1181,10 @@ AudioServer::ThreadSource::~ThreadSource() {
Stop();
// Remove us from sources list.
- for (auto i = audio_thread_->sources_.begin();
- i != audio_thread_->sources_.end(); ++i) {
+ for (auto i = audio_server_->sources_.begin();
+ i != audio_server_->sources_.end(); ++i) {
if (*i == this) {
- audio_thread_->sources_.erase(i);
+ audio_server_->sources_.erase(i);
break;
}
}
@@ -779,22 +1194,22 @@ AudioServer::ThreadSource::~ThreadSource() {
alDeleteSources(1, &source_);
CHECK_AL_ERROR;
- al_source_count_--;
+ g_base->audio_server->al_source_count_--;
#endif // BA_ENABLE_AUDIO
}
-auto AudioServer::ThreadSource::GetDefaultOwnerThread() const -> EventLoopID {
+auto AudioServer::ThreadSource_::GetDefaultOwnerThread() const -> EventLoopID {
return EventLoopID::kAudio;
}
-void AudioServer::ThreadSource::UpdateAvailability() {
+void AudioServer::ThreadSource_::UpdateAvailability() {
#if BA_ENABLE_AUDIO
assert(g_base->InAudioThread());
- // If it's waiting to be picked up by a client or has pending client commands,
- // skip.
+ // If it's waiting to be picked up by a client or has pending client
+ // commands, skip.
if (!client_source_->TryLock(6)) {
return;
}
@@ -806,17 +1221,17 @@ void AudioServer::ThreadSource::UpdateAvailability() {
}
// We consider ourselves busy if there's an active looping play command
- // (regardless of its actual physical play state - music could be turned off,
- // stuttering, etc.).
- // If it's non-looping, we check its play state and snatch it if it's not
- // playing.
+ // (regardless of its actual physical play state - music could be turned
+ // off, stuttering, etc.). If it's non-looping, we check its play state and
+ // snatch it if it's not playing.
bool busy;
if (looping_ || (is_streamed_ && streamer_.Exists() && streamer_->loops())) {
busy = want_to_play_;
} else {
- // If our context is paused, we know nothing is playing
+ // If our context is suspended, we know nothing is playing
// (and we can't ask AL cuz we have no context).
- if (g_base->audio_server->paused()) {
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
busy = false;
} else {
ALint state;
@@ -827,8 +1242,9 @@ void AudioServer::ThreadSource::UpdateAvailability() {
}
// Ok, now if we can get a lock on the availability list, go ahead and
- // make this guy available; give him a new play id and reset his state.
- // If we can't get a lock it's no biggie... we'll come back to this guy later.
+ // make this guy available; give him a new play id and reset his state. If
+ // we can't get a lock it's no biggie... we'll come back to this guy
+ // later.
if (!busy) {
if (g_base->audio->available_sources_mutex().try_lock()) {
@@ -850,86 +1266,144 @@ void AudioServer::ThreadSource::UpdateAvailability() {
#endif // BA_ENABLE_AUDIO
}
-void AudioServer::ThreadSource::Update() {
+void AudioServer::ThreadSource_::Update() {
#if BA_ENABLE_AUDIO
assert(is_streamed_ && is_actually_playing_);
streamer_->Update();
#endif
}
-void AudioServer::ThreadSource::SetIsMusic(bool m) { is_music_ = m; }
+void AudioServer::ThreadSource_::SetIsMusic(bool m) { is_music_ = m; }
-void AudioServer::ThreadSource::SetGain(float g) {
+void AudioServer::ThreadSource_::SetGain(float g) {
gain_ = g;
UpdateVolume();
}
-void AudioServer::ThreadSource::SetFade(float f) {
+void AudioServer::ThreadSource_::SetFade(float f) {
fade_ = f;
UpdateVolume();
}
-void AudioServer::ThreadSource::SetLooping(bool loop) {
+void AudioServer::ThreadSource_::SetLooping(bool loop) {
looping_ = loop;
- if (!g_base->audio_server->paused()) {
#if BA_ENABLE_AUDIO
- alSourcei(source_, AL_LOOPING, loop);
- CHECK_AL_ERROR;
-#endif
- }
-}
-
-void AudioServer::ThreadSource::SetPositional(bool p) {
-#if BA_ENABLE_AUDIO
- if (!g_base->audio_server->paused()) {
- // TODO(ericf): Don't allow setting of positional
- // on stereo sounds - we check this at initial play()
- // but should do it here too.
- alSourcei(source_, AL_SOURCE_RELATIVE, !p);
- CHECK_AL_ERROR;
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
+ return;
}
+ alSourcei(source_, AL_LOOPING, loop);
+ CHECK_AL_ERROR;
#endif
}
-void AudioServer::ThreadSource::SetPosition(float x, float y, float z) {
+void AudioServer::ThreadSource_::SetPositional(bool p) {
#if BA_ENABLE_AUDIO
- if (!g_base->audio_server->paused()) {
- bool oob = false;
- if (x < -500) {
- oob = true;
- x = -500;
- } else if (x > 500) {
- oob = true;
- x = 500;
- }
- if (y < -500) {
- oob = true;
- y = -500;
- } else if (y > 500) {
- oob = true;
- y = 500;
- }
- if (z < -500) {
- oob = true;
- z = -500;
- } else if (z > 500) {
- oob = true;
- z = 500;
- }
- if (oob) {
- BA_LOG_ONCE(LogLevel::kError,
- "AudioServer::ThreadSource::SetPosition"
- " got out-of-bounds value.");
- }
- ALfloat source_pos[] = {x, y, z};
- alSourcefv(source_, AL_POSITION, source_pos);
- CHECK_AL_ERROR;
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
+ return;
}
+ // TODO(ericf): Don't allow setting of positional
+ // on stereo sounds - we check this at initial play()
+ // but should do it here too.
+ alSourcei(source_, AL_SOURCE_RELATIVE, !p);
+ CHECK_AL_ERROR;
+
+#endif
+}
+
+void AudioServer::ThreadSource_::SetPosition(float x, float y, float z) {
+#if BA_ENABLE_AUDIO
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
+ return;
+ }
+ bool oob = false;
+ if (x < -500) {
+ oob = true;
+ x = -500;
+ } else if (x > 500) {
+ oob = true;
+ x = 500;
+ }
+ if (y < -500) {
+ oob = true;
+ y = -500;
+ } else if (y > 500) {
+ oob = true;
+ y = 500;
+ }
+ if (z < -500) {
+ oob = true;
+ z = -500;
+ } else if (z > 500) {
+ oob = true;
+ z = 500;
+ }
+ if (oob) {
+ BA_LOG_ONCE(LogLevel::kError,
+ "AudioServer::ThreadSource::SetPosition"
+ " got out-of-bounds value.");
+ }
+ ALfloat source_pos[] = {x, y, z};
+ alSourcefv(source_, AL_POSITION, source_pos);
+ CHECK_AL_ERROR;
+
#endif // BA_ENABLE_AUDIO
}
+auto AudioServer::ThreadSource_::Play(const Object::Ref* sound)
+ -> uint32_t {
+#if BA_ENABLE_AUDIO
+
+ assert(g_base->InAudioThread());
+ assert(sound->Exists());
+
+ // Stop whatever we were doing.
+ Stop();
+
+ assert(source_sound_ == nullptr);
+ source_sound_ = sound;
+
+ if (!g_base->audio_server->suspended_
+ && !g_base->audio_server->shutting_down_) {
+ // Ok, here's where we might start needing to access our media... can't
+ // hold off any longer...
+ (**source_sound_).Load();
+
+ is_streamed_ = (**source_sound_).is_streamed();
+ current_is_music_ = is_music_;
+
+ if (is_streamed_) {
+ streamer_ = Object::New(
+ (**source_sound_).file_name_full().c_str(), source_, looping_);
+ } else {
+ alSourcei(source_, AL_BUFFER,
+ static_cast((**source_sound_).buffer()));
+ }
+ CHECK_AL_ERROR;
+
+ // Always update our volume and pitch here (we may be changing from
+ // music to nonMusic, etc.)
+ UpdateVolume();
+ UpdatePitch();
+
+ bool music_should_play = ((g_base->audio_server->music_volume_ > 0.000001f)
+ && !g_base->audio_server->suspended_
+ && !g_base->audio_server->shutting_down_);
+ if ((!current_is_music_) || music_should_play) {
+ ExecPlay();
+ }
+ }
+ want_to_play_ = true;
+
+#endif // BA_ENABLE_AUDIO
+
+ return play_id();
+}
+
// Actually begin playback.
-void AudioServer::ThreadSource::ExecPlay() {
+void AudioServer::ThreadSource_::ExecPlay() {
#if BA_ENABLE_AUDIO
assert(g_core);
@@ -946,12 +1420,12 @@ void AudioServer::ThreadSource::ExecPlay() {
looping_ = false;
// Push us on the list of streaming sources if we're not on it.
- for (auto&& i : audio_thread_->streaming_sources_) {
+ for (auto&& i : audio_server_->streaming_sources_) {
if (i == this) {
throw Exception();
}
}
- audio_thread_->streaming_sources_.push_back(this);
+ audio_server_->streaming_sources_.push_back(this);
// Make sure stereo sounds aren't positional.
// This is default behavior on Mac/Win, but we enforce it for linux.
@@ -960,7 +1434,7 @@ void AudioServer::ThreadSource::ExecPlay() {
bool do_normal = true;
// In vr mode, play non-positional sounds positionally in space roughly
// where the menu is.
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
do_normal = false;
SetPositional(true);
SetPosition(0.0f, 4.5f, -3.0f);
@@ -992,92 +1466,19 @@ void AudioServer::ThreadSource::ExecPlay() {
#endif // BA_ENABLE_AUDIO
}
-auto AudioServer::ThreadSource::Play(const Object::Ref* sound)
- -> uint32_t {
-#if BA_ENABLE_AUDIO
-
- // FatalError("Testing other thread.");
-
- assert(g_base->InAudioThread());
- assert(sound->Exists());
-
- // Stop whatever we were doing.
- Stop();
-
- assert(source_sound_ == nullptr);
- source_sound_ = sound;
-
- if (!g_base->audio_server->paused()) {
- // Ok, here's where we might start needing to access our media... can't hold
- // off any longer...
- (**source_sound_).Load();
-
- is_streamed_ = (**source_sound_).is_streamed();
- current_is_music_ = is_music_;
-
- if (is_streamed_) {
- streamer_ = Object::New(
- (**source_sound_).file_name_full().c_str(), source_, looping_);
- } else {
- alSourcei(source_, AL_BUFFER,
- static_cast((**source_sound_).buffer()));
- }
- CHECK_AL_ERROR;
-
- // Always update our volume and pitch here (we may be changing from music to
- // nonMusic, etc.)
- UpdateVolume();
- UpdatePitch();
-
- bool music_should_play = ((g_base->audio_server->music_volume_ > 0.000001f)
- && !g_base->audio_server->paused());
- if ((!current_is_music_) || music_should_play) {
- ExecPlay();
- }
- }
- want_to_play_ = true;
-
-#endif // BA_ENABLE_AUDIO
-
- return play_id();
-}
-
-void AudioServer::ThreadSource::ExecStop() {
-#if BA_ENABLE_AUDIO
- assert(g_base->InAudioThread());
- assert(!g_base->audio_server->paused());
- assert(is_actually_playing_);
- if (streamer_.Exists()) {
- assert(is_streamed_);
- streamer_->Stop();
- for (auto i = audio_thread_->streaming_sources_.begin();
- i != audio_thread_->streaming_sources_.end(); ++i) {
- if (*i == this) {
- audio_thread_->streaming_sources_.erase(i);
- break;
- }
- }
- } else {
- alSourceStop(source_);
- CHECK_AL_ERROR;
- }
- CHECK_AL_ERROR;
- is_actually_playing_ = false;
-
-#endif // BA_ENABLE_AUDIO
-}
-
// Do a complete stop... take us off the music list, detach our source, etc.
-void AudioServer::ThreadSource::Stop() {
+void AudioServer::ThreadSource_::Stop() {
#if BA_ENABLE_AUDIO
assert(g_base->audio_server);
- // If our context is paused we can't actually stop now; just record our
+ // If our context is suspended we can't actually stop now; just record our
// intent.
- if (g_base->audio_server->paused()) {
+ if (g_base->audio_server->suspended_) {
want_to_play_ = false;
} else {
- if (is_actually_playing_) ExecStop();
+ if (is_actually_playing_) {
+ ExecStop();
+ }
if (streamer_.Exists()) {
streamer_.Clear();
}
@@ -1094,55 +1495,78 @@ void AudioServer::ThreadSource::Stop() {
#endif // BA_ENABLE_AUDIO
}
-void AudioServer::ThreadSource::UpdateVolume() {
+void AudioServer::ThreadSource_::ExecStop() {
#if BA_ENABLE_AUDIO
assert(g_base->InAudioThread());
- if (!g_base->audio_server->paused()) {
- float val = gain_ * fade_;
- if (current_is_music()) {
- val *= audio_thread_->music_volume() / 7.0f;
- } else {
- val *= audio_thread_->sound_volume();
+ assert(!g_base->audio_server->suspended_);
+ assert(is_actually_playing_);
+ if (streamer_.Exists()) {
+ assert(is_streamed_);
+ streamer_->Stop();
+ for (auto i = audio_server_->streaming_sources_.begin();
+ i != audio_server_->streaming_sources_.end(); ++i) {
+ if (*i == this) {
+ audio_server_->streaming_sources_.erase(i);
+ break;
+ }
}
- alSourcef(source_, AL_GAIN, std::max(0.0f, val));
+ } else {
+ alSourceStop(source_);
CHECK_AL_ERROR;
}
+ CHECK_AL_ERROR;
+ is_actually_playing_ = false;
+
#endif // BA_ENABLE_AUDIO
}
-void AudioServer::ThreadSource::UpdatePitch() {
+void AudioServer::ThreadSource_::UpdateVolume() {
#if BA_ENABLE_AUDIO
assert(g_base->InAudioThread());
- if (!g_base->audio_server->paused()) {
- float val = 1.0f;
- if (current_is_music()) {
- } else {
- val *= audio_thread_->sound_pitch();
- }
- alSourcef(source_, AL_PITCH, val);
- CHECK_AL_ERROR;
+ if (audio_server_->suspended_ || audio_server_->shutting_down_) {
+ return;
}
+ float val = gain_ * fade_;
+ val *= audio_server_->app_active_volume_;
+
+ if (current_is_music()) {
+ val *= audio_server_->music_volume_ / 7.0f;
+ } else {
+ val *= audio_server_->sound_volume_;
+ }
+ alSourcef(source_, AL_GAIN, std::max(0.0f, val));
+ CHECK_AL_ERROR;
+
+#endif // BA_ENABLE_AUDIO
+}
+
+void AudioServer::ThreadSource_::UpdatePitch() {
+#if BA_ENABLE_AUDIO
+ assert(g_base->InAudioThread());
+ if (g_base->audio_server->suspended_
+ || g_base->audio_server->shutting_down_) {
+ return;
+ }
+ float val = 1.0f;
+ if (current_is_music()) {
+ } else {
+ val *= audio_server_->sound_pitch_;
+ }
+ alSourcef(source_, AL_PITCH, val);
+ CHECK_AL_ERROR;
+
#endif // BA_ENABLE_AUDIO
}
void AudioServer::PushSetVolumesCall(float music_volume, float sound_volume) {
event_loop()->PushCall([this, music_volume, sound_volume] {
- SetSoundVolume(sound_volume);
- SetMusicVolume(music_volume);
+ SetSoundVolume_(sound_volume);
+ SetMusicVolume_(music_volume);
});
}
void AudioServer::PushSetSoundPitchCall(float val) {
- event_loop()->PushCall([this, val] { SetSoundPitch(val); });
-}
-
-void AudioServer::PushSetSuspendedCall(bool pause) {
- event_loop()->PushCall([this, pause] {
- if (g_buildconfig.ostype_android()) {
- Log(LogLevel::kError, "Shouldn't be getting SetPausedCall on android.");
- }
- SetPaused(pause);
- });
+ event_loop()->PushCall([this, val] { SetSoundPitch_(val); });
}
void AudioServer::PushComponentUnloadCall(
@@ -1164,7 +1588,7 @@ void AudioServer::PushComponentUnloadCall(
void AudioServer::PushHavePendingLoadsCall() {
event_loop()->PushCall([this] {
have_pending_loads_ = true;
- UpdateTimerInterval();
+ UpdateTimerInterval_();
});
}
@@ -1187,44 +1611,44 @@ void AudioServer::ClearSoundRefDeleteList() {
sound_ref_delete_list_.clear();
}
-void AudioServer::BeginInterruption() {
- assert(!g_base->InAudioThread());
- g_base->audio_server->PushSetSuspendedCall(true);
+// void AudioServer::BeginInterruption() {
+// assert(!g_base->InAudioThread());
+// g_base->audio_server->PushSetSuspendedCall(true);
- // Wait a reasonable amount of time for the thread to act on it.
- millisecs_t t = g_core->GetAppTimeMillisecs();
- while (true) {
- if (g_base->audio_server->paused()) {
- break;
- }
- if (g_core->GetAppTimeMillisecs() - t > 1000) {
- Log(LogLevel::kError, "Timed out waiting for audio pause.");
- break;
- }
- core::CorePlatform::SleepMillisecs(2);
- }
-}
+// // Wait a reasonable amount of time for the thread to act on it.
+// millisecs_t t = g_core->GetAppTimeMillisecs();
+// while (true) {
+// if (g_base->audio_server->suspended()) {
+// break;
+// }
+// if (g_core->GetAppTimeMillisecs() - t > 1000) {
+// Log(LogLevel::kError, "Timed out waiting for audio suspend.");
+// break;
+// }
+// core::CorePlatform::SleepMillisecs(2);
+// }
+// }
-void AudioServer::OnThreadPause() { SetPaused(true); }
+// void AudioServer::EndInterruption() {
+// assert(!g_base->InAudioThread());
+// g_base->audio_server->PushSetSuspendedCall(false);
-void AudioServer::OnThreadResume() { SetPaused(false); }
+// // Wait a reasonable amount of time for the thread to act on it.
+// millisecs_t t = g_core->GetAppTimeMillisecs();
+// while (true) {
+// if (!g_base->audio_server->suspended()) {
+// break;
+// }
+// if (g_core->GetAppTimeMillisecs() - t > 1000) {
+// Log(LogLevel::kError, "Timed out waiting for audio unsuspend.");
+// break;
+// }
+// core::CorePlatform::SleepMillisecs(2);
+// }
+// }
-void AudioServer::EndInterruption() {
- assert(!g_base->InAudioThread());
- g_base->audio_server->PushSetSuspendedCall(false);
+void AudioServer::OnThreadSuspend_() { SetSuspended_(true); }
- // Wait a reasonable amount of time for the thread to act on it.
- millisecs_t t = g_core->GetAppTimeMillisecs();
- while (true) {
- if (!g_base->audio_server->paused()) {
- break;
- }
- if (g_core->GetAppTimeMillisecs() - t > 1000) {
- Log(LogLevel::kError, "Timed out waiting for audio unpause.");
- break;
- }
- core::CorePlatform::SleepMillisecs(2);
- }
-}
+void AudioServer::OnThreadUnsuspend_() { SetSuspended_(false); }
} // namespace ballistica::base
diff --git a/src/ballistica/base/audio/audio_server.h b/src/ballistica/base/audio/audio_server.h
index 5c5202ec..411cd3cf 100644
--- a/src/ballistica/base/audio/audio_server.h
+++ b/src/ballistica/base/audio/audio_server.h
@@ -12,14 +12,14 @@
namespace ballistica::base {
-/// A module that handles audio processing.
+/// Wrangles audio off in its own thread.
class AudioServer {
public:
- static auto source_id_from_play_id(uint32_t play_id) -> uint32_t {
+ static auto SourceIdFromPlayId(uint32_t play_id) -> uint32_t {
return play_id & 0xFFFFu;
}
- static auto play_count_from_play_id(uint32_t play_id) -> uint32_t {
+ static auto PlayCountFromPlayId(uint32_t play_id) -> uint32_t {
return play_id >> 16u;
}
@@ -28,10 +28,6 @@ class AudioServer {
void PushSetVolumesCall(float music_volume, float sound_volume);
void PushSetSoundPitchCall(float val);
- void PushSetSuspendedCall(bool pause);
-
- static void BeginInterruption();
- static void EndInterruption();
void PushSetListenerPositionCall(const Vector3f& p);
void PushSetListenerOrientationCall(const Vector3f& forward,
@@ -41,10 +37,12 @@ class AudioServer {
void PushComponentUnloadCall(
const std::vector*>& components);
- /// For use by g_logic_module().
void ClearSoundRefDeleteList();
- auto paused() const -> bool { return paused_; }
+ auto paused() const -> bool { return suspended_; }
+
+ void Shutdown();
+ auto shutdown_completed() const { return shutdown_completed_; }
// Client sources use these to pass settings to the server.
void PushSourceSetIsMusicCall(uint32_t play_id, bool val);
@@ -66,38 +64,41 @@ class AudioServer {
auto event_loop() const -> EventLoop* { return event_loop_; }
- private:
- class ThreadSource;
- struct Impl;
+ void OnDeviceDisconnected();
+ void OpenALSoftLogCallback(const std::string& msg);
- void OnAppStartInThread();
+ private:
+ class ThreadSource_;
+ struct Impl_;
+
+ void OnAppStartInThread_();
~AudioServer();
- void OnThreadPause();
- void OnThreadResume();
+ void OnThreadSuspend_();
+ void OnThreadUnsuspend_();
- void SetPaused(bool paused);
+ void SetSuspended_(bool suspended);
- void SetMusicVolume(float volume);
- void SetSoundVolume(float volume);
- void SetSoundPitch(float pitch);
- auto music_volume() -> float { return music_volume_; }
- auto sound_volume() -> float { return sound_volume_; }
- auto sound_pitch() -> float { return sound_pitch_; }
+ void SetMusicVolume_(float volume);
+ void SetSoundVolume_(float volume);
+ void SetSoundPitch_(float pitch);
+
+ void CompleteShutdown_();
/// If a sound play id is currently playing, return the sound.
- auto GetPlayingSound(uint32_t play_id) -> ThreadSource*;
+ auto GetPlayingSound_(uint32_t play_id) -> ThreadSource_*;
- void Reset();
- void Process();
+ void Reset_();
+ void Process_();
+ void ProcessDeviceDisconnects_(seconds_t real_time_seconds);
/// Send a component to the audio thread to delete.
- void DeleteAssetComponent(Asset* c);
+ // void DeleteAssetComponent_(Asset* c);
- void UpdateTimerInterval();
- void UpdateAvailableSources();
- void UpdateMusicPlayState();
- void ProcessSoundFades();
+ void UpdateTimerInterval_();
+ void UpdateAvailableSources_();
+ void UpdateMusicPlayState_();
+ void ProcessSoundFades_();
// Some threads such as audio hold onto allocated Media-Component-Refs to keep
// media components alive that they need. Media-Component-Refs, however, must
@@ -107,32 +108,49 @@ class AudioServer {
// Note: should use unique_ptr for this, but build fails on raspberry pi
// (gcc 8.3.0). Works on Ubuntu 9.3 so should try again later.
- // std::unique_ptr impl_{};
- Impl* impl_{};
+ std::unique_ptr impl_{};
+ // Impl* impl_{};
EventLoop* event_loop_{};
Timer* process_timer_{};
- bool have_pending_loads_{};
- bool paused_{};
- millisecs_t last_sound_fade_process_time_{};
-
float sound_volume_{1.0f};
float sound_pitch_{1.0f};
float music_volume_{1.0f};
+ float app_active_volume_{1.0f};
+
+ bool have_pending_loads_{};
+ bool app_active_{true};
+ bool suspended_{};
+ bool shutdown_completed_{};
+ bool shutting_down_{};
+ bool shipped_reconnect_logs_{};
+ // bool report_reset_results_{};
+ // int reset_result_reports_remaining_{3};
+ // int reconnect_fail_count_{};
+ int al_source_count_{};
+ seconds_t last_connected_time_{};
+ seconds_t last_reset_attempt_time_{-999.0};
+ seconds_t shutdown_start_time_{};
+ seconds_t last_started_playing_time_{};
+ millisecs_t last_sound_fade_process_time_{};
+
+ std::mutex openalsoft_android_log_mutex_;
+ std::string openalsoft_android_log_;
/// Indexed list of sources.
- std::vector sources_;
- std::vector streaming_sources_;
+ std::vector sources_;
+ std::vector streaming_sources_;
millisecs_t last_stream_process_time_{};
+ millisecs_t last_sanity_check_time_{};
// Holds refs to all sources.
// Use sources, not this, for faster iterating.
- std::vector > sound_source_refs_;
- struct SoundFadeNode;
+ std::vector> sound_source_refs_;
+ struct SoundFadeNode_;
// NOTE: would use unordered_map here but gcc doesn't seem to allow
// forward-declared template params with them.
- std::map sound_fade_nodes_;
+ std::map sound_fade_nodes_;
// This mutex controls access to our list of media component shared ptrs to
// delete in the main thread.
@@ -140,10 +158,6 @@ class AudioServer {
// Our list of sound media components to delete via the main thread.
std::vector*> sound_ref_delete_list_;
-
- millisecs_t last_sanity_check_time_{};
-
- static int al_source_count_;
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/audio/audio_source.cc b/src/ballistica/base/audio/audio_source.cc
index 541d6d89..07d0bbdf 100644
--- a/src/ballistica/base/audio/audio_source.cc
+++ b/src/ballistica/base/audio/audio_source.cc
@@ -15,7 +15,7 @@ AudioSource::AudioSource(int id_in) : id_(id_in) {}
AudioSource::~AudioSource() { assert(client_queue_size_ == 0); }
void AudioSource::MakeAvailable(uint32_t play_id_new) {
- assert(AudioServer::source_id_from_play_id(play_id_new) == id_);
+ assert(AudioServer::SourceIdFromPlayId(play_id_new) == id_);
assert(client_queue_size_ == 0);
assert(locked());
play_id_ = play_id_new;
diff --git a/src/ballistica/base/audio/audio_streamer.cc b/src/ballistica/base/audio/audio_streamer.cc
index 60e03fa5..bc6f1488 100644
--- a/src/ballistica/base/audio/audio_streamer.cc
+++ b/src/ballistica/base/audio/audio_streamer.cc
@@ -68,7 +68,9 @@ void AudioStreamer::Stop() {
}
void AudioStreamer::Update() {
- if (eof_) return;
+ if (eof_) {
+ return;
+ }
CHECK_AL_ERROR;
diff --git a/src/ballistica/base/audio/audio_streamer.h b/src/ballistica/base/audio/audio_streamer.h
index e4104fa6..37467d15 100644
--- a/src/ballistica/base/audio/audio_streamer.h
+++ b/src/ballistica/base/audio/audio_streamer.h
@@ -23,17 +23,18 @@ class AudioStreamer : public Object {
auto Play() -> bool;
void Stop();
void Update();
- enum Format { INVALID_FORMAT, MONO16_FORMAT, STEREO16_FORMAT };
+ enum class Format : uint8_t { kInvalid, kMono16, kStereo16 };
auto al_format() const -> ALenum {
switch (format_) {
- case MONO16_FORMAT:
+ case Format::kMono16:
return AL_FORMAT_MONO16;
- case STEREO16_FORMAT:
+ case Format::kStereo16:
return AL_FORMAT_STEREO16;
default:
break;
}
- return INVALID_FORMAT;
+ FatalError("Invalid AL format.");
+ return AL_FORMAT_MONO16;
}
auto loops() const -> bool { return loops_; }
auto file_name() const -> const std::string& { return file_name_; }
@@ -46,13 +47,13 @@ class AudioStreamer : public Object {
void set_format(Format format) { format_ = format; }
private:
- Format format_ = INVALID_FORMAT;
- bool playing_ = false;
+ Format format_{Format::kInvalid};
+ bool playing_{};
+ bool loops_{};
+ bool eof_{};
ALuint buffers_[kAudioStreamBufferCount]{};
- ALuint source_ = 0;
+ ALuint source_{};
std::string file_name_;
- bool loops_ = false;
- bool eof_ = false;
};
#endif // BA_ENABLE_AUDIO
diff --git a/src/ballistica/base/audio/ogg_stream.cc b/src/ballistica/base/audio/ogg_stream.cc
index abe45317..5b51d427 100644
--- a/src/ballistica/base/audio/ogg_stream.cc
+++ b/src/ballistica/base/audio/ogg_stream.cc
@@ -53,9 +53,9 @@ OggStream::OggStream(const char* file_name, ALuint source, bool loop)
vorbis_info_ = ov_info(&ogg_file_, -1);
if (vorbis_info_->channels == 1) {
- set_format(MONO16_FORMAT);
+ set_format(Format::kMono16);
} else {
- set_format(STEREO16_FORMAT);
+ set_format(Format::kStereo16);
}
}
diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc
index dcedd3c3..09dbf208 100644
--- a/src/ballistica/base/base.cc
+++ b/src/ballistica/base/base.cc
@@ -9,6 +9,7 @@
#include "ballistica/base/audio/audio_server.h"
#include "ballistica/base/dynamics/bg/bg_dynamics_server.h"
#include "ballistica/base/graphics/graphics_server.h"
+#include "ballistica/base/graphics/support/screen_messages.h"
#include "ballistica/base/graphics/text/text_graphics.h"
#include "ballistica/base/input/input.h"
#include "ballistica/base/logic/logic.h"
@@ -20,12 +21,12 @@
#include "ballistica/base/python/class/python_class_feature_set_data.h"
#include "ballistica/base/python/support/python_context_call.h"
#include "ballistica/base/support/app_config.h"
+#include "ballistica/base/support/app_timer.h"
+#include "ballistica/base/support/base_build_switches.h"
#include "ballistica/base/support/huffman.h"
#include "ballistica/base/support/plus_soft.h"
#include "ballistica/base/support/stdio_console.h"
-#include "ballistica/base/support/stress_test.h"
#include "ballistica/base/ui/dev_console.h"
-#include "ballistica/base/ui/ui.h"
#include "ballistica/base/ui/ui_delegate.h"
#include "ballistica/core/python/core_python.h"
#include "ballistica/shared/foundation/event_loop.h"
@@ -39,7 +40,7 @@ core::CoreFeatureSet* g_core{};
BaseFeatureSet* g_base{};
BaseFeatureSet::BaseFeatureSet()
- : app_adapter{AppAdapter::Create()},
+ : app_adapter{BaseBuildSwitches::CreateAppAdapter()},
app_config{new AppConfig()},
app_mode_{AppModeEmpty::GetSingleton()},
assets{new Assets()},
@@ -51,7 +52,7 @@ BaseFeatureSet::BaseFeatureSet()
bg_dynamics_server{g_core->HeadlessMode() ? nullptr
: new BGDynamicsServer},
context_ref{new ContextRef(nullptr)},
- graphics{Graphics::Create()},
+ graphics{BaseBuildSwitches::CreateGraphics()},
graphics_server{new GraphicsServer()},
huffman{new Huffman()},
input{new Input()},
@@ -59,11 +60,10 @@ BaseFeatureSet::BaseFeatureSet()
network_reader{new NetworkReader()},
network_writer{new NetworkWriter()},
networking{new Networking()},
- platform{BasePlatform::Create()},
+ platform{BaseBuildSwitches::CreatePlatform()},
python{new BasePython()},
stdio_console{g_buildconfig.enable_stdio_console() ? new StdioConsole()
: nullptr},
- stress_test_{new StressTest()},
text_graphics{new TextGraphics()},
ui{new UI()},
utils{new Utils()} {
@@ -151,6 +151,43 @@ auto BaseFeatureSet::IsBaseCompletelyImported() -> bool {
return base_import_completed_ && base_native_import_completed_;
}
+void BaseFeatureSet::SuccessScreenMessage() {
+ if (auto* event_loop = logic->event_loop()) {
+ event_loop->PushCall([this] {
+ python->objs().Get(BasePython::ObjID::kSuccessMessageCall).Call();
+ });
+ } else {
+ Log(LogLevel::kError,
+ "SuccessScreenMessage called without logic event_loop in place.");
+ }
+}
+
+void BaseFeatureSet::ErrorScreenMessage() {
+ if (auto* event_loop = logic->event_loop()) {
+ event_loop->PushCall([this] {
+ python->objs().Get(BasePython::ObjID::kErrorMessageCall).Call();
+ });
+ } else {
+ Log(LogLevel::kError,
+ "ErrorScreenMessage called without logic event_loop in place.");
+ }
+}
+
+auto BaseFeatureSet::GetV2AccountID() -> std::optional {
+ auto gil = Python::ScopedInterpreterLock();
+ auto result =
+ python->objs().Get(BasePython::ObjID::kGetV2AccountIdCall).Call();
+ if (result.Exists()) {
+ if (result.ValueIsNone()) {
+ return {};
+ }
+ return result.ValueAsString();
+ } else {
+ Log(LogLevel::kError, "GetV2AccountID() py call errored.");
+ return {};
+ }
+}
+
void BaseFeatureSet::OnAssetsAvailable() {
assert(InLogicThread());
@@ -158,9 +195,17 @@ void BaseFeatureSet::OnAssetsAvailable() {
}
void BaseFeatureSet::StartApp() {
+ // {
+ // // TEST - recreate the ID python dumps in its thread tracebacks.
+ // auto val = PyThread_get_thread_ident();
+ // printf("MAIN THREAD IS %#018lx\n", val);
+ // }
+
BA_PRECONDITION(g_core->InMainThread());
BA_PRECONDITION(g_base);
+ auto start_time = g_core->GetAppTimeSeconds();
+
// Currently limiting this to once per process.
BA_PRECONDITION(!called_start_app_);
called_start_app_ = true;
@@ -197,14 +242,11 @@ void BaseFeatureSet::StartApp() {
assets_server->OnMainThreadStartApp();
app_adapter->OnMainThreadStartApp();
- // Take note that we're now 'running'. Various code such as anything that
- // pushes messages to threads can watch for this state to avoid crashing
- // if called early.
+ // Ok; we're now official 'started'. Various code such as anything that
+ // pushes messages to threads can watch for this state (via IsAppStarted()
+ // to avoid crashing if called early.
app_started_ = true;
- // Inform anyone who wants to know that we're done starting.
- platform->OnMainThreadStartAppComplete();
-
// As the last step of this phase, tell the logic thread to apply the app
// config which will kick off screen creation and otherwise get the ball
// rolling.
@@ -214,6 +256,159 @@ void BaseFeatureSet::StartApp() {
}
g_core->LifecycleLog("start-app end (main thread)");
+
+ // Make some noise if this takes more than a few seconds. If we pass 5
+ // seconds or so we start to trigger App-Not-Responding reports which
+ // isn't good.
+ auto duration = g_core->GetAppTimeSeconds() - start_time;
+ if (duration > 3.0) {
+ char buffer[128];
+ snprintf(buffer, sizeof(buffer),
+ "StartApp() took too long (%.2lf seconds).", duration);
+ Log(LogLevel::kWarning, buffer);
+ }
+}
+
+void BaseFeatureSet::SuspendApp() {
+ assert(g_core);
+ assert(g_core->InMainThread());
+
+ if (app_suspended_) {
+ Log(LogLevel::kWarning,
+ "AppAdapter::SuspendApp() called with app already suspended.");
+ return;
+ }
+
+ millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()};
+
+ // Apple mentioned 5 seconds to run stuff once backgrounded or they bring
+ // down the hammer. Let's aim to stay under 2.
+ millisecs_t max_duration{2000};
+
+ g_core->platform->LowLevelDebugLog(
+ "SuspendApp@"
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
+ app_suspended_ = true;
+
+ // IMPORTANT: Any pause related stuff that event-loop-threads need to do
+ // should be done from their registered pause-callbacks. If we instead
+ // push runnables to them from here they may or may not be called before
+ // their event-loop is actually paused.
+
+ // Pause all event loops.
+ EventLoop::SetEventLoopsSuspended(true);
+
+ if (g_base->network_reader) {
+ g_base->network_reader->OnAppSuspend();
+ }
+ g_base->networking->OnAppSuspend();
+
+ // We assume that the OS will completely suspend our process the moment we
+ // return from this call (though this is not technically true on all
+ // platforms). So we want to spin here and give our various event loop
+ // threads time to park themselves.
+ std::vector running_loops;
+ do {
+ // If/when we get to a point with no threads waiting to be paused, we're
+ // good to go.
+ // auto loops{EventLoop::GetStillSuspendingEventLoops()};
+ running_loops = EventLoop::GetStillSuspendingEventLoops();
+ // running_loop_count = loops.size();
+ if (running_loops.empty()) {
+ if (g_buildconfig.debug_build()) {
+ Log(LogLevel::kDebug,
+ "SuspendApp() completed in "
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs()
+ - start_time)
+ + "ms.");
+ }
+ return;
+ }
+ } while (std::abs(core::CorePlatform::GetCurrentMillisecs() - start_time)
+ < max_duration);
+
+ // If we made it here, we timed out. Complain.
+ std::string msg =
+ std::string("SuspendApp() took too long; ")
+ + std::to_string(running_loops.size())
+ + " event-loops not yet suspended after "
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs() - start_time)
+ + " ms: (";
+ bool first = true;
+ for (auto* loop : running_loops) {
+ if (!first) {
+ msg += ", ";
+ }
+ // Note: not adding a default here so compiler complains if we
+ // add/change something.
+ switch (loop->identifier()) {
+ case EventLoopID::kInvalid:
+ msg += "invalid";
+ break;
+ case EventLoopID::kLogic:
+ msg += "logic";
+ break;
+ case EventLoopID::kAssets:
+ msg += "assets";
+ break;
+ case EventLoopID::kFileOut:
+ msg += "fileout";
+ break;
+ case EventLoopID::kMain:
+ msg += "main";
+ break;
+ case EventLoopID::kAudio:
+ msg += "audio";
+ break;
+ case EventLoopID::kNetworkWrite:
+ msg += "networkwrite";
+ break;
+ case EventLoopID::kSuicide:
+ msg += "suicide";
+ break;
+ case EventLoopID::kStdin:
+ msg += "stdin";
+ break;
+ case EventLoopID::kBGDynamics:
+ msg += "bgdynamics";
+ break;
+ }
+ first = false;
+ }
+ msg += ").";
+
+ Log(LogLevel::kError, msg);
+}
+
+void BaseFeatureSet::UnsuspendApp() {
+ assert(g_core);
+ assert(g_core->InMainThread());
+
+ if (!app_suspended_) {
+ Log(LogLevel::kWarning,
+ "AppAdapter::UnsuspendApp() called with app not in suspendedstate.");
+ return;
+ }
+ millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()};
+ g_core->platform->LowLevelDebugLog(
+ "UnsuspendApp@"
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
+ app_suspended_ = false;
+
+ // Spin all event-loops back up.
+ EventLoop::SetEventLoopsSuspended(false);
+
+ // Run resumes that expect to happen in the main thread.
+ g_base->network_reader->OnAppUnsuspend();
+ g_base->networking->OnAppUnsuspend();
+
+ if (g_buildconfig.debug_build()) {
+ Log(LogLevel::kDebug,
+ "UnsuspendApp() completed in "
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs()
+ - start_time)
+ + "ms.");
+ }
}
void BaseFeatureSet::OnAppShutdownComplete() {
@@ -420,6 +615,10 @@ auto BaseFeatureSet::IsUnmodifiedBlessedBuild() -> bool {
return false;
}
+auto BaseFeatureSet::InMainThread() const -> bool {
+ return g_core->InMainThread();
+}
+
auto BaseFeatureSet::InAssetsThread() const -> bool {
if (auto* loop = assets_server->event_loop()) {
return loop->ThreadIsCurrent();
@@ -461,8 +660,9 @@ auto BaseFeatureSet::InGraphicsContext() const -> bool {
void BaseFeatureSet::ScreenMessage(const std::string& s,
const Vector3f& color) {
- logic->event_loop()->PushCall(
- [this, s, color] { graphics->AddScreenMessage(s, color); });
+ logic->event_loop()->PushCall([this, s, color] {
+ graphics->screenmessages->AddScreenMessage(s, color);
+ });
}
void BaseFeatureSet::DoV1CloudLog(const std::string& msg) {
@@ -658,6 +858,10 @@ void BaseFeatureSet::DoPushObjCall(const PythonObjectSetBase* objset, int id,
auto BaseFeatureSet::IsAppStarted() const -> bool { return app_started_; }
+auto BaseFeatureSet::IsAppBootstrapped() const -> bool {
+ return logic->app_bootstrapping_complete();
+}
+
auto BaseFeatureSet::ShutdownSuppressBegin() -> bool {
std::scoped_lock lock(shutdown_suppress_lock_);
@@ -687,8 +891,6 @@ void BaseFeatureSet::ShutdownSuppressDisallow() {
shutdown_suppress_disallowed_ = true;
}
-auto BaseFeatureSet::GetReturnValue() const -> int { return return_value(); }
-
void BaseFeatureSet::QuitApp(bool confirm, QuitType quit_type) {
// If they want a confirm dialog and we're able to present one, do that.
if (confirm && !g_core->HeadlessMode() && !g_base->input->IsInputLocked()
@@ -713,4 +915,71 @@ void BaseFeatureSet::QuitApp(bool confirm, QuitType quit_type) {
}
}
+void BaseFeatureSet::PushMainThreadRunnable(Runnable* runnable) {
+ app_adapter->DoPushMainThreadRunnable(runnable);
+}
+
+auto BaseFeatureSet::ClipboardIsSupported() -> bool {
+ // We only call our actual virtual function once.
+ if (!have_clipboard_is_supported_) {
+ clipboard_is_supported_ = app_adapter->DoClipboardIsSupported();
+ have_clipboard_is_supported_ = true;
+ }
+ return clipboard_is_supported_;
+}
+
+auto BaseFeatureSet::ClipboardHasText() -> bool {
+ // If subplatform says they don't support clipboards, don't even ask.
+ if (!ClipboardIsSupported()) {
+ return false;
+ }
+ return app_adapter->DoClipboardHasText();
+}
+
+void BaseFeatureSet::ClipboardSetText(const std::string& text) {
+ // If subplatform says they don't support clipboards, this is an error.
+ if (!ClipboardIsSupported()) {
+ throw Exception("ClipboardSetText called with no clipboard support.",
+ PyExcType::kRuntime);
+ }
+ app_adapter->DoClipboardSetText(text);
+}
+
+auto BaseFeatureSet::ClipboardGetText() -> std::string {
+ // If subplatform says they don't support clipboards, this is an error.
+ if (!ClipboardIsSupported()) {
+ throw Exception("ClipboardGetText called with no clipboard support.",
+ PyExcType::kRuntime);
+ }
+ return app_adapter->DoClipboardGetText();
+}
+
+void BaseFeatureSet::SetAppActive(bool active) {
+ assert(InMainThread());
+
+ // Note: in some cases I'm seeing repeat active/inactive sets; for example
+ // on Mac SDL if I hide the app and then click on it in the dock I get a
+ // 'inactive' for the hide followed by a 'active', 'inactive', 'active' on
+ // the dock click. So our strategy here to filter that out is just to tell
+ // the logic thread that it has changed but have them directly read the
+ // shared atomic value, so they should generally skip over flip-flops like
+ // that and will just read the final value a few times in a row.
+
+ g_core->platform->LowLevelDebugLog(
+ "SetAppActive(" + std::to_string(active) + ")@"
+ + std::to_string(core::CorePlatform::GetCurrentMillisecs()));
+
+ // Issue a gentle warning if they are feeding us the same state twice in a
+ // row; might imply faulty logic on an app-adapter or whatnot.
+ if (app_active_set_ && app_active_ == active) {
+ Log(LogLevel::kWarning, "SetAppActive called with state "
+ + std::to_string(active) + " twice in a row.");
+ }
+ app_active_set_ = true;
+ app_active_ = active;
+
+ g_base->logic->event_loop()->PushCall(
+ [] { g_base->logic->OnAppActiveChanged(); });
+}
+
} // namespace ballistica::base
diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h
index b45aa0b6..00ac6262 100644
--- a/src/ballistica/base/base.h
+++ b/src/ballistica/base/base.h
@@ -3,6 +3,7 @@
#ifndef BALLISTICA_BASE_BASE_H_
#define BALLISTICA_BASE_BASE_H_
+#include
#include
#include
#include
@@ -53,12 +54,15 @@ class ClassicSoftInterface;
class CollisionMeshAsset;
class CollisionCache;
class DevConsole;
+class DisplayTimer;
class Context;
class ContextRef;
class DataAsset;
class FrameDef;
class Graphics;
class GraphicsServer;
+struct GraphicsSettings;
+struct GraphicsClientContext;
class Huffman;
class ImageMesh;
class Input;
@@ -87,6 +91,7 @@ class NetGraph;
class Networking;
class NetworkReader;
class NetworkWriter;
+class NinePatchMesh;
class ObjectComponent;
class PythonClassUISound;
class PythonContextCall;
@@ -97,13 +102,14 @@ class RenderPass;
class RenderTarget;
class RemoteAppServer;
class RemoteControlInput;
+class Repeater;
class ScoreToBeat;
+class ScreenMessages;
class AppAdapterSDL;
class SDLContext;
class SoundAsset;
class SpriteMesh;
class StdioConsole;
-class StressTest;
class Module;
class TestInput;
class TextGroup;
@@ -267,6 +273,7 @@ enum class TextureCompressionType : uint8_t {
kPVR,
kETC1,
kETC2,
+ kASTC,
};
enum class TextureMinQuality : uint8_t {
@@ -603,14 +610,38 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// Start app systems in motion.
void StartApp() override;
- /// Issue a high level app quit request. Can be called from any thread.
- /// 'soft' means the app can simply reset/hide itself instead of actually
- /// exiting the process (common behavior on mobile platforms). 'back'
- /// means that a soft-quit should behave as if a back-button was pressed,
- /// which may trigger different behavior in the OS than a standard soft
- /// quit. If 'confirm' is true, a confirmation dialog will be presented if
- /// the current app-mode provides one and the app is in gui mode.
- /// Otherwise the quit will be immediate.
+ /// Set the app's active state. Should be called from the main thread.
+ /// Generally called by the AppAdapter. Being inactive means the app
+ /// experience is not front and center and thus it may want to throttle
+ /// down its rendering rate, pause single play gameplay, etc. This does
+ /// not, however, cause any extreme action such as halting event loops;
+ /// use Suspend/Resume for that. And note that the app may still be
+ /// visible while inactive, so it should not *completely* stop
+ /// drawing/etc.
+ void SetAppActive(bool active);
+
+ /// Put the app into a suspended state. Should be called from the main
+ /// thread. Generally called by the AppAdapter. Suspends event loops,
+ /// closes network sockets, etc. Generally corresponds to being
+ /// backgrounded on mobile platforms. It is assumed that, as soon as this
+ /// call returns, all engine work is finished and all threads can be
+ /// immediately suspended by the OS without any problems.
+ void SuspendApp();
+
+ /// Return the app to a running state from a suspended one. Can correspond
+ /// to foregrounding on mobile, unminimizing on desktop, etc. Spins
+ /// threads back up, re-opens network sockets, etc.
+ void UnsuspendApp();
+
+ auto app_suspended() const { return app_suspended_; }
+
+ /// Issue a high level app quit request. Can be called from any thread and
+ /// can be safely called repeatedly. If 'confirm' is true, a confirmation
+ /// dialog will be presented if the environment and situation allows;
+ /// otherwise the quit process will start immediately. A QuitType arg can
+ /// optionally be passed to influence quit behavior; on some platforms
+ /// such as mobile the default is for the app to recede to the background
+ /// but physically remain running.
void QuitApp(bool confirm = false, QuitType quit_type = QuitType::kSoft);
/// Called when app shutdown process completes. Sets app to exit.
@@ -629,6 +660,13 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
return *context_ref;
}
+ /// Utility call to print 'Success!' with a happy sound.
+ /// Safe to call from any thread.
+ void SuccessScreenMessage();
+ /// Utility call to print 'Error.' with a beep sound.
+ /// Safe to call from any thread.
+ void ErrorScreenMessage();
+
void SetCurrentContext(const ContextRef& context);
/// Try to load the plus feature-set and return whether it is available.
@@ -663,6 +701,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// allowing certain functionality before this time.
auto IsBaseCompletelyImported() -> bool;
+ auto InMainThread() const -> bool;
auto InAssetsThread() const -> bool override;
auto InLogicThread() const -> bool override;
auto InAudioThread() const -> bool override;
@@ -673,11 +712,18 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// High level screen-message call usable from any thread.
void ScreenMessage(const std::string& s, const Vector3f& color) override;
- /// Has StartApp been called (and completely finished its work)?
- /// Code that sends calls/messages to other threads or otherwise uses
- /// app functionality may want to check this to avoid crashes.
+ /// Has StartApp been called (and completely finished its work)? Code that
+ /// sends calls/messages to other threads or otherwise uses app
+ /// functionality may want to check this to avoid crashes. Note that some
+ /// app functionality such as loading assets is not available until
+ /// IsAppBootstrapped returns true. This call is thread safe.
auto IsAppStarted() const -> bool override;
+ /// Has the app bootstrapping phase completed? The bootstrapping phase
+ /// involves initial screen/graphics setup. Asset loading is not allowed
+ /// until it is complete.
+ auto IsAppBootstrapped() const -> bool override;
+
void PlusDirectSendV1CloudLogs(const std::string& prefix,
const std::string& suffix, bool instant,
int* result) override;
@@ -712,6 +758,28 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// loading.
void OnAssetsAvailable();
+ void PushMainThreadRunnable(Runnable* runnable) override;
+
+ /// Return the currently signed in V2 account id as
+ /// reported by the Python layer.
+ auto GetV2AccountID() -> std::optional;
+
+ /// Return whether clipboard operations are supported at all. This gets
+ /// called when determining whether to display clipboard related UI
+ /// elements/etc.
+ auto ClipboardIsSupported() -> bool;
+
+ /// Return whether there is currently text on the clipboard.
+ auto ClipboardHasText() -> bool;
+
+ /// Set current clipboard text. Raises an Exception if clipboard is
+ /// unsupported.
+ void ClipboardSetText(const std::string& text);
+
+ /// Return current text from the clipboard. Raises an Exception if
+ /// clipboard is unsupported or if there's no text on the clipboard.
+ auto ClipboardGetText() -> std::string;
+
// Const subsystems.
AppAdapter* const app_adapter;
AppConfig* const app_config;
@@ -741,8 +809,6 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
void set_app_mode(AppMode* mode);
auto* app_mode() const { return app_mode_; }
- auto* stress_test() const { return stress_test_; }
-
/// Whether we're running under ballisticakit_server.py
/// (affects some app behavior).
auto server_wrapper_managed() { return server_wrapper_managed_; }
@@ -750,10 +816,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
// Non-const bits (fixme: clean up access to these).
TouchInput* touch_input{};
- auto return_value() const { return return_value_; }
- void set_return_value(int val) { return_value_ = val; }
-
- auto GetReturnValue() const -> int override;
+ auto app_active() -> bool const { return app_active_; }
private:
BaseFeatureSet();
@@ -765,11 +828,12 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
AppMode* app_mode_;
PlusSoftInterface* plus_soft_{};
ClassicSoftInterface* classic_soft_{};
- StressTest* stress_test_;
-
std::mutex shutdown_suppress_lock_;
+ bool have_clipboard_is_supported_{};
+ bool clipboard_is_supported_{};
+ bool app_active_set_{};
+ bool app_suspended_{};
bool shutdown_suppress_disallowed_{};
- int shutdown_suppress_count_{};
bool tried_importing_plus_{};
bool tried_importing_classic_{};
bool tried_importing_ui_v1_{};
@@ -780,7 +844,11 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
bool base_native_import_completed_{};
bool basn_log_behavior_{};
bool server_wrapper_managed_{};
- int return_value_{};
+ /// Main thread informs logic thread when this changes, but then logic
+ /// reads original value set by main. need to be sure they never read
+ /// stale values.
+ std::atomic_bool app_active_{true};
+ int shutdown_suppress_count_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/dynamics/bg/bg_dynamics.cc b/src/ballistica/base/dynamics/bg/bg_dynamics.cc
index a1b82815..231a964b 100644
--- a/src/ballistica/base/dynamics/bg/bg_dynamics.cc
+++ b/src/ballistica/base/dynamics/bg/bg_dynamics.cc
@@ -39,10 +39,15 @@ void BGDynamics::Emit(const BGDynamicsEmission& e) {
void BGDynamics::Step(const Vector3f& cam_pos, int step_millisecs) {
assert(g_base->InLogicThread());
+ // Don't actually start doing anything until there's a
+ // client-graphics-context. We need this to calculate qualities/etc.
+ if (!g_base->graphics->has_client_context()) {
+ return;
+ }
+
// The BG dynamics thread just processes steps as fast as it can;
// we need to throttle what we send or tell it to cut back if its behind
int step_count = g_base->bg_dynamics_server->step_count();
- // printf("STEP COUNT %d\n", step_count);
// If we're really getting behind, start pruning stuff.
if (step_count > 3) {
@@ -62,6 +67,9 @@ void BGDynamics::Step(const Vector3f& cam_pos, int step_millisecs) {
// Pass a newly allocated raw pointer to the bg-dynamics thread; it takes care
// of disposing it when done.
auto d = Object::NewDeferred();
+ d->graphics_quality = Graphics::GraphicsQualityFromRequest(
+ g_base->graphics->settings()->graphics_quality,
+ g_base->graphics->client_context()->auto_graphics_quality);
d->step_millisecs = step_millisecs;
d->cam_pos = cam_pos;
@@ -174,7 +182,7 @@ void BGDynamics::Draw(FrameDef* frame_def) {
// In high-quality, we draw in the overlay pass so that we don't get wiped
// out by depth-of-field.
- bool draw_in_overlay = (frame_def->quality() >= GraphicsQuality::kHigh);
+ bool draw_in_overlay = frame_def->quality() >= GraphicsQuality::kHigh;
SpriteComponent c(draw_in_overlay ? frame_def->overlay_3d_pass()
: frame_def->beauty_pass());
c.SetCameraAligned(true);
@@ -232,7 +240,7 @@ void BGDynamics::Draw(FrameDef* frame_def) {
tendrils_mesh_->SetIndexData(ds->tendril_indices);
tendrils_mesh_->SetData(
Object::Ref>(ds->tendril_vertices));
- bool draw_in_overlay = (frame_def->quality() >= GraphicsQuality::kHigh);
+ bool draw_in_overlay = frame_def->quality() >= GraphicsQuality::kHigh;
SmokeComponent c(draw_in_overlay ? frame_def->overlay_3d_pass()
: frame_def->beauty_pass());
c.SetOverlay(draw_in_overlay);
diff --git a/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc b/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc
index 424e46c5..d0ff57bf 100644
--- a/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc
+++ b/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc
@@ -2282,7 +2282,8 @@ void BGDynamicsServer::Step(StepData* step_data) {
auto ref(Object::CompleteDeferred(step_data));
// Keep our quality in sync with the graphics thread's.
- graphics_quality_ = g_base->graphics_server->graphics_quality();
+ graphics_quality_ = step_data->graphics_quality;
+ assert(graphics_quality_ != GraphicsQuality::kUnset);
cam_pos_ = step_data->cam_pos;
diff --git a/src/ballistica/base/dynamics/bg/bg_dynamics_server.h b/src/ballistica/base/dynamics/bg/bg_dynamics_server.h
index 46918f45..f52694f0 100644
--- a/src/ballistica/base/dynamics/bg/bg_dynamics_server.h
+++ b/src/ballistica/base/dynamics/bg/bg_dynamics_server.h
@@ -73,6 +73,7 @@ class BGDynamicsServer {
auto GetDefaultOwnerThread() const -> EventLoopID override {
return EventLoopID::kBGDynamics;
}
+ GraphicsQuality graphics_quality{};
int step_millisecs{};
Vector3f cam_pos{0.0f, 0.0f, 0.0f};
diff --git a/src/ballistica/base/graphics/component/empty_component.h b/src/ballistica/base/graphics/component/empty_component.h
index 1a809936..6a301812 100644
--- a/src/ballistica/base/graphics/component/empty_component.h
+++ b/src/ballistica/base/graphics/component/empty_component.h
@@ -11,8 +11,7 @@ namespace ballistica::base {
// transform/scissor/etc state changes.
class EmptyComponent : public RenderComponent {
public:
- explicit EmptyComponent(RenderPass* pass)
- : RenderComponent(pass), transparent_(false) {}
+ explicit EmptyComponent(RenderPass* pass) : RenderComponent(pass) {}
void SetTransparent(bool val) {
EnsureConfiguring();
transparent_ = val;
@@ -22,7 +21,7 @@ class EmptyComponent : public RenderComponent {
void WriteConfig() override { ConfigForEmpty(transparent_); }
private:
- bool transparent_;
+ bool transparent_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/graphics/component/object_component.h b/src/ballistica/base/graphics/component/object_component.h
index 5c1bf11b..1038cb98 100644
--- a/src/ballistica/base/graphics/component/object_component.h
+++ b/src/ballistica/base/graphics/component/object_component.h
@@ -113,6 +113,14 @@ class ObjectComponent : public RenderComponent {
void WriteConfig() override;
protected:
+ ReflectionType reflection_{ReflectionType::kNone};
+ LightShadowType light_shadow_{LightShadowType::kObject};
+ bool world_space_{};
+ bool transparent_{};
+ bool premultiplied_{};
+ bool have_color_add_{};
+ bool double_sided_{};
+ bool do_colorize_2_{};
float color_r_{1.0f};
float color_g_{1.0f};
float color_b_{1.0f};
@@ -133,14 +141,6 @@ class ObjectComponent : public RenderComponent {
float reflection_scale_b_{1.0f};
Object::Ref texture_;
Object::Ref colorize_texture_;
- ReflectionType reflection_{ReflectionType::kNone};
- LightShadowType light_shadow_{LightShadowType::kObject};
- bool world_space_{};
- bool transparent_{};
- bool premultiplied_{};
- bool have_color_add_{};
- bool double_sided_{};
- bool do_colorize_2_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/graphics/component/post_process_component.h b/src/ballistica/base/graphics/component/post_process_component.h
index 516bcd86..3c671c20 100644
--- a/src/ballistica/base/graphics/component/post_process_component.h
+++ b/src/ballistica/base/graphics/component/post_process_component.h
@@ -9,8 +9,7 @@ namespace ballistica::base {
class PostProcessComponent : public RenderComponent {
public:
- explicit PostProcessComponent(RenderPass* pass)
- : RenderComponent(pass), normal_distort_(0.0f), eyes_(false) {}
+ explicit PostProcessComponent(RenderPass* pass) : RenderComponent(pass) {}
void SetNormalDistort(float d) {
EnsureConfiguring();
normal_distort_ = d;
@@ -22,8 +21,8 @@ class PostProcessComponent : public RenderComponent {
protected:
void WriteConfig() override;
- bool eyes_;
- float normal_distort_;
+ bool eyes_{};
+ float normal_distort_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/graphics/component/simple_component.h b/src/ballistica/base/graphics/component/simple_component.h
index 6452401a..1585edbb 100644
--- a/src/ballistica/base/graphics/component/simple_component.h
+++ b/src/ballistica/base/graphics/component/simple_component.h
@@ -7,35 +7,10 @@
namespace ballistica::base {
-// used for UI and overlays and things - no world tinting/etc is applied
+/// Used for UI and overlays and things; no world tinting/etc is applied.
class SimpleComponent : public RenderComponent {
public:
- explicit SimpleComponent(RenderPass* pass)
- : RenderComponent(pass),
- color_r_(1.0f),
- color_g_(1.0f),
- color_b_(1.0f),
- color_a_(1.0f),
- colorize_color_r_(1.0f),
- colorize_color_g_(1.0f),
- colorize_color_b_(1.0f),
- colorize_color_a_(1.0f),
- colorize_color2_r_(1.0f),
- colorize_color2_g_(1.0f),
- colorize_color2_b_(1.0f),
- colorize_color2_a_(1.0f),
- shadow_offset_x_(0.0f),
- shadow_offset_y_(0.0f),
- shadow_blur_(0.0f),
- shadow_opacity_(0.0f),
- glow_amount_(0.0f),
- glow_blur_(0.0f),
- flatness_(0.0f),
- transparent_(false),
- premultiplied_(false),
- have_color_(false),
- double_sided_(false),
- do_colorize_2_(false) {}
+ explicit SimpleComponent(RenderPass* pass) : RenderComponent(pass) {}
void SetPremultiplied(bool val) {
EnsureConfiguring();
@@ -56,16 +31,16 @@ class SimpleComponent : public RenderComponent {
EnsureConfiguring();
texture_ = t;
}
- // used with colorize color 1 and 2
- // red areas of the texture will get multiplied by colorize-color1
- // and green areas by colorize-color2
+
+ /// Used with colorize color 1 and 2. Red areas of the texture will get
+ /// multiplied by colorize-color1 and green areas by colorize-color2.
void SetColorizeTexture(TextureAsset* t) {
EnsureConfiguring();
colorize_texture_ = t;
}
- // Red multiplies source color, green adds colorize1-color, and blue adds
- // white (currently requires colorize1 and colorize 2 to be set).
+ /// Red multiplies source color, green adds colorize1-color, and blue adds
+ /// white (currently requires colorize1 and colorize 2 to be set).
void SetMaskTexture(TextureAsset* t) {
EnsureConfiguring();
mask_texture_ = t;
@@ -124,10 +99,10 @@ class SimpleComponent : public RenderComponent {
do_colorize_2_ = true;
}
- void SetShadow(float offsetX, float offsetY, float blur, float opacity) {
+ void SetShadow(float offset_x, float offset_y, float blur, float opacity) {
EnsureConfiguring();
- shadow_offset_x_ = offsetX;
- shadow_offset_y_ = offsetY;
+ shadow_offset_x_ = offset_x;
+ shadow_offset_y_ = offset_y;
shadow_blur_ = blur;
shadow_opacity_ = opacity;
}
@@ -147,23 +122,34 @@ class SimpleComponent : public RenderComponent {
void WriteConfig() override;
protected:
- float color_r_, color_g_, color_b_, color_a_;
- float colorize_color_r_, colorize_color_g_, colorize_color_b_,
- colorize_color_a_;
- float colorize_color2_r_, colorize_color2_g_, colorize_color2_b_,
- colorize_color2_a_;
- float shadow_offset_x_, shadow_offset_y_, shadow_blur_, shadow_opacity_;
- float glow_amount_, glow_blur_;
- float flatness_;
+ bool do_colorize_2_{};
+ bool transparent_{};
+ bool premultiplied_{};
+ bool have_color_{};
+ bool double_sided_{};
+ float color_r_{1.0f};
+ float color_g_{1.0f};
+ float color_b_{1.0f};
+ float color_a_{1.0f};
+ float colorize_color_r_{1.0f};
+ float colorize_color_g_{1.0f};
+ float colorize_color_b_{1.0f};
+ float colorize_color_a_{1.0f};
+ float colorize_color2_r_{1.0f};
+ float colorize_color2_g_{1.0f};
+ float colorize_color2_b_{1.0f};
+ float colorize_color2_a_{1.0f};
+ float shadow_offset_x_{};
+ float shadow_offset_y_{};
+ float shadow_blur_{};
+ float shadow_opacity_{};
+ float glow_amount_{};
+ float glow_blur_{};
+ float flatness_{};
Object::Ref texture_;
Object::Ref colorize_texture_;
Object::Ref mask_texture_;
Object::Ref mask_uv2_texture_;
- bool do_colorize_2_;
- bool transparent_;
- bool premultiplied_;
- bool have_color_;
- bool double_sided_;
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/graphics/component/smoke_component.h b/src/ballistica/base/graphics/component/smoke_component.h
index 45fa1e92..d174e643 100644
--- a/src/ballistica/base/graphics/component/smoke_component.h
+++ b/src/ballistica/base/graphics/component/smoke_component.h
@@ -9,13 +9,8 @@ namespace ballistica::base {
class SmokeComponent : public RenderComponent {
public:
- explicit SmokeComponent(RenderPass* pass)
- : RenderComponent(pass),
- color_r_(1.0f),
- color_g_(1.0f),
- color_b_(1.0f),
- color_a_(1.0f),
- overlay_(false) {}
+ explicit SmokeComponent(RenderPass* pass) : RenderComponent(pass) {}
+
void SetColor(float r, float g, float b, float a = 1.0f) {
EnsureConfiguring();
color_r_ = r;
@@ -23,6 +18,7 @@ class SmokeComponent : public RenderComponent {
color_b_ = b;
color_a_ = a;
}
+
void SetOverlay(bool overlay) {
EnsureConfiguring();
overlay_ = overlay;
@@ -30,8 +26,12 @@ class SmokeComponent : public RenderComponent {
protected:
void WriteConfig() override;
- float color_r_, color_g_, color_b_, color_a_;
- bool overlay_;
+
+ bool overlay_{};
+ float color_r_{1.0f};
+ float color_g_{1.0f};
+ float color_b_{1.0f};
+ float color_a_{1.0f};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/graphics/gl/gl_sys.cc b/src/ballistica/base/graphics/gl/gl_sys.cc
index 5d6e42ab..be6b3d40 100644
--- a/src/ballistica/base/graphics/gl/gl_sys.cc
+++ b/src/ballistica/base/graphics/gl/gl_sys.cc
@@ -52,7 +52,7 @@ bool g_sys_gl_inited{};
// Provide an empty implementation of this if noone provided a real one.
#ifndef BA_HAS_SYS_GL_INIT
-void SysGLInit() { assert(!g_sys_gl_inited); }
+void SysGLInit(RendererGL* renderer) { assert(!g_sys_gl_inited); }
#endif // BA_HAS_SYS_GL_INIT
diff --git a/src/ballistica/base/graphics/gl/gl_sys.h b/src/ballistica/base/graphics/gl/gl_sys.h
index e2519c70..a33eac0c 100644
--- a/src/ballistica/base/graphics/gl/gl_sys.h
+++ b/src/ballistica/base/graphics/gl/gl_sys.h
@@ -220,6 +220,7 @@ inline void glDepthRange(double min, double max) {
#endif
namespace ballistica::base {
+class RendererGL;
extern bool g_sys_gl_inited;
@@ -228,7 +229,7 @@ extern bool g_sys_gl_inited;
// called only once and then g_sys_gl_inited set. A platform that defines
// this should define BA_HAS_SYS_GL_INIT; otherwise a default empty
// implementation will be defined.
-void SysGLInit();
+void SysGLInit(RendererGL* renderer);
} // namespace ballistica::base
diff --git a/src/ballistica/base/graphics/gl/gl_sys_windows.cc b/src/ballistica/base/graphics/gl/gl_sys_windows.cc
index f627bcd1..ae65f49e 100644
--- a/src/ballistica/base/graphics/gl/gl_sys_windows.cc
+++ b/src/ballistica/base/graphics/gl/gl_sys_windows.cc
@@ -5,19 +5,22 @@
#include "SDL.h"
#include "ballistica/base/graphics/gl/gl_sys.h"
+#include "ballistica/base/graphics/gl/renderer_gl.h"
#include "ballistica/shared/ballistica.h"
#pragma comment(lib, "opengl32.lib")
-// #pragma comment(lib, "glu32.lib")
PFNGLGETINTERNALFORMATIVPROC glGetInternalformativ{};
PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC
glGetFramebufferAttachmentParameteriv{};
PFNGLBLENDFUNCSEPARATEPROC glBlendFuncSeparate{};
+
+PFNGLCOMPRESSEDTEXIMAGE2DPROC glCompressedTexImage2DBA{};
PFNGLACTIVETEXTUREPROC glActiveTextureBA{};
+// PFNGLCOMPRESSEDTEXIMAGE2DPROC glCompressedTexImageARB{};
// PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTextureARB{};
+
PFNGLPOINTPARAMETERFVARBPROC glPointParameterfvARB{};
-// PFNGLPOINTPARAMETERFARBPROC glPointParameterfARB{};
PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT{};
PFNGLCREATEPROGRAMPROC glCreateProgram{};
PFNGLCREATESHADERPROC glCreateShader{};
@@ -57,7 +60,6 @@ PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray{};
PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray{};
PFNGLUNIFORMMATRIX4FVARBPROC glUniformMatrix4fv{};
PFNGLBINDATTRIBLOCATIONPROC glBindAttribLocation{};
-PFNGLCOMPRESSEDTEXIMAGE2DPROC glCompressedTexImage2DBA{};
PFNGLGETSHADERIVPROC glGetShaderiv{};
PFNGLGETPROGRAMIVPROC glGetProgramiv{};
PFNGLDELETESHADERPROC glDeleteShader{};
@@ -90,11 +92,16 @@ static auto GetGLFunc_(const char* name, bool required) -> void* {
#define GET2(PTRTYPE, FUNC, REQUIRED) \
FUNC##BA = (PTRTYPE)GetGLFunc_(#FUNC, REQUIRED)
-void SysGLInit() {
+void SysGLInit(RendererGL* renderer) {
assert(!g_sys_gl_inited);
SDL_GL_LoadLibrary(nullptr);
+ // Check overall GL version here before loading any extended functions.
+ // We'd rather die with a 'Your OpenGL is too old' error rather than a
+ // 'Could not load function foofDinglePlop2XZ'.
+ renderer->CheckGLVersion();
+
void* testval{};
PFNGLGETINTERNALFORMATIVPROC fptr;
@@ -104,18 +111,13 @@ void SysGLInit() {
// so we can survive without it.
GET(PFNGLGETINTERNALFORMATIVPROC, glGetInternalformativ, false);
- // For checking srgb stuff.
+ GET(PFNGLBLENDFUNCSEPARATEPROC, glBlendFuncSeparate, true);
GET(PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC,
- glGetFramebufferAttachmentParameteriv, false);
-
- // Needed for VR overlay.
- GET(PFNGLBLENDFUNCSEPARATEPROC, glBlendFuncSeparate, false);
-
+ glGetFramebufferAttachmentParameteriv, true);
+ GET(PFNGLGETSTRINGIPROC, glGetStringi, true);
GET2(PFNGLACTIVETEXTUREPROC, glActiveTexture, true);
- // GET(PFNGLCLIENTACTIVETEXTUREARBPROC, glClientActiveTextureARB, true);
GET(PFNWGLSWAPINTERVALEXTPROC, wglSwapIntervalEXT, true);
GET(PFNGLPOINTPARAMETERFVARBPROC, glPointParameterfvARB, true);
- // GET(PFNGLPOINTPARAMETERFARBPROC, glPointParameterfARB, true);
GET(PFNGLCREATEPROGRAMPROC, glCreateProgram, true);
GET(PFNGLCREATESHADERPROC, glCreateShader, true);
GET(PFNGLSHADERSOURCEPROC, glShaderSource, true);
@@ -159,7 +161,6 @@ void SysGLInit() {
GET(PFNGLDETACHSHADERPROC, glDetachShader, true);
GET(PFNGLGETSHADERINFOLOGPROC, glGetShaderInfoLog, true);
GET(PFNGLGETPROGRAMINFOLOGPROC, glGetProgramInfoLog, true);
- GET(PFNGLGETSTRINGIPROC, glGetStringi, true);
GET(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray, true);
GET(PFNGLGENVERTEXARRAYSPROC, glGenVertexArrays, true);
GET(PFNGLDELETEVERTEXARRAYSPROC, glDeleteVertexArrays, true);
diff --git a/src/ballistica/base/graphics/gl/gl_sys_windows.h b/src/ballistica/base/graphics/gl/gl_sys_windows.h
index 228543aa..60ce107e 100644
--- a/src/ballistica/base/graphics/gl/gl_sys_windows.h
+++ b/src/ballistica/base/graphics/gl/gl_sys_windows.h
@@ -31,9 +31,13 @@ extern PFNGLGETINTERNALFORMATIVPROC glGetInternalformativ;
extern PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC
glGetFramebufferAttachmentParameteriv;
extern PFNGLBLENDFUNCSEPARATEPROC glBlendFuncSeparate;
+
+// Hopefully can switch this back if SDL gets fixed.
extern PFNGLACTIVETEXTUREPROC glActiveTextureBA;
+extern PFNGLCOMPRESSEDTEXIMAGE2DPROC glCompressedTexImage2DBA;
// extern PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTextureARB;
// extern PFNGLPOINTPARAMETERFARBPROC glPointParameterfARB;
+
extern PFNGLPOINTPARAMETERFVARBPROC glPointParameterfvARB;
extern PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT;
extern PFNGLCREATEPROGRAMPROC glCreateProgram;
@@ -74,7 +78,6 @@ extern PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray;
extern PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray;
extern PFNGLUNIFORMMATRIX4FVARBPROC glUniformMatrix4fv;
extern PFNGLBINDATTRIBLOCATIONPROC glBindAttribLocation;
-extern PFNGLCOMPRESSEDTEXIMAGE2DPROC glCompressedTexImage2DBA;
extern PFNGLGETSHADERIVPROC glGetShaderiv;
extern PFNGLGETPROGRAMIVPROC glGetProgramiv;
extern PFNGLDELETESHADERPROC glDeleteShader;
diff --git a/src/ballistica/base/graphics/gl/program/program_gl.h b/src/ballistica/base/graphics/gl/program/program_gl.h
index 437d2723..84f1f57b 100644
--- a/src/ballistica/base/graphics/gl/program/program_gl.h
+++ b/src/ballistica/base/graphics/gl/program/program_gl.h
@@ -238,10 +238,10 @@ class RendererGL::ProgramGL {
// Update matrices as necessary.
- uint32_t mvpState =
+ int mvp_state =
g_base->graphics_server->GetModelViewProjectionMatrixState();
- if (mvpState != mvp_state_) {
- mvp_state_ = mvpState;
+ if (mvp_state != mvp_state_) {
+ mvp_state_ = mvp_state;
glUniformMatrix4fv(
mvp_uniform_, 1, 0,
g_base->graphics_server->GetModelViewProjectionMatrix().m);
@@ -251,7 +251,7 @@ class RendererGL::ProgramGL {
if (pflags_ & PFLAG_USES_MODEL_WORLD_MATRIX) {
// With world space points this would be identity; don't waste time.
assert(!(pflags_ & PFLAG_WORLD_SPACE_PTS));
- uint32_t state = g_base->graphics_server->GetModelWorldMatrixState();
+ int state = g_base->graphics_server->GetModelWorldMatrixState();
if (state != model_world_matrix_state_) {
model_world_matrix_state_ = state;
glUniformMatrix4fv(model_world_matrix_uniform_, 1, 0,
@@ -264,8 +264,7 @@ class RendererGL::ProgramGL {
// With world space points this would be identity; don't waste time.
assert(!(pflags_ & PFLAG_WORLD_SPACE_PTS));
// There's no state for just modelview but this works.
- uint32_t state =
- g_base->graphics_server->GetModelViewProjectionMatrixState();
+ int state = g_base->graphics_server->GetModelViewProjectionMatrixState();
if (state != model_view_matrix_state_) {
model_view_matrix_state_ = state;
glUniformMatrix4fv(model_view_matrix_uniform_, 1, 0,
@@ -275,7 +274,7 @@ class RendererGL::ProgramGL {
BA_DEBUG_CHECK_GL_ERROR;
if (pflags_ & PFLAG_USES_CAM_POS) {
- uint32_t state = g_base->graphics_server->cam_pos_state();
+ int state = g_base->graphics_server->cam_pos_state();
if (state != cam_pos_state_) {
cam_pos_state_ = state;
const Vector3f& p(g_base->graphics_server->cam_pos());
@@ -285,7 +284,7 @@ class RendererGL::ProgramGL {
BA_DEBUG_CHECK_GL_ERROR;
if (pflags_ & PFLAG_USES_CAM_ORIENT_MATRIX) {
- uint32_t state = g_base->graphics_server->GetCamOrientMatrixState();
+ int state = g_base->graphics_server->GetCamOrientMatrixState();
if (state != cam_orient_matrix_state_) {
cam_orient_matrix_state_ = state;
glUniformMatrix4fv(cam_orient_matrix_uniform_, 1, 0,
@@ -295,7 +294,7 @@ class RendererGL::ProgramGL {
BA_DEBUG_CHECK_GL_ERROR;
if (pflags_ & PFLAG_USES_SHADOW_PROJECTION_MATRIX) {
- uint32_t state =
+ int state =
g_base->graphics_server->light_shadow_projection_matrix_state();
if (state != light_shadow_projection_matrix_state_) {
light_shadow_projection_matrix_state_ = state;
@@ -336,19 +335,19 @@ class RendererGL::ProgramGL {
Object::Ref vertex_shader_;
std::string name_;
GLuint program_{};
- int pflags_{};
- uint32_t mvp_state_{};
GLint mvp_uniform_{};
GLint model_world_matrix_uniform_{};
GLint model_view_matrix_uniform_{};
GLint light_shadow_projection_matrix_uniform_{};
- uint32_t light_shadow_projection_matrix_state_{};
- uint32_t model_world_matrix_state_{};
- uint32_t model_view_matrix_state_{};
GLint cam_pos_uniform_{};
- uint32_t cam_pos_state_{};
GLint cam_orient_matrix_uniform_{};
- GLuint cam_orient_matrix_state_{};
+ int cam_orient_matrix_state_{};
+ int light_shadow_projection_matrix_state_{};
+ int pflags_{};
+ int mvp_state_{};
+ int cam_pos_state_{};
+ int model_world_matrix_state_{};
+ int model_view_matrix_state_{};
BA_DISALLOW_CLASS_COPIES(ProgramGL);
};
diff --git a/src/ballistica/base/graphics/gl/renderer_gl.cc b/src/ballistica/base/graphics/gl/renderer_gl.cc
index ffa42e0f..c5cd9bd1 100644
--- a/src/ballistica/base/graphics/gl/renderer_gl.cc
+++ b/src/ballistica/base/graphics/gl/renderer_gl.cc
@@ -57,11 +57,6 @@ namespace ballistica::base {
bool RendererGL::funky_depth_issue_set_{};
bool RendererGL::funky_depth_issue_{};
-#if BA_OSTYPE_ANDROID
-bool RendererGL::is_speedy_android_device_{};
-bool RendererGL::is_extra_speedy_android_device_{};
-#endif
-
RendererGL::RendererGL() {
assert(g_base->app_adapter->InGraphicsContext());
@@ -72,7 +67,7 @@ RendererGL::RendererGL() {
// Run any one-time setup the platform might need to do
// (grabbing function pointers, etc.)
if (!g_sys_gl_inited) {
- SysGLInit();
+ SysGLInit(this);
g_sys_gl_inited = true;
}
@@ -121,11 +116,15 @@ auto RendererGL::GLErrorToString(GLenum err) -> std::string {
static auto CheckGLExtension(const std::vector& exts,
const char* ext) -> bool {
assert(strlen(ext) < 100);
- const int variant_count{8};
+ const int variant_count{10};
char variants[variant_count][128];
int i = 0;
snprintf(variants[i], sizeof(variants[i]), "OES_%s", ext);
i++;
+ snprintf(variants[i], sizeof(variants[i]), "GL_OES_%s", ext);
+ i++;
+ snprintf(variants[i], sizeof(variants[i]), "GL_KHR_%s", ext);
+ i++;
snprintf(variants[i], sizeof(variants[i]), "GL_ARB_%s", ext);
i++;
snprintf(variants[i], sizeof(variants[i]), "GL_APPLE_%s", ext);
@@ -152,10 +151,49 @@ static auto CheckGLExtension(const std::vector& exts,
return false;
}
+// This is split into its own call because systems that load GL calls
+// dynamically may want to run the check before trying to load said GL
+// calls. It's better to die with a 'Your OpenGL is too old' error rather
+// than a 'Could not load function foofDinglePlop2XZ'.
+void RendererGL::CheckGLVersion() {
+ if (checked_gl_version_) {
+ return;
+ }
+ const char* version_str = (const char*)glGetString(GL_VERSION);
+ BA_PRECONDITION_FATAL(version_str);
+
+ // Do a rough check to make sure we're running 3 or newer of GL/GLES. This
+ // query should be available even on older versions which is why we do it
+ // before the GL_MAJOR_VERSION/GL_MINOR_VERSION business which is not.
+ if (gl_is_es()) {
+ // GL ES version strings start with 'OpenGL ES X' with X being version.
+ const char* prefix = "OpenGL ES ";
+ int prefixlen = strlen(prefix);
+ BA_PRECONDITION_FATAL(!strncmp(version_str, prefix, prefixlen));
+ if (version_str[prefixlen] != '3') {
+ FatalError(
+ std::string("Your OpenGL ES version is too old (") + version_str
+ + "). We require 3.0 or later. Try updating your graphics drivers.");
+ }
+ } else {
+ // Regular GL version strings start with numeric version.
+
+ if (version_str[0] != '3' && version_str[0] != '4') {
+ FatalError(
+ std::string("Your OpenGL version is too old (") + version_str
+ + "). We require 3.0 or later. Try updating your graphics drivers.");
+ }
+ }
+ checked_gl_version_ = true;
+}
+
void RendererGL::CheckGLCapabilities_() {
BA_DEBUG_CHECK_GL_ERROR;
assert(g_base->app_adapter->InGraphicsContext());
+ // Die if our overall GL version is too old.
+ CheckGLVersion();
+
const char* renderer = (const char*)glGetString(GL_RENDERER);
BA_PRECONDITION_FATAL(renderer);
const char* vendor = (const char*)glGetString(GL_VENDOR);
@@ -163,15 +201,9 @@ void RendererGL::CheckGLCapabilities_() {
const char* version_str = (const char*)glGetString(GL_VERSION);
BA_PRECONDITION_FATAL(version_str);
- // Do a rough check to make sure we're running 3 or newer of GL/GLES.
- // This query should be available even on older versions.
- if (version_str[0] != '3' && version_str[0] != '4') {
- FatalError(std::string("Invalid OpenGL version found (") + version_str
- + "). We require 3.0 or later.");
- }
-
// Now fetch exact major/minor versions. This query requires version 3.0
- // or newer which is why we checked that above.
+ // or newer which is why we checked overall version in CheckGLVersion()
+ // above.
glGetError(); // Clear any existing error so we don't die on it here.
glGetIntegerv(GL_MAJOR_VERSION, &gl_version_major_);
BA_PRECONDITION_FATAL(glGetError() == GL_NO_ERROR);
@@ -185,9 +217,11 @@ void RendererGL::CheckGLCapabilities_() {
basestr = "OpenGL";
}
- Log(LogLevel::kInfo, std::string("Using ") + basestr + " (vendor: " + vendor
- + ", renderer: " + renderer
- + ", version: " + version_str + ").");
+ if (g_buildconfig.debug_build()) {
+ Log(LogLevel::kInfo, std::string("Using ") + basestr + " (vendor: " + vendor
+ + ", renderer: " + renderer
+ + ", version: " + version_str + ").");
+ }
// Build a vector of extensions. Newer GLs give us extensions as lists
// already, but on older ones we may need to break a single string apart
@@ -261,8 +295,12 @@ void RendererGL::CheckGLCapabilities_() {
// Flag certain devices as 'speedy' - we use this to enable high/higher
// quality and whatnot (even in cases where ES3 isnt available).
- is_speedy_android_device_ = true;
- is_extra_speedy_android_device_ = false;
+
+ // Let just consider ES 3.2 stuff speedy.
+ assert(gl_version_major() == 3);
+ is_speedy_android_device_ = gl_version_minor() >= 2;
+
+ // is_extra_speedy_android_device_ = false;
is_adreno_ = (strstr(renderer, "Adreno") != nullptr);
// draws_shields_funny_ = false; // Start optimistic.
@@ -328,9 +366,9 @@ void RendererGL::CheckGLCapabilities_() {
// g_core->platform->set_is_tegra_k1(is_tegra_k1_);
// Extra-speedy implies speedy too..
- if (is_extra_speedy_android_device_) {
- is_speedy_android_device_ = true;
- }
+ // if (is_extra_speedy_android_device_) {
+ // is_speedy_android_device_ = true;
+ // }
#endif // BA_OSTYPE_ANDROID
@@ -347,12 +385,12 @@ void RendererGL::CheckGLCapabilities_() {
}
}
- // All android devices should support etc1.
+ // Pretty much all Android devices should support ETC1.
if (CheckGLExtension(extensions, "compressed_ETC1_RGB8_texture")) {
c_types.push_back(TextureCompressionType::kETC1);
} else {
if (g_buildconfig.ostype_android()) {
- Log(LogLevel::kError, "Android device missing ETC1 support");
+ Log(LogLevel::kError, "Android device missing ETC1 support.");
}
}
@@ -362,11 +400,16 @@ void RendererGL::CheckGLCapabilities_() {
c_types.push_back(TextureCompressionType::kETC2);
}
+ // ASTC is generally available on newer mobile hardware.
+ if (CheckGLExtension(extensions, "texture_compression_astc_ldr")) {
+ c_types.push_back(TextureCompressionType::kASTC);
+ }
+
g_base->graphics_server->SetTextureCompressionTypes(c_types);
// Both GL 3 and GL ES 3.0 support depth textures (and thus our high
// quality mode) as a core feature.
- g_base->graphics->SetSupportsHighQualityGraphics(true);
+ // g_base->graphics->SetSupportsHighQualityGraphics(true);
// Store the tex-compression type we support.
BA_DEBUG_CHECK_GL_ERROR;
@@ -453,44 +496,51 @@ void RendererGL::CheckGLCapabilities_() {
}
auto RendererGL::GetMSAASamplesForFramebuffer_(int width, int height) -> int {
-#if BA_RIFT_BUILD
- return 4;
-#else
- // We currently aim for 4 up to 800 height and 2 beyond that.
- if (height > 800) {
- return 2;
+ if (g_buildconfig.ostype_android()) {
+ // We currently aim for 4 up to 800 height and 2 beyond that.
+ if (height > 800) {
+ return 2;
+ } else {
+ return 4;
+ }
} else {
return 4;
}
-#endif
}
void RendererGL::UpdateMSAAEnabled_() {
-#if BA_RIFT_BUILD
- if (msaa_max_samples_rgb8_ > 0) {
+ if (g_buildconfig.ostype_macos()) {
+ // Let's go ahead and flip this on for Apple Silicon Macs.
+#if __aarch64__
enable_msaa_ = true;
+#else
+ enable_msaa_ = false;
+#endif
+ } else if (g_buildconfig.rift_build()) {
+ if (msaa_max_samples_rgb8_ > 0) {
+ enable_msaa_ = true;
+ } else {
+ enable_msaa_ = false;
+ }
+ } else if (g_buildconfig.ostype_android()) {
+ // lets allow full 1080p msaa with newer stuff..
+ int max_msaa_res = is_tegra_k1_ ? 1200 : 800;
+
+ // To start, see if it looks like we support msaa on paper.
+ enable_msaa_ =
+ ((screen_render_target()->physical_height()
+ <= static_cast(max_msaa_res))
+ && (msaa_max_samples_rgb8_ > 0) && (msaa_max_samples_rgb565_ > 0));
+
+ // Ok, lets be careful here; msaa blitting/etc seems to be particular in
+ // terms of supported formats/etc so let's only enable it on
+ // explicitly-tested hardware for now.
+ if (!is_tegra_4_ && !is_tegra_k1_ && !is_recent_adreno_) {
+ enable_msaa_ = false;
+ }
} else {
enable_msaa_ = false;
}
-#else
-
- // lets allow full 1080p msaa with newer stuff..
- int max_msaa_res = is_tegra_k1_ ? 1200 : 800;
-
- // To start, see if it looks like we support msaa on paper.
- enable_msaa_ =
- ((screen_render_target()->physical_height()
- <= static_cast(max_msaa_res))
- && (msaa_max_samples_rgb8_ > 0) && (msaa_max_samples_rgb565_ > 0));
-
- // Ok, lets be careful here; msaa blitting/etc seems to be particular in
- // terms of supported formats/etc so let's only enable it on
- // explicitly-tested hardware for now.
- if (!is_tegra_4_ && !is_tegra_k1_ && !is_recent_adreno_) {
- enable_msaa_ = false;
- }
-
-#endif // BA_RIFT_BUILD
}
auto RendererGL::IsMSAAEnabled() const -> bool { return enable_msaa_; }
@@ -796,7 +846,7 @@ void RendererGL::SyncGLState_() {
BA_DEBUG_CHECK_GL_ERROR;
#if BA_RIFT_BUILD
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
glFrontFace(GL_CCW);
}
BA_DEBUG_CHECK_GL_ERROR;
@@ -836,7 +886,7 @@ void RendererGL::SyncGLState_() {
// texture, and in that case we need alpha to accumulate; not get
// overwritten. could probably enable this everywhere but I don't know if
// it's supported on all hardware or slower.
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
#if BA_OSTYPE_WINDOWS
if (glBlendFuncSeparate == nullptr) {
FatalError(
@@ -2348,7 +2398,7 @@ void RendererGL::SetBlendPremult(bool b) {
// texture, and in that case we need alpha to accumulate; not get
// overwritten. could probably enable this everywhere but I don't know if
// it's supported on all hardware or is slower or whatnot..
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
GL_ONE_MINUS_SRC_ALPHA);
} else {
@@ -2517,26 +2567,12 @@ auto RendererGL::GetFunkyDepthIssue_() -> bool {
std::string RendererGL::GetAutoAndroidRes() {
assert(g_base->app_adapter->InGraphicsContext());
- const char* renderer = (const char*)glGetString(GL_RENDERER);
-
- // On the adreno 4xxx or 5xxx series we should be able to do anything.
- if (strstr(renderer, "Adreno (TM) 4") || strstr(renderer, "Adreno (TM) 5")) {
+ // Simplifying this to just 1080p for anything we label 'speedy' and 720p
+ // for everything else.
+ if (is_speedy_android_device_) {
return "1080p";
}
-
- // On extra-speedy devices we should be able to do 1920x1200.
- if (is_extra_speedy_android_device_) {
- return "1080p";
- }
-
- // Amazon Fire tablet (as of Jan '18) needs REAL low res to feel smooth.
- if (g_core->platform->GetDeviceName() == "Amazon KFAUWI") {
- return "480p";
- }
-
- // fall back to the old 'Auto' values elsewhere
- // - this is generally 720p (but varies in a few cases)
- return "Auto";
+ return "720p";
}
#endif // BA_OSTYPE_ANDROID
@@ -2548,18 +2584,10 @@ auto RendererGL::GetAutoTextureQuality() -> TextureQuality {
#if BA_OSTYPE_ANDROID
{
// Lets be cheaper in VR mode since we have to draw twice.
- if (g_core->IsVRMode()) {
- qual = TextureQuality::kMedium;
+ if (g_core->vr_mode()) {
+ qual = TextureQuality::kHigh;
} else {
- // On android we default to high quality mode if we support ETC2;
- // otherwise go with medium.
- if (g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC2)
- || is_speedy_android_device_) {
- qual = TextureQuality::kHigh;
- } else {
- qual = TextureQuality::kMedium;
- }
+ qual = TextureQuality::kHigh;
}
}
#else // BA_OSTYPE_ANDROID
@@ -2577,10 +2605,10 @@ auto RendererGL::GetAutoGraphicsQuality() -> GraphicsQuality {
GraphicsQuality q{GraphicsQuality::kMedium};
#if BA_OSTYPE_ANDROID
// lets be cheaper in VR mode since we draw twice..
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
q = GraphicsQuality::kMedium;
} else {
- if (is_extra_speedy_android_device_) {
+ if (is_speedy_android_device_) {
q = GraphicsQuality::kHigher;
} else {
q = GraphicsQuality::kHigh;
@@ -2598,7 +2626,8 @@ void RendererGL::RetainShader_(ProgramGL* p) { shaders_.emplace_back(p); }
void RendererGL::Load() {
assert(g_base->app_adapter->InGraphicsContext());
assert(!data_loaded_);
- assert(g_base->graphics_server->graphics_quality_set());
+ assert(g_base->graphics_server->graphics_quality()
+ != GraphicsQuality::kUnset);
BA_DEBUG_CHECK_GL_ERROR;
if (!got_screen_framebuffer_) {
got_screen_framebuffer_ = true;
@@ -3235,7 +3264,7 @@ void RendererGL::CardboardDisableScissor() { glDisable(GL_SCISSOR_TEST); }
void RendererGL::CardboardEnableScissor() { glEnable(GL_SCISSOR_TEST); }
void RendererGL::VREyeRenderBegin() {
- assert(g_core->IsVRMode());
+ assert(g_core->vr_mode());
// On rift we need to turn off srgb conversion for each eye render
// so we can dump our linear data into oculus' srgb buffer as-is.
@@ -3257,7 +3286,7 @@ void RendererGL::VRSyncRenderStates() {
void RendererGL::RenderFrameDefEnd() {
// Need to set some states to keep cardboard happy.
#if BA_CARDBOARD_BUILD
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
SyncGLState_();
glEnable(GL_SCISSOR_TEST);
}
diff --git a/src/ballistica/base/graphics/gl/renderer_gl.h b/src/ballistica/base/graphics/gl/renderer_gl.h
index c3afef5a..16917d81 100644
--- a/src/ballistica/base/graphics/gl/renderer_gl.h
+++ b/src/ballistica/base/graphics/gl/renderer_gl.h
@@ -64,6 +64,7 @@ class RendererGL : public Renderer {
class ProgramSpriteGL;
public:
+ void CheckGLVersion();
static void CheckGLError(const char* file, int line);
static auto GLErrorToString(GLenum err) -> std::string;
static auto GetGLTextureFormat(TextureFormat f) -> GLenum;
@@ -259,6 +260,7 @@ class RendererGL : public Renderer {
void SetBlend(bool b);
void SetBlendPremult(bool b);
+ GraphicsQuality vignette_quality_{};
bool blend_{};
bool blend_premult_{};
bool first_extension_check_{true};
@@ -275,6 +277,7 @@ class RendererGL : public Renderer {
bool got_screen_framebuffer_{};
bool double_sided_{};
bool invalidate_framebuffer_support_{};
+ bool checked_gl_version_{};
GLint gl_version_major_{};
GLint gl_version_minor_{};
int last_blur_res_count_{};
@@ -291,7 +294,6 @@ class RendererGL : public Renderer {
GLint screen_framebuffer_{};
GLuint random_tex_{};
GLuint vignette_tex_{};
- GraphicsQuality vignette_quality_{};
GLint viewport_x_{};
GLint viewport_y_{};
GLint viewport_width_{};
@@ -340,8 +342,7 @@ class RendererGL : public Renderer {
static bool funky_depth_issue_set_;
static bool funky_depth_issue_;
#if BA_OSTYPE_ANDROID
- static bool is_speedy_android_device_;
- static bool is_extra_speedy_android_device_;
+ bool is_speedy_android_device_{};
#endif
ProgramGL* current_program_{};
std::vector scissor_rects_;
diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc
index c2e873ef..77f30abf 100644
--- a/src/ballistica/base/graphics/graphics.cc
+++ b/src/ballistica/base/graphics/graphics.cc
@@ -5,44 +5,29 @@
#include "ballistica/base/app_adapter/app_adapter.h"
#include "ballistica/base/app_mode/app_mode.h"
#include "ballistica/base/dynamics/bg/bg_dynamics.h"
-#include "ballistica/base/graphics/component/empty_component.h"
#include "ballistica/base/graphics/component/object_component.h"
#include "ballistica/base/graphics/component/post_process_component.h"
#include "ballistica/base/graphics/component/simple_component.h"
#include "ballistica/base/graphics/component/special_component.h"
#include "ballistica/base/graphics/component/sprite_component.h"
#include "ballistica/base/graphics/graphics_server.h"
-#include "ballistica/base/graphics/graphics_vr.h"
#include "ballistica/base/graphics/support/camera.h"
#include "ballistica/base/graphics/support/net_graph.h"
-#include "ballistica/base/graphics/text/text_graphics.h"
+#include "ballistica/base/graphics/support/screen_messages.h"
#include "ballistica/base/input/input.h"
#include "ballistica/base/logic/logic.h"
-#include "ballistica/base/platform/base_platform.h"
#include "ballistica/base/python/support/python_context_call.h"
#include "ballistica/base/support/app_config.h"
-#include "ballistica/base/ui/dev_console.h"
#include "ballistica/base/ui/ui.h"
-#include "ballistica/core/core.h"
#include "ballistica/shared/foundation/event_loop.h"
-#include "ballistica/shared/generic/utils.h"
-#include "ballistica/shared/python/python.h"
namespace ballistica::base {
-const float kScreenMessageZDepth{-0.06f};
-const float kScreenMeshZDepth{-0.05f};
+const float kScreenTextZDepth{-0.06f};
const float kProgressBarZDepth{0.0f};
const int kProgressBarFadeTime{500};
const float kDebugImgZDepth{-0.04f};
-
-auto Graphics::Create() -> Graphics* {
-#if BA_VR_BUILD
- return new GraphicsVR();
-#else
- return new Graphics();
-#endif
-}
+const float kScreenMeshZDepth{-0.05f};
auto Graphics::IsShaderTransparent(ShadingType c) -> bool {
switch (c) {
@@ -93,17 +78,17 @@ auto Graphics::IsShaderTransparent(ShadingType c) -> bool {
}
}
-Graphics::Graphics() = default;
+Graphics::Graphics() : screenmessages{new ScreenMessages()} {}
Graphics::~Graphics() = default;
void Graphics::OnAppStart() { assert(g_base->InLogicThread()); }
-void Graphics::OnAppPause() {
+void Graphics::OnAppSuspend() {
assert(g_base->InLogicThread());
SetGyroEnabled(false);
}
-void Graphics::OnAppResume() {
+void Graphics::OnAppUnsuspend() {
assert(g_base->InLogicThread());
g_base->graphics->SetGyroEnabled(true);
}
@@ -115,9 +100,12 @@ void Graphics::OnAppShutdownComplete() { assert(g_base->InLogicThread()); }
void Graphics::DoApplyAppConfig() {
assert(g_base->InLogicThread());
+ // Any time we load the config we ship a new graphics-settings to
+ // the graphics server since something likely changed.
+ graphics_settings_dirty_ = true;
+
show_fps_ = g_base->app_config->Resolve(AppConfig::BoolID::kShowFPS);
show_ping_ = g_base->app_config->Resolve(AppConfig::BoolID::kShowPing);
- tv_border_ = g_base->app_config->Resolve(AppConfig::BoolID::kEnableTVBorder);
bool disable_camera_shake =
g_base->app_config->Resolve(AppConfig::BoolID::kDisableCameraShake);
@@ -126,12 +114,59 @@ void Graphics::DoApplyAppConfig() {
bool disable_camera_gyro =
g_base->app_config->Resolve(AppConfig::BoolID::kDisableCameraGyro);
set_camera_gyro_explicitly_disabled(disable_camera_gyro);
+
+ applied_app_config_ = true;
+
+ // At this point we may want to send initial graphics settings to the
+ // graphics server if we haven't.
+ UpdateInitialGraphicsSettingsSend_();
+}
+
+void Graphics::UpdateInitialGraphicsSettingsSend_() {
+ assert(g_base->InLogicThread());
+ if (sent_initial_graphics_settings_) {
+ return;
+ }
+
+ // We need to send an initial graphics-settings to the server to kick
+ // things off, but we need a few things to be in place first.
+ auto app_config_ready = applied_app_config_;
+
+ // At some point we may want to wait to know our actual screen res before
+ // sending. This won't apply everywhere though since on some platforms the
+ // screen doesn't exist until we send this.
+ auto screen_resolution_ready = true;
+
+ if (app_config_ready && screen_resolution_ready) {
+ // Update/grab the current settings snapshot.
+ auto* settings = GetGraphicsSettingsSnapshot();
+
+ // We need to explicitly push settings to the graphics server to kick
+ // things off. We need to keep this settings instance alive until
+ // handled by the graphics context (which might be in another thread
+ // where we're not allowed to muck with settings' refs from). So let's
+ // explicitly increment its refcount here in the logic thread now and
+ // then push a call back here to decrement it when we're done.
+ settings->ObjectIncrementStrongRefCount();
+
+ g_base->app_adapter->PushGraphicsContextCall([settings] {
+ assert(g_base->app_adapter->InGraphicsContext());
+ g_base->graphics_server->ApplySettings(settings->Get());
+ g_base->logic->event_loop()->PushCall([settings] {
+ // Release our strong ref back here in the logic thread.
+ assert(g_base->InLogicThread());
+ settings->ObjectDecrementStrongRefCount();
+ });
+ });
+
+ sent_initial_graphics_settings_ = true;
+ }
}
void Graphics::StepDisplayTime() { assert(g_base->InLogicThread()); }
void Graphics::AddCleanFrameCommand(const Object::Ref& c) {
- BA_PRECONDITION(g_base->InLogicThread());
+ assert(g_base->InLogicThread());
clean_frame_commands_.push_back(c);
}
@@ -180,11 +215,9 @@ auto Graphics::VSyncFromAppConfig() -> VSyncRequest {
}
auto Graphics::GraphicsQualityFromAppConfig() -> GraphicsQualityRequest {
- // Graphics quality.
std::string gqualstr =
g_base->app_config->Resolve(AppConfig::StringID::kGraphicsQuality);
GraphicsQualityRequest graphics_quality_requested;
-
if (gqualstr == "Auto") {
graphics_quality_requested = GraphicsQualityRequest::kAuto;
} else if (gqualstr == "Higher") {
@@ -308,39 +341,6 @@ auto Graphics::GetShadowDensity(float x, float y, float z) -> float {
}
}
-class Graphics::ScreenMessageEntry {
- public:
- ScreenMessageEntry(std::string s_in, bool align_left_in, uint32_t c,
- const Vector3f& color_in, TextureAsset* texture_in,
- TextureAsset* tint_texture_in, const Vector3f& tint_in,
- const Vector3f& tint2_in)
- : align_left(align_left_in),
- creation_time(c),
- s_raw(std::move(s_in)),
- color(color_in),
- texture(texture_in),
- tint_texture(tint_texture_in),
- tint(tint_in),
- tint2(tint2_in) {}
- auto GetText() -> TextGroup&;
- void UpdateTranslation();
- bool align_left;
- uint32_t creation_time;
- Vector3f color;
- Vector3f tint;
- Vector3f tint2;
- std::string s_raw;
- std::string s_translated;
- Object::Ref texture;
- Object::Ref tint_texture;
- float v_smoothed{};
- bool translation_dirty{true};
- bool mesh_dirty{true};
-
- private:
- Object::Ref s_mesh_;
-};
-
// Draw controls and things that lie on top of the action.
void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
RenderPass* pass = frame_def->overlay_pass();
@@ -358,7 +358,6 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
last_fps_ = total_frames_rendered - last_total_frames_rendered_;
last_total_frames_rendered_ = total_frames_rendered;
}
- float v{};
if (show_fps_) {
char fps_str[32];
@@ -372,7 +371,7 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
}
SimpleComponent c(pass);
c.SetTransparent(true);
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
c.SetColor(1, 1, 1, 1);
} else {
c.SetColor(0.8f, 0.8f, 0.8f, 1.0f);
@@ -380,13 +379,17 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
int text_elem_count = fps_text_group_->GetElementCount();
for (int e = 0; e < text_elem_count; e++) {
c.SetTexture(fps_text_group_->GetElementTexture(e));
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
c.SetShadow(-0.003f * fps_text_group_->GetElementUScale(e),
-0.003f * fps_text_group_->GetElementVScale(e), 0.0f, 1.0f);
c.SetMaskUV2Texture(fps_text_group_->GetElementMaskUV2Texture(e));
}
c.SetFlatness(1.0f);
- c.DrawMesh(fps_text_group_->GetElementMesh(e));
+ {
+ auto xf = c.ScopedTransform();
+ c.Translate(6.0f, 6.0f, kScreenTextZDepth);
+ c.DrawMesh(fps_text_group_->GetElementMesh(e));
+ }
}
c.Submit();
}
@@ -419,8 +422,8 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
c.SetFlatness(1.0f);
{
auto xf = c.ScopedTransform();
- c.Translate(14.0f + (show_fps_ ? 30.0f : 0.0f), 0.1f,
- kScreenMessageZDepth);
+ c.Translate(6.0f + 14.0f + (show_fps_ ? 35.0f : 0.0f), 6.0f + 1.0f,
+ kScreenTextZDepth);
c.Scale(0.7f, 0.7f);
c.DrawMesh(ping_text_group_->GetElementMesh(e));
}
@@ -448,7 +451,7 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
c.SetFlatness(1.0f);
{
auto xf = c.ScopedTransform();
- c.Translate(4.0f, (show_fps_ ? 66.0f : 40.0f), kScreenMessageZDepth);
+ c.Translate(4.0f, (show_fps_ ? 66.0f : 40.0f), kScreenTextZDepth);
c.Scale(0.7f, 0.7f);
c.DrawMesh(net_info_text_group_->GetElementMesh(e));
}
@@ -466,9 +469,8 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
if (now - it->second->LastUsedTime() > 1000) {
it = debug_graphs_.erase(it);
} else {
- it->second->Draw(pass,
- static_cast(g_core->GetAppTimeMillisecs()),
- 50.0f, debug_graph_y, 500.0f, 100.0f);
+ it->second->Draw(pass, g_base->logic->display_time() * 1000.0, 50.0f,
+ debug_graph_y, 500.0f, 100.0f);
debug_graph_y += 110.0f;
++it;
@@ -476,367 +478,7 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) {
}
}
- // Screen messages (bottom).
- {
- // Delete old ones.
- if (!screen_messages_.empty()) {
- millisecs_t cutoff;
- if (g_core->GetAppTimeMillisecs() > 5000) {
- cutoff = g_core->GetAppTimeMillisecs() - 5000;
- for (auto i = screen_messages_.begin(); i != screen_messages_.end();) {
- if (i->creation_time < cutoff) {
- auto next = i;
- next++;
- screen_messages_.erase(i);
- i = next;
- } else {
- i++;
- }
- }
- }
- }
-
- // Delete if we have too many.
- while ((screen_messages_.size()) > 4) {
- screen_messages_.erase(screen_messages_.begin());
- }
-
- // Draw all existing.
- if (!screen_messages_.empty()) {
- bool vr = g_core->IsVRMode();
-
- // These are less disruptive in the middle for menus but at the bottom
- // during gameplay.
- float start_v = g_base->graphics->screen_virtual_height() * 0.05f;
- float scale;
- switch (g_base->ui->scale()) {
- case UIScale::kSmall:
- scale = 1.5f;
- break;
- case UIScale::kMedium:
- scale = 1.2f;
- break;
- default:
- scale = 1.0f;
- break;
- }
-
- // Shadows.
- {
- SimpleComponent c(pass);
- c.SetTransparent(true);
- c.SetTexture(
- g_base->assets->SysTexture(SysTextureID::kSoftRectVertical));
-
- float screen_width = g_base->graphics->screen_virtual_width();
-
- v = start_v;
-
- millisecs_t youngest_age = 9999;
-
- for (auto i = screen_messages_.rbegin(); i != screen_messages_.rend();
- i++) {
- // Update the translation if need be.
- i->UpdateTranslation();
-
- millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time;
- youngest_age = std::min(youngest_age, age);
- float s_extra = 1.0f;
- if (age < 100) {
- s_extra = std::min(1.2f, 1.2f * (static_cast(age) / 100.0f));
- } else if (age < 150) {
- s_extra =
- 1.2f - 0.2f * ((150.0f - static_cast(age)) / 50.0f);
- }
-
- float a;
- if (age > 3000) {
- a = 1.0f - static_cast(age - 3000) / 2000;
- } else {
- a = 1;
- }
- a *= 0.8f;
-
- if (vr) {
- a *= 0.8f;
- }
-
- if (i->translation_dirty) {
- BA_LOG_ONCE(
- LogLevel::kWarning,
- "Found dirty translation on screenmessage draw pass 1; raw="
- + i->s_raw);
- }
- float str_height =
- g_base->text_graphics->GetStringHeight(i->s_translated.c_str());
- float str_width =
- g_base->text_graphics->GetStringWidth(i->s_translated.c_str());
-
- if ((str_width * scale) > (screen_width - 40)) {
- s_extra *= ((screen_width - 40) / (str_width * scale));
- }
-
- float r = i->color.x;
- float g = i->color.y;
- float b = i->color.z;
- GetSafeColor(&r, &g, &b);
-
- float v_extra = scale * (static_cast(youngest_age) * 0.01f);
-
- float fade;
- if (age < 100) {
- fade = 1.0f;
- } else {
- fade = std::max(0.0f, (200.0f - static_cast(age)) / 100.0f);
- }
- c.SetColor(r * fade, g * fade, b * fade, a);
-
- {
- auto xf = c.ScopedTransform();
-
- if (i->v_smoothed == 0.0f) {
- i->v_smoothed = v + v_extra;
- } else {
- float smoothing = 0.8f;
- i->v_smoothed = smoothing * i->v_smoothed
- + (1.0f - smoothing) * (v + v_extra);
- }
- c.Translate(screen_width * 0.5f, i->v_smoothed,
- vr ? 60 : kScreenMessageZDepth);
- if (vr) {
- // Let's drop down a bit in vr mode.
- c.Translate(0, -10.0f, 0);
- c.Scale((str_width + 60) * scale * s_extra,
- (str_height + 20) * scale * s_extra);
-
- // Align our bottom with where we just scaled from.
- c.Translate(0, 0.5f, 0);
- } else {
- c.Scale((str_width + 110) * scale * s_extra,
- (str_height + 40) * scale * s_extra);
-
- // Align our bottom with where we just scaled from.
- c.Translate(0, 0.5f, 0);
- }
- c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1));
- }
-
- v += scale * (36 + str_height);
- if (v > g_base->graphics->screen_virtual_height() + 30) {
- break;
- }
- }
- c.Submit();
- }
-
- // Now the strings themselves.
- {
- SimpleComponent c(pass);
- c.SetTransparent(true);
-
- float screen_width = g_base->graphics->screen_virtual_width();
- v = start_v;
- millisecs_t youngest_age = 9999;
-
- for (auto i = screen_messages_.rbegin(); i != screen_messages_.rend();
- i++) {
- millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time;
- youngest_age = std::min(youngest_age, age);
- float s_extra = 1.0f;
- if (age < 100) {
- s_extra = std::min(1.2f, 1.2f * (static_cast(age) / 100.0f));
- } else if (age < 150) {
- s_extra =
- 1.2f - 0.2f * ((150.0f - static_cast(age)) / 50.0f);
- }
- float a;
- if (age > 3000) {
- a = 1.0f - static_cast(age - 3000) / 2000;
- } else {
- a = 1;
- }
- if (i->translation_dirty) {
- BA_LOG_ONCE(
- LogLevel::kWarning,
- "Found dirty translation on screenmessage draw pass 2; raw="
- + i->s_raw);
- }
- float str_height =
- g_base->text_graphics->GetStringHeight(i->s_translated.c_str());
- float str_width =
- g_base->text_graphics->GetStringWidth(i->s_translated.c_str());
-
- if ((str_width * scale) > (screen_width - 40)) {
- s_extra *= ((screen_width - 40) / (str_width * scale));
- }
- float r = i->color.x;
- float g = i->color.y;
- float b = i->color.z;
- GetSafeColor(&r, &g, &b, 0.85f);
-
- int elem_count = i->GetText().GetElementCount();
- for (int e = 0; e < elem_count; e++) {
- // Gracefully skip unloaded textures.
- TextureAsset* t = i->GetText().GetElementTexture(e);
- if (!t->preloaded()) {
- continue;
- }
- c.SetTexture(t);
- if (i->GetText().GetElementCanColor(e)) {
- c.SetColor(r, g, b, a);
- } else {
- c.SetColor(1, 1, 1, a);
- }
- c.SetFlatness(i->GetText().GetElementMaxFlatness(e));
- {
- auto xf = c.ScopedTransform();
- c.Translate(screen_width * 0.5f, i->v_smoothed,
- vr ? 150 : kScreenMessageZDepth);
- c.Scale(scale * s_extra, scale * s_extra);
- c.Translate(0, 20);
- c.DrawMesh(i->GetText().GetElementMesh(e));
- }
- }
-
- v += scale * (36 + str_height);
- if (v > g_base->graphics->screen_virtual_height() + 30) {
- break;
- }
- }
- c.Submit();
- }
- }
- }
-
- // Screen messages (top).
- {
- // Delete old ones.
- if (!screen_messages_top_.empty()) {
- millisecs_t cutoff;
- if (g_core->GetAppTimeMillisecs() > 5000) {
- cutoff = g_core->GetAppTimeMillisecs() - 5000;
- for (auto i = screen_messages_top_.begin();
- i != screen_messages_top_.end();) {
- if (i->creation_time < cutoff) {
- auto next = i;
- next++;
- screen_messages_top_.erase(i);
- i = next;
- } else {
- i++;
- }
- }
- }
- }
-
- // Delete if we have too many.
- while ((screen_messages_top_.size()) > 6) {
- screen_messages_top_.erase(screen_messages_top_.begin());
- }
-
- if (!screen_messages_top_.empty()) {
- SimpleComponent c(pass);
- c.SetTransparent(true);
-
- // Draw all existing.
- float h = pass->virtual_width() - 300.0f;
- v = g_base->graphics->screen_virtual_height() - 50.0f;
-
- float v_base = g_base->graphics->screen_virtual_height();
- float last_v = -999.0f;
-
- float min_spacing = 25.0f;
-
- for (auto i = screen_messages_top_.rbegin();
- i != screen_messages_top_.rend(); i++) {
- // Update the translation if need be.
- i->UpdateTranslation();
-
- millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time;
- float s_extra = 1.0f;
- if (age < 100) {
- s_extra = std::min(1.1f, 1.1f * (static_cast(age) / 100.0f));
- } else if (age < 150) {
- s_extra = 1.1f - 0.1f * ((150.0f - static_cast(age)) / 50.0f);
- }
-
- float a;
- if (age > 3000) {
- a = 1.0f - static_cast(age - 3000) / 2000;
- } else {
- a = 1;
- }
-
- i->v_smoothed += 0.1f;
- if (i->v_smoothed - last_v < min_spacing) {
- i->v_smoothed +=
- 8.0f * (1.0f - ((i->v_smoothed - last_v) / min_spacing));
- }
- last_v = i->v_smoothed;
-
- // Draw the image if they provided one.
- if (i->texture.Exists()) {
- c.Submit();
-
- SimpleComponent c2(pass);
- c2.SetTransparent(true);
- c2.SetTexture(i->texture);
- if (i->tint_texture.Exists()) {
- c2.SetColorizeTexture(i->tint_texture.Get());
- c2.SetColorizeColor(i->tint.x, i->tint.y, i->tint.z);
- c2.SetColorizeColor2(i->tint2.x, i->tint2.y, i->tint2.z);
- c2.SetMaskTexture(
- g_base->assets->SysTexture(SysTextureID::kCharacterIconMask));
- }
- c2.SetColor(1, 1, 1, a);
- {
- auto xf = c2.ScopedTransform();
- c2.Translate(h - 14, v_base + 10 + i->v_smoothed,
- kScreenMessageZDepth);
- c2.Scale(22.0f * s_extra, 22.0f * s_extra);
- c2.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1));
- }
- c2.Submit();
- }
-
- float r = i->color.x;
- float g = i->color.y;
- float b = i->color.z;
- GetSafeColor(&r, &g, &b);
-
- int elem_count = i->GetText().GetElementCount();
- for (int e = 0; e < elem_count; e++) {
- // Gracefully skip unloaded textures.
- TextureAsset* t = i->GetText().GetElementTexture(e);
- if (!t->preloaded()) {
- continue;
- }
- c.SetTexture(t);
- if (i->GetText().GetElementCanColor(e)) {
- c.SetColor(r, g, b, a);
- } else {
- c.SetColor(1, 1, 1, a);
- }
- c.SetShadow(-0.003f * i->GetText().GetElementUScale(e),
- -0.003f * i->GetText().GetElementVScale(e), 0.0f,
- 1.0f * a);
- c.SetFlatness(i->GetText().GetElementMaxFlatness(e));
- c.SetMaskUV2Texture(i->GetText().GetElementMaskUV2Texture(e));
- {
- auto xf = c.ScopedTransform();
- c.Translate(h, v_base + 2 + i->v_smoothed, kScreenMessageZDepth);
- c.Scale(0.6f * s_extra, 0.6f * s_extra);
- c.DrawMesh(i->GetText().GetElementMesh(e));
- }
- }
- assert(!i->translation_dirty);
- v -= g_base->text_graphics->GetStringHeight(i->s_translated.c_str())
- * 0.6f
- + 8.0f;
- }
- c.Submit();
- }
- }
+ screenmessages->DrawMiscOverlays(frame_def);
}
auto Graphics::GetDebugGraph(const std::string& name, bool smoothed)
@@ -864,11 +506,11 @@ void Graphics::GetSafeColor(float* red, float* green, float* blue,
*blue = std::min(1.0f, (*blue) * s);
}
- // We may still be short of our target intensity due to clamping (ie: (10,0,0)
- // will not look any brighter than (1,0,0)) if that's the case, just convert
- // the difference to a grey value and add that to all channels... this *still*
- // might not get us there so lets do it a few times if need be. (i'm sure
- // there's a less bone-headed way to do this)
+ // We may still be short of our target intensity due to clamping (ie:
+ // (10,0,0) will not look any brighter than (1,0,0)) if that's the case,
+ // just convert the difference to a grey value and add that to all
+ // channels... this *still* might not get us there so lets do it a few times
+ // if need be. (i'm sure there's a less bone-headed way to do this)
for (int i = 0; i < 4; i++) {
float remaining =
(0.2989f * (*red) + 0.5870f * (*green) + 0.1140f * (*blue)) - 1.0f;
@@ -882,32 +524,6 @@ void Graphics::GetSafeColor(float* red, float* green, float* blue,
}
}
-void Graphics::AddScreenMessage(const std::string& msg, const Vector3f& color,
- bool top, TextureAsset* texture,
- TextureAsset* tint_texture,
- const Vector3f& tint, const Vector3f& tint2) {
- assert(g_base->InLogicThread());
-
- // So we know we're always dealing with valid utf8.
- std::string m = Utils::GetValidUTF8(msg.c_str(), "ga9msg");
-
- if (top) {
- float start_v = -40.0f;
- if (!screen_messages_top_.empty()) {
- start_v = std::min(
- start_v,
- std::max(-100.0f, screen_messages_top_.back().v_smoothed - 25.0f));
- }
- screen_messages_top_.emplace_back(m, true, g_core->GetAppTimeMillisecs(),
- color, texture, tint_texture, tint,
- tint2);
- screen_messages_top_.back().v_smoothed = start_v;
- } else {
- screen_messages_.emplace_back(m, false, g_core->GetAppTimeMillisecs(),
- color, texture, tint_texture, tint, tint2);
- }
-}
-
void Graphics::Reset() {
assert(g_base->InLogicThread());
fade_ = 0;
@@ -917,9 +533,7 @@ void Graphics::Reset() {
camera_ = Object::New();
}
- // Wipe out top screen messages since they might be using textures that are
- // being reset. Bottom ones are ok since they have no textures.
- screen_messages_top_.clear();
+ screenmessages->Reset();
}
void Graphics::InitInternalComponents(FrameDef* frame_def) {
@@ -930,7 +544,7 @@ void Graphics::InitInternalComponents(FrameDef* frame_def) {
// Let's draw a bit bigger than screen to account for tv-border-mode.
float w = pass->virtual_width();
float h = pass->virtual_height();
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
screen_mesh_->SetPositionAndSize(
-(0.5f * kVRBorder) * w, (-0.5f * kVRBorder) * h, kScreenMeshZDepth,
(1.0f + kVRBorder) * w, (1.0f + kVRBorder) * h);
@@ -960,6 +574,26 @@ auto Graphics::GetEmptyFrameDef() -> FrameDef* {
return frame_def;
}
+auto Graphics::GetGraphicsSettingsSnapshot() -> Snapshot* {
+ assert(g_base->InLogicThread());
+
+ // If need be, ask the app-adapter to build us a new settings instance.
+ if (graphics_settings_dirty_) {
+ auto* new_settings = g_base->app_adapter->GetGraphicsSettings();
+ new_settings->index = next_settings_index_++;
+ settings_snapshot_ = Object::New>(new_settings);
+ graphics_settings_dirty_ = false;
+
+ // We keep a cached copy of this value since we use it a lot.
+ tv_border_ = settings_snapshot_->Get()->tv_border;
+
+ // This can affect placeholder settings; keep those up to date.
+ UpdatePlaceholderSettings();
+ }
+ assert(settings_snapshot_.Exists());
+ return settings_snapshot_.Get();
+}
+
void Graphics::ClearFrameDefDeleteList() {
assert(g_base->InLogicThread());
std::scoped_lock lock(frame_def_delete_list_mutex_);
@@ -977,7 +611,7 @@ void Graphics::ClearFrameDefDeleteList() {
}
void Graphics::FadeScreen(bool to, millisecs_t time, PyObject* endcall) {
- BA_PRECONDITION(g_base->InLogicThread());
+ assert(g_base->InLogicThread());
// If there's an ourstanding fade-end command, go ahead and run it.
// (otherwise, overlapping fades can cause things to get lost)
if (fade_end_call_.Exists()) {
@@ -1050,14 +684,13 @@ void Graphics::UpdateGyro(microsecs_t time_microsecs,
tilt_vel_ = tilt_smoothed_ * 3.0f;
tilt_pos_ += tilt_vel_ * timescale;
- // Technically this will behave slightly differently at different time scales,
- // but it should be close to correct..
- // tilt_pos_ *= 0.991f;
+ // Technically this will behave slightly differently at different time
+ // scales, but it should be close to correct.. tilt_pos_ *= 0.991f;
tilt_pos_ *= std::max(0.0f, 1.0f - 0.01f * timescale);
// Some gyros seem wonky and either give us crazy big values or consistently
- // offset ones. Let's keep a running tally of magnitude that slowly drops over
- // time, and if it reaches a certain value lets just kill gyro input.
+ // offset ones. Let's keep a running tally of magnitude that slowly drops
+ // over time, and if it reaches a certain value lets just kill gyro input.
if (gyro_broken_) {
tilt_pos_ *= 0.0f;
} else {
@@ -1105,6 +738,8 @@ void Graphics::DrawDevUI(FrameDef* frame_def) {
void Graphics::BuildAndPushFrameDef() {
assert(g_base->InLogicThread());
+
+ assert(g_base->logic->app_bootstrapping_complete());
assert(camera_.Exists());
assert(!g_core->HeadlessMode());
@@ -1113,10 +748,6 @@ void Graphics::BuildAndPushFrameDef() {
assert(!building_frame_def_);
building_frame_def_ = true;
- // We should not be building/pushing any frames until the native
- // layer is fully bootstrapped.
- BA_PRECONDITION_FATAL(g_base->logic->app_bootstrapping_complete());
-
microsecs_t app_time_microsecs = g_core->GetAppTimeMicrosecs();
// Store how much time this frame_def represents.
@@ -1172,13 +803,6 @@ void Graphics::BuildAndPushFrameDef() {
internal_components_inited_ = true;
}
- // If graphics quality has changed since our last draw, inform anyone who
- // wants to know.
- if (last_frame_def_graphics_quality_ != frame_def->quality()) {
- last_frame_def_graphics_quality_ = frame_def->quality();
- g_base->app_mode()->GraphicsQualityChanged(frame_def->quality());
- }
-
ApplyCamera(frame_def);
if (progress_bar_) {
@@ -1220,7 +844,7 @@ void Graphics::BuildAndPushFrameDef() {
// Sanity test: If we're in VR, the only reason we should have stuff in
// the flat overlay pass is if there's windows present (we want to avoid
// drawing/blitting the 2d UI buffer during gameplay for efficiency).
- if (g_core->IsVRMode()) {
+ if (g_core->vr_mode()) {
if (frame_def->GetOverlayFlatPass()->HasDrawCommands()) {
if (!g_base->ui->MainMenuVisible()) {
BA_LOG_ONCE(LogLevel::kError,
@@ -1239,7 +863,7 @@ void Graphics::BuildAndPushFrameDef() {
RunCleanFrameCommands();
}
- frame_def->Finalize();
+ frame_def->Complete();
// Include all mesh-data loads and unloads that have accumulated up to
// this point the graphics thread will have to handle these before
@@ -1373,7 +997,10 @@ void Graphics::DrawFades(FrameDef* frame_def) {
// Guard against accidental fades that never fade back in.
if (fade_ <= 0.0f && fade_out_) {
millisecs_t faded_time = real_time - (fade_start_ + fade_time_);
- if (faded_time > 15000) {
+
+ // TEMP HACK - don't trigger this while inactive.
+ // Need to make overall fade logic smarter.
+ if (faded_time > 15000 && g_base->app_active()) {
Log(LogLevel::kError, "FORCE-ENDING STUCK FADE");
fade_out_ = false;
fade_ = 1.0f;
@@ -1450,10 +1077,10 @@ void Graphics::DoDrawFade(FrameDef* frame_def, float amt) {
void Graphics::DrawCursor(FrameDef* frame_def) {
assert(g_base->InLogicThread());
- millisecs_t app_time_millisecs = frame_def->app_time_millisecs();
+ auto app_time = frame_def->app_time();
- bool can_show_cursor = g_base->app_adapter->ShouldUseCursor();
- bool should_show_cursor =
+ auto can_show_cursor = g_base->app_adapter->ShouldUseCursor();
+ auto should_show_cursor =
camera_->manual() || g_base->input->IsCursorVisible();
if (g_base->app_adapter->HasHardwareCursor()) {
@@ -1467,9 +1094,9 @@ void Graphics::DrawCursor(FrameDef* frame_def) {
// Ship this state when it changes and also every now and then just in
// case things go wonky.
if (new_cursor_visibility != hardware_cursor_visible_
- || app_time_millisecs - last_cursor_visibility_event_time_ > 2000) {
+ || app_time - last_cursor_visibility_event_time_ > 2.137) {
hardware_cursor_visible_ = new_cursor_visibility;
- last_cursor_visibility_event_time_ = app_time_millisecs;
+ last_cursor_visibility_event_time_ = app_time;
g_base->app_adapter->PushMainThreadCall([this] {
assert(g_core && g_core->InMainThread());
g_base->app_adapter->SetHardwareCursorVisible(hardware_cursor_visible_);
@@ -1485,9 +1112,9 @@ void Graphics::DrawCursor(FrameDef* frame_def) {
{
auto xf = c.ScopedTransform();
- // Note: we don't plug in known cursor position values here; we tell the
- // renderer to insert the latest values on its end; this can lessen
- // cursor lag substantially.
+ // Note: we don't plug in known cursor position values here; we tell
+ // the renderer to insert the latest values on its end; this can
+ // lessen cursor lag substantially.
c.CursorTranslate();
c.Translate(csize * 0.40f, csize * -0.38f, kCursorZDepth);
c.Scale(csize, csize);
@@ -1540,21 +1167,6 @@ void Graphics::DrawBlotches(FrameDef* frame_def) {
}
}
-void Graphics::SetSupportsHighQualityGraphics(bool s) {
- supports_high_quality_graphics_ = s;
- has_supports_high_quality_graphics_value_ = true;
-}
-
-void Graphics::ClearScreenMessageTranslations() {
- assert(g_base && g_base->InLogicThread());
- for (auto&& i : screen_messages_) {
- i.translation_dirty = true;
- }
- for (auto&& i : screen_messages_top_) {
- i.translation_dirty = true;
- }
-}
-
void Graphics::ReturnCompletedFrameDef(FrameDef* frame_def) {
std::scoped_lock lock(frame_def_delete_list_mutex_);
g_base->graphics->frame_def_delete_list_.push_back(frame_def);
@@ -1885,51 +1497,57 @@ void Graphics::DrawRadialMeter(MeshIndexedSimpleFull* m, float amt) {
}
}
-auto Graphics::ScreenMessageEntry::GetText() -> TextGroup& {
- if (translation_dirty) {
- BA_LOG_ONCE(
- LogLevel::kWarning,
- "Found dirty translation on screenmessage GetText; raw=" + s_raw);
- }
- if (!s_mesh_.Exists()) {
- s_mesh_ = Object::New();
- mesh_dirty = true;
- }
- if (mesh_dirty) {
- s_mesh_->SetText(
- s_translated,
- align_left ? TextMesh::HAlign::kLeft : TextMesh::HAlign::kCenter,
- TextMesh::VAlign::kBottom);
- mesh_dirty = false;
- }
- return *s_mesh_;
-}
-
void Graphics::OnScreenSizeChange() {}
-void Graphics::SetScreenSize(float virtual_width, float virtual_height,
- float pixel_width, float pixel_height) {
+void Graphics::CalcVirtualRes_(float* x, float* y) {
+ float x_in = *x;
+ float y_in = *y;
+ if (*x / *y > static_cast(kBaseVirtualResX)
+ / static_cast(kBaseVirtualResY)) {
+ *y = kBaseVirtualResY;
+ *x = *y * (x_in / y_in);
+ } else {
+ *x = kBaseVirtualResX;
+ *y = *x * (y_in / x_in);
+ }
+}
+
+void Graphics::SetScreenResolution(float x, float y) {
assert(g_base->InLogicThread());
- res_x_virtual_ = virtual_width;
- res_y_virtual_ = virtual_height;
- res_x_ = pixel_width;
- res_y_ = pixel_height;
+
+ // Ignore redundant sets.
+ if (res_x_ == x && res_y_ == y) {
+ return;
+ }
+
+ // We'll need to ship a new settings to the server with this change.
+ graphics_settings_dirty_ = true;
+
+ res_x_ = x;
+ res_y_ = y;
+
+ // Calc virtual res. In vr mode our virtual res is independent of our
+ // screen size (since it gets drawn to an overlay).
+ if (g_core->vr_mode()) {
+ res_x_virtual_ = kBaseVirtualResX;
+ res_y_virtual_ = kBaseVirtualResY;
+ } else {
+ res_x_virtual_ = res_x_;
+ res_y_virtual_ = res_y_;
+ CalcVirtualRes_(&res_x_virtual_, &res_y_virtual_);
+ }
// Need to rebuild internal components (some are sized to the screen).
internal_components_inited_ = false;
- // This will inform all applicable logic thread subsystems.
- g_base->logic->OnScreenSizeChange(virtual_width, virtual_height, pixel_width,
- pixel_height);
-}
+ // Inform all our logic thread buddies of this change.
+ g_base->logic->OnScreenSizeChange(res_x_virtual_, res_y_virtual_, res_x_,
+ res_y_);
-void Graphics::ScreenMessageEntry::UpdateTranslation() {
- if (translation_dirty) {
- s_translated = g_base->assets->CompileResourceString(
- s_raw, "Graphics::ScreenMessageEntry::UpdateTranslation");
- translation_dirty = false;
- mesh_dirty = true;
- }
+ // This may trigger us sending initial graphics settings to the
+ // graphics-server to kick off drawing.
+ got_screen_resolution_ = true;
+ UpdateInitialGraphicsSettingsSend_();
}
auto Graphics::CubeMapFromReflectionType(ReflectionType reflection_type)
@@ -2011,8 +1629,77 @@ void Graphics::LanguageChanged() {
Log(LogLevel::kWarning,
"Graphics::LanguageChanged() called during draw; should not happen.");
}
- // Also clear translations on all screen-messages.
- ClearScreenMessageTranslations();
+ screenmessages->ClearScreenMessageTranslations();
+}
+
+auto Graphics::GraphicsQualityFromRequest(GraphicsQualityRequest request,
+ GraphicsQuality auto_val)
+ -> GraphicsQuality {
+ switch (request) {
+ case GraphicsQualityRequest::kLow:
+ return GraphicsQuality::kLow;
+ case GraphicsQualityRequest::kMedium:
+ return GraphicsQuality::kMedium;
+ case GraphicsQualityRequest::kHigh:
+ return GraphicsQuality::kHigh;
+ case GraphicsQualityRequest::kHigher:
+ return GraphicsQuality::kHigher;
+ case GraphicsQualityRequest::kAuto:
+ return auto_val;
+ default:
+ Log(LogLevel::kError, "Unhandled GraphicsQualityRequest value: "
+ + std::to_string(static_cast(request)));
+ return GraphicsQuality::kLow;
+ }
+}
+
+auto Graphics::TextureQualityFromRequest(TextureQualityRequest request,
+ TextureQuality auto_val)
+ -> TextureQuality {
+ switch (request) {
+ case TextureQualityRequest::kLow:
+ return TextureQuality::kLow;
+ case TextureQualityRequest::kMedium:
+ return TextureQuality::kMedium;
+ case TextureQualityRequest::kHigh:
+ return TextureQuality::kHigh;
+ case TextureQualityRequest::kAuto:
+ return auto_val;
+ default:
+ Log(LogLevel::kError, "Unhandled TextureQualityRequest value: "
+ + std::to_string(static_cast(request)));
+ return TextureQuality::kLow;
+ }
+}
+
+void Graphics::set_client_context(Snapshot* context) {
+ assert(g_base->InLogicThread());
+
+ // Currently we only expect this to be set once. That will change once we
+ // support renderer swapping/etc.
+ assert(!g_base->logic->graphics_ready());
+ assert(!client_context_snapshot_.Exists());
+ client_context_snapshot_ = context;
+
+ // Placeholder settings are affected by client context, so update them
+ // when it changes.
+ UpdatePlaceholderSettings();
+
+ // Let the logic system know its free to proceed beyond bootstrapping.
+ g_base->logic->OnGraphicsReady();
+}
+
+// This call exists for the graphics-server to call when they've changed
+void Graphics::UpdatePlaceholderSettings() {
+ assert(g_base->InLogicThread());
+
+ // Need both of these in place.
+ if (!settings_snapshot_.Exists() || !has_client_context()) {
+ return;
+ }
+
+ texture_quality_placeholder_ = TextureQualityFromRequest(
+ settings()->texture_quality, client_context()->auto_texture_quality);
}
} // namespace ballistica::base
diff --git a/src/ballistica/base/graphics/graphics.h b/src/ballistica/base/graphics/graphics.h
index 92e6c18f..93ef8310 100644
--- a/src/ballistica/base/graphics/graphics.h
+++ b/src/ballistica/base/graphics/graphics.h
@@ -6,13 +6,15 @@
#include
#include