more work on v2 and google play

This commit is contained in:
Eric 2022-11-29 11:50:18 -08:00
parent e3f3f28c62
commit 74c9f67582
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
47 changed files with 925 additions and 520 deletions

View File

@ -420,41 +420,41 @@
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/60/ad/38269b7f1c7dc20cb9a506cd0681", "assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/60/ad/38269b7f1c7dc20cb9a506cd0681",
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/72/85/d6fc4d16b7081d91fba2850b5b10", "assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/72/85/d6fc4d16b7081d91fba2850b5b10",
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/e9/ae/1d674d0c086eaa0bd1c3b1db0505", "assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/e9/ae/1d674d0c086eaa0bd1c3b1db0505",
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/89/ec/d472036fbb09f310891761beb39a", "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/b1/c3/2d8b079670d84bde0558f6454f1f",
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/b0/05/e530acaba539f040ce61e22561dc", "assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/b0/05/e530acaba539f040ce61e22561dc",
"assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/61/03/89070ca765e06da3a419a579f503", "assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/61/03/89070ca765e06da3a419a579f503",
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/1f/7f/af259ba9b41556e5e667ad4c646d", "assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/05/87/d3e03edbc59eb7e7da7ef8b17134",
"assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/3c/22/78a56fc40426ab19ad4e76924b78", "assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/12/12/b39bec3a244399223b45f084e0b2",
"assets/build/ba_data/data/languages/croatian.json": "https://files.ballistica.net/cache/ba1/c9/73/01a1343af814131b1ee96af0b687", "assets/build/ba_data/data/languages/croatian.json": "https://files.ballistica.net/cache/ba1/c9/73/01a1343af814131b1ee96af0b687",
"assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/61/20/01291c2cb72b22f204730c0d7574", "assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/cb/df/f2d54d3146a159c678a47d3ff01b",
"assets/build/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/6a/fa/fcf4a804beaff927b0f12c179eaa", "assets/build/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/6a/fa/fcf4a804beaff927b0f12c179eaa",
"assets/build/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/68/93/da8e9874f41a786edf52ba4ccaad", "assets/build/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/68/93/da8e9874f41a786edf52ba4ccaad",
"assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/a0/1d/5fbc922d01521142c2a347b1b024", "assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/74/1d/04985b013519340632a0f5eb6d81",
"assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/4c/c7/0184b8178869d1a3827a1bfcd5bb", "assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/ac/f5/c0922a99e40dfc9f5e026d43b533",
"assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/c7/2e/e0520f58206da01b829e02ff4576", "assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/86/26/060476f46994c035ae0d52640657",
"assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/e8/84/6c9f123e9a0d82fc595c8f55ac7c", "assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/c7/cf/35a6ebc876c7476b72547a914d07",
"assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/8a/09/3e0fa9e44913b53f4dab195d3fae", "assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/06/58/071d6f7bbb5e93a3e074dbd323ae",
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/5f/51/c15d74d2fe4e88ee1e3db0986500", "assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/a3/f7/b12c68a8c1ecabbf7b8c41452986",
"assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/aa/da/dfc8d710af960d7300c7090faeab", "assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/ae/89/47486b987d14f58b6cf2d665ce4b",
"assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/09/55/b50104638f60636af2263877bb7f", "assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/9f/df/469e166c6a0d42bca4baae3a6cb6",
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/d8/f2/aa16bc336bd7660cc86c3264bfc4", "assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/b2/81/53c8cd7617d649403e539c3a6171",
"assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/3a/6b/34714586cb4e9f1b12f8ae54cac8", "assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/26/57/b9443fccbc90602a5ace74935365",
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/12/62/862228b229057877e89fb195d41d", "assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/5f/b2/b9301d67bef699a092a4c04dd522",
"assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/7c/38/d4a44c481757d355836f292ede48", "assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/7c/38/d4a44c481757d355836f292ede48",
"assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/10/13/1228836444f7557211f0058ef9bd", "assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/cd/ed/4d6d0778d256ccc0ff2e27b970c6",
"assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/19/e9/59c891b1fb85f3ba9f19283c233d", "assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/67/93/372c2a2428a830056e9ba22bbf95",
"assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/61/5b/847c03407d1c3a85866833323676", "assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/01/6f/516598d76c29b2fa45ff351426dc",
"assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/d7/06/9d70642d0a4d1e3b1c2149d7a17c", "assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/d7/06/9d70642d0a4d1e3b1c2149d7a17c",
"assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/34/ed/b97350983272e4b23bf140d7a5f4", "assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/05/fb/a05a110238d0da5f902d06e5daa7",
"assets/build/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/4e/91/6f2a9a3ce733908e91377a6ddb9a", "assets/build/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/4e/91/6f2a9a3ce733908e91377a6ddb9a",
"assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/f9/4b/d9f01814224066856695452ef57c", "assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/20/a9/163d189884edf802636bf291e432",
"assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/ce/be/2f06c3436871fd464ff3a62597d9", "assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/c9/48/63093604be4e04447974b9e6337d",
"assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/91/0a/35c4baf539d5951fc03a794c0e0b", "assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/91/0a/35c4baf539d5951fc03a794c0e0b",
"assets/build/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/64/22/7bc899ecbec52cf978a1faf1c127", "assets/build/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/2b/25/aa93983666b88d1c584f06b742b0",
"assets/build/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/f7/df/7ba5f99c5c2c4c86fc0503fcf0b7", "assets/build/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/f7/df/7ba5f99c5c2c4c86fc0503fcf0b7",
"assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/ee/08/1f77c7c320d8d8504a11ee495db3", "assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/bb/f5/0eb74375f3c9ea827b73c67f4a25",
"assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/f3/92/fd7ee5fa8a92fcc8fd2219a88a2f", "assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/1b/83/844f9e6f4e9e47a2d788d3faa9d2",
"assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/2e/86/10d3e39d35014d039cc9ea886ca7", "assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/17/15/973f4101bf2264173bd1c1729426",
"assets/build/ba_data/data/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/1f/ae/abe3f105b3c4b51f6b7942773305", "assets/build/ba_data/data/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/1f/ae/abe3f105b3c4b51f6b7942773305",
"assets/build/ba_data/data/maps/big_g.json": "https://files.ballistica.net/cache/ba1/47/0a/a617cc85d927b576c4e6fc1091ed", "assets/build/ba_data/data/maps/big_g.json": "https://files.ballistica.net/cache/ba1/47/0a/a617cc85d927b576c4e6fc1091ed",
"assets/build/ba_data/data/maps/bridgit.json": "https://files.ballistica.net/cache/ba1/03/4b/57ee9b42854b26f23f81bd8c58ef", "assets/build/ba_data/data/maps/bridgit.json": "https://files.ballistica.net/cache/ba1/03/4b/57ee9b42854b26f23f81bd8c58ef",
@ -4003,50 +4003,50 @@
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e", "assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/1c/77/ac670a5118abdf8a7687af0e159b", "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/1c/77/ac670a5118abdf8a7687af0e159b",
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a", "ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/bd/82/98ea775b22a1113323a1ddf12b6a", "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/0f/f4/4a4f1087d2bb778d9be504442270",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e1/0d/0a431edcdee394a4e4d5b18608d7", "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/0e/10/5c509346d44b89ddd12aec7ec7de",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e8/94/162a944636170ac881d3f3dfd805", "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cd/9f/a6f5274ff94f95e902cca445a510",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f8/96/fea047474c276064176b65f7e48a", "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/04/2a/263e10497930fa6159e5352b7370",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/12/eb/226aba01e295a807614c64c44d40", "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/59/62/28c8370b93d43d1c13acfbddee57",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/23/71/f60c8e90699d887979c4ad26a2e2", "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d8/4e/ad0d3316f74e82ffde5c7b976fc2",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8d/80/05e3d712c67d0fcde0e8605d0be7", "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4a/41/08aa8c9e197e7cc75b6e8d97dc55",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7f/05/498a538fd28fcd1de964c046b8f2", "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/81/6b/e7bc7863fc9c6b6a241d93f7d4e3",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d1/6e/01e46632d17db0597d1aef3394d1", "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ec/44/094cd342e6a8eb2f1671f680a5e8",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/aa/12/7992a25feae2b0bf2c8fe2023187", "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/fc/f9/b44862de39500fc85a3b3960a132",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/99/a9/2b251e46b2e9e4ef143a0308d9f3", "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/78/7d/c4f86fc89838006fa2bacf4fa9a8",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6a/93/faee77acd35111d083998df65aaa", "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d7/40/0d31b629247be2c16ee042ee9165",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/25/c1/9b6efb09c364beae30a40510bfcf", "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/1f/39/781ca02803724b5a84a197d3d264",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/18/76/3f8e144f6727ee8f5f5f4b0b6ddc", "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/39/f9/a085edd64d394ce0f4fee7f857e9",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3c/e8/02284ba36b4de9ca68cdd7c3e689", "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fe/60/82caae9fc89ce89688ea5013e5c9",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/15/cf/c02041bdc6fa5d0042408e591dcf", "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9c/e9/3fb7c6b71287d2b3c3ab5f11f6c3",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/62/98/ee64d80a6332d7d9fc57a2605a2d", "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/c4/df/b58bf2cf14619d257ca145242cdd",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/54/e6/c4c75d29a7c19c34ba5876c4c350", "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/b5/f9/d5ee77a243aee14ad0cd0f5e53e5",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/69/3c/96b8690702f596182a305b5b4489", "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/b8/bc/71febcf1ffd431bd80936d313b73",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/b4/b4/81668b3afad33372276a46545aae", "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/5d/ff/c36c1a0d0713aca6aa0f640de48b",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c3/1a/ae199ada4bf5a649f73fe1663868", "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2f/98/0f10aa690341351b047d9dc3fe65",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a2/4f/2cf4047fdbac4a661ca99d4aedb8", "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/14/aa/d9e350323ed97ca2c87667c569bb",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/06/f5/b0fdcf55008fc53e1660f7bc841f", "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/64/a9/03eb8d0e32bf35b10941eb8ca082",
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2c/af/159cc0021a3751da19f4d6832602", "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/52/2a/31fb94be0f4f5e9ded5a100e298d",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cc/4d/0586cbf47105ba1224a445cd72f4", "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a0/dc/5479a3f5499236fd0b72cbec5c9b",
"build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f8/33/ae7f84447a19e465c134355b359e", "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/41/6b/175b26bc4b4429aac713d4cc3c20",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/40/eb/004a4ea3094210114fd739cb9fcb", "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e0/5c/5a5c6cbd883cba37f31d991722fe",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/45/ed/36918bf061396d6e1f9814afce8c", "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/81/bb/51740409b1cf442d8beed60f759c",
"build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8f/28/7c06af16187bf2db92cf99763f4e", "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a2/7d/c630ea8a7975c3502cbd3ee6716f",
"build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/69/aeba19cb88e6b57c4ab9325f5877", "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/66/a2d065477f9677a2b869c5ebc0a9",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/69/4e/b8354e50de6f2afb45b342919868", "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8d/5e/335032c7fc26a8026db29b0bb1f0",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/70/65/f35430e7328bc7ac30de3960dfc7", "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/26/94/73a12ae50fe3750547ce218f3857",
"build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d8/47/c89b62ea5a71854b303f9e85e5d7", "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9a/77/a3607ad916ab9d0327cdd61800c5",
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f2/19/280f6773b3563e012ad6bfad33dc", "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9d/ac/b2dd3b8a8b231210f19b0ec30299",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1e/17/e5ef6ce0e41360a43c63ef9c0974", "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a6/cd/4b2cb9cbb7a617e408006f2eba7e",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cd/78/1adf82e5c3e456d2ea2d4290c61c", "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9e/f7/cadeae6e408cabe57d9e1cf782d6",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/6b/5f/c5dc0b2a2809bc1d3ea57fb985d0", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/0a/c3/b0d1aa43e3e66de3c213b7fda5f1",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/56/ee/49a4a2d7940fd239a66c04657c90", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/3f/39/fb44f0b1907cc8a9def711f90488",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/39/72/fb9b3400c5d128ad156818ede03d", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/56/d5/e469a881eacc313ac6ca5e48c450",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/22/65/550d27a960822b8846c0c0a440be", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/81/f9/ffcebd4dc6f76392516644b04978",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/33/3b/9b38515580edd4616f9955f7e33c", "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/75/e2/2e4066c5bc8e429723169e8d0ca2",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/fe/1a/80ddcd73df9985ab768e617a6c2b", "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/9c/b9/1702985e444ba529f8adcb933039",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/1d/27/0d69901bb721f986fdbfc490100f", "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/76/ab/cb3cfe64a3a8b976fc15166a7878",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/e9/59/ab5278ceeae3656f91c6d3c68c83", "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/81/c4/b3845fc3fabd2bd3d997685e63f7",
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/25/77/8093dfffddaa80cd513ddaa61867", "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/25/77/8093dfffddaa80cd513ddaa61867",
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/2d/4f/f4fe67827f36cd59cd5193333a02", "src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/2d/4f/f4fe67827f36cd59cd5193333a02",
"src/ballistica/generated/python_embedded/bootstrap_monolithic.inc": "https://files.ballistica.net/cache/ba1/ef/c1/aa5f1aa10af89f5c0b1e616355fd" "src/ballistica/generated/python_embedded/bootstrap_monolithic.inc": "https://files.ballistica.net/cache/ba1/ef/c1/aa5f1aa10af89f5c0b1e616355fd"

View File

@ -73,6 +73,7 @@
<w>allsettings</w> <w>allsettings</w>
<w>allteams</w> <w>allteams</w>
<w>allwarnings</w> <w>allwarnings</w>
<w>alogins</w>
<w>aman</w> <w>aman</w>
<w>amazonaws</w> <w>amazonaws</w>
<w>aname</w> <w>aname</w>
@ -374,6 +375,7 @@
<w>certifi</w> <w>certifi</w>
<w>cfconfig</w> <w>cfconfig</w>
<w>cfenv</w> <w>cfenv</w>
<w>cfgdict</w>
<w>cfgdir</w> <w>cfgdir</w>
<w>cfgkey</w> <w>cfgkey</w>
<w>cfgkeys</w> <w>cfgkeys</w>
@ -2513,6 +2515,7 @@
<w>svne</w> <w>svne</w>
<w>svvv</w> <w>svvv</w>
<w>swht</w> <w>swht</w>
<w>swidth</w>
<w>swiftc</w> <w>swiftc</w>
<w>swip</w> <w>swip</w>
<w>swipsound</w> <w>swipsound</w>
@ -2549,6 +2552,9 @@
<w>targs</w> <w>targs</w>
<w>tasklabel</w> <w>tasklabel</w>
<w>tbegin</w> <w>tbegin</w>
<w>tbfile</w>
<w>tbfiles</w>
<w>tbpath</w>
<w>tbtcolor</w> <w>tbtcolor</w>
<w>tbtn</w> <w>tbtn</w>
<w>tbttxt</w> <w>tbttxt</w>
@ -2860,6 +2866,7 @@
<w>webpages</w> <w>webpages</w>
<w>weeeird</w> <w>weeeird</w>
<w>whatevs</w> <w>whatevs</w>
<w>whatisv</w>
<w>wheee</w> <w>wheee</w>
<w>whos</w> <w>whos</w>
<w>widgetdeathtime</w> <w>widgetdeathtime</w>

View File

@ -1,11 +1,16 @@
### 1.7.14 (build 20934, api 7, 2022-11-16) ### 1.7.14 (build 20942, api 7, 2022-11-29)
- Android Google Play logins now provide V2 accounts with access to all V2 features such as a globally-unique account tag, cloud-console, and workspaces. They should still retain their V1 data as well. - Android Google Play logins now provide V2 accounts with access to all V2 features such as a globally-unique account tag, cloud-console, and workspaces. They should still retain their V1 data as well.
- V2 accounts now have a 'Manage Account' button in the app account window which will sign you into a browser with your current account. - V2 accounts now have a 'Manage Account' button in the app account window which will sign you into a browser with your current account.
- Removed Google App Invite functionality which has been deprecated for a while now. Google Play users can still get tickets by sharing the app via codes (same as other platforms). - Removed Google App Invite functionality which has been deprecated for a while now. Google Play users can still get tickets by sharing the app via codes (same as other platforms).
- Updated Android root-detection library to the latest version. Please holler if you are getting new false 'your device is rooted' errors when trying to play tournaments or anything like that. - Updated Android root-detection library to the latest version. Please holler if you are getting new false 'your device is rooted' errors when trying to play tournaments or anything like that.
- Removed a few obsolete internal functions: `_ba.is_ouya_build()`, `_ba.android_media_scan_file()`. - Removed a few obsolete internal functions: `_ba.is_ouya_build()`, `_ba.android_media_scan_file()`.
- Renaming some methods/data to disambiguate 'login' vs 'sign-in', both in the app and on ballistica.net. Those two terms are somewhat ambiguous and interchangeable in English and can either be a verb or a noun. I'd like to keep things clear in Ballistica by always using 'sign-in' for the verb form and 'login' for the noun. For example: 'You can now sign in to your account using your Google Play login'. - Renaming some methods/data to disambiguate 'login' vs 'sign-in', both in the app and on ballistica.net. Those two terms are somewhat ambiguous and interchangeable in English and can either be a verb or a noun. I'd like to keep things clear in Ballistica by always using 'sign-in' for the verb form and 'login' for the noun. For example: 'You can now sign in to your account using your Google Play login'.
- WARNING: There are currently some rough edges with Google Play V2 accounts; for example Google Play achievements and leaderboards UIs are not currently showing up. I will be cleaning all of this up before the official 1.7.14 release. - Fixed the 'your config is broken' dialog that shows on desktop builds if the game's config file is corrupt and can't be read. It should let you edit the config or replace it with a default.
- `ba.printobjects()` is now `ba.ls_objects()`. It technically logs and doesn't print so the former name was a bit misleading.
- Added `ba.ls_input_devices()` to dump debug info about the current set of input devices. Can be helpful to diagnose mysterious devices joining games unintentionally and things like that.
- Added 'raw' bool arg to `ba.pushcall()`. Passing True for it disables context save/restore and thread checks.
- Added `ba.internal.dump_tracebacks()` which can be used to dump the stack state of all Python threads after some delay. Useful for debugging deadlock; just call right before said deadlock occurs. Results will be logged on the next app launch if they cannot be immediately.
- Fixed a low level event-loop issue that in some cases was preventing the Android version from properly pausing/resuming the app or managing connections while in the background. If you look at the devices section on ballistica.net you should now see your device disappear when you background the app and reappear when you foreground it. Please holler if not.
### 1.7.13 (build 20919, api 7, 2022-11-03) ### 1.7.13 (build 20919, api 7, 2022-11-03)
- Android target-sdk has been updated to 33 (Android 13). Please holler if anything seems broken or is behaving differently than before on Android. - Android target-sdk has been updated to 33 (Android 13). Please holler if anything seems broken or is behaving differently than before on Android.

View File

@ -1 +1 @@
136821726394202151644063370854718971574 199621046220623727886241861967391185247

View File

@ -2429,12 +2429,42 @@ def lock_all_input() -> None:
return None return None
def login_adapter_back_end_active_change(login_type: str, active: bool) -> None:
"""(internal)"""
return None
def login_adapter_get_sign_in_token(login_type: str, attempt_id: int) -> None: def login_adapter_get_sign_in_token(login_type: str, attempt_id: int) -> None:
"""(internal)""" """(internal)"""
return None return None
def ls_input_devices() -> None:
"""Print debugging info about game objects.
Category: **General Utility Functions**
This call only functions in debug builds of the game.
It prints various info about the current object count, etc.
"""
return None
def ls_objects() -> None:
"""Log debugging info about C++ level objects.
Category: **General Utility Functions**
This call only functions in debug builds of the game.
It prints various info about the current object count, etc.
"""
return None
def mac_music_app_get_library_source() -> None: def mac_music_app_get_library_source() -> None:
"""(internal)""" """(internal)"""
@ -2653,32 +2683,17 @@ def printnodes() -> None:
return None return None
def printobjects() -> None:
"""Print debugging info about game objects.
Category: **General Utility Functions**
This call only functions in debug builds of the game.
It prints various info about the current object count, etc.
"""
return None
def pushcall( def pushcall(
call: Callable, call: Callable,
from_other_thread: bool = False, from_other_thread: bool = False,
suppress_other_thread_warning: bool = False, suppress_other_thread_warning: bool = False,
other_thread_use_fg_context: bool = False, other_thread_use_fg_context: bool = False,
raw: bool = False,
) -> None: ) -> None:
"""Pushes a call onto the event loop to be run during the next cycle. """Push a call to the logic event-loop.
Category: **General Utility Functions** Category: **General Utility Functions**
This can be handy for calls that are disallowed from within other
callbacks, etc.
This call expects to be used in the logic thread, and will automatically This call expects to be used in the logic thread, and will automatically
save and restore the ba.Context to behave seamlessly. save and restore the ba.Context to behave seamlessly.
@ -2687,6 +2702,7 @@ def pushcall(
the call will always run in the UI context on the logic thread the call will always run in the UI context on the logic thread
or whichever context is in the foreground if or whichever context is in the foreground if
other_thread_use_fg_context is True. other_thread_use_fg_context is True.
Passing raw=True will disable thread checks and context sets/restores.
""" """
return None return None

View File

@ -252,7 +252,6 @@ def submit_score(
name: Any, name: Any,
score: int | None, score: int | None,
callback: Callable, callback: Callable,
friend_callback: Callable | None,
order: str = 'increasing', order: str = 'increasing',
tournament_id: str | None = None, tournament_id: str | None = None,
score_type: str = 'points', score_type: str = 'points',

View File

@ -43,7 +43,8 @@ from _ba import (
newnode, newnode,
playsound, playsound,
printnodes, printnodes,
printobjects, ls_objects,
ls_input_devices,
pushcall, pushcall,
quit, quit,
rowwidget, rowwidget,
@ -316,7 +317,8 @@ __all__ = [
'print_error', 'print_error',
'print_exception', 'print_exception',
'printnodes', 'printnodes',
'printobjects', 'ls_objects',
'ls_input_devices',
'pushcall', 'pushcall',
'quit', 'quit',
'rowwidget', 'rowwidget',

View File

@ -48,6 +48,9 @@ class AccountV1Subsystem:
_ba.pushcall(do_auto_sign_in) _ba.pushcall(do_auto_sign_in)
def on_app_pause(self) -> None:
"""Should be called when app is pausing."""
def on_app_resume(self) -> None: def on_app_resume(self) -> None:
"""Should be called when the app is resumed.""" """Should be called when the app is resumed."""

View File

@ -4,8 +4,11 @@
from __future__ import annotations from __future__ import annotations
import hashlib
import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from efro.call import tpartial
import _ba import _ba
if TYPE_CHECKING: if TYPE_CHECKING:
@ -15,6 +18,9 @@ if TYPE_CHECKING:
from ba._login import LoginAdapter from ba._login import LoginAdapter
DEBUG_LOG = False
class AccountV2Subsystem: class AccountV2Subsystem:
"""Subsystem for modern account handling in the app. """Subsystem for modern account handling in the app.
@ -37,7 +43,8 @@ class AccountV2Subsystem:
self.login_adapters: dict[LoginType, LoginAdapter] = {} self.login_adapters: dict[LoginType, LoginAdapter] = {}
self._implicit_signed_in_adapter: LoginAdapter | None = None self._implicit_signed_in_adapter: LoginAdapter | None = None
self._auto_signed_in = False self._implicit_state_changed = False
self._can_do_auto_sign_in = True
if _ba.app.platform == 'android' and _ba.app.subplatform == 'google': if _ba.app.platform == 'android' and _ba.app.subplatform == 'google':
from ba._login import LoginAdapterGPGS from ba._login import LoginAdapterGPGS
@ -127,7 +134,7 @@ class AccountV2Subsystem:
def on_implicit_sign_in( def on_implicit_sign_in(
self, login_type: LoginType, login_id: str, display_name: str self, login_type: LoginType, login_id: str, display_name: str
) -> None: ) -> None:
"""An implicit login happened.""" """An implicit sign-in happened (called by native layer)."""
from ba._login import LoginAdapter from ba._login import LoginAdapter
with _ba.Context('ui'): with _ba.Context('ui'):
@ -138,7 +145,7 @@ class AccountV2Subsystem:
) )
def on_implicit_sign_out(self, login_type: LoginType) -> None: def on_implicit_sign_out(self, login_type: LoginType) -> None:
"""An implicit logout happened.""" """An implicit sign-out happened (called by native layer)."""
with _ba.Context('ui'): with _ba.Context('ui'):
self.login_adapters[login_type].set_implicit_login_state(None) self.login_adapters[login_type].set_implicit_login_state(None)
@ -153,6 +160,12 @@ class AccountV2Subsystem:
self._initial_login_completed = True self._initial_login_completed = True
_ba.app.on_initial_login_completed() _ba.app.on_initial_login_completed()
@staticmethod
def _hashstr(val: str) -> str:
md5 = hashlib.md5()
md5.update(val.encode())
return md5.hexdigest()
def on_implicit_login_state_changed( def on_implicit_login_state_changed(
self, self,
login_type: LoginType, login_type: LoginType,
@ -160,18 +173,66 @@ class AccountV2Subsystem:
) -> None: ) -> None:
"""Called when implicit login state changes. """Called when implicit login state changes.
Logins that tend to sign themselves in/out in the background are Login systems that tend to sign themselves in/out in the
considered implicit. We may choose to honor or ignore their states, background are considered implicit. We may choose to honor or
allowing the user to opt for other login types even if the default ignore their states, allowing the user to opt for other login
implicit one can't be explicitly logged out or otherwise controlled. types even if the default implicit one can't be explicitly
logged out or otherwise controlled.
""" """
assert _ba.in_logic_thread() assert _ba.in_logic_thread()
cfg = _ba.app.config
cfgkey = 'ImplicitLoginStates'
cfgdict = _ba.app.config.setdefault(cfgkey, {})
# Store which (if any) adapter is currently implicitly signed in. # Store which (if any) adapter is currently implicitly signed in.
# Making the assumption there will only ever be one implicit
# adapter at a time; may need to update this if that changes.
prev_state = cfgdict.get(login_type.value)
if state is None: if state is None:
self._implicit_signed_in_adapter = None self._implicit_signed_in_adapter = None
new_state = cfgdict[login_type.value] = None
else: else:
self._implicit_signed_in_adapter = self.login_adapters[login_type] self._implicit_signed_in_adapter = self.login_adapters[login_type]
new_state = cfgdict[login_type.value] = self._hashstr(
state.login_id
)
# Special case: if the user is already signed in but not with
# this implicit login, we may want to let them know that the
# 'Welcome back FOO' they likely just saw is not actually
# accurate.
if bool(False):
if (
self.primary is not None
and not self.login_adapters[login_type].is_back_end_active()
):
_ba.timer(
2.0,
tpartial(
_ba.screenmessage,
'Warning: Ignoring your'
' Google Play Games account.\n'
'If you want to use it,'
' sign out of your current account.',
(1, 0.5, 0),
),
)
cfg.commit()
# We want to respond any time the implicit state changes;
# generally this means the user has explicitly signed in/out or
# switched accounts within that back-end.
if prev_state != new_state:
if DEBUG_LOG:
logging.debug(
'AccountV2: Implicit state changed (%s -> %s);'
' will update app sign-in state accordingly.',
prev_state,
new_state,
)
self._implicit_state_changed = True
# We may want to auto-sign-in based on this new state. # We may want to auto-sign-in based on this new state.
self._update_auto_sign_in() self._update_auto_sign_in()
@ -187,12 +248,58 @@ class AccountV2Subsystem:
def _update_auto_sign_in(self) -> None: def _update_auto_sign_in(self) -> None:
from ba._internal import get_v1_account_state from ba._internal import get_v1_account_state
# We attempt auto-sign-in only once. # If implicit state has changed, try to respond.
if self._auto_signed_in: if self._implicit_state_changed:
if self._implicit_signed_in_adapter is None:
# If implicit back-end is signed out, follow suit
# immediately; no need to wait for network connectivity.
if DEBUG_LOG:
logging.debug(
'AccountV2: Signing out as result'
' of implicit state change...',
)
_ba.app.accounts_v2.set_primary_credentials(None)
self._implicit_state_changed = False
# Once we've made a move here we don't want to
# do any more automatic ones.
self._can_do_auto_sign_in = False
else:
# Ok; we've got a new implicit state. If we've got
# connectivity, let's attempt to sign in with it.
# Consider this an 'explicit' sign in because the
# implicit-login state change presumably was triggered
# by some user action (signing in, signing out, or
# switching accounts via the back-end).
# NOTE: should test case where we don't have
# connectivity here.
if _ba.app.cloud.is_connected():
if DEBUG_LOG:
logging.debug(
'AccountV2: Signing in as result'
' of implicit state change...',
)
self._implicit_signed_in_adapter.sign_in(
self._on_explicit_sign_in_completed
)
self._implicit_state_changed = False
# Once we've made a move here we don't want to
# do any more automatic ones.
self._can_do_auto_sign_in = False
if not self._can_do_auto_sign_in:
return return
# If we're not currently signed in, we have connectivity, and # If we're not currently signed in, we have connectivity, and
# we have an available implicit adapter, do an auto-sign-in. # we have an available implicit login, auto-sign-in with it.
# The implicit-state-change logic above should keep things
# mostly in-sync, but due to connectivity or other issues that
# might not always be the case. We prefer to keep people signed
# in as a rule, even if there are corner cases where this might
# not be what they want (A user signing out and then restarting
# may be auto-signed back in).
connected = _ba.app.cloud.is_connected() connected = _ba.app.cloud.is_connected()
signed_in_v1 = get_v1_account_state() == 'signed_in' signed_in_v1 = get_v1_account_state() == 'signed_in'
signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials() signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials()
@ -202,14 +309,44 @@ class AccountV2Subsystem:
and not signed_in_v2 and not signed_in_v2
and self._implicit_signed_in_adapter is not None and self._implicit_signed_in_adapter is not None
): ):
self._auto_signed_in = True # Only attempt this once if DEBUG_LOG:
self._implicit_signed_in_adapter.sign_in(self._on_sign_in_completed) logging.debug(
'AccountV2: Signing in due to on-launch-auto-sign-in...',
)
self._can_do_auto_sign_in = False # Only ATTEMPT once
self._implicit_signed_in_adapter.sign_in(
self._on_implicit_sign_in_completed
)
def _on_sign_in_completed( def _on_explicit_sign_in_completed(
self, self,
adapter: LoginAdapter, adapter: LoginAdapter,
result: LoginAdapter.SignInResult | Exception, result: LoginAdapter.SignInResult | Exception,
) -> None: ) -> None:
"""A sign-in has completed that the user asked for explicitly."""
from ba._language import Lstr
del adapter # Unused.
# Make some noise on errors.
# (May want to make this more descriptive).
if isinstance(result, Exception):
with _ba.Context('ui'):
_ba.screenmessage(
Lstr(resource='errorText'),
color=(1, 1, 0),
)
_ba.playsound(_ba.getsound('error'))
return
_ba.app.accounts_v2.set_primary_credentials(result.credentials)
def _on_implicit_sign_in_completed(
self,
adapter: LoginAdapter,
result: LoginAdapter.SignInResult | Exception,
) -> None:
"""A sign-in has completed that the user didn't ask for explicitly."""
from ba._internal import get_v1_account_state from ba._internal import get_v1_account_state
del adapter # Unused. del adapter # Unused.
@ -219,7 +356,9 @@ class AccountV2Subsystem:
return return
# If we're still connected and still not signed in, # If we're still connected and still not signed in,
# plug in the credentials we got. # plug in the credentials we got. We want to be extra cautious
# in case the user has since explicitly signed in since we
# kicked off.
connected = _ba.app.cloud.is_connected() connected = _ba.app.cloud.is_connected()
signed_in_v1 = get_v1_account_state() == 'signed_in' signed_in_v1 = get_v1_account_state() == 'signed_in'
signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials() signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials()

View File

@ -354,6 +354,7 @@ class App:
from bastd import maps as stdmaps from bastd import maps as stdmaps
from bastd.actor import spazappearance from bastd.actor import spazappearance
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
from ba._apputils import log_dumped_tracebacks
assert _ba.in_logic_thread() assert _ba.in_logic_thread()
@ -407,9 +408,9 @@ class App:
# overwrite a broken one or whatnot and wipe out data. # overwrite a broken one or whatnot and wipe out data.
if not self.config_file_healthy: if not self.config_file_healthy:
if self.platform in ('mac', 'linux', 'windows'): if self.platform in ('mac', 'linux', 'windows'):
from bastd.ui import configerror from bastd.ui.configerror import ConfigErrorWindow
configerror.ConfigErrorWindow() _ba.pushcall(ConfigErrorWindow)
return return
# For now on other systems we just overwrite the bum config. # For now on other systems we just overwrite the bum config.
@ -459,6 +460,9 @@ class App:
'on_app_launch found state %s; expected LAUNCHING.', self.state 'on_app_launch found state %s; expected LAUNCHING.', self.state
) )
# If any traceback dumps happened last run, log and clear them.
log_dumped_tracebacks()
self._launch_completed = True self._launch_completed = True
self._update_state() self._update_state()
@ -483,8 +487,21 @@ class App:
assert _ba.in_logic_thread() assert _ba.in_logic_thread()
if self._app_paused: if self._app_paused:
self.state = self.State.PAUSED # Entering paused state:
if self.state is not self.State.PAUSED:
self.state = self.State.PAUSED
self.cloud.on_app_pause()
self.accounts_v1.on_app_pause()
self.plugins.on_app_pause()
else: else:
# Leaving paused state:
if self.state is self.State.PAUSED:
self.fg_state += 1
self.cloud.on_app_resume()
self.accounts_v1.on_app_resume()
self.music.on_app_resume()
self.plugins.on_app_resume()
if self._initial_login_completed and self._meta_scan_completed: if self._initial_login_completed and self._meta_scan_completed:
self.state = self.State.RUNNING self.state = self.State.RUNNING
if not self._called_on_app_running: if not self._called_on_app_running:
@ -498,19 +515,16 @@ class App:
def on_app_pause(self) -> None: def on_app_pause(self) -> None:
"""Called when the app goes to a suspended state.""" """Called when the app goes to a suspended state."""
assert not self._app_paused # Should avoid redundant calls.
self._app_paused = True self._app_paused = True
self._update_state() self._update_state()
self.plugins.on_app_pause()
def on_app_resume(self) -> None: def on_app_resume(self) -> None:
"""Run when the app resumes from a suspended state.""" """Run when the app resumes from a suspended state."""
assert self._app_paused # Should avoid redundant calls.
self._app_paused = False self._app_paused = False
self._update_state() self._update_state()
self.fg_state += 1
self.accounts_v1.on_app_resume()
self.music.on_app_resume()
self.plugins.on_app_resume()
def on_app_shutdown(self) -> None: def on_app_shutdown(self) -> None:
"""(internal)""" """(internal)"""

View File

@ -5,12 +5,13 @@ from __future__ import annotations
import gc import gc
import os import os
import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba import _ba
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any, TextIO
import ba import ba
@ -260,3 +261,51 @@ def print_corrupt_file_error() -> None:
_ba.timer( _ba.timer(
2.0, Call(_ba.playsound, _ba.getsound('error')), timetype=TimeType.REAL 2.0, Call(_ba.playsound, _ba.getsound('error')), timetype=TimeType.REAL
) )
_tbfiles: list[TextIO] = []
def dump_tracebacks(delay: float) -> None:
"""Dump a traceback of all Python threads after a delay in seconds.
Can be used for debugging deadlock situations. Will dump to a preset
file location in the app config dir. Will attempt to log and clear
the results after dumping. It will be done at next launch otherwise,
or can be done explicitly via log_dumped_tracebacks().
Do not use this call during regular operation of the app; it is only
intended for debugging as it can leak file descriptors/etc.
"""
# pylint: disable=consider-using-with
import faulthandler
from ba._generated.enums import TimeType
tbpath = os.path.join(os.path.dirname(_ba.app.config_file_path), '_tbdump')
# faulthandler needs the raw file descriptor to still be valid when
# it fires, so stuff this into a global var to make sure it doesn't get
# cleaned up.
tbfile = open(tbpath, 'w', encoding='utf-8')
_tbfiles.append(tbfile)
faulthandler.dump_traceback_later(delay, file=tbfile)
# Attempt to log shortly after dumping.
with _ba.Context('ui'):
_ba.timer(delay + 1.0, log_dumped_tracebacks, timetype=TimeType.REAL)
def log_dumped_tracebacks() -> None:
"""If a traceback dump exists, log it and clear it. No-op otherwise."""
try:
tbpath = os.path.join(
os.path.dirname(_ba.app.config_file_path), '_tbdump'
)
if os.path.exists(tbpath):
with open(tbpath, 'r', encoding='utf-8') as infile:
logging.info('Dumped tracebacks:\n%s', infile.read())
os.unlink(tbpath)
except Exception:
logging.exception('Error logging dumped tracebacks.')

View File

@ -47,7 +47,7 @@ def bootstrap() -> None:
# Give a soft warning if we're being used with a different binary # Give a soft warning if we're being used with a different binary
# version than we expect. # version than we expect.
expected_build = 20934 expected_build = 20942
running_build: int = env['build_number'] running_build: int = env['build_number']
if running_build != expected_build: if running_build != expected_build:
print( print(
@ -120,7 +120,8 @@ def bootstrap() -> None:
import __main__ import __main__
# Clear out the standard quit/exit messages since they don't # Clear out the standard quit/exit messages since they don't
# work for us. # work in our embedded situation (should revisit this once we're
# usable from a standard interpreter).
del __main__.__builtins__.quit del __main__.__builtins__.quit
del __main__.__builtins__.exit del __main__.__builtins__.exit

View File

@ -33,6 +33,12 @@ class CloudSubsystem:
""" """
return False # Needs to be overridden return False # Needs to be overridden
def on_app_pause(self) -> None:
"""Should be called when the app pauses."""
def on_app_resume(self) -> None:
"""Should be called when the app resumes."""
def on_connectivity_changed(self, connected: bool) -> None: def on_connectivity_changed(self, connected: bool) -> None:
"""Called when cloud connectivity state changes.""" """Called when cloud connectivity state changes."""
if DEBUG_LOG: if DEBUG_LOG:

View File

@ -11,7 +11,7 @@ from ba._gameactivity import GameActivity
from ba._general import WeakCall from ba._general import WeakCall
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Sequence from typing import Sequence
from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.playerspaz import PlayerSpaz
import ba import ba
@ -56,56 +56,6 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
# Preload achievement images in case we get some. # Preload achievement images in case we get some.
_ba.timer(2.0, WeakCall(self._preload_achievements)) _ba.timer(2.0, WeakCall(self._preload_achievements))
def _show_standard_scores_to_beat_ui(
self, scores: list[dict[str, Any]]
) -> None:
from efro.util import asserttype
from ba._gameutils import timestring, animate
from ba._nodeactor import NodeActor
from ba._generated.enums import TimeFormat
display_type = self.get_score_type()
if scores is not None:
# Sort by originating date so that the most recent is first.
scores.sort(reverse=True, key=lambda s: asserttype(s['time'], int))
# Now make a display for the most recent challenge.
for score in scores:
if score['type'] == 'score_challenge':
tval = (
score['player']
+ ': '
+ timestring(
int(score['value']) * 10,
timeformat=TimeFormat.MILLISECONDS,
).evaluate()
if display_type == 'time'
else str(score['value'])
)
hattach = 'center' if display_type == 'time' else 'left'
halign = 'center' if display_type == 'time' else 'left'
pos = (20, -70) if display_type == 'time' else (20, -130)
txt = NodeActor(
_ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': hattach,
'h_align': halign,
'color': (0.7, 0.4, 1, 1),
'shadow': 0.5,
'flatness': 1.0,
'position': pos,
'scale': 0.6,
'text': tval,
},
)
).autoretain()
assert txt.node is not None
animate(txt.node, 'scale', {1.0: 0.0, 1.1: 0.7, 1.2: 0.6})
break
# FIXME: this is now redundant with activityutils.getscoreconfig(); # FIXME: this is now redundant with activityutils.getscoreconfig();
# need to kill this. # need to kill this.
def get_score_type(self) -> str: def get_score_type(self) -> str:

View File

@ -461,12 +461,12 @@ def login_adapter_get_sign_in_token_response(
) -> None: ) -> None:
"""Login adapter do-sign-in completed.""" """Login adapter do-sign-in completed."""
from bacommon.login import LoginType from bacommon.login import LoginType
from ba._login import LoginAdapterGPGS from ba._login import LoginAdapterNative
login_type = LoginType(login_type_str) login_type = LoginType(login_type_str)
attempt_id = int(attempt_id_str) attempt_id = int(attempt_id_str)
result = None if result_str == '' else result_str result = None if result_str == '' else result_str
with _ba.Context('ui'): with _ba.Context('ui'):
adapter = _ba.app.accounts_v2.login_adapters[login_type] adapter = _ba.app.accounts_v2.login_adapters[login_type]
assert isinstance(adapter, LoginAdapterGPGS) assert isinstance(adapter, LoginAdapterNative)
adapter.on_sign_in_complete(attempt_id=attempt_id, result=result) adapter.on_sign_in_complete(attempt_id=attempt_id, result=result)

View File

@ -104,7 +104,6 @@ def submit_score(
name: Any, name: Any,
score: int | None, score: int | None,
callback: Callable, callback: Callable,
friend_callback: Callable | None,
order: str = 'increasing', order: str = 'increasing',
tournament_id: str | None = None, tournament_id: str | None = None,
score_type: str = 'points', score_type: str = 'points',
@ -125,7 +124,6 @@ def submit_score(
name=name, name=name,
score=score, score=score,
callback=callback, callback=callback,
friend_callback=friend_callback,
order=order, order=order,
tournament_id=tournament_id, tournament_id=tournament_id,
score_type=score_type, score_type=score_type,

View File

@ -102,6 +102,9 @@ class LoginAdapter:
# (possibly) push it to the app for handling. # (possibly) push it to the app for handling.
self._update_implicit_login_state() self._update_implicit_login_state()
# This might affect whether we consider that back-end as 'active'.
self._update_back_end_active()
def set_active_logins(self, logins: dict[LoginType, str]) -> None: def set_active_logins(self, logins: dict[LoginType, str]) -> None:
"""Keep the adapter informed of actively used logins. """Keep the adapter informed of actively used logins.
@ -116,7 +119,7 @@ class LoginAdapter:
logging.debug( logging.debug(
'LoginAdapter: %s adapter got active logins %s.', 'LoginAdapter: %s adapter got active logins %s.',
self.login_type.name, self.login_type.name,
logins, {k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
) )
self._active_login_id = logins.get(self.login_type) self._active_login_id = logins.get(self.login_type)
@ -229,6 +232,10 @@ class LoginAdapter:
# Kick off the process by fetching a sign-in token. # Kick off the process by fetching a sign-in token.
self.get_sign_in_token(completion_cb=_got_sign_in_token_result) self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
def is_back_end_active(self) -> bool:
"""Is this adapter's back-end currently active?"""
return self._back_end_active
def get_sign_in_token( def get_sign_in_token(
self, completion_cb: Callable[[str | None], None] self, completion_cb: Callable[[str | None], None]
) -> None: ) -> None:
@ -289,8 +296,8 @@ class LoginAdapter:
self._back_end_active = is_active self._back_end_active = is_active
class LoginAdapterGPGS(LoginAdapter): class LoginAdapterNative(LoginAdapter):
"""Google Play Game Services adapter.""" """A login adapter that does its work in the native layer."""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__(LoginType.GPGS) super().__init__(LoginType.GPGS)
@ -308,6 +315,9 @@ class LoginAdapterGPGS(LoginAdapter):
self._sign_in_attempt_num += 1 self._sign_in_attempt_num += 1
_ba.login_adapter_get_sign_in_token(self.login_type.value, attempt_id) _ba.login_adapter_get_sign_in_token(self.login_type.value, attempt_id)
def on_back_end_active_change(self, active: bool) -> None:
_ba.login_adapter_back_end_active_change(self.login_type.value, active)
def on_sign_in_complete(self, attempt_id: int, result: str | None) -> None: def on_sign_in_complete(self, attempt_id: int, result: str | None) -> None:
"""Called by the native layer on a completed attempt.""" """Called by the native layer on a completed attempt."""
assert _ba.in_logic_thread() assert _ba.in_logic_thread()
@ -316,3 +326,7 @@ class LoginAdapterGPGS(LoginAdapter):
return return
callback = self._sign_in_attempts.pop(attempt_id) callback = self._sign_in_attempts.pop(attempt_id)
callback(result) callback(result)
class LoginAdapterGPGS(LoginAdapterNative):
"""Google Play Game Services adapter."""

View File

@ -100,6 +100,8 @@ from ba._apputils import (
is_browser_likely_available, is_browser_likely_available,
get_remote_app_name, get_remote_app_name,
should_submit_debug_info, should_submit_debug_info,
dump_tracebacks,
log_dumped_tracebacks,
) )
from ba._benchmark import ( from ba._benchmark import (
run_gpu_benchmark, run_gpu_benchmark,
@ -330,4 +332,6 @@ __all__ = [
'sign_out_v1', 'sign_out_v1',
'sign_in_v1', 'sign_in_v1',
'mark_config_dirty', 'mark_config_dirty',
'dump_tracebacks',
'log_dumped_tracebacks',
] ]

View File

@ -116,7 +116,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
self._newly_complete: bool | None = None self._newly_complete: bool | None = None
self._is_more_levels: bool | None = None self._is_more_levels: bool | None = None
self._next_level_name: str | None = None self._next_level_name: str | None = None
self._show_friend_scores: bool | None = None
self._show_info: dict[str, Any] | None = None self._show_info: dict[str, Any] | None = None
self._name_str: str | None = None self._name_str: str | None = None
self._friends_loading_status: ba.Actor | None = None self._friends_loading_status: ba.Actor | None = None
@ -177,12 +176,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
.replace(' ', '_') .replace(' ', '_')
) )
# If game-center/etc scores are available we show our friends'
# scores. Otherwise we show our local high scores.
self._show_friend_scores = ba.internal.game_service_has_leaderboard(
self._game_name_str, self._game_config_str
)
try: try:
self._old_best_rank = self._campaign.getlevel( self._old_best_rank = self._campaign.getlevel(
self._level_name self._level_name
@ -366,21 +359,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
ba.internal.set_ui_input_device(None) # Menu is up for grabs. ba.internal.set_ui_input_device(None) # Menu is up for grabs.
if self._show_friend_scores:
ba.buttonwidget(
parent=rootc,
color=(0.45, 0.4, 0.5),
position=(h_offs - 520, v_offs + 480),
size=(300, 60),
label=ba.Lstr(resource='topFriendsText'),
on_activate_call=ba.WeakCall(self._ui_gc),
transition_delay=delay + 0.5,
icon=self._game_service_leaderboards_texture,
icon_color=self._game_service_icon_color,
autoselect=True,
selectable=can_select_extra_buttons,
)
if self._have_achievements and self._account_has_achievements: if self._have_achievements and self._account_has_achievements:
ba.buttonwidget( ba.buttonwidget(
parent=rootc, parent=rootc,
@ -773,18 +751,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
[p.name for p in self._playerinfos] [p.name for p in self._playerinfos]
) )
if self._show_friend_scores:
self._friends_loading_status = Text(
ba.Lstr(
value='${A}...',
subs=[('${A}', ba.Lstr(resource='loadingText'))],
),
position=(-405, 150 + 30),
color=(1, 1, 1, 0.4),
transition=Text.Transition.FADE_IN,
scale=0.7,
transition_delay=2.0,
)
self._score_loading_status = Text( self._score_loading_status = Text(
ba.Lstr( ba.Lstr(
value='${A}...', value='${A}...',
@ -850,8 +816,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
# We expect this only in kiosk mode; complain otherwise. # We expect this only in kiosk mode; complain otherwise.
if not (ba.app.demo_mode or ba.app.arcade_mode): if not (ba.app.demo_mode or ba.app.arcade_mode):
print('got not-signed-in at score-submit; unexpected') print('got not-signed-in at score-submit; unexpected')
if self._show_friend_scores:
ba.pushcall(ba.WeakCall(self._got_friend_score_results, None))
ba.pushcall(ba.WeakCall(self._got_score_results, None)) ba.pushcall(ba.WeakCall(self._got_score_results, None))
else: else:
assert self._game_name_str is not None assert self._game_name_str is not None
@ -862,9 +826,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
name_str, name_str,
self._score, self._score,
ba.WeakCall(self._got_score_results), ba.WeakCall(self._got_score_results),
ba.WeakCall(self._got_friend_score_results)
if self._show_friend_scores
else None,
order=self._score_order, order=self._score_order,
tournament_id=self.session.tournament_id, tournament_id=self.session.tournament_id,
score_type=self._score_type, score_type=self._score_type,
@ -899,138 +860,118 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
assert txt.node assert txt.node
txt.node.client_only = True txt.node.client_only = True
# If we have no friend scores, display local best scores. ts_height = 300
if self._show_friend_scores: ts_h_offs = -480
v_offs = 40
Text(
ba.Lstr(resource='yourBestScoresText')
if self._score_type == 'points'
else ba.Lstr(resource='yourBestTimesText'),
maxwidth=210,
position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
transition=Text.Transition.IN_RIGHT,
v_align=Text.VAlign.CENTER,
scale=1.2,
transition_delay=1.8,
).autoretain()
# Host has a button, so we need client-only text. display_scores = list(our_high_scores)
ts_height = 300 display_count = 5
ts_h_offs = -480
v_offs = 40
txt = Text(
ba.Lstr(resource='topFriendsText'),
maxwidth=210,
position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
transition=Text.Transition.IN_RIGHT,
v_align=Text.VAlign.CENTER,
scale=1.2,
transition_delay=1.8,
).autoretain()
assert txt.node
txt.node.client_only = True
else:
ts_height = 300 while len(display_scores) < display_count:
ts_h_offs = -480 display_scores.append((0, None))
v_offs = 40
Text(
ba.Lstr(resource='yourBestScoresText')
if self._score_type == 'points'
else ba.Lstr(resource='yourBestTimesText'),
maxwidth=210,
position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
transition=Text.Transition.IN_RIGHT,
v_align=Text.VAlign.CENTER,
scale=1.2,
transition_delay=1.8,
).autoretain()
display_scores = list(our_high_scores) showed_ours = False
display_count = 5 h_offs_extra = 85 if self._score_type == 'points' else 130
v_offs_extra = 20
while len(display_scores) < display_count: v_offs_names = 0
display_scores.append((0, None)) scale = 1.0
p_count = len(self._playerinfos)
showed_ours = False h_offs_extra -= 75
h_offs_extra = 85 if self._score_type == 'points' else 130 if p_count > 1:
v_offs_extra = 20 h_offs_extra -= 20
v_offs_names = 0 if p_count == 2:
scale = 1.0 scale = 0.9
p_count = len(self._playerinfos) elif p_count == 3:
h_offs_extra -= 75 scale = 0.65
if p_count > 1: elif p_count == 4:
h_offs_extra -= 20 scale = 0.5
if p_count == 2: times: list[tuple[float, float]] = []
scale = 0.9 for i in range(display_count):
elif p_count == 3: times.insert(
scale = 0.65 random.randrange(0, len(times) + 1),
elif p_count == 4: (1.9 + i * 0.05, 2.3 + i * 0.05),
scale = 0.5 )
times: list[tuple[float, float]] = [] for i in range(display_count):
for i in range(display_count): try:
times.insert( if display_scores[i][1] is None:
random.randrange(0, len(times) + 1),
(1.9 + i * 0.05, 2.3 + i * 0.05),
)
for i in range(display_count):
try:
if display_scores[i][1] is None:
name_str = '-'
else:
# noinspection PyUnresolvedReferences
name_str = ', '.join(
[p['name'] for p in display_scores[i][1]['players']]
)
except Exception:
ba.print_exception(
f'Error calcing name_str for {display_scores}'
)
name_str = '-' name_str = '-'
if display_scores[i] == our_score and not showed_ours:
flash = True
color0 = (0.6, 0.4, 0.1, 1.0)
color1 = (0.6, 0.6, 0.6, 1.0)
tdelay1 = 3.7
tdelay2 = 3.7
showed_ours = True
else: else:
flash = False # noinspection PyUnresolvedReferences
color0 = (0.6, 0.4, 0.1, 1.0) name_str = ', '.join(
color1 = (0.6, 0.6, 0.6, 1.0) [p['name'] for p in display_scores[i][1]['players']]
tdelay1 = times[i][0] )
tdelay2 = times[i][1] except Exception:
Text( ba.print_exception(
str(display_scores[i][0]) f'Error calcing name_str for {display_scores}'
if self._score_type == 'points' )
else ba.timestring( name_str = '-'
display_scores[i][0] * 10, if display_scores[i] == our_score and not showed_ours:
timeformat=ba.TimeFormat.MILLISECONDS, flash = True
suppress_format_warning=True, color0 = (0.6, 0.4, 0.1, 1.0)
), color1 = (0.6, 0.6, 0.6, 1.0)
position=( tdelay1 = 3.7
ts_h_offs + 20 + h_offs_extra, tdelay2 = 3.7
v_offs_extra showed_ours = True
+ ts_height / 2 else:
+ -ts_height * (i + 1) / 10 flash = False
+ v_offs color0 = (0.6, 0.4, 0.1, 1.0)
+ 11.0, color1 = (0.6, 0.6, 0.6, 1.0)
), tdelay1 = times[i][0]
h_align=Text.HAlign.RIGHT, tdelay2 = times[i][1]
v_align=Text.VAlign.CENTER, Text(
color=color0, str(display_scores[i][0])
flash=flash, if self._score_type == 'points'
transition=Text.Transition.IN_RIGHT, else ba.timestring(
transition_delay=tdelay1, display_scores[i][0] * 10,
).autoretain() timeformat=ba.TimeFormat.MILLISECONDS,
suppress_format_warning=True,
),
position=(
ts_h_offs + 20 + h_offs_extra,
v_offs_extra
+ ts_height / 2
+ -ts_height * (i + 1) / 10
+ v_offs
+ 11.0,
),
h_align=Text.HAlign.RIGHT,
v_align=Text.VAlign.CENTER,
color=color0,
flash=flash,
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay1,
).autoretain()
Text( Text(
ba.Lstr(value=name_str), ba.Lstr(value=name_str),
position=( position=(
ts_h_offs + 35 + h_offs_extra, ts_h_offs + 35 + h_offs_extra,
v_offs_extra v_offs_extra
+ ts_height / 2 + ts_height / 2
+ -ts_height * (i + 1) / 10 + -ts_height * (i + 1) / 10
+ v_offs_names + v_offs_names
+ v_offs + v_offs
+ 11.0, + 11.0,
), ),
maxwidth=80.0 + 100.0 * len(self._playerinfos), maxwidth=80.0 + 100.0 * len(self._playerinfos),
v_align=Text.VAlign.CENTER, v_align=Text.VAlign.CENTER,
color=color1, color=color1,
flash=flash, flash=flash,
scale=scale, scale=scale,
transition=Text.Transition.IN_RIGHT, transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay2, transition_delay=tdelay2,
).autoretain() ).autoretain()
# Show achievements for this level. # Show achievements for this level.
ts_height = -150 ts_height = -150

View File

@ -616,9 +616,6 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
for bottype in self._bot_types_initial: for bottype in self._bot_types_initial:
self._spawn_bot(bottype) self._spawn_bot(bottype)
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
self._show_standard_scores_to_beat_ui(scores)
def _on_bot_spawn(self, spaz: SpazBot) -> None: def _on_bot_spawn(self, spaz: SpazBot) -> None:
# We want to move to the left by default. # We want to move to the left by default.
spaz.target_point_default = ba.Vec3(0, 0, 0) spaz.target_point_default = ba.Vec3(0, 0, 0)

View File

@ -682,9 +682,6 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]):
self._bots = SpazBotSet() self._bots = SpazBotSet()
ba.timer(4.0, self._start_updating_waves) ba.timer(4.0, self._start_updating_waves)
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
self._show_standard_scores_to_beat_ui(scores)
def _get_dist_grp_totals(self, grps: list[Any]) -> tuple[int, int]: def _get_dist_grp_totals(self, grps: list[Any]) -> tuple[int, int]:
totalpts = 0 totalpts = 0
totaldudes = 0 totaldudes = 0

View File

@ -684,9 +684,6 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]):
}, },
) )
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
self._show_standard_scores_to_beat_ui(scores)
def _update_waves(self) -> None: def _update_waves(self) -> None:
# pylint: disable=too-many-branches # pylint: disable=too-many-branches

View File

@ -326,9 +326,6 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
else: else:
super().handlemessage(msg) super().handlemessage(msg)
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
self._show_standard_scores_to_beat_ui(scores)
def end_game(self) -> None: def end_game(self) -> None:
# Tell our bots to celebrate just to rub it in. # Tell our bots to celebrate just to rub it in.
self._bots.final_celebrate() self._bots.final_celebrate()

View File

@ -37,6 +37,8 @@ class AccountSettingsWindow(ba.Window):
self._close_once_signed_in = close_once_signed_in self._close_once_signed_in = close_once_signed_in
ba.set_analytics_screen('Account Window') ba.set_analytics_screen('Account Window')
self._explicitly_signed_out_of_gpgs = False
# If they provided an origin-widget, scale up from that. # If they provided an origin-widget, scale up from that.
scale_origin: tuple[float, float] | None scale_origin: tuple[float, float] | None
if origin_widget is not None: if origin_widget is not None:
@ -104,7 +106,7 @@ class AccountSettingsWindow(ba.Window):
# Legacy v1 device accounts are currently always available # Legacy v1 device accounts are currently always available
# (though we need to start phasing them out at some point). # (though we need to start phasing them out at some point).
self._show_sign_in_buttons.append('Local') self._show_sign_in_buttons.append('Device')
top_extra = 15 if uiscale is ba.UIScale.SMALL else 0 top_extra = 15 if uiscale is ba.UIScale.SMALL else 0
super().__init__( super().__init__(
@ -229,17 +231,22 @@ class AccountSettingsWindow(ba.Window):
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui import confirm from bastd.ui import confirm
primary_v2_account = ba.app.accounts_v2.primary
v1_state = ba.internal.get_v1_account_state() v1_state = ba.internal.get_v1_account_state()
account_type = ( v1_account_type = (
ba.internal.get_v1_account_type() ba.internal.get_v1_account_type()
if v1_state == 'signed_in' if v1_state == 'signed_in'
else 'unknown' else 'unknown'
) )
is_google = account_type == 'Google Play' # We expose GPGS-specific functionality only if it is 'active'
# (meaning the current GPGS player matches one of our account's
show_local_signed_in_as = False # logins).
local_signed_in_as_space = 50.0 gpgs_adapter = ba.app.accounts_v2.login_adapters.get(LoginType.GPGS)
is_gpgs = (
False if gpgs_adapter is None else gpgs_adapter.is_back_end_active()
)
show_signed_in_as = self._signed_in show_signed_in_as = self._signed_in
signed_in_as_space = 95.0 signed_in_as_space = 95.0
@ -257,23 +264,25 @@ class AccountSettingsWindow(ba.Window):
and self._signing_in_adapter is None and self._signing_in_adapter is None
and 'Google Play' in self._show_sign_in_buttons and 'Google Play' in self._show_sign_in_buttons
) )
show_device_sign_in_button = (
v1_state == 'signed_out'
and self._signing_in_adapter is None
and 'Local' in self._show_sign_in_buttons
)
show_v2_proxy_sign_in_button = ( show_v2_proxy_sign_in_button = (
v1_state == 'signed_out' v1_state == 'signed_out'
and self._signing_in_adapter is None and self._signing_in_adapter is None
and 'V2Proxy' in self._show_sign_in_buttons and 'V2Proxy' in self._show_sign_in_buttons
) )
show_device_sign_in_button = (
v1_state == 'signed_out'
and self._signing_in_adapter is None
and 'Device' in self._show_sign_in_buttons
)
sign_in_button_space = 70.0 sign_in_button_space = 70.0
show_game_service_button = self._signed_in and account_type in [ show_game_service_button = self._signed_in and v1_account_type in [
'Game Center' 'Game Center'
] ]
game_service_button_space = 60.0 game_service_button_space = 60.0
show_what_is_v2 = self._signed_in and v1_account_type == 'V2'
show_linked_accounts_text = ( show_linked_accounts_text = (
self._signed_in self._signed_in
and ba.internal.get_v1_account_misc_read_val( and ba.internal.get_v1_account_misc_read_val(
@ -282,7 +291,7 @@ class AccountSettingsWindow(ba.Window):
) )
linked_accounts_text_space = 60.0 linked_accounts_text_space = 60.0
show_achievements_button = self._signed_in and account_type in ( show_achievements_button = self._signed_in and v1_account_type in (
'Google Play', 'Google Play',
'Local', 'Local',
'V2', 'V2',
@ -294,7 +303,7 @@ class AccountSettingsWindow(ba.Window):
) )
achievements_text_space = 27.0 achievements_text_space = 27.0
show_leaderboards_button = self._signed_in and is_google show_leaderboards_button = self._signed_in and is_gpgs
leaderboards_button_space = 60.0 leaderboards_button_space = 60.0
show_campaign_progress = self._signed_in show_campaign_progress = self._signed_in
@ -306,7 +315,9 @@ class AccountSettingsWindow(ba.Window):
show_reset_progress_button = False show_reset_progress_button = False
reset_progress_button_space = 70.0 reset_progress_button_space = 70.0
show_manage_v2_account_button = self._signed_in and account_type == 'V2' show_manage_v2_account_button = (
self._signed_in and v1_account_type == 'V2'
)
manage_v2_account_button_space = 100.0 manage_v2_account_button_space = 100.0
show_player_profiles_button = self._signed_in show_player_profiles_button = self._signed_in
@ -325,7 +336,7 @@ class AccountSettingsWindow(ba.Window):
show_unlink_accounts_button = show_link_accounts_button show_unlink_accounts_button = show_link_accounts_button
unlink_accounts_button_space = 90.0 unlink_accounts_button_space = 90.0
show_sign_out_button = self._signed_in and account_type in [ show_sign_out_button = self._signed_in and v1_account_type in [
'Local', 'Local',
'Google Play', 'Google Play',
'V2', 'V2',
@ -337,15 +348,13 @@ class AccountSettingsWindow(ba.Window):
# to be verified. # to be verified.
show_cancel_sign_in_button = self._signing_in_adapter is not None or ( show_cancel_sign_in_button = self._signing_in_adapter is not None or (
ba.app.accounts_v2.have_primary_credentials() ba.app.accounts_v2.have_primary_credentials()
and ba.app.accounts_v2.primary is None and primary_v2_account is None
) )
cancel_sign_in_button_space = 70.0 cancel_sign_in_button_space = 70.0
if self._subcontainer is not None: if self._subcontainer is not None:
self._subcontainer.delete() self._subcontainer.delete()
self._sub_height = 60.0 self._sub_height = 60.0
if show_local_signed_in_as:
self._sub_height += local_signed_in_as_space
if show_signed_in_as: if show_signed_in_as:
self._sub_height += signed_in_as_space self._sub_height += signed_in_as_space
if show_signing_in_text: if show_signing_in_text:
@ -398,27 +407,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = None first_selectable = None
v = self._sub_height - 10.0 v = self._sub_height - 10.0
if show_local_signed_in_as: self._account_name_what_is_text: ba.Widget | None
v -= local_signed_in_as_space * 0.6 self._account_name_what_is_y = 0.0
ba.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v),
size=(0, 0),
text=ba.Lstr(
resource='accountSettingsWindow.deviceSpecificAccountText',
subs=[
('${NAME}', ba.internal.get_v1_account_display_string())
],
),
scale=0.7,
color=(0.5, 0.5, 0.6),
maxwidth=self._sub_width * 0.9,
flatness=1.0,
h_align='center',
v_align='center',
)
v -= local_signed_in_as_space * 0.4
self._account_name_text: ba.Widget | None self._account_name_text: ba.Widget | None
if show_signed_in_as: if show_signed_in_as:
v -= signed_in_as_space * 0.2 v -= signed_in_as_space * 0.2
@ -437,7 +427,7 @@ class AccountSettingsWindow(ba.Window):
h_align='center', h_align='center',
v_align='center', v_align='center',
) )
v -= signed_in_as_space * 0.4 v -= signed_in_as_space * 0.5
self._account_name_text = ba.textwidget( self._account_name_text = ba.textwidget(
parent=self._subcontainer, parent=self._subcontainer,
position=(self._sub_width * 0.5, v), position=(self._sub_width * 0.5, v),
@ -449,10 +439,39 @@ class AccountSettingsWindow(ba.Window):
h_align='center', h_align='center',
v_align='center', v_align='center',
) )
if show_what_is_v2:
self._account_name_what_is_y = v - 23.0
self._account_name_what_is_text = ba.textwidget(
parent=self._subcontainer,
position=(0.0, self._account_name_what_is_y),
size=(200.0, 60),
text=ba.Lstr(
value='${WHAT} -->',
subs=[('${WHAT}', ba.Lstr(resource='whatIsThisText'))],
),
scale=0.6,
color=(0.3, 0.7, 0.05),
maxwidth=200.0,
h_align='right',
v_align='center',
autoselect=True,
selectable=True,
on_activate_call=ba.WeakCall(self._on_what_is_v2_press),
click_activate=True,
)
if first_selectable is None:
first_selectable = self._account_name_what_is_text
else:
self._account_name_what_is_text = None
self._refresh_account_name_text() self._refresh_account_name_text()
v -= signed_in_as_space * 0.4 v -= signed_in_as_space * 0.4
else: else:
self._account_name_text = None self._account_name_text = None
self._account_name_what_is_text = None
if self._back_button is None: if self._back_button is None:
bbtn = ba.internal.get_special_widget('back_button') bbtn = ba.internal.get_special_widget('back_button')
@ -709,12 +728,12 @@ class AccountSettingsWindow(ba.Window):
if show_game_service_button: if show_game_service_button:
button_width = 300 button_width = 300
v -= game_service_button_space * 0.85 v -= game_service_button_space * 0.85
account_type = ba.internal.get_v1_account_type() v1_account_type = ba.internal.get_v1_account_type()
if account_type == 'Game Center': if v1_account_type == 'Game Center':
account_type_name = ba.Lstr(resource='gameCenterText') v1_account_type_name = ba.Lstr(resource='gameCenterText')
else: else:
raise ValueError( raise ValueError(
"unknown account type: '" + str(account_type) + "'" "unknown account type: '" + str(v1_account_type) + "'"
) )
self._game_service_button = btn = ba.buttonwidget( self._game_service_button = btn = ba.buttonwidget(
parent=self._subcontainer, parent=self._subcontainer,
@ -724,7 +743,7 @@ class AccountSettingsWindow(ba.Window):
autoselect=True, autoselect=True,
on_activate_call=ba.internal.show_online_score_ui, on_activate_call=ba.internal.show_online_score_ui,
size=(button_width, 50), size=(button_width, 50),
label=account_type_name, label=v1_account_type_name,
) )
if first_selectable is None: if first_selectable is None:
first_selectable = btn first_selectable = btn
@ -767,11 +786,15 @@ class AccountSettingsWindow(ba.Window):
autoselect=True, autoselect=True,
icon=ba.gettexture( icon=ba.gettexture(
'googlePlayAchievementsIcon' 'googlePlayAchievementsIcon'
if is_google if is_gpgs
else 'achievementsIcon' else 'achievementsIcon'
), ),
icon_color=(0.8, 0.95, 0.7) if is_google else (0.85, 0.8, 0.9), icon_color=(0.8, 0.95, 0.7) if is_gpgs else (0.85, 0.8, 0.9),
on_activate_call=self._on_achievements_press, on_activate_call=(
self._on_custom_achievements_press
if is_gpgs
else self._on_achievements_press
),
size=(button_width, 50), size=(button_width, 50),
label='', label='',
) )
@ -1044,33 +1067,25 @@ class AccountSettingsWindow(ba.Window):
) )
self._needs_refresh = False self._needs_refresh = False
def _on_custom_achievements_press(self) -> None:
ba.timer(
0.15,
ba.Call(ba.internal.show_online_score_ui, 'achievements'),
timetype=ba.TimeType.REAL,
)
def _on_achievements_press(self) -> None: def _on_achievements_press(self) -> None:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui import achievements from bastd.ui import achievements
account_state = ba.internal.get_v1_account_state() assert self._achievements_button is not None
account_type = ( achievements.AchievementsWindow(
ba.internal.get_v1_account_type() position=self._achievements_button.get_screen_space_center()
if account_state == 'signed_in'
else 'unknown'
) )
# for google play we use the built-in UI; otherwise pop up our own
if account_type == 'Google Play': def _on_what_is_v2_press(self) -> None:
ba.timer( bamasteraddr = ba.internal.get_master_server_address(version=2)
0.15, ba.open_url(f'{bamasteraddr}/whatisv2')
ba.Call(ba.internal.show_online_score_ui, 'achievements'),
timetype=ba.TimeType.REAL,
)
elif account_type != 'unknown':
assert self._achievements_button is not None
achievements.AchievementsWindow(
position=self._achievements_button.get_screen_space_center()
)
else:
print(
'ERROR: unknown account type in on_achievements_press:',
account_type,
)
def _on_manage_account_press(self) -> None: def _on_manage_account_press(self) -> None:
ba.screenmessage(ba.Lstr(resource='oneMomentText')) ba.screenmessage(ba.Lstr(resource='oneMomentText'))
@ -1195,6 +1210,7 @@ class AccountSettingsWindow(ba.Window):
) )
def _refresh_account_name_text(self) -> None: def _refresh_account_name_text(self) -> None:
if self._account_name_text is None: if self._account_name_text is None:
return return
try: try:
@ -1202,7 +1218,20 @@ class AccountSettingsWindow(ba.Window):
except Exception: except Exception:
ba.print_exception() ba.print_exception()
name_str = '??' name_str = '??'
ba.textwidget(edit=self._account_name_text, text=name_str) ba.textwidget(edit=self._account_name_text, text=name_str)
if self._account_name_what_is_text is not None:
swidth = ba.internal.get_string_width(
name_str, suppress_warning=True
)
# Eww; number-fudging. Need to recalibrate this if
# account name scaling changes.
x = self._sub_width * 0.5 - swidth * 0.75 - 170
ba.textwidget(
edit=self._account_name_what_is_text,
position=(x, self._account_name_what_is_y),
)
def _refresh_achievements(self) -> None: def _refresh_achievements(self) -> None:
if ( if (
@ -1263,6 +1292,11 @@ class AccountSettingsWindow(ba.Window):
def _sign_out_press(self) -> None: def _sign_out_press(self) -> None:
if ba.app.accounts_v2.have_primary_credentials(): if ba.app.accounts_v2.have_primary_credentials():
if (
ba.app.accounts_v2.primary is not None
and LoginType.GPGS in ba.app.accounts_v2.primary.logins
):
self._explicitly_signed_out_of_gpgs = True
ba.app.accounts_v2.set_primary_credentials(None) ba.app.accounts_v2.set_primary_credentials(None)
else: else:
ba.internal.sign_out_v1() ba.internal.sign_out_v1()
@ -1334,6 +1368,27 @@ class AccountSettingsWindow(ba.Window):
# when finished. # when finished.
ba.app.accounts_v2.set_primary_credentials(result.credentials) ba.app.accounts_v2.set_primary_credentials(result.credentials)
# Special case - if the user has explicitly logged out and
# logged in again with GPGS via this button, warn them that
# they need to use the app if they want to switch to a
# different GPGS account.
if (
self._explicitly_signed_out_of_gpgs
and adapter.login_type is LoginType.GPGS
):
# Delay this slightly so it hopefully pops up after
# credentials go through and the account name shows up.
ba.timer(
1.5,
ba.Call(
ba.screenmessage,
ba.Lstr(
resource=self._r
+ '.googlePlayGamesAccountSwitchText'
),
),
)
# Speed any UI updates along. # Speed any UI updates along.
self._needs_refresh = True self._needs_refresh = True
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)

View File

@ -20,12 +20,12 @@ class ConfigErrorWindow(ba.Window):
self._config_file_path = ba.app.config_file_path self._config_file_path = ba.app.config_file_path
width = 800 width = 800
super().__init__( super().__init__(
ba.containerwidget(size=(width, 300), transition='in_right') ba.containerwidget(size=(width, 400), transition='in_right')
) )
padding = 20 padding = 20
ba.textwidget( ba.textwidget(
parent=self._root_widget, parent=self._root_widget,
position=(padding, 220), position=(padding, 220 + 60),
size=(width - 2 * padding, 100 - 2 * padding), size=(width - 2 * padding, 100 - 2 * padding),
h_align='center', h_align='center',
v_align='top', v_align='top',
@ -41,7 +41,7 @@ class ConfigErrorWindow(ba.Window):
) )
ba.textwidget( ba.textwidget(
parent=self._root_widget, parent=self._root_widget,
position=(padding, 198), position=(padding, 198 + 60),
size=(width - 2 * padding, 100 - 2 * padding), size=(width - 2 * padding, 100 - 2 * padding),
h_align='center', h_align='center',
v_align='top', v_align='top',

View File

@ -48,6 +48,7 @@
<w>allobjs</w> <w>allobjs</w>
<w>allocs</w> <w>allocs</w>
<w>allwarnings</w> <w>allwarnings</w>
<w>alogins</w>
<w>alot</w> <w>alot</w>
<w>alphaimg</w> <w>alphaimg</w>
<w>alphapixels</w> <w>alphapixels</w>
@ -217,6 +218,7 @@
<w>cend</w> <w>cend</w>
<w>centiseconds</w> <w>centiseconds</w>
<w>certifi</w> <w>certifi</w>
<w>cfgdict</w>
<w>cfgdir</w> <w>cfgdir</w>
<w>cfgpath</w> <w>cfgpath</w>
<w>changeme</w> <w>changeme</w>
@ -1334,6 +1336,7 @@
<w>subtypestr</w> <w>subtypestr</w>
<w>successmsg</w> <w>successmsg</w>
<w>sval</w> <w>sval</w>
<w>swidth</w>
<w>swiftc</w> <w>swiftc</w>
<w>symbolification</w> <w>symbolification</w>
<w>syscalls</w> <w>syscalls</w>
@ -1345,6 +1348,9 @@
<w>targs</w> <w>targs</w>
<w>tasklabel</w> <w>tasklabel</w>
<w>tbegin</w> <w>tbegin</w>
<w>tbfile</w>
<w>tbfiles</w>
<w>tbpath</w>
<w>tcls</w> <w>tcls</w>
<w>tdels</w> <w>tdels</w>
<w>tdiff</w> <w>tdiff</w>
@ -1508,6 +1514,7 @@
<w>weeeird</w> <w>weeeird</w>
<w>welp</w> <w>welp</w>
<w>whaaaaaaa</w> <w>whaaaaaaa</w>
<w>whatisv</w>
<w>wheee</w> <w>wheee</w>
<w>wheeee</w> <w>wheeee</w>
<w>wiimote</w> <w>wiimote</w>

View File

@ -156,7 +156,6 @@ void AppFlavor::UpdatePauseResume() {
void AppFlavor::OnPause() { void AppFlavor::OnPause() {
assert(InMainThread()); assert(InMainThread());
// Avoid reading gyro values for a short time to avoid hitches when restored.
g_graphics->SetGyroEnabled(false); g_graphics->SetGyroEnabled(false);
// IMPORTANT: Any on-pause related stuff that threads need to do must // IMPORTANT: Any on-pause related stuff that threads need to do must
@ -234,20 +233,59 @@ void AppFlavor::SetProductPrice(const std::string& product,
void AppFlavor::PauseApp() { void AppFlavor::PauseApp() {
assert(InMainThread()); assert(InMainThread());
millisecs_t start_time{Platform::GetCurrentMilliseconds()};
// Apple mentioned 5 seconds to run stuff once backgrounded or
// they bring down the hammer. Let's aim to stay under 2.
millisecs_t max_duration{2000};
Platform::DebugLog("PauseApp@" Platform::DebugLog("PauseApp@"
+ std::to_string(Platform::GetCurrentMilliseconds())); + std::to_string(Platform::GetCurrentMilliseconds()));
assert(!sys_paused_app_); assert(!sys_paused_app_);
sys_paused_app_ = true; sys_paused_app_ = true;
UpdatePauseResume(); UpdatePauseResume();
// We assume that the OS will completely suspend our process the moment
// we return from this call (though this is not technically true on all
// platforms). So we want to spin and wait for threads to actually
// process the pause message.
size_t running_thread_count{};
while (std::abs(Platform::GetCurrentMilliseconds() - start_time)
< max_duration) {
// If/when we get to a point with no threads waiting to be paused,
// we're good to go.
auto threads{Thread::GetStillPausingThreads()};
running_thread_count = threads.size();
if (running_thread_count == 0) {
Log(LogLevel::kDebug,
"PauseApp() completed in "
+ std::to_string(Platform::GetCurrentMilliseconds() - start_time)
+ "ms.");
return;
}
}
// If we made it here, we timed out. Complain.
Log(LogLevel::kError,
std::string("PauseApp() took too long; ")
+ std::to_string(running_thread_count)
+ " threads not yet paused after "
+ std::to_string(Platform::GetCurrentMilliseconds() - start_time)
+ " ms.");
} }
void AppFlavor::ResumeApp() { void AppFlavor::ResumeApp() {
assert(InMainThread()); assert(InMainThread());
millisecs_t start_time{Platform::GetCurrentMilliseconds()};
Platform::DebugLog("ResumeApp@" Platform::DebugLog("ResumeApp@"
+ std::to_string(Platform::GetCurrentMilliseconds())); + std::to_string(Platform::GetCurrentMilliseconds()));
assert(sys_paused_app_); assert(sys_paused_app_);
sys_paused_app_ = false; sys_paused_app_ = false;
UpdatePauseResume(); UpdatePauseResume();
Log(LogLevel::kDebug,
"ResumeApp() completed in "
+ std::to_string(Platform::GetCurrentMilliseconds() - start_time)
+ "ms.");
} }
void AppFlavor::DidFinishRenderingFrame(FrameDef* frame) {} void AppFlavor::DidFinishRenderingFrame(FrameDef* frame) {}
@ -265,6 +303,7 @@ void AppFlavor::PrimeEventPump() {
#pragma mark Push-Calls #pragma mark Push-Calls
// FIXME - move this call to Platform.
void AppFlavor::PushShowOnlineScoreUICall(const std::string& show, void AppFlavor::PushShowOnlineScoreUICall(const std::string& show,
const std::string& game, const std::string& game,
const std::string& game_version) { const std::string& game_version) {
@ -316,14 +355,6 @@ void AppFlavor::PushOpenURLCall(const std::string& url) {
thread()->PushCall([url] { g_platform->OpenURL(url); }); thread()->PushCall([url] { g_platform->OpenURL(url); });
} }
void AppFlavor::PushGetFriendScoresCall(const std::string& game,
const std::string& game_version,
void* data) {
thread()->PushCall([game, game_version, data] {
g_platform->GetFriendScores(game, game_version, data);
});
}
void AppFlavor::PushSubmitScoreCall(const std::string& game, void AppFlavor::PushSubmitScoreCall(const std::string& game,
const std::string& game_version, const std::string& game_version,
int64_t score) { int64_t score) {

View File

@ -43,16 +43,17 @@ class AppFlavor {
/// Should process any pending OS events, etc. /// Should process any pending OS events, etc.
virtual auto RunEvents() -> void; virtual auto RunEvents() -> void;
// These should be called by the window, view-controller, sdl, /// Put the app into a paused state. Should be called from the main
// or whatever is driving the app. They must be called from the main thread. /// thread. Pauses work, closes network sockets, etc.
/// Corresponds to being backgrounded on mobile, etc.
/// Should be called on mobile when the app is backgrounded. /// It is assumed that, as soon as this call returns, all work is
/// Pauses threads, closes network sockets, etc. /// finished and all threads can be suspended by the OS without any
/// negative side effects.
auto PauseApp() -> void; auto PauseApp() -> void;
auto paused() const -> bool { return actually_paused_; } auto paused() const -> bool { return actually_paused_; }
/// Should be called on mobile when the app is foregrounded. /// Resume the app; corresponds to returning to foreground on mobile/etc.
/// Spins threads back up, re-opens network sockets, etc. /// Spins threads back up, re-opens network sockets, etc.
auto ResumeApp() -> void; auto ResumeApp() -> void;
@ -101,9 +102,6 @@ class AppFlavor {
auto PushShowOnlineScoreUICall(const std::string& show, auto PushShowOnlineScoreUICall(const std::string& show,
const std::string& game, const std::string& game,
const std::string& game_version) -> void; const std::string& game_version) -> void;
auto PushGetFriendScoresCall(const std::string& game,
const std::string& game_version, void* data)
-> void;
auto PushSubmitScoreCall(const std::string& game, auto PushSubmitScoreCall(const std::string& game,
const std::string& game_version, int64_t score) const std::string& game_version, int64_t score)
-> void; -> void;

View File

@ -32,7 +32,7 @@
namespace ballistica { namespace ballistica {
// These are set automatically via script; don't modify them here. // These are set automatically via script; don't modify them here.
const int kAppBuildNumber = 20934; const int kAppBuildNumber = 20942;
const char* kAppVersion = "1.7.14"; const char* kAppVersion = "1.7.14";
// Our standalone globals. // Our standalone globals.

View File

@ -11,7 +11,7 @@
namespace ballistica { namespace ballistica {
void Object::PrintObjects() { void Object::LsObjects() {
#if BA_DEBUG_BUILD #if BA_DEBUG_BUILD
std::string s; std::string s;
{ {
@ -47,7 +47,7 @@ void Object::PrintObjects() {
for (auto&& i : obj_map) { for (auto&& i : obj_map) {
sorted.emplace_back(i.second, i.first); sorted.emplace_back(i.second, i.first);
} }
std::sort(sorted.begin(), sorted.end()); std::sort(sorted.rbegin(), sorted.rend());
for (auto&& i : sorted) { for (auto&& i : sorted) {
s += "\n " + std::to_string(i.first) + ": " + i.second; s += "\n " + std::to_string(i.first) + ": " + i.second;
} }
@ -56,7 +56,7 @@ void Object::PrintObjects() {
} }
Log(LogLevel::kInfo, s); Log(LogLevel::kInfo, s);
#else #else
Log(LogLevel::kInfo, "PrintObjects() only functions in debug builds."); Log(LogLevel::kInfo, "LsObjects() only functions in debug builds.");
#endif // BA_DEBUG_BUILD #endif // BA_DEBUG_BUILD
} }

View File

@ -21,8 +21,8 @@ class Object {
Object(); Object();
virtual ~Object(); virtual ~Object();
/// Prints a tally of object types and counts (debug build only). /// Logs a tally of ba::Object types and counts (debug build only).
static void PrintObjects(); static void LsObjects();
// Object classes can provide descriptive names for themselves; // Object classes can provide descriptive names for themselves;
// these are used for debugging and other purposes. // these are used for debugging and other purposes.

View File

@ -91,7 +91,7 @@ auto Thread::RunAssetsThreadP(void* data) -> void* {
return nullptr; return nullptr;
} }
void Thread::SetPaused(bool paused) { void Thread::PushSetPaused(bool paused) {
// Can be toggled from the main thread only. // Can be toggled from the main thread only.
assert(std::this_thread::get_id() == g_app->main_thread_id); assert(std::this_thread::get_id() == g_app->main_thread_id);
PushThreadMessage(ThreadMessage(paused ? ThreadMessage::Type::kPause PushThreadMessage(ThreadMessage(paused ? ThreadMessage::Type::kPause
@ -101,13 +101,24 @@ void Thread::SetPaused(bool paused) {
void Thread::WaitForNextEvent(bool single_cycle) { void Thread::WaitForNextEvent(bool single_cycle) {
// If we're running a single cycle we never stop to wait. // If we're running a single cycle we never stop to wait.
if (single_cycle) { if (single_cycle) {
// Need to revisit this if we ever do single-cycle for
// the gil-holding thread so we don't starve other Python threads.
assert(!acquires_python_gil_);
return; return;
} }
// We also never wait if we have pending runnables. // We also never wait if we have pending runnables; we wan't to run
// (we run all existing runnables in each loop cycle, but one of those // things as soon as we can. We chew through all runnables at the end
// may have enqueued more). // of the loop so it might seem like there should never be any here,
if (has_pending_runnables()) { // but runnables can add other runnables that won't get processed until
// the next time through.
// BUG FIX: We now skip this if we're paused since we don't run runnables
// in that case. This was preventing us from releasing the GIL while paused
// (and I assume causing us to spin full-speed through the loop; ugh).
// NOTE: It is theoretically possible for a runnable to add another runnable
// each time through the loop which would effectively starve the GIL as
// well; do we need to worry about that case?
if (has_pending_runnables() && !paused_) {
return; return;
} }
@ -479,9 +490,6 @@ void Thread::PushThreadMessage(const ThreadMessage& t) {
// Plop the data on to the list; we're assuming the mutex is locked. // Plop the data on to the list; we're assuming the mutex is locked.
thread_messages_.push_back(t); thread_messages_.push_back(t);
// Keep our own count; apparently size() on an stl list involves iterating.
// FIXME: Actually I don't think this is the case anymore; should check.
// Debugging: show message count states. // Debugging: show message count states.
if (explicit_bool(false)) { if (explicit_bool(false)) {
static int one_off = 0; static int one_off = 0;
@ -525,13 +533,29 @@ void Thread::PushThreadMessage(const ThreadMessage& t) {
thread_message_cv_.notify_all(); thread_message_cv_.notify_all();
} }
void Thread::SetThreadsPaused(bool paused) { auto Thread::SetThreadsPaused(bool paused) -> void {
assert(std::this_thread::get_id() == g_app->main_thread_id);
g_app->threads_paused = paused; g_app->threads_paused = paused;
for (auto&& i : g_app->pausable_threads) { for (auto&& i : g_app->pausable_threads) {
i->SetPaused(paused); i->PushSetPaused(paused);
} }
} }
auto Thread::GetStillPausingThreads() -> std::vector<Thread*> {
std::vector<Thread*> threads;
assert(std::this_thread::get_id() == g_app->main_thread_id);
// Only return results if an actual pause is in effect.
if (g_app->threads_paused) {
for (auto&& i : g_app->pausable_threads) {
if (!i->paused()) {
threads.push_back(i);
}
}
}
return threads;
}
auto Thread::AreThreadsPaused() -> bool { return g_app->threads_paused; } auto Thread::AreThreadsPaused() -> bool { return g_app->threads_paused; }
auto Thread::NewTimer(millisecs_t length, bool repeat, auto Thread::NewTimer(millisecs_t length, bool repeat,
@ -553,6 +577,7 @@ auto Thread::GetCurrentThreadName() -> std::string {
} }
} }
// Ask pthread for the thread name if we don't have one.
// FIXME - move this to platform. // FIXME - move this to platform.
#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS || BA_OSTYPE_LINUX #if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS || BA_OSTYPE_LINUX
std::string name = "unknown (sys-name="; std::string name = "unknown (sys-name=";

View File

@ -45,7 +45,8 @@ class Thread {
void SetAcquiresPythonGIL(); void SetAcquiresPythonGIL();
void SetPaused(bool paused); void PushSetPaused(bool paused);
auto thread_id() const -> std::thread::id { return thread_id_; } auto thread_id() const -> std::thread::id { return thread_id_; }
// Needed in rare cases where we jump physical threads. // Needed in rare cases where we jump physical threads.
@ -97,6 +98,10 @@ class Thread {
/// the app through a flood of packets. /// the app through a flood of packets.
auto CheckPushSafety() -> bool; auto CheckPushSafety() -> bool;
static auto GetStillPausingThreads() -> std::vector<Thread*>;
auto paused() { return paused_; }
private: private:
struct ThreadMessage { struct ThreadMessage {
enum class Type { kShutdown = 999, kRunnable, kPause, kResume }; enum class Type { kShutdown = 999, kRunnable, kPause, kResume };

View File

@ -61,9 +61,9 @@
#if BA_OSTYPE_MACOS #if BA_OSTYPE_MACOS
#if BA_XCODE_BUILD #if BA_XCODE_BUILD
#include <OpenGL/gl.h> #include <OpenGL/gl.h>
#include <OpenGL/glext.h>
#include <OpenGL/glu.h> #include <OpenGL/glu.h>
#endif // BA_XCODE_BUILD #endif // BA_XCODE_BUILD
#include <OpenGL/glext.h>
#endif // BA_OSTYPE_MACOS #endif // BA_OSTYPE_MACOS
#endif // BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID #endif // BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID

View File

@ -1818,4 +1818,48 @@ auto Input::GetKeyName(int keycode) -> std::string {
#pragma clang diagnostic pop #pragma clang diagnostic pop
#endif // BA_SDL2_BUILD || BA_MINSDL_BUILD #endif // BA_SDL2_BUILD || BA_MINSDL_BUILD
auto Input::LsInputDevices() -> void {
BA_PRECONDITION(InLogicThread());
std::string out;
std::string ind{" "};
int index{0};
for (auto& device : input_devices_) {
if (index != 0) {
out += "\n";
}
out += std::to_string(index + 1) + ":\n";
out += ind + "name: " + device->GetDeviceName() + "\n";
out += ind + "index: " + std::to_string(device->index()) + "\n";
out += (ind + "is-controller: " + std::to_string(device->IsController())
+ "\n");
out += (ind + "is-sdl-controller: "
+ std::to_string(device->IsSDLController()) + "\n");
out += (ind + "is-touch-screen: " + std::to_string(device->IsTouchScreen())
+ "\n");
out += (ind + "is-remote-control: "
+ std::to_string(device->IsRemoteControl()) + "\n");
out += (ind + "is-test-input: " + std::to_string(device->IsTestInput())
+ "\n");
out +=
(ind + "is-keyboard: " + std::to_string(device->IsKeyboard()) + "\n");
out += (ind + "is-mfi-controller: "
+ std::to_string(device->IsMFiController()) + "\n");
out += (ind + "is-local: " + std::to_string(device->IsLocal()) + "\n");
out += (ind + "is-ui-only: " + std::to_string(device->IsUIOnly()) + "\n");
out += (ind + "is-remote-app: " + std::to_string(device->IsRemoteApp())
+ "\n");
out += ind + "attached-to: "
+ (device->GetRemotePlayer() != nullptr ? "remote-player"
: device->GetPlayer() != nullptr ? "local-player"
: "nothing");
++index;
}
Log(LogLevel::kInfo, out);
}
} // namespace ballistica } // namespace ballistica

View File

@ -126,6 +126,7 @@ class Input {
auto PushTouchEvent(const TouchEvent& touch_event) -> void; auto PushTouchEvent(const TouchEvent& touch_event) -> void;
auto PushDestroyKeyboardInputDevices() -> void; auto PushDestroyKeyboardInputDevices() -> void;
auto PushCreateKeyboardInputDevices() -> void; auto PushCreateKeyboardInputDevices() -> void;
auto LsInputDevices() -> void;
/// Roughly how long in milliseconds have all input devices been idle. /// Roughly how long in milliseconds have all input devices been idle.
auto input_idle_time() const { return input_idle_time_; } auto input_idle_time() const { return input_idle_time_; }
@ -170,7 +171,6 @@ class Input {
bool have_non_touch_inputs_{}; bool have_non_touch_inputs_{};
float cursor_pos_x_{}; float cursor_pos_x_{};
float cursor_pos_y_{}; float cursor_pos_y_{};
// millisecs_t last_input_time_{};
millisecs_t last_click_time_{}; millisecs_t last_click_time_{};
millisecs_t double_click_time_{200}; millisecs_t double_click_time_{200};
millisecs_t last_mouse_move_time_{}; millisecs_t last_mouse_move_time_{};

View File

@ -37,8 +37,6 @@ class PlatformApple : public Platform {
const std::vector<float>& widths, float scale) const std::vector<float>& widths, float scale)
-> void* override; -> void* override;
auto GetTextTextureData(void* tex) -> uint8_t* override; auto GetTextTextureData(void* tex) -> uint8_t* override;
auto GetFriendScores(const std::string& game, const std::string& game_version,
void* py_callback) -> void override;
auto SubmitScore(const std::string& game, const std::string& version, auto SubmitScore(const std::string& game, const std::string& version,
int64_t score) -> void override; int64_t score) -> void override;
auto ReportAchievement(const std::string& achievement) -> void override; auto ReportAchievement(const std::string& achievement) -> void override;

View File

@ -196,6 +196,11 @@ auto Platform::LoginAdapterGetSignInToken(const std::string& login_type,
}); });
} }
auto Platform::LoginAdapterBackEndActiveChange(const std::string& login_type,
bool active) -> void {
// Default is no-op.
}
auto Platform::GetDeviceV1AccountUUIDPrefix() -> std::string { auto Platform::GetDeviceV1AccountUUIDPrefix() -> std::string {
Log(LogLevel::kError, "GetDeviceV1AccountUUIDPrefix() unimplemented"); Log(LogLevel::kError, "GetDeviceV1AccountUUIDPrefix() unimplemented");
return "u"; return "u";
@ -862,13 +867,6 @@ auto Platform::ConvertIncomingLeaderboardScore(
return score; return score;
} }
void Platform::GetFriendScores(const std::string& game,
const std::string& game_version, void* data) {
// As a default, just fail gracefully.
Log(LogLevel::kError, "FIXME: GetFriendScores unimplemented");
g_logic->PushFriendScoreSetCall(FriendScoreSet(false, data));
}
void Platform::SubmitScore(const std::string& game, const std::string& version, void Platform::SubmitScore(const std::string& game, const std::string& version,
int64_t score) { int64_t score) {
Log(LogLevel::kError, "FIXME: SubmitScore() unimplemented"); Log(LogLevel::kError, "FIXME: SubmitScore() unimplemented");

View File

@ -339,6 +339,10 @@ class Platform {
/// Called when a Python LoginAdapter is requesting an explicit sign-in. /// Called when a Python LoginAdapter is requesting an explicit sign-in.
virtual auto LoginAdapterGetSignInToken(const std::string& login_type, virtual auto LoginAdapterGetSignInToken(const std::string& login_type,
int attempt_id) -> void; int attempt_id) -> void;
/// Called when a Python LoginAdapter is informing us that a back-end is
/// active/inactive.
virtual auto LoginAdapterBackEndActiveChange(const std::string& login_type,
bool active) -> void;
#pragma mark MUSIC PLAYBACK ---------------------------------------------------- #pragma mark MUSIC PLAYBACK ----------------------------------------------------
@ -368,9 +372,6 @@ class Platform {
virtual auto ConvertIncomingLeaderboardScore( virtual auto ConvertIncomingLeaderboardScore(
const std::string& leaderboard_id, int score) -> int; const std::string& leaderboard_id, int score) -> int;
virtual auto GetFriendScores(const std::string& game,
const std::string& game_version,
void* py_callback) -> void;
virtual auto SubmitScore(const std::string& game, const std::string& version, virtual auto SubmitScore(const std::string& game, const std::string& version,
int64_t score) -> void; int64_t score) -> void;
virtual auto ReportAchievement(const std::string& achievement) -> void; virtual auto ReportAchievement(const std::string& achievement) -> void;

View File

@ -6,6 +6,7 @@
#include "ballistica/app/app_flavor.h" #include "ballistica/app/app_flavor.h"
#include "ballistica/assets/component/texture.h" #include "ballistica/assets/component/texture.h"
#include "ballistica/core/logging.h" #include "ballistica/core/logging.h"
#include "ballistica/core/thread.h"
#include "ballistica/graphics/graphics.h" #include "ballistica/graphics/graphics.h"
#include "ballistica/logic/connection/connection_set.h" #include "ballistica/logic/connection/connection_set.h"
#include "ballistica/logic/host_activity.h" #include "ballistica/logic/host_activity.h"
@ -308,18 +309,29 @@ auto PyPushCall(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* {
int from_other_thread{}; int from_other_thread{};
int suppress_warning{}; int suppress_warning{};
int other_thread_use_fg_context{}; int other_thread_use_fg_context{};
static const char* kwlist[] = {"call", "from_other_thread", int raw{0};
static const char* kwlist[] = {"call",
"from_other_thread",
"suppress_other_thread_warning", "suppress_other_thread_warning",
"other_thread_use_fg_context", nullptr}; "other_thread_use_fg_context",
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|ppp", "raw",
nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|pppp",
const_cast<char**>(kwlist), &call_obj, const_cast<char**>(kwlist), &call_obj,
&from_other_thread, &suppress_warning, &from_other_thread, &suppress_warning,
&other_thread_use_fg_context)) { &other_thread_use_fg_context, &raw)) {
return nullptr; return nullptr;
} }
// The from-other-thread case is basically a different call. // 'raw' mode does no thread checking and no context saves/restores.
if (from_other_thread) { if (raw) {
Py_INCREF(call_obj);
g_logic->thread()->PushCall([call_obj] {
assert(InLogicThread());
PythonRef(call_obj, PythonRef::kSteal).Call();
});
} else if (from_other_thread) {
// Warn the user not to use this from the logic thread since it doesnt // Warn the user not to use this from the logic thread since it doesnt
// save/restore context. // save/restore context.
if (!suppress_warning && InLogicThread()) { if (!suppress_warning && InLogicThread()) {
@ -1132,15 +1144,12 @@ auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
{"pushcall", (PyCFunction)PyPushCall, METH_VARARGS | METH_KEYWORDS, {"pushcall", (PyCFunction)PyPushCall, METH_VARARGS | METH_KEYWORDS,
"pushcall(call: Callable, from_other_thread: bool = False,\n" "pushcall(call: Callable, from_other_thread: bool = False,\n"
" suppress_other_thread_warning: bool = False,\n" " suppress_other_thread_warning: bool = False,\n"
" other_thread_use_fg_context: bool = False) -> None\n" " other_thread_use_fg_context: bool = False,\n"
"\n" " raw: bool = False) -> None\n"
"Pushes a call onto the event loop to be run during the next cycle.\n"
"\n" "\n"
"Push a call to the logic event-loop.\n"
"Category: **General Utility Functions**\n" "Category: **General Utility Functions**\n"
"\n" "\n"
"This can be handy for calls that are disallowed from within other\n"
"callbacks, etc.\n"
"\n"
"This call expects to be used in the logic thread, and will " "This call expects to be used in the logic thread, and will "
"automatically\n" "automatically\n"
"save and restore the ba.Context to behave seamlessly.\n" "save and restore the ba.Context to behave seamlessly.\n"
@ -1149,8 +1158,9 @@ auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
"however, you can pass 'from_other_thread' as True. In this case\n" "however, you can pass 'from_other_thread' as True. In this case\n"
"the call will always run in the UI context on the logic thread\n" "the call will always run in the UI context on the logic thread\n"
"or whichever context is in the foreground if\n" "or whichever context is in the foreground if\n"
"other_thread_use_fg_context is True."}, "other_thread_use_fg_context is True.\n"
"Passing raw=True will disable thread checks and context"
" sets/restores."},
{"getactivity", (PyCFunction)PyGetActivity, {"getactivity", (PyCFunction)PyGetActivity,
METH_VARARGS | METH_KEYWORDS, METH_VARARGS | METH_KEYWORDS,
"getactivity(doraise: bool = True) -> <varies>\n" "getactivity(doraise: bool = True) -> <varies>\n"

View File

@ -638,6 +638,22 @@ auto PyLoginAdapterGetSignInToken(PyObject* self, PyObject* args,
BA_PYTHON_CATCH; BA_PYTHON_CATCH;
} }
auto PyLoginAdapterBackEndActiveChange(PyObject* self, PyObject* args,
PyObject* keywds) -> PyObject* {
BA_PYTHON_TRY;
const char* login_type;
int active;
static const char* kwlist[] = {"login_type", "active", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "sp",
const_cast<char**>(kwlist), &login_type,
&active)) {
return nullptr;
}
g_platform->LoginAdapterBackEndActiveChange(login_type, active);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PySetInternalLanguageKeys(PyObject* self, PyObject* args) -> PyObject* { auto PySetInternalLanguageKeys(PyObject* self, PyObject* args) -> PyObject* {
BA_PYTHON_TRY; BA_PYTHON_TRY;
PyObject* list_obj; PyObject* list_obj;
@ -715,10 +731,18 @@ auto PyAndroidShowWifiSettings(PyObject* self, PyObject* args, PyObject* keywds)
BA_PYTHON_CATCH; BA_PYTHON_CATCH;
} }
auto PyPrintObjects(PyObject* self, PyObject* args, PyObject* keywds) auto PyLsObjects(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* { -> PyObject* {
BA_PYTHON_TRY; BA_PYTHON_TRY;
Object::PrintObjects(); Object::LsObjects();
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PyLsInputDevices(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
g_input->LsInputDevices();
Py_RETURN_NONE; Py_RETURN_NONE;
BA_PYTHON_CATCH; BA_PYTHON_CATCH;
} }
@ -756,6 +780,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
"\n" "\n"
"If this returns False, UIs should not show 'copy to clipboard'\n" "If this returns False, UIs should not show 'copy to clipboard'\n"
"buttons, etc."}, "buttons, etc."},
{"clipboard_has_text", (PyCFunction)PyClipboardHasText, METH_NOARGS, {"clipboard_has_text", (PyCFunction)PyClipboardHasText, METH_NOARGS,
"clipboard_has_text() -> bool\n" "clipboard_has_text() -> bool\n"
"\n" "\n"
@ -765,6 +790,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
"\n" "\n"
"This will return False if no system clipboard is available; no need\n" "This will return False if no system clipboard is available; no need\n"
" to call ba.clipboard_is_supported() separately."}, " to call ba.clipboard_is_supported() separately."},
{"clipboard_set_text", (PyCFunction)PyClipboardSetText, {"clipboard_set_text", (PyCFunction)PyClipboardSetText,
METH_VARARGS | METH_KEYWORDS, METH_VARARGS | METH_KEYWORDS,
"clipboard_set_text(value: str) -> None\n" "clipboard_set_text(value: str) -> None\n"
@ -775,6 +801,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
"\n" "\n"
"Ensure that ba.clipboard_is_supported() returns True before adding\n" "Ensure that ba.clipboard_is_supported() returns True before adding\n"
" buttons/etc. that make use of this functionality."}, " buttons/etc. that make use of this functionality."},
{"clipboard_get_text", (PyCFunction)PyClipboardGetText, METH_NOARGS, {"clipboard_get_text", (PyCFunction)PyClipboardGetText, METH_NOARGS,
"clipboard_get_text() -> str\n" "clipboard_get_text() -> str\n"
"\n" "\n"
@ -784,9 +811,20 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
"\n" "\n"
"Ensure that ba.clipboard_has_text() returns True before calling\n" "Ensure that ba.clipboard_has_text() returns True before calling\n"
" this function."}, " this function."},
{"printobjects", (PyCFunction)PyPrintObjects,
{"ls_objects", (PyCFunction)PyLsObjects, METH_VARARGS | METH_KEYWORDS,
"ls_objects() -> None\n"
"\n"
"Log debugging info about C++ level objects.\n"
"\n"
"Category: **General Utility Functions**\n"
"\n"
"This call only functions in debug builds of the game.\n"
"It prints various info about the current object count, etc."},
{"ls_input_devices", (PyCFunction)PyLsInputDevices,
METH_VARARGS | METH_KEYWORDS, METH_VARARGS | METH_KEYWORDS,
"printobjects() -> None\n" "ls_input_devices() -> None\n"
"\n" "\n"
"Print debugging info about game objects.\n" "Print debugging info about game objects.\n"
"\n" "\n"
@ -859,6 +897,14 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
"\n" "\n"
"(internal)"}, "(internal)"},
{"login_adapter_back_end_active_change",
(PyCFunction)PyLoginAdapterBackEndActiveChange,
METH_VARARGS | METH_KEYWORDS,
"login_adapter_back_end_active_change(login_type: str, active: bool)"
" -> None\n"
"\n"
"(internal)"},
{"submit_analytics_counts", (PyCFunction)PySubmitAnalyticsCounts, {"submit_analytics_counts", (PyCFunction)PySubmitAnalyticsCounts,
METH_VARARGS | METH_KEYWORDS, METH_VARARGS | METH_KEYWORDS,
"submit_analytics_counts() -> None\n" "submit_analytics_counts() -> None\n"

View File

@ -19,3 +19,13 @@ class LoginType(Enum):
# Google Play Game Services # Google Play Game Services
GPGS = 'gpgs' GPGS = 'gpgs'
@property
def displayname(self) -> str:
"""Human readable name for this value."""
cls = type(self)
match self:
case cls.EMAIL:
return 'Email/Password'
case cls.GPGS:
return 'Google Play Games'

View File

@ -729,8 +729,10 @@ class Updater:
def _check_misc(self) -> None: def _check_misc(self) -> None:
# Misc sanity checks. # Misc sanity checks.
if not self._public:
# Make sure we're set to prod master server. # Make sure we're set to prod master server.
# (but ONLY when checking; still want to be able to run updates).
if not self._public and self._check:
with open( with open(
'src/ballistica/internal/master_server_config.h', 'src/ballistica/internal/master_server_config.h',
encoding='utf-8', encoding='utf-8',

View File

@ -80,6 +80,15 @@ class IntegrityError(ValueError):
"""Data has been tampered with or corrupted in some form.""" """Data has been tampered with or corrupted in some form."""
class AuthenticationError(Exception):
"""Authentication has failed for some operation.
This can be raised if server-side-verification does not match
client-supplied credentials, if an invalid password is supplied
for a sign-in attempt, etc.
"""
def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool: def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
"""Is the provided exception from urllib a communication-related error? """Is the provided exception from urllib a communication-related error?

View File

@ -43,6 +43,20 @@ class SysResponse:
users of the api never see them. users of the api never see them.
""" """
def set_local_exception(self, exc: Exception) -> None:
"""Attach a local exception to facilitate better logging/handling.
Be aware that this data does not get serialized and only
exists on the local object.
"""
setattr(self, '_sr_local_exception', exc)
def get_local_exception(self) -> Exception | None:
"""Fetch a local attached exception."""
value = getattr(self, '_sr_local_exception', None)
assert isinstance(value, Exception | None)
return value
# Some standard response types: # Some standard response types:

View File

@ -6,7 +6,6 @@ Supports static typing for message types and possible return types.
from __future__ import annotations from __future__ import annotations
import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from efro.error import CleanError, RemoteError, CommunicationError from efro.error import CleanError, RemoteError, CommunicationError
@ -158,17 +157,18 @@ class MessageSender:
bound_obj, msg_encoded bound_obj, msg_encoded
) )
except Exception as exc: except Exception as exc:
# Any error in the raw send call gets recorded as either response = ErrorSysResponse(
# a local or communication error. error_message='Error in MessageSender @send_method.',
return ErrorSysResponse(
error_message=f'Error in MessageSender @send_method'
f' ({type(exc)}): {exc}',
error_type=( error_type=(
ErrorSysResponse.ErrorType.COMMUNICATION ErrorSysResponse.ErrorType.COMMUNICATION
if isinstance(exc, CommunicationError) if isinstance(exc, CommunicationError)
else ErrorSysResponse.ErrorType.LOCAL else ErrorSysResponse.ErrorType.LOCAL
), ),
) )
# Can include the actual exception since we'll be looking at
# this locally; might be helpful.
response.set_local_exception(exc)
return response
return self._decode_raw_response(bound_obj, message, response_encoded) return self._decode_raw_response(bound_obj, message, response_encoded)
async def fetch_raw_response_async( async def fetch_raw_response_async(
@ -193,17 +193,18 @@ class MessageSender:
bound_obj, msg_encoded bound_obj, msg_encoded
) )
except Exception as exc: except Exception as exc:
# Any error in the raw send call gets recorded as either response = ErrorSysResponse(
# a local or communication error. error_message='Error in MessageSender @send_async_method.',
return ErrorSysResponse(
error_message=f'Error in MessageSender @send_async_method'
f' ({type(exc)}): {exc}',
error_type=( error_type=(
ErrorSysResponse.ErrorType.COMMUNICATION ErrorSysResponse.ErrorType.COMMUNICATION
if isinstance(exc, CommunicationError) if isinstance(exc, CommunicationError)
else ErrorSysResponse.ErrorType.LOCAL else ErrorSysResponse.ErrorType.LOCAL
), ),
) )
# Can include the actual exception since we'll be looking at
# this locally; might be helpful.
response.set_local_exception(exc)
return response
return self._decode_raw_response(bound_obj, message, response_encoded) return self._decode_raw_response(bound_obj, message, response_encoded)
def unpack_raw_response( def unpack_raw_response(
@ -250,18 +251,14 @@ class MessageSender:
self._decode_filter_call( self._decode_filter_call(
bound_obj, message, response_dict, response bound_obj, message, response_dict, response
) )
except Exception: except Exception as exc:
# If we got to this point, we successfully communicated
# with the other end so errors represent protocol mismatches
# or other invalid data. For now let's just log it but perhaps
# we'd want to somehow embed it in the ErrorSysResponse to be
# available directly to the user later.
logging.exception('Error decoding raw response')
response = ErrorSysResponse( response = ErrorSysResponse(
error_message='Error decoding raw response;' error_message='Error decoding raw response.',
' see log for details.',
error_type=ErrorSysResponse.ErrorType.LOCAL, error_type=ErrorSysResponse.ErrorType.LOCAL,
) )
# Since we'll be looking at this locally, we can include
# extra info for logging/etc.
response.set_local_exception(exc)
return response return response
def _unpack_raw_response( def _unpack_raw_response(
@ -282,16 +279,24 @@ class MessageSender:
# Some error occurred. Raise a local Exception for it. # Some error occurred. Raise a local Exception for it.
if isinstance(raw_response, ErrorSysResponse): if isinstance(raw_response, ErrorSysResponse):
# Errors that happened locally can attach their exceptions
# here for extra logging goodness.
local_exception = raw_response.get_local_exception()
if ( if (
raw_response.error_type raw_response.error_type
is ErrorSysResponse.ErrorType.COMMUNICATION is ErrorSysResponse.ErrorType.COMMUNICATION
): ):
raise CommunicationError(raw_response.error_message) raise CommunicationError(
raw_response.error_message
) from local_exception
# If something went wrong on *our* end of the connection, # If something went wrong on *our* end of the connection,
# don't say it was a remote error. # don't say it was a remote error.
if raw_response.error_type is ErrorSysResponse.ErrorType.LOCAL: if raw_response.error_type is ErrorSysResponse.ErrorType.LOCAL:
raise RuntimeError(raw_response.error_message) raise RuntimeError(
raw_response.error_message
) from local_exception
# If they want to support clean errors, do those. # If they want to support clean errors, do those.
if ( if (
@ -299,14 +304,18 @@ class MessageSender:
and raw_response.error_type and raw_response.error_type
is ErrorSysResponse.ErrorType.REMOTE_CLEAN is ErrorSysResponse.ErrorType.REMOTE_CLEAN
): ):
raise CleanError(raw_response.error_message) raise CleanError(
raw_response.error_message
) from local_exception
if ( if (
self.protocol.forward_communication_errors self.protocol.forward_communication_errors
and raw_response.error_type and raw_response.error_type
is ErrorSysResponse.ErrorType.REMOTE_COMMUNICATION is ErrorSysResponse.ErrorType.REMOTE_COMMUNICATION
): ):
raise CommunicationError(raw_response.error_message) raise CommunicationError(
raw_response.error_message
) from local_exception
# Everything else gets lumped in as a remote error. # Everything else gets lumped in as a remote error.
raise RemoteError( raise RemoteError(
@ -316,7 +325,7 @@ class MessageSender:
if self._peer_desc_call is None if self._peer_desc_call is None
else self._peer_desc_call(bound_obj) else self._peer_desc_call(bound_obj)
), ),
) ) from local_exception
assert isinstance(raw_response, Response) assert isinstance(raw_response, Response)
return raw_response return raw_response

View File

@ -162,6 +162,13 @@ def _spelling(words: list[str]) -> None:
print(f'Modified {num_modded_dictionaries} dictionaries.') print(f'Modified {num_modded_dictionaries} dictionaries.')
def pur() -> None:
"""Run pur using project's Python version."""
import subprocess
subprocess.run([sys.executable, '-m', 'pur'] + sys.argv[2:], check=True)
def spelling_all() -> None: def spelling_all() -> None:
"""Add all misspellings from a pycharm run.""" """Add all misspellings from a pycharm run."""
import subprocess import subprocess