diff --git a/.efrocachemap b/.efrocachemap index b7a571d2..62b7c7dd 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -421,41 +421,41 @@ "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": "2a2c4783fddc4b24d07b4ce0d8a74393", - "build/assets/ba_data/data/languages/arabic.json": "295c559911fa251f401f8cdcad91c226", - "build/assets/ba_data/data/languages/belarussian.json": "e151808b6b4f6dc159cf55ee62adad3c", - "build/assets/ba_data/data/languages/chinese.json": "0a9d9534e7329d1e886adae6fdc007c4", + "build/assets/ba_data/data/langdata.json": "dca47b4976752e76e852f7a4be2c7aa3", + "build/assets/ba_data/data/languages/arabic.json": "e0001d8542c904bc3c98a174f256efd9", + "build/assets/ba_data/data/languages/belarussian.json": "7fe38341815ca6ff4d95224196e7a67e", + "build/assets/ba_data/data/languages/chinese.json": "a87b1ebef100d9cdce0d29123735e90c", "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/danish.json": "3fd69080783d5c9dcc0af737f02b6f1e", "build/assets/ba_data/data/languages/dutch.json": "22b44a33bf81142ba2befad14eb5746e", - "build/assets/ba_data/data/languages/english.json": "6fb6ec37e79064edb4b8864eabdd024d", + "build/assets/ba_data/data/languages/english.json": "fe32cbe79483153db283e363da039a7a", "build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880", - "build/assets/ba_data/data/languages/filipino.json": "2efdfb879135b196a272dd47fc2039a2", - "build/assets/ba_data/data/languages/french.json": "4e218dcd488fa63e7db5b4da2261b9e1", + "build/assets/ba_data/data/languages/filipino.json": "afbda3adf14555e1567ee63c32e340e7", + "build/assets/ba_data/data/languages/french.json": "49ff6d211537b8003b8241438dca661d", "build/assets/ba_data/data/languages/german.json": "450fa41ae264f29a5d1af22143d0d0ad", - "build/assets/ba_data/data/languages/gibberish.json": "63c6212c774622346f3ad0d87ff31e80", + "build/assets/ba_data/data/languages/gibberish.json": "00a9f70bccdfa407043e69a42e5b944c", "build/assets/ba_data/data/languages/greek.json": "287c0ec437b38772284ef9d3e4fb2fc3", "build/assets/ba_data/data/languages/hindi.json": "8ea0c58a44a24edb131d0e53b074d1f6", "build/assets/ba_data/data/languages/hungarian.json": "796a290a8c44a1e7635208c2ff5fdc6e", - "build/assets/ba_data/data/languages/indonesian.json": "53961b1484a1831f32bec2cc2941e672", - "build/assets/ba_data/data/languages/italian.json": "58ecf53a963dbeca1bbf3605e5ab6a2f", + "build/assets/ba_data/data/languages/indonesian.json": "ba39cade3c965b2ddbee122880b6f0db", + "build/assets/ba_data/data/languages/italian.json": "b19a9f0b868a6c89e13a04609bf377dd", "build/assets/ba_data/data/languages/korean.json": "ca1122a9ee551da3f75ae632012bd0e2", "build/assets/ba_data/data/languages/malay.json": "832562ce997fc70704b9234c95fb2e38", - "build/assets/ba_data/data/languages/persian.json": "a391d80ff58ea22926499e4b19d2c0d0", - "build/assets/ba_data/data/languages/polish.json": "7a4a6cb882cf90dad32e6607215525bf", - "build/assets/ba_data/data/languages/portuguese.json": "51e362956f89da3eec980f587c092253", + "build/assets/ba_data/data/languages/persian.json": "89f5c6f900317aaa20ec520e5fd65d32", + "build/assets/ba_data/data/languages/polish.json": "8bb8f7baa5efcf6cdd627140d209c2b2", + "build/assets/ba_data/data/languages/portuguese.json": "7374b6a5cadcf573ee4ab4a5b4bc8c0a", "build/assets/ba_data/data/languages/romanian.json": "aeebdd54f65939c2facc6ac50c117826", - "build/assets/ba_data/data/languages/russian.json": "561504cca28eb3204ac194950029e565", + "build/assets/ba_data/data/languages/russian.json": "9e068a3afdec43b207c83aedfb812e82", "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": "5a4dbd505060dd02d634bd4de4d5faab", - "build/assets/ba_data/data/languages/swedish.json": "77d671f10613291ebf9c71da66f18a18", - "build/assets/ba_data/data/languages/tamil.json": "65ab7798d637fa62a703750179eeb723", + "build/assets/ba_data/data/languages/spanish.json": "0fca8361c6873e6445f0bea34a211b78", + "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": "33f63753c9af9a5b238d229a0bf23fbc", - "build/assets/ba_data/data/languages/turkish.json": "776d1a0c9ef2333a9110d93558ab19e2", - "build/assets/ba_data/data/languages/ukrainian.json": "f72eb51abfbbb56e27866895d7e947d2", + "build/assets/ba_data/data/languages/turkish.json": "c3674336e746bf7b23617a6ff296c69a", + "build/assets/ba_data/data/languages/ukrainian.json": "e5c861187c4c6db37d1a033f4ef3dd5a", "build/assets/ba_data/data/languages/venetian.json": "9fe1a58d9e5dfb00f31ce3b2eb9993f4", "build/assets/ba_data/data/languages/vietnamese.json": "921cd1e50f60fe3e101f246e172750ba", "build/assets/ba_data/data/maps/big_g.json": "1dd301d490643088a435ce75df971054", @@ -1524,9 +1524,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 +1539,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", @@ -4056,53 +4056,53 @@ "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": "ad13d636bcb25150044a7644846b8a09", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "7c5df955611590ef491bf614fbd60179", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "ae38bd212ae64b51482a2ccb9c1cbfd3", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "d0bcee2dd5567719aa35667c5206dffc", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "eb3cd4f86175afcf8ffa2749afa32fa3", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "6b86ba36c3719773008feaa6cdc0d0f8", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "877c9ae4532fef809a3dcbd8ffea343c", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "7df313c48c87460f56fa837502965088", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "bc40bb549d26437fb8679c1e9d088272", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "47aa08cb9f5e660023f0f3c0e4ffd65e", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "bbb0a8383d6ce1ca887190ea49223f4f", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "7dd91e3407d49981c1c975d4f01ac205", - "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "c87883aa2f832e792e945fd9208d712a", - "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "fea8fd84d8c060f2f82f402902b8c54e", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "0a454a8be47f37231655761d15e3f7e5", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "4c79db3a882eb0b8b225a8df0339b1cc", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "43b9ef321f8e80da29ddb19a760dbd77", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "6f891004f2f07c452dea29bd53f29d30", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "bf7a1ce0e7a2015d538406c6f6df761c", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "cf213dce81901a67c9970b3befdaa320", - "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "473e7e6c0cf90b9e6ac653552b18f68d", - "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "4e11b895cbf2e1339cf34bc06c54a4ea", - "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "473e7e6c0cf90b9e6ac653552b18f68d", - "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "4e11b895cbf2e1339cf34bc06c54a4ea", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "d9af1a429cff9346e0cad6fcea017e5b", - "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "ae5f87286947575463c386cfe1c443e4", - "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "d9af1a429cff9346e0cad6fcea017e5b", - "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "ae5f87286947575463c386cfe1c443e4", - "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "110eef3dc285a35a1899510e368c73b1", - "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "2692dc69f7cb2501f0aaa8675f559987", - "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "110eef3dc285a35a1899510e368c73b1", - "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "2692dc69f7cb2501f0aaa8675f559987", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "344954c4f788d7d9b4d7035ebb6131d8", - "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "48c4873dae2344c1d4092a1d85dab424", - "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "abcede4e60fa8877f18e66e086fb7387", - "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "48c4873dae2344c1d4092a1d85dab424", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "6149911c660a9864b651cc1a8e50eec1", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "57cef68ab703ba819bd0fbe9e4b1c331", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "a47bab28b86c7cefce891b8e5c8b687a", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "d68ebb1139363d711b044de65e17b204", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "ea1349137f64f3d662b9a95278ca4c02", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "c8731ff226716cee3d1e46027ead1cfe", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "4ee6b633a99c5bcbea4f5dee5bda186e", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "2c3bd4952b30d88247229ad309f73092", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "a2c1b1a31a42a24e5f2188983ff97c0e", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "8d146105a6df3376ec2f575401f29ec2", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "5e0e63f5fea108212bfb35c4fdc05dd2", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "85797e98f9a21f168a986b7eb3219688", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "95bc2213d2fb30e8248ddf06764a3663", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "f09d7055120d50f37b605ff3a4cbf61d", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "8f23de3825b9e9cf6ed4715125aeb956", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "f7b88875b37a39db57d77bebb05f822f", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "0b719dc609de364814299a5baa9a5b0b", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "07aa53835bdd2d2a4113d61a89a7da1d", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "1a1c4db580157e38effe4b81c35aca52", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "1e44d0a7160d442b1eb4d348952b0d06", + "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "a85fcf6a12483bde66559dbdce88b730", + "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "d53e31771fe31615c66bd0d1e1292b2f", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "e1804380d5ebec56f8f0af2e2547316e", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "d9665cdf870247b6d8e9475419b04038", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "0cac36823713816d03a6922b96cecd0a", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "bd741122315ced7b703de1fed2d372cd", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "f11edd5da3ce1a3e804bfc61b7ca9c1e", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "3d647f5f63b3ac3900903cf72496cef2", + "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "f812111f1eb4ea76e99558305ce3980f", + "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "c16c1b360b27850f1ac4d3dc55194b0a", + "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "f812111f1eb4ea76e99558305ce3980f", + "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "c16c1b360b27850f1ac4d3dc55194b0a", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "473c84d44b472d3ae73df6a2f1b56839", + "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "970c415cbb7a3087a163fa6e96e634a1", + "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "473c84d44b472d3ae73df6a2f1b56839", + "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "970c415cbb7a3087a163fa6e96e634a1", + "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "86498db547380838a1c95b45ef17d28b", + "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "bf11ae92718ae3dd5116d623799dd27f", + "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "86498db547380838a1c95b45ef17d28b", + "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "bf11ae92718ae3dd5116d623799dd27f", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "147c5a5210a55978060381cc44a122db", + "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "380d456bc409849174a36c5d2f61aa1b", + "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "4fae9b286a6d1c5c729c49c483c1422a", + "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "380d456bc409849174a36c5d2f61aa1b", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "93f75d31841fe18d6bc66681c99b05a1", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "cd6500ab076fa3bdff92fc8b65bc1d64", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "0dcde1146f11e4cfd3d609f4330f7a9a", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "663e6ed62de0f857fd9a4de57067dbd3", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "ae28db2f400d3b778d4a011a72339757", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "3b13cdbaf70cf43d0a14b7a2dd744945", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "54798340a1dfedaa9052aeee180e28ff", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "18b635d88c074261a3be4f45e8244340", "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": "6df0f34207346d89a72924249ddd4706", + "src/ballistica/base/mgen/pyembed/binding_base.inc": "72bfed2cce8ff19741989dec28302f3f", "src/ballistica/base/mgen/pyembed/binding_base_app.inc": "00f81f9bd92386ec12a6e60170678a98", "src/ballistica/classic/mgen/pyembed/binding_classic.inc": "3ceb412513963f0818ab39c58bf292e3", "src/ballistica/core/mgen/pyembed/binding_core.inc": "9d0a3c9636138e35284923e0c8311c69", diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e6c34d..650440be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.28 (build 21491, api 8, 2023-10-22) +### 1.7.28 (build 21583, api 8, 2023-11-09) - 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 @@ -6,7 +6,7 @@ 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. - `ballistica/base/app_adapter/app_adapter_sdl.cc` for an example of the now + `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 @@ -154,6 +154,57 @@ 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). ### 1.7.27 (build 21282, api 8, 2023-08-30) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d8c74cc1..6aacc6ad 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -43,6 +43,10 @@ ### Era0S - Bug Fixer - Modder +- Added a feature ### VinniTR - Fixes + +### Rikko +- Created the original "reject_recently_left_players" plugin diff --git a/ballisticakit-cmake/CMakeLists.txt b/ballisticakit-cmake/CMakeLists.txt index 297cfe0b..12934e4e 100644 --- a/ballisticakit-cmake/CMakeLists.txt +++ b/ballisticakit-cmake/CMakeLists.txt @@ -360,6 +360,8 @@ set(BALLISTICA_SOURCES ${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 @@ -436,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 @@ -458,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 diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj index affcd239..ff64f440 100644 --- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj @@ -352,6 +352,8 @@ + + @@ -428,16 +430,19 @@ + + + + + - - @@ -450,6 +455,8 @@ + + diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters index b3e5ebdf..441851e6 100644 --- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters @@ -490,6 +490,12 @@ ballistica\base\graphics\support + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + ballistica\base\graphics\text @@ -718,6 +724,12 @@ ballistica\base\support + + ballistica\base\support + + + ballistica\base\support + ballistica\base\support @@ -727,6 +739,9 @@ ballistica\base\support + + ballistica\base\support + ballistica\base\support @@ -736,18 +751,18 @@ ballistica\base\support + + ballistica\base\support + + + ballistica\base\support + ballistica\base\support ballistica\base\support - - ballistica\base\support - - - ballistica\base\support - ballistica\base\ui @@ -784,6 +799,12 @@ ballistica\classic\python\methods + + ballistica\classic\support + + + ballistica\classic\support + ballistica\classic\support diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj index 82d010b3..c3b0779c 100644 --- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj @@ -347,6 +347,8 @@ + + @@ -423,16 +425,19 @@ + + + + + - - @@ -445,6 +450,8 @@ + + diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters index b3e5ebdf..441851e6 100644 --- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters @@ -490,6 +490,12 @@ ballistica\base\graphics\support + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + ballistica\base\graphics\text @@ -718,6 +724,12 @@ ballistica\base\support + + ballistica\base\support + + + ballistica\base\support + ballistica\base\support @@ -727,6 +739,9 @@ ballistica\base\support + + ballistica\base\support + ballistica\base\support @@ -736,18 +751,18 @@ ballistica\base\support + + ballistica\base\support + + + ballistica\base\support + ballistica\base\support ballistica\base\support - - ballistica\base\support - - - ballistica\base\support - ballistica\base\ui @@ -784,6 +799,12 @@ ballistica\classic\python\methods + + ballistica\classic\support + + + ballistica\classic\support + ballistica\classic\support diff --git a/config/projectconfig.json b/config/projectconfig.json index 6dbb5ae0..68dd6d90 100644 --- a/config/projectconfig.json +++ b/config/projectconfig.json @@ -10,7 +10,8 @@ "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", diff --git a/config/spinoffconfig.py b/config/spinoffconfig.py index 490697b0..1cbbc0f8 100644 --- a/config/spinoffconfig.py +++ b/config/spinoffconfig.py @@ -103,6 +103,7 @@ ctx.src_unchecked_paths = { '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/xcuserdata', 'ballisticakit-android/BallisticaKit/src/*/res/*/*.png', 'ballisticakit-android/BallisticaKit/src/*/assets/ballistica_files', 'ballisticakit-android/local.properties', @@ -274,6 +275,7 @@ ctx.filter_file_extensions = { '.frag', '.vert', '.xcsettings', + '.xcstrings', '.filters', } diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json index 4d72b271..55998cf7 100644 --- a/src/assets/.asset_manifest_public.json +++ b/src/assets/.asset_manifest_public.json @@ -366,7 +366,6 @@ "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", @@ -418,7 +417,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", diff --git a/src/assets/Makefile b/src/assets/Makefile index 2a6a7c45..d2bfbc27 100644 --- a/src/assets/Makefile +++ b/src/assets/Makefile @@ -341,7 +341,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 \ @@ -616,7 +615,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 \ diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py index 9eb09af0..3b0c723a 100644 --- a/src/assets/ba_data/python/babase/__init__.py +++ b/src/assets/ba_data/python/babase/__init__.py @@ -64,7 +64,6 @@ from _babase import ( 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, @@ -75,6 +74,8 @@ from _babase import ( music_player_set_volume, music_player_shutdown, music_player_stop, + native_review_request, + native_review_request_supported, native_stack_trace, print_load_info, pushcall, @@ -85,7 +86,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, @@ -266,7 +266,6 @@ __all__ = [ 'lock_all_input', 'LoginAdapter', 'Lstr', - 'mac_music_app_get_library_source', 'mac_music_app_get_playlists', 'mac_music_app_get_volume', 'mac_music_app_init', @@ -279,6 +278,8 @@ __all__ = [ 'music_player_set_volume', 'music_player_shutdown', 'music_player_stop', + 'native_review_request', + 'native_review_request_supported', 'native_stack_trace', 'NodeNotFoundError', 'normalized_color', @@ -303,7 +304,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..d1baa52d 100644 --- a/src/assets/ba_data/python/babase/_accountv2.py +++ b/src/assets/ba_data/python/babase/_accountv2.py @@ -31,6 +31,8 @@ class AccountV2Subsystem: """ 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) @@ -259,6 +253,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 +268,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( @@ -419,14 +421,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, str] 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 ec0e0d8b..b014d677 100644 --- a/src/assets/ba_data/python/babase/_app.py +++ b/src/assets/ba_data/python/babase/_app.py @@ -184,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 @@ -433,7 +432,7 @@ class App: """(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.""" @@ -583,19 +582,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. @@ -915,6 +901,12 @@ class App: _babase.lifecyclelog('fade-and-shutdown-graphics begin') _babase.fade_screen(False, time=0.15) await asyncio.sleep(0.15) + + # 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: 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/_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/_hooks.py b/src/assets/ba_data/python/babase/_hooks.py index 8ea73a5e..535d3084 100644 --- a/src/assets/ba_data/python/babase/_hooks.py +++ b/src/assets/ba_data/python/babase/_hooks.py @@ -33,6 +33,23 @@ def reset_to_main_menu() -> None: logging.warning('reset_to_main_menu: no-op due to classic not present.') +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 @@ -123,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 @@ -312,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, diff --git a/src/assets/ba_data/python/babase/_login.py b/src/assets/ba_data/python/babase/_login.py index f6c761c4..63cbd450 100644 --- a/src/assets/ba_data/python/babase/_login.py +++ b/src/assets/ba_data/python/babase/_login.py @@ -149,7 +149,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 +161,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 +229,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 +240,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 +268,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 +281,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 @@ -340,8 +338,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 +373,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/baclassic/_benchmark.py b/src/assets/ba_data/python/baclassic/_benchmark.py index 18e04335..6bb22743 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 @@ -84,7 +85,8 @@ def run_stress_test( 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 +136,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..6d60b50a 100644 --- a/src/assets/ba_data/python/baclassic/_input.py +++ b/src/assets/ba_data/python/baclassic/_input.py @@ -40,7 +40,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': 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/_subsystem.py b/src/assets/ba_data/python/baclassic/_subsystem.py index 757661fc..f8aba3bc 100644 --- a/src/assets/ba_data/python/baclassic/_subsystem.py +++ b/src/assets/ba_data/python/baclassic/_subsystem.py @@ -627,15 +627,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, 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 0d19c817..c784b9a5 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -52,7 +52,7 @@ if TYPE_CHECKING: # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 21491 +TARGET_BALLISTICA_BUILD = 21583 TARGET_BALLISTICA_VERSION = '1.7.28' diff --git a/src/assets/ba_data/python/baplus/_subsystem.py b/src/assets/ba_data/python/baplus/_subsystem.py index 00d4f0bf..97a9cdb5 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 diff --git a/src/assets/ba_data/python/bascenev1/_session.py b/src/assets/ba_data/python/bascenev1/_session.py index ffef80cd..843db5d3 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,11 @@ if TYPE_CHECKING: import bascenev1 +# How long 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. +REJOIN_COOLDOWN = 10 + class Session: """Defines a high level series of bascenev1.Activity-es. @@ -203,6 +209,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 +264,29 @@ 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(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 +304,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( + 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 +814,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/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/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py index 535d6719..9ff875a9 100644 --- a/src/assets/ba_data/python/bauiv1/__init__.py +++ b/src/assets/ba_data/python/bauiv1/__init__.py @@ -66,6 +66,8 @@ from babase import ( lock_all_input, LoginAdapter, Lstr, + native_review_request, + native_review_request_supported, NotFoundError, Permission, Plugin, @@ -191,6 +193,8 @@ __all__ = [ 'LoginAdapter', 'Lstr', 'Mesh', + 'native_review_request', + 'native_review_request_supported', 'NotFoundError', 'open_file_externally', 'open_url', diff --git a/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py index 30a168c3..7dc42b0e 100644 --- a/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py +++ b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py @@ -51,6 +51,18 @@ 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='', + 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), diff --git a/src/assets/ba_data/python/bauiv1lib/account/settings.py b/src/assets/ba_data/python/bauiv1lib/account/settings.py index 4cea92da..52d9a7e9 100644 --- a/src/assets/ba_data/python/bauiv1lib/account/settings.py +++ b/src/assets/ba_data/python/bauiv1lib/account/settings.py @@ -64,12 +64,13 @@ class AccountSettingsWindow(bui.Window): ) # 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' + # 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 @@ -98,6 +99,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 +231,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() @@ -242,9 +248,53 @@ class AccountSettingsWindow(bui.Window): False if gpgs_adapter is None else gpgs_adapter.is_back_end_active() ) + # Ditto for Game Center. + game_center_adapter = plus.accounts.login_adapters.get( + LoginType.GAME_CENTER + ) + is_game_center = ( + False + if game_center_adapter is None + else game_center_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: + primary_account = bui.app.plus.accounts.primary + if primary_account is not None: + # Show Google Play Games account name if the current account + # has such a login attached. + lname = primary_account.logins.get(LoginType.GPGS) + if lname is not None: + icontxt = bui.charstr( + bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO + ) + via_lines.append(f'{icontxt}{lname}') + + # Show Game Center account name if the current account + # has such a login attached. + lname = primary_account.logins.get(LoginType.GAME_CENTER) + if lname is not None: + icontxt = bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO) + via_lines.append(f'{icontxt}{lname}') + + # 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 +308,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 @@ -281,11 +336,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 is_game_center achievements_button_space = 60.0 show_achievements_text = ( @@ -349,10 +402,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: @@ -466,6 +522,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.6, + 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.6, + 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 +581,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 +588,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 +633,13 @@ class AccountSettingsWindow(bui.Window): ( '${B}', bui.Lstr( - resource=self._r + '.signInWithGooglePlayText' + resource=self._r + '.signInWithText', + subs=[ + ( + '${SERVICE}', + bui.Lstr(resource='googlePlayText'), + ) + ], ), ), ], @@ -572,6 +657,45 @@ 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), + 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 @@ -748,7 +872,11 @@ class AccountSettingsWindow(bui.Window): 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') + # Update: Apparently Game Center is just called 'Game Center' + # in all languages. Can revisit if not true. + # https://developer.apple.com/forums/thread/725779 + v1_account_type_name = bui.Lstr(value='Game Center') + # v1_account_type_name = bui.Lstr(resource='gameCenterText') else: raise ValueError( "unknown account type: '" + str(v1_account_type) + "'" @@ -1469,8 +1597,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. - logging.warning('Got error in v2 sign-in result: %s.', result) - 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 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/gather/privatetab.py b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py index 1cf4e43a..952b862c 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py @@ -458,9 +458,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 +962,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..dc2b2dc3 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py @@ -361,6 +361,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 @@ -658,6 +659,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 +963,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 +979,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 +989,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/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py index 505484f1..cee57b66 100644 --- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py +++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py @@ -1350,7 +1350,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/settings/advanced.py b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py index 930ecaa3..e7724815 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py @@ -807,6 +807,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 +872,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: diff --git a/src/assets/ba_data/python/bauiv1lib/settings/audio.py b/src/assets/ba_data/python/bauiv1lib/settings/audio.py index bfe71e40..643e23fa 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 diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py index c9780d15..3c67b23f 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,7 +789,7 @@ class GamepadSettingsWindow(bui.Window): ), ) - bui.apptimer(0, doit) + bui.pushcall(doit) return btn def _cancel(self) -> None: 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/keyboard.py b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py index 6f37aabb..60aab600 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, diff --git a/src/assets/ba_data/python/bauiv1lib/specialoffer.py b/src/assets/ba_data/python/bauiv1lib/specialoffer.py index 7fe7729c..af6621a8 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,7 +547,11 @@ 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: + feedback.ask_for_rating() else: SpecialOfferWindow(app.classic.special_offer) diff --git a/src/assets/ba_data/python/bauiv1lib/store/browser.py b/src/assets/ba_data/python/bauiv1lib/store/browser.py index 269a8164..03af613a 100644 --- a/src/assets/ba_data/python/bauiv1lib/store/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/store/browser.py @@ -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() diff --git a/src/ballistica/base/app_adapter/app_adapter.cc b/src/ballistica/base/app_adapter/app_adapter.cc index a7f1cf89..5f0a8b59 100644 --- a/src/ballistica/base/app_adapter/app_adapter.cc +++ b/src/ballistica/base/app_adapter/app_adapter.cc @@ -2,64 +2,17 @@ #include "ballistica/base/app_adapter/app_adapter.h" -#if BA_OSTYPE_ANDROID // Remove conditional once android sources are public. -#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/python/base_python.h" #include "ballistica/base/support/app_config.h" -#include "ballistica/base/support/stress_test.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; @@ -67,31 +20,8 @@ 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()); } @@ -315,4 +245,76 @@ auto AppAdapter::GetGraphicsClientContext() -> GraphicsClientContext* { return new GraphicsClientContext(); } +auto AppAdapter::GetKeyRepeatDelay() -> float { return 0.3f; } +auto AppAdapter::GetKeyRepeatInterval() -> float { return 0.08f; } + +auto AppAdapter::ClipboardIsSupported() -> bool { + // We only call our actual virtual function once. + if (!have_clipboard_is_supported_) { + clipboard_is_supported_ = DoClipboardIsSupported(); + have_clipboard_is_supported_ = true; + } + return clipboard_is_supported_; +} + +auto AppAdapter::ClipboardHasText() -> bool { + // If subplatform says they don't support clipboards, don't even ask. + if (!ClipboardIsSupported()) { + return false; + } + return DoClipboardHasText(); +} + +void AppAdapter::ClipboardSetText(const std::string& text) { + // If subplatform says they don't support clipboards, this is an error. + if (!ClipboardIsSupported()) { + throw Exception("ClipboardSetText called with no clipboard support.", + PyExcType::kRuntime); + } + DoClipboardSetText(text); +} + +auto AppAdapter::ClipboardGetText() -> std::string { + // If subplatform says they don't support clipboards, this is an error. + if (!ClipboardIsSupported()) { + throw Exception("ClipboardGetText called with no clipboard support.", + PyExcType::kRuntime); + } + return DoClipboardGetText(); +} + +auto AppAdapter::DoClipboardIsSupported() -> bool { return false; } + +auto AppAdapter::DoClipboardHasText() -> bool { + // 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."); } + } // namespace ballistica::base diff --git a/src/ballistica/base/app_adapter/app_adapter.h b/src/ballistica/base/app_adapter/app_adapter.h index 25fb83de..7ae86045 100644 --- a/src/ballistica/base/app_adapter/app_adapter.h +++ b/src/ballistica/base/app_adapter/app_adapter.h @@ -15,8 +15,7 @@ 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(); @@ -189,6 +188,13 @@ class AppAdapter { /// 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 @@ -197,9 +203,24 @@ class AppAdapter { /// settings coming in. virtual void ApplyGraphicsSettings(const GraphicsSettings* settings); - protected: - AppAdapter(); - virtual ~AppAdapter(); + virtual auto GetKeyRepeatDelay() -> float; + virtual auto GetKeyRepeatInterval() -> float; + + /// Return whether clipboard operations are supported at all. This gets + /// called when determining whether to display clipboard related UI + /// elements/etc. + auto ClipboardIsSupported() -> bool; + + /// Return whether there is currently text on the clipboard. + auto ClipboardHasText() -> bool; + + /// Set current clipboard text. Raises an Exception if clipboard is + /// unsupported. + void ClipboardSetText(const std::string& text); + + /// Return current text from the clipboard. Raises an Exception if + /// clipboard is unsupported or if there's no text on the clipboard. + auto ClipboardGetText() -> std::string; /// Push a raw pointer Runnable to the platform's 'main' thread. The main /// thread should call its RunAndLogErrors() method and then delete it. @@ -209,10 +230,34 @@ 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(); + + protected: + virtual ~AppAdapter(); + + virtual auto DoClipboardIsSupported() -> bool; + virtual auto DoClipboardHasText() -> bool; + virtual void DoClipboardSetText(const std::string& text); + virtual auto DoClipboardGetText() -> std::string; + + /// Override to implement native review requests. Will be called in the + /// main thread. + virtual void DoNativeReviewRequest(); + private: void OnAppSuspend_(); void OnAppUnsuspend_(); - bool app_suspended_{}; + + bool app_suspended_ : 1 {}; + bool have_clipboard_is_supported_ : 1 {}; + bool clipboard_is_supported_ : 1 {}; }; } // 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 aaefa65e..885f8092 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 @@ -41,7 +48,17 @@ auto AppAdapterApple::ManagesMainThreadEventLoop() const -> bool { void AppAdapterApple::DoPushMainThreadRunnable(Runnable* runnable) { // Kick this along to swift. - BallisticaKit::FromCppPushRawRunnableToMain(runnable); + BallisticaKit::FromCpp::pushRawRunnableToMain(runnable); +} + +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::DoApplyAppConfig() { assert(g_base->InLogicThread()); } @@ -122,16 +139,16 @@ auto AppAdapterApple::TryRender() -> bool { // 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) { - if (((std::abs(resize_target_resolution_.x + 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)) - && g_core->GetAppTimeSeconds() - start_time < 0.1 && result) { + > 0.01f)); + if (size_differs && g_core->GetAppTimeSeconds() - start_time < 0.1 + && result) { result = g_base->graphics_server->TryRender(); - } else { - break; } } } @@ -182,13 +199,13 @@ void AppAdapterApple::SetHardwareCursorVisible(bool visible) { assert(g_core->InMainThread()); #if BA_OSTYPE_MACOS - BallisticaKit::CocoaFromCppSetCursorVisible(visible); + BallisticaKit::CocoaFromCpp::setCursorVisible(visible); #endif } void AppAdapterApple::TerminateApp() { #if BA_OSTYPE_MACOS - BallisticaKit::CocoaFromCppTerminateApp(); + BallisticaKit::CocoaFromCpp::terminateApp(); #else AppAdapter::TerminateApp(); #endif @@ -205,7 +222,7 @@ auto AppAdapterApple::FullscreenControlAvailable() const -> bool { auto AppAdapterApple::FullscreenControlGet() const -> bool { #if BA_OSTYPE_MACOS - return BallisticaKit::CocoaFromCppGetMainWindowIsFullscreen(); + return BallisticaKit::CocoaFromCpp::getMainWindowIsFullscreen(); #else return false; #endif @@ -213,7 +230,7 @@ auto AppAdapterApple::FullscreenControlGet() const -> bool { void AppAdapterApple::FullscreenControlSet(bool fullscreen) { #if BA_OSTYPE_MACOS - return BallisticaKit::CocoaFromCppSetMainWindowFullscreen(fullscreen); + return BallisticaKit::CocoaFromCpp::setMainWindowFullscreen(fullscreen); #endif } @@ -224,6 +241,79 @@ auto AppAdapterApple::FullscreenControlKeyShortcut() const 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 9217299b..036bf820 100644 --- a/src/ballistica/base/app_adapter/app_adapter_apple.h +++ b/src/ballistica/base/app_adapter/app_adapter_apple.h @@ -26,6 +26,8 @@ class AppAdapterApple : public AppAdapter { return val; } + void OnMainThreadStartApp() override; + auto ManagesMainThreadEventLoop() const -> bool override; void DoApplyAppConfig() override; @@ -41,6 +43,11 @@ class AppAdapterApple : public AppAdapter { 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; void DoPushGraphicsContextRunnable(Runnable* runnable) override; @@ -50,6 +57,11 @@ class AppAdapterApple : public AppAdapter { 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: class ScopedAllowGraphics_; diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.cc b/src/ballistica/base/app_adapter/app_adapter_sdl.cc index b43f8397..f5d4ceb4 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}; @@ -79,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. @@ -415,7 +416,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; } @@ -805,23 +808,9 @@ void AppAdapterSDL::CursorPositionForDraw(float* x, float* y) { auto AppAdapterSDL::FullscreenControlAvailable() const -> bool { return true; } auto AppAdapterSDL::FullscreenControlKeyShortcut() const -> std::optional { - if (g_buildconfig.ostype_windows()) { - // On Windows 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"; - } - if (g_buildconfig.ostype_macos()) { - // The Mac+SDL situation is a bit of a mess. By default, there is 'Enter - // Full Screen' in the window menu which is mapped to fn-F, but that - // will only work if a window was created in SDL as windowed. If we - // fullscreen that window and restart the app, we'll then have a *real* - // fullscreen sdl window and that shortcut won't work anymore. So to - // keep things consistent we advertise ctrl-f which we always handle - // ourselves. Maybe this situation will be cleaned up in SDL 3, but its - // not a huge deal anyway since our Cocoa Mac version behaves cleanly. - return "Ctrl+F"; - } - return {}; + // 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; } @@ -832,6 +821,32 @@ auto AppAdapterSDL::HasDirectKeyboardInput() -> bool { 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 585c29f1..0219ae07 100644 --- a/src/ballistica/base/app_adapter/app_adapter_sdl.h +++ b/src/ballistica/base/app_adapter/app_adapter_sdl.h @@ -45,6 +45,8 @@ class AppAdapterSDL : public AppAdapter { auto GetGraphicsSettings() -> GraphicsSettings* override; + auto GetKeyName(int keycode) -> std::string override; + protected: void DoPushMainThreadRunnable(Runnable* runnable) override; void RunMainThreadEventLoopToCompletion() override; @@ -52,6 +54,10 @@ 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_; diff --git a/src/ballistica/base/assets/assets_server.cc b/src/ballistica/base/assets/assets_server.cc index 0fddc71f..aaf27ba5 100644 --- a/src/ballistica/base/assets/assets_server.cc +++ b/src/ballistica/base/assets/assets_server.cc @@ -5,7 +5,6 @@ #include "ballistica/base/assets/asset.h" #include "ballistica/base/assets/assets.h" #include "ballistica/base/graphics/graphics.h" -#include "ballistica/base/graphics/graphics_server.h" #include "ballistica/base/support/huffman.h" #include "ballistica/shared/foundation/event_loop.h" @@ -26,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) { @@ -259,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 aa757cb4..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; @@ -157,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; diff --git a/src/ballistica/base/audio/audio_server.cc b/src/ballistica/base/audio/audio_server.cc index 1e873026..a7a9ad1a 100644 --- a/src/ballistica/base/audio/audio_server.cc +++ b/src/ballistica/base/audio/audio_server.cc @@ -32,9 +32,9 @@ LPALCDEVICEPAUSESOFT alcDevicePauseSOFT; LPALCDEVICERESUMESOFT alcDeviceResumeSOFT; #endif -const int kAudioProcessIntervalNormal{500}; -const int kAudioProcessIntervalFade{50}; -const int kAudioProcessIntervalPendingLoad{1}; +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{}; @@ -161,7 +161,7 @@ void AudioServer::OnAppStartInThread_() { // 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 diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc index b7e4abb3..6a7a18cd 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()); @@ -465,8 +502,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) { @@ -721,4 +759,8 @@ void BaseFeatureSet::QuitApp(bool confirm, QuitType quit_type) { } } +void BaseFeatureSet::PushMainThreadRunnable(Runnable* runnable) { + app_adapter->DoPushMainThreadRunnable(runnable); +} + } // namespace ballistica::base diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h index 473e5edf..27243583 100644 --- a/src/ballistica/base/base.h +++ b/src/ballistica/base/base.h @@ -53,6 +53,7 @@ class ClassicSoftInterface; class CollisionMeshAsset; class CollisionCache; class DevConsole; +class DisplayTimer; class Context; class ContextRef; class DataAsset; @@ -99,13 +100,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; @@ -630,6 +632,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. @@ -721,6 +730,12 @@ 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; + // Const subsystems. AppAdapter* const app_adapter; AppConfig* const app_config; @@ -750,8 +765,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_; } @@ -774,7 +787,6 @@ class BaseFeatureSet : public FeatureSetNativeComponent, AppMode* app_mode_; PlusSoftInterface* plus_soft_{}; ClassicSoftInterface* classic_soft_{}; - StressTest* stress_test_; std::mutex shutdown_suppress_lock_; bool shutdown_suppress_disallowed_{}; diff --git a/src/ballistica/base/graphics/gl/renderer_gl.cc b/src/ballistica/base/graphics/gl/renderer_gl.cc index adb60eae..c24db341 100644 --- a/src/ballistica/base/graphics/gl/renderer_gl.cc +++ b/src/ballistica/base/graphics/gl/renderer_gl.cc @@ -185,9 +185,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 @@ -453,44 +455,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_; } diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc index cc01e49f..7fe3f3e9 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,7 +78,7 @@ auto Graphics::IsShaderTransparent(ShadingType c) -> bool { } } -Graphics::Graphics() = default; +Graphics::Graphics() : screenmessages{new ScreenMessages()} {} Graphics::~Graphics() = default; void Graphics::OnAppStart() { assert(g_base->InLogicThread()); } @@ -182,7 +167,7 @@ void Graphics::UpdateInitialGraphicsSettingsSend_() { 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); } @@ -231,11 +216,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") { @@ -359,40 +342,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}; - millisecs_t smooth_time{}; - - 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(); @@ -410,7 +359,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]; @@ -472,7 +420,7 @@ void Graphics::DrawMiscOverlays(FrameDef* frame_def) { { auto xf = c.ScopedTransform(); c.Translate(14.0f + (show_fps_ ? 30.0f : 0.0f), 0.1f, - kScreenMessageZDepth); + kScreenTextZDepth); c.Scale(0.7f, 0.7f); c.DrawMesh(ping_text_group_->GetElementMesh(e)); } @@ -500,7 +448,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)); } @@ -528,382 +476,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(); - - // This logic needs to run at a fixed hz or it breaks on high frame - // rates. - auto now_millisecs = pass->frame_def()->display_time_millisecs(); - i->smooth_time = std::max(i->smooth_time, now_millisecs - 100); - while (i->smooth_time < now_millisecs) { - i->smooth_time += 1000 / 60; - 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; - } - - // This logic needs to run at a fixed hz or it breaks on high frame - // rates. - auto now_millisecs = pass->frame_def()->display_time_millisecs(); - i->smooth_time = std::max(i->smooth_time, now_millisecs - 100); - while (i->smooth_time < now_millisecs) { - i->smooth_time += 1000 / 60; - 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) @@ -949,32 +522,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; @@ -984,9 +531,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) { @@ -1058,7 +603,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()) { @@ -1186,7 +731,7 @@ void Graphics::DrawDevUI(FrameDef* frame_def) { void Graphics::BuildAndPushFrameDef() { assert(g_base->InLogicThread()); - BA_PRECONDITION_FATAL(g_base->logic->app_bootstrapping_complete()); + assert(g_base->logic->app_bootstrapping_complete()); assert(camera_.Exists()); assert(!g_core->HeadlessMode()); @@ -1611,16 +1156,6 @@ void Graphics::DrawBlotches(FrameDef* frame_def) { } } -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); @@ -1951,26 +1486,6 @@ 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::CalcVirtualRes_(float* x, float* y) { @@ -2024,15 +1539,6 @@ void Graphics::SetScreenResolution(float x, float y) { UpdateInitialGraphicsSettingsSend_(); } -void Graphics::ScreenMessageEntry::UpdateTranslation() { - if (translation_dirty) { - s_translated = g_base->assets->CompileResourceString( - s_raw, "Graphics::ScreenMessageEntry::UpdateTranslation"); - translation_dirty = false; - mesh_dirty = true; - } -} - auto Graphics::CubeMapFromReflectionType(ReflectionType reflection_type) -> SysCubeMapTextureID { switch (reflection_type) { @@ -2112,8 +1618,8 @@ 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, @@ -2136,6 +1642,7 @@ auto Graphics::GraphicsQualityFromRequest(GraphicsQualityRequest request, return GraphicsQuality::kLow; } } + auto Graphics::TextureQualityFromRequest(TextureQualityRequest request, TextureQuality auto_val) -> TextureQuality { diff --git a/src/ballistica/base/graphics/graphics.h b/src/ballistica/base/graphics/graphics.h index f04ff79c..115f0adc 100644 --- a/src/ballistica/base/graphics/graphics.h +++ b/src/ballistica/base/graphics/graphics.h @@ -53,8 +53,7 @@ const float kCursorZDepth{0.9f}; // Client class for graphics operations (used from the logic thread). class Graphics { public: - /// Instantiate the Graphics subclass for the current build. - static auto Create() -> Graphics*; + Graphics(); void OnAppStart(); void OnAppPause(); @@ -119,8 +118,6 @@ class Graphics { return res_y_virtual_; } - void ClearScreenMessageTranslations(); - // Given a point in space, returns the shadow density that should be drawn // into the shadow pass. Does this belong somewhere else? auto GetShadowDensity(float x, float y, float z) -> float; @@ -128,21 +125,13 @@ class Graphics { static void GetSafeColor(float* r, float* g, float* b, float target_intensity = 0.6f); - // Print a message to the on-screen list. - void AddScreenMessage(const std::string& msg, - const Vector3f& color = {1, 1, 1}, bool top = false, - TextureAsset* texture = nullptr, - TextureAsset* tint_texture = nullptr, - const Vector3f& tint = {1, 1, 1}, - const Vector3f& tint2 = {1, 1, 1}); - // Fade the local screen in or out over the given time period. void FadeScreen(bool to, millisecs_t time, PyObject* endcall); static void DrawRadialMeter(MeshIndexedSimpleFull* m, float amt); - // Ways to add a few simple component types quickly. - // (uses particle rendering for efficient batches). + // Ways to add a few simple component types quickly (uses particle + // rendering for efficient batches). void DrawBlotch(const Vector3f& pos, float size, float r, float g, float b, float a) { DoDrawBlotch(&blotch_indices_, &blotch_verts_, pos, size, r, g, b, a); @@ -240,13 +229,8 @@ class Graphics { float upper_top); void ReleaseFadeEndCommand(); - // auto tv_border() const { - // assert(g_base->InLogicThread()); - // return tv_border_; - // } - - // Nodes that draw flat stuff into the overlay pass should query this z value - // for where to draw in z. + // Nodes that draw flat stuff into the overlay pass should query this z + // value for where to draw in z. auto overlay_node_z_depth() { fetched_overlay_node_z_depth_ = true; return overlay_node_z_depth_; @@ -296,8 +280,8 @@ class Graphics { void AddMeshDataCreate(MeshData* d); void AddMeshDataDestroy(MeshData* d); - // For debugging: ensures that only transparent or opaque components - // are submitted while enabled. + // For debugging: ensures that only transparent or opaque components are + // submitted while enabled. auto drawing_transparent_only() const { return drawing_transparent_only_; } void set_drawing_transparent_only(bool val) { drawing_transparent_only_ = val; @@ -362,8 +346,8 @@ class Graphics { } /// For temporary use in arbitrary threads. This should be removed when - /// possible and replaced with proper safe thread-specific access - /// patterns (so we can support switching renderers/etc.). + /// possible and replaced with proper safe thread-specific access patterns + /// (so we can support switching renderers/etc.). auto placeholder_client_context() const -> const GraphicsClientContext* { // Using this from arbitrary threads is currently ok currently since // context never changes once set. Will need to kill this call once that @@ -372,10 +356,9 @@ class Graphics { return client_context_snapshot_.Get()->Get(); } - protected: - class ScreenMessageEntry; + ScreenMessages* const screenmessages; - Graphics(); + protected: virtual ~Graphics(); virtual void DoDrawFade(FrameDef* frame_def, float amt); static void CalcVirtualRes_(float* x, float* y); @@ -453,8 +436,6 @@ class Graphics { std::map> debug_graphs_; std::mutex frame_def_delete_list_mutex_; std::list> clean_frame_commands_; - std::list screen_messages_; - std::list screen_messages_top_; std::vector recycle_frame_defs_; std::vector blotch_indices_; std::vector blotch_verts_; @@ -473,23 +454,22 @@ class Graphics { float gyro_mag_test_{}; float overlay_node_z_depth_{}; float progress_bar_progress_{}; - float screen_gamma_{1.0f}; float shadow_lower_bottom_{-4.0f}; float shadow_lower_top_{4.0f}; float shadow_upper_bottom_{30.0f}; float shadow_upper_top_{40.0f}; + seconds_t last_cursor_visibility_event_time_{}; millisecs_t fade_start_{}; millisecs_t fade_time_{}; millisecs_t next_stat_update_time_{}; millisecs_t progress_bar_end_time_{-9999}; millisecs_t last_progress_bar_draw_time_{}; millisecs_t last_progress_bar_start_time_{}; - microsecs_t last_suppress_gyro_time_{}; - seconds_t last_cursor_visibility_event_time_{}; - microsecs_t next_frame_number_filtered_increment_time_{}; - microsecs_t last_create_frame_def_time_microsecs_{}; millisecs_t last_create_frame_def_time_millisecs_{}; millisecs_t last_jitter_update_time_{}; + microsecs_t last_suppress_gyro_time_{}; + microsecs_t next_frame_number_filtered_increment_time_{}; + microsecs_t last_create_frame_def_time_microsecs_{}; Object::Ref screen_mesh_; Object::Ref progress_bar_bottom_mesh_; Object::Ref progress_bar_top_mesh_; diff --git a/src/ballistica/base/graphics/graphics_server.cc b/src/ballistica/base/graphics/graphics_server.cc index 98351f47..949efced 100644 --- a/src/ballistica/base/graphics/graphics_server.cc +++ b/src/ballistica/base/graphics/graphics_server.cc @@ -137,14 +137,10 @@ auto GraphicsServer::TryRender() -> bool { auto GraphicsServer::WaitForRenderFrameDef_() -> FrameDef* { assert(g_base->app_adapter->InGraphicsContext()); - millisecs_t app_time = g_core->GetAppTimeMillisecs(); + millisecs_t start_time = g_core->GetAppTimeMillisecs(); - if (!renderer_) { - return nullptr; - } - - // If the app is paused, never render. - if (g_base->app_adapter->app_suspended()) { + // Don't bother waiting if we can't/shouldn't render anyway. + if (!renderer_ || shutting_down_ || g_base->app_adapter->app_suspended()) { return nullptr; } @@ -173,12 +169,12 @@ auto GraphicsServer::WaitForRenderFrameDef_() -> FrameDef* { // if we've been waiting for too long, give up. On some platforms such // as Android, this frame will still get flipped whether we draw in it // or not, so we *really* want to not skip drawing if we can help it. - millisecs_t t = g_core->GetAppTimeMillisecs() - app_time; + millisecs_t t = g_core->GetAppTimeMillisecs() - start_time; if (t >= 1000) { if (g_buildconfig.debug_build()) { Log(LogLevel::kWarning, - "GraphicsServer: aborting GetRenderFrameDef after " - + std::to_string(t) + "ms."); + "GraphicsServer: timed out at " + std::to_string(t) + + "ms waiting for logic thread to send us a FrameDef."); } break; // Fail. } @@ -336,38 +332,6 @@ void GraphicsServer::LoadRenderer() { texture_quality_ = Graphics::TextureQualityFromRequest( texture_quality_requested_, renderer_->GetAutoTextureQuality()); - // If we don't support high quality graphics, make sure we're no higher than - // medium. - // BA_PRECONDITION(g_base->graphics->has_supports_high_quality_graphics_value()); - // if (!g_base->graphics->supports_high_quality_graphics() - // && graphics_quality_ > GraphicsQuality::kMedium) { - // graphics_quality_ = GraphicsQuality::kMedium; - // } - // graphics_quality_set_ = true; - - // Update texture quality based on request. - // switch (texture_quality_requested_) { - // case TextureQualityRequest::kLow: - // texture_quality_ = TextureQuality::kLow; - // break; - // case TextureQualityRequest::kMedium: - // texture_quality_ = TextureQuality::kMedium; - // break; - // case TextureQualityRequest::kHigh: - // texture_quality_ = TextureQuality::kHigh; - // break; - // case TextureQualityRequest::kAuto: - // texture_quality_ = renderer_->GetAutoTextureQuality(); - // break; - // default: - // Log(LogLevel::kError, - // "Unhandled TextureQualityRequest value: " - // + - // std::to_string(static_cast(texture_quality_requested_))); - // texture_quality_ = TextureQuality::kLow; - // } - // texture_quality_set_ = true; - // Ok we've got our qualities figured out; now load/update the renderer. renderer_->Load(); @@ -657,4 +621,15 @@ auto GraphicsServer::InGraphicsContext_() const -> bool { return g_base->app_adapter->InGraphicsContext(); } +void GraphicsServer::Shutdown() { + BA_PRECONDITION(!shutting_down_); + BA_PRECONDITION(g_base->InGraphicsContext()); + shutting_down_ = true; + + // We don't actually do anything here currently; just take note + // that we're shutting down so we no longer wait for frames to come + // in from the main thread. + shutdown_completed_ = true; +} + } // namespace ballistica::base diff --git a/src/ballistica/base/graphics/graphics_server.h b/src/ballistica/base/graphics/graphics_server.h index ed2cbd5b..dc3df09e 100644 --- a/src/ballistica/base/graphics/graphics_server.h +++ b/src/ballistica/base/graphics/graphics_server.h @@ -231,14 +231,6 @@ class GraphicsServer { return tv_border_; } - // auto graphics_quality_set() const { - // return graphics_quality_ != GraphicsQuality::kUnset; - // } - - // auto texture_quality_set() const { - // return texture_quality_ != TextureQuality::kUnset; - // } - auto SupportsTextureCompressionType(TextureCompressionType t) const -> bool { assert(InGraphicsContext_()); assert(texture_compression_types_set_); @@ -249,10 +241,6 @@ class GraphicsServer { void SetTextureCompressionTypes( const std::list& types); - // auto texture_compression_types_are_set() const { - // return texture_compression_types_set_; - // } - void set_renderer_context_lost(bool lost) { renderer_context_lost_ = lost; } auto renderer_context_lost() const { return renderer_context_lost_; } @@ -289,6 +277,11 @@ class GraphicsServer { return texture_compression_types_; } + /// Start spinning down the graphics server/etc. + void Shutdown(); + + auto shutdown_completed() const { return shutdown_completed_; } + private: /// Pass a freshly allocated GraphicsContext instance, which the graphics /// system will take ownership of. @@ -324,6 +317,10 @@ class GraphicsServer { } } + TextureQualityRequest texture_quality_requested_{}; + TextureQuality texture_quality_{}; + GraphicsQualityRequest graphics_quality_requested_{}; + GraphicsQuality graphics_quality_{}; bool renderer_loaded_ : 1 {}; bool model_view_projection_matrix_dirty_ : 1 {true}; bool model_world_matrix_dirty_ : 1 {true}; @@ -331,11 +328,9 @@ class GraphicsServer { bool renderer_context_lost_ : 1 {}; bool texture_compression_types_set_ : 1 {}; bool cam_orient_matrix_dirty_ : 1 {true}; + bool shutting_down_ : 1 {}; + bool shutdown_completed_ : 1 {}; Snapshot* client_context_{}; - TextureQualityRequest texture_quality_requested_{}; - TextureQuality texture_quality_{}; - GraphicsQualityRequest graphics_quality_requested_{}; - GraphicsQuality graphics_quality_{}; float res_x_{}; float res_y_{}; float res_x_virtual_{}; diff --git a/src/ballistica/base/graphics/mesh/nine_patch_mesh.h b/src/ballistica/base/graphics/mesh/nine_patch_mesh.h index fad02593..64b6638d 100644 --- a/src/ballistica/base/graphics/mesh/nine_patch_mesh.h +++ b/src/ballistica/base/graphics/mesh/nine_patch_mesh.h @@ -18,7 +18,7 @@ class NinePatchMesh : public MeshIndexedSimpleFull { /// Calculate a border value for a NinePatchMesh based on dimensions and a /// desired max corner radius. For calculating left or right borders, - /// matching_dimension should be width and other_dimension should be + /// `matching_dimension` should be width and `other_dimension` should be /// height. For top or bottom borders it is the opposite. static auto BorderForRadius(float corner_radius, float matching_dimension, float other_dimension) -> float { diff --git a/src/ballistica/base/graphics/mesh/text_mesh.cc b/src/ballistica/base/graphics/mesh/text_mesh.cc index f2c7e378..6b735175 100644 --- a/src/ballistica/base/graphics/mesh/text_mesh.cc +++ b/src/ballistica/base/graphics/mesh/text_mesh.cc @@ -360,7 +360,7 @@ void TextMesh::SetText(const std::string& text_in, HAlign alignment_h, // compile it and add its final spans to our mesh. if (packer) { std::vector spans; - packer->compile(); + packer->Compile(); // DEBUGGING - add a single quad above our first // span showing the entire texture for debugging purposes diff --git a/src/ballistica/base/graphics/support/camera.cc b/src/ballistica/base/graphics/support/camera.cc index d12ad130..050057b1 100644 --- a/src/ballistica/base/graphics/support/camera.cc +++ b/src/ballistica/base/graphics/support/camera.cc @@ -23,8 +23,7 @@ const float kPanMax = 9.0f; const float kPanMin = -9.0f; Camera::Camera() - : last_mode_set_time_(g_core->GetAppTimeMillisecs()), - lock_panning_(g_core->IsVRMode()), + : lock_panning_(g_core->IsVRMode()), pan_speed_scale_(g_core->IsVRMode() ? 0.3f : 1.0f) {} Camera::~Camera() = default; @@ -607,7 +606,8 @@ void Camera::Update(millisecs_t elapsed) { float damping = 0.006f; float damping2 = 0.006f; float xy_blend_speed = 0.0002f; - millisecs_t real_time = g_core->GetAppTimeMillisecs(); + time_ += elapsed; + // millisecs_t real_time = g_core->GetAppTimeMillisecs(); // Prevent camera "explosions" if we've been unable to update for a while. elapsed = std::min(millisecs_t{100}, elapsed); @@ -653,18 +653,18 @@ void Camera::Update(millisecs_t elapsed) { } if (!g_core->IsVRMode()) { - smooth_speed_.x += elapsedf * rand_component - * (-0.5f - + Utils::precalc_rand_1((real_time / rand_incr_1) - % kPrecalcRandsCount)); - smooth_speed_.y += elapsedf * rand_component - * (-0.5f - + Utils::precalc_rand_2((real_time / rand_incr_2) - % kPrecalcRandsCount)); - smooth_speed_.z += elapsedf * rand_component - * (-0.5f - + Utils::precalc_rand_3((real_time / rand_incr_3) - % kPrecalcRandsCount)); + smooth_speed_.x += + elapsedf * rand_component + * (-0.5f + + Utils::precalc_rand_1((time_ / rand_incr_1) % kPrecalcRandsCount)); + smooth_speed_.y += + elapsedf * rand_component + * (-0.5f + + Utils::precalc_rand_2((time_ / rand_incr_2) % kPrecalcRandsCount)); + smooth_speed_.z += + elapsedf * rand_component + * (-0.5f + + Utils::precalc_rand_3((time_ / rand_incr_3) % kPrecalcRandsCount)); } if (RandomFloat() < 0.1f && !g_core->IsVRMode()) { @@ -714,15 +714,14 @@ void Camera::Update(millisecs_t elapsed) { shake_vel_.x += 0.05f * shake_amount_ * (0.5f - - Utils::precalc_rand_1(real_time % 122 * i % kPrecalcRandsCount)); + - Utils::precalc_rand_1(time_ % 122 * i % kPrecalcRandsCount)); shake_vel_.y += 0.05f * shake_amount_ * (0.5f - - Utils::precalc_rand_2(real_time % 323 * i % kPrecalcRandsCount)); + - Utils::precalc_rand_2(time_ % 323 * i % kPrecalcRandsCount)); shake_vel_.z += 0.05f * shake_amount_ - * (0.5f - - Utils::precalc_rand_3(real_time % 76 * i % kPrecalcRandsCount)); + * (0.5f - Utils::precalc_rand_3(time_ % 76 * i % kPrecalcRandsCount)); } for (int j = 0; j < iterations; j++) { @@ -740,8 +739,8 @@ void Camera::Update(millisecs_t elapsed) { uint32_t interval = g_core->IsVRMode() ? 50 : 100; // Occasionally, update microphone position for audio. - if (real_time - last_listener_update_time_ > interval) { - last_listener_update_time_ = real_time; + if (time_ - last_listener_update_time_ > interval) { + last_listener_update_time_ = time_; bool do_regular_update = true; if (g_core->IsVRMode()) { #if BA_VR_MODE @@ -891,7 +890,8 @@ void Camera::SetMode(CameraMode m) { if (mode_ != m) { mode_ = m; smooth_next_frame_ = false; - last_mode_set_time_ = g_core->GetAppTimeMillisecs(); + // last_mode_set_time_ = g_core->GetAppTimeMillisecs(); + // last_mode_set_time_ = time_; heading_ = kInitialHeading; } } diff --git a/src/ballistica/base/graphics/support/camera.h b/src/ballistica/base/graphics/support/camera.h index 7b13bb4c..f6fe6504 100644 --- a/src/ballistica/base/graphics/support/camera.h +++ b/src/ballistica/base/graphics/support/camera.h @@ -92,37 +92,51 @@ class Camera : public Object { void set_pan_speed_scale(float val) { pan_speed_scale_ = val; } private: - float pan_speed_scale_{1.0f}; - bool lock_panning_{}; - Vector3f vr_offset_{0.0f, 0.0f, 0.0f}; - Vector3f vr_extra_offset_{0.0f, 0.0f, 0.0f}; - Vector3f vr_offset_smooth_{0.0f, 0.0f, 0.0f}; - millisecs_t last_mode_set_time_{}; - std::list areas_of_interest_; CameraMode mode_{CameraMode::kFollow}; - bool manual_{}; - bool smooth_next_frame_{}; - bool have_real_areas_of_interest_{}; + bool manual_ : 1 {}; + bool smooth_next_frame_ : 1 {}; + bool have_real_areas_of_interest_ : 1 {}; + bool lock_panning_ : 1 {}; // Manual stuff. - bool panning_{}; - bool orbiting_{}; - bool rolling_{}; - bool trucking_{}; - bool alt_down_{}; - bool cmd_down_{}; - bool ctrl_down_{}; - bool mouse_left_down_{}; - bool mouse_middle_down_{}; - bool mouse_right_down_{}; + bool panning_ : 1 {}; + bool orbiting_ : 1 {}; + bool rolling_ : 1 {}; + bool trucking_ : 1 {}; + bool alt_down_ : 1 {}; + bool cmd_down_ : 1 {}; + bool ctrl_down_ : 1 {}; + bool mouse_left_down_ : 1 {}; + bool mouse_middle_down_ : 1 {}; + bool mouse_right_down_ : 1 {}; + + bool happy_thoughts_mode_ : 1 {}; + bool x_constrained_ : 1 {true}; + float pan_speed_scale_{1.0f}; float heading_{kInitialHeading}; - Vector3f extra_pos_{0.0f, 0.0f, 0.0f}; - Vector3f extra_pos_2_{0.0f, 0.0f, 0.0f}; float area_of_interest_bounds_[6]{-9999, -9999, -9999, 9999, 9999, 9999}; float pan_pos_{}; float pan_speed_{}; float pan_target_{}; float shake_amount_{}; + float target_radius_{2.0f}; + float target_radius_smoothed_{2.0f}; + float field_of_view_x_{5.0f}; + float field_of_view_y_{kCameraFOVY}; + float field_of_view_x_smoothed_{1.0f}; + float field_of_view_y_smoothed_{1.0f}; + float min_target_radius_{5.0f}; + float area_of_interest_near_{1.0f}; + float area_of_interest_far_{2.0f}; + float xy_constrain_blend_{0.5f}; + // millisecs_t last_mode_set_time_{}; + millisecs_t last_listener_update_time_{}; + millisecs_t time_{}; + Vector3f vr_offset_{0.0f, 0.0f, 0.0f}; + Vector3f vr_extra_offset_{0.0f, 0.0f, 0.0f}; + Vector3f vr_offset_smooth_{0.0f, 0.0f, 0.0f}; + Vector3f extra_pos_{0.0f, 0.0f, 0.0f}; + Vector3f extra_pos_2_{0.0f, 0.0f, 0.0f}; Vector3f shake_pos_{0.0f, 0.0f, 0.0f}; Vector3f shake_vel_{0.0f, 0.0f, 0.0f}; Vector3f position_{0.0f, 1.0f, -1.0f}; @@ -131,20 +145,8 @@ class Camera : public Object { Vector3f position_offset_smoothed_{0.0f, 0.0f, 0.0f}; Vector3f smooth_speed_{0.0f, 0.0f, 0.0f}; Vector3f smooth_speed_2_{0.0f, 0.0f, 0.0f}; - float target_radius_{2.0f}; - float target_radius_smoothed_{2.0f}; - float field_of_view_x_{5.0f}; - float field_of_view_y_{kCameraFOVY}; - float field_of_view_x_smoothed_{1.0f}; - float field_of_view_y_smoothed_{1.0f}; - millisecs_t last_listener_update_time_{}; - bool happy_thoughts_mode_{}; - float min_target_radius_{5.0f}; Vector3f up_{0.0f, 1.0f, 0.0f}; - float area_of_interest_near_{1.0f}; - float area_of_interest_far_{2.0f}; - bool x_constrained_{true}; - float xy_constrain_blend_{0.5f}; + std::list areas_of_interest_; std::vector area_of_interest_points_{{0.0f, 0.0f, 0.0f}}; }; diff --git a/src/ballistica/base/graphics/support/graphics_settings.cc b/src/ballistica/base/graphics/support/graphics_settings.cc index a132fe88..fd2a5ed0 100644 --- a/src/ballistica/base/graphics/support/graphics_settings.cc +++ b/src/ballistica/base/graphics/support/graphics_settings.cc @@ -6,7 +6,6 @@ #include "ballistica/base/graphics/graphics.h" #include "ballistica/base/support/app_config.h" -#include "ballistica/shared/foundation/object.h" namespace ballistica::base { diff --git a/src/ballistica/base/graphics/support/screen_messages.cc b/src/ballistica/base/graphics/support/screen_messages.cc new file mode 100644 index 00000000..4dcebfd0 --- /dev/null +++ b/src/ballistica/base/graphics/support/screen_messages.cc @@ -0,0 +1,538 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/graphics/support/screen_messages.h" + +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/mesh/nine_patch_mesh.h" +#include "ballistica/base/graphics/text/text_graphics.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/shared/generic/utils.h" + +namespace ballistica::base { + +const float kScreenMessageZDepth{-0.06f}; + +class ScreenMessages::ScreenMessageEntry { + public: + ScreenMessageEntry(std::string text, bool top_style, uint32_t c, + const Vector3f& color, TextureAsset* texture, + TextureAsset* tint_texture, const Vector3f& tint, + const Vector3f& tint2) + : top_style(top_style), + creation_time(c), + s_raw(std::move(text)), + color(color), + texture(texture), + tint_texture(tint_texture), + tint(tint), + tint2(tint2) {} + auto GetText() -> TextGroup&; + void UpdateTranslation(); + bool top_style; + uint32_t creation_time; + Vector3f color; + Vector3f tint; + Vector3f tint2; + std::string s_raw; + std::string s_translated; + float str_width{}; + float str_height{}; + Object::Ref texture; + Object::Ref tint_texture; + float v_smoothed{}; + bool translation_dirty{true}; + bool mesh_dirty{true}; + millisecs_t smooth_time{}; + Object::Ref shadow_mesh_; + + private: + Object::Ref s_mesh_; +}; + +ScreenMessages::ScreenMessages() = default; + +void ScreenMessages::DrawMiscOverlays(FrameDef* frame_def) { + RenderPass* pass = frame_def->overlay_pass(); + + // 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)); + g_base->assets->SysTexture(SysTextureID::kShadowSharp)); + + float screen_width = g_base->graphics->screen_virtual_width(); + + float 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(); + + // Don't actually need the text just yet but need shadow mesh + // which is calculated as part of it. + i->GetText(); + + 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.7f; + + // 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 = i->str_height; + float str_width = i->str_width; + + 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; + Graphics::GetSafeColor(&r, &g, &b); + + float v_extra = scale * (static_cast(youngest_age) * 0.01f); + + float fade; + if (age < 100) { + fade = 1.0f; + } else { + // Don't fade ALL the way to black; leaves a tiny bit of color + // showing which looks nice. + fade = std::max(0.07f, (200.0f - static_cast(age)) / 100.0f); + } + c.SetColor(r * fade, g * fade, b * fade, a); + + { + auto xf = c.ScopedTransform(); + + // This logic needs to run at a fixed hz or it breaks on high frame + // rates. + auto now_millisecs = pass->frame_def()->display_time_millisecs(); + i->smooth_time = std::max(i->smooth_time, now_millisecs - 100); + while (i->smooth_time < now_millisecs) { + i->smooth_time += 1000 / 60; + 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); + // c.Scale(scale * s_extra, scale * s_extra); + + // // Align our bottom with where we just scaled from. + // c.Translate(0, 0.5f, 0); + { + // c.Scale((str_width + 110) * scale * s_extra, + // (str_height + 40) * scale * s_extra); + c.Scale(scale * s_extra, scale * s_extra); + c.Translate(0, 20); + + // Align our bottom with where we just scaled from. + c.Translate(0, 0.5f, 0); + } + // c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); + assert(i->shadow_mesh_.Exists()); + c.DrawMesh(i->shadow_mesh_.Get()); + } + + 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(); + float 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 = i->str_height; + float str_width = i->str_width; + + 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; + Graphics::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; + float 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; + } + + // This logic needs to run at a fixed hz or it breaks on high frame + // rates. + auto now_millisecs = pass->frame_def()->display_time_millisecs(); + i->smooth_time = std::max(i->smooth_time, now_millisecs - 100); + while (i->smooth_time < now_millisecs) { + i->smooth_time += 1000 / 60; + 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; + Graphics::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(); + } + } +} + +void ScreenMessages::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 ScreenMessages::Reset() { + // 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(); +} + +void ScreenMessages::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; + } +} + +auto ScreenMessages::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, + top_style ? TextMesh::HAlign::kLeft : TextMesh::HAlign::kCenter, + TextMesh::VAlign::kBottom); + + str_width = g_base->text_graphics->GetStringWidth(s_translated.c_str()); + str_height = g_base->text_graphics->GetStringHeight(s_translated.c_str()); + + if (!top_style) { + float x_extend = 40.0f; + float y_extend = 40.0f; + float y_offset = -5.0f; + float corner_radius = 60.0f; + float width_fin = str_width + x_extend * 2.0f; + float height_fin = str_height + y_extend * 2.0f; + float x_border = + NinePatchMesh::BorderForRadius(corner_radius, width_fin, height_fin); + float y_border = + NinePatchMesh::BorderForRadius(corner_radius, height_fin, width_fin); + shadow_mesh_ = Object::New( + -0.5f * width_fin, -y_extend + y_offset, 0.0f, width_fin, height_fin, + x_border, y_border, x_border, y_border); + } + + mesh_dirty = false; + } + return *s_mesh_; +} + +void ScreenMessages::ScreenMessageEntry::UpdateTranslation() { + if (translation_dirty) { + s_translated = g_base->assets->CompileResourceString( + s_raw, "Graphics::ScreenMessageEntry::UpdateTranslation"); + translation_dirty = false; + mesh_dirty = true; + } +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/graphics/support/screen_messages.h b/src/ballistica/base/graphics/support/screen_messages.h new file mode 100644 index 00000000..91652149 --- /dev/null +++ b/src/ballistica/base/graphics/support/screen_messages.h @@ -0,0 +1,37 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_SUPPORT_SCREEN_MESSAGES_H_ +#define BALLISTICA_BASE_GRAPHICS_SUPPORT_SCREEN_MESSAGES_H_ + +#include "ballistica/base/base.h" +#include "ballistica/shared/math/vector3f.h" + +namespace ballistica::base { + +/// Wrangles a set of screen-messages. +class ScreenMessages { + public: + ScreenMessages(); + + void ClearScreenMessageTranslations(); + + // Print a message to the on-screen list. + void AddScreenMessage(const std::string& msg, + const Vector3f& color = {1, 1, 1}, bool top = false, + TextureAsset* texture = nullptr, + TextureAsset* tint_texture = nullptr, + const Vector3f& tint = {1, 1, 1}, + const Vector3f& tint2 = {1, 1, 1}); + + void DrawMiscOverlays(FrameDef* frame_def); + void Reset(); + + private: + class ScreenMessageEntry; + std::list screen_messages_; + std::list screen_messages_top_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_SUPPORT_SCREEN_MESSAGES_H_ diff --git a/src/ballistica/base/graphics/text/font_page_map_data.h b/src/ballistica/base/graphics/text/font_page_map_data.h index 10ff01c3..60ea0aa8 100644 --- a/src/ballistica/base/graphics/text/font_page_map_data.h +++ b/src/ballistica/base/graphics/text/font_page_map_data.h @@ -12,25 +12,25 @@ #include "ballistica/base/graphics/text/text_graphics.h" namespace ballistica::base { -// the total number of glyph pages we have +// The total number of glyph pages we have. #define BA_GLYPH_PAGE_COUNT 8 -// the total number of glyphs we have +// The total number of glyphs we have. const int kGlyphCount = 1280; -// the starting glyph index for each page +// The starting glyph index for each page. uint32_t g_glyph_page_start_index_map[8] = {0, 258, 416, 546, 698, 981, 1138, 1276}; -// the number of glyphs on each page +// The number of glyphs on each page. uint32_t g_glyph_page_glyph_counts[8] = {258, 158, 130, 152, 283, 157, 138, 4}; -// our dynamically-loaded glyph structs for each page +// Our dynamically-loaded glyph structs for each page. TextGraphics::Glyph* g_glyph_pages[8] = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; -// the page index for each glyph -uint16_t g_glyph_map[kGlyphCount] = { +// The page index for each glyph. +uint8_t g_glyph_map[kGlyphCount] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, diff --git a/src/ballistica/base/graphics/text/text_graphics.cc b/src/ballistica/base/graphics/text/text_graphics.cc index 5a5cd8de..fae81787 100644 --- a/src/ballistica/base/graphics/text/text_graphics.cc +++ b/src/ballistica/base/graphics/text/text_graphics.cc @@ -116,8 +116,7 @@ TextGraphics::TextGraphics() { } } - // init glyph values for our big font page - // (a 8x8 array) + // Init glyph values for our big font page (a 8x8 array). { float x_offs = 0.009f; float y_offs = 0.0059f; @@ -284,7 +283,7 @@ TextGraphics::TextGraphics() { (1.0f / 8.0f) * static_cast(x + 1) + x_offs + scale_extra; g.tex_max_y = (1.0f / 8.0f) * static_cast(y) + y_offs; - // just scooted letters over.. account for that + // Just scooted letters over; account for that. float foo_x = 0.0183f; float foo_y = 0.000f; g.tex_min_x += foo_x; @@ -292,12 +291,12 @@ TextGraphics::TextGraphics() { g.tex_min_y += foo_y; g.tex_max_y += foo_y; - // clamp based on char width + // Clamp based on char width. float scale = w * 1.32f; g.x_size *= scale; g.tex_max_x = g.tex_min_x + (g.tex_max_x - g.tex_min_x) * scale; - // add bot offset + // Add bot offset. if (bot_offset != 0.0f) { g.tex_min_y = g.tex_max_y + (g.tex_min_y - g.tex_max_y) @@ -305,7 +304,7 @@ TextGraphics::TextGraphics() { g.pen_offset_y -= bot_offset; g.y_size += bot_offset; } - // add left offset + // Add left offset. if (left_offset != 0.0f) { g.tex_min_x = g.tex_max_x + (g.tex_min_x - g.tex_max_x) @@ -313,14 +312,14 @@ TextGraphics::TextGraphics() { g.pen_offset_x -= left_offset; g.x_size += left_offset; } - // add right offset + // Add right offset. if (right_offset != 0.0f) { g.tex_max_x = g.tex_min_x + (g.tex_max_x - g.tex_min_x) * ((g.x_size + right_offset) / g.x_size); g.x_size += right_offset; } - // add top offset + // Add top offset. if (top_offset != 0.0f) { g.tex_max_y = g.tex_min_y + (g.tex_max_y - g.tex_min_y) @@ -844,13 +843,13 @@ void TextGraphics::GetFontPageCharRange(int page, uint32_t* first_char, // Our special pages: switch (page) { case static_cast(FontPage::kOSRendered): { - // we allow the OS to render anything not in one of our glyph textures - // (technically this overlaps the private-use range which we use our own - // textures for, but that's handled as a special-case by - // TextGroup::setText + // We allow the OS to render anything not in one of our glyph textures + // (technically this overlaps the private-use range which we use our + // own textures for, but that's handled as a special-case by + // TextGroup::SetText. (*first_char) = kGlyphCount; - (*last_char) = kTextMaxUnicodeVal; // hmm what's the max unicode value we - // should ever see?.. + // hmm what's the max unicode value we should ever see?.. + (*last_char) = kTextMaxUnicodeVal; break; } case static_cast(FontPage::kExtras1): { @@ -887,52 +886,57 @@ void TextGraphics::GetFontPagesForText(const std::string& text, int last_page = -1; std::vector unicode = Utils::UnicodeFromUTF8(text, "c03853"); for (uint32_t val : unicode) { - int page; + int page{-1}; - // Hack: allow showing euro even if we don't support unicode font rendering. - if (g_buildconfig.enable_os_font_rendering()) { - if (val == 8364) { - val = 0xE000; - } - } + // Hack: allow showing euro even if we don't support unicode font + // rendering. + // if (g_buildconfig.enable_os_font_rendering()) { + // if (val == 8364) { + // val = 0xE000; + // } + // } - // For values in the custom-char range (U+E000–U+F8FF) we point at our own - // custom page(s) + bool covered{}; + + // For values in the custom-char range (U+E000–U+F8FF) we point at our + // own custom page(s) if (val >= 0xE000 && val <= 0xF8FF) { // The 25 chars after this are in our fontExtras sheet. if (val < 0xE000 + 25) { // Special value denoting our custom font page. page = static_cast(FontPage::kExtras1); + covered = true; } else if (val < 0xE000 + 50) { // Special value denoting our custom font page. page = static_cast(FontPage::kExtras2); + covered = true; } else if (val < 0xE000 + 75) { // Special value denoting our custom font page. page = static_cast(FontPage::kExtras3); + covered = true; } else if (val < 0xE000 + 100) { // Special value denoting our custom font page. page = static_cast(FontPage::kExtras4); - } else { - // We dont cover this.. just go with '?' - val = '?'; - page = g_glyph_map[val]; + covered = true; } - } else if (val >= kGlyphCount) { - // Otherwise if its outside of our texture-coverage area. + } else if (val < kGlyphCount) { + page = g_glyph_map[val]; + covered = true; + } + + if (!covered) { if (g_buildconfig.enable_os_font_rendering()) { page = static_cast(FontPage::kOSRendered); } else { val = '?'; page = g_glyph_map[val]; } - } else { - // yay we cover it! - page = g_glyph_map[val]; } - // compare to lastPage to avoid doing a set insert for *everything* since - // most will be the same + + // Compare to last_page to avoid doing a set insert for *everything* + // since most will be the same. if (page != last_page) { - (*font_pages).insert(page); + font_pages->insert(page); last_page = page; } } @@ -1009,12 +1013,8 @@ void TextGraphics::GetOSTextSpanBoundsAndWidth(const std::string& s, Rect* r, // Send this entry to the back of the list since we used it. text_span_bounds_cache_.erase(entry->list_iterator_); - // I guess inspection doesn't realize entry lives on after this?... -#pragma clang diagnostic push -#pragma ide diagnostic ignored "UnusedValue" entry->list_iterator_ = text_span_bounds_cache_.insert(text_span_bounds_cache_.end(), entry); -#pragma clang diagnostic pop return; } auto entry(Object::New()); @@ -1068,7 +1068,9 @@ auto TextGraphics::GetStringWidth(const char* text, bool big) -> float { line_length += GetOSTextSpanWidth(s); os_span.clear(); } - if (line_length > max_line_length) max_line_length = line_length; + if (line_length > max_line_length) { + max_line_length = line_length; + } line_length = 0; t++; } else { @@ -1145,7 +1147,9 @@ void TextGraphics::BreakUpString(const char* text, float width, s_begin = t; } } else { - if (*t == 0) throw Exception(); + if (*t == 0) { + throw Exception(); + } uint32_t val = Utils::GetUTF8Value(t); Utils::AdvanceUTF8(&t); diff --git a/src/ballistica/base/graphics/text/text_packer.cc b/src/ballistica/base/graphics/text/text_packer.cc index 2f9ea250..fffd74b1 100644 --- a/src/ballistica/base/graphics/text/text_packer.cc +++ b/src/ballistica/base/graphics/text/text_packer.cc @@ -24,11 +24,11 @@ void TextPacker::AddSpan(const std::string& text, float x, float y, } // FIXME - we currently run into minor problems because we measure our text -// bounds at one size and then scale that linearly when trying to fit things -// into the texture. However, fonts don't always scale linearly (and even when -// that's an option it can be expensive). +// bounds at one size and then scale that linearly when trying to fit +// things into the texture. However, fonts don't always scale linearly (and +// even when that's an option it can be expensive). -void TextPacker::compile() { +void TextPacker::Compile() { assert(!compiled_); if (spans_.empty()) { compiled_ = true; @@ -57,24 +57,23 @@ void TextPacker::compile() { width *= 2; } - // Alternately, if we're too big, crank our scale down so that our widest span - // fits. + // Alternately, if we're too big, crank our scale down so that our widest + // span fits. if (widest_unscaled_span_width * scale > width * 0.9f) { scale *= ((width * 0.9f) / (widest_unscaled_span_width * scale)); } float start_height = height; int mini_shrink_tries = 0; - // Ok; we've now locked in a width and scale. - // Now we go through and position our spans. - // We may need to do this more than once if our height comes out too big. - // (hopefully this will never be a problem in practice) + // Ok; we've now locked in a width and scale. Now we go through and + // position our spans. We may need to do this more than once if our height + // comes out too big. (hopefully this will never be a problem in practice) while (true) { height = start_height; - // We currently just lay out left-to-right, top-to-bottom. - // This could be somewhat wasteful in particular configurations. - // (leaving half-filled lines, etc) so it might be worth improving later. + // We currently just lay out left-to-right, top-to-bottom. This could be + // somewhat wasteful in particular configurations. (leaving half-filled + // lines, etc) so it might be worth improving later. float widest_fill_right = 0.0f; float fill_right = 0.0f; float fill_bottom = 0.0f; @@ -87,26 +86,27 @@ void TextPacker::compile() { // Start a new line if this would put us past the end. if (fill_right + span_width > width) { if (fill_right > widest_fill_right) { - widest_fill_right = fill_right; // Keep track of how far over we go. + // Keep track of how far over we go. + widest_fill_right = fill_right; } fill_right = 0.0f; fill_bottom += line_height; line_height = 0.0f; } - // Position x such that x + left bound - buffer lines up with our current - // right point. + // Position x such that x + left bound - buffer lines up with our + // current right point. float to_left = (i.bounds.l - span_buffer) * scale; i.tex_x = fill_right - to_left; fill_right += span_width; - // Position y such that y - top bound - buffer lines up with our current - // bottom point. + // Position y such that y - top bound - buffer lines up with our + // current bottom point. float to_top = (-i.bounds.t - span_buffer) * scale; i.tex_y = fill_bottom - to_top; - // If our total height is greater than the current line height, expand the - // line's. + // If our total height is greater than the current line height, expand + // the line's. if (span_height > line_height) { line_height = span_height; } @@ -125,9 +125,9 @@ void TextPacker::compile() { // If it doesn't fit, repeat again with a smaller scale until it does. // Dropping our scale has a disproportional effect on the final height - // (since it opens up more relative horizontal space). - // I'm not sure how to figure out how much to drop by other than - // incrementally dropping values until we fit. + // (since it opens up more relative horizontal space). I'm not sure + // how to figure out how much to drop by other than incrementally + // dropping values until we fit. scale *= 0.75f; } else if (((widest_fill_right < (width * mini_shrink_threshold_h) @@ -135,15 +135,15 @@ void TextPacker::compile() { || fill_bottom + line_height < (height * mini_shrink_threshold_v)) && mini_shrink_tries < 3) { - // If we're here it means we *barely* use more than half of the texture in - // one direction or the other; let's shrink just a tiny bit and we should - // be able to chop our texture size in half + // If we're here it means we *barely* use more than half of the + // texture in one direction or the other; let's shrink just a tiny bit + // and we should be able to chop our texture size in half if (widest_fill_right < width * mini_shrink_threshold_h && width > 16) { float scale_val = 0.99f * (((width * 0.5f) / widest_fill_right)); if (scale_val < 1.0f) { - // FIXME - should think about a fixed multiplier here; - // under the hood the system might be caching glyphs based on scale - // and this would leave us with fewer different scales in the end and + // FIXME - should think about a fixed multiplier here; under the + // hood the system might be caching glyphs based on scale and + // this would leave us with fewer different scales in the end and // thus better caching performance scale *= scale_val; } @@ -151,9 +151,9 @@ void TextPacker::compile() { } else { float scale_val = 0.99f * (height * 0.5f) / (fill_bottom + line_height); if (scale_val < 1.0f) { - // FIXME - should think about a fixed multiplier here; - // under the hood the system might be caching glyphs based on scale - // and this would leave us with fewer different scales in the end and + // FIXME - should think about a fixed multiplier here; under the + // hood the system might be caching glyphs based on scale and + // this would leave us with fewer different scales in the end and // thus better caching performance scale *= scale_val; } @@ -165,8 +165,8 @@ void TextPacker::compile() { } } - // Lastly, now that our texture width and height are completely finalized, we - // can calculate UVs. + // Lastly, now that our texture width and height are completely finalized, + // we can calculate UVs. for (auto&& i : spans_) { // Now store uv coords for this span; they should include the buffer. i.u_min = (i.tex_x + (i.bounds.l - span_buffer) * scale) / width; @@ -182,11 +182,11 @@ void TextPacker::compile() { } // TODO(ericf): now we calculate a hash that's unique to this text - // configuration; we'll use that as a key for the texture we'll generate/use. - // ..this way multiple meshes can share the same generated texture. - // *technically* we could calculate this hash and check for an existing - // texture before we bother laying out our spans, but that might not save us - // much time and would complicate things. + // configuration; we'll use that as a key for the texture we'll + // generate/use. ..this way multiple meshes can share the same generated + // texture. *technically* we could calculate this hash and check for an + // existing texture before we bother laying out our spans, but that might + // not save us much time and would complicate things. hash_ = std::to_string(resolution_scale_); for (auto&& i : spans_) { char buffer[64]; diff --git a/src/ballistica/base/graphics/text/text_packer.h b/src/ballistica/base/graphics/text/text_packer.h index d1889ba8..e2c40310 100644 --- a/src/ballistica/base/graphics/text/text_packer.h +++ b/src/ballistica/base/graphics/text/text_packer.h @@ -21,7 +21,7 @@ class TextPacker : public Object { // outside of here anyway so might as well recycle. void AddSpan(const std::string& text, float x, float y, const Rect& bounds); - auto hash() const -> const std::string& { + const auto& hash() const { assert(compiled_); return hash_; } @@ -51,32 +51,32 @@ class TextPacker : public Object { // Once done adding spans, call this to calculate final span UV values, // texture configuration, and hash. - void compile(); + void Compile(); - auto spans() const -> const std::list& { return spans_; } + const auto& spans() const { return spans_; } - auto texture_width() const -> int { + auto texture_width() const { assert(compiled_); return texture_width_; } - auto texture_height() const -> int { + auto texture_height() const { assert(compiled_); return texture_height_; } - auto text_scale() const -> float { + auto text_scale() const { assert(compiled_); return text_scale_; } private: + bool compiled_{false}; float resolution_scale_; + float text_scale_{}; int texture_width_{}; int texture_height_{}; - float text_scale_{}; std::string hash_; - bool compiled_{false}; std::list spans_; }; diff --git a/src/ballistica/base/input/device/joystick_input.cc b/src/ballistica/base/input/device/joystick_input.cc index 7d6df160..be7e4ab4 100644 --- a/src/ballistica/base/input/device/joystick_input.cc +++ b/src/ballistica/base/input/device/joystick_input.cc @@ -9,6 +9,7 @@ #include "ballistica/base/input/input.h" #include "ballistica/base/python/base_python.h" #include "ballistica/base/support/classic_soft.h" +#include "ballistica/base/support/repeater.h" #include "ballistica/base/ui/ui.h" #include "ballistica/core/core.h" #include "ballistica/shared/foundation/event_loop.h" @@ -17,10 +18,6 @@ namespace ballistica::base { -const char* kMFiControllerName = "iOS/Mac Controller"; - -const int kJoystickRepeatDelay{500}; - // Joy values below this are candidates for calibration. const float kJoystickCalibrationThreshold{6000.0f}; @@ -68,7 +65,12 @@ JoystickInput::JoystickInput(int sdl_joystick_id, // that instead. // #if BA_SDL2_BUILD sdl_joystick_id_ = SDL_JoystickInstanceID(sdl_joystick_); - raw_sdl_joystick_name_ = SDL_JoystickName(sdl_joystick_); + if (auto* name = SDL_JoystickName(sdl_joystick_)) { + raw_sdl_joystick_name_ = name; + } else { + // This can return nullptr if SDL can't find a name. + raw_sdl_joystick_name_ = "Unknown Controller"; + } // Special case: on windows, xinput stuff comes in with unique names // "XInput Controller #3", etc. Let's replace these with simply "XInput @@ -78,20 +80,6 @@ JoystickInput::JoystickInput(int sdl_joystick_id, && raw_sdl_joystick_name_.size() <= 22) { raw_sdl_joystick_name_ = "XInput Controller"; } - // #else - // raw_sdl_joystick_name_ = SDL_JoystickName(sdl_joystick_id_); - // #endif // BA_SDL2_BUILD - - // If its an SDL joystick and we're using our custom sdl 1.2 build, ask it. - // #if BA_XCODE_BUILD && BA_OSTYPE_MACOS && !BA_SDL2_BUILD - // raw_sdl_joystick_identifier_ = - // SDL_JoystickIdentifier(sdl_joystick_id_); - // #endif - - // Some special-cases on mac. - if (strstr(raw_sdl_joystick_name_.c_str(), "PLAYSTATION") != nullptr) { - is_mac_ps3_controller_ = true; - } #else // BA_ENABLE_SDL_JOYSTICKS throw Exception(); // Shouldn't happen. @@ -101,8 +89,6 @@ JoystickInput::JoystickInput(int sdl_joystick_id, // Its a manual joystick. sdl_joystick_ = nullptr; - is_mfi_controller_ = (custom_device_name_ == kMFiControllerName); - // Hard code a few remote controls. // The newer way to do this is just set 'UI-Only' on the device config is_remote_control_ = ((custom_device_name_ == "Amazon Remote") @@ -162,7 +148,17 @@ auto JoystickInput::HasMeaningfulButtonNames() -> bool { return g_buildconfig.ostype_android(); } +void JoystickInput::SetButtonName(int button, const std::string& name) { + button_names_[button] = name; +} + auto JoystickInput::GetButtonName(int index) -> std::string { + // First check any explicit ones we were passed. + auto i = button_names_.find(index); + if (i != button_names_.end()) { + return i->second; + } + // FIXME: Should get fancier here now that PS4 and XBone // controllers are supported through this. if (is_mfi_controller_) { @@ -179,6 +175,7 @@ auto JoystickInput::GetButtonName(int index) -> std::string { break; } } + if (g_buildconfig.ostype_android()) { // Special case: if this is a samsung controller, return the dice // button icons. @@ -446,46 +443,6 @@ void JoystickInput::Update() { } } } - - // If a button's being held, potentially pass repeats along. - if (up_held_ || down_held_ || left_held_ || right_held_) { - // Don't ask for the widget unless we have something held. - // (otherwise we prevent other inputs from getting at it) - if (g_base->ui->GetWidgetForInput(this)) { - millisecs_t repeat_delay = kJoystickRepeatDelay; - - millisecs_t t = g_core->GetAppTimeMillisecs(); - auto c = WidgetMessage::Type::kEmptyMessage; - if (t - last_hold_time_ < repeat_delay) { - return; - } - - if (t - last_hold_time_ >= repeat_delay) { - bool pass = false; - if (up_held_) { - pass = true; - c = WidgetMessage::Type::kMoveUp; - } else if (down_held_) { - pass = true; - c = WidgetMessage::Type::kMoveDown; - } else if (left_held_) { - pass = true; - c = WidgetMessage::Type::kMoveLeft; - } else if (right_held_) { - pass = true; - c = WidgetMessage::Type::kMoveRight; - } - if (pass) { - g_base->ui->SendWidgetMessage(WidgetMessage(c)); - } - - // Set another repeat to happen sooner. - last_hold_time_ = - t - - static_cast(static_cast(repeat_delay) * 0.8f); - } - } - } } void JoystickInput::SetStandardExtendedButtons() { @@ -508,6 +465,8 @@ void JoystickInput::ResetHeldStates() { SDL_Event e; dpad_right_held_ = dpad_left_held_ = dpad_up_held_ = dpad_down_held_ = false; + ui_repeater_.Clear(); + run_buttons_held_.clear(); run_trigger1_value_ = run_trigger2_value_ = 0.0f; UpdateRunningState(); @@ -594,28 +553,28 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { || dpad_down_held_)) return; - bool isHoldPositionEvent = false; + bool is_hold_position_event = false; // Keep track of whether hold-position is being held. If so, we don't send - // window events. (some joysticks always give us significant axis values but + // window events (some joysticks always give us significant axis values but // rely on hold position to keep from doing stuff usually). if (e->type == SDL_JOYBUTTONDOWN && e->jbutton.button == hold_position_button_) { need_to_send_held_state_ = true; hold_position_held_ = true; - isHoldPositionEvent = true; + is_hold_position_event = true; } if (e->type == SDL_JOYBUTTONUP && e->jbutton.button == hold_position_button_) { need_to_send_held_state_ = true; hold_position_held_ = false; - isHoldPositionEvent = true; + is_hold_position_event = true; } // Let's ignore events for just a moment after we're created. // (some joysticks seem to spit out erroneous button-pressed events when // first plugged in ). - if (time - creation_time_ < 250 && !isHoldPositionEvent) { + if (time - creation_time_ < 250 && !is_hold_position_event) { return; } @@ -700,7 +659,7 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { } } - // If its the ignore button, ignore it. + // If its an ignored button, ignore it. if ((e->type == SDL_JOYBUTTONDOWN || e->type == SDL_JOYBUTTONUP) && (e->jbutton.button == ignored_button_ || e->jbutton.button == ignored_button2_ @@ -709,49 +668,6 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { return; } - // A little pre-filtering on mac PS3 gamepads. (try to filter out some noise - // we're seeing, etc). - if (g_buildconfig.ostype_macos() && is_mac_ps3_controller_) { - switch (e->type) { - case SDL_JOYAXISMOTION: { - // On my ps3 controller, I seem to be seeing occasional joy-axis-events - // coming in with values of -32768 when nothing is being touched. - // Filtering those out here.. Should look into this more and see if its - // SDL's fault or else forward a bug to apple. - if ((e->jaxis.axis == 0 || e->jaxis.axis == 1) - && e->jaxis.value == -32768 - && (time - ps3_last_joy_press_time_ > 2000) && !ps3_jaxis1_pressed_ - && !ps3_jaxis2_pressed_) { - printf( - "BAJoyStick notice: filtering out errand PS3 axis %d value of " - "%d\n", - static_cast(e->jaxis.axis), - static_cast(e->jaxis.value)); - fflush(stdout); - - // std::cout << "BSJoyStick notice: filtering out errant PS3 axis " << - // int(e->jaxis.axis) << " value of " << e->jaxis.value << std::endl; - return; - } - - if (abs(e->jaxis.value) >= kJoystickDiscreteThreshold) { - ps3_last_joy_press_time_ = time; - } - - // Keep track of whether its pressed for next time. - if (e->jaxis.axis == 0) { - ps3_jaxis1_pressed_ = (abs(e->jaxis.value) > 3000); - } else if (e->jaxis.axis == 1) { - ps3_jaxis2_pressed_ = (abs(e->jaxis.value) > 3000); - } - - break; - } - default: - break; - } - } - // A few high level button press interceptions. if (e->type == SDL_JOYBUTTONDOWN) { if (e->jbutton.button == start_button_ @@ -806,111 +722,116 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { } } - // If we're in a dialog, send dialog events. - // We keep track of special x/y values for dialog usage. + // If we're in the ui, send ui events. + // We keep track of special x/y values for ui usage. // These are formed as combinations of the actual joy value // and the hold-position state. // Think of hold-position as somewhat of a 'magnitude' to the joy event's // direction. They're really one and the same event. (we just need to store // their states ourselves since they don't both come through at once). - bool isAnalogStickJAxisEvent = false; + // FIXME: Ugh need to rip out this old hold-position stuff. + bool is_analog_stick_jaxis_event = false; if (e->type == SDL_JOYAXISMOTION) { if (e->jaxis.axis == analog_lr_) { dialog_jaxis_x_ = e->jaxis.value; - isAnalogStickJAxisEvent = true; + is_analog_stick_jaxis_event = true; } else if (e->jaxis.axis == analog_ud_) { dialog_jaxis_y_ = e->jaxis.value; - isAnalogStickJAxisEvent = true; + is_analog_stick_jaxis_event = true; } } - int dialogJaxisX = dialog_jaxis_x_; + int ui_jaxis_x = dialog_jaxis_x_; if (hold_position_held_) { - dialogJaxisX = 0; // Throttle is off. + ui_jaxis_x = 0; // Throttle is off. } - int dialogJaxisY = dialog_jaxis_y_; + int ui_jaxis_y = dialog_jaxis_y_; if (hold_position_held_) { - dialogJaxisY = 0; // Throttle is off. + ui_jaxis_y = 0; // Throttle is off. } // We might not wanna grab at the UI if we're a axis-motion event // below our 'pressed' threshold.. Otherwise fuzzy analog joystick // readings would cause rampant UI stealing even if no events are being sent. - bool would_go_to_dialog = false; + bool would_go_to_ui = false; auto wm = WidgetMessage::Type::kEmptyMessage; - if (isAnalogStickJAxisEvent || isHoldPositionEvent) { + if (is_analog_stick_jaxis_event || is_hold_position_event) { // Even when we're not sending, clear out some 'held' states. - if (left_held_ && dialogJaxisX >= -kJoystickDiscreteThreshold) { + if (left_held_ && ui_jaxis_x >= -kJoystickDiscreteThreshold) { left_held_ = false; + ui_repeater_.Clear(); } - if (right_held_ && dialogJaxisX <= kJoystickDiscreteThreshold) { + if (right_held_ && ui_jaxis_x <= kJoystickDiscreteThreshold) { right_held_ = false; + ui_repeater_.Clear(); } - if (up_held_ && dialogJaxisY >= -kJoystickDiscreteThreshold) { + if (up_held_ && ui_jaxis_y >= -kJoystickDiscreteThreshold) { up_held_ = false; + ui_repeater_.Clear(); } - if (down_held_ && dialogJaxisY <= kJoystickDiscreteThreshold) { + if (down_held_ && ui_jaxis_y <= kJoystickDiscreteThreshold) { down_held_ = false; + ui_repeater_.Clear(); + } + if ((!right_held_) && ui_jaxis_x > kJoystickDiscreteThreshold) { + would_go_to_ui = true; + } + if ((!left_held_) && ui_jaxis_x < -kJoystickDiscreteThreshold) { + would_go_to_ui = true; + } + if ((!up_held_) && ui_jaxis_y < -kJoystickDiscreteThreshold) { + would_go_to_ui = true; + } + if ((!down_held_) && ui_jaxis_y > kJoystickDiscreteThreshold) { + would_go_to_ui = true; } - if ((!right_held_) && dialogJaxisX > kJoystickDiscreteThreshold) - would_go_to_dialog = true; - if ((!left_held_) && dialogJaxisX < -kJoystickDiscreteThreshold) - would_go_to_dialog = true; - if ((!up_held_) && dialogJaxisY < -kJoystickDiscreteThreshold) - would_go_to_dialog = true; - if ((!down_held_) && dialogJaxisY > kJoystickDiscreteThreshold) - would_go_to_dialog = true; } else if ((e->type == SDL_JOYHATMOTION && e->jhat.hat == hat_) || (e->type == SDL_JOYBUTTONDOWN && e->jbutton.button != hold_position_button_)) { // Other button-downs and hat motions always go. - would_go_to_dialog = true; + would_go_to_ui = true; } // Resets always circumvent dialogs. - if (resetting_) would_go_to_dialog = false; + if (resetting_) { + would_go_to_ui = false; + } - // Anything that would go to a dialog also counts to mark us as - // 'recently-used'. - if (would_go_to_dialog) { + // Anything that would go to ui also counts to mark us as 'recently-used'. + if (would_go_to_ui) { UpdateLastInputTime(); } - if (would_go_to_dialog && g_base->ui->GetWidgetForInput(this)) { - bool pass = false; + if (would_go_to_ui && g_base->ui->GetWidgetForInput(this)) { + bool pass{}; // Special case.. either joy-axis-motion or hold-position events trigger // these. - if (isAnalogStickJAxisEvent || isHoldPositionEvent) { - if (dialogJaxisX > kJoystickDiscreteThreshold) { - // To the right. + if (is_analog_stick_jaxis_event || is_hold_position_event) { + if (ui_jaxis_x > kJoystickDiscreteThreshold) { if (!right_held_ && !up_held_ && !down_held_) { - last_hold_time_ = g_core->GetAppTimeMillisecs(); right_held_ = true; + pass = true; wm = WidgetMessage::Type::kMoveRight; - pass = true; } - } else if (dialogJaxisX < -kJoystickDiscreteThreshold) { + } else if (ui_jaxis_x < -kJoystickDiscreteThreshold) { if (!left_held_ && !up_held_ && !down_held_) { - last_hold_time_ = g_core->GetAppTimeMillisecs(); - wm = WidgetMessage::Type::kMoveLeft; - pass = true; left_held_ = true; + pass = true; + wm = WidgetMessage::Type::kMoveLeft; } } - if (dialogJaxisY > kJoystickDiscreteThreshold) { + if (ui_jaxis_y > kJoystickDiscreteThreshold) { if (!down_held_ && !left_held_ && !right_held_) { - last_hold_time_ = g_core->GetAppTimeMillisecs(); - wm = WidgetMessage::Type::kMoveDown; - pass = true; down_held_ = true; - } - } else if (dialogJaxisY < -kJoystickDiscreteThreshold) { - if (!up_held_ && !left_held_ && !right_held_) { - last_hold_time_ = g_core->GetAppTimeMillisecs(); - wm = WidgetMessage::Type::kMoveUp; pass = true; + wm = WidgetMessage::Type::kMoveDown; + } + } else if (ui_jaxis_y < -kJoystickDiscreteThreshold) { + if (!up_held_ && !left_held_ && !right_held_) { up_held_ = true; + pass = true; + wm = WidgetMessage::Type::kMoveUp; } } } @@ -924,7 +845,6 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { switch (e->jhat.value) { case SDL_HAT_LEFT: { if (!left_held_) { - last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveLeft; pass = true; left_held_ = true; @@ -935,7 +855,6 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { case SDL_HAT_RIGHT: { if (!right_held_) { - last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveRight; pass = true; right_held_ = true; @@ -945,7 +864,6 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { } case SDL_HAT_UP: { if (!up_held_) { - last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveUp; pass = true; up_held_ = true; @@ -955,7 +873,6 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { } case SDL_HAT_DOWN: { if (!down_held_) { - last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveDown; pass = true; down_held_ = true; @@ -968,6 +885,7 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { down_held_ = false; left_held_ = false; right_held_ = false; + ui_repeater_.Clear(); } default: break; @@ -1007,7 +925,20 @@ void JoystickInput::HandleSDLEvent(const SDL_Event* e) { break; } if (pass) { - g_base->ui->SendWidgetMessage(WidgetMessage(wm)); + switch (wm) { + case WidgetMessage::Type::kMoveUp: + case WidgetMessage::Type::kMoveDown: + case WidgetMessage::Type::kMoveLeft: + case WidgetMessage::Type::kMoveRight: + // For UI movement, set up a repeater so we can hold the button. + ui_repeater_ = Repeater::New( + kUINavigationRepeatDelay, kUINavigationRepeatInterval, + [wm] { g_base->ui->SendWidgetMessage(WidgetMessage(wm)); }); + break; + default: + // Other messages are just one-shots. + g_base->ui->SendWidgetMessage(WidgetMessage(wm)); + } } return; } @@ -1502,22 +1433,7 @@ auto JoystickInput::GetRawDeviceName() -> std::string { auto JoystickInput::GetDeviceExtraDescription() -> std::string { std::string s; - // On mac, PS3 controllers can connect via USB or bluetooth, - // and it can be confusing if one is doing both, - // so lets specify here. - if (GetDeviceName() == "PLAYSTATION(R)3 Controller") { - // For bluetooth we get a serial in the form "04-76-6e-d1-17-90" while - // on USB we get a simple int (the usb location id): "-9340234" - // so lets consider it wireless if its got a dash not at the beginning. - s = " (USB)"; - - auto dname = GetDeviceIdentifier(); - for (const char* tst = dname.c_str(); *tst; tst++) { - if (*tst == '-' && tst != dname) { - s = " (Bluetooth)"; - } - } - } + // (no longer being used). return s; } diff --git a/src/ballistica/base/input/device/joystick_input.h b/src/ballistica/base/input/device/joystick_input.h index e6133154..985fc78c 100644 --- a/src/ballistica/base/input/device/joystick_input.h +++ b/src/ballistica/base/input/device/joystick_input.h @@ -3,6 +3,7 @@ #ifndef BALLISTICA_BASE_INPUT_DEVICE_JOYSTICK_INPUT_H_ #define BALLISTICA_BASE_INPUT_DEVICE_JOYSTICK_INPUT_H_ +#include #include #include @@ -16,7 +17,7 @@ namespace ballistica::base { const int kJoystickDiscreteThreshold{15000}; const float kJoystickDiscreteThresholdFloat{0.46f}; const int kJoystickAnalogCalibrationDivisions{20}; -extern const char* kMFiControllerName; +// extern const char* kMFiControllerName; /// A physical game controller. class JoystickInput : public InputDevice { @@ -49,7 +50,6 @@ class JoystickInput : public InputDevice { auto GetPartyButtonName() const -> std::string override; - auto GetButtonName(int index) -> std::string override; auto GetAxisName(int index) -> std::string override; auto IsController() -> bool override { return true; } @@ -73,6 +73,11 @@ class JoystickInput : public InputDevice { auto HasMeaningfulButtonNames() -> bool override; + auto GetButtonName(int index) -> std::string override; + + /// Custom controller types can pass in controller-specific button names. + void SetButtonName(int button, const std::string& name); + protected: auto GetRawDeviceName() -> std::string override; auto GetDeviceExtraDescription() -> std::string override; @@ -87,51 +92,51 @@ class JoystickInput : public InputDevice { void UpdateRunningState(); auto GetCalibratedValue(float raw, float neutral) const -> int32_t; - std::string raw_sdl_joystick_name_; - std::string raw_sdl_joystick_identifier_; - float run_value_{}; JoystickInput* child_joy_stick_{}; JoystickInput* parent_joy_stick_{}; millisecs_t last_ui_only_print_time_{}; - bool ui_only_{}; - bool unassigned_buttons_run_{true}; - bool start_button_activates_default_widget_{true}; - bool auto_recalibrate_analog_stick_{}; millisecs_t creation_time_{}; - bool did_initial_reset_{}; // FIXME - should take this out and replace it with a bool // (we never actually access the sdl joystick directly outside of our // constructor) SDL_Joystick* sdl_joystick_{}; - bool is_test_input_{}; - bool is_remote_control_{}; - bool is_remote_app_{}; - bool is_mfi_controller_{}; - bool is_mac_ps3_controller_{}; - - millisecs_t ps3_last_joy_press_time_{-10000}; + bool ui_only_ : 1 {}; + bool unassigned_buttons_run_ : 1 {true}; + bool start_button_activates_default_widget_ : 1 {true}; + bool auto_recalibrate_analog_stick_ : 1 {}; + bool did_initial_reset_ : 1 {}; + bool is_test_input_ : 1 {}; + bool is_remote_control_ : 1 {}; + bool is_remote_app_ : 1 {}; + bool is_mfi_controller_ : 1 {}; // For dialogs. - bool left_held_{}; - bool right_held_{}; - bool up_held_{}; - bool down_held_{}; - bool hold_position_held_{}; - bool need_to_send_held_state_{}; - int hat_{}; - int analog_lr_{}; + bool left_held_ : 1 {}; + bool right_held_ : 1 {}; + bool up_held_ : 1 {}; + bool down_held_ : 1 {}; + bool hold_position_held_ : 1 {}; + bool need_to_send_held_state_ : 1 {}; + + bool hat_held_ : 1 {}; + bool dpad_right_held_ : 1 {}; + bool dpad_left_held_ : 1 {}; + bool dpad_up_held_ : 1 {}; + bool dpad_down_held_ : 1 {}; + + bool ignore_completely_ : 1 {}; + bool resetting_ : 1 {}; + bool calibrate_ : 1 {}; + bool can_configure_ : 1 {}; + + int hat_{0}; + int analog_lr_{0}; int analog_ud_{1}; - millisecs_t last_hold_time_{}; - bool hat_held_{}; - bool dpad_right_held_{}; - bool dpad_left_held_{}; - bool dpad_up_held_{}; - bool dpad_down_held_{}; // Mappings of ba buttons to SDL buttons. - int jump_button_{}; + int jump_button_{0}; int punch_button_{1}; int bomb_button_{2}; int pickup_button_{3}; @@ -143,7 +148,6 @@ class JoystickInput : public InputDevice { // Used on rift build; we have one button which we disallow from joining but // the rest we allow. (all devices are treated as one and the same there). int remote_enter_button_{-1}; - bool ignore_completely_{}; int ignored_button_{-1}; int ignored_button2_{-1}; int ignored_button3_{-1}; @@ -153,12 +157,6 @@ class JoystickInput : public InputDevice { int run_trigger1_{-1}; int run_trigger2_{-1}; int vr_reorient_button_{-1}; - float run_trigger1_min_{}; - float run_trigger1_max_{}; - float run_trigger2_min_{}; - float run_trigger2_max_{}; - float run_trigger1_value_{}; - float run_trigger2_value_{}; int left_button_{-1}; int right_button_{-1}; int up_button_{-1}; @@ -167,15 +165,19 @@ class JoystickInput : public InputDevice { int right_button2_{-1}; int up_button2_{-1}; int down_button2_{-1}; - std::set run_buttons_held_; int sdl_joystick_id_{}; - bool ps3_jaxis1_pressed_{}; - bool ps3_jaxis2_pressed_{}; + float run_value_{}; + float run_trigger1_min_{}; + float run_trigger1_max_{}; + float run_trigger2_min_{}; + float run_trigger2_max_{}; + float run_trigger1_value_{}; + float run_trigger2_value_{}; float calibration_threshold_{}; float calibration_break_threshold_{}; float analog_calibration_vals_[kJoystickAnalogCalibrationDivisions]{}; - std::string custom_device_name_; - bool can_configure_{}; + float calibrated_neutral_x_{}; + float calibrated_neutral_y_{}; int32_t dialog_jaxis_x_{}; int32_t dialog_jaxis_y_{}; int32_t jaxis_raw_x_{}; @@ -183,11 +185,13 @@ class JoystickInput : public InputDevice { int32_t jaxis_x_{}; int32_t jaxis_y_{}; millisecs_t calibration_start_time_x_{}; - float calibrated_neutral_x_{}; millisecs_t calibration_start_time_y_{}; - float calibrated_neutral_y_{}; - bool resetting_{}; - bool calibrate_{}; + std::set run_buttons_held_; + std::string custom_device_name_; + std::string raw_sdl_joystick_name_; + std::string raw_sdl_joystick_identifier_; + std::map button_names_; + Object::Ref ui_repeater_; BA_DISALLOW_CLASS_COPIES(JoystickInput); }; diff --git a/src/ballistica/base/input/device/keyboard_input.cc b/src/ballistica/base/input/device/keyboard_input.cc index 4a1908d4..0da1eeb5 100644 --- a/src/ballistica/base/input/device/keyboard_input.cc +++ b/src/ballistica/base/input/device/keyboard_input.cc @@ -2,8 +2,10 @@ #include "ballistica/base/input/device/keyboard_input.h" +#include "ballistica/base/app_adapter/app_adapter.h" #include "ballistica/base/platform/base_platform.h" #include "ballistica/base/support/classic_soft.h" +#include "ballistica/base/support/repeater.h" #include "ballistica/base/ui/ui.h" namespace ballistica::base { @@ -42,10 +44,12 @@ KeyboardInput::KeyboardInput(KeyboardInput* parent_keyboard_input_in) { KeyboardInput::~KeyboardInput() = default; -auto KeyboardInput::HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) - -> bool { +auto KeyboardInput::HandleKey(const SDL_Keysym* keysym, bool down) -> bool { // Only allow the *main* keyboard to talk to the UI if (parent_keyboard_input_ == nullptr) { + // Any new event coming in cancels repeats. + ui_repeater_.Clear(); + if (g_base->ui->GetWidgetForInput(this)) { bool pass = false; auto c = WidgetMessage::Type::kEmptyMessage; @@ -78,10 +82,8 @@ auto KeyboardInput::HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) case SDLK_SPACE: case SDLK_KP_ENTER: case SDLK_RETURN: - if (!repeat) { - c = WidgetMessage::Type::kActivate; - pass = true; - } + c = WidgetMessage::Type::kActivate; + pass = true; break; case SDLK_ESCAPE: // (limit to kb1 so we don't get double-beeps on failure) @@ -123,32 +125,44 @@ auto KeyboardInput::HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) } } if (pass) { - g_base->ui->SendWidgetMessage(WidgetMessage(c, keysym)); + // For movement and key press widget events, set up repeats. + // Otherwise run a single time immediately. + switch (c) { + case WidgetMessage::Type::kMoveUp: + case WidgetMessage::Type::kMoveDown: + case WidgetMessage::Type::kMoveLeft: + case WidgetMessage::Type::kMoveRight: + case WidgetMessage::Type::kKey: + // Note: Need to pass keysym along as a value; not a pointer. + ui_repeater_ = Repeater::New( + g_base->app_adapter->GetKeyRepeatDelay(), + g_base->app_adapter->GetKeyRepeatInterval(), + [c, keysym = *keysym] { + g_base->ui->SendWidgetMessage(WidgetMessage(c, &keysym)); + }); + break; + default: + g_base->ui->SendWidgetMessage(WidgetMessage(c, keysym)); + break; + } } return (pass); } } // Bring up menu if start is pressed. - if (keysym->sym == start_key_ && !repeat && !g_base->ui->MainMenuVisible()) { + if (keysym->sym == start_key_ && !g_base->ui->MainMenuVisible()) { g_base->ui->PushMainMenuPressCall(this); return true; } -// Clion seems to think child_keyboard_input_ will never be set here (it will). -#pragma clang diagnostic push -#pragma ide diagnostic ignored "UnreachableCode" -#pragma ide diagnostic ignored "ConstantConditionsOC" - // At this point, if we have a child input, let it try to handle things. if (child_keyboard_input_ && enable_child_) { - if (child_keyboard_input_->HandleKey(keysym, repeat, down)) { + if (child_keyboard_input_->HandleKey(keysym, down)) { return true; } } -#pragma clang diagnostic pop - if (!AttachedToPlayer()) { if (down && ((keysym->sym == jump_key_) || (keysym->sym == punch_key_) @@ -173,152 +187,151 @@ auto KeyboardInput::HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) // (removing init values from input_type and input_type_2 gives a // 'possibly uninited value used' warning but leaving them gives a // 'values unused' warning. Grumble.) - explicit_bool(input_type - == (explicit_bool(false) ? input_type_2 : InputType::kLast)); + // explicit_bool(input_type + // == (explicit_bool(false) ? input_type_2 : + // InputType::kLast)); - if (!repeat) { - // Keyboard 1 supports assigned keys plus arrow keys if they're unused. - if (keysym->sym == left_key_ - || (device_number() == 1 && keysym->sym == SDLK_LEFT - && !left_key_assigned())) { - player_input = true; - input_type = InputType::kLeftRight; - left_held_ = down; - if (down) { - if (right_held_) { - input_value = 0; - } else { - input_value = -32767; - } + // Keyboard 1 supports assigned keys plus arrow keys if they're unused. + if (keysym->sym == left_key_ + || (device_number() == 1 && keysym->sym == SDLK_LEFT + && !left_key_assigned())) { + player_input = true; + input_type = InputType::kLeftRight; + left_held_ = down; + if (down) { + if (right_held_) { + input_value = 0; } else { - if (right_held_) { - input_value = 32767; - } - } - } else if (keysym->sym == right_key_ - || (device_number() == 1 && keysym->sym == SDLK_RIGHT - && !right_key_assigned())) { - // Keyboard 1 supports assigned keys plus arrow keys if they're unused. - player_input = true; - input_type = InputType::kLeftRight; - right_held_ = down; - if (down) { - if (left_held_) { - input_value = 0; - } else { - input_value = 32767; - } - } else { - if (left_held_) { - input_value = -32767; - } - } - } else if (keysym->sym == up_key_ - || (device_number() == 1 && keysym->sym == SDLK_UP - && !up_key_assigned())) { - player_input = true; - input_type = InputType::kUpDown; - up_held_ = down; - if (down) { - if (down_held_) { - input_value = 0; - } else { - input_value = 32767; - } - } else { - if (down_held_) input_value = -32767; - } - } else if (keysym->sym == down_key_ - || (device_number() == 1 && keysym->sym == SDLK_DOWN - && !down_key_assigned())) { - player_input = true; - input_type = InputType::kUpDown; - down_held_ = down; - if (down) { - if (up_held_) { - input_value = 0; - } else { - input_value = -32767; - } - } else { - if (up_held_) input_value = 32767; - } - } else if (keysym->sym == punch_key_) { - player_input = true; - UpdateRun(keysym->sym, down); - if (down) { - input_type = InputType::kPunchPress; - } else { - input_type = InputType::kPunchRelease; - } - } else if (keysym->sym == bomb_key_) { - player_input = true; - UpdateRun(keysym->sym, down); - if (down) - input_type = InputType::kBombPress; - else - input_type = InputType::kBombRelease; - } else if (keysym->sym == hold_position_key_) { - player_input = true; - if (down) { - input_type = InputType::kHoldPositionPress; - } else { - input_type = InputType::kHoldPositionRelease; - } - } else if (keysym->sym == pick_up_key_) { - player_input = true; - UpdateRun(keysym->sym, down); - if (down) { - input_type = InputType::kPickUpPress; - } else { - input_type = InputType::kPickUpRelease; - } - } else if ((device_number() == 1 && keysym->sym == SDLK_RETURN) - || (device_number() == 1 && keysym->sym == SDLK_KP_ENTER) - || keysym->sym == jump_key_) { - // Keyboard 1 claims certain keys if they are otherwise unclaimed - // (arrow keys, enter/return, etc). - player_input = true; - UpdateRun(keysym->sym, down); - if (down) { - input_type = InputType::kJumpPress; - have_input_2 = true; - input_type_2 = InputType::kFlyPress; - } else { - input_type = InputType::kJumpRelease; - have_input_2 = true; - input_type_2 = InputType::kFlyRelease; + input_value = -32767; } } else { - // Any other keys get processed as run keys. - // keypad keys go to player 2 - anything else to player 1. - switch (keysym->sym) { - case SDLK_KP_0: - case SDLK_KP_1: - case SDLK_KP_2: - case SDLK_KP_3: - case SDLK_KP_4: - case SDLK_KP_5: - case SDLK_KP_6: - case SDLK_KP_7: - case SDLK_KP_8: - case SDLK_KP_9: - case SDLK_KP_PLUS: - case SDLK_KP_MINUS: - case SDLK_KP_ENTER: - if (device_number() == 2) { - UpdateRun(keysym->sym, down); - return true; - } - break; - default: - if (device_number() == 1) { - UpdateRun(keysym->sym, down); - return true; - } - break; + if (right_held_) { + input_value = 32767; } } + } else if (keysym->sym == right_key_ + || (device_number() == 1 && keysym->sym == SDLK_RIGHT + && !right_key_assigned())) { + // Keyboard 1 supports assigned keys plus arrow keys if they're unused. + player_input = true; + input_type = InputType::kLeftRight; + right_held_ = down; + if (down) { + if (left_held_) { + input_value = 0; + } else { + input_value = 32767; + } + } else { + if (left_held_) { + input_value = -32767; + } + } + } else if (keysym->sym == up_key_ + || (device_number() == 1 && keysym->sym == SDLK_UP + && !up_key_assigned())) { + player_input = true; + input_type = InputType::kUpDown; + up_held_ = down; + if (down) { + if (down_held_) { + input_value = 0; + } else { + input_value = 32767; + } + } else { + if (down_held_) input_value = -32767; + } + } else if (keysym->sym == down_key_ + || (device_number() == 1 && keysym->sym == SDLK_DOWN + && !down_key_assigned())) { + player_input = true; + input_type = InputType::kUpDown; + down_held_ = down; + if (down) { + if (up_held_) { + input_value = 0; + } else { + input_value = -32767; + } + } else { + if (up_held_) input_value = 32767; + } + } else if (keysym->sym == punch_key_) { + player_input = true; + UpdateRun_(keysym->sym, down); + if (down) { + input_type = InputType::kPunchPress; + } else { + input_type = InputType::kPunchRelease; + } + } else if (keysym->sym == bomb_key_) { + player_input = true; + UpdateRun_(keysym->sym, down); + if (down) + input_type = InputType::kBombPress; + else + input_type = InputType::kBombRelease; + } else if (keysym->sym == hold_position_key_) { + player_input = true; + if (down) { + input_type = InputType::kHoldPositionPress; + } else { + input_type = InputType::kHoldPositionRelease; + } + } else if (keysym->sym == pick_up_key_) { + player_input = true; + UpdateRun_(keysym->sym, down); + if (down) { + input_type = InputType::kPickUpPress; + } else { + input_type = InputType::kPickUpRelease; + } + } else if ((device_number() == 1 && keysym->sym == SDLK_RETURN) + || (device_number() == 1 && keysym->sym == SDLK_KP_ENTER) + || keysym->sym == jump_key_) { + // Keyboard 1 claims certain keys if they are otherwise unclaimed + // (arrow keys, enter/return, etc). + player_input = true; + UpdateRun_(keysym->sym, down); + if (down) { + input_type = InputType::kJumpPress; + have_input_2 = true; + input_type_2 = InputType::kFlyPress; + } else { + input_type = InputType::kJumpRelease; + have_input_2 = true; + input_type_2 = InputType::kFlyRelease; + } + } else { + // Any other keys get processed as run keys. + // keypad keys go to player 2 - anything else to player 1. + switch (keysym->sym) { + case SDLK_KP_0: + case SDLK_KP_1: + case SDLK_KP_2: + case SDLK_KP_3: + case SDLK_KP_4: + case SDLK_KP_5: + case SDLK_KP_6: + case SDLK_KP_7: + case SDLK_KP_8: + case SDLK_KP_9: + case SDLK_KP_PLUS: + case SDLK_KP_MINUS: + case SDLK_KP_ENTER: + if (device_number() == 2) { + UpdateRun_(keysym->sym, down); + return true; + } + break; + default: + if (device_number() == 1) { + UpdateRun_(keysym->sym, down); + return true; + } + break; + } } if (player_input) { @@ -344,7 +357,7 @@ void KeyboardInput::ResetHeldStates() { } } -void KeyboardInput::UpdateRun(SDL_Keycode key, bool down) { +void KeyboardInput::UpdateRun_(SDL_Keycode key, bool down) { bool was_held = (!keys_held_.empty()); if (down) { keys_held_.insert(key); @@ -407,51 +420,51 @@ void KeyboardInput::UpdateMapping() { int val = cl ? cl->GetControllerValue(this, "buttonJump") : -1; jump_key_ = (val == -1) ? jump_key_default : (SDL_Keycode)val; - UpdateArrowKeys(jump_key_); + UpdateArrowKeys_(jump_key_); val = cl ? cl->GetControllerValue(this, "buttonPunch") : -1; punch_key_ = (val == -1) ? punch_key_default : (SDL_Keycode)val; - UpdateArrowKeys(punch_key_); + UpdateArrowKeys_(punch_key_); val = cl ? cl->GetControllerValue(this, "buttonBomb") : -1; bomb_key_ = (val == -1) ? bomb_key_default : (SDL_Keycode)val; - UpdateArrowKeys(bomb_key_); + UpdateArrowKeys_(bomb_key_); val = cl ? cl->GetControllerValue(this, "buttonPickUp") : -1; pick_up_key_ = (val == -1) ? pick_up_key_default : (SDL_Keycode)val; - UpdateArrowKeys(pick_up_key_); + UpdateArrowKeys_(pick_up_key_); val = cl ? cl->GetControllerValue(this, "buttonHoldPosition") : -1; hold_position_key_ = (val == -1) ? hold_position_key_default : (SDL_Keycode)val; - UpdateArrowKeys(hold_position_key_); + UpdateArrowKeys_(hold_position_key_); val = cl ? cl->GetControllerValue(this, "buttonStart") : -1; start_key_ = (val == -1) ? start_key_default : (SDL_Keycode)val; - UpdateArrowKeys(start_key_); + UpdateArrowKeys_(start_key_); val = cl ? cl->GetControllerValue(this, "buttonUp") : -1; up_key_ = (val == -1) ? up_key_default : (SDL_Keycode)val; - UpdateArrowKeys(up_key_); + UpdateArrowKeys_(up_key_); val = cl ? cl->GetControllerValue(this, "buttonDown") : -1; down_key_ = (val == -1) ? down_key_default : (SDL_Keycode)val; - UpdateArrowKeys(down_key_); + UpdateArrowKeys_(down_key_); val = cl ? cl->GetControllerValue(this, "buttonLeft") : -1; left_key_ = (val == -1) ? left_key_default : (SDL_Keycode)val; - UpdateArrowKeys(left_key_); + UpdateArrowKeys_(left_key_); val = cl ? cl->GetControllerValue(this, "buttonRight") : -1; right_key_ = (val == -1) ? right_key_default : (SDL_Keycode)val; - UpdateArrowKeys(right_key_); + UpdateArrowKeys_(right_key_); enable_child_ = true; up_held_ = down_held_ = left_held_ = right_held_ = false; } -void KeyboardInput::UpdateArrowKeys(SDL_Keycode key) { +void KeyboardInput::UpdateArrowKeys_(SDL_Keycode key) { if (key == SDLK_UP) { up_key_assigned_ = true; } else if (key == SDLK_DOWN) { @@ -464,8 +477,7 @@ void KeyboardInput::UpdateArrowKeys(SDL_Keycode key) { } auto KeyboardInput::GetButtonName(int index) -> std::string { - return g_base->platform->GetKeyName(index); - // return InputDevice::GetButtonName(index); + return g_base->app_adapter->GetKeyName(index); } auto KeyboardInput::GetRawDeviceName() -> std::string { return "Keyboard"; } diff --git a/src/ballistica/base/input/device/keyboard_input.h b/src/ballistica/base/input/device/keyboard_input.h index 6d790e9c..8138b5c6 100644 --- a/src/ballistica/base/input/device/keyboard_input.h +++ b/src/ballistica/base/input/device/keyboard_input.h @@ -8,6 +8,7 @@ #include "ballistica/base/input/device/input_device.h" #include "ballistica/core/platform/support/min_sdl.h" +#include "ballistica/shared/foundation/object.h" namespace ballistica::base { @@ -15,7 +16,7 @@ class KeyboardInput : public InputDevice { public: explicit KeyboardInput(KeyboardInput* parent); ~KeyboardInput() override; - auto HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) -> bool; + auto HandleKey(const SDL_Keysym* keysym, bool down) -> bool; void UpdateMapping() override; auto GetRawDeviceName() -> std::string override; void ResetHeldStates() override; @@ -29,8 +30,17 @@ class KeyboardInput : public InputDevice { auto GetButtonName(int index) -> std::string override; private: - void UpdateArrowKeys(SDL_Keycode key); - void UpdateRun(SDL_Keycode key, bool down); + void UpdateArrowKeys_(SDL_Keycode key); + void UpdateRun_(SDL_Keycode key, bool down); + bool down_held_ : 1 {}; + bool up_held_ : 1 {}; + bool left_held_ : 1 {}; + bool right_held_ : 1 {}; + bool enable_child_ : 1 {}; + bool left_key_assigned_ : 1 {}; + bool right_key_assigned_ : 1 {}; + bool up_key_assigned_ : 1 {}; + bool down_key_assigned_ : 1 {}; SDL_Keycode up_key_{}; SDL_Keycode down_key_{}; SDL_Keycode left_key_{}; @@ -41,18 +51,10 @@ class KeyboardInput : public InputDevice { SDL_Keycode pick_up_key_{}; SDL_Keycode hold_position_key_{}; SDL_Keycode start_key_{}; - bool down_held_{}; - bool up_held_{}; - bool left_held_{}; - bool right_held_{}; - bool enable_child_{}; - bool left_key_assigned_{}; - bool right_key_assigned_{}; - bool up_key_assigned_{}; - bool down_key_assigned_{}; KeyboardInput* parent_keyboard_input_{}; KeyboardInput* child_keyboard_input_{}; std::set keys_held_; + Object::Ref ui_repeater_; }; } // namespace ballistica::base diff --git a/src/ballistica/base/input/device/test_input.cc b/src/ballistica/base/input/device/test_input.cc index 7e20a291..b7491c2e 100644 --- a/src/ballistica/base/input/device/test_input.cc +++ b/src/ballistica/base/input/device/test_input.cc @@ -22,7 +22,7 @@ TestInput::~TestInput() { } void TestInput::Reset() { - assert(g_core->InMainThread()); + assert(g_base->InLogicThread()); reset_ = true; } @@ -33,7 +33,7 @@ void TestInput::HandleAlreadyPressedTwice() { } void TestInput::Process(millisecs_t time) { - assert(g_core->InMainThread()); + assert(g_base->InLogicThread()); if (reset_) { reset_ = false; diff --git a/src/ballistica/base/input/device/test_input.h b/src/ballistica/base/input/device/test_input.h index 34e2e4ce..b89accf4 100644 --- a/src/ballistica/base/input/device/test_input.h +++ b/src/ballistica/base/input/device/test_input.h @@ -16,20 +16,21 @@ class TestInput { private: void HandleAlreadyPressedTwice(); + int lr_{}; int ud_{}; - bool jump_pressed_{}; - bool bomb_pressed_{}; - bool pickup_pressed_{}; - bool punch_pressed_{}; + int join_press_count_{}; + bool jump_pressed_ : 1 {}; + bool bomb_pressed_ : 1 {}; + bool pickup_pressed_ : 1 {}; + bool punch_pressed_ : 1 {}; + bool print_non_join_ : 1 {}; + bool print_already_did2_ : 1 {}; + bool reset_ : 1 {true}; millisecs_t next_event_time_{}; millisecs_t join_start_time_{}; millisecs_t join_end_time_{9999}; - int join_press_count_{}; - bool reset_{true}; JoystickInput* joystick_{}; - bool print_non_join_{}; - bool print_already_did2_{}; }; } // namespace ballistica::base diff --git a/src/ballistica/base/input/input.cc b/src/ballistica/base/input/input.cc index acefa84b..fe66c1a9 100644 --- a/src/ballistica/base/input/input.cc +++ b/src/ballistica/base/input/input.cc @@ -8,14 +8,11 @@ #include "ballistica/base/graphics/support/camera.h" #include "ballistica/base/input/device/joystick_input.h" #include "ballistica/base/input/device/keyboard_input.h" -#include "ballistica/base/input/device/test_input.h" #include "ballistica/base/input/device/touch_input.h" #include "ballistica/base/logic/logic.h" #include "ballistica/base/python/base_python.h" -#include "ballistica/base/support/app_config.h" #include "ballistica/base/ui/dev_console.h" #include "ballistica/base/ui/ui.h" -#include "ballistica/shared/buildconfig/buildconfig_common.h" #include "ballistica/shared/foundation/event_loop.h" #include "ballistica/shared/generic/utils.h" @@ -147,24 +144,22 @@ auto Input::GetNewNumberedIdentifier_(const std::string& name, return full_id; } -void Input::CreateTouchInput() { - assert(g_core->InMainThread()); - assert(touch_input_ == nullptr); - touch_input_ = Object::NewDeferred(); - PushAddInputDeviceCall(touch_input_, false); -} +// void Input::CreateTouchInput() { +// assert(g_core->InMainThread()); +// } void Input::AnnounceConnects_() { + assert(g_base->InLogicThread()); + static bool first_print = true; // For the first announcement just say "X controllers detected" and don't // have a sound. - if (first_print && g_core->GetAppTimeSeconds() < 5.0) { + if (first_print && g_core->GetAppTimeSeconds() < 3.0) { first_print = false; // Disabling this completely on Android for now; we often get large // numbers of devices there that aren't actually devices. - bool do_print_initial_counts{!g_buildconfig.ostype_android()}; // If there's been several connected, just give a number. @@ -238,12 +233,15 @@ void Input::ShowStandardInputDeviceConnectedMessage_(InputDevice* j) { } newly_connected_controllers_.push_back(j->GetDeviceName() + suffix); - // Set a timer to go off and announce the accumulated additions. + // Set a timer to go off and announce controller additions. This allows + // several connecting at (almost) the same time to be announced as a + // single event. if (connect_print_timer_id_ != 0) { g_base->logic->DeleteAppTimer(connect_print_timer_id_); } connect_print_timer_id_ = g_base->logic->NewAppTimer( - 250, false, NewLambdaRunnable([this] { AnnounceConnects_(); })); + 500 * 1000, false, + NewLambdaRunnable([this] { AnnounceConnects_(); }).Get()); } void Input::ShowStandardInputDeviceDisconnectedMessage_(InputDevice* j) { @@ -258,7 +256,8 @@ void Input::ShowStandardInputDeviceDisconnectedMessage_(InputDevice* j) { g_base->logic->DeleteAppTimer(disconnect_print_timer_id_); } disconnect_print_timer_id_ = g_base->logic->NewAppTimer( - 250, false, NewLambdaRunnable([this] { AnnounceDisconnects_(); })); + 250 * 1000, false, + NewLambdaRunnable([this] { AnnounceDisconnects_(); }).Get()); } void Input::PushAddInputDeviceCall(InputDevice* input_device, @@ -557,7 +556,14 @@ void Input::UpdateEnabledControllerSubsystems_() { // } } -void Input::OnAppStart() { assert(g_base->InLogicThread()); } +void Input::OnAppStart() { + assert(g_base->InLogicThread()); + if (g_core->platform->HasTouchScreen()) { + assert(touch_input_ == nullptr); + touch_input_ = Object::NewDeferred(); + PushAddInputDeviceCall(touch_input_, false); + } +} void Input::OnAppPause() { assert(g_base->InLogicThread()); } @@ -759,63 +765,12 @@ void Input::PrintLockLabels_() { Log(LogLevel::kError, s); } -void Input::ProcessStressTesting(int player_count) { - assert(g_core->InMainThread()); - assert(player_count >= 0); - - millisecs_t time = g_core->GetAppTimeMillisecs(); - - // FIXME: If we don't check for stress_test_last_leave_time_ we totally - // confuse the game.. need to be able to survive that. - - // Kill some off if we have too many. - while (static_cast(test_inputs_.size()) > player_count) { - delete test_inputs_.front(); - test_inputs_.pop_front(); - } - - // If we have less than full test-inputs, add one randomly. - if (static_cast(test_inputs_.size()) < player_count - && ((rand() % 1000 < 10))) { // NOLINT - test_inputs_.push_back(new TestInput()); - } - - // Every so often lets kill the oldest one off. - if (explicit_bool(true)) { - if (test_inputs_.size() > 0 && (rand() % 2000 < 3)) { // NOLINT - stress_test_last_leave_time_ = time; - - // Usually do oldest; sometimes newest. - if (rand() % 5 == 0) { // NOLINT - delete test_inputs_.back(); - test_inputs_.pop_back(); - } else { - delete test_inputs_.front(); - test_inputs_.pop_front(); - } - } - } - - if (time - stress_test_time_ > 1000) { - stress_test_time_ = time; // reset.. - for (auto& test_input : test_inputs_) { - (*test_input).Reset(); - } - } - while (stress_test_time_ < time) { - stress_test_time_++; - for (auto& test_input : test_inputs_) { - (*test_input).Process(stress_test_time_); - } - } -} - void Input::PushTextInputEvent(const std::string& text) { assert(g_base->logic->event_loop()); g_base->logic->event_loop()->PushCall([this, text] { MarkInputActive(); - // If if the app doesn't want direct text input right now. + // If the app doesn't want direct text input right now, ignore. if (!g_base->app_adapter->HasDirectKeyboardInput()) { return; } @@ -825,6 +780,22 @@ void Input::PushTextInputEvent(const std::string& text) { return; } + // Also ignore if there are any mod keys being held. + // We process some of our own keyboard shortcuts and don't + // want text input to come through at the same time. + if (keys_held_.contains(SDLK_LCTRL) || keys_held_.contains(SDLK_RCTRL) + || keys_held_.contains(SDLK_LALT) || keys_held_.contains(SDLK_RALT) + || keys_held_.contains(SDLK_LGUI) || keys_held_.contains(SDLK_RGUI)) { + return; + } + + // Ignore back-tick and tilde because we use that key to toggle the console. + // FIXME: Perhaps should allow typing it if some control-character is + // held? + if (text == "`" || text == "~") { + return; + } + // We try to handle char filtering here (to keep it consistent across // platforms) but make a stink if they sent us something that we can't // at least translate to unicode. @@ -848,6 +819,7 @@ void Input::PushTextInputEvent(const std::string& text) { && g_base->ui->dev_console()->HandleTextEditing(text)) { return; } + g_base->ui->SendWidgetMessage(WidgetMessage( WidgetMessage::Type::kTextInput, nullptr, 0, 0, 0, 0, text.c_str())); }); @@ -986,6 +958,34 @@ void Input::HandleKeyPress_(const SDL_Keysym& keysym) { return; } + // Nowadays we don't want the OS to deliver repeat events to us, + // so filter out any that we get and make noise that they should stop. We + // explicitly handle repeats for UI purposes at the InputDevice or Widget + // level now. + if (keys_held_.find(keysym.sym) != keys_held_.end()) { + // Look out for several repeats coming in within the span of a few + // seconds and complain if it happens. This should allow for the random + // fluke repeat key press event due to funky OS circumstances. + static int count{}; + static seconds_t last_count_reset_time{}; + auto now = g_core->GetAppTimeSeconds(); + if (now - last_count_reset_time > 2.0) { + count = 0; + last_count_reset_time = now; + } else { + count++; + if (count > 10) { + BA_LOG_ONCE( + LogLevel::kWarning, + "Input::HandleKeyPress_ seems to be getting passed repeat key" + " press events. Only initial press events should be passed."); + } + } + return; + } + + keys_held_.insert(keysym.sym); + // If someone is capturing these events, give them a crack at it. if (keyboard_input_capture_press_) { if (keyboard_input_capture_press_(keysym)) { @@ -998,52 +998,36 @@ void Input::HandleKeyPress_(const SDL_Keysym& keysym) { // ideally we should use the modifiers bundled with the key presses) UpdateModKeyStates_(&keysym, true); - bool repeat_press; - if (keys_held_.count(keysym.sym) != 0) { - repeat_press = true; - } else { - repeat_press = false; - keys_held_.insert(keysym.sym); - } - // Mobile-specific stuff. - if (g_buildconfig.ostype_ios_tvos() || g_buildconfig.ostype_android()) { - switch (keysym.sym) { - // FIXME: See if this stuff is still necessary. Was this perhaps - // specifically to support the console? - case SDLK_DELETE: - case SDLK_RETURN: - case SDLK_KP_ENTER: - case SDLK_BACKSPACE: { - // FIXME: I don't remember what this was put here for, but now that - // we have hardware keyboards it crashes text fields by sending - // them a TEXT_INPUT message with no string.. I made them resistant - // to that case but wondering if we can take this out? - g_base->ui->SendWidgetMessage( - WidgetMessage(WidgetMessage::Type::kTextInput, &keysym)); - break; - } - default: - break; - } - } + // if (g_buildconfig.ostype_ios_tvos() || g_buildconfig.ostype_android()) { + // switch (keysym.sym) { + // // FIXME: See if this stuff is still necessary. Was this perhaps + // // specifically to support the console? + // case SDLK_DELETE: + // case SDLK_RETURN: + // case SDLK_KP_ENTER: + // case SDLK_BACKSPACE: { + // // FIXME: I don't remember what this was put here for, but now that + // // we have hardware keyboards it crashes text fields by sending + // // them a TEXT_INPUT message with no string.. I made them resistant + // // to that case but wondering if we can take this out? + // g_base->ui->SendWidgetMessage( + // WidgetMessage(WidgetMessage::Type::kTextInput, &keysym)); + // break; + // } + // default: + // break; + // } + // } // Explicitly handle fullscreen-toggles in some cases. if (g_base->app_adapter->FullscreenControlAvailable()) { bool do_toggle{}; - // On our Mac SDL builds we support ctrl+F for toggling fullscreen. - // On our nice Cocoa build, fullscreening happens magically through the - // view menu fullscreen controls. - if (g_buildconfig.ostype_macos() && !g_buildconfig.xcode_build()) { - if (!repeat_press && keysym.sym == SDLK_f && ((keysym.mod & KMOD_CTRL))) { - do_toggle = true; - } - } - // On Windows we support both F11 and Alt+Enter for toggling fullscreen. - if (g_buildconfig.ostype_windows()) { - if (!repeat_press - && (keysym.sym == SDLK_F11 - || (keysym.sym == SDLK_RETURN && ((keysym.mod & KMOD_ALT))))) { + // On our SDL builds we support both F11 and Alt+Enter for toggling + // fullscreen. + if (g_buildconfig.sdl_build()) { + if ((keysym.sym == SDLK_F11 + || (keysym.sym == SDLK_RETURN && ((keysym.mod & KMOD_ALT))))) { do_toggle = true; } } @@ -1055,120 +1039,117 @@ void Input::HandleKeyPress_(const SDL_Keysym& keysym) { } } - // Control-Q quits. On Mac, the usual Cmd-Q gets handled implicitly by the - // app-adapter. - // UPDATE: Disabling this for now. Looks like standard OS shortcuts like - // Alt+F4 on windows or Cmd-Q on Mac are doing the right thing with SDL - // builds these days so these are not needed. - // if (!repeat_press && keysym.sym == SDLK_q && (keysym.mod & KMOD_CTRL)) { - // g_base->QuitApp(true); - // return; - // } + // Ctrl-V or Cmd-V sends paste commands to the console or any interested + // text fields. + if (keysym.sym == SDLK_v + && ((keysym.mod & KMOD_CTRL) || (keysym.mod & KMOD_GUI))) { + if (auto* console = g_base->ui->dev_console()) { + if (console->PasteFromClipboard()) { + return; + } + } + g_base->ui->SendWidgetMessage(WidgetMessage(WidgetMessage::Type::kPaste)); + return; + } - // Let the console intercept stuff if it wants at this point. + // Dev Console. if (auto* console = g_base->ui->dev_console()) { + if (keysym.sym == SDLK_BACKQUOTE || keysym.sym == SDLK_F2) { + // (reset input so characters don't continue walking and stuff) + g_base->input->ResetHoldStates(); + console->ToggleState(); + return; + } if (console->HandleKeyPress(&keysym)) { return; } } - // Ctrl-V or Cmd-V sends paste commands to any interested text fields. - // Command-Q or Control-Q quits. - if (!repeat_press && keysym.sym == SDLK_v - && ((keysym.mod & KMOD_CTRL) || (keysym.mod & KMOD_GUI))) { - g_base->ui->SendWidgetMessage(WidgetMessage(WidgetMessage::Type::kPaste)); - return; - } - bool handled = false; - // None of the following stuff accepts key repeats. - if (!repeat_press) { - switch (keysym.sym) { - // Menu button on android/etc. pops up the menu. - case SDLK_MENU: { - if (!g_base->ui->MainMenuVisible()) { - g_base->ui->PushMainMenuPressCall(touch_input_); - } - handled = true; - break; + switch (keysym.sym) { + // Menu button on android/etc. pops up the menu. + case SDLK_MENU: { + if (!g_base->ui->MainMenuVisible()) { + g_base->ui->PushMainMenuPressCall(touch_input_); } - - case SDLK_EQUALS: - case SDLK_PLUS: - if (keysym.mod & KMOD_CTRL) { - g_base->app_mode()->ChangeGameSpeed(1); - handled = true; - } - break; - - case SDLK_MINUS: - if (keysym.mod & KMOD_CTRL) { - g_base->app_mode()->ChangeGameSpeed(-1); - handled = true; - } - break; - - case SDLK_F5: { - if (g_base->ui->PartyIconVisible()) { - g_base->ui->ActivatePartyIcon(); - } - handled = true; - break; - } - - case SDLK_F7: - assert(g_base->logic->event_loop()); - g_base->logic->event_loop()->PushCall( - [] { g_base->graphics->ToggleManualCamera(); }); - handled = true; - break; - - case SDLK_F8: - assert(g_base->logic->event_loop()); - g_base->logic->event_loop()->PushCall( - [] { g_base->graphics->ToggleNetworkDebugDisplay(); }); - handled = true; - break; - - case SDLK_F9: - g_base->python->objs().PushCall( - BasePython::ObjID::kLanguageTestToggleCall); - handled = true; - break; - - case SDLK_F10: - assert(g_base->logic->event_loop()); - g_base->logic->event_loop()->PushCall( - [] { g_base->graphics->ToggleDebugDraw(); }); - handled = true; - break; - - case SDLK_ESCAPE: - - if (!g_base->ui->MainMenuVisible()) { - // There's no main menu up. Ask for one. - - // Note: keyboard_input_ may be nullptr but escape key should - // still function for menus; it just won't claim ownership. - g_base->ui->PushMainMenuPressCall(keyboard_input_); - } else { - // Ok there *is* a main menu up. Send it a cancel message. - g_base->ui->SendWidgetMessage( - WidgetMessage(WidgetMessage::Type::kCancel)); - } - handled = true; - break; - - default: - break; + handled = true; + break; } + + case SDLK_EQUALS: + case SDLK_PLUS: + if (keysym.mod & KMOD_CTRL) { + g_base->app_mode()->ChangeGameSpeed(1); + handled = true; + } + break; + + case SDLK_MINUS: + if (keysym.mod & KMOD_CTRL) { + g_base->app_mode()->ChangeGameSpeed(-1); + handled = true; + } + break; + + case SDLK_F5: { + if (g_base->ui->PartyIconVisible()) { + g_base->ui->ActivatePartyIcon(); + } + handled = true; + break; + } + + case SDLK_F7: + assert(g_base->logic->event_loop()); + g_base->logic->event_loop()->PushCall( + [] { g_base->graphics->ToggleManualCamera(); }); + handled = true; + break; + + case SDLK_F8: + assert(g_base->logic->event_loop()); + g_base->logic->event_loop()->PushCall( + [] { g_base->graphics->ToggleNetworkDebugDisplay(); }); + handled = true; + break; + + case SDLK_F9: + g_base->python->objs().PushCall( + BasePython::ObjID::kLanguageTestToggleCall); + handled = true; + break; + + case SDLK_F10: + assert(g_base->logic->event_loop()); + g_base->logic->event_loop()->PushCall( + [] { g_base->graphics->ToggleDebugDraw(); }); + handled = true; + break; + + case SDLK_ESCAPE: + if (!g_base->ui->MainMenuVisible()) { + // There's no main menu up. Ask for one. + + // Note: keyboard_input_ may be nullptr but escape key should + // still function for menus; it just won't claim ownership. + g_base->ui->PushMainMenuPressCall(keyboard_input_); + } else { + // Ok there *is* a main menu up. Send it a cancel message. + g_base->ui->SendWidgetMessage( + WidgetMessage(WidgetMessage::Type::kCancel)); + } + handled = true; + break; + + default: + break; } - // If we haven't claimed it, pass it along as potential player/widget input. + // If we haven't handled this, pass it along as potential player/widget input. if (!handled) { if (keyboard_input_) { - keyboard_input_->HandleKey(&keysym, repeat_press, true); + keyboard_input_->HandleKey(&keysym, true); } } } @@ -1184,7 +1165,7 @@ void Input::HandleKeyRelease_(const SDL_Keysym& keysym) { // In some cases we may receive duplicate key-release events (if a // keyboard reset was run, it deals out key releases, but then the // keyboard driver issues them as well). - if (keys_held_.count(keysym.sym) == 0) { + if (keys_held_.find(keysym.sym) == keys_held_.end()) { return; } @@ -1205,7 +1186,7 @@ void Input::HandleKeyRelease_(const SDL_Keysym& keysym) { } if (keyboard_input_) { - keyboard_input_->HandleKey(&keysym, false, false); + keyboard_input_->HandleKey(&keysym, false); } } diff --git a/src/ballistica/base/input/input.h b/src/ballistica/base/input/input.h index e256a2ff..aa404385 100644 --- a/src/ballistica/base/input/input.h +++ b/src/ballistica/base/input/input.h @@ -114,10 +114,9 @@ class Input { // something. auto HaveControllerWithPlayer() -> bool; auto HaveRemoteAppController() -> bool; - void ProcessStressTesting(int player_count); auto keyboard_input() const -> KeyboardInput* { return keyboard_input_; } auto keyboard_input_2() const -> KeyboardInput* { return keyboard_input_2_; } - void CreateTouchInput(); + // void CreateTouchInput(); void PushTextInputEvent(const std::string& text); void PushKeyPressEventSimple(int keycode); @@ -205,7 +204,6 @@ class Input { std::list input_lock_permanent_labels_; std::list input_unlock_permanent_labels_; std::list recent_input_locks_unlocks_; - std::list test_inputs_; std::list newly_connected_controllers_; std::list newly_disconnected_controllers_; std::unordered_map > @@ -214,8 +212,6 @@ class Input { std::set keys_held_; millisecs_t last_input_device_count_update_time_{}; millisecs_t last_input_temp_lock_time_{}; - millisecs_t stress_test_time_{}; - millisecs_t stress_test_last_leave_time_{}; void* single_touch_{}; KeyboardInput* keyboard_input_{}; KeyboardInput* keyboard_input_2_{}; diff --git a/src/ballistica/base/input/support/remote_app_server.cc b/src/ballistica/base/input/support/remote_app_server.cc index 1ab678df..d5f78b95 100644 --- a/src/ballistica/base/input/support/remote_app_server.cc +++ b/src/ballistica/base/input/support/remote_app_server.cc @@ -5,6 +5,7 @@ #include "ballistica/base/assets/assets.h" #include "ballistica/base/audio/audio.h" #include "ballistica/base/graphics/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/networking/network_reader.h" @@ -162,7 +163,8 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, g_base->assets->GetResourceString("controllerDisconnectedText"); Utils::StringReplaceOne(&s, "${CONTROLLER}", m); g_base->logic->event_loop()->PushCall([s] { - g_base->graphics->AddScreenMessage(s, Vector3f(1, 1, 1)); + g_base->graphics->screenmessages->AddScreenMessage( + s, Vector3f(1, 1, 1)); }); g_base->logic->event_loop()->PushCall([] { g_base->audio->PlaySound( @@ -369,8 +371,10 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, std::string s = g_base->assets->GetResourceString("controllerReconnectedText"); Utils::StringReplaceOne(&s, "${CONTROLLER}", m); - g_base->logic->event_loop()->PushCall( - [s] { g_base->graphics->AddScreenMessage(s, Vector3f(1, 1, 1)); }); + g_base->logic->event_loop()->PushCall([s] { + g_base->graphics->screenmessages->AddScreenMessage(s, + Vector3f(1, 1, 1)); + }); g_base->logic->event_loop()->PushCall([] { g_base->audio->PlaySound( g_base->assets->SysSound(SysSoundID::kGunCock)); @@ -416,8 +420,10 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, std::string s = g_base->assets->GetResourceString("controllerConnectedText"); Utils::StringReplaceOne(&s, "${CONTROLLER}", m); - g_base->logic->event_loop()->PushCall( - [s] { g_base->graphics->AddScreenMessage(s, Vector3f(1, 1, 1)); }); + g_base->logic->event_loop()->PushCall([s] { + g_base->graphics->screenmessages->AddScreenMessage(s, + Vector3f(1, 1, 1)); + }); g_base->logic->event_loop()->PushCall([] { g_base->audio->PlaySound( diff --git a/src/ballistica/base/logic/logic.cc b/src/ballistica/base/logic/logic.cc index 67a05cf2..909a8ddd 100644 --- a/src/ballistica/base/logic/logic.cc +++ b/src/ballistica/base/logic/logic.cc @@ -99,8 +99,8 @@ void Logic::OnGraphicsReady() { // Anyone dealing in display-time should be able to handle a wide // variety of rates anyway. NOTE: This length is currently milliseconds. headless_display_time_step_timer_ = event_loop()->NewTimer( - kHeadlessMinDisplayTimeStep / 1000, true, - NewLambdaRunnable([this] { StepDisplayTime_(); })); + kHeadlessMinDisplayTimeStep, true, + NewLambdaRunnable([this] { StepDisplayTime_(); }).Get()); } else { // In gui mode, push an initial frame to the graphics server. From this // point it will be self-sustaining, sending us a frame request each @@ -133,9 +133,10 @@ void Logic::CompleteAppBootstrapping_() { // Set up our timers. process_pending_work_timer_ = event_loop()->NewTimer( - 0, true, NewLambdaRunnable([this] { ProcessPendingWork_(); })); - asset_prune_timer_ = event_loop()->NewTimer( - 2345, true, NewLambdaRunnable([] { g_base->assets->Prune(); })); + 0, true, NewLambdaRunnable([this] { ProcessPendingWork_(); }).Get()); + // asset_prune_timer_ = event_loop()->NewTimer( + // 2345 * 1000, true, NewLambdaRunnable([] { g_base->assets->Prune(); + // }).Get()); // Let our initial dummy app-mode know it has become active. g_base->app_mode()->OnActivate(); @@ -381,9 +382,7 @@ void Logic::OnAppModeChanged() { "Resetting headless display step timer due to app-mode change."); } assert(headless_display_time_step_timer_); - // NOTE: This is currently milliseconds. - headless_display_time_step_timer_->SetLength(kHeadlessMinDisplayTimeStep - / 1000); + headless_display_time_step_timer_->SetLength(kHeadlessMinDisplayTimeStep); } } @@ -439,8 +438,8 @@ void Logic::PostUpdateDisplayTimeForHeadlessMode_() { Log(LogLevel::kDebug, buffer); } - auto sleep_millisecs = headless_display_step_microsecs / 1000; - headless_display_time_step_timer_->SetLength(sleep_millisecs); + auto sleep_microsecs = headless_display_step_microsecs; + headless_display_time_step_timer_->SetLength(sleep_microsecs); } void Logic::UpdateDisplayTimeForFrameDraw_() { @@ -581,7 +580,7 @@ void Logic::UpdatePendingWorkTimer_() { // If there's loading to do, keep at it rather vigorously. if (have_pending_loads_) { assert(process_pending_work_timer_); - process_pending_work_timer_->SetLength(1); + process_pending_work_timer_->SetLength(1 * 1000); } else { // Otherwise we've got nothing to do; go to sleep until something // changes. @@ -635,8 +634,8 @@ void Logic::NotifyOfPendingAssetLoads() { UpdatePendingWorkTimer_(); } -auto Logic::NewAppTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int { +auto Logic::NewAppTimer(microsecs_t length, bool repeat, Runnable* runnable) + -> int { // App-Timers simply get injected into our loop and run alongside our own // stuff. assert(g_base->InLogicThread()); @@ -649,7 +648,7 @@ void Logic::DeleteAppTimer(int timer_id) { event_loop()->DeleteTimer(timer_id); } -void Logic::SetAppTimerLength(int timer_id, millisecs_t length) { +void Logic::SetAppTimerLength(int timer_id, microsecs_t length) { assert(g_base->InLogicThread()); Timer* t = event_loop()->GetTimer(timer_id); if (t) { @@ -660,14 +659,14 @@ void Logic::SetAppTimerLength(int timer_id, millisecs_t length) { } } -auto Logic::NewDisplayTimer(microsecs_t length, bool repeat, - const Object::Ref& runnable) -> int { +auto Logic::NewDisplayTimer(microsecs_t length, bool repeat, Runnable* runnable) + -> int { // Display-Timers go into a timer-list that we exec explicitly when we // step display-time. assert(g_base->InLogicThread()); int offset = 0; - Timer* t = display_timers_->NewTimer(g_core->GetAppTimeMicrosecs(), length, - offset, repeat ? -1 : 0, runnable); + Timer* t = display_timers_->NewTimer(display_time_microsecs_, length, offset, + repeat ? -1 : 0, runnable); return t->id(); } diff --git a/src/ballistica/base/logic/logic.h b/src/ballistica/base/logic/logic.h index b215be32..2e21436e 100644 --- a/src/ballistica/base/logic/logic.h +++ b/src/ballistica/base/logic/logic.h @@ -89,13 +89,12 @@ class Logic { void HandleInterruptSignal(); void HandleTerminateSignal(); - auto NewAppTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int; + auto NewAppTimer(microsecs_t length, bool repeat, Runnable* runnable) -> int; void DeleteAppTimer(int timer_id); - void SetAppTimerLength(int timer_id, millisecs_t length); + void SetAppTimerLength(int timer_id, microsecs_t length); - auto NewDisplayTimer(microsecs_t length, bool repeat, - const Object::Ref& runnable) -> int; + auto NewDisplayTimer(microsecs_t length, bool repeat, Runnable* runnable) + -> int; void DeleteDisplayTimer(int timer_id); void SetDisplayTimerLength(int timer_id, microsecs_t length); @@ -149,8 +148,6 @@ class Logic { bool shutdown_completed_ : 1 {}; bool graphics_ready_ : 1 {}; Timer* process_pending_work_timer_{}; - Timer* asset_prune_timer_{}; - Timer* debug_timer_{}; EventLoop* event_loop_{}; std::unique_ptr display_timers_; }; diff --git a/src/ballistica/base/platform/apple/base_platform_apple.cc b/src/ballistica/base/platform/apple/base_platform_apple.cc index 55226812..66227ce8 100644 --- a/src/ballistica/base/platform/apple/base_platform_apple.cc +++ b/src/ballistica/base/platform/apple/base_platform_apple.cc @@ -4,11 +4,14 @@ #include "ballistica/base/platform/apple/base_platform_apple.h" #if BA_XCODE_BUILD -#include +#include "ballistica/base/platform/apple/apple_utils.h" +#include "ballistica/base/platform/apple/from_swift.h" #endif #if BA_XCODE_BUILD -#include "ballistica/base/platform/apple/apple_utils.h" +// This needs to be below ballistica headers since it relies on +// some types in them but does not include headers itself. +#include #endif namespace ballistica::base { @@ -16,13 +19,14 @@ namespace ballistica::base { BasePlatformApple::BasePlatformApple() { // On iOS, keep the device from falling asleep in our app #if BA_OSTYPE_IOS_TVOS - AppleUtils::DisableIdleTimer(); + // AppleUtils::DisableIdleTimer(); #endif } void BasePlatformApple::DoPurchase(const std::string& item) { #if BA_USE_STORE_KIT - AppleUtils::DoStoreKitPurchase(item); + BallisticaKit::StoreKitContext::purchase(item); + // AppleUtils::DoStoreKitPurchase(item); #else BasePlatform::DoPurchase(item); #endif @@ -30,7 +34,8 @@ void BasePlatformApple::DoPurchase(const std::string& item) { void BasePlatformApple::RestorePurchases() { #if BA_USE_STORE_KIT - AppleUtils::DoStoreKitPurchaseRestore(); + BallisticaKit::StoreKitContext::restorePurchases(); + // AppleUtils::DoStoreKitPurchaseRestore(); #else BasePlatform::RestorePurchases(); #endif @@ -38,8 +43,9 @@ void BasePlatformApple::RestorePurchases() { void BasePlatformApple::PurchaseAck(const std::string& purchase, const std::string& order_id) { -#if BA_XCODE_BUILD - AppleUtils::PurchaseAck(purchase, order_id); +#if BA_USE_STORE_KIT + BallisticaKit::StoreKitContext::purchaseAck(purchase, order_id); + // AppleUtils::PurchaseAck(purchase, order_id); #else BasePlatform::PurchaseAck(purchase, order_id); #endif @@ -48,9 +54,9 @@ void BasePlatformApple::PurchaseAck(const std::string& purchase, void BasePlatformApple::DoOpenURL(const std::string& url) { #if BA_XCODE_BUILD #if BA_OSTYPE_MACOS - BallisticaKit::CocoaFromCppOpenURL(url); + BallisticaKit::CocoaFromCpp::openURL(url); #else - BallisticaKit::UIKitFromCppOpenURL(url); + BallisticaKit::UIKitFromCpp::openURL(url); #endif // BA_OSTYPE_MACOS #else @@ -59,6 +65,34 @@ void BasePlatformApple::DoOpenURL(const std::string& url) { #endif // BA_XCODE_BUILD } +void BasePlatformApple::LoginAdapterGetSignInToken( + const std::string& login_type, int attempt_id) { +#if BA_USE_GAME_CENTER + if (login_type == "game_center") { + BallisticaKit::GameCenterContext::getSignInToken(attempt_id); + } else { + Log(LogLevel::kError, + "Got unexpected get-sign-in-token login-type: " + login_type); + } +#else + BasePlatform::LoginAdapterGetSignInToken(login_type, attempt_id); +#endif +} + +void BasePlatformApple::LoginAdapterBackEndActiveChange( + const std::string& login_type, bool active) { +#if BA_USE_GAME_CENTER + if (login_type == "game_center") { + BallisticaKit::GameCenterContext::backEndActiveChange(active); + } else { + Log(LogLevel::kError, + "Got unexpected back-end-active-change login-type: " + login_type); + } +#else + BasePlatform::LoginAdapterBackEndActiveChange(login_type, active); +#endif +} + } // namespace ballistica::base #endif // BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS diff --git a/src/ballistica/base/platform/apple/base_platform_apple.h b/src/ballistica/base/platform/apple/base_platform_apple.h index d0fbaad8..9ccc5bb1 100644 --- a/src/ballistica/base/platform/apple/base_platform_apple.h +++ b/src/ballistica/base/platform/apple/base_platform_apple.h @@ -16,6 +16,10 @@ class BasePlatformApple : public BasePlatform { void PurchaseAck(const std::string& purchase, const std::string& order_id) override; void DoOpenURL(const std::string& url) override; + void LoginAdapterGetSignInToken(const std::string& login_type, + int attempt_id) override; + void LoginAdapterBackEndActiveChange(const std::string& login_type, + bool active) override; }; } // namespace ballistica::base diff --git a/src/ballistica/base/platform/base_platform.cc b/src/ballistica/base/platform/base_platform.cc index 9be958c2..d6a2cfff 100644 --- a/src/ballistica/base/platform/base_platform.cc +++ b/src/ballistica/base/platform/base_platform.cc @@ -7,7 +7,6 @@ #include "ballistica/base/base.h" #include "ballistica/base/input/input.h" #include "ballistica/base/logic/logic.h" -#include "ballistica/base/platform/support/min_sdl_key_names.h" #include "ballistica/base/python/base_python.h" #include "ballistica/base/ui/ui.h" #include "ballistica/core/core.h" @@ -16,73 +15,8 @@ #include "ballistica/shared/python/python.h" #include "ballistica/shared/python/python_sys.h" -// ------------------------- PLATFORM SELECTION -------------------------------- - -// This ugly chunk of macros simply pulls in the correct platform class header -// for each platform and defines the actual class g_base->platform will be. - -// Android --------------------------------------------------------------------- - -#if BA_OSTYPE_ANDROID -#if BA_GOOGLE_BUILD -#include "ballistica/base/platform/android/google/base_plat_andr_google.h" -#define BA_PLATFORM_CLASS BasePlatformAndroidGoogle -#elif BA_AMAZON_BUILD -#include "ballistica/base/platform/android/amazon/base_plat_andr_amazon.h" -#define BA_PLATFORM_CLASS BasePlatformAndroidAmazon -#elif BA_CARDBOARD_BUILD -#include "ballistica/base/platform/android/cardboard/base_pl_an_cardboard.h" -#define BA_PLATFORM_CLASS BasePlatformAndroidCardboard -#else // Generic android. -#include "ballistica/base/platform/android/base_platform_android.h" -#define BA_PLATFORM_CLASS BasePlatformAndroid -#endif // (Android subplatform) - -// Apple ----------------------------------------------------------------------- - -#elif BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS -#include "ballistica/base/platform/apple/base_platform_apple.h" -#define BA_PLATFORM_CLASS BasePlatformApple - -// Windows --------------------------------------------------------------------- - -#elif BA_OSTYPE_WINDOWS -#if BA_RIFT_BUILD -#include "ballistica/base/platform/windows/base_platform_windows_oculus.h" -#define BA_PLATFORM_CLASS BasePlatformWindowsOculus -#else // generic windows -#include "ballistica/base/platform/windows/base_platform_windows.h" -#define BA_PLATFORM_CLASS BasePlatformWindows -#endif // windows subtype - -// Linux ----------------------------------------------------------------------- - -#elif BA_OSTYPE_LINUX -#include "ballistica/base/platform/linux/base_platform_linux.h" -#define BA_PLATFORM_CLASS BasePlatformLinux -#else - -// Generic --------------------------------------------------------------------- - -#define BA_PLATFORM_CLASS BasePlatform - -#endif - -// ----------------------- END PLATFORM SELECTION ------------------------------ - -#ifndef BA_PLATFORM_CLASS -#error no BA_PLATFORM_CLASS defined for this platform -#endif - namespace ballistica::base { -auto BasePlatform::Create() -> BasePlatform* { - auto platform = new BA_PLATFORM_CLASS(); - platform->PostInit(); - assert(platform->ran_base_post_init_); - return platform; -} - BasePlatform::BasePlatform() = default; void BasePlatform::PostInit() { @@ -92,20 +26,6 @@ void BasePlatform::PostInit() { BasePlatform::~BasePlatform() = default; -auto BasePlatform::GetKeyName(int keycode) -> std::string { - // On our actual SDL platforms we're trying to be *pure* sdl so - // call their function for this. Otherwise we call our own version - // of it which is basically the same thing (at least for now). -#if BA_MINSDL_BUILD - return MinSDL_GetKeyName(keycode); -#elif BA_SDL_BUILD - return SDL_GetKeyName(static_cast(keycode)); -#else - Log(LogLevel::kWarn, "CorePlatform::GetKeyName not implemented here."); - return "?"; -#endif -} - void BasePlatform::LoginAdapterGetSignInToken(const std::string& login_type, int attempt_id) { // Default implementation simply calls completion callback immediately. diff --git a/src/ballistica/base/platform/base_platform.h b/src/ballistica/base/platform/base_platform.h index 82f38679..6470074a 100644 --- a/src/ballistica/base/platform/base_platform.h +++ b/src/ballistica/base/platform/base_platform.h @@ -15,8 +15,12 @@ namespace ballistica::base { /// with a single platform (Windows, Mac, etc.). class BasePlatform { public: - /// Instantiate the CorePlatform subclass for the current build. - static auto Create() -> BasePlatform*; + BasePlatform(); + + /// Called after our singleton has been instantiated. Any construction + /// functionality requiring virtual functions resolving to their final + /// class versions can go here. + virtual void PostInit(); #pragma mark APP EVENTS / LIFECYCLE -------------------------------------------- @@ -57,20 +61,18 @@ class BasePlatform { /// Called when the app should set itself up to intercept ctrl-c presses. virtual void SetupInterruptHandling(); -#pragma mark INPUT DEVICES ----------------------------------------------------- - - // Return a name for a ballistica keycode. - virtual auto GetKeyName(int keycode) -> std::string; - #pragma mark ACCOUNTS ---------------------------------------------------------- /// Called when a Python LoginAdapter is requesting an explicit sign-in. + /// See the LoginAdapter class in Python for usage details. virtual void LoginAdapterGetSignInToken(const std::string& login_type, int attempt_id); /// Called when a Python LoginAdapter is informing us that a back-end is - /// active/inactive. + /// active/inactive. See the LoginAdapter class in Python for usage + /// details. virtual void LoginAdapterBackEndActiveChange(const std::string& login_type, bool active); + #pragma mark MISC -------------------------------------------------------------- /// Do we define a platform-specific string editor? This is something like @@ -94,6 +96,8 @@ class BasePlatform { /// Must be called in the logic thread. void StringEditorCancel(); + auto ran_base_post_init() const { return ran_base_post_init_; } + protected: /// Pop up a text edit dialog. virtual void DoInvokeStringEditor(const std::string& title, @@ -106,17 +110,11 @@ class BasePlatform { /// Make a purchase. virtual void DoPurchase(const std::string& item); - BasePlatform(); virtual ~BasePlatform(); private: - /// Called after our singleton has been instantiated. Any construction - /// functionality requiring virtual functions resolving to their final - /// class versions can go here. - virtual void PostInit(); - + bool ran_base_post_init_ : 1 {}; PythonRef string_edit_adapter_{}; - bool ran_base_post_init_{}; std::string public_device_uuid_; }; diff --git a/src/ballistica/base/platform/support/min_sdl_key_names.h b/src/ballistica/base/platform/support/min_sdl_key_names.h index f26912c8..57eae886 100644 --- a/src/ballistica/base/platform/support/min_sdl_key_names.h +++ b/src/ballistica/base/platform/support/min_sdl_key_names.h @@ -367,6 +367,22 @@ auto MinSDL_GetKeyName(int keycode) -> std::string { static char name[8]; char* end; + // Handle a few specially per platform. + if (g_buildconfig.ostype_macos()) { + switch (key) { + case SDLK_LGUI: + return "Left Command"; + case SDLK_RGUI: + return "Right Command"; + case SDLK_LALT: + return "Left Option"; + case SDLK_RALT: + return "Right Option"; + default: + break; + } + } + if (key & SDLK_SCANCODE_MASK) { return GetScancodeName((SDL_Scancode)(key & ~SDLK_SCANCODE_MASK)); } diff --git a/src/ballistica/base/python/base_python.h b/src/ballistica/base/python/base_python.h index dc31d93f..5646cabe 100644 --- a/src/ballistica/base/python/base_python.h +++ b/src/ballistica/base/python/base_python.h @@ -48,6 +48,7 @@ class BasePython { kTemporarilyUnavailableMessageCall, kInProgressMessageCall, kErrorMessageCall, + kSuccessMessageCall, kPurchaseNotValidErrorCall, kPurchaseAlreadyInProgressErrorCall, kVROrientationResetCBMessageCall, @@ -111,6 +112,7 @@ class BasePython { kGetDevConsoleTabNamesCall, kAppDevConsoleDoRefreshTabCall, kUnsupportedControllerMessageCall, + kGetV2AccountIdCall, kLast // Sentinel; must be at end. }; diff --git a/src/ballistica/base/python/class/python_class_app_timer.cc b/src/ballistica/base/python/class/python_class_app_timer.cc index dd75823f..1ef19306 100644 --- a/src/ballistica/base/python/class/python_class_app_timer.cc +++ b/src/ballistica/base/python/class/python_class_app_timer.cc @@ -94,8 +94,8 @@ auto PythonClassAppTimer::tp_new(PyTypeObject* type, PyObject* args, auto runnable(Object::New(call_obj)); self->timer_id_ = g_base->logic->NewAppTimer( - static_cast(length * 1000.0), repeat, - Object::New(call_obj)); + static_cast(length * 1000000.0), repeat, + Object::New(call_obj).Get()); self->have_timer_ = true; diff --git a/src/ballistica/base/python/class/python_class_display_timer.cc b/src/ballistica/base/python/class/python_class_display_timer.cc index eb9330bf..d95d7918 100644 --- a/src/ballistica/base/python/class/python_class_display_timer.cc +++ b/src/ballistica/base/python/class/python_class_display_timer.cc @@ -104,7 +104,7 @@ auto PythonClassDisplayTimer::tp_new(PyTypeObject* type, PyObject* args, } self->timer_id_ = g_base->logic->NewDisplayTimer( static_cast(length * 1000000.0), repeat, - Object::New(call_obj)); + Object::New(call_obj).Get()); self->have_timer_ = true; diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc index befd17d3..2d13c813 100644 --- a/src/ballistica/base/python/methods/python_methods_app.cc +++ b/src/ballistica/base/python/methods/python_methods_app.cc @@ -9,7 +9,6 @@ #include "ballistica/base/logic/logic.h" #include "ballistica/base/python/base_python.h" #include "ballistica/base/python/support/python_context_call_runnable.h" -#include "ballistica/base/support/stress_test.h" #include "ballistica/base/ui/dev_console.h" #include "ballistica/base/ui/ui.h" #include "ballistica/shared/foundation/event_loop.h" @@ -365,8 +364,8 @@ static auto PyAppTimer(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("Timer length cannot be < 0.", PyExcType::kValue); } g_base->logic->NewAppTimer( - static_cast(length * 1000.0), false, - Object::New(call_obj)); + static_cast(length * 1000000.0), false, + Object::New(call_obj).Get()); Py_RETURN_NONE; BA_PYTHON_CATCH; } @@ -459,7 +458,7 @@ static auto PyDisplayTimer(PyObject* self, PyObject* args, PyObject* keywds) } g_base->logic->NewDisplayTimer( static_cast(length * 1000000.0), false, - Object::New(call_obj)); + Object::New(call_obj).Get()); Py_RETURN_NONE; BA_PYTHON_CATCH; } @@ -602,7 +601,7 @@ static auto PyCommitConfig(PyObject* self, PyObject* args, PyObject* keywds) } fclose(f_out); - // Now backup any existing config to .prev. + // Now move any existing config to .prev. if (g_core->platform->FilePathExists(path)) { // On windows, rename doesn't overwrite existing files.. need to kill // the old explicitly. @@ -784,32 +783,6 @@ static PyMethodDef PyEnvDef = { "This info is now exposed through babase.App; refer to those docs for\n" "info on specific elements."}; -// -------------------------- set_stress_testing ------------------------------- - -static auto PySetStressTesting(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - int enable; - int player_count; - if (!PyArg_ParseTuple(args, "pi", &enable, &player_count)) { - return nullptr; - } - g_base->app_adapter->PushMainThreadCall([enable, player_count] { - g_base->stress_test()->Set(enable, player_count); - }); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PySetStressTestingDef = { - "set_stress_testing", // name - PySetStressTesting, // method - METH_VARARGS, // flags - - "set_stress_testing(testing: bool, player_count: int) -> None\n" - "\n" - "(internal)", -}; - // -------------------------------- emit_log ----------------------------------- static auto PyEmitLog(PyObject* self, PyObject* args, PyObject* keywds) @@ -1115,25 +1088,6 @@ static PyMethodDef PyMacMusicAppSetVolumeDef = { "(internal)", }; -// ------------------------ mac_music_app_get_library -------------------------- - -static auto PyMacMusicAppGetLibrarySource(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - g_core->platform->MacMusicAppGetLibrarySource(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -static PyMethodDef PyMacMusicAppGetLibrarySourceDef = { - "mac_music_app_get_library_source", // name - (PyCFunction)PyMacMusicAppGetLibrarySource, // method - METH_VARARGS | METH_KEYWORDS, // flags - - "mac_music_app_get_library_source() -> None\n" - "\n" - "(internal)"}; - // --------------------------- mac_music_app_stop ------------------------------ static auto PyMacMusicAppStop(PyObject* self, PyObject* args, PyObject* keywds) @@ -1645,6 +1599,52 @@ static PyMethodDef PyAudioShutdownIsCompleteDef = { "(internal)\n", }; +// ----------------------- graphics_shutdown_begin ----------------------------- + +static auto PyGraphicsShutdownBegin(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + + g_base->app_adapter->PushGraphicsContextCall( + [] { g_base->graphics_server->Shutdown(); }); + + Py_RETURN_NONE; + + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGraphicsShutdownBeginDef = { + "graphics_shutdown_begin", // name + (PyCFunction)PyGraphicsShutdownBegin, // method + METH_NOARGS, // flags + + "graphics_shutdown_begin() -> None\n" + "\n" + "(internal)\n", +}; + +// -------------------- graphics_shutdown_is_complete -------------------------- + +static auto PyGraphicsShutdownIsComplete(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + + if (g_base->graphics_server->shutdown_completed()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGraphicsShutdownIsCompleteDef = { + "graphics_shutdown_is_complete", // name + (PyCFunction)PyGraphicsShutdownIsComplete, // method + METH_NOARGS, // flags + + "graphics_shutdown_is_complete() -> bool\n" + "\n" + "(internal)\n", +}; + // ----------------------------------------------------------------------------- auto PythonMethodsApp::GetMethods() -> std::vector { @@ -1656,7 +1656,6 @@ auto PythonMethodsApp::GetMethods() -> std::vector { PyCanDisplayFullUnicodeDef, PyEmitLogDef, PyV1CloudLogDef, - PySetStressTestingDef, PyEnvDef, PyPreEnvDef, PyCommitConfigDef, @@ -1677,7 +1676,6 @@ auto PythonMethodsApp::GetMethods() -> std::vector { PyMacMusicAppInitDef, PyMacMusicAppGetVolumeDef, PyMacMusicAppSetVolumeDef, - PyMacMusicAppGetLibrarySourceDef, PyMacMusicAppStopDef, PyMacMusicAppPlayPlaylistDef, PyMacMusicAppGetPlaylistsDef, @@ -1702,6 +1700,8 @@ auto PythonMethodsApp::GetMethods() -> std::vector { PyDevConsoleInputAdapterFinishDef, PyAudioShutdownBeginDef, PyAudioShutdownIsCompleteDef, + PyGraphicsShutdownBeginDef, + PyGraphicsShutdownIsCompleteDef, }; } diff --git a/src/ballistica/base/python/methods/python_methods_graphics.cc b/src/ballistica/base/python/methods/python_methods_graphics.cc index 63eac46f..d2ab871f 100644 --- a/src/ballistica/base/python/methods/python_methods_graphics.cc +++ b/src/ballistica/base/python/methods/python_methods_graphics.cc @@ -6,6 +6,7 @@ #include "ballistica/base/assets/assets.h" #include "ballistica/base/graphics/graphics.h" #include "ballistica/base/graphics/support/camera.h" +#include "ballistica/base/graphics/support/screen_messages.h" #include "ballistica/base/graphics/text/text_graphics.h" #include "ballistica/base/logic/logic.h" #include "ballistica/base/python/base_python.h" @@ -74,7 +75,7 @@ static auto PyScreenMessage(PyObject* self, PyObject* args, PyObject* keywds) } // This version simply displays it locally. - g_base->graphics->AddScreenMessage(message_str, color); + g_base->graphics->screenmessages->AddScreenMessage(message_str, color); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -536,6 +537,7 @@ static auto PyFadeScreen(PyObject* self, PyObject* args, PyObject* keywds) &endcall)) { return nullptr; } + BA_PRECONDITION(g_base->InLogicThread()); g_base->graphics->FadeScreen(static_cast(fade), static_cast(1000.0f * time), endcall); Py_RETURN_NONE; diff --git a/src/ballistica/base/python/methods/python_methods_misc.cc b/src/ballistica/base/python/methods/python_methods_misc.cc index b7d1f2a9..bf4783e2 100644 --- a/src/ballistica/base/python/methods/python_methods_misc.cc +++ b/src/ballistica/base/python/methods/python_methods_misc.cc @@ -5,6 +5,7 @@ #include #include +#include "ballistica/base/app_adapter/app_adapter.h" #include "ballistica/base/assets/sound_asset.h" #include "ballistica/base/input/input.h" #include "ballistica/base/platform/base_platform.h" @@ -128,7 +129,7 @@ static PyMethodDef PyHasTouchScreenDef = { static auto PyClipboardIsSupported(PyObject* self) -> PyObject* { BA_PYTHON_TRY; - if (g_core->platform->ClipboardIsSupported()) { + if (g_base->app_adapter->ClipboardIsSupported()) { Py_RETURN_TRUE; } Py_RETURN_FALSE; @@ -154,7 +155,7 @@ static PyMethodDef PyClipboardIsSupportedDef = { static auto PyClipboardHasText(PyObject* self) -> PyObject* { BA_PYTHON_TRY; - if (g_core->platform->ClipboardHasText()) { + if (g_base->app_adapter->ClipboardHasText()) { Py_RETURN_TRUE; } Py_RETURN_FALSE; @@ -187,7 +188,7 @@ static auto PyClipboardSetText(PyObject* self, PyObject* args, PyObject* keywds) const_cast(kwlist), &value)) { return nullptr; } - g_core->platform->ClipboardSetText(value); + g_base->app_adapter->ClipboardSetText(value); Py_RETURN_NONE; BA_PYTHON_CATCH; } @@ -211,7 +212,7 @@ static PyMethodDef PyClipboardSetTextDef = { static auto PyClipboardGetText(PyObject* self) -> PyObject* { BA_PYTHON_TRY; - return PyUnicode_FromString(g_core->platform->ClipboardGetText().c_str()); + return PyUnicode_FromString(g_base->app_adapter->ClipboardGetText().c_str()); Py_RETURN_FALSE; BA_PYTHON_CATCH; } @@ -1681,6 +1682,87 @@ static PyMethodDef PyAssetLoadsAllowedDef = { "(internal)", }; +// -------------------- using_google_play_game_services ------------------------ + +static auto PyUsingGooglePlayGameServices(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + if (g_buildconfig.use_google_play_game_services()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyUsingGooglePlayGameServicesDef = { + "using_google_play_game_services", // name + (PyCFunction)PyUsingGooglePlayGameServices, // method + METH_NOARGS, // flags + + "using_google_play_game_services() -> bool\n" + "\n" + "(internal)", +}; + +// ---------------------------- using_game_center ------------------------------ + +static auto PyUsingGameCenter(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + if (g_buildconfig.use_game_center()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyUsingGameCenterDef = { + "using_game_center", // name + (PyCFunction)PyUsingGameCenter, // method + METH_NOARGS, // flags + + "using_game_center() -> bool\n" + "\n" + "(internal)", +}; + +// --------------------- native_review_request_supported ----------------------- + +static auto PyNativeReviewRequestSupported(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + if (g_base->app_adapter->NativeReviewRequestSupported()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyNativeReviewRequestSupportedDef = { + "native_review_request_supported", // name + (PyCFunction)PyNativeReviewRequestSupported, // method + METH_NOARGS, // flags + + "native_review_request_supported() -> bool\n" + "\n" + "(internal)", +}; + +// -------------------------- native_review_request ---------------------------- + +static auto PyNativeReviewRequest(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + g_base->app_adapter->NativeReviewRequest(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyNativeReviewRequestDef = { + "native_review_request", // name + (PyCFunction)PyNativeReviewRequest, // method + METH_NOARGS, // flags + + "native_review_request() -> None\n" + "\n" + "(internal)", +}; // ----------------------------------------------------------------------------- auto PythonMethodsMisc::GetMethods() -> std::vector { @@ -1744,6 +1826,10 @@ auto PythonMethodsMisc::GetMethods() -> std::vector { PyDevConsoleBaseScaleDef, PyDevConsoleRequestRefreshDef, PyAssetLoadsAllowedDef, + PyUsingGooglePlayGameServicesDef, + PyUsingGameCenterDef, + PyNativeReviewRequestSupportedDef, + PyNativeReviewRequestDef, }; } diff --git a/src/ballistica/base/support/app_timer.h b/src/ballistica/base/support/app_timer.h index e116c26a..6289ab75 100644 --- a/src/ballistica/base/support/app_timer.h +++ b/src/ballistica/base/support/app_timer.h @@ -13,16 +13,24 @@ namespace ballistica::base { class AppTimer : public Object { public: - AppTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) { + AppTimer(seconds_t length, bool repeat, Runnable* runnable) { assert(g_base->InLogicThread()); - timer_id_ = base::g_base->logic->NewAppTimer(length, repeat, runnable); + timer_id_ = base::g_base->logic->NewAppTimer( + static_cast(length * 1000000.0), repeat, runnable); } - void SetLength(millisecs_t length) { - assert(g_base->InLogicThread()); - base::g_base->logic->SetAppTimerLength(timer_id_, length); + template + static auto New(seconds_t length, bool repeat, const F& lambda) { + return Object::New(length, repeat, + NewLambdaRunnable(lambda).Get()); } + + void SetLength(seconds_t length) { + assert(g_base->InLogicThread()); + base::g_base->logic->SetAppTimerLength( + timer_id_, static_cast(length * 1000000.0)); + } + ~AppTimer() override { assert(g_base->InLogicThread()); base::g_base->logic->DeleteAppTimer(timer_id_); @@ -32,13 +40,6 @@ class AppTimer : public Object { int timer_id_; }; -/// Create a AppTimer from a raw lambda. -template -auto NewAppTimer(millisecs_t length, bool repeat, const F& lambda) - -> Object::Ref { - return Object::New(length, repeat, NewLambdaRunnable(lambda)); -} - } // namespace ballistica::base #endif // BALLISTICA_BASE_SUPPORT_APP_TIMER_H_ diff --git a/src/ballistica/base/support/base_build_switches.cc b/src/ballistica/base/support/base_build_switches.cc new file mode 100644 index 00000000..96abc907 --- /dev/null +++ b/src/ballistica/base/support/base_build_switches.cc @@ -0,0 +1,120 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/support/base_build_switches.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.h" +#include "ballistica/base/graphics/graphics_vr.h" + +// ------------------------- PLATFORM SELECTION -------------------------------- + +// This ugly chunk of macros simply pulls in the correct platform class header +// for each platform and defines the actual class g_base->platform will be. + +// Android --------------------------------------------------------------------- + +#if BA_OSTYPE_ANDROID +#if BA_GOOGLE_BUILD +#include "ballistica/base/platform/android/google/base_plat_andr_google.h" +#define BA_PLATFORM_CLASS BasePlatformAndroidGoogle +#elif BA_AMAZON_BUILD +#include "ballistica/base/platform/android/amazon/base_plat_andr_amazon.h" +#define BA_PLATFORM_CLASS BasePlatformAndroidAmazon +#elif BA_CARDBOARD_BUILD +#include "ballistica/base/platform/android/cardboard/base_pl_an_cardboard.h" +#define BA_PLATFORM_CLASS BasePlatformAndroidCardboard +#else // Generic android. +#include "ballistica/base/platform/android/base_platform_android.h" +#define BA_PLATFORM_CLASS BasePlatformAndroid +#endif // (Android subplatform) + +// Apple ----------------------------------------------------------------------- + +#elif BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS +#include "ballistica/base/platform/apple/base_platform_apple.h" +#define BA_PLATFORM_CLASS BasePlatformApple + +// Windows --------------------------------------------------------------------- + +#elif BA_OSTYPE_WINDOWS +#if BA_RIFT_BUILD +#include "ballistica/base/platform/windows/base_platform_windows_oculus.h" +#define BA_PLATFORM_CLASS BasePlatformWindowsOculus +#else // generic windows +#include "ballistica/base/platform/windows/base_platform_windows.h" +#define BA_PLATFORM_CLASS BasePlatformWindows +#endif // windows subtype + +// Linux ----------------------------------------------------------------------- + +#elif BA_OSTYPE_LINUX +#include "ballistica/base/platform/linux/base_platform_linux.h" +#define BA_PLATFORM_CLASS BasePlatformLinux +#else + +// Generic --------------------------------------------------------------------- + +#define BA_PLATFORM_CLASS BasePlatform + +#endif + +// ----------------------- END PLATFORM SELECTION ------------------------------ + +#ifndef BA_PLATFORM_CLASS +#error no BA_PLATFORM_CLASS defined for this platform +#endif + +namespace ballistica::base { + +auto BaseBuildSwitches::CreatePlatform() -> BasePlatform* { + auto platform = new BA_PLATFORM_CLASS(); + platform->PostInit(); + assert(platform->ran_base_post_init()); + return platform; +} + +auto BaseBuildSwitches::CreateGraphics() -> Graphics* { +#if BA_VR_BUILD + return new GraphicsVR(); +#else + return new Graphics(); +#endif +} + +auto BaseBuildSwitches::CreateAppAdapter() -> AppAdapter* { + assert(g_core); + + 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; +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/support/base_build_switches.h b/src/ballistica/base/support/base_build_switches.h new file mode 100644 index 00000000..ea5b4ba4 --- /dev/null +++ b/src/ballistica/base/support/base_build_switches.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_SUPPORT_BASE_BUILD_SWITCHES_H_ +#define BALLISTICA_BASE_SUPPORT_BASE_BUILD_SWITCHES_H_ + +#include "ballistica/base/base.h" + +namespace ballistica::base { + +/// Constructs various app components based on the current build config. +class BaseBuildSwitches { + public: + static auto CreateGraphics() -> Graphics*; + static auto CreatePlatform() -> BasePlatform*; + static auto CreateAppAdapter() -> AppAdapter*; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_SUPPORT_BASE_BUILD_SWITCHES_H_ diff --git a/src/ballistica/base/support/display_timer.h b/src/ballistica/base/support/display_timer.h new file mode 100644 index 00000000..f8f72db8 --- /dev/null +++ b/src/ballistica/base/support/display_timer.h @@ -0,0 +1,45 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_SUPPORT_DISPLAY_TIMER_H_ +#define BALLISTICA_BASE_SUPPORT_DISPLAY_TIMER_H_ + +#include "ballistica/base/base.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/generic/lambda_runnable.h" + +namespace ballistica::base { + +class DisplayTimer : public Object { + public: + DisplayTimer(seconds_t length, bool repeat, Runnable* runnable) { + assert(g_base->InLogicThread()); + timer_id_ = base::g_base->logic->NewDisplayTimer( + static_cast(length * 1000000.0), repeat, runnable); + } + + template + static auto New(seconds_t length, bool repeat, const F& lambda) { + return Object::New(length, repeat, + NewLambdaRunnable(lambda).Get()); + } + + void SetLength(seconds_t length) { + assert(g_base->InLogicThread()); + base::g_base->logic->SetDisplayTimerLength( + timer_id_, static_cast(length * 1000000.0)); + } + + ~DisplayTimer() override { + assert(g_base->InLogicThread()); + base::g_base->logic->DeleteDisplayTimer(timer_id_); + } + + private: + int timer_id_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_SUPPORT_DISPLAY_TIMER_H_ diff --git a/src/ballistica/base/support/plus_soft.h b/src/ballistica/base/support/plus_soft.h index b2b387e1..00994d3b 100644 --- a/src/ballistica/base/support/plus_soft.h +++ b/src/ballistica/base/support/plus_soft.h @@ -33,6 +33,7 @@ class PlusSoftInterface { virtual auto PutLog(bool fatal) -> bool = 0; virtual void AAT() = 0; virtual void AATE() = 0; + virtual auto GAHU() -> std::optional = 0; virtual void V1LoginDidChange() = 0; virtual void SetAdCompletionCall(PyObject* obj, bool pass_actually_showed) = 0; diff --git a/src/ballistica/base/support/repeater.cc b/src/ballistica/base/support/repeater.cc new file mode 100644 index 00000000..899a9cce --- /dev/null +++ b/src/ballistica/base/support/repeater.cc @@ -0,0 +1,55 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/support/repeater.h" + +#include "ballistica/base/support/display_timer.h" +#include "ballistica/shared/foundation/event_loop.h" + +namespace ballistica::base { +Repeater::Repeater(seconds_t initial_delay, seconds_t repeat_delay, + Runnable* runnable) + : initial_delay_(initial_delay), + repeat_delay_(repeat_delay), + runnable_(runnable) { + assert(g_base->InLogicThread()); + assert(initial_delay >= 0.0); + assert(repeat_delay >= 0.0); + + // Let's go ahead and run our initial time in a deferred call; + // this is generally safer than running in the middle of whatever UI + // code set this up. + auto weak_this = Object::WeakRef(this); + g_base->logic->event_loop()->PushCall([weak_this] { + if (weak_this.Exists()) { + weak_this->runnable_->RunAndLogErrors(); + if (!weak_this.Exists()) { + // Runnable might have killed us. + return; + } + // Kick off our initial delay timer (generally the longer one). + weak_this->timer_ = + DisplayTimer::New(weak_this->initial_delay_, false, [weak_this] { + // Timer should not have fired if we died. + assert(weak_this.Exists()); + weak_this->runnable_->RunAndLogErrors(); + if (!weak_this.Exists()) { + // Runnable might have killed us. + return; + } + // Kick off our repeat timer (generally the short one). + weak_this->timer_ = + DisplayTimer::New(weak_this->repeat_delay_, true, [weak_this] { + // Timer should not have fired if we died. + assert(weak_this.Exists()); + weak_this->runnable_->RunAndLogErrors(); + // Doesn't matter if Runnable killed us since we don't + // touch anything for the remainder of this function. + }); + }); + } + }); +} + +Repeater::~Repeater() { assert(g_base->InLogicThread()); } + +} // namespace ballistica::base diff --git a/src/ballistica/base/support/repeater.h b/src/ballistica/base/support/repeater.h new file mode 100644 index 00000000..2568ac24 --- /dev/null +++ b/src/ballistica/base/support/repeater.h @@ -0,0 +1,36 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_SUPPORT_REPEATER_H_ +#define BALLISTICA_BASE_SUPPORT_REPEATER_H_ + +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/generic/lambda_runnable.h" + +namespace ballistica::base { + +/// Runs some code immediately and then repeatedly after a delay. Useful for +/// jobs such as selecting ui elements while keys or buttons are held. +/// Uses display-time so emphasizes visual smoothness over accuracy. +class Repeater : public Object { + public: + Repeater(seconds_t initial_delay, seconds_t repeat_delay, Runnable* runnable); + ~Repeater(); + + template + static auto New(seconds_t initial_delay, seconds_t repeat_delay, + const F& lambda) { + return Object::New(initial_delay, repeat_delay, + NewLambdaRunnable(lambda).Get()); + } + + private: + seconds_t initial_delay_; + seconds_t repeat_delay_; + Object::Ref timer_; + Object::Ref runnable_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_SUPPORT_REPEATER_H_ diff --git a/src/ballistica/base/support/stress_test.h b/src/ballistica/base/support/stress_test.h deleted file mode 100644 index 6da0ec7b..00000000 --- a/src/ballistica/base/support/stress_test.h +++ /dev/null @@ -1,25 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_BASE_SUPPORT_STRESS_TEST_H_ -#define BALLISTICA_BASE_SUPPORT_STRESS_TEST_H_ - -#include "ballistica/shared/ballistica.h" - -namespace ballistica::base { - -class StressTest { - public: - void Set(bool enable, int player_count); - void Update(); - - private: - FILE* stress_test_stats_file_{}; - millisecs_t last_stress_test_update_time_{}; - bool stress_testing_{}; - int stress_test_player_count_{8}; - int last_total_frames_rendered_{}; -}; - -} // namespace ballistica::base - -#endif // BALLISTICA_BASE_SUPPORT_STRESS_TEST_H_ diff --git a/src/ballistica/base/ui/dev_console.cc b/src/ballistica/base/ui/dev_console.cc index 8b7dc05a..437b81a4 100644 --- a/src/ballistica/base/ui/dev_console.cc +++ b/src/ballistica/base/ui/dev_console.cc @@ -2,15 +2,15 @@ #include "ballistica/base/ui/dev_console.h" +#include "ballistica/base/app_adapter/app_adapter.h" #include "ballistica/base/app_mode/app_mode.h" #include "ballistica/base/audio/audio.h" #include "ballistica/base/graphics/component/simple_component.h" -#include "ballistica/base/graphics/mesh/nine_patch_mesh.h" #include "ballistica/base/graphics/text/text_graphics.h" -#include "ballistica/base/input/input.h" #include "ballistica/base/logic/logic.h" #include "ballistica/base/platform/base_platform.h" #include "ballistica/base/python/base_python.h" +#include "ballistica/base/support/repeater.h" #include "ballistica/base/ui/ui.h" #include "ballistica/shared/foundation/event_loop.h" #include "ballistica/shared/generic/utils.h" @@ -20,14 +20,12 @@ namespace ballistica::base { // How much of the screen the console covers when it is at full size. -const float kDevConsoleSize = 0.9f; -const int kDevConsoleLineLimit = 80; -const int kDevConsoleStringBreakUpSize = 1950; -const int kDevConsoleActivateKey1 = SDLK_BACKQUOTE; -const int kDevConsoleActivateKey2 = SDLK_F2; +const float kDevConsoleSize{0.9f}; +const int kDevConsoleLineLimit{80}; +const int kDevConsoleStringBreakUpSize{1950}; const float kDevConsoleTabButtonCornerRadius{16.0f}; -const double kTransitionSeconds = 0.15; +const double kTransitionSeconds{0.15}; enum class DevConsoleHAnchor_ { kLeft, kCenter, kRight }; enum class DevButtonStyle_ { kNormal, kDark }; @@ -85,6 +83,13 @@ static auto XOffs(DevConsoleHAnchor_ attach) -> float { return 0.0f; } +static auto IsValidHungryChar_(uint32_t this_char) -> bool { + // Include letters, numbers, and underscore. + return ((this_char >= 65 && this_char <= 90) + || (this_char >= 97 && this_char <= 122) + || (this_char >= 48 && this_char <= 57) || this_char == '_'); +} + static void DrawRect(RenderPass* pass, Mesh* mesh, float bottom, float x, float y, float width, float height, const Vector3f& bgcolor) { @@ -171,11 +176,7 @@ class DevConsole::Text_ : public DevConsole::Widget_ { } void Draw(RenderPass* pass, float bottom) override { - Vector3f fgcolor; - Vector3f bgcolor; - fgcolor = Vector3f{0.8f, 0.7f, 0.8f}; - bgcolor = Vector3f{0.25, 0.2f, 0.3f}; - + auto fgcolor = Vector3f{0.8f, 0.7f, 0.8f}; DrawText(pass, &text_group, scale, bottom, x + XOffs(h_attach), y, fgcolor); } }; @@ -327,7 +328,6 @@ class DevConsole::ToggleButton_ : public DevConsole::Widget_ { void Draw(RenderPass* pass, float bottom) override { DrawRect(pass, &mesh, bottom, x + XOffs(attach), y, width, height, - pressed ? Vector3f{0.5f, 0.2f, 1.0f} : on ? Vector3f{0.5f, 0.4f, 0.6f} : Vector3f{0.25, 0.2f, 0.3f}); @@ -448,14 +448,11 @@ DevConsole::DevConsole() { if (g_buildconfig.test_build()) { title += " (test)"; } - title_text_group_.SetText(title); built_text_group_.SetText("Built: " __DATE__ " " __TIME__); prompt_text_group_.SetText(">"); } -DevConsole::~DevConsole() = default; - void DevConsole::RefreshTabButtons_() { // IMPORTANT: This code should always be run in its own top level call and // never directly from user code. Otherwise we can wind up mucking with @@ -685,6 +682,11 @@ void DevConsole::set_input_string(const std::string& val) { assert(g_base->InLogicThread()); input_string_ = val; input_text_dirty_ = true; + // Move carat to end. + carat_char_ = + static_cast(Utils::UnicodeFromUTF8(input_string_, "fj43t").size()); + assert(CaratCharValid_()); + carat_dirty_ = true; } void DevConsole::InputAdapterFinish() { @@ -695,28 +697,14 @@ void DevConsole::InputAdapterFinish() { auto DevConsole::HandleKeyPress(const SDL_Keysym* keysym) -> bool { assert(g_base->InLogicThread()); - // Handle our toggle buttons no matter whether we're active. - switch (keysym->sym) { - case kDevConsoleActivateKey1: - case kDevConsoleActivateKey2: { - if (!g_buildconfig.demo_build() && !g_buildconfig.arcade_build()) { - // (reset input so characters don't continue walking and stuff) - g_base->input->ResetHoldStates(); - if (auto console = g_base->ui->dev_console()) { - console->ToggleState(); - } - } - return true; - } - default: - break; - } + // Any presses or releases cancels repeat actions. + key_repeater_.Clear(); if (state_ == State_::kInactive) { return false; } - // Handle some stuff only while active. + // Stuff we always look for. switch (keysym->sym) { case SDLK_ESCAPE: Dismiss(); @@ -725,57 +713,311 @@ auto DevConsole::HandleKeyPress(const SDL_Keysym* keysym) -> bool { break; } - // Handle some stuff only with the Python terminal visible. - if (python_terminal_visible_) { + // Stuff we look for only when direct keyboard input is enabled and our + // Python terminal is up. + if (python_terminal_visible_ && g_base->ui->UIHasDirectKeyboardInput()) { + bool do_carat_right{}; + bool do_hungry_carat_right{}; + bool do_carat_left{}; + bool do_hungry_carat_left{}; + bool do_history_up{}; + bool do_history_down{}; + bool do_backspace{}; + bool do_forward_delete{}; + bool do_hungry_backspace{}; + bool do_hungry_forward_delete{}; + bool do_move_to_end{}; + bool do_move_to_beginning{}; + bool do_kill_line{}; switch (keysym->sym) { - case SDLK_BACKSPACE: - case SDLK_DELETE: { - std::vector unichars = - Utils::UnicodeFromUTF8(input_string_, "fjco38"); - if (!unichars.empty()) { - unichars.resize(unichars.size() - 1); - input_string_ = Utils::UTF8FromUnicode(unichars); - input_text_dirty_ = true; - } - break; - } - case SDLK_UP: - case SDLK_DOWN: { - if (input_history_.empty()) { - break; - } - if (keysym->sym == SDLK_UP) { - input_history_position_++; + case SDLK_BACKSPACE: { + if (keysym->mod & KMOD_ALT) { + do_hungry_backspace = true; } else { - input_history_position_--; - } - int input_history_position_used = - (input_history_position_ - 1) - % static_cast(input_history_.size()); - int j = 0; - for (auto& i : input_history_) { - if (j == input_history_position_used) { - input_string_ = i; - input_text_dirty_ = true; - break; - } - j++; + do_backspace = true; } break; } + case SDLK_DELETE: { + if (keysym->mod & KMOD_ALT) { + do_hungry_forward_delete = true; + } else { + do_forward_delete = true; + } + break; + } + case SDLK_HOME: + do_move_to_beginning = true; + break; + case SDLK_END: + do_move_to_end = true; + break; + case SDLK_UP: + do_history_up = true; + break; + case SDLK_DOWN: + do_history_down = true; + break; + case SDLK_RIGHT: + if (keysym->mod & KMOD_ALT) { + do_hungry_carat_right = true; + } else { + do_carat_right = true; + } + break; + case SDLK_LEFT: + if (keysym->mod & KMOD_ALT) { + do_hungry_carat_left = true; + } else { + do_carat_left = true; + } + break; case SDLK_KP_ENTER: case SDLK_RETURN: { Exec(); break; } + + // Wheeee emacs key shortcuts!! + case SDLK_n: + if (keysym->mod & KMOD_CTRL) { + do_history_down = true; + } + break; + case SDLK_f: + if (keysym->mod & KMOD_CTRL) { + do_carat_right = true; + } else if (keysym->mod & KMOD_ALT) { + do_hungry_carat_right = true; + } + break; + case SDLK_b: + if (keysym->mod & KMOD_CTRL) { + do_carat_left = true; + } else if (keysym->mod & KMOD_ALT) { + do_hungry_carat_left = true; + } + break; + case SDLK_p: + if (keysym->mod & KMOD_CTRL) { + do_history_up = true; + } + break; + case SDLK_a: + if (keysym->mod & KMOD_CTRL) { + do_move_to_beginning = true; + } + break; + case SDLK_d: + if (keysym->mod & KMOD_CTRL) { + do_forward_delete = true; + } else if (keysym->mod & KMOD_ALT) { + do_hungry_forward_delete = true; + } + break; + case SDLK_e: + if (keysym->mod & KMOD_CTRL) { + do_move_to_end = true; + } + break; + case SDLK_k: + if (keysym->mod & KMOD_CTRL) { + do_kill_line = true; + } default: { break; } } + if (do_kill_line) { + auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjco38"); + assert(CaratCharValid_()); + unichars.resize(carat_char_); + assert(CaratCharValid_()); + input_string_ = Utils::UTF8FromUnicode(unichars); + input_text_dirty_ = true; + carat_dirty_ = true; + } + if (do_move_to_beginning) { + carat_char_ = 0; + assert(CaratCharValid_()); + carat_dirty_ = true; + } + if (do_move_to_end) { + // Move carat to end. + carat_char_ = static_cast( + Utils::UnicodeFromUTF8(input_string_, "fj43t").size()); + assert(CaratCharValid_()); + carat_dirty_ = true; + } + if (do_hungry_backspace || do_hungry_carat_left) { + auto do_delete = do_hungry_backspace; + key_repeater_ = Repeater::New( + g_base->app_adapter->GetKeyRepeatDelay(), + g_base->app_adapter->GetKeyRepeatInterval(), [this, do_delete] { + auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjco38"); + bool found_valid{}; + // Delete/move until we've found at least one valid char and the + // stop at the first invalid one. + while (carat_char_ > 0) { + assert(CaratCharValid_()); + auto this_char = unichars[carat_char_ - 1]; + auto is_valid = IsValidHungryChar_(this_char); + if (found_valid && !is_valid) { + break; + } + if (is_valid) { + found_valid = true; + } + if (do_delete) { + unichars.erase(unichars.begin() + carat_char_ - 1); + } + carat_char_ -= 1; + assert(CaratCharValid_()); + } + if (do_delete) { + input_string_ = Utils::UTF8FromUnicode(unichars); + input_text_dirty_ = true; + } + carat_dirty_ = true; + }); + } + if (do_hungry_forward_delete || do_hungry_carat_right) { + auto do_delete = do_hungry_forward_delete; + key_repeater_ = Repeater::New( + g_base->app_adapter->GetKeyRepeatDelay(), + g_base->app_adapter->GetKeyRepeatInterval(), [this, do_delete] { + auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjco38"); + bool found_valid{}; + // Move until we've found at least one valid char and the + // stop at the first invalid one. + while (carat_char_ < static_cast(unichars.size())) { + assert(CaratCharValid_()); + auto this_char = unichars[carat_char_]; + auto is_valid = IsValidHungryChar_(this_char); + if (found_valid && !is_valid) { + break; + } + if (is_valid) { + found_valid = true; + } + if (do_delete) { + unichars.erase(unichars.begin() + carat_char_); + } else { + carat_char_ += 1; + } + assert(CaratCharValid_()); + } + if (do_delete) { + input_string_ = Utils::UTF8FromUnicode(unichars); + input_text_dirty_ = true; + } + carat_dirty_ = true; + }); + } + if (do_backspace) { + key_repeater_ = Repeater::New( + g_base->app_adapter->GetKeyRepeatDelay(), + g_base->app_adapter->GetKeyRepeatInterval(), [this] { + auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjco38"); + if (!unichars.empty() && carat_char_ > 0) { + assert(CaratCharValid_()); + unichars.erase(unichars.begin() + carat_char_ - 1); + input_string_ = Utils::UTF8FromUnicode(unichars); + input_text_dirty_ = true; + carat_char_ -= 1; + assert(CaratCharValid_()); + carat_dirty_ = true; + } + }); + } + if (do_forward_delete) { + key_repeater_ = Repeater::New( + g_base->app_adapter->GetKeyRepeatDelay(), + g_base->app_adapter->GetKeyRepeatInterval(), [this] { + auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjco33"); + if (!unichars.empty() + && carat_char_ < static_cast(unichars.size())) { + assert(CaratCharValid_()); + unichars.erase(unichars.begin() + carat_char_); + input_string_ = Utils::UTF8FromUnicode(unichars); + input_text_dirty_ = true; + carat_dirty_ = true; // Didn't move but might change size. + assert(CaratCharValid_()); + } + }); + } + if (do_carat_left || do_carat_right) { + key_repeater_ = Repeater::New( + g_base->app_adapter->GetKeyRepeatDelay(), + g_base->app_adapter->GetKeyRepeatInterval(), + [do_carat_left, do_carat_right, this] { + int offset = do_carat_right ? 1 : -1; + carat_char_ = std::clamp( + carat_char_ + offset, 0, + static_cast( + Utils::UnicodeFromUTF8(input_string_, "fffwe").size())); + assert(CaratCharValid_()); + carat_dirty_ = true; + }); + } + + if ((do_history_up || do_history_down) && !input_history_.empty()) { + if (do_history_up) { + input_history_position_++; + } else { + input_history_position_--; + } + int input_history_position_used = + (input_history_position_ - 1) + % static_cast(input_history_.size()); + int j = 0; + for (auto& i : input_history_) { + if (j == input_history_position_used) { + input_string_ = i; + carat_char_ = static_cast( + Utils::UnicodeFromUTF8(input_string_, "fffwe").size()); + assert(CaratCharValid_()); + input_text_dirty_ = true; + carat_dirty_ = true; + break; + } + j++; + } + } + return true; } + + // By default don't claim key events; we want to be able to show the + // console while still playing/navigating normally. + return false; +} + +auto DevConsole::HandleTextEditing(const std::string& text) -> bool { + assert(g_base->InLogicThread()); + if (state_ == State_::kInactive) { + return false; + } + assert(CaratCharValid_()); + auto unichars = Utils::UnicodeFromUTF8(input_string_, "jfof8"); + auto addunichars = Utils::UnicodeFromUTF8(text, "jfoef8"); + unichars.insert(unichars.begin() + carat_char_, addunichars.begin(), + addunichars.end()); + input_string_ = Utils::UTF8FromUnicode(unichars); + input_text_dirty_ = true; + carat_char_ += addunichars.size(); + assert(CaratCharValid_()); + carat_dirty_ = true; return true; } +auto DevConsole::HandleKeyRelease(const SDL_Keysym* keysym) -> bool { + // Any presses or releases cancels repeat actions. + key_repeater_.Clear(); + + // Otherwise absorb *all* key-ups when we're active. + return state_ != State_::kInactive; +} + void DevConsole::Exec() { BA_PRECONDITION(g_base->InLogicThread()); if (!input_enabled_) { @@ -794,7 +1036,17 @@ void DevConsole::Exec() { input_history_.pop_back(); } input_string_.resize(0); + carat_char_ = 0; + assert(CaratCharValid_()); input_text_dirty_ = true; + carat_dirty_ = true; +} + +// Just for sanity testing. +auto DevConsole::CaratCharValid_() -> bool { + return carat_char_ >= 0 + && carat_char_ <= static_cast( + Utils::UnicodeFromUTF8(input_string_, "fwewffe").size()); } void DevConsole::SubmitPythonCommand_(const std::string& command) { @@ -861,35 +1113,6 @@ void DevConsole::ToggleState() { transition_start_ = g_base->logic->display_time(); } -auto DevConsole::HandleTextEditing(const std::string& text) -> bool { - assert(g_base->InLogicThread()); - if (state_ == State_::kInactive) { - return false; - } - - // Ignore back-tick because we use that key to toggle the console. - // - // FIXME: Perhaps should allow typing it if some control-character is - // held? - if (text == "`") { - return false; - } - input_string_ += text; - input_text_dirty_ = true; - return true; -} - -auto DevConsole::HandleKeyRelease(const SDL_Keysym* keysym) -> bool { - // Always absorb our activate keys. - if (keysym->sym == kDevConsoleActivateKey1 - || keysym->sym == kDevConsoleActivateKey2) { - return true; - } - - // Otherwise absorb *all* key-ups when we're active. - return state_ != State_::kInactive; -} - void DevConsole::Print(const std::string& s_in) { assert(g_base->InLogicThread()); std::string s = Utils::GetValidUTF8(s_in.c_str(), "cspr"); @@ -1014,13 +1237,14 @@ void DevConsole::Draw(FrameDef* frame_def) { if (input_text_dirty_) { input_text_group_.SetText(input_string_); input_text_dirty_ = false; - last_input_text_change_time_ = pass->frame_def()->app_time_millisecs(); } { SimpleComponent c(pass); c.SetFlatness(1.0f); c.SetTransparent(true); c.SetColor(0.4f, 0.33f, 0.45f, 0.8f); + + // Build. int elem_count = built_text_group_.GetElementCount(); for (int e = 0; e < elem_count; e++) { c.SetTexture(built_text_group_.GetElementTexture(e)); @@ -1032,6 +1256,8 @@ void DevConsole::Draw(FrameDef* frame_def) { c.DrawMesh(built_text_group_.GetElementMesh(e)); } } + + // Title. elem_count = title_text_group_.GetElementCount(); for (int e = 0; e < elem_count; e++) { c.SetTexture(title_text_group_.GetElementTexture(e)); @@ -1042,6 +1268,8 @@ void DevConsole::Draw(FrameDef* frame_def) { c.DrawMesh(title_text_group_.GetElementMesh(e)); } } + + // Prompt. elem_count = prompt_text_group_.GetElementCount(); for (int e = 0; e < elem_count; e++) { c.SetTexture(prompt_text_group_.GetElementTexture(e)); @@ -1053,6 +1281,8 @@ void DevConsole::Draw(FrameDef* frame_def) { c.DrawMesh(prompt_text_group_.GetElementMesh(e)); } } + + // Input line. elem_count = input_text_group_.GetElementCount(); for (int e = 0; e < elem_count; e++) { c.SetTexture(input_text_group_.GetElementTexture(e)); @@ -1066,21 +1296,36 @@ void DevConsole::Draw(FrameDef* frame_def) { } // Carat. - millisecs_t real_time = pass->frame_def()->app_time_millisecs(); - if (real_time % 200 < 100 - || (real_time - last_input_text_change_time_ < 100)) { + if (!carat_mesh_.Exists() || carat_dirty_) { + // Note: we explicitly update here if carat is dirty because + // that updates last_carat_change_time_ which affects whether + // we draw or not. GetCaratX_() only updates it *if* we draw. + UpdateCarat_(); + } + millisecs_t app_time = pass->frame_def()->app_time_millisecs(); + millisecs_t since_change = app_time - last_carat_x_change_time_; + if (since_change < 300 || since_change % 1000 < 500) { SimpleComponent c(pass); c.SetTransparent(true); - c.SetColor(1, 1, 1, 0.7f); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShadow)); + c.SetColor(0.8, 0.0, 1.0, 0.3f); { auto xf = c.ScopedTransform(); - c.Translate( - (19.0f - + g_base->text_graphics->GetStringWidth(input_string_) * 0.5f) - * bs, - bottom + 22.5f * bs, kDevConsoleZDepth); - c.Scale(6.0f * bs, 12.0f * bs, 1.0f); - c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); + auto carat_x = GetCaratX_(); + c.Translate(15.0f * bs, bottom + 14.5f * bs, kDevConsoleZDepth); + c.Scale(0.5f * bs, 0.5f * bs, 1.0f); + c.Translate(carat_x, 0.0f, 0.0f); + c.DrawMesh(carat_glow_mesh_.Get()); + } + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShadowSharp)); + c.SetColor(1.0, 1.0, 1.0, 1.0f); + { + auto xf = c.ScopedTransform(); + auto carat_x = GetCaratX_(); + c.Translate(15.0f * bs, bottom + 14.5f * bs, kDevConsoleZDepth); + c.Scale(0.5f * bs, 0.5f * bs, 1.0f); + c.Translate(carat_x, 0.0f, 0.0f); + c.DrawMesh(carat_mesh_.Get()); } } @@ -1192,4 +1437,86 @@ void DevConsole::StepDisplayTime() { } } +auto DevConsole::PasteFromClipboard() -> bool { + if (state_ != State_::kInactive) { + if (python_terminal_visible_) { + if (g_base->app_adapter->ClipboardIsSupported()) { + if (g_base->app_adapter->ClipboardHasText()) { + auto text = g_base->app_adapter->ClipboardGetText(); + if (strstr(text.c_str(), "\n") || strstr(text.c_str(), "\r")) { + g_base->audio->PlaySound( + g_base->assets->SysSound(SysSoundID::kErrorBeep)); + ScreenMessage("Can only paste single lines of text.", + Vector3f(1.0f, 0.0f, 0.0f)); + } else { + HandleTextEditing(text); + } + // Ok, we either pasted or complained, so consider it handled. + return true; + } + } + } + } + return false; +} + +void DevConsole::UpdateCarat_() { + last_carat_x_change_time_ = g_core->GetAppTimeMillisecs(); + auto unichars = Utils::UnicodeFromUTF8(input_string_, "fjfwef"); + auto unichars_clamped = unichars; + + unichars_clamped.resize(carat_char_); + auto clamped_str = Utils::UTF8FromUnicode(unichars_clamped); + carat_x_ = g_base->text_graphics->GetStringWidth(clamped_str); + + // Use a base width if we're not covering a char, and use the char's width + // if we are. + float width = 14.0f; + if (carat_char_ < static_cast(unichars.size())) { + std::vector covered_char{unichars[carat_char_]}; + auto covered_char_str = Utils::UTF8FromUnicode(covered_char); + width = + std::max(3.0f, g_base->text_graphics->GetStringWidth(covered_char_str)); + } + + float height = 32.0f; + float x_extend = 15.0f; + float y_extend = 20.0f; + float x_offset = 2.0f; + float y_offset = -0.0f; + float corner_radius = 20.0f; + float width_fin = width + x_extend * 2.0f; + float height_fin = height + y_extend * 2.0f; + float x_border = + NinePatchMesh::BorderForRadius(corner_radius, width_fin, height_fin); + float y_border = + NinePatchMesh::BorderForRadius(corner_radius, height_fin, width_fin); + carat_glow_mesh_ = Object::New( + -x_extend + x_offset, -y_extend + y_offset, 0.0f, width_fin, height_fin, + x_border, y_border, x_border, y_border); + + corner_radius = 3.0f; + x_extend = 0.0f; + y_extend = -3.0f; + x_offset = 1.0f; + y_offset = 0.0f; + width_fin = width + x_extend * 2.0f; + height_fin = height + y_extend * 2.0f; + x_border = + NinePatchMesh::BorderForRadius(corner_radius, width_fin, height_fin); + y_border = + NinePatchMesh::BorderForRadius(corner_radius, height_fin, width_fin); + carat_mesh_ = Object::New( + -x_extend + x_offset, -y_extend + y_offset, 0.0f, width_fin, height_fin, + x_border, y_border, x_border, y_border); +} + +auto DevConsole::GetCaratX_() -> float { + if (carat_dirty_) { + UpdateCarat_(); + carat_dirty_ = false; + } + return carat_x_; +} + } // namespace ballistica::base diff --git a/src/ballistica/base/ui/dev_console.h b/src/ballistica/base/ui/dev_console.h index c362ed6d..7e080ace 100644 --- a/src/ballistica/base/ui/dev_console.h +++ b/src/ballistica/base/ui/dev_console.h @@ -8,6 +8,7 @@ #include #include +#include "ballistica/base/graphics/mesh/nine_patch_mesh.h" #include "ballistica/base/graphics/renderer/renderer.h" #include "ballistica/shared/foundation/object.h" #include "ballistica/shared/python/python_ref.h" @@ -20,7 +21,6 @@ const float kDevConsoleZDepth = 0.0f; class DevConsole { public: DevConsole(); - ~DevConsole(); auto IsActive() const -> bool { return (state_ != State_::kInactive); } auto HandleTextEditing(const std::string& text) -> bool; auto HandleKeyPress(const SDL_Keysym* keysym) -> bool; @@ -33,6 +33,9 @@ class DevConsole { /// Tell the console to quietly go away no matter what state it is in. void Dismiss(); + /// Attempt to Paste. Returns true if it happened. + auto PasteFromClipboard() -> bool; + /// Print text to the console. void Print(const std::string& s_in); void Draw(FrameDef* frame_def); @@ -76,24 +79,30 @@ class DevConsole { class OutputLine_; enum class State_ : uint8_t { kInactive, kMini, kFull }; + auto CaratCharValid_() -> bool; + auto GetCaratX_() -> float; + void UpdateCarat_(); auto Bottom_() const -> float; void SubmitPythonCommand_(const std::string& command); void InvokeStringEditor_(); void RefreshTabButtons_(); void RefreshTabContents_(); - bool input_text_dirty_{true}; - bool input_enabled_{}; - bool last_line_mesh_dirty_{true}; - bool python_terminal_visible_{}; - bool python_terminal_pressed_{}; - bool refresh_pending_{}; + int input_history_position_{}; int ui_lock_count_{}; + int carat_char_{0}; State_ state_{State_::kInactive}; State_ state_prev_{State_::kInactive}; - millisecs_t last_input_text_change_time_{}; - double transition_start_{}; - int input_history_position_{}; + bool input_text_dirty_ : 1 {true}; + bool input_enabled_ : 1 {}; + bool last_line_mesh_dirty_ : 1 {true}; + bool python_terminal_visible_ : 1 {}; + bool python_terminal_pressed_ : 1 {}; + bool refresh_pending_ : 1 {}; + bool carat_dirty_ : 1 {true}; + float carat_x_{}; + seconds_t transition_start_{}; + millisecs_t last_carat_x_change_time_{}; ImageMesh bg_mesh_; ImageMesh stripe_mesh_; ImageMesh border_mesh_; @@ -103,15 +112,17 @@ class DevConsole { TextGroup input_text_group_; std::string last_line_; std::string input_string_; - std::list tabs_{"Python", "AppModes", "Logging", "Graphics", - "UI"}; - std::string active_tab_{"Python"}; + std::list tabs_; + std::string active_tab_; PythonRef string_edit_adapter_; - Object::Ref last_line_mesh_group_; std::list input_history_; std::list output_lines_; std::vector > widgets_; std::vector > tab_buttons_; + Object::Ref last_line_mesh_group_; + Object::Ref key_repeater_; + Object::Ref carat_mesh_; + Object::Ref carat_glow_mesh_; }; } // namespace ballistica::base diff --git a/src/ballistica/base/ui/ui.cc b/src/ballistica/base/ui/ui.cc index e9b50093..06930e4c 100644 --- a/src/ballistica/base/ui/ui.cc +++ b/src/ballistica/base/ui/ui.cc @@ -194,7 +194,7 @@ auto UI::UIHasDirectKeyboardInput() const -> bool { // we'll probably want to pop up a controller-centric on-screen-keyboard // thingie instead. auto* ui_input_device = g_base->ui->GetUIInputDevice(); - if (auto* keyboard = g_base->ui->GetUIInputDevice()) { + if (auto* keyboard = g_base->input->keyboard_input()) { if (ui_input_device == keyboard || ui_input_device == nullptr) { return true; } @@ -532,7 +532,7 @@ void UI::OnAssetsAvailable() { assert(g_base->InLogicThread()); // Spin up the dev console. - if (!g_core->HeadlessMode()) { + if (!g_core->HeadlessMode() && !g_buildconfig.demo_build()) { assert(dev_console_ == nullptr); dev_console_ = new DevConsole(); diff --git a/src/ballistica/base/ui/ui.h b/src/ballistica/base/ui/ui.h index 02cecbb7..7ed5dd92 100644 --- a/src/ballistica/base/ui/ui.h +++ b/src/ballistica/base/ui/ui.h @@ -17,6 +17,14 @@ class Widget; namespace ballistica::base { +/// Delay before moving through elements in the UI when a key/button/stick +/// is held +const seconds_t kUINavigationRepeatDelay{0.25}; + +/// Interval after the initial delay when moving through UI elements when a +/// key/button/stick is held. +const seconds_t kUINavigationRepeatInterval{0.1}; + // Our global UI subsystem. This acts as a manager/wrapper for individual UI // feature-sets that provide specific UI functionality. class UI { @@ -84,10 +92,10 @@ class UI { auto GetUIInputDevice() const -> InputDevice*; /// Return true if there is a full desktop-style hardware keyboard - /// attached and the active UI InputDevice is set to it or not set. This + /// attached and no non-keyboard device is currently controlling the UI. This /// also may take language or user preferences into account. Editable text /// elements can use this to opt in to accepting key events directly - /// instead of popping up a string edit dialog. + /// instead of popping up string edit dialogs. auto UIHasDirectKeyboardInput() const -> bool; /// Schedule a back button press. Can be called from any thread. diff --git a/src/ballistica/classic/classic.cc b/src/ballistica/classic/classic.cc index 11abf286..56a42e41 100644 --- a/src/ballistica/classic/classic.cc +++ b/src/ballistica/classic/classic.cc @@ -2,7 +2,9 @@ #include "ballistica/classic/classic.h" +#include "ballistica/base/support/app_timer.h" #include "ballistica/classic/python/classic_python.h" +#include "ballistica/classic/support/stress_test.h" #include "ballistica/classic/support/v1_account.h" #include "ballistica/core/platform/core_platform.h" #include "ballistica/scene_v1/python/scene_v1_python.h" @@ -54,7 +56,9 @@ void ClassicFeatureSet::OnModuleExec(PyObject* module) { } ClassicFeatureSet::ClassicFeatureSet() - : python{new ClassicPython()}, v1_account{new V1Account()} { + : python{new ClassicPython()}, + v1_account{new V1Account()}, + stress_test_{new StressTest()} { // We're a singleton. If there's already one of us, something's wrong. assert(g_classic == nullptr); } diff --git a/src/ballistica/classic/classic.h b/src/ballistica/classic/classic.h index 97cbe97f..efba8d9d 100644 --- a/src/ballistica/classic/classic.h +++ b/src/ballistica/classic/classic.h @@ -28,6 +28,7 @@ namespace ballistica::classic { // Predeclared types our feature-set provides. class ClassicFeatureSet; class ClassicPython; +class StressTest; class V1Account; enum class V1AccountType { @@ -107,9 +108,12 @@ class ClassicFeatureSet : public FeatureSetNativeComponent, void set_v1_account_type(V1AccountType tp) { v1_account_type_ = tp; } void PlayMusic(const std::string& music_type, bool continuous) override; + auto* stress_test() const { return stress_test_; } + private: ClassicFeatureSet(); V1AccountType v1_account_type_{V1AccountType::kInvalid}; + StressTest* stress_test_; }; } // namespace ballistica::classic diff --git a/src/ballistica/classic/python/methods/python_methods_classic.cc b/src/ballistica/classic/python/methods/python_methods_classic.cc index 6f6765c0..52780c22 100644 --- a/src/ballistica/classic/python/methods/python_methods_classic.cc +++ b/src/ballistica/classic/python/methods/python_methods_classic.cc @@ -4,7 +4,10 @@ #include "ballistica/base/graphics/graphics.h" #include "ballistica/base/graphics/support/camera.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/classic/support/stress_test.h" #include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/shared/foundation/event_loop.h" #include "ballistica/shared/python/python.h" #include "ballistica/shared/python/python_sys.h" @@ -145,10 +148,39 @@ static PyMethodDef PyValueTestDef = { "(internal)", }; +// -------------------------- set_stress_testing ------------------------------- + +static auto PySetStressTesting(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + int enable; + int player_count; + if (!PyArg_ParseTuple(args, "pi", &enable, &player_count)) { + return nullptr; + } + g_base->logic->event_loop()->PushCall([enable, player_count] { + g_classic->stress_test()->Set(enable, player_count); + }); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetStressTestingDef = { + "set_stress_testing", // name + PySetStressTesting, // method + METH_VARARGS, // flags + + "set_stress_testing(testing: bool, player_count: int) -> None\n" + "\n" + "(internal)", +}; + // ----------------------------------------------------------------------------- auto PythonMethodsClassic::GetMethods() -> std::vector { - return {PyValueTestDef}; + return { + PyValueTestDef, + PySetStressTestingDef, + }; } #pragma clang diagnostic pop diff --git a/src/ballistica/base/support/stress_test.cc b/src/ballistica/classic/support/stress_test.cc similarity index 61% rename from src/ballistica/base/support/stress_test.cc rename to src/ballistica/classic/support/stress_test.cc index 0438e972..b92e6074 100644 --- a/src/ballistica/base/support/stress_test.cc +++ b/src/ballistica/classic/support/stress_test.cc @@ -1,15 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/base/support/stress_test.h" +#include "ballistica/classic/support/stress_test.h" #include "ballistica/base/graphics/graphics_server.h" #include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/base/input/device/test_input.h" #include "ballistica/base/input/input.h" +#include "ballistica/base/support/app_timer.h" +#include "ballistica/classic/classic.h" -namespace ballistica::base { +namespace ballistica::classic { void StressTest::Set(bool enable, int player_count) { - assert(g_core->InMainThread()); + assert(g_base->InLogicThread()); bool was_stress_testing = stress_testing_; stress_testing_ = enable; stress_test_player_count_ = player_count; @@ -28,17 +31,22 @@ void StressTest::Set(bool enable, int player_count) { // Assume zero if there's no graphics yet. last_total_frames_rendered_ = 0; } + + update_timer_ = base::AppTimer::New(1.0 / 30.0, true, [this] { Update(); }); + } + if (!stress_testing_) { + update_timer_.Clear(); } } void StressTest::Update() { - assert(g_core->InMainThread()); + assert(g_base->InLogicThread()); // Handle a little misc stuff here. // If we're currently running stress-tests, update that stuff. if (stress_testing_ && g_base->input) { // Update our fake inputs to make our dudes run around. - g_base->input->ProcessStressTesting(stress_test_player_count_); + ProcessInputs(stress_test_player_count_); // Every 10 seconds update our stress-test stats. millisecs_t t = g_core->GetAppTimeMillisecs(); @@ -51,10 +59,10 @@ void StressTest::Update() { stress_test_stats_file_ = g_core->platform->FOpen(f_name.c_str(), "wb"); if (stress_test_stats_file_ != nullptr) { - fprintf( - stress_test_stats_file_, - "time,averageFps,nodes,meshes,collision_meshes,textures,sounds," - "pssMem,sharedDirtyMem,privateDirtyMem\n"); + fprintf(stress_test_stats_file_, + "time,averageFps,nodes,meshes,collision_meshes,textures," + "sounds," + "pssMem,sharedDirtyMem,privateDirtyMem\n"); fflush(stress_test_stats_file_); } } @@ -98,4 +106,56 @@ void StressTest::Update() { } } -} // namespace ballistica::base +void StressTest::ProcessInputs(int player_count) { + assert(g_base->InLogicThread()); + assert(player_count >= 0); + + millisecs_t time = g_core->GetAppTimeMillisecs(); + + // FIXME: If we don't check for stress_test_last_leave_time_ we totally + // confuse the game.. need to be able to survive that. + + // Kill some off if we have too many. + while (static_cast(test_inputs_.size()) > player_count) { + delete test_inputs_.front(); + test_inputs_.pop_front(); + } + + // If we have less than full test-inputs, add one randomly. + if (static_cast(test_inputs_.size()) < player_count + && ((rand() % 1000 < 10))) { // NOLINT + test_inputs_.push_back(new base::TestInput()); + } + + // Every so often lets kill the oldest one off. + if (explicit_bool(true)) { + if (test_inputs_.size() > 0 && (rand() % 2000 < 3)) { // NOLINT + stress_test_last_leave_time_ = time; + + // Usually do oldest; sometimes newest. + if (rand() % 5 == 0) { // NOLINT + delete test_inputs_.back(); + test_inputs_.pop_back(); + } else { + delete test_inputs_.front(); + test_inputs_.pop_front(); + } + } + } + + if (time - stress_test_time_ > 1000) { + stress_test_time_ = time; // reset.. + for (auto& test_input : test_inputs_) { + (*test_input).Reset(); + } + } + + while (stress_test_time_ < time) { + stress_test_time_++; + for (auto& test_input : test_inputs_) { + (*test_input).Process(stress_test_time_); + } + } +} + +} // namespace ballistica::classic diff --git a/src/ballistica/classic/support/stress_test.h b/src/ballistica/classic/support/stress_test.h new file mode 100644 index 00000000..96c5fd71 --- /dev/null +++ b/src/ballistica/classic/support/stress_test.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CLASSIC_SUPPORT_STRESS_TEST_H_ +#define BALLISTICA_CLASSIC_SUPPORT_STRESS_TEST_H_ + +#include "ballistica/base/base.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::classic { + +class StressTest { + public: + void Set(bool enable, int player_count); + void Update(); + + private: + void ProcessInputs(int player_count); + std::list test_inputs_; + + millisecs_t stress_test_time_{}; + millisecs_t stress_test_last_leave_time_{}; + int stress_test_player_count_{8}; + int last_total_frames_rendered_{}; + bool stress_testing_ : 1 {}; + millisecs_t last_stress_test_update_time_{}; + FILE* stress_test_stats_file_{}; + Object::Ref update_timer_{}; +}; + +} // namespace ballistica::classic + +#endif // BALLISTICA_CLASSIC_SUPPORT_STRESS_TEST_H_ diff --git a/src/ballistica/core/platform/apple/core_platform_apple.cc b/src/ballistica/core/platform/apple/core_platform_apple.cc index 04d79880..2122cfe6 100644 --- a/src/ballistica/core/platform/apple/core_platform_apple.cc +++ b/src/ballistica/core/platform/apple/core_platform_apple.cc @@ -4,14 +4,20 @@ #include "ballistica/core/platform/apple/core_platform_apple.h" #if BA_XCODE_BUILD -#include #include #endif #include #if BA_XCODE_BUILD -#include "ballistica/base/platform/apple/apple_utils.h" +#include "ballistica/base/platform/apple/from_swift.h" +#include "ballistica/shared/math/rect.h" +#endif + +#if BA_XCODE_BUILD +// This needs to be below ballistica headers since it relies on +// some types in them but does not include headers itself. +#include #endif namespace ballistica::core { @@ -28,14 +34,42 @@ auto CorePlatformApple::GetDeviceV1AccountUUIDPrefix() -> std::string { #endif } +auto CorePlatformApple::DoGetDeviceName() -> std::string { +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD + // Ask swift for a pretty name if possible. + auto val = BallisticaKit::CocoaFromCpp::getDeviceName(); + if (val) { + return val.get(); + } +#elif BA_OSTYPE_IOS_TVOS && BA_XCODE_BUILD + return BallisticaKit::UIKitFromCpp::getDeviceName(); +#endif + return CorePlatform::DoGetDeviceName(); +} + +auto CorePlatformApple::DoGetDeviceDescription() -> std::string { +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD + return BallisticaKit::CocoaFromCpp::getDeviceModelName(); +#endif + return CorePlatform::DoGetDeviceDescription(); +} + +auto CorePlatformApple::GetOSVersionString() -> std::string { +#if BA_XCODE_BUILD + return BallisticaKit::FromCpp::getOSVersion(); +#endif + return CorePlatform::GetOSVersionString(); +} + // Legacy for device-accounts; don't modify this code. auto CorePlatformApple::GetRealLegacyDeviceUUID(std::string* uuid) -> bool { #if BA_OSTYPE_MACOS && BA_XCODE_BUILD - *uuid = base::AppleUtils::GetMacUUID(); + *uuid = std::string(BallisticaKit::CocoaFromCpp::getLegacyDeviceUUID()); return true; #endif #if BA_OSTYPE_IOS_TVOS - *uuid = base::AppleUtils::GetIOSUUID(); + *uuid = std::string(BallisticaKit::UIKitFromCpp::getLegacyDeviceUUID()); + // *uuid = base::AppleUtils::GetIOSUUID(); return true; #endif return false; @@ -69,14 +103,17 @@ auto CorePlatformApple::GetDeviceUUIDInputs() -> std::list { std::list out; #if BA_OSTYPE_MACOS #if BA_XCODE_BUILD - out.push_back(base::AppleUtils::GetMacUUID()); + out.push_back( + std::string(BallisticaKit::CocoaFromCpp::getLegacyDeviceUUID())); #else // BA_XCODE_BUILD out.push_back(GetMacUUIDFallback()); #endif // BA_XCODE_BUILD #endif // BA_OSTYPE_MACOS #if BA_OSTYPE_IOS_TVOS - out.push_back(base::AppleUtils::GetIOSUUID()); + // out.push_back(base::AppleUtils::GetIOSUUID()); + out.push_back( + std::string(BallisticaKit::UIKitFromCpp::getLegacyDeviceUUID())); #endif return out; } @@ -96,28 +133,13 @@ auto CorePlatformApple::DoGetConfigDirectoryMonolithicDefault() printf("FIXME: get proper default-config-dir\n"); return std::string(getenv("HOME")) + "/Library"; #elif BA_OSTYPE_MACOS && BA_XCODE_BUILD - return base::AppleUtils::GetApplicationSupportPath() + "/BallisticaKit"; + return std::string(BallisticaKit::CocoaFromCpp::getApplicationSupportPath()) + + "/BallisticaKit"; #else return CorePlatform::DoGetConfigDirectoryMonolithicDefault(); #endif } -auto CorePlatformApple::GetLocale() -> std::string { -#if BA_XCODE_BUILD - return BallisticaKit::FromCppGetLocaleString(); -#else - return CorePlatform::GetLocale(); -#endif -} - -auto CorePlatformApple::DoGetDeviceName() -> std::string { -#if BA_OSTYPE_MACOS && BA_XCODE_BUILD - return base::AppleUtils::GetDeviceName(); -#else - return CorePlatform::DoGetDeviceName(); -#endif -} - auto CorePlatformApple::DoHasTouchScreen() -> bool { #if BA_OSTYPE_IOS return true; @@ -128,13 +150,14 @@ auto CorePlatformApple::DoHasTouchScreen() -> bool { auto CorePlatformApple::GetDefaultUIScale() -> UIScale { #if BA_OSTYPE_IOS - if (base::AppleUtils::IsTablet()) { + if (BallisticaKit::UIKitFromCpp::isTablet()) { + // if (base::AppleUtils::IsTablet()) { return UIScale::kMedium; } else { return UIScale::kSmall; } #else - // Default case handles mac & tvos. + // The default case handles mac & tvos. return CorePlatform::GetDefaultUIScale(); #endif } @@ -163,37 +186,37 @@ void CorePlatformApple::EmitPlatformLog(const std::string& name, LogLevel level, auto CorePlatformApple::DoGetDataDirectoryMonolithicDefault() -> std::string { #if BA_XCODE_BUILD - return BallisticaKit::FromCppGetResourcesPath(); + return BallisticaKit::FromCpp::getResourcesPath(); #else // Fall back to default. return CorePlatform::DoGetDataDirectoryMonolithicDefault(); #endif } -void CorePlatformApple::GetTextBoundsAndWidth(const std::string& text, Rect* r, - float* width) { -#if BA_XCODE_BUILD && !BA_HEADLESS_BUILD - base::AppleUtils::GetTextBoundsAndWidth(text, r, width); -#else - CorePlatform::GetTextBoundsAndWidth(text, r, width); +#if BA_XCODE_BUILD +class TextTextureWrapper_ { + public: + TextTextureWrapper_(int width, int height, + const std::vector& strings, + const std::vector& positions, + const std::vector& widths, float scale) + : data{BallisticaKit::TextTextureData::init(width, height, strings, + positions, widths, scale)} {} + BallisticaKit::TextTextureData data; +}; #endif -} - -void CorePlatformApple::FreeTextTexture(void* tex) { -#if BA_XCODE_BUILD && !BA_HEADLESS_BUILD - base::AppleUtils::FreeTextTexture(tex); -#else - CorePlatform::FreeTextTexture(tex); -#endif -} auto CorePlatformApple::CreateTextTexture( int width, int height, const std::vector& strings, const std::vector& positions, const std::vector& widths, float scale) -> void* { #if BA_XCODE_BUILD && !BA_HEADLESS_BUILD - return base::AppleUtils::CreateTextTexture(width, height, strings, positions, - widths, scale); + auto* wrapper = + new TextTextureWrapper_(width, height, strings, positions, widths, scale); + // wrapper->old = base::AppleUtils::CreateTextTexture(width, height, strings, + // positions, widths, + // scale); + return wrapper; #else return CorePlatform::CreateTextTexture(width, height, strings, positions, widths, scale); @@ -202,16 +225,50 @@ auto CorePlatformApple::CreateTextTexture( auto CorePlatformApple::GetTextTextureData(void* tex) -> uint8_t* { #if BA_XCODE_BUILD && !BA_HEADLESS_BUILD - return base::AppleUtils::GetTextTextureData(tex); + auto* wrapper = static_cast(tex); + return static_cast(wrapper->data.getTextTextureData()); + // return base::AppleUtils::GetTextTextureData(wrapper->old); #else return CorePlatform::GetTextTextureData(tex); #endif } +void CorePlatformApple::GetTextBoundsAndWidth(const std::string& text, Rect* r, + float* width) { +#if BA_XCODE_BUILD && !BA_HEADLESS_BUILD + + auto vals = BallisticaKit::TextTextureData::getTextBoundsAndWidth(text); + assert(vals.getCount() == 5); + r->l = vals[0]; + r->r = vals[1]; + r->b = vals[2]; + r->t = vals[3]; + *width = vals[4]; + +// base::AppleUtils::GetTextBoundsAndWidth(text, r, width); +// printf("GOT BOUNDS l=%.2f r=%.2f b=%.2f t=%.2f w=%.2f\n", r->l, r->r, r->b, +// r->t, *width); printf("SWIFT BOUNDS l=%.2f r=%.2f b=%.2f t=%.2f w=%.2f\n", +// vals[0], vals[1], vals[2], vals[3], vals[4]); +#else + CorePlatform::GetTextBoundsAndWidth(text, r, width); +#endif +} + +void CorePlatformApple::FreeTextTexture(void* tex) { +#if BA_XCODE_BUILD && !BA_HEADLESS_BUILD + auto* wrapper = static_cast(tex); + // base::AppleUtils::FreeTextTexture(wrapper->old); + delete wrapper; +#else + CorePlatform::FreeTextTexture(tex); +#endif +} + void CorePlatformApple::SubmitScore(const std::string& game, const std::string& version, int64_t score) { #if BA_USE_GAME_CENTER - base::AppleUtils::SubmitScore(game, version, score); + BallisticaKit::GameCenterContext::submitScore(game, version, score); + // base::AppleUtils::SubmitScore(game, version, score); #else CorePlatform::SubmitScore(game, version, score); #endif @@ -219,7 +276,8 @@ void CorePlatformApple::SubmitScore(const std::string& game, void CorePlatformApple::ReportAchievement(const std::string& achievement) { #if BA_USE_GAME_CENTER - base::AppleUtils::ReportAchievement(achievement); + BallisticaKit::GameCenterContext::reportAchievement(achievement); + // base::AppleUtils::ReportAchievement(achievement); #else CorePlatform::ReportAchievement(achievement); #endif @@ -227,7 +285,8 @@ void CorePlatformApple::ReportAchievement(const std::string& achievement) { void CorePlatformApple::ResetAchievements() { #if BA_USE_GAME_CENTER - base::AppleUtils::ResetGameCenterAchievements(); + BallisticaKit::GameCenterContext::resetAchievements(); + // base::AppleUtils::ResetGameCenterAchievements(); #else CorePlatform::ResetAchievements(); #endif @@ -236,7 +295,8 @@ void CorePlatformApple::ResetAchievements() { auto CorePlatformApple::HaveLeaderboard(const std::string& game, const std::string& config) -> bool { #if BA_USE_GAME_CENTER - return base::AppleUtils::HaveGameCenterLeaderboard(game, config); + return BallisticaKit::GameCenterContext::haveLeaderboard(game, config); + // return base::AppleUtils::HaveGameCenterLeaderboard(game, config); #else return CorePlatform::HaveLeaderboard(game, config); #endif @@ -246,47 +306,52 @@ void CorePlatformApple::ShowOnlineScoreUI(const std::string& show, const std::string& game, const std::string& game_version) { #if BA_USE_GAME_CENTER - base::AppleUtils::ShowOnlineScoreUI(show, game, game_version); + BallisticaKit::GameCenterContext::showOnlineScoreUI(show, game, game_version); + // base::AppleUtils::ShowOnlineScoreUI(show, game, game_version); #else CorePlatform::ShowOnlineScoreUI(show, game, game_version); #endif } -auto CorePlatformApple::NewAutoReleasePool() -> void* { -#if BA_XCODE_BUILD - return base::AppleUtils::NewAutoReleasePool(); -#else - return CorePlatform::NewAutoReleasePool(); -#endif -} +// auto CorePlatformApple::NewAutoReleasePool() -> void* { +// #if BA_XCODE_BUILD +// return base::AppleUtils::NewAutoReleasePool(); +// #else +// return CorePlatform::NewAutoReleasePool(); +// #endif +// } -void CorePlatformApple::DrainAutoReleasePool(void* pool) { -#if BA_XCODE_BUILD - base::AppleUtils::DrainAutoReleasePool(pool); -#else - CorePlatform::DrainAutoReleasePool(pool); -#endif -} +// void CorePlatformApple::DrainAutoReleasePool(void* pool) { +// #if BA_XCODE_BUILD +// base::AppleUtils::DrainAutoReleasePool(pool); +// #else +// CorePlatform::DrainAutoReleasePool(pool); +// #endif +// } -void CorePlatformApple::GameCenterLogin() { -#if BA_USE_GAME_CENTER - base::AppleUtils::DoGameCenterLogin(); -#else - CorePlatform::GameCenterLogin(); -#endif -} +// void CorePlatformApple::GameCenterLogin() { +// #if BA_USE_GAME_CENTER +// BallisticaKit::GameCenterContext::signIn(); +// // base::AppleUtils::DoGameCenterLogin(); +// #else +// CorePlatform::GameCenterLogin(); +// #endif +// } auto CorePlatformApple::IsOSPlayingMusic() -> bool { #if BA_XCODE_BUILD - return base::AppleUtils::IsMusicPlaying(); + // FIXME - should look into doing this properly these days, or whether + // this is still needed at all. + return false; + // return base::AppleUtils::IsMusicPlaying(); #else return CorePlatform::IsOSPlayingMusic(); #endif } void CorePlatformApple::OpenFileExternally(const std::string& path) { -#if BA_XCODE_BUILD - base::AppleUtils::EditTextFile(path.c_str()); +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD + BallisticaKit::CocoaFromCpp::openFileExternally(path); #else CorePlatform::OpenFileExternally(path); #endif @@ -294,7 +359,7 @@ void CorePlatformApple::OpenFileExternally(const std::string& path) { void CorePlatformApple::OpenDirExternally(const std::string& path) { #if BA_OSTYPE_MACOS && BA_XCODE_BUILD - BallisticaKit::CocoaFromCppOpenDirExternally(path); + BallisticaKit::CocoaFromCpp::openDirExternally(path); #else CorePlatform::OpenDirExternally(path); #endif @@ -302,50 +367,60 @@ void CorePlatformApple::OpenDirExternally(const std::string& path) { void CorePlatformApple::MacMusicAppInit() { #if BA_OSTYPE_MACOS && BA_XCODE_BUILD - base::AppleUtils::MacMusicAppInit(); + BallisticaKit::CocoaFromCpp::macMusicAppInit(); + // base::AppleUtils::MacMusicAppInit(); #else CorePlatform::MacMusicAppInit(); #endif } auto CorePlatformApple::MacMusicAppGetVolume() -> int { #if BA_OSTYPE_MACOS && BA_XCODE_BUILD - return static_cast(base::AppleUtils::MacMusicAppGetVolume()); + return BallisticaKit::CocoaFromCpp::macMusicAppGetVolume(); + // return static_cast(base::AppleUtils::MacMusicAppGetVolume()); #else return CorePlatform::MacMusicAppGetVolume(); #endif } void CorePlatformApple::MacMusicAppSetVolume(int volume) { #if BA_OSTYPE_MACOS && BA_XCODE_BUILD - base::AppleUtils::MacMusicAppSetVolume(volume); + return BallisticaKit::CocoaFromCpp::macMusicAppSetVolume(volume); + // base::AppleUtils::MacMusicAppSetVolume(volume); #else CorePlatform::MacMusicAppSetVolume(volume); #endif } -void CorePlatformApple::MacMusicAppGetLibrarySource() { -#if BA_OSTYPE_MACOS && BA_XCODE_BUILD - base::AppleUtils::MacMusicAppGetLibrarySource(); -#else - CorePlatform::MacMusicAppGetLibrarySource(); -#endif -} + void CorePlatformApple::MacMusicAppStop() { #if BA_OSTYPE_MACOS && BA_XCODE_BUILD - base::AppleUtils::MacMusicAppStop(); + return BallisticaKit::CocoaFromCpp::macMusicAppStop(); + // base::AppleUtils::MacMusicAppStop(); #else CorePlatform::MacMusicAppStop(); #endif } + auto CorePlatformApple::MacMusicAppPlayPlaylist(const std::string& playlist) -> bool { #if BA_OSTYPE_MACOS && BA_XCODE_BUILD - return base::AppleUtils::MacMusicAppPlayPlaylist(playlist.c_str()); + return BallisticaKit::CocoaFromCpp::macMusicAppPlayPlaylist(playlist); + // return base::AppleUtils::MacMusicAppPlayPlaylist(playlist.c_str()); #else return CorePlatform::MacMusicAppPlayPlaylist(playlist); #endif } + auto CorePlatformApple::MacMusicAppGetPlaylists() -> std::list { #if BA_OSTYPE_MACOS && BA_XCODE_BUILD - return base::AppleUtils::MacMusicAppGetPlaylists(); + BallisticaKit::CocoaFromCpp::macMusicAppGetPlaylists(); + // mac_music_app_playlists_.clear(); + // mac_music_app_playlists_.push_back("foof"); + // mac_music_app_playlists_.push_back("barf"); + // std::list out; + // for (auto&& val : vals) { + // out.push_back(std::string(val)); + // } + // return out; + return mac_music_app_playlists(); #else return CorePlatform::MacMusicAppGetPlaylists(); #endif @@ -371,35 +446,14 @@ auto CorePlatformApple::GetSubplatformName() -> std::string { #endif } -auto CorePlatformApple::DoClipboardIsSupported() -> bool { +auto CorePlatformApple::GetLocale() -> std::string { #if BA_XCODE_BUILD - return base::AppleUtils::ClipboardIsSupported(); + if (!locale_.has_value()) { + locale_ = std::string(BallisticaKit::FromCpp::getLocaleString()); + } + return *locale_; #else - return CorePlatform::DoClipboardIsSupported(); -#endif -} - -auto CorePlatformApple::DoClipboardHasText() -> bool { -#if BA_XCODE_BUILD - return base::AppleUtils::ClipboardHasText(); -#else - return CorePlatform::DoClipboardHasText(); -#endif -} - -void CorePlatformApple::DoClipboardSetText(const std::string& text) { -#if BA_XCODE_BUILD - base::AppleUtils::ClipboardSetText(text); -#else - CorePlatform::DoClipboardSetText(text); -#endif -} - -auto CorePlatformApple::DoClipboardGetText() -> std::string { -#if BA_XCODE_BUILD - return base::AppleUtils::ClipboardGetText(); -#else - return CorePlatform::DoClipboardGetText(); + return CorePlatform::GetLocale(); #endif } diff --git a/src/ballistica/core/platform/apple/core_platform_apple.h b/src/ballistica/core/platform/apple/core_platform_apple.h index 6419215d..3d8bc6dd 100644 --- a/src/ballistica/core/platform/apple/core_platform_apple.h +++ b/src/ballistica/core/platform/apple/core_platform_apple.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -21,8 +22,6 @@ class CorePlatformApple : public CorePlatform { auto GenerateUUID() -> std::string override; auto DoGetConfigDirectoryMonolithicDefault() -> std::optional override; - auto GetLocale() -> std::string override; - auto DoGetDeviceName() -> std::string override; auto DoHasTouchScreen() -> bool override; auto GetDefaultUIScale() -> UIScale override; auto IsRunningOnDesktop() -> bool override; @@ -44,33 +43,31 @@ class CorePlatformApple : public CorePlatform { -> bool override; void ShowOnlineScoreUI(const std::string& show, const std::string& game, const std::string& game_version) override; - auto NewAutoReleasePool() -> void* override; - void DrainAutoReleasePool(void* pool) override; void ResetAchievements() override; - void GameCenterLogin() override; + // void GameCenterLogin() override; auto IsOSPlayingMusic() -> bool override; void OpenFileExternally(const std::string& path) override; void OpenDirExternally(const std::string& path) override; void MacMusicAppInit() override; auto MacMusicAppGetVolume() -> int override; void MacMusicAppSetVolume(int volume) override; - void MacMusicAppGetLibrarySource() override; void MacMusicAppStop() override; auto MacMusicAppPlayPlaylist(const std::string& playlist) -> bool override; auto MacMusicAppGetPlaylists() -> std::list override; auto GetPlatformName() -> std::string override; auto GetSubplatformName() -> std::string override; - auto DoClipboardIsSupported() -> bool override; - auto DoClipboardHasText() -> bool override; - void DoClipboardSetText(const std::string& text) override; - auto DoClipboardGetText() -> std::string override; auto GetDeviceUUIDInputs() -> std::list override; + auto GetLocale() -> std::string override; + auto DoGetDeviceName() -> std::string override; + auto DoGetDeviceDescription() -> std::string override; + auto GetOSVersionString() -> std::string override; protected: auto DoGetDataDirectoryMonolithicDefault() -> std::string override; private: + std::optional locale_; }; } // namespace ballistica::core diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc index 35a9e4e1..e20c5155 100644 --- a/src/ballistica/core/platform/core_platform.cc +++ b/src/ballistica/core/platform/core_platform.cc @@ -112,6 +112,8 @@ void CorePlatform::PostInit() { // this sometimes (mainly on windows). Should look into that // more closely or at least log it somewhere. device_name_ = Utils::GetValidUTF8(DoGetDeviceName().c_str(), "dn"); + device_description_ = + Utils::GetValidUTF8(DoGetDeviceDescription().c_str(), "fc"); ran_base_post_init_ = true; // Are we running in a terminal? @@ -424,6 +426,11 @@ auto CorePlatform::GetDeviceName() -> std::string { return device_name_; } +auto CorePlatform::GetDeviceDescription() -> std::string { + assert(ran_base_post_init_); + return device_description_; +} + auto CorePlatform::DoGetDeviceName() -> std::string { // Check devicename in env_var char* devicename; @@ -439,7 +446,11 @@ auto CorePlatform::DoGetDeviceName() -> std::string { nbuffer[sizeof(nbuffer) - 1] = 0; // Make sure its terminated. return nbuffer; } - return "Untitled Device"; + return "Unnamed Device"; +} + +auto CorePlatform::DoGetDeviceDescription() -> std::string { + return "Unknown Device Type"; } auto CorePlatform::IsRunningOnTV() -> bool { return false; } @@ -567,7 +578,7 @@ auto CorePlatform::GetIsStdinATerminal() -> bool { auto CorePlatform::GetOSVersionString() -> std::string { return ""; } auto CorePlatform::GetLegacyUserAgentString() -> std::string { - std::string device = GetDeviceName(); + std::string device = GetDeviceDescription(); std::string version = GetOSVersionString(); if (!version.empty()) { version = " " + version; @@ -597,7 +608,7 @@ auto CorePlatform::GetLegacyUserAgentString() -> std::string { subplatform = "DeMo"; } else if (g_buildconfig.arcade_build()) { subplatform = "ArCd"; - } else { + } else if (g_buildconfig.test_build()) { subplatform = "TstB"; } @@ -608,10 +619,10 @@ auto CorePlatform::GetLegacyUserAgentString() -> std::string { subplatform += " OnTV"; } - std::string out{std::string("BallisticaKit ") + kEngineVersion + subplatform - + " (" + std::to_string(kEngineBuildNumber) + ") (" - + g_buildconfig.platform_string() + version + "; " + device - + "; " + GetLocale() + ")"}; + std::string out{std::string("BallisticaKit ") + kEngineVersion + " (" + + std::to_string(kEngineBuildNumber) + ")" + subplatform + + " (" + g_buildconfig.platform_string() + version + "; " + + device + "; " + GetLocale() + ")"}; // This gets shipped to various places which might choke on fancy unicode // characters, so let's limit to simple ascii. @@ -721,15 +732,15 @@ auto CorePlatform::DemangleCXXSymbol(const std::string& s) -> std::string { #endif } -auto CorePlatform::NewAutoReleasePool() -> void* { throw Exception(); } +// auto CorePlatform::NewAutoReleasePool() -> void* { throw Exception(); } -void CorePlatform::DrainAutoReleasePool(void* pool) { throw Exception(); } +// void CorePlatform::DrainAutoReleasePool(void* pool) { throw Exception(); } void CorePlatform::ResetAchievements() { Log(LogLevel::kError, "ResetAchievements() unimplemented"); } -void CorePlatform::GameCenterLogin() { throw Exception(); } +// void CorePlatform::GameCenterLogin() { throw Exception(); } void CorePlatform::RunEvents() {} @@ -814,10 +825,6 @@ void CorePlatform::MacMusicAppSetVolume(int volume) { Log(LogLevel::kError, "MacMusicAppSetVolume() unimplemented"); } -void CorePlatform::MacMusicAppGetLibrarySource() { - Log(LogLevel::kError, "MacMusicAppGetLibrarySource() unimplemented"); -} - void CorePlatform::MacMusicAppStop() { Log(LogLevel::kError, "MacMusicAppStop() unimplemented"); } @@ -1053,92 +1060,6 @@ auto CorePlatform::GetCurrentWholeSeconds() -> int64_t { .count(); } -auto CorePlatform::ClipboardIsSupported() -> bool { - // We only call our actual virtual function once. - if (!have_clipboard_is_supported_) { - clipboard_is_supported_ = DoClipboardIsSupported(); - have_clipboard_is_supported_ = true; - } - return clipboard_is_supported_; -} - -auto CorePlatform::ClipboardHasText() -> bool { - // If subplatform says they don't support clipboards, don't even ask. - if (!ClipboardIsSupported()) { - return false; - } - return DoClipboardHasText(); -} - -void CorePlatform::ClipboardSetText(const std::string& text) { - // If subplatform says they don't support clipboards, this is an error. - if (!ClipboardIsSupported()) { - throw Exception("ClipboardSetText called with no clipboard support.", - PyExcType::kRuntime); - } - DoClipboardSetText(text); -} - -auto CorePlatform::ClipboardGetText() -> std::string { - // If subplatform says they don't support clipboards, this is an error. - if (!ClipboardIsSupported()) { - throw Exception("ClipboardGetText called with no clipboard support.", - PyExcType::kRuntime); - } - return DoClipboardGetText(); -} - -auto CorePlatform::DoClipboardIsSupported() -> bool { - // Go through SDL functionality on SDL based platforms; - // otherwise default to no clipboard. -#if BA_SDL_BUILD - return true; -#else - return false; -#endif -} - -auto CorePlatform::DoClipboardHasText() -> bool { - // Go through SDL functionality on SDL based platforms; - // otherwise default to no clipboard. -#if BA_SDL_BUILD - return SDL_HasClipboardText(); -#else - // Shouldn't get here since we default to no clipboard support. - FatalError("Shouldn't get here."); - return false; -#endif -} - -void CorePlatform::DoClipboardSetText(const std::string& text) { - // Go through SDL functionality on SDL based platforms; - // otherwise default to no clipboard. -#if BA_SDL_BUILD - SDL_SetClipboardText(text.c_str()); -#else - // Shouldn't get here since we default to no clipboard support. - FatalError("Shouldn't get here."); -#endif -} - -auto CorePlatform::DoClipboardGetText() -> std::string { - // Go through SDL functionality on SDL based platforms; - // otherwise default to no clipboard. -#if BA_SDL_BUILD - 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; -#else - // Shouldn't get here since we default to no clipboard support. - FatalError("Shouldn't get here."); - return ""; -#endif -} - auto CorePlatform::System(const char* cmd) -> int { // By default can support this everywhere outside of Apple's more // sandboxed platforms (iOS and equivalent). Actually should check diff --git a/src/ballistica/core/platform/core_platform.h b/src/ballistica/core/platform/core_platform.h index 23fd4fae..d57bcb8b 100644 --- a/src/ballistica/core/platform/core_platform.h +++ b/src/ballistica/core/platform/core_platform.h @@ -76,24 +76,6 @@ class CorePlatform { /// requires the path to already exist. auto AbsPath(const std::string& path, std::string* outpath) -> bool; -#pragma mark CLIPBOARD --------------------------------------------------------- - - /// 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; - #pragma mark PRINTING/LOGGING -------------------------------------------------- /// Display a message to any default log for the platform (android log, @@ -134,13 +116,15 @@ class CorePlatform { /// Return the directory where game replay files live. auto GetReplaysDir() -> std::string; - /// Return en_US or whatnot. + /// Return something like `en_US` or whatnot. virtual auto GetLocale() -> std::string; /// Get the older more complex user-agent-string, used for communication - /// with v1 servers/etc. This can go away eventually. - auto GetLegacyUserAgentString() -> std::string; + /// with v1 servers/etc. This should go away eventually. + virtual auto GetLegacyUserAgentString() -> std::string; + /// Return a human readable os version such as "10.4.2". + /// Can return a blank string when not known/relevant. virtual auto GetOSVersionString() -> std::string; /// Set an environment variable as utf8, overwriting if it already exists. @@ -153,6 +137,9 @@ class CorePlatform { /// results, etc. auto GetDeviceName() -> std::string; + /// Return a general identifier for the hardware device. + auto GetDeviceDescription() -> std::string; + /// Get a UUID for use with things like device-accounts. This function /// should not be used for other purposes, should not be modified, and /// eventually should go away after device accounts are phased out. Also, @@ -223,16 +210,15 @@ class CorePlatform { #pragma mark APPLE ------------------------------------------------------------- - virtual auto NewAutoReleasePool() -> void*; - virtual void DrainAutoReleasePool(void* pool); + // virtual auto NewAutoReleasePool() -> void*; + // virtual void DrainAutoReleasePool(void* pool); // FIXME: Can we consolidate these with the general music playback calls? virtual void MacMusicAppInit(); virtual auto MacMusicAppGetVolume() -> int; virtual void MacMusicAppSetVolume(int volume); - virtual void MacMusicAppGetLibrarySource(); - virtual void MacMusicAppStop(); - virtual auto MacMusicAppPlayPlaylist(const std::string& playlist) -> bool; virtual auto MacMusicAppGetPlaylists() -> std::list; + virtual auto MacMusicAppPlayPlaylist(const std::string& playlist) -> bool; + virtual void MacMusicAppStop(); #pragma mark TEXT RENDERING ---------------------------------------------------- @@ -253,7 +239,7 @@ class CorePlatform { virtual void SignInV1(const std::string& account_type); virtual void SignOutV1(); - virtual void GameCenterLogin(); + // virtual void GameCenterLogin(); virtual void V1LoginDidChange(); /// Returns the ID to use for the device account. @@ -414,6 +400,11 @@ class CorePlatform { /// Are we being run from a terminal? (should we show prompts, etc?). auto is_stdin_a_terminal() const { return is_stdin_a_terminal_; } + void set_music_app_playlists(const std::list& playlists) { + mac_music_app_playlists_ = playlists; + } + auto mac_music_app_playlists() const { return mac_music_app_playlists_; } + protected: /// Are we being run from a terminal? (should we show prompts, etc?). virtual auto GetIsStdinATerminal() -> bool; @@ -421,9 +412,15 @@ class CorePlatform { /// Called once per platform to determine touchscreen presence. virtual auto DoHasTouchScreen() -> bool; - /// Platforms should override this to provide device name. + /// Platforms should override this to provide a device name suitable for + /// displaying in network join lists/etc. Technically this is more like + /// hostname. virtual auto DoGetDeviceName() -> std::string; + /// Platforms should override this to provide a generic description of the + /// device; something like "iPhone 12 Pro". + virtual auto DoGetDeviceDescription() -> std::string; + /// Attempt to actually create a directory. /// Should *not* raise Exceptions if it already exists or if quiet is true. virtual void DoMakeDir(const std::string& dir, bool quiet); @@ -456,11 +453,6 @@ class CorePlatform { /// Generate a random UUID string. virtual auto GenerateUUID() -> std::string; - virtual auto DoClipboardIsSupported() -> bool; - virtual auto DoClipboardHasText() -> bool; - virtual void DoClipboardSetText(const std::string& text); - virtual auto DoClipboardGetText() -> std::string; - /// Print a log message to be included in crash logs or other debug /// mechanisms (example: Crashlytics). V1-cloud-log messages get forwarded /// to here as well. It can be useful to call this directly to report extra @@ -468,25 +460,26 @@ class CorePlatform { /// 'noteworthy' or presented to the user as standard Log() calls are. virtual void HandleDebugLog(const std::string& msg); - protected: CorePlatform(); virtual ~CorePlatform(); private: - bool is_stdin_a_terminal_{}; - bool have_has_touchscreen_value_{}; - bool have_touchscreen_{}; - bool is_tegra_k1_{}; - bool have_clipboard_is_supported_{}; - bool clipboard_is_supported_{}; - bool made_volatile_data_dir_{}; - bool have_device_uuid_{}; - bool ran_base_post_init_{}; + bool is_stdin_a_terminal_ : 1 {}; + bool have_has_touchscreen_value_ : 1 {}; + bool have_touchscreen_ : 1 {}; + bool is_tegra_k1_ : 1 {}; + bool made_volatile_data_dir_ : 1 {}; + bool have_device_uuid_ : 1 {}; + bool ran_base_post_init_ : 1 {}; millisecs_t start_time_millisecs_{}; std::string device_name_; + std::string device_description_; std::string legacy_device_uuid_; std::string volatile_data_dir_; std::string replays_dir_; + + // temp. + std::list mac_music_app_playlists_; }; /// For capturing and printing stack-traces and related errors. Platforms diff --git a/src/ballistica/core/platform/linux/core_platform_linux.cc b/src/ballistica/core/platform/linux/core_platform_linux.cc index 855503ac..c6e32e87 100644 --- a/src/ballistica/core/platform/linux/core_platform_linux.cc +++ b/src/ballistica/core/platform/linux/core_platform_linux.cc @@ -3,8 +3,10 @@ #if BA_OSTYPE_LINUX #include "ballistica/core/platform/linux/core_platform_linux.h" -#include +#include +#include +#include #include namespace ballistica::core { @@ -29,6 +31,53 @@ std::string CorePlatformLinux::GenerateUUID() { return val; } +auto CorePlatformLinux::DoGetDeviceDescription() -> std::string { + // Let's look for something pretty like "Ubuntu 20.04", etc. + FILE* file = fopen("/etc/os-release", "r"); + std::optional out; + if (file != NULL) { + char line[256]; // Adjust the buffer size as needed + + while (fgets(line, sizeof(line), file)) { + if (strstr(line, "PRETTY_NAME=") != nullptr) { + // Extract the distribution name and version + char* start = strchr(line, '"'); + char* end = strrchr(line, '"'); + if (start != nullptr && end != nullptr) { + *end = '\0'; // Remove the trailing quote + out = start + 1; + } + break; + } + } + fclose(file); + } + if (out.has_value()) { + return *out; + } + return CorePlatform::GetDeviceDescription(); +} + +auto CorePlatformLinux::GetOSVersionString() -> std::string { + std::optional out; + struct utsname uts; + if (uname(&uts) == 0) { + out = uts.release; + + // Try to parse 3 version numbers. + unsigned int major, minor, bugfix; + if (sscanf(uts.release, "%u.%u.%u", &major, &minor, &bugfix) == 3) { + char buf[128]; + snprintf(buf, sizeof(buf), "%.u.%u.%u", major, minor, bugfix); + out = buf; + } + } + if (out.has_value()) { + return *out; + } + return CorePlatform::GetOSVersionString(); +} + auto CorePlatformLinux::GetDeviceUUIDInputs() -> std::list { std::list out; diff --git a/src/ballistica/core/platform/linux/core_platform_linux.h b/src/ballistica/core/platform/linux/core_platform_linux.h index 2bdcbf97..22834b6c 100644 --- a/src/ballistica/core/platform/linux/core_platform_linux.h +++ b/src/ballistica/core/platform/linux/core_platform_linux.h @@ -21,6 +21,8 @@ class CorePlatformLinux : public CorePlatform { auto GetPlatformName() -> std::string override; auto GetSubplatformName() -> std::string override; auto GetDeviceUUIDInputs() -> std::list override; + auto DoGetDeviceDescription() -> std::string override; + auto GetOSVersionString() -> std::string override; }; } // namespace ballistica::core diff --git a/src/ballistica/core/platform/support/min_sdl.h b/src/ballistica/core/platform/support/min_sdl.h index b827e46d..e20e4010 100644 --- a/src/ballistica/core/platform/support/min_sdl.h +++ b/src/ballistica/core/platform/support/min_sdl.h @@ -328,7 +328,7 @@ typedef enum { #define SDLK_SCANCODE_MASK (1 << 30) #define SDL_SCANCODE_TO_KEYCODE(X) (X | SDLK_SCANCODE_MASK) -enum { +enum SDL_KeycodeEnum { SDLK_UNKNOWN = 0, SDLK_RETURN = '\r', diff --git a/src/ballistica/core/platform/windows/core_platform_windows.cc b/src/ballistica/core/platform/windows/core_platform_windows.cc index 8a44a78e..7a3d06ac 100644 --- a/src/ballistica/core/platform/windows/core_platform_windows.cc +++ b/src/ballistica/core/platform/windows/core_platform_windows.cc @@ -814,15 +814,33 @@ std::string CorePlatformWindows::DoGetDeviceName() { wchar_t computer_name[256]; DWORD computer_name_size = 256; int result = GetComputerName(computer_name, &computer_name_size); - if (result == 0) { - device_name = "BallisticaKit Game"; - } else { + if (result != 0) { device_name = UTF8Encode(computer_name); - if (device_name.size() == 0) { - device_name = "BallisticaKit Game"; + if (device_name.size() != 0) { + return device_name; } } - return device_name; + // Fall back on default. + return CorePlatform::DoGetDeviceName(); +} + +std::string CorePlatformWindows::DoGetDeviceDescription() { + std::string device_name; + wchar_t computer_name[256]; + DWORD computer_name_size = 256; + + // We currently return computer name for both the device name + // and description. Is there a way to get a more hardware-y name + // (like manufacturer make/model?) + int result = GetComputerName(computer_name, &computer_name_size); + if (result != 0) { + device_name = UTF8Encode(computer_name); + if (device_name.size() != 0) { + return device_name; + } + } + // Fall back on default. + return CorePlatform::DoGetDeviceDescription(); } bool CorePlatformWindows::DoHasTouchScreen() { return false; } diff --git a/src/ballistica/core/platform/windows/core_platform_windows.h b/src/ballistica/core/platform/windows/core_platform_windows.h index fe77ae71..ede060eb 100644 --- a/src/ballistica/core/platform/windows/core_platform_windows.h +++ b/src/ballistica/core/platform/windows/core_platform_windows.h @@ -40,6 +40,7 @@ class CorePlatformWindows : public CorePlatform { void DoMakeDir(const std::string& dir, bool quiet) override; auto GetLocale() -> std::string override; auto DoGetDeviceName() -> std::string override; + auto DoGetDeviceDescription() -> std::string override; auto DoHasTouchScreen() -> bool override; void EmitPlatformLog(const std::string& name, LogLevel level, const std::string& msg) override; diff --git a/src/ballistica/core/support/base_soft.h b/src/ballistica/core/support/base_soft.h index 31c83d30..1a9ba259 100644 --- a/src/ballistica/core/support/base_soft.h +++ b/src/ballistica/core/support/base_soft.h @@ -44,6 +44,7 @@ class BaseSoftInterface { virtual auto IsAppStarted() const -> bool = 0; virtual auto IsAppBootstrapped() const -> bool = 0; virtual auto GetReturnValue() const -> int = 0; + virtual void PushMainThreadRunnable(Runnable* runnable) = 0; }; } // namespace ballistica::core diff --git a/src/ballistica/scene_v1/node/flag_node.cc b/src/ballistica/scene_v1/node/flag_node.cc index 05b26746..ede8eb98 100644 --- a/src/ballistica/scene_v1/node/flag_node.cc +++ b/src/ballistica/scene_v1/node/flag_node.cc @@ -5,7 +5,6 @@ #include "ballistica/base/dynamics/bg/bg_dynamics_shadow.h" #include "ballistica/base/graphics/component/object_component.h" #include "ballistica/base/graphics/component/simple_component.h" -#include "ballistica/base/graphics/graphics_server.h" #include "ballistica/base/graphics/support/area_of_interest.h" #include "ballistica/base/graphics/support/camera.h" #include "ballistica/scene_v1/assets/scene_texture.h" diff --git a/src/ballistica/scene_v1/python/class/python_class_base_timer.cc b/src/ballistica/scene_v1/python/class/python_class_base_timer.cc index 0e095b7a..faa8f8d6 100644 --- a/src/ballistica/scene_v1/python/class/python_class_base_timer.cc +++ b/src/ballistica/scene_v1/python/class/python_class_base_timer.cc @@ -94,7 +94,7 @@ auto PythonClassBaseTimer::tp_new(PyTypeObject* type, PyObject* args, self->timer_id_ = SceneV1Context::Current().NewTimer( TimeType::kBase, static_cast(length * 1000.0), static_cast(repeat), - Object::New(call_obj)); + Object::New(call_obj).Get()); self->have_timer_ = true; return reinterpret_cast(self); diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_timer.cc b/src/ballistica/scene_v1/python/class/python_class_scene_timer.cc index e505b6ad..f9a8c608 100644 --- a/src/ballistica/scene_v1/python/class/python_class_scene_timer.cc +++ b/src/ballistica/scene_v1/python/class/python_class_scene_timer.cc @@ -100,7 +100,7 @@ auto PythonClassSceneTimer::tp_new(PyTypeObject* type, PyObject* args, self->timer_id_ = SceneV1Context::Current().NewTimer( TimeType::kSim, static_cast(length * 1000.0), static_cast(repeat), - Object::New(call_obj)); + Object::New(call_obj).Get()); self->have_timer_ = true; return reinterpret_cast(self); diff --git a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc index 77b887bb..9cf3846e 100644 --- a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc +++ b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc @@ -6,6 +6,7 @@ #include "ballistica/base/dynamics/bg/bg_dynamics.h" #include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/support/screen_messages.h" #include "ballistica/base/python/base_python.h" #include "ballistica/base/python/class/python_class_simple_sound.h" #include "ballistica/base/python/support/python_context_call_runnable.h" @@ -98,7 +99,7 @@ static auto PyTimer(PyObject* self, PyObject* args, PyObject* keywds) SceneV1Context::Current().NewTimer( TimeType::kSim, static_cast(length * 1000.0), static_cast(repeat), - Object::New(call_obj)); + Object::New(call_obj).Get()); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -207,7 +208,7 @@ static auto PyBaseTimer(PyObject* self, PyObject* args, PyObject* keywds) SceneV1Context::Current().NewTimer( TimeType::kBase, static_cast(length * 1000.0), static_cast(repeat), - Object::New(call_obj)); + Object::New(call_obj).Get()); Py_RETURN_NONE; BA_PYTHON_CATCH; @@ -755,7 +756,7 @@ static auto PyBroadcastMessage(PyObject* self, PyObject* args, PyObject* keywds) } // Now display it locally. - g_base->graphics->AddScreenMessage( + g_base->graphics->screenmessages->AddScreenMessage( message, color, static_cast(top), texture ? texture->texture_data() : nullptr, tint_texture ? tint_texture->texture_data() : nullptr, tint_color, diff --git a/src/ballistica/scene_v1/support/client_input_device_delegate.cc b/src/ballistica/scene_v1/support/client_input_device_delegate.cc index 8c0972ca..1d2ef24d 100644 --- a/src/ballistica/scene_v1/support/client_input_device_delegate.cc +++ b/src/ballistica/scene_v1/support/client_input_device_delegate.cc @@ -29,8 +29,14 @@ void ClientInputDeviceDelegate::AttachToLocalPlayer(Player* player) { } // We also need to send an old-style message as a fallback. + // // FIXME: Can remove this once backwards-compat-protocol is > 29. - { + // + // UPDATE: Only send this if player id fits. This could cause problems + // for older clients in very rare cases, but the only alternative is + // to not support those clients. I doubt there are many such old + // clients out there anyway. + if (player->id() < 256) { std::vector data(3); data[0] = BA_MESSAGE_ATTACH_REMOTE_PLAYER; data[1] = static_cast_check_fit(remote_device_id_); diff --git a/src/ballistica/scene_v1/support/client_session.cc b/src/ballistica/scene_v1/support/client_session.cc index d10e11e4..f796c64c 100644 --- a/src/ballistica/scene_v1/support/client_session.cc +++ b/src/ballistica/scene_v1/support/client_session.cc @@ -5,6 +5,7 @@ #include "ballistica/base/audio/audio.h" #include "ballistica/base/dynamics/bg/bg_dynamics.h" #include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/support/screen_messages.h" #include "ballistica/base/networking/networking.h" #include "ballistica/scene_v1/assets/scene_collision_mesh.h" #include "ballistica/scene_v1/assets/scene_mesh.h" @@ -819,7 +820,7 @@ void ClientSession::Update(int time_advance_millisecs, double time_advance) { std::string s = ReadString(); float f[9]; ReadFloats(9, f); - g_base->graphics->AddScreenMessage( + g_base->graphics->screenmessages->AddScreenMessage( s, Vector3f(f[0], f[1], f[2]), true, texture->texture_data(), tint_texture->texture_data(), Vector3f(f[3], f[4], f[5]), Vector3f(f[6], f[7], f[8])); diff --git a/src/ballistica/scene_v1/support/host_activity.cc b/src/ballistica/scene_v1/support/host_activity.cc index b357fbb7..17fe55bd 100644 --- a/src/ballistica/scene_v1/support/host_activity.cc +++ b/src/ballistica/scene_v1/support/host_activity.cc @@ -178,7 +178,7 @@ void HostActivity::Start() { // Create our step timer - gets called whenever scene should step. step_scene_timer_id_ = host_session->NewTimer(TimeType::kBase, kGameStepMilliseconds, true, - NewLambdaRunnable([this] { StepScene(); })); + NewLambdaRunnable([this] { StepScene(); }).Get()); session_base_timer_ids_.push_back(step_scene_timer_id_); UpdateStepTimerLength(); } @@ -363,7 +363,7 @@ auto HostActivity::globals_node() const -> GlobalsNode* { } auto HostActivity::NewSimTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int { + Runnable* runnable) -> int { if (shutting_down_) { BA_LOG_PYTHON_TRACE_ONCE( "WARNING: Creating game timer during host-activity shutdown"); @@ -384,7 +384,7 @@ auto HostActivity::NewSimTimer(millisecs_t length, bool repeat, } auto HostActivity::NewBaseTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int { + Runnable* runnable) -> int { if (shutting_down_) { BA_LOG_PYTHON_TRACE_ONCE( "WARNING: Creating session-time timer during host-activity shutdown"); @@ -543,10 +543,10 @@ void HostActivity::DumpFullState(SessionStream* out) { } auto HostActivity::NewTimer(TimeType timetype, TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int { + Runnable* runnable) -> int { // Make sure the runnable passed in is reference-managed already. // (we may not add an initial reference ourself) - assert(runnable.IsValidManagedObject()); + assert(Object::IsValidManagedObject(runnable)); // We currently support game and base timers. switch (timetype) { diff --git a/src/ballistica/scene_v1/support/host_activity.h b/src/ballistica/scene_v1/support/host_activity.h index 5f583ff9..507e75d0 100644 --- a/src/ballistica/scene_v1/support/host_activity.h +++ b/src/ballistica/scene_v1/support/host_activity.h @@ -25,7 +25,7 @@ class HostActivity : public SceneV1Context { // ContextTarget time/timer support. auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int override; + Runnable* runnable) -> int override; void DeleteTimer(TimeType timetype, int timer_id) override; auto GetTime(TimeType timetype) -> millisecs_t override; @@ -77,11 +77,9 @@ class HostActivity : public SceneV1Context { private: void HandleOutOfBoundsNodes(); - auto NewSimTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int; + auto NewSimTimer(millisecs_t length, bool repeat, Runnable* runnable) -> int; void DeleteSimTimer(int timer_id); - auto NewBaseTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int; + auto NewBaseTimer(millisecs_t length, bool repeat, Runnable* runnable) -> int; void DeleteBaseTimer(int timer_id); void UpdateStepTimerLength(); void StepScene(); diff --git a/src/ballistica/scene_v1/support/host_session.cc b/src/ballistica/scene_v1/support/host_session.cc index a5d04506..9c726733 100644 --- a/src/ballistica/scene_v1/support/host_session.cc +++ b/src/ballistica/scene_v1/support/host_session.cc @@ -40,7 +40,7 @@ HostSession::HostSession(PyObject* session_type_obj) // Create a timer to step our session scene. step_scene_timer_ = base_timers_.NewTimer(base_time_millisecs_, kGameStepMilliseconds, 0, -1, - NewLambdaRunnable([this] { StepScene(); })); + NewLambdaRunnable([this] { StepScene(); }).Get()); // Set up our output-stream, which will go to a replay and/or the network. // We don't dump to a replay if we're doing the main menu; that replay @@ -766,8 +766,8 @@ void HostSession::GetCorrectionMessages( } auto HostSession::NewTimer(TimeType timetype, TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int { - assert(runnable.IsValidManagedObject()); + Runnable* runnable) -> int { + assert(Object::IsValidManagedObject(runnable)); // We currently support game and base timers. switch (timetype) { diff --git a/src/ballistica/scene_v1/support/host_session.h b/src/ballistica/scene_v1/support/host_session.h index d17de58d..cf53cccf 100644 --- a/src/ballistica/scene_v1/support/host_session.h +++ b/src/ballistica/scene_v1/support/host_session.h @@ -38,7 +38,7 @@ class HostSession : public Session { // ContextTarget time/timer support auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int override; + Runnable* runnable) -> int override; void DeleteTimer(TimeType timetype, int timer_id) override; auto GetTime(TimeType timetype) -> millisecs_t override; diff --git a/src/ballistica/scene_v1/support/scene_v1_app_mode.cc b/src/ballistica/scene_v1/support/scene_v1_app_mode.cc index 9fec9239..59b25ae8 100644 --- a/src/ballistica/scene_v1/support/scene_v1_app_mode.cc +++ b/src/ballistica/scene_v1/support/scene_v1_app_mode.cc @@ -11,7 +11,6 @@ #include "ballistica/base/python/base_python.h" #include "ballistica/base/support/app_config.h" #include "ballistica/base/support/plus_soft.h" -#include "ballistica/base/ui/ui.h" #include "ballistica/scene_v1/connection/connection_set.h" #include "ballistica/scene_v1/connection/connection_to_client_udp.h" #include "ballistica/scene_v1/connection/connection_to_host.h" diff --git a/src/ballistica/scene_v1/support/scene_v1_context.cc b/src/ballistica/scene_v1/support/scene_v1_context.cc index d4f8f9c5..fa3becf2 100644 --- a/src/ballistica/scene_v1/support/scene_v1_context.cc +++ b/src/ballistica/scene_v1/support/scene_v1_context.cc @@ -55,12 +55,10 @@ auto SceneV1Context::GetAsHostActivity() -> HostActivity* { return nullptr; } auto SceneV1Context::GetMutableScene() -> Scene* { return nullptr; } auto SceneV1Context::NewTimer(TimeType timetype, TimerMedium length, - bool repeat, - const Object::Ref& runnable) -> int { + bool repeat, Runnable* runnable) -> int { // Make sure the passed runnable has a ref-count already // (don't want them to rely on us to create initial one). - assert(runnable.Exists()); - assert(Object::IsValidManagedObject(runnable.Get())); + assert(Object::IsValidManagedObject(runnable)); switch (timetype) { case TimeType::kSim: diff --git a/src/ballistica/scene_v1/support/scene_v1_context.h b/src/ballistica/scene_v1/support/scene_v1_context.h index 16414ecd..60e27916 100644 --- a/src/ballistica/scene_v1/support/scene_v1_context.h +++ b/src/ballistica/scene_v1/support/scene_v1_context.h @@ -61,7 +61,7 @@ class SceneV1Context : public base::Context { // Default NewTimer implementation throws a descriptive error, so it can // be useful to fall back on for unsupported cases. virtual auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int; + Runnable* runnable) -> int; virtual void DeleteTimer(TimeType timetype, int timer_id); virtual auto GetTexture(const std::string& name) -> Object::Ref; diff --git a/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc b/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc index c516adf1..0afe1ccf 100644 --- a/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc +++ b/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc @@ -73,8 +73,8 @@ void SceneV1InputDeviceDelegate::RequestPlayer() { return; } - // If we have a local host-session, ask it for a player.. otherwise if we have - // a client-session, ask it for a player. + // If we have a local host-session, ask it for a player.. otherwise if we + // have a client-session, ask it for a player. assert(g_base->logic); if (auto* hs = dynamic_cast(appmode->GetForegroundSession())) { { diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index 8a23ce50..40022420 100644 --- a/src/ballistica/shared/ballistica.cc +++ b/src/ballistica/shared/ballistica.cc @@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int { namespace ballistica { // These are set automatically via script; don't modify them here. -const int kEngineBuildNumber = 21491; +const int kEngineBuildNumber = 21583; const char* kEngineVersion = "1.7.28"; const int kEngineApiVersion = 8; diff --git a/src/ballistica/shared/buildconfig/buildconfig_cmake.h b/src/ballistica/shared/buildconfig/buildconfig_cmake.h index d8a338eb..e4d69fcb 100644 --- a/src/ballistica/shared/buildconfig/buildconfig_cmake.h +++ b/src/ballistica/shared/buildconfig/buildconfig_cmake.h @@ -13,9 +13,9 @@ // We currently support regular and client builds on 64 bit mac posix #if __amd64__ -#define BA_PLATFORM_STRING "x86_64_macos" +#define BA_PLATFORM_STRING "macos x86_64" #elif __aarch64__ -#define BA_PLATFORM_STRING "arm64_macos" +#define BA_PLATFORM_STRING "macos arm64" #else #error Unknown processor architecture. #endif @@ -26,16 +26,16 @@ #elif __linux__ #if __amd64__ -#define BA_PLATFORM_STRING "x86_64_linux" +#define BA_PLATFORM_STRING "linux x86_64" #define BA_OSTYPE_LINUX 1 #elif __i386__ -#define BA_PLATFORM_STRING "x86_32_linux" +#define BA_PLATFORM_STRING "linux x86" #define BA_OSTYPE_LINUX 1 #elif __arm__ -#define BA_PLATFORM_STRING "arm_linux" +#define BA_PLATFORM_STRING "linux arm" #define BA_OSTYPE_LINUX 1 #elif __aarch64__ -#define BA_PLATFORM_STRING "arm64_linux" +#define BA_PLATFORM_STRING "linux arm64" #define BA_OSTYPE_LINUX 1 #else diff --git a/src/ballistica/shared/buildconfig/buildconfig_common.h b/src/ballistica/shared/buildconfig/buildconfig_common.h index 7c68aa97..62ff658f 100644 --- a/src/ballistica/shared/buildconfig/buildconfig_common.h +++ b/src/ballistica/shared/buildconfig/buildconfig_common.h @@ -166,6 +166,10 @@ namespace ballistica { #define BA_AMAZON_BUILD 0 #endif +#ifndef BA_STEAM_BUILD +#define BA_STEAM_BUILD 0 +#endif + #ifndef BA_GOOGLE_BUILD #define BA_GOOGLE_BUILD 0 #endif @@ -259,6 +263,7 @@ class BuildConfig { bool google_build() const { return EXPBOOL_(BA_GOOGLE_BUILD); } bool demo_build() const { return EXPBOOL_(BA_DEMO_BUILD); } bool arcade_build() const { return EXPBOOL_(BA_ARCADE_BUILD); } + bool steam_build() const { return EXPBOOL_(BA_STEAM_BUILD); } bool contains_python_dist() const { return EXPBOOL_(BA_CONTAINS_PYTHON_DIST); } diff --git a/src/ballistica/shared/buildconfig/buildconfig_windows_common.h b/src/ballistica/shared/buildconfig/buildconfig_windows_common.h index 86654b77..69c62f06 100644 --- a/src/ballistica/shared/buildconfig/buildconfig_windows_common.h +++ b/src/ballistica/shared/buildconfig/buildconfig_windows_common.h @@ -32,7 +32,16 @@ #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers -#define BA_PLATFORM_STRING "x86_windows" +#if defined(_M_ARM64) +#define BA_PLATFORM_STRING "windows arm64" +#elif defined(_M_IX86) +#define BA_PLATFORM_STRING "windows x86" +#elif defined(_M_X64) +#define BA_PLATFORM_STRING "windows x86_64" +#else +#error unknown cpu architecture +#endif + #define BA_OSTYPE_WINDOWS 1 #define BA_SOCKET_SEND_DATA_TYPE char diff --git a/src/ballistica/shared/foundation/event_loop.cc b/src/ballistica/shared/foundation/event_loop.cc index 9133557b..88b7ad5b 100644 --- a/src/ballistica/shared/foundation/event_loop.cc +++ b/src/ballistica/shared/foundation/event_loop.cc @@ -217,12 +217,12 @@ void EventLoop::WaitForNextEvent_(bool single_cycle) { // If we've got active timers, wait for messages with a timeout so we can // run the next timer payload. if (!suspended_ && timers_.ActiveTimerCount() > 0) { - millisecs_t apptime = g_core->GetAppTimeMillisecs(); - millisecs_t wait_time = timers_.TimeToNextExpire(apptime); + microsecs_t apptime = g_core->GetAppTimeMicrosecs(); + microsecs_t wait_time = timers_.TimeToNextExpire(apptime); if (wait_time > 0) { std::unique_lock lock(thread_message_mutex_); if (thread_messages_.empty()) { - thread_message_cv_.wait_for(lock, std::chrono::milliseconds(wait_time), + thread_message_cv_.wait_for(lock, std::chrono::microseconds(wait_time), [this] { // Go back to sleep on spurious wakeups // if we didn't wind up with any new @@ -251,24 +251,24 @@ void EventLoop::WaitForNextEvent_(bool single_cycle) { // Note to self (Oct '23): can probably kill this at some point, // but am still using some non-ARC objc stuff from logic thread // so should keep it around just a bit longer just in case. -void EventLoop::LoopUpkeep_(bool single_cycle) { - assert(g_core); - // Keep our autorelease pool clean on mac/ios - // FIXME: Should define a CorePlatform::ThreadHelper or something - // so we don't have platform-specific code here. -#if BA_XCODE_BUILD - // Let's not do autorelease pools when being called ad-hoc, - // since in that case we're part of another run loop - // (and its crashing on drain for some reason) - if (!single_cycle) { - if (auto_release_pool_) { - g_core->platform->DrainAutoReleasePool(auto_release_pool_); - auto_release_pool_ = nullptr; - } - auto_release_pool_ = g_core->platform->NewAutoReleasePool(); - } -#endif -} +// void EventLoop::LoopUpkeep_(bool single_cycle) { +// assert(g_core); +// // Keep our autorelease pool clean on mac/ios +// // FIXME: Should define a CorePlatform::ThreadHelper or something +// // so we don't have platform-specific code here. +// #if BA_XCODE_BUILD +// // Let's not do autorelease pools when being called ad-hoc, +// // since in that case we're part of another run loop +// // (and its crashing on drain for some reason) +// if (!single_cycle) { +// if (auto_release_pool_) { +// g_core->platform->DrainAutoReleasePool(auto_release_pool_); +// auto_release_pool_ = nullptr; +// } +// auto_release_pool_ = g_core->platform->NewAutoReleasePool(); +// } +// #endif +//} void EventLoop::RunToCompletion() { Run_(false); } void EventLoop::RunSingleCycle() { Run_(true); } @@ -276,7 +276,7 @@ void EventLoop::RunSingleCycle() { Run_(true); } void EventLoop::Run_(bool single_cycle) { assert(g_core); while (true) { - LoopUpkeep_(single_cycle); + // LoopUpkeep_(single_cycle); WaitForNextEvent_(single_cycle); @@ -298,8 +298,6 @@ void EventLoop::Run_(bool single_cycle) { assert(!suspended_); RunSuspendCallbacks_(); suspended_ = true; - last_suspend_time_ = g_core->GetAppTimeMillisecs(); - messages_since_suspended_ = 0; break; } case ThreadMessage_::Type::kUnsuspend: { @@ -319,7 +317,7 @@ void EventLoop::Run_(bool single_cycle) { } if (!suspended_) { - timers_.Run(g_core->GetAppTimeMillisecs()); + timers_.Run(g_core->GetAppTimeMicrosecs()); RunPendingRunnables_(); } @@ -607,12 +605,12 @@ auto EventLoop::AreEventLoopsSuspended() -> bool { return g_core->event_loops_suspended; } -auto EventLoop::NewTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> Timer* { +auto EventLoop::NewTimer(microsecs_t length, bool repeat, Runnable* runnable) + -> Timer* { assert(g_core); assert(ThreadIsCurrent()); - assert(runnable.Exists()); - return timers_.NewTimer(g_core->GetAppTimeMillisecs(), length, 0, + assert(Object::IsValidManagedObject(runnable)); + return timers_.NewTimer(g_core->GetAppTimeMicrosecs(), length, 0, repeat ? -1 : 0, runnable); } @@ -774,8 +772,8 @@ auto EventLoop::CheckPushRunnableSafety_() -> bool { void EventLoop::AcquireGIL_() { assert(g_base_soft && g_base_soft->InLogicThread()); auto debug_timing{g_core->core_config().debug_timing}; - millisecs_t startms{debug_timing ? core::CorePlatform::GetCurrentMillisecs() - : 0}; + millisecs_t startmillisecs{ + debug_timing ? core::CorePlatform::GetCurrentMillisecs() : 0}; if (py_thread_state_) { PyEval_RestoreThread(py_thread_state_); @@ -783,10 +781,10 @@ void EventLoop::AcquireGIL_() { } if (debug_timing) { - auto duration{core::CorePlatform::GetCurrentMillisecs() - startms}; + auto duration{core::CorePlatform::GetCurrentMillisecs() - startmillisecs}; if (duration > (1000 / 120)) { - Log(LogLevel::kInfo, - "GIL acquire took too long (" + std::to_string(duration) + " ms)."); + Log(LogLevel::kInfo, "GIL acquire took too long (" + + std::to_string(duration) + " millisecs)."); } } } diff --git a/src/ballistica/shared/foundation/event_loop.h b/src/ballistica/shared/foundation/event_loop.h index 40f03631..9bb2d057 100644 --- a/src/ballistica/shared/foundation/event_loop.h +++ b/src/ballistica/shared/foundation/event_loop.h @@ -45,22 +45,17 @@ class EventLoop { auto thread_id() const -> std::thread::id { return thread_id_; } - // Needed in rare cases where we jump physical threads. - // (Our 'main' thread on Android can switch under us as - // rendering contexts are recreated in new threads/etc.) - // void set_thread_id(std::thread::id id) { thread_id_ = id; } - void RunToCompletion(); void RunSingleCycle(); auto identifier() const -> EventLoopID { return identifier_; } - // Register a timer to run on the thread. - auto NewTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> Timer*; + /// Register a timer to run on the thread. + auto NewTimer(microsecs_t length, bool repeat, Runnable* runnable) -> Timer*; Timer* GetTimer(int id); void DeleteTimer(int id); + /// Add a runnable to this thread's event-loop. /// Pass a Runnable that has been allocated with NewUnmanaged(). /// It will be owned and disposed of by the thread. @@ -154,33 +149,28 @@ class EventLoop { void BootstrapThread_(); - void LoopUpkeep_(bool single_cycle); + // void LoopUpkeep_(bool single_cycle); // FIXME: Should generalize this to some sort of PlatformThreadData class. #if BA_XCODE_BUILD - void* auto_release_pool_{}; + // void* auto_release_pool_{}; #endif - bool bootstrapped_{}; - bool writing_tally_{}; - bool suspended_{}; - bool done_{}; - bool acquires_python_gil_{}; EventLoopID identifier_{EventLoopID::kInvalid}; - millisecs_t last_suspend_time_{}; - int listen_sd_{}; - int messages_since_suspended_{}; - millisecs_t last_suspended_message_report_time_{}; - millisecs_t last_complaint_time_{}; - ThreadSource source_; + ThreadSource source_{}; + bool bootstrapped_ : 1 {}; + bool writing_tally_ : 1 {}; + bool suspended_ : 1 {}; + bool done_ : 1 {}; + bool acquires_python_gil_ : 1 {}; std::thread::id thread_id_{}; + std::condition_variable thread_message_cv_; + std::condition_variable client_listener_cv_; std::list> runnables_; std::list suspend_callbacks_; std::list unsuspend_callbacks_; - std::condition_variable thread_message_cv_; - std::condition_variable client_listener_cv_; - std::mutex thread_message_mutex_; std::list thread_messages_; + std::mutex thread_message_mutex_; std::mutex client_listener_mutex_; std::list> data_to_client_; PyThreadState* py_thread_state_{}; diff --git a/src/ballistica/shared/foundation/fatal_error.cc b/src/ballistica/shared/foundation/fatal_error.cc index 71bac44c..967e6a30 100644 --- a/src/ballistica/shared/foundation/fatal_error.cc +++ b/src/ballistica/shared/foundation/fatal_error.cc @@ -5,6 +5,8 @@ #include "ballistica/core/platform/core_platform.h" #include "ballistica/core/support/base_soft.h" #include "ballistica/shared/foundation/logging.h" +#include "ballistica/shared/generic/lambda_runnable.h" +#include "ballistica/shared/python/python.h" namespace ballistica { @@ -131,32 +133,38 @@ void FatalError::DoBlockingFatalErrorDialog(const std::string& message) { // done. if (g_core->InMainThread()) { g_core->platform->BlockingFatalErrorDialog(message); - } else { - printf("FIXME REIMPLEMENT BLOCKING FATAL ERROR FOR BG THREAD\n"); - // bool started{}; - // bool finished{}; - // bool* startedptr{&started}; - // bool* finishedptr{&finished}; - // g_core->main_event_loop()->PushCall([message, startedptr, finishedptr] { - // *startedptr = true; - // g_core->platform->BlockingFatalErrorDialog(message); - // *finishedptr = true; - // }); + } else if (g_base_soft) { + bool started{}; + bool finished{}; + bool* startedptr{&started}; + bool* finishedptr{&finished}; - // // Wait a short amount of time for the main thread to take action. - // // There's a chance that it can't (if threads are paused, if it is - // // blocked on a synchronous call to another thread, etc.) so if we don't - // // see something happening soon, just give up on showing a dialog. - // auto starttime = core::CorePlatform::GetCurrentMillisecs(); - // while (!started) { - // if (core::CorePlatform::GetCurrentMillisecs() - starttime > 1000) { - // return; - // } - // core::CorePlatform::SleepMillisecs(10); - // } - // while (!finished) { - // core::CorePlatform::SleepMillisecs(10); - // } + // If our thread is holding the GIL, release it to give the main + // thread a better chance to get to the point of displaying the fatal error. + if (Python::HaveGIL()) { + Python::PermanentlyReleaseGIL(); + } + g_base_soft->PushMainThreadRunnable( + NewLambdaRunnableUnmanaged([message, startedptr, finishedptr] { + *startedptr = true; + g_core->platform->BlockingFatalErrorDialog(message); + *finishedptr = true; + })); + + // Wait a short amount of time for the main thread to take action. + // There's a chance that it can't (if threads are paused, if it is + // blocked on a synchronous call to another thread, etc.) so if we don't + // see something happening soon, just give up on showing a dialog. + auto starttime = core::CorePlatform::GetCurrentMillisecs(); + while (!started) { + if (core::CorePlatform::GetCurrentMillisecs() - starttime > 3000) { + return; + } + core::CorePlatform::SleepMillisecs(10); + } + while (!finished) { + core::CorePlatform::SleepMillisecs(10); + } } } diff --git a/src/ballistica/shared/foundation/object.h b/src/ballistica/shared/foundation/object.h index c4f39b5d..53b9eeea 100644 --- a/src/ballistica/shared/foundation/object.h +++ b/src/ballistica/shared/foundation/object.h @@ -13,15 +13,8 @@ namespace ballistica { /// Objects supporting strong and weak referencing and thread enforcement. class Object { - protected: - /// Our base constructor is marked protected because we *require* Objects - /// to be dynamically allocated. This allows us extra measures of control - /// over their construction and destruction, and it does not seem that - /// there is a pressing use case for a statically allocated Object that - /// would justify diluting that control. - Object(); - public: + Object(); virtual ~Object(); // Object classes can provide descriptive names for themselves; these are @@ -575,6 +568,10 @@ class Object { [[nodiscard]] static auto New(ARGS&&... args) -> Object::Ref { auto* ptr = new TALLOC(std::forward(args)...); #if BA_DEBUG_BUILD + /// Objects assume they are statically allocated by default; it's up + /// to us to tell them when they're not. + ptr->object_is_static_allocated_ = false; + /// Make sure things aren't creating strong refs to themselves in their /// constructors. if (ptr->object_has_been_strong_reffed_) { @@ -599,6 +596,10 @@ class Object { [[nodiscard]] static auto NewDeferred(ARGS&&... args) -> T* { T* ptr = new T(std::forward(args)...); #if BA_DEBUG_BUILD + /// Objects assume they are statically allocated by default; it's up + /// to us to tell them when they're not. + ptr->object_is_static_allocated_ = false; + /// Make sure things aren't creating strong refs to themselves in their /// constructors. if (ptr->object_has_been_strong_reffed_) { @@ -650,6 +651,9 @@ class Object { [[nodiscard]] static auto NewUnmanaged(ARGS&&... args) -> T* { T* ptr = new T(std::forward(args)...); #if BA_DEBUG_BUILD + /// Objects assume they are statically allocated by default; it's up + /// to us to tell them when they're not. + ptr->object_is_static_allocated_ = false; ptr->object_is_unmanaged_ = true; #endif return ptr; @@ -668,6 +672,7 @@ class Object { auto operator new(size_t size) -> void* { return new char[size]; } void ObjectUpdateForAcquire(); + bool object_is_static_allocated_ : 1 {true}; bool object_has_been_strong_reffed_ : 1 {}; bool object_is_ref_counted_ : 1 {}; bool object_is_pending_deferred_ : 1 {}; diff --git a/src/ballistica/shared/generic/timer_list.cc b/src/ballistica/shared/generic/timer_list.cc index 84dd97f5..7bcf8b27 100644 --- a/src/ballistica/shared/generic/timer_list.cc +++ b/src/ballistica/shared/generic/timer_list.cc @@ -160,10 +160,11 @@ auto TimerList::GetExpiredTimer(TimerMedium target_time) -> Timer* { auto TimerList::NewTimer(TimerMedium current_time, TimerMedium length, TimerMedium offset, int repeat_count, - const Object::Ref& runnable) -> Timer* { + Runnable* runnable) -> Timer* { assert(!are_clearing_); auto* t = new Timer(this, next_timer_id_++, current_time, length, offset, repeat_count); + assert(Object::IsValidManagedObject(runnable)); t->runnable_ = runnable; // Clion (correctly) points out that t may get deallocated in this call, diff --git a/src/ballistica/shared/generic/timer_list.h b/src/ballistica/shared/generic/timer_list.h index 6c0957e2..a954b759 100644 --- a/src/ballistica/shared/generic/timer_list.h +++ b/src/ballistica/shared/generic/timer_list.h @@ -22,8 +22,8 @@ class TimerList { // Create a timer with provided runnable. auto NewTimer(TimerMedium current_time, TimerMedium length, - TimerMedium offset, int repeat_count, - const Object::Ref& runnable) -> Timer*; + TimerMedium offset, int repeat_count, Runnable* runnable) + -> Timer*; // Return a timer by its id, or nullptr if the timer no longer exists. auto GetTimer(int id) -> Timer*; diff --git a/src/ballistica/shared/python/python_ref.cc b/src/ballistica/shared/python/python_ref.cc index de883e13..51f0d88c 100644 --- a/src/ballistica/shared/python/python_ref.cc +++ b/src/ballistica/shared/python/python_ref.cc @@ -151,6 +151,12 @@ auto PythonRef::Type() const -> PythonRef { return {PyObject_Type(obj_), PythonRef::kSteal}; } +auto PythonRef::ValueIsNone() const -> bool { + assert(Python::HaveGIL()); + ThrowIfUnset(); + return obj_ == Py_None; +} + auto PythonRef::ValueAsLString() const -> std::string { assert(Python::HaveGIL()); ThrowIfUnset(); diff --git a/src/ballistica/shared/python/python_ref.h b/src/ballistica/shared/python/python_ref.h index 0370dffe..141aeebf 100644 --- a/src/ballistica/shared/python/python_ref.h +++ b/src/ballistica/shared/python/python_ref.h @@ -162,6 +162,8 @@ class PythonRef { /// Return the object's Python type object. auto Type() const -> PythonRef; + auto ValueIsNone() const -> bool; + /// For string and babase.Lstr types, returns a utf8 string. /// Throws an exception for other types. auto ValueAsLString() const -> std::string; diff --git a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc index 6b1cdf49..8b1c6811 100644 --- a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc +++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc @@ -2717,14 +2717,14 @@ static auto PyOpenURL(PyObject* self, PyObject* args, PyObject* keywds) return nullptr; } // Need to pass a self-contained string to a lambda; not a char*. - std::string address2{address}; + std::string address_s{address}; assert(g_base->app_adapter); if (force_internal) { g_base->ui->ShowURL(address); } else { g_base->app_adapter->PushMainThreadCall( - [address2] { g_base->platform->OpenURL(address2); }); + [address_s] { g_base->platform->OpenURL(address_s); }); } Py_RETURN_NONE; BA_PYTHON_CATCH; diff --git a/src/ballistica/ui_v1/python/ui_v1_python.cc b/src/ballistica/ui_v1/python/ui_v1_python.cc index d76ecd34..f12321b0 100644 --- a/src/ballistica/ui_v1/python/ui_v1_python.cc +++ b/src/ballistica/ui_v1/python/ui_v1_python.cc @@ -2,7 +2,6 @@ #include "ballistica/ui_v1/python/ui_v1_python.h" -#include "ballistica/base/assets/assets.h" #include "ballistica/base/audio/audio.h" #include "ballistica/base/input/device/keyboard_input.h" #include "ballistica/base/input/input.h" diff --git a/src/ballistica/ui_v1/ui_v1.cc b/src/ballistica/ui_v1/ui_v1.cc index 30083701..cf8ffc9d 100644 --- a/src/ballistica/ui_v1/ui_v1.cc +++ b/src/ballistica/ui_v1/ui_v1.cc @@ -3,10 +3,8 @@ #include "ballistica/ui_v1/ui_v1.h" #include "ballistica/base/app_mode/app_mode.h" -#include "ballistica/base/audio/audio.h" #include "ballistica/base/graphics/component/empty_component.h" #include "ballistica/base/input/input.h" -#include "ballistica/base/python/base_python.h" #include "ballistica/base/support/app_config.h" #include "ballistica/ui_v1/python/ui_v1_python.h" #include "ballistica/ui_v1/support/root_ui.h" diff --git a/src/ballistica/ui_v1/widget/button_widget.cc b/src/ballistica/ui_v1/widget/button_widget.cc index 25300269..29c6db6a 100644 --- a/src/ballistica/ui_v1/widget/button_widget.cc +++ b/src/ballistica/ui_v1/widget/button_widget.cc @@ -57,7 +57,7 @@ void ButtonWidget::OnRepeatTimerExpired() { DoActivate(true); // Speed up repeats after the first. - repeat_timer_->SetLength(150); + repeat_timer_->SetLength(0.150); } else { repeat_timer_.Clear(); } @@ -165,7 +165,9 @@ void ButtonWidget::Draw(base::RenderPass* pass, bool draw_transparent) { // Account for our icon if we have it. float s_width_available = std::max(30.0f, width_ - 30); - if (show_icons) s_width_available -= (34.0f * icon_scale_); + if (show_icons) { + s_width_available -= (34.0f * icon_scale_); + } if ((string_width * string_scale) > s_width_available) { float squish_scale = s_width_available / (string_width * string_scale); @@ -485,8 +487,8 @@ auto ButtonWidget::HandleMessage(const base::WidgetMessage& m) -> bool { pressed_ = true; if (repeat_) { - repeat_timer_ = - base::NewAppTimer(300, true, [this] { OnRepeatTimerExpired(); }); + repeat_timer_ = base::AppTimer::New( + 0.3, true, [this] { OnRepeatTimerExpired(); }); // If we're a repeat button we trigger immediately. // (waiting till mouse up sort of defeats the purpose here) diff --git a/src/ballistica/ui_v1/widget/h_scroll_widget.cc b/src/ballistica/ui_v1/widget/h_scroll_widget.cc index d94c89f8..40b832fc 100644 --- a/src/ballistica/ui_v1/widget/h_scroll_widget.cc +++ b/src/ballistica/ui_v1/widget/h_scroll_widget.cc @@ -405,8 +405,8 @@ auto HScrollWidget::HandleMessage(const base::WidgetMessage& m) -> bool { // Top level touches eventually get passed as mouse-downs if no // scrolling has started. if (static_cast(m.type)) { - touch_delay_timer_ = base::NewAppTimer( - 150, false, [this] { OnTouchDelayTimerExpired(); }); + touch_delay_timer_ = base::AppTimer::New( + 0.150, false, [this] { OnTouchDelayTimerExpired(); }); } // If we're handling a scroll-touch, take note that we need to diff --git a/src/ballistica/ui_v1/widget/scroll_widget.cc b/src/ballistica/ui_v1/widget/scroll_widget.cc index 6e83222f..67ce6d2c 100644 --- a/src/ballistica/ui_v1/widget/scroll_widget.cc +++ b/src/ballistica/ui_v1/widget/scroll_widget.cc @@ -330,8 +330,8 @@ auto ScrollWidget::HandleMessage(const base::WidgetMessage& m) -> bool { // After a short delay we go ahead and handle this as a regular // click if it hasn't turned into a scroll or a child scroll. if (!child_is_scrolling_) { - touch_delay_timer_ = base::NewAppTimer( - 150, false, [this] { OnTouchDelayTimerExpired(); }); + touch_delay_timer_ = base::AppTimer::New( + 0.150, false, [this] { OnTouchDelayTimerExpired(); }); } } } diff --git a/src/ballistica/ui_v1/widget/text_widget.cc b/src/ballistica/ui_v1/widget/text_widget.cc index edeeeaea..61781355 100644 --- a/src/ballistica/ui_v1/widget/text_widget.cc +++ b/src/ballistica/ui_v1/widget/text_widget.cc @@ -2,6 +2,7 @@ #include "ballistica/ui_v1/widget/text_widget.h" +#include "ballistica/base/app_adapter/app_adapter.h" #include "ballistica/base/audio/audio.h" #include "ballistica/base/graphics/component/empty_component.h" #include "ballistica/base/graphics/component/simple_component.h" @@ -467,6 +468,11 @@ void TextWidget::set_res_scale(float res_scale) { void TextWidget::SetText(const std::string& text_in_raw) { std::string text_in = Utils::GetValidUTF8(text_in_raw.c_str(), "twst1"); + // Ignore redundant sets. + if (text_in == text_raw_) { + return; + } + // In some cases we want to make sure this is a valid resource-string // since catching the error here is much more useful than if we catch // it at draw-time. However this is expensive so we only do it for debug @@ -509,10 +515,6 @@ void TextWidget::SetText(const std::string& text_in_raw) { Python::PrintStackTrace(); } } - if (text_in != text_raw_) { - text_translation_dirty_ = true; - } - text_raw_ = text_in; // Do our clamping in unicode-space. if (Utils::UTF8StringLength(text_raw_.c_str()) > max_chars_) { @@ -521,6 +523,8 @@ void TextWidget::SetText(const std::string& text_in_raw) { uni.resize(static_cast(max_chars_)); text_raw_ = Utils::UTF8FromUnicode(uni); } + text_translation_dirty_ = true; + text_raw_ = text_in; carat_position_ = 9999; } @@ -648,10 +652,10 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { // If we're doing inline editing, handle clipboard paste. if (editable() && !ShouldUseStringEditor_() && m.type == base::WidgetMessage::Type::kPaste) { - if (g_core->platform->ClipboardIsSupported()) { - if (g_core->platform->ClipboardHasText()) { + if (g_base->app_adapter->ClipboardIsSupported()) { + if (g_base->app_adapter->ClipboardHasText()) { // Just enter it char by char as if we had typed it... - AddCharsToText_(g_core->platform->ClipboardGetText()); + AddCharsToText_(g_base->app_adapter->ClipboardGetText()); } } } @@ -659,7 +663,6 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { if (m.has_keysym && !ShouldUseStringEditor_()) { last_carat_change_time_millisecs_ = static_cast(g_base->logic->display_time() * 1000.0); - text_group_dirty_ = true; bool claimed = false; switch (m.keysym.sym) { @@ -724,112 +727,29 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { break; } if (!claimed) { - // Pop in a char. - if (editable()) { - claimed = true; - - // #if BA_SDL2_BUILD || BA_MINSDL_BUILD - // // On SDL2, chars come through as TEXT_INPUT messages; - // // can ignore this. - // #else - // std::vector unichars = - // Utils::UnicodeFromUTF8(text_raw_, "2jf987"); - // int len = static_cast(unichars.size()); - - // if (len < max_chars_) { - // if ((m.keysym.unicode >= 32) && (m.keysym.sym != SDLK_TAB)) - // { - // claimed = true; - // int pos = carat_position_; - // if (pos > len) pos = len; - // unichars.insert(unichars.begin() + pos, - // m.keysym.unicode); text_raw_ = - // Utils::UTF8FromUnicode(unichars); text_translation_dirty_ - // = true; carat_position_++; - // } else { - // // These don't seem to come through cleanly as unicode: - // // FIXME - should re-check this on SDL2 builds - - // claimed = true; - // std::string s; - // uint32_t pos = carat_position_; - // if (pos > len) pos = len; - // switch (m.keysym.sym) { - // case SDLK_KP0: - // s = '0'; - // break; - // case SDLK_KP1: - // s = '1'; - // break; - // case SDLK_KP2: - // s = '2'; - // break; - // case SDLK_KP3: - // s = '3'; - // break; - // case SDLK_KP4: - // s = '4'; - // break; - // case SDLK_KP5: - // s = '5'; - // break; - // case SDLK_KP6: - // s = '6'; - // break; - // case SDLK_KP7: - // s = '7'; - // break; - // case SDLK_KP8: - // s = '8'; - // break; - // case SDLK_KP9: - // s = '9'; - // break; - // case SDLK_KP_PERIOD: - // s = '.'; - // break; - // case SDLK_KP_DIVIDE: - // s = '/'; - // break; - // case SDLK_KP_MULTIPLY: - // s = '*'; - // break; - // case SDLK_KP_MINUS: - // s = '-'; - // break; - // case SDLK_KP_PLUS: - // s = '+'; - // break; - // case SDLK_KP_EQUALS: - // s = '='; - // break; - // default: - // break; - // } - // if (s.size() > 0) { - // unichars.insert(unichars.begin() + pos, s[0]); - // text_raw_ = Utils::UTF8FromUnicode(unichars); - // text_translation_dirty_ = true; - // carat_position_++; - // } - // } - // } - // #endif // BA_SDL2_BUILD - } + // Direct text edits come through as seperate events, but we still + // want to claim key down events here; otherwise they'll do weird + // stuff like navigate to other widgets. + claimed = true; } return claimed; } switch (m.type) { case base::WidgetMessage::Type::kTextInput: { - // If we're using an edit dialog, any attempted text input just kicks us - // over to that. - if (editable() && ShouldUseStringEditor_()) { - InvokeStringEditor_(); - } else { - // Otherwise apply the text directly. - if (editable() && m.sval != nullptr) { - AddCharsToText_(*m.sval); - return true; + if (editable()) { + if (ShouldUseStringEditor_()) { + // Normally we shouldn't be getting direct text input events in + // situations where we're using string editors, but it still might + // be possible; for instance if a game controller is driving the + // ui when a key is typed. We simply ignore the event in that case + // because otherwise the text input would be fighting with the + // string-editor. + } else { + // Apply text directly. + if (m.sval != nullptr) { + AddCharsToText_(*m.sval); + return true; + } } } break; diff --git a/src/meta/babasemeta/pyembed/binding_base.py b/src/meta/babasemeta/pyembed/binding_base.py index 95e1da12..ec06b3d5 100644 --- a/src/meta/babasemeta/pyembed/binding_base.py +++ b/src/meta/babasemeta/pyembed/binding_base.py @@ -31,6 +31,7 @@ values = [ _hooks.temporarily_unavailable_message, # kTemporarilyUnavailableMessageCall _hooks.in_progress_message, # kInProgressMessageCall _hooks.error_message, # kErrorMessageCall + _hooks.success_message, # kSuccessMessageCall _hooks.purchase_not_valid_error, # kPurchaseNotValidErrorCall _hooks.purchase_already_in_progress_error, # kPurchaseAlreadyInProgressErrorCall _hooks.orientation_reset_cb_message, # kVROrientationResetCBMessageCall @@ -57,6 +58,7 @@ values = [ _hooks.string_edit_adapter_can_be_replaced, # kStringEditAdapterCanBeReplacedCall _hooks.get_dev_console_tab_names, # kGetDevConsoleTabNamesCall _hooks.unsupported_controller_message, # kUnsupportedControllerMessageCall + _hooks.get_v2_account_id, # kGetV2AccountIdCall _language.Lstr, # kLStrClass _general.Call, # kCallClass _apputils.garbage_collect_session_end, # kGarbageCollectSessionEndCall diff --git a/tools/bacommon/login.py b/tools/bacommon/login.py index c8ec0057..1c32775e 100644 --- a/tools/bacommon/login.py +++ b/tools/bacommon/login.py @@ -11,6 +11,12 @@ if TYPE_CHECKING: pass +# NOTE TO SELF: +# Whenever adding login types here, make sure to update all +# basn nodes before trying to send values through to bamaster, +# as they need to be extractable by basn en route. + + class LoginType(Enum): """Types of logins available.""" @@ -20,6 +26,9 @@ class LoginType(Enum): # Google Play Game Services GPGS = 'gpgs' + # Apple's Game Center + GAME_CENTER = 'game_center' + @property def displayname(self) -> str: """Human readable name for this value.""" @@ -29,3 +38,5 @@ class LoginType(Enum): return 'Email/Password' case cls.GPGS: return 'Google Play Games' + case cls.GAME_CENTER: + return 'Game Center' diff --git a/tools/batools/build.py b/tools/batools/build.py index 682c7cf3..004ab926 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -43,8 +43,8 @@ class PyRequirement: # remove our custom module based stuff soon if nobody complains, which # would free us to theoretically move to a requirements.txt based setup. PY_REQUIREMENTS = [ - PyRequirement(pipname='pylint', minversion=[3, 0, 1]), - PyRequirement(pipname='mypy', minversion=[1, 6, 0]), + PyRequirement(pipname='pylint', minversion=[3, 0, 2]), + PyRequirement(pipname='mypy', minversion=[1, 6, 1]), PyRequirement(pipname='cpplint', minversion=[1, 6, 1]), PyRequirement(pipname='pytest', minversion=[7, 4, 2]), PyRequirement(pipname='pytz', minversion=[2023, 3]), diff --git a/tools/batools/project/_checks.py b/tools/batools/project/_checks.py index 6de16e06..3955fa22 100755 --- a/tools/batools/project/_checks.py +++ b/tools/batools/project/_checks.py @@ -590,4 +590,8 @@ def check_misc(self: ProjectUpdater) -> None: if ( '// V2 Master Server:\n' '\n' '// PROD\n' '#if 1\n' ) not in msconfig: - raise CleanError('Not using prod v2 master server.') + if ( + os.environ.get('BA_ALLOW_NON_PROD_V2_MASTER_SERVER', '0') + != '1' + ): + raise CleanError('Not using prod v2 master server.') diff --git a/tools/batools/pruneincludes.py b/tools/batools/pruneincludes.py index 3a282fa0..b34fa2be 100755 --- a/tools/batools/pruneincludes.py +++ b/tools/batools/pruneincludes.py @@ -60,8 +60,15 @@ class Pruner: entries = self._get_entries() + processed_paths = set[str]() + with tempfile.TemporaryDirectory() as tempdir: for entry in entries: + # Entries list might have repeats. + if entry.file in processed_paths: + continue + processed_paths.add(entry.file) + if not entry.file.startswith(cwd): raise CleanError( f'compile-commands file {entry.file}' diff --git a/tools/batools/xcodeproject.py b/tools/batools/xcodeproject.py index 911efc4f..3a15fcad 100644 --- a/tools/batools/xcodeproject.py +++ b/tools/batools/xcodeproject.py @@ -50,7 +50,7 @@ def update_xcode_project( for p in all_source_files if os.path.splitext(p)[1] in suffixes ), - has_app_delegate_mm=True, + # has_app_delegate_mm=True, projname=projname, ) else: @@ -64,7 +64,7 @@ def update_xcode_project( for p in all_source_files if os.path.splitext(p)[1] in suffixes ), - has_app_delegate_mm=True, + # has_app_delegate_mm=True, projname=projname, ) @@ -83,7 +83,7 @@ class Updater: existing_data: str, sources: list[str], projname: str, - has_app_delegate_mm: bool = False, + # has_app_delegate_mm: bool = False, ) -> None: if not path.endswith('.xcodeproj'): raise RuntimeError(f"Path does not end in .xcodeproj: '{path}'.") @@ -93,7 +93,7 @@ class Updater: self.existing_data = existing_data self.sources = sources self.project = None - self.has_app_delegate_mm = has_app_delegate_mm + # self.has_app_delegate_mm = has_app_delegate_mm # Project name variations. self.pnameu = projname @@ -165,8 +165,8 @@ class Updater: srcgrp = self._get_unique_group(f'{self.pnameu} Shared') self.add_paths(srcgrp) - if self.has_app_delegate_mm: - self.mod_app_delegate_mm() + # if self.has_app_delegate_mm: + # self.mod_app_delegate_mm() # Groups we made should be sorted already since we sorted while # building them, but let's sort the top level group we placed @@ -302,6 +302,7 @@ class Updater: ) return grps[0] + # (No longer used; just leaving here as reference though) def mod_app_delegate_mm(self) -> None: """Set per-file compiler flags.""" files = self.project.get_files_by_name('app_delegate.mm') @@ -415,13 +416,27 @@ class Updater: ) def _target_names_for_file(self, filename: str) -> list[str] | None: - # Cocoa stuff only applies to our macOS build. + # Cocoa stuff only applies to our macOS targets. if filename.startswith('Cocoa') and filename.endswith('.swift'): - return [f'{self.pnameu} macOS'] + return [ + f'{self.pnameu} macOS TestBuild', + f'{self.pnameu} macOS AppStore', + f'{self.pnameu} macOS Steam', + ] + # A few things only for AppStore bound builds. + if filename in {'StoreKitContext.swift', 'GameCenterContext.swift'}: + return [ + f'{self.pnameu} iOS', + f'{self.pnameu} tvOS', + f'{self.pnameu} macOS AppStore', + ] - # UIKit stuff applies to our iOS/tvOS builds. + # UIKit stuff applies to our iOS/tvOS targets. if filename.startswith('UIKit') and filename.endswith('.swift'): - return [f'{self.pnameu} iOS', f'{self.pnameu} tvOS'] + return [ + f'{self.pnameu} iOS', + f'{self.pnameu} tvOS', + ] # Everything else applies to everything. return None diff --git a/tools/efro/error.py b/tools/efro/error.py index f2045ce9..7f902162 100644 --- a/tools/efro/error.py +++ b/tools/efro/error.py @@ -107,7 +107,7 @@ class AuthenticationError(Exception): def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool: """Is the provided exception from urllib a communication-related error? - Url, if provided can provide extra context for when to treat an error + Url, if provided, can provide extra context for when to treat an error as such an error. This should be passed an exception which resulted from opening or diff --git a/tools/efro/message/_receiver.py b/tools/efro/message/_receiver.py index 6659f76b..f8c38783 100644 --- a/tools/efro/message/_receiver.py +++ b/tools/efro/message/_receiver.py @@ -310,7 +310,9 @@ class MessageReceiver: msgtype.__qualname__, ) else: - logging.exception('Error in efro.message handling.') + logging.exception( + 'Error handling raw efro.message. msg=%s', msg + ) return rstr def handle_raw_message_async( @@ -367,7 +369,12 @@ class MessageReceiver: msgtype.__qualname__, ) else: - logging.exception('Error in efro.message handling.') + logging.exception( + 'Error handling raw async efro.message.' + ' msgtype=%s msg_decoded=%s.', + msgtype, + msg_decoded, + ) return rstr async def _handle_raw_message_async( diff --git a/tools/efrotools/pybuild.py b/tools/efrotools/pybuild.py index e561a314..79a6dd7b 100644 --- a/tools/efrotools/pybuild.py +++ b/tools/efrotools/pybuild.py @@ -14,7 +14,7 @@ from efrotools import readfile, writefile, replace_exact # Python version we build here (not necessarily same as we use in repo). PY_VER_ANDROID = '3.11' -PY_VER_EXACT_ANDROID = '3.11.5' +PY_VER_EXACT_ANDROID = '3.11.6' PY_VER_APPLE = '3.11' PY_VER_EXACT_APPLE = '3.11.5' @@ -36,25 +36,25 @@ VERSION_MIN_TVOS = '9.0' # See https://stackoverflow.com/questions/74059978/ # why-is-lldb-generating-exc-bad-instruction-with-user-compiled-library-on-macos # -# For now will try to ride out this LTS version as long as possible. -OPENSSL_VER_APPLE = '3.0.10' -OPENSSL_VER_ANDROID = '3.0.10' +# For now will try to ride out this 3.0 LTS version as long as possible. +OPENSSL_VER_APPLE = '3.0.12' +OPENSSL_VER_ANDROID = '3.0.12' LIBFFI_VER_APPLE = '3.4.4' BZIP2_VER_APPLE = '1.0.8' -XZ_VER_APPLE = '5.4.4' +XZ_VER_APPLE = '5.4.5' # Android repo doesn't seem to be getting updated much so manually # bumping various versions to keep things up to date. ZLIB_VER_ANDROID = '1.3' -XZ_VER_ANDROID = '5.4.4' +XZ_VER_ANDROID = '5.4.5' BZIP2_VER_ANDROID = '1.0.8' GDBM_VER_ANDROID = '1.23' LIBFFI_VER_ANDROID = '3.4.4' -LIBUUID_VER_ANDROID = ('2.38', '2.38.1') -NCURSES_VER_ANDROID = '6.3' +LIBUUID_VER_ANDROID = ('2.39', '2.39.2') +NCURSES_VER_ANDROID = '6.4' READLINE_VER_ANDROID = '8.2' -SQLITE_VER_ANDROID = ('2023', '3430000') +SQLITE_VER_ANDROID = ('2023', '3440000') # Filenames we prune from Python lib dirs in source repo to cut down on # size. diff --git a/tools/efrotools/xcodebuild.py b/tools/efrotools/xcodebuild.py index 718a4dbd..c221ef7b 100644 --- a/tools/efrotools/xcodebuild.py +++ b/tools/efrotools/xcodebuild.py @@ -55,6 +55,7 @@ class SigningConfig: class _Section(Enum): COMPILEC = 'CompileC' + COMPILEXCSTRINGS = 'CompileXCStrings' SWIFTCOMPILE = 'SwiftCompile' SWIFTGENERATEPCH = 'SwiftGeneratePch' SWIFTDRIVER = 'SwiftDriver' @@ -91,6 +92,7 @@ class _Section(Enum): EXTRACTAPPINTENTSMETADATA = 'ExtractAppIntentsMetadata' SWIFTMERGEGENERATEDHEADERS = 'SwiftMergeGeneratedHeaders' GENERATEDSYMFILE = 'GenerateDSYMFile' + GENERATEASSETSYMBOLS = 'GenerateAssetSymbols' class XCodeBuild: @@ -575,6 +577,12 @@ class XCodeBuild: line, ignore_line_starts=['builtin-validationUtility'], ) + elif self._section is _Section.COMPILEXCSTRINGS: + self._print_simple_section_line( + line, + prefix='Compiling strings', + ignore_line_start_tails=['/xcstringstool'], + ) elif self._section is _Section.CONVERTICONSETFILE: self._print_simple_section_line( line, @@ -653,6 +661,9 @@ class XCodeBuild: '/copypng', '/iconutil', ], + ignore_containing=[ + 'note: detected encoding of input file as Unicode (UTF-8)' + ], ) elif self._section is _Section.PROCESSPRODUCTPACKAGING: if '.net.froemling.ballistica.ios"' in line: @@ -689,6 +700,17 @@ class XCodeBuild: '"com.apple.Music.library.read",', ], ) + elif self._section is _Section.GENERATEASSETSYMBOLS: + self._print_simple_section_line( + line, + ignore_containing=[ + '/* com.apple.actool.compilation-results */', + '/GeneratedAssetSymbols-Index.plist', + '/GeneratedAssetSymbols.h', + '/GeneratedAssetSymbols.swift', + ], + ) + elif self._section is _Section.PROCESSPRODUCTPACKAGINGDER: self._print_simple_section_line( line, @@ -944,12 +966,16 @@ class XCodeBuild: prefix_index: int | None = None, ignore_line_starts: list[str] | None = None, ignore_line_start_tails: list[str] | None = None, + ignore_containing: list[str] | None = None, prefix_unexpected: bool = True, ) -> None: + # pylint: disable=too-many-branches if ignore_line_starts is None: ignore_line_starts = [] if ignore_line_start_tails is None: ignore_line_start_tails = [] + if ignore_containing is None: + ignore_containing = [] # First line of the section. if self._section_line_count == 0: @@ -975,6 +1001,8 @@ class XCodeBuild: return if any(splits[0].endswith(tail) for tail in ignore_line_start_tails): return + if any(c in line for c in ignore_containing): + return # Fall back on printing anything we don't recognize. if prefix is None and prefix_unexpected: