diff --git a/.efrocachemap b/.efrocachemap
index f8e226f7..a7a574be 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -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/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/data/langdata.json": "https://files.ballistica.net/cache/ba1/25/11/cfcf4238fb55433521e26cce0d10",
- "assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/e2/24/5e7ea9ca5c9de4d3b7a28e53564d",
+ "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/46/74/5bd691984b02c1fe036445eb5361",
+ "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/chinese.json": "https://files.ballistica.net/cache/ba1/ea/22/bb0950095686a71030c67ac74b3b",
- "assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/3c/22/78a56fc40426ab19ad4e76924b78",
+ "assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/d3/84/5e7df03070af059070ad1bd93cee",
+ "assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/c2/a4/993d5f0f30d0670d7053466929b6",
"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/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/df/08/29edc91f648c34c3aa7960e04c13",
- "assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/4c/c7/0184b8178869d1a3827a1bfcd5bb",
- "assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/c7/2e/e0520f58206da01b829e02ff4576",
- "assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/e8/84/6c9f123e9a0d82fc595c8f55ac7c",
- "assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/8a/09/3e0fa9e44913b53f4dab195d3fae",
- "assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/08/15/13981ce51e1e9f974357a9e0a59c",
- "assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/aa/da/dfc8d710af960d7300c7090faeab",
- "assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/91/98/42701cd595c2f70b7484614a8f49",
- "assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/d8/f2/aa16bc336bd7660cc86c3264bfc4",
- "assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/0d/b4/e225e3838c4b5d9381dcc4594517",
- "assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/12/62/862228b229057877e89fb195d41d",
+ "assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/91/8c/67fb1ca2b8f256f133950b41ba2c",
+ "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/86/26/060476f46994c035ae0d52640657",
+ "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/06/58/071d6f7bbb5e93a3e074dbd323ae",
+ "assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/45/e1/32cc8660b71d3d6b5fe1faff694d",
+ "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/9f/df/469e166c6a0d42bca4baae3a6cb6",
+ "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/18/36/799b6271fe8cb0e73b2eabb7f400",
+ "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/persian.json": "https://files.ballistica.net/cache/ba1/df/b1/b2c9ebaad5e873ebedd365726d3d",
- "assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/19/e9/59c891b1fb85f3ba9f19283c233d",
- "assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/61/5b/847c03407d1c3a85866833323676",
+ "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/67/93/372c2a2428a830056e9ba22bbf95",
+ "assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/c5/d4/f933bd3d80ded2b6ac2ea1af6eb2",
"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/6e/c3/df824198c6fe2da6ace053d30641",
"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/spanish.json": "https://files.ballistica.net/cache/ba1/ce/be/2f06c3436871fd464ff3a62597d9",
+ "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/6d/b7/dfc80f923378a408010cf4ee627a",
"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/turkish.json": "https://files.ballistica.net/cache/ba1/ee/08/1f77c7c320d8d8504a11ee495db3",
- "assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/f3/92/fd7ee5fa8a92fcc8fd2219a88a2f",
- "assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/2e/86/10d3e39d35014d039cc9ea886ca7",
+ "assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/b5/e8/606f6d3dc1f4bffc96128abaef03",
+ "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/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/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",
@@ -4003,51 +4003,51 @@
"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",
"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/5d/0f/3592238512deb417b34428b91e09",
- "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/96/68/f540e5eefc5fc496f6531693f635",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a2/3e/3c403004cc2f9e6902bc21ed36e2",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fb/63/67f2fbaadfd5dd9604c33d3be317",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/a5/ec/375c414801b769b64c1848ee0331",
- "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/99/a6/8b421200970caea159fa12be513f",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e3/64/45e980a2cabf12cb97d621fa4b66",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/67/5a/a6455e835d5fa38e66ce2e06c712",
- "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ea/01/04ab6a0bc93eefa1523312420297",
- "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b6/61/bff5c094a4bc66cbcd3727422687",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2f/d1/780a1948216e46af050245f96d70",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/19/56/b64bef7218b736ceb7cf07a4073c",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f2/e7/7c9bd7adb70893d23b1bb7df31f8",
- "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/9d/f7/3aef724ad3f821687c13edf1b7e0",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/67/41/9e4ab9f852b1cd8f386f2de5a7d3",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4d/ed/39635f875286955f76eb90729998",
- "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e6/24/5a90398d108b1887d532cf525682",
- "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/db/ab/7c9126758b5903ea15cd40bad187",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/aa/7e/f1f9ccfb744c00d97cba806bbe0f",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/99/fe/2ddc2fec5207e1a465acd307bc24",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/24/40/c2fb8cdbea5b58242c3b51b52d14",
- "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/20/4e/60e18e1b04c88e1cdd4a0836d710",
- "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a4/1e/dbb500fc6af85da634aeeb81af3e",
- "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e1/4a/75c95d1e0a44e833b866f5a02cbd",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/58/f2/5997aa7d771a67246ba55be7538d",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/57/21/b34020e41aae91cd276efc77a9d2",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/08/c3/cb7fbc13bc7aa3026f4363f6f1d6",
- "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f3/54/5c6f3bfaa7feb59faf2a44945d38",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ad/93/21d395043d633d70eef84502fd9f",
- "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/33/83/67d6444edb230cfc3d9ad7c030be",
- "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e6/81/aff0232cd83ec62aa0ec10716568",
- "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5c/96/aa9f7ad96fa1f70ff80dbfaa6f7e",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/aa/56/194ab27e57149fb6b253bf8d754d",
- "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7e/1d/f330b35ca15bd654333a778b100d",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9a/5b/bcf80b7e94eb7bb1e949c41c58f8",
- "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5e/bc/1a9f73dc64f3ffcc489aa58b6251",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/ea/4f/8e568645129f921392345e4b81a3",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/e6/0a/43f0c01db3245e332a6cbb2c04a6",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/7a/33/215327a641ff1d522d3554ec2577",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/46/59/69428486eb6bb6ba752b37a31db1",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/97/d3/716990aa94a4378f91afe137c773",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/c4/fd/7532b2a98bf8c1b0ea68485a9d8e",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/0d/06/a6b7a7ca3834c8a05b538c5f1934",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/46/54/fdb879571df92b8636e880f66207",
- "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/c0/32/b7907e3859a5c5013a3d97b6b523",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/cf/52/f613c9b0d400e6a3cc46d1b10243",
+ "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a2/a1/9bfcb44eed26b64ee45c10e71ebf",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e4/6a/b6828e87838c864910769e10b082",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/15/1b/4b88f866b595f9aee3589fe61f22",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/b3/bd/61195ab2b707b27ced0b88aab751",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3f/5f/15a89a2c8aabd42d6fd990dcef61",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e1/a7/5c1fbf021cca742d3835b1b9ded8",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1f/0d/b7ce6c28626235b46ed2f3b90d40",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/4d/67/34b91ed9e0236b4a5d5bfd72dfa9",
+ "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/bf/4a/e071a870261746f96a76f6dbb3c6",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/00/78/253923b1d2bd27fac67351eb06cd",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ba/8d/f11ab9ad9ebf7512c5b7d8b35a6f",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/38/bf/edf22880968b719327db7d8dd998",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/09/bf/758bd310a00cb749657149580e81",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9c/95/5b9f366fff40806b7ee2062f7050",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/24/a7/35eb9753ebaa9e41bc87e8f27dc3",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ab/c6/1b1576f9d2f767815f7519353bd4",
+ "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/5a/d4/d2896f57e07ee1cbb43877c174cd",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/30/e9/d0dbf1bf4886264f9fc51b54e419",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/dc/62/a75c243384728a69acfb273e91f7",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1b/bc/4d8cc7c9807c8b7a02d67de95dba",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/10/e6/ccd40f2a83735c5da9193333cbf1",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/eb/3e/91f24852fb1766c9cf273e0952a6",
+ "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6f/64/c1cb80d0f007af1e7a7386281fc2",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/12/e5/ed36f54413a114c8d0762c2f57b1",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2c/8c/1597a2fe945c36e76c375fa5e412",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ce/28/61941473817cd934b21cc8447f4d",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/18/e3/01ee52c5bb52687324ecd3bb3f78",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3d/85/62176cf03dd0be0e11ec6ed56230",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cf/62/7587a846e73295a7e12c66d0e19c",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ff/43/3ff93b30de352362dde3a173db18",
+ "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5e/15/0279dbe76b979d494d69c03b0527",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/20/e6/7f97b816171100873578dfebc237",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/da/75/32d072333f57926ed2b649b01543",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/17/c2/e334283a230cfe50e5d4da8c02d4",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c4/41/6a346833e2193a05b339d596f3dc",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/62/4a/87bc0017df139dbbc0195be774f5",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/fd/44/5db285cf7fd3301cd46f944dab6d",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/2f/09/dedf1229e5fa5ab73cea4b8b575b",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/c8/3e/7f3ce434876f2992a4043f398073",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/ed/a7/2088ab8e7e0ceb2ea768370bb464",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/03/51/0d6c5b45b326d2d7d069f8b371fc",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/77/fe/4f0d3a4ac3f71e4b214c5c5e4cc9",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/42/ef/e9b23bd9661675f6a9ec2bd7745d",
+ "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_monolithic.inc": "https://files.ballistica.net/cache/ba1/ef/c1/aa5f1aa10af89f5c0b1e616355fd"
}
\ No newline at end of file
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index 883a8699..32534d60 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -65,6 +65,7 @@
aiomain
alarmsound
alibaba
+ alibname
allerrors
allobjc
allobjs
@@ -72,6 +73,7 @@
allsettings
allteams
allwarnings
+ alogins
aman
amazonaws
aname
@@ -373,6 +375,7 @@
certifi
cfconfig
cfenv
+ cfgdict
cfgdir
cfgkey
cfgkeys
@@ -1103,6 +1106,7 @@
googlevr
goosey
gotresponse
+ gpgs
gpio
gprev
gpsui
@@ -1866,6 +1870,7 @@
pentry
perma
perrdetail
+ phandle
phasers
phasescriptexecution
phello
@@ -2510,6 +2515,7 @@
svne
svvv
swht
+ swidth
swiftc
swip
swipsound
@@ -2546,6 +2552,9 @@
targs
tasklabel
tbegin
+ tbfile
+ tbfiles
+ tbpath
tbtcolor
tbtn
tbttxt
@@ -2857,6 +2866,7 @@
webpages
weeeird
whatevs
+ whatisv
wheee
whos
widgetdeathtime
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff0b82a9..23388791 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,18 @@
-### 1.7.14 (build 20921, api 7, 2022-11-03)
+### 1.7.14 (build 20956, api 7, 2022-12-01)
+- 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.
+- 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.
+- 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'.
+- 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.
+- Device accounts are now marked as deprecated, and signing in with one now brings up an 'upgrade' UI which allows converting it to a V2 account. It is my hope to push the entire client ecosystem to V2 accounts as quickly as possible since trying to support both independent V1 accounts and V2 accounts is a substantial technical burden.
+- Fixed an issue where Log calls made within `Thread::PushThreadMessage()` could result in deadlock.
### 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.
diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json
index 460603ae..895eda83 100644
--- a/assets/.asset_manifest_public.json
+++ b/assets/.asset_manifest_public.json
@@ -40,6 +40,7 @@
"ba_data/python/ba/__pycache__/_language.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_level.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_lobby.cpython-310.opt-1.pyc",
+ "ba_data/python/ba/__pycache__/_login.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_map.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_math.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_messages.cpython-310.opt-1.pyc",
@@ -110,6 +111,7 @@
"ba_data/python/ba/_language.py",
"ba_data/python/ba/_level.py",
"ba_data/python/ba/_lobby.py",
+ "ba_data/python/ba/_login.py",
"ba_data/python/ba/_map.py",
"ba_data/python/ba/_math.py",
"ba_data/python/ba/_messages.py",
@@ -147,6 +149,7 @@
"ba_data/python/bacommon/__pycache__/bacloud.cpython-310.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/build.cpython-310.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/cloud.cpython-310.opt-1.pyc",
+ "ba_data/python/bacommon/__pycache__/login.cpython-310.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/net.cpython-310.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/servermanager.cpython-310.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/transfer.cpython-310.opt-1.pyc",
@@ -154,6 +157,7 @@
"ba_data/python/bacommon/bacloud.py",
"ba_data/python/bacommon/build.py",
"ba_data/python/bacommon/cloud.py",
+ "ba_data/python/bacommon/login.py",
"ba_data/python/bacommon/net.py",
"ba_data/python/bacommon/servermanager.py",
"ba_data/python/bacommon/transfer.py",
@@ -352,18 +356,19 @@
"ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-310.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/trophies.cpython-310.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/url.cpython-310.opt-1.pyc",
+ "ba_data/python/bastd/ui/__pycache__/v2upgrade.cpython-310.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/watch.cpython-310.opt-1.pyc",
"ba_data/python/bastd/ui/account/__init__.py",
"ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-310.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/link.cpython-310.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/settings.cpython-310.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-310.opt-1.pyc",
- "ba_data/python/bastd/ui/account/__pycache__/v2.cpython-310.opt-1.pyc",
+ "ba_data/python/bastd/ui/account/__pycache__/v2proxy.cpython-310.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-310.opt-1.pyc",
"ba_data/python/bastd/ui/account/link.py",
"ba_data/python/bastd/ui/account/settings.py",
"ba_data/python/bastd/ui/account/unlink.py",
- "ba_data/python/bastd/ui/account/v2.py",
+ "ba_data/python/bastd/ui/account/v2proxy.py",
"ba_data/python/bastd/ui/account/viewer.py",
"ba_data/python/bastd/ui/achievements.py",
"ba_data/python/bastd/ui/appinvite.py",
@@ -510,6 +515,7 @@
"ba_data/python/bastd/ui/tournamentscores.py",
"ba_data/python/bastd/ui/trophies.py",
"ba_data/python/bastd/ui/url.py",
+ "ba_data/python/bastd/ui/v2upgrade.py",
"ba_data/python/bastd/ui/watch.py",
"ba_data/python/efro/__init__.py",
"ba_data/python/efro/__pycache__/__init__.cpython-310.opt-1.pyc",
diff --git a/assets/Makefile b/assets/Makefile
index 92750a22..e764adfc 100644
--- a/assets/Makefile
+++ b/assets/Makefile
@@ -174,6 +174,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
build/ba_data/python/ba/_language.py \
build/ba_data/python/ba/_level.py \
build/ba_data/python/ba/_lobby.py \
+ build/ba_data/python/ba/_login.py \
build/ba_data/python/ba/_map.py \
build/ba_data/python/ba/_math.py \
build/ba_data/python/ba/_messages.py \
@@ -286,7 +287,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
build/ba_data/python/bastd/ui/account/link.py \
build/ba_data/python/bastd/ui/account/settings.py \
build/ba_data/python/bastd/ui/account/unlink.py \
- build/ba_data/python/bastd/ui/account/v2.py \
+ build/ba_data/python/bastd/ui/account/v2proxy.py \
build/ba_data/python/bastd/ui/account/viewer.py \
build/ba_data/python/bastd/ui/achievements.py \
build/ba_data/python/bastd/ui/appinvite.py \
@@ -380,6 +381,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
build/ba_data/python/bastd/ui/tournamentscores.py \
build/ba_data/python/bastd/ui/trophies.py \
build/ba_data/python/bastd/ui/url.py \
+ build/ba_data/python/bastd/ui/v2upgrade.py \
build/ba_data/python/bastd/ui/watch.py \
build/server/ballisticacore_server.py
@@ -425,6 +427,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
build/ba_data/python/ba/__pycache__/_language.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_level.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_lobby.cpython-310.opt-1.pyc \
+ build/ba_data/python/ba/__pycache__/_login.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_map.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_math.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_messages.cpython-310.opt-1.pyc \
@@ -537,7 +540,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
build/ba_data/python/bastd/ui/account/__pycache__/link.cpython-310.opt-1.pyc \
build/ba_data/python/bastd/ui/account/__pycache__/settings.cpython-310.opt-1.pyc \
build/ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-310.opt-1.pyc \
- build/ba_data/python/bastd/ui/account/__pycache__/v2.cpython-310.opt-1.pyc \
+ build/ba_data/python/bastd/ui/account/__pycache__/v2proxy.cpython-310.opt-1.pyc \
build/ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-310.opt-1.pyc \
build/ba_data/python/bastd/ui/__pycache__/achievements.cpython-310.opt-1.pyc \
build/ba_data/python/bastd/ui/__pycache__/appinvite.cpython-310.opt-1.pyc \
@@ -631,6 +634,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
build/ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-310.opt-1.pyc \
build/ba_data/python/bastd/ui/__pycache__/trophies.cpython-310.opt-1.pyc \
build/ba_data/python/bastd/ui/__pycache__/url.cpython-310.opt-1.pyc \
+ build/ba_data/python/bastd/ui/__pycache__/v2upgrade.cpython-310.opt-1.pyc \
build/ba_data/python/bastd/ui/__pycache__/watch.cpython-310.opt-1.pyc \
build/server/__pycache__/ballisticacore_server.cpython-310.opt-1.pyc
@@ -654,6 +658,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
build/ba_data/python/bacommon/bacloud.py \
build/ba_data/python/bacommon/build.py \
build/ba_data/python/bacommon/cloud.py \
+ build/ba_data/python/bacommon/login.py \
build/ba_data/python/bacommon/net.py \
build/ba_data/python/bacommon/servermanager.py \
build/ba_data/python/bacommon/transfer.py \
@@ -686,6 +691,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
build/ba_data/python/bacommon/__pycache__/bacloud.cpython-310.opt-1.pyc \
build/ba_data/python/bacommon/__pycache__/build.cpython-310.opt-1.pyc \
build/ba_data/python/bacommon/__pycache__/cloud.cpython-310.opt-1.pyc \
+ build/ba_data/python/bacommon/__pycache__/login.cpython-310.opt-1.pyc \
build/ba_data/python/bacommon/__pycache__/net.cpython-310.opt-1.pyc \
build/ba_data/python/bacommon/__pycache__/servermanager.cpython-310.opt-1.pyc \
build/ba_data/python/bacommon/__pycache__/transfer.cpython-310.opt-1.pyc \
diff --git a/assets/src/ba_data/python/._ba_sources_hash b/assets/src/ba_data/python/._ba_sources_hash
index f5e2bdca..121cc8c6 100644
--- a/assets/src/ba_data/python/._ba_sources_hash
+++ b/assets/src/ba_data/python/._ba_sources_hash
@@ -1 +1 @@
-144047512702782890975548019773691278470
\ No newline at end of file
+313825873705357186590048512620581045590
\ No newline at end of file
diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py
index b1ef6d75..f4a79295 100644
--- a/assets/src/ba_data/python/_ba.py
+++ b/assets/src/ba_data/python/_ba.py
@@ -1227,16 +1227,6 @@ def android_get_external_files_dir() -> str:
return str()
-def android_media_scan_file(file_name: str) -> None:
-
- """(internal)
-
- Refreshes Android MTP Index for a file; use this to get file
- modifications to be reflected in Android File Transfer.
- """
- return None
-
-
def android_show_wifi_settings() -> None:
"""(internal)"""
@@ -2406,15 +2396,6 @@ def is_os_playing_music() -> bool:
return bool()
-def is_ouya_build() -> bool:
-
- """(internal)
-
- Returns whether we're running the ouya-specific version
- """
- return bool()
-
-
def is_party_icon_visible() -> bool:
"""(internal)"""
@@ -2448,6 +2429,42 @@ def lock_all_input() -> 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:
+
+ """(internal)"""
+ 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:
"""(internal)"""
@@ -2666,32 +2683,17 @@ def printnodes() -> 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(
call: Callable,
from_other_thread: bool = False,
suppress_other_thread_warning: bool = False,
other_thread_use_fg_context: bool = False,
+ raw: bool = False,
) -> 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**
- 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
save and restore the ba.Context to behave seamlessly.
@@ -2700,6 +2702,7 @@ def pushcall(
the call will always run in the UI context on the logic thread
or whichever context is in the foreground if
other_thread_use_fg_context is True.
+ Passing raw=True will disable thread checks and context sets/restores.
"""
return None
diff --git a/assets/src/ba_data/python/_bainternal.py b/assets/src/ba_data/python/_bainternal.py
index d1bf8109..99fd8ee4 100644
--- a/assets/src/ba_data/python/_bainternal.py
+++ b/assets/src/ba_data/python/_bainternal.py
@@ -252,7 +252,6 @@ def submit_score(
name: Any,
score: int | None,
callback: Callable,
- friend_callback: Callable | None,
order: str = 'increasing',
tournament_id: str | None = None,
score_type: str = 'points',
diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py
index 290d8482..82738c9e 100644
--- a/assets/src/ba_data/python/ba/__init__.py
+++ b/assets/src/ba_data/python/ba/__init__.py
@@ -43,7 +43,8 @@ from _ba import (
newnode,
playsound,
printnodes,
- printobjects,
+ ls_objects,
+ ls_input_devices,
pushcall,
quit,
rowwidget,
@@ -316,7 +317,8 @@ __all__ = [
'print_error',
'print_exception',
'printnodes',
- 'printobjects',
+ 'ls_objects',
+ 'ls_input_devices',
'pushcall',
'quit',
'rowwidget',
diff --git a/assets/src/ba_data/python/ba/_accountv1.py b/assets/src/ba_data/python/ba/_accountv1.py
index c2b81164..7f946097 100644
--- a/assets/src/ba_data/python/ba/_accountv1.py
+++ b/assets/src/ba_data/python/ba/_accountv1.py
@@ -48,6 +48,9 @@ class AccountV1Subsystem:
_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:
"""Should be called when the app is resumed."""
diff --git a/assets/src/ba_data/python/ba/_accountv2.py b/assets/src/ba_data/python/ba/_accountv2.py
index 77b4ecdd..f5fee480 100644
--- a/assets/src/ba_data/python/ba/_accountv2.py
+++ b/assets/src/ba_data/python/ba/_accountv2.py
@@ -4,13 +4,23 @@
from __future__ import annotations
+import hashlib
+import logging
from typing import TYPE_CHECKING
+from efro.call import tpartial
+from efro.error import CommunicationError
+from bacommon.login import LoginType
import _ba
if TYPE_CHECKING:
from typing import Any
+ from ba._login import LoginAdapter
+
+
+DEBUG_LOG = False
+
class AccountV2Subsystem:
"""Subsystem for modern account handling in the app.
@@ -30,9 +40,23 @@ class AccountV2Subsystem:
self._kicked_off_workspace_load = False
+ self.login_adapters: dict[LoginType, LoginAdapter] = {}
+
+ self._implicit_signed_in_adapter: LoginAdapter | None = None
+ self._implicit_state_changed = False
+ self._can_do_auto_sign_in = True
+
+ if _ba.app.platform == 'android' and _ba.app.subplatform == 'google':
+ from ba._login import LoginAdapterGPGS
+
+ self.login_adapters[LoginType.GPGS] = LoginAdapterGPGS()
+
def on_app_launch(self) -> None:
"""Should be called at standard on_app_launch time."""
+ for adapter in self.login_adapters.values():
+ adapter.on_app_launch()
+
def set_primary_credentials(self, credentials: str | None) -> None:
"""Set credentials for the primary app account."""
raise RuntimeError('This should be overridden.')
@@ -49,7 +73,7 @@ class AccountV2Subsystem:
@property
def primary(self) -> AccountV2Handle | None:
"""The primary account for the app, or None if not logged in."""
- return None
+ return self.do_get_primary()
def do_get_primary(self) -> AccountV2Handle | None:
"""Internal - should be overridden by subclass."""
@@ -60,9 +84,11 @@ class AccountV2Subsystem:
) -> None:
"""Callback run after the primary account changes.
- Will be called with None on log-outs or when new credentials
+ Will be called with None on log-outs and when new credentials
are set but have not yet been verified.
"""
+ assert _ba.in_logic_thread()
+
# Currently don't do anything special on sign-outs.
if account is None:
return
@@ -99,6 +125,30 @@ class AccountV2Subsystem:
self._initial_login_completed = True
_ba.app.on_initial_login_completed()
+ def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
+ """Should be called when logins for the active account change."""
+
+ for adapter in self.login_adapters.values():
+ adapter.set_active_logins(logins)
+
+ def on_implicit_sign_in(
+ self, login_type: LoginType, login_id: str, display_name: str
+ ) -> None:
+ """An implicit sign-in happened (called by native layer)."""
+ from ba._login import LoginAdapter
+
+ with _ba.Context('ui'):
+ self.login_adapters[login_type].set_implicit_login_state(
+ LoginAdapter.ImplicitLoginState(
+ login_id=login_id, display_name=display_name
+ )
+ )
+
+ def on_implicit_sign_out(self, login_type: LoginType) -> None:
+ """An implicit sign-out happened (called by native layer)."""
+ with _ba.Context('ui'):
+ self.login_adapters[login_type].set_implicit_login_state(None)
+
def on_no_initial_primary_account(self) -> None:
"""Callback run if the app has no primary account after launch.
@@ -110,6 +160,240 @@ class AccountV2Subsystem:
self._initial_login_completed = True
_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(
+ self,
+ login_type: LoginType,
+ state: LoginAdapter.ImplicitLoginState | None,
+ ) -> None:
+ """Called when implicit login state changes.
+
+ Login systems that tend to sign themselves in/out in the
+ background are considered implicit. We may choose to honor or
+ ignore their states, allowing the user to opt for other login
+ types even if the default implicit one can't be explicitly
+ logged out or otherwise controlled.
+ """
+ from ba._language import Lstr
+
+ 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.
+ # 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:
+ self._implicit_signed_in_adapter = None
+ new_state = cfgdict[login_type.value] = None
+ else:
+ 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 (
+ self.primary is not None
+ and not self.login_adapters[login_type].is_back_end_active()
+ ):
+ if login_type is LoginType.GPGS:
+ service_str = Lstr(resource='googlePlayText')
+ else:
+ service_str = None
+ if service_str is not None:
+ _ba.timer(
+ 2.0,
+ tpartial(
+ _ba.screenmessage,
+ Lstr(
+ resource='notUsingAccountText',
+ subs=[
+ ('${ACCOUNT}', state.display_name),
+ ('${SERVICE}', service_str),
+ ],
+ ),
+ (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.
+ self._update_auto_sign_in()
+
+ def on_cloud_connectivity_changed(self, connected: bool) -> None:
+ """Should be called with cloud connectivity changes."""
+ del connected # Unused.
+ assert _ba.in_logic_thread()
+
+ # We may want to auto-sign-in based on this new state.
+ self._update_auto_sign_in()
+
+ def _update_auto_sign_in(self) -> None:
+ from ba._internal import get_v1_account_state
+
+ # If implicit state has changed, try to respond.
+ if self._implicit_state_changed:
+ if self._implicit_signed_in_adapter is None:
+ # If implicit back-end is signed out, follow suit
+ # 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
+
+ # If we're not currently signed in, we have connectivity, and
+ # 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()
+ signed_in_v1 = get_v1_account_state() == 'signed_in'
+ signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials()
+ if (
+ connected
+ and not signed_in_v1
+ and not signed_in_v2
+ and self._implicit_signed_in_adapter is not None
+ ):
+ if DEBUG_LOG:
+ 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_explicit_sign_in_completed(
+ self,
+ adapter: LoginAdapter,
+ result: LoginAdapter.SignInResult | Exception,
+ ) -> 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 since the user knows
+ # a sign-in attempt is happening in this case.
+ if isinstance(result, Exception):
+ # We expect the occasional communication errors;
+ # Log a full exception for anything else though.
+ if not isinstance(result, CommunicationError):
+ logging.warning(
+ 'Error on explicit accountv2 sign in attempt.',
+ exc_info=result,
+ )
+ with _ba.Context('ui'):
+ _ba.screenmessage(
+ Lstr(resource='internal.signInErrorText'),
+ color=(1, 0, 0),
+ )
+ _ba.playsound(_ba.getsound('error'))
+
+ # Also I suppose we should sign them out in this case since
+ # it could be misleading to be still signed in with the old
+ # account.
+ _ba.app.accounts_v2.set_primary_credentials(None)
+ 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
+
+ del adapter # Unused.
+
+ # Log errors but don't inform the user; they're not aware of this
+ # attempt and ignorance is bliss.
+ if isinstance(result, Exception):
+ # We expect the occasional communication errors;
+ # Log a full exception for anything else though.
+ if not isinstance(result, CommunicationError):
+ logging.warning(
+ 'Error on implicit accountv2 sign in attempt.',
+ exc_info=result,
+ )
+ return
+
+ # If we're still connected and still not signed in,
+ # 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()
+ signed_in_v1 = get_v1_account_state() == 'signed_in'
+ signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials()
+ if connected and not signed_in_v1 and not signed_in_v2:
+ _ba.app.accounts_v2.set_primary_credentials(result.credentials)
+
def _on_set_active_workspace_completed(self) -> None:
if not self._initial_login_completed:
self._initial_login_completed = True
@@ -129,8 +413,17 @@ class AccountV2Handle:
self.workspacename: str | None = None
self.workspaceid: str | None = None
+ # Login types and their display-names associated with this account.
+ self.logins: dict[LoginType, str] = {}
+
def __enter__(self) -> None:
- """Support for "with" statement."""
+ """Support for "with" statement.
+
+ This allows cloud messages to be sent on our behalf.
+ """
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
- """Support for "with" statement."""
+ """Support for "with" statement.
+
+ This allows cloud messages to be sent on our behalf.
+ """
diff --git a/assets/src/ba_data/python/ba/_achievement.py b/assets/src/ba_data/python/ba/_achievement.py
index c666e3f3..6d0ebb10 100644
--- a/assets/src/ba_data/python/ba/_achievement.py
+++ b/assets/src/ba_data/python/ba/_achievement.py
@@ -697,7 +697,7 @@ class Achievement:
# Even though there are technically achievements when we're not
# signed in, lets not show them (otherwise we tend to get
# confusing 'controller connected' achievements popping up while
- # waiting to log in which can be confusing).
+ # waiting to sign in which can be confusing).
if _internal.get_v1_account_state() != 'signed_in':
return
diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py
index 46ac8e6f..89a6184f 100644
--- a/assets/src/ba_data/python/ba/_app.py
+++ b/assets/src/ba_data/python/ba/_app.py
@@ -354,6 +354,7 @@ class App:
from bastd import maps as stdmaps
from bastd.actor import spazappearance
from ba._generated.enums import TimeType
+ from ba._apputils import log_dumped_tracebacks
assert _ba.in_logic_thread()
@@ -407,9 +408,9 @@ class App:
# overwrite a broken one or whatnot and wipe out data.
if not self.config_file_healthy:
if self.platform in ('mac', 'linux', 'windows'):
- from bastd.ui import configerror
+ from bastd.ui.configerror import ConfigErrorWindow
- configerror.ConfigErrorWindow()
+ _ba.pushcall(ConfigErrorWindow)
return
# 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
)
+ # If any traceback dumps happened last run, log and clear them.
+ log_dumped_tracebacks()
+
self._launch_completed = True
self._update_state()
@@ -483,8 +487,21 @@ class App:
assert _ba.in_logic_thread()
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:
+ # 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:
self.state = self.State.RUNNING
if not self._called_on_app_running:
@@ -498,19 +515,16 @@ class App:
def on_app_pause(self) -> None:
"""Called when the app goes to a suspended state."""
+ assert not self._app_paused # Should avoid redundant calls.
self._app_paused = True
self._update_state()
- self.plugins.on_app_pause()
def on_app_resume(self) -> None:
"""Run when the app resumes from a suspended state."""
+ assert self._app_paused # Should avoid redundant calls.
self._app_paused = False
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:
"""(internal)"""
diff --git a/assets/src/ba_data/python/ba/_apputils.py b/assets/src/ba_data/python/ba/_apputils.py
index 166b4d33..9633b078 100644
--- a/assets/src/ba_data/python/ba/_apputils.py
+++ b/assets/src/ba_data/python/ba/_apputils.py
@@ -5,12 +5,13 @@ from __future__ import annotations
import gc
import os
+import logging
from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
- from typing import Any
+ from typing import Any, TextIO
import ba
@@ -260,3 +261,51 @@ def print_corrupt_file_error() -> None:
_ba.timer(
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.')
diff --git a/assets/src/ba_data/python/ba/_bootstrap.py b/assets/src/ba_data/python/ba/_bootstrap.py
index 5766328a..9d058ae7 100644
--- a/assets/src/ba_data/python/ba/_bootstrap.py
+++ b/assets/src/ba_data/python/ba/_bootstrap.py
@@ -47,7 +47,7 @@ def bootstrap() -> None:
# Give a soft warning if we're being used with a different binary
# version than we expect.
- expected_build = 20921
+ expected_build = 20956
running_build: int = env['build_number']
if running_build != expected_build:
print(
@@ -120,7 +120,8 @@ def bootstrap() -> None:
import __main__
# 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__.exit
diff --git a/assets/src/ba_data/python/ba/_cloud.py b/assets/src/ba_data/python/ba/_cloud.py
index 029ed482..785e68c7 100644
--- a/assets/src/ba_data/python/ba/_cloud.py
+++ b/assets/src/ba_data/python/ba/_cloud.py
@@ -4,6 +4,7 @@
from __future__ import annotations
+import logging
from typing import TYPE_CHECKING, overload
import _ba
@@ -14,6 +15,8 @@ if TYPE_CHECKING:
from efro.message import Message, Response
import bacommon.cloud
+DEBUG_LOG = False
+
# TODO: Should make it possible to define a protocol in bacommon.cloud and
# autogenerate this. That would give us type safety between this and
# internal protocols.
@@ -30,6 +33,21 @@ class CloudSubsystem:
"""
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:
+ """Called when cloud connectivity state changes."""
+ if DEBUG_LOG:
+ logging.debug('CloudSubsystem: Connectivity is now %s.', connected)
+
+ # Inform things that use this.
+ # (TODO: should generalize this into some sort of registration system)
+ _ba.app.accounts_v2.on_cloud_connectivity_changed(connected)
+
@overload
def send_message_cb(
self,
@@ -66,6 +84,26 @@ class CloudSubsystem:
) -> None:
...
+ @overload
+ def send_message_cb(
+ self,
+ msg: bacommon.cloud.SignInMessage,
+ on_response: Callable[
+ [bacommon.cloud.SignInResponse | Exception], None
+ ],
+ ) -> None:
+ ...
+
+ @overload
+ def send_message_cb(
+ self,
+ msg: bacommon.cloud.ManageAccountMessage,
+ on_response: Callable[
+ [bacommon.cloud.ManageAccountResponse | Exception], None
+ ],
+ ) -> None:
+ ...
+
def send_message_cb(
self,
msg: Message,
@@ -110,7 +148,6 @@ class CloudSubsystem:
def cloud_console_exec(code: str) -> None:
"""Called by the cloud console to run code in the logic thread."""
import sys
- import logging
import __main__
from ba._generated.enums import TimeType
diff --git a/assets/src/ba_data/python/ba/_coopgame.py b/assets/src/ba_data/python/ba/_coopgame.py
index 0df8beb2..6fedc3ca 100644
--- a/assets/src/ba_data/python/ba/_coopgame.py
+++ b/assets/src/ba_data/python/ba/_coopgame.py
@@ -11,7 +11,7 @@ from ba._gameactivity import GameActivity
from ba._general import WeakCall
if TYPE_CHECKING:
- from typing import Any, Sequence
+ from typing import Sequence
from bastd.actor.playerspaz import PlayerSpaz
import ba
@@ -56,56 +56,6 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
# Preload achievement images in case we get some.
_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();
# need to kill this.
def get_score_type(self) -> str:
diff --git a/assets/src/ba_data/python/ba/_gameutils.py b/assets/src/ba_data/python/ba/_gameutils.py
index 2d73d258..bd9c417a 100644
--- a/assets/src/ba_data/python/ba/_gameutils.py
+++ b/assets/src/ba_data/python/ba/_gameutils.py
@@ -278,7 +278,7 @@ def show_damage_count(
def timestring(
- timeval: float,
+ timeval: float | int,
centi: bool = True,
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
suppress_format_warning: bool = False,
diff --git a/assets/src/ba_data/python/ba/_hooks.py b/assets/src/ba_data/python/ba/_hooks.py
index 632fa5c5..9d020aa6 100644
--- a/assets/src/ba_data/python/ba/_hooks.py
+++ b/assets/src/ba_data/python/ba/_hooks.py
@@ -247,6 +247,14 @@ def google_play_purchases_not_available_message() -> None:
)
+def google_play_services_not_available_message() -> None:
+ from ba._language import Lstr
+
+ _ba.screenmessage(
+ Lstr(resource='googlePlayServicesNotAvailableText'), color=(1, 0, 0)
+ )
+
+
def empty_call() -> None:
pass
@@ -424,3 +432,41 @@ def hash_strings(inputs: list[str]) -> str:
def have_account_v2_credentials() -> bool:
"""Do we have primary account-v2 credentials set?"""
return _ba.app.accounts_v2.have_primary_credentials()
+
+
+def implicit_sign_in(
+ login_type_str: str, login_id: str, display_name: str
+) -> None:
+ """An implicit login happened."""
+ from bacommon.login import LoginType
+
+ _ba.app.accounts_v2.on_implicit_sign_in(
+ login_type=LoginType(login_type_str),
+ login_id=login_id,
+ display_name=display_name,
+ )
+
+
+def implicit_sign_out(login_type_str: str) -> None:
+ """An implicit logout happened."""
+ from bacommon.login import LoginType
+
+ _ba.app.accounts_v2.on_implicit_sign_out(
+ login_type=LoginType(login_type_str)
+ )
+
+
+def login_adapter_get_sign_in_token_response(
+ login_type_str: str, attempt_id_str: str, result_str: str
+) -> None:
+ """Login adapter do-sign-in completed."""
+ from bacommon.login import LoginType
+ from ba._login import LoginAdapterNative
+
+ login_type = LoginType(login_type_str)
+ attempt_id = int(attempt_id_str)
+ result = None if result_str == '' else result_str
+ with _ba.Context('ui'):
+ adapter = _ba.app.accounts_v2.login_adapters[login_type]
+ assert isinstance(adapter, LoginAdapterNative)
+ adapter.on_sign_in_complete(attempt_id=attempt_id, result=result)
diff --git a/assets/src/ba_data/python/ba/_internal.py b/assets/src/ba_data/python/ba/_internal.py
index 6d6e6009..4fb62ba2 100644
--- a/assets/src/ba_data/python/ba/_internal.py
+++ b/assets/src/ba_data/python/ba/_internal.py
@@ -104,7 +104,6 @@ def submit_score(
name: Any,
score: int | None,
callback: Callable,
- friend_callback: Callable | None,
order: str = 'increasing',
tournament_id: str | None = None,
score_type: str = 'points',
@@ -125,7 +124,6 @@ def submit_score(
name=name,
score=score,
callback=callback,
- friend_callback=friend_callback,
order=order,
tournament_id=tournament_id,
score_type=score_type,
@@ -321,7 +319,9 @@ def get_v1_account_state() -> str:
"""(internal)"""
if HAVE_INTERNAL:
return _bainternal.get_v1_account_state()
- raise _no_bainternal_error()
+
+ # Without internal present just consider ourself always signed out.
+ return 'signed_out'
def get_v1_account_display_string(full: bool = True) -> str:
diff --git a/assets/src/ba_data/python/ba/_login.py b/assets/src/ba_data/python/ba/_login.py
new file mode 100644
index 00000000..0ca4098e
--- /dev/null
+++ b/assets/src/ba_data/python/ba/_login.py
@@ -0,0 +1,330 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Login related functionality."""
+
+from __future__ import annotations
+
+import logging
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, final
+
+from bacommon.login import LoginType
+import _ba
+
+if TYPE_CHECKING:
+ from typing import Callable
+
+
+DEBUG_LOG = False
+
+
+class LoginAdapter:
+ """Allows using implicit login types in an explicit way.
+
+ Some login types such as Google Play Game Services or Game Center are
+ basically always present and often do not provide a way to log out
+ from within a running app, so this adapter exists to use them in a
+ flexible manner by 'attaching' and 'detaching' from an always-present
+ login, allowing for its use alongside other login types. It also
+ provides common functionality for server-side account verification and
+ other handy bits.
+ """
+
+ @dataclass
+ class SignInResult:
+ """Describes the final result of a sign-in attempt."""
+
+ credentials: str
+
+ @dataclass
+ class ImplicitLoginState:
+ """Describes the current state of an implicit login."""
+
+ login_id: str
+ display_name: str
+
+ def __init__(self, login_type: LoginType):
+ assert _ba.in_logic_thread()
+ self.login_type = login_type
+ self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = (
+ None
+ )
+ self._on_app_launch_called = False
+ self._implicit_login_state_dirty = False
+ self._back_end_active = False
+
+ # Which login of our type (if any) is associated with the
+ # current active primary account.
+ self._active_login_id: str | None = None
+
+ def on_app_launch(self) -> None:
+ """Should be called for each adapter in on_app_launch."""
+
+ assert not self._on_app_launch_called
+ self._on_app_launch_called = True
+
+ # Any implicit state we received up until now needs to be pushed
+ # to the app account subsystem.
+ self._update_implicit_login_state()
+
+ def set_implicit_login_state(
+ self, state: ImplicitLoginState | None
+ ) -> None:
+ """Keep the adapter informed of implicit login states.
+
+ This should be called by the adapter back-end when an account
+ of their associated type gets logged in or out.
+ """
+ assert _ba.in_logic_thread()
+
+ # Ignore redundant sets.
+ if state == self._implicit_login_state:
+ return
+
+ if DEBUG_LOG:
+ if state is None:
+ logging.debug(
+ 'LoginAdapter: %s implicit state changed;'
+ ' now signed out.',
+ self.login_type.name,
+ )
+ else:
+ logging.debug(
+ 'LoginAdapter: %s implicit state changed;'
+ ' now signed in as %s.',
+ self.login_type.name,
+ state.display_name,
+ )
+
+ self._implicit_login_state = state
+ self._implicit_login_state_dirty = True
+
+ # (possibly) push it to the app for handling.
+ 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:
+ """Keep the adapter informed of actively used logins.
+
+ This should be called by the app's account subsystem to
+ keep adapters up to date on the full set of logins attached
+ to the currently-in-use account.
+ Note that the logins dict passed in should be immutable as
+ only a reference to it is stored, not a copy.
+ """
+ assert _ba.in_logic_thread()
+ if DEBUG_LOG:
+ logging.debug(
+ 'LoginAdapter: %s adapter got active logins %s.',
+ self.login_type.name,
+ {k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
+ )
+
+ self._active_login_id = logins.get(self.login_type)
+ self._update_back_end_active()
+
+ def on_back_end_active_change(self, active: bool) -> None:
+ """Called when active state for the back-end is (possibly) changing.
+
+ Meant to be overridden by subclasses.
+ Being active means that the implicit login provided by the back-end
+ is actually being used by the app. It should therefore register
+ unlocked achievements, leaderboard scores, allow viewing native
+ UIs, etc. When not active it should ignore everything and behave
+ as if logged out, even if it technically is still logged in.
+ """
+ assert _ba.in_logic_thread()
+ del active # Unused.
+
+ @final
+ def sign_in(
+ self,
+ result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
+ ) -> None:
+ """Attempt an explicit sign in via this adapter.
+
+ This can be called even if the back-end is not implicitly signed in;
+ the adapter will attempt to sign in if possible. An exception will
+ be returned if the sign-in attempt fails.
+ """
+ assert _ba.in_logic_thread()
+ from ba._general import Call
+
+ if DEBUG_LOG:
+ logging.debug(
+ 'LoginAdapter: %s adapter sign_in() called;'
+ ' fetching sign-in-token...',
+ self.login_type.name,
+ )
+
+ def _got_sign_in_token_result(result: str | None) -> None:
+ import bacommon.cloud
+
+ # Failed to get a sign-in-token.
+ if result is None:
+ if DEBUG_LOG:
+ logging.debug(
+ 'LoginAdapter: %s adapter sign-in-token fetch failed;'
+ ' aborting sign-in.',
+ self.login_type.name,
+ )
+ _ba.pushcall(
+ Call(
+ result_cb,
+ self,
+ RuntimeError('fetch-sign-in-token failed.'),
+ )
+ )
+ return
+
+ # Got a sign-in token! Now pass it to the cloud which will use
+ # it to verify our identity and give us app credentials on
+ # success.
+ if DEBUG_LOG:
+ logging.debug(
+ 'LoginAdapter: %s adapter sign-in-token fetch succeeded;'
+ ' passing to cloud for verification...',
+ self.login_type.name,
+ )
+
+ def _got_sign_in_response(
+ response: bacommon.cloud.SignInResponse | Exception,
+ ) -> None:
+
+ if isinstance(response, Exception):
+ if DEBUG_LOG:
+ logging.debug(
+ 'LoginAdapter: %s adapter got error'
+ ' sign-in response: %s',
+ self.login_type.name,
+ response,
+ )
+ _ba.pushcall(Call(result_cb, self, response))
+ else:
+ if DEBUG_LOG:
+ logging.debug(
+ 'LoginAdapter: %s adapter got successful'
+ ' sign-in response',
+ self.login_type.name,
+ )
+ if response.credentials is None:
+ result2: LoginAdapter.SignInResult | Exception = (
+ RuntimeError(
+ 'No credentials returned after'
+ ' submitting sign-in-token.'
+ )
+ )
+ else:
+ result2 = self.SignInResult(
+ credentials=response.credentials
+ )
+ _ba.pushcall(Call(result_cb, self, result2))
+
+ _ba.app.cloud.send_message_cb(
+ bacommon.cloud.SignInMessage(self.login_type, result),
+ on_response=_got_sign_in_response,
+ )
+
+ # Kick off the process by fetching a sign-in token.
+ self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
+
+ def is_back_end_active(self) -> bool:
+ """Is this adapter's back-end currently active?"""
+ return self._back_end_active
+
+ def get_sign_in_token(
+ self, completion_cb: Callable[[str | None], None]
+ ) -> None:
+ """Get a sign-in token from the adapter back end.
+
+ This token is then passed to the master-server to complete the
+ login process.
+ The adapter can use this opportunity to bring up account creation
+ UI, call its internal sign_in function, etc. as needed.
+ The provided completion_cb should then be called with either a token
+ or None if sign in failed or was cancelled.
+ """
+ from ba._general import Call
+
+ # Default implementation simply fails immediately.
+ _ba.pushcall(Call(completion_cb, None))
+
+ def _update_implicit_login_state(self) -> None:
+ # If we've received an implicit login state, schedule it to be
+ # sent along to the app. We wait until on-app-launch has been
+ # called so that account-client-v2 has had a chance to load
+ # any existing state so it can properly respond to this.
+ if self._implicit_login_state_dirty and self._on_app_launch_called:
+ from ba._general import Call
+
+ if DEBUG_LOG:
+ logging.debug(
+ 'LoginAdapter: %s adapter sending'
+ ' implicit-state-changed to app.',
+ self.login_type.name,
+ )
+
+ _ba.pushcall(
+ Call(
+ _ba.app.accounts_v2.on_implicit_login_state_changed,
+ self.login_type,
+ self._implicit_login_state,
+ )
+ )
+ self._implicit_login_state_dirty = False
+
+ def _update_back_end_active(self) -> None:
+ was_active = self._back_end_active
+ if self._implicit_login_state is None:
+ is_active = False
+ else:
+ is_active = (
+ self._implicit_login_state.login_id == self._active_login_id
+ )
+ if was_active != is_active:
+ if DEBUG_LOG:
+ logging.debug(
+ 'LoginAdapter: %s adapter back-end-active is now %s.',
+ self.login_type.name,
+ is_active,
+ )
+ self.on_back_end_active_change(is_active)
+ self._back_end_active = is_active
+
+
+class LoginAdapterNative(LoginAdapter):
+ """A login adapter that does its work in the native layer."""
+
+ def __init__(self) -> None:
+ super().__init__(LoginType.GPGS)
+
+ # Store int ids for in-flight attempts since they may go through
+ # various platform layers and back.
+ self._sign_in_attempt_num = 123
+ self._sign_in_attempts: dict[int, Callable[[str | None], None]] = {}
+
+ def get_sign_in_token(
+ self, completion_cb: Callable[[str | None], None]
+ ) -> None:
+ attempt_id = self._sign_in_attempt_num
+ self._sign_in_attempts[attempt_id] = completion_cb
+ self._sign_in_attempt_num += 1
+ _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:
+ """Called by the native layer on a completed attempt."""
+ assert _ba.in_logic_thread()
+ if attempt_id not in self._sign_in_attempts:
+ logging.exception('sign-in attempt_id %d not found', attempt_id)
+ return
+ callback = self._sign_in_attempts.pop(attempt_id)
+ callback(result)
+
+
+class LoginAdapterGPGS(LoginAdapterNative):
+ """Google Play Game Services adapter."""
diff --git a/assets/src/ba_data/python/ba/internal.py b/assets/src/ba_data/python/ba/internal.py
index cab4bab7..c688faf1 100644
--- a/assets/src/ba_data/python/ba/internal.py
+++ b/assets/src/ba_data/python/ba/internal.py
@@ -80,6 +80,7 @@ from _ba import (
get_replays_dir,
)
+from ba._login import LoginAdapter
from ba._map import (
get_map_class,
register_map,
@@ -99,6 +100,8 @@ from ba._apputils import (
is_browser_likely_available,
get_remote_app_name,
should_submit_debug_info,
+ dump_tracebacks,
+ log_dumped_tracebacks,
)
from ba._benchmark import (
run_gpu_benchmark,
@@ -178,6 +181,7 @@ from ba._internal import (
)
__all__ = [
+ 'LoginAdapter',
'show_online_score_ui',
'set_ui_input_device',
'is_party_icon_visible',
@@ -247,7 +251,6 @@ __all__ = [
'set_telnet_access_enabled',
'new_replay_session',
'get_replays_dir',
- # DIVIDER
'get_unowned_maps',
'get_unowned_game_types',
'get_map_class',
@@ -329,4 +332,6 @@ __all__ = [
'sign_out_v1',
'sign_in_v1',
'mark_config_dirty',
+ 'dump_tracebacks',
+ 'log_dumped_tracebacks',
]
diff --git a/assets/src/ba_data/python/ba/modutils.py b/assets/src/ba_data/python/ba/modutils.py
index 2bda5c49..6fef1e8c 100644
--- a/assets/src/ba_data/python/ba/modutils.py
+++ b/assets/src/ba_data/python/ba/modutils.py
@@ -87,7 +87,7 @@ def show_user_scripts() -> None:
' See settings/advanced'
' in the game for more info.'
)
- _ba.android_media_scan_file(file_name)
+
except Exception:
from ba import _error
diff --git a/assets/src/ba_data/python/bastd/activity/coopscore.py b/assets/src/ba_data/python/bastd/activity/coopscore.py
index 5c32fdeb..63864605 100644
--- a/assets/src/ba_data/python/bastd/activity/coopscore.py
+++ b/assets/src/ba_data/python/bastd/activity/coopscore.py
@@ -116,7 +116,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
self._newly_complete: bool | None = None
self._is_more_levels: bool | 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._name_str: str | None = None
self._friends_loading_status: ba.Actor | None = None
@@ -177,12 +176,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
.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:
self._old_best_rank = self._campaign.getlevel(
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.
- 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:
ba.buttonwidget(
parent=rootc,
@@ -773,18 +751,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
[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(
ba.Lstr(
value='${A}...',
@@ -850,8 +816,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
# We expect this only in kiosk mode; complain otherwise.
if not (ba.app.demo_mode or ba.app.arcade_mode):
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))
else:
assert self._game_name_str is not None
@@ -862,9 +826,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
name_str,
self._score,
ba.WeakCall(self._got_score_results),
- ba.WeakCall(self._got_friend_score_results)
- if self._show_friend_scores
- else None,
order=self._score_order,
tournament_id=self.session.tournament_id,
score_type=self._score_type,
@@ -899,138 +860,118 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
assert txt.node
txt.node.client_only = True
- # If we have no friend scores, display local best scores.
- if self._show_friend_scores:
+ ts_height = 300
+ 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.
- ts_height = 300
- 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:
+ display_scores = list(our_high_scores)
+ display_count = 5
- ts_height = 300
- 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()
+ while len(display_scores) < display_count:
+ display_scores.append((0, None))
- display_scores = list(our_high_scores)
- display_count = 5
-
- while len(display_scores) < display_count:
- display_scores.append((0, None))
-
- showed_ours = False
- h_offs_extra = 85 if self._score_type == 'points' else 130
- v_offs_extra = 20
- v_offs_names = 0
- scale = 1.0
- p_count = len(self._playerinfos)
- h_offs_extra -= 75
- if p_count > 1:
- h_offs_extra -= 20
- if p_count == 2:
- scale = 0.9
- elif p_count == 3:
- scale = 0.65
- elif p_count == 4:
- scale = 0.5
- times: list[tuple[float, float]] = []
- for i in range(display_count):
- times.insert(
- 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}'
- )
+ showed_ours = False
+ h_offs_extra = 85 if self._score_type == 'points' else 130
+ v_offs_extra = 20
+ v_offs_names = 0
+ scale = 1.0
+ p_count = len(self._playerinfos)
+ h_offs_extra -= 75
+ if p_count > 1:
+ h_offs_extra -= 20
+ if p_count == 2:
+ scale = 0.9
+ elif p_count == 3:
+ scale = 0.65
+ elif p_count == 4:
+ scale = 0.5
+ times: list[tuple[float, float]] = []
+ for i in range(display_count):
+ times.insert(
+ 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 = '-'
- 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:
- flash = False
- color0 = (0.6, 0.4, 0.1, 1.0)
- color1 = (0.6, 0.6, 0.6, 1.0)
- tdelay1 = times[i][0]
- tdelay2 = times[i][1]
- Text(
- str(display_scores[i][0])
- if self._score_type == 'points'
- else ba.timestring(
- display_scores[i][0] * 10,
- 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()
+ # 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 = '-'
+ 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:
+ flash = False
+ color0 = (0.6, 0.4, 0.1, 1.0)
+ color1 = (0.6, 0.6, 0.6, 1.0)
+ tdelay1 = times[i][0]
+ tdelay2 = times[i][1]
+ Text(
+ str(display_scores[i][0])
+ if self._score_type == 'points'
+ else ba.timestring(
+ display_scores[i][0] * 10,
+ 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(
- ba.Lstr(value=name_str),
- position=(
- ts_h_offs + 35 + h_offs_extra,
- v_offs_extra
- + ts_height / 2
- + -ts_height * (i + 1) / 10
- + v_offs_names
- + v_offs
- + 11.0,
- ),
- maxwidth=80.0 + 100.0 * len(self._playerinfos),
- v_align=Text.VAlign.CENTER,
- color=color1,
- flash=flash,
- scale=scale,
- transition=Text.Transition.IN_RIGHT,
- transition_delay=tdelay2,
- ).autoretain()
+ Text(
+ ba.Lstr(value=name_str),
+ position=(
+ ts_h_offs + 35 + h_offs_extra,
+ v_offs_extra
+ + ts_height / 2
+ + -ts_height * (i + 1) / 10
+ + v_offs_names
+ + v_offs
+ + 11.0,
+ ),
+ maxwidth=80.0 + 100.0 * len(self._playerinfos),
+ v_align=Text.VAlign.CENTER,
+ color=color1,
+ flash=flash,
+ scale=scale,
+ transition=Text.Transition.IN_RIGHT,
+ transition_delay=tdelay2,
+ ).autoretain()
# Show achievements for this level.
ts_height = -150
diff --git a/assets/src/ba_data/python/bastd/game/football.py b/assets/src/ba_data/python/bastd/game/football.py
index a88583e4..a8d87771 100644
--- a/assets/src/ba_data/python/bastd/game/football.py
+++ b/assets/src/ba_data/python/bastd/game/football.py
@@ -616,9 +616,6 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
for bottype in self._bot_types_initial:
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:
# We want to move to the left by default.
spaz.target_point_default = ba.Vec3(0, 0, 0)
diff --git a/assets/src/ba_data/python/bastd/game/onslaught.py b/assets/src/ba_data/python/bastd/game/onslaught.py
index 90aa87be..bf275137 100644
--- a/assets/src/ba_data/python/bastd/game/onslaught.py
+++ b/assets/src/ba_data/python/bastd/game/onslaught.py
@@ -682,9 +682,6 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]):
self._bots = SpazBotSet()
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]:
totalpts = 0
totaldudes = 0
diff --git a/assets/src/ba_data/python/bastd/game/runaround.py b/assets/src/ba_data/python/bastd/game/runaround.py
index d8f5e4b4..379ae8e3 100644
--- a/assets/src/ba_data/python/bastd/game/runaround.py
+++ b/assets/src/ba_data/python/bastd/game/runaround.py
@@ -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:
# pylint: disable=too-many-branches
diff --git a/assets/src/ba_data/python/bastd/game/thelaststand.py b/assets/src/ba_data/python/bastd/game/thelaststand.py
index 2040e5c2..62e98c91 100644
--- a/assets/src/ba_data/python/bastd/game/thelaststand.py
+++ b/assets/src/ba_data/python/bastd/game/thelaststand.py
@@ -326,9 +326,6 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
else:
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:
# Tell our bots to celebrate just to rub it in.
self._bots.final_celebrate()
diff --git a/assets/src/ba_data/python/bastd/ui/account/settings.py b/assets/src/ba_data/python/bastd/ui/account/settings.py
index 3aae9917..04d17a36 100644
--- a/assets/src/ba_data/python/bastd/ui/account/settings.py
+++ b/assets/src/ba_data/python/bastd/ui/account/settings.py
@@ -6,13 +6,22 @@
from __future__ import annotations
import time
+import logging
from typing import TYPE_CHECKING
+import bacommon.cloud
+from bacommon.login import LoginType
import ba
import ba.internal
if TYPE_CHECKING:
- pass
+ from ba.internal import LoginAdapter
+
+# We only show v1 linking controls when directly signed in with
+# V1 accounts. Generally V2 accounts should use the web ui for linking.
+# However we have an escape-hatch here if someone needs to access
+# V1 linking for the V1 portion of their V2 account.
+FORCE_ENABLE_V1_LINKING = False
class AccountSettingsWindow(ba.Window):
@@ -27,12 +36,15 @@ class AccountSettingsWindow(ba.Window):
):
# pylint: disable=too-many-statements
- self._sign_in_v2_button: ba.Widget | None = None
+ self._sign_in_v2_proxy_button: ba.Widget | None = None
self._sign_in_device_button: ba.Widget | None = None
+ self._signing_in_adapter: LoginAdapter | None = None
self._close_once_signed_in = close_once_signed_in
ba.set_analytics_screen('Account Window')
+ self._explicitly_signed_out_of_gpgs = False
+
# If they provided an origin-widget, scale up from that.
scale_origin: tuple[float, float] | None
if origin_widget is not None:
@@ -92,16 +104,15 @@ class AccountSettingsWindow(ba.Window):
# Determine which sign-in/sign-out buttons we should show.
self._show_sign_in_buttons: list[str] = []
- if app.platform == 'android' and app.subplatform == 'google':
+ if LoginType.GPGS in ba.app.accounts_v2.login_adapters:
self._show_sign_in_buttons.append('Google Play')
- # Local accounts are generally always available with a few key
- # exceptions.
- self._show_sign_in_buttons.append('Local')
+ # Always want to show our web-based v2 login option.
+ self._show_sign_in_buttons.append('V2Proxy')
- # Ditto with shiny new V2 ones.
- if bool(True):
- self._show_sign_in_buttons.append('V2')
+ # Legacy v1 device accounts are currently always available
+ # (though we need to start phasing them out at some point).
+ self._show_sign_in_buttons.append('Device')
top_extra = 15 if uiscale is ba.UIScale.SMALL else 0
super().__init__(
@@ -226,17 +237,22 @@ class AccountSettingsWindow(ba.Window):
# pylint: disable=cyclic-import
from bastd.ui import confirm
- account_state = ba.internal.get_v1_account_state()
- account_type = (
+ primary_v2_account = ba.app.accounts_v2.primary
+
+ v1_state = ba.internal.get_v1_account_state()
+ v1_account_type = (
ba.internal.get_v1_account_type()
- if account_state == 'signed_in'
+ if v1_state == 'signed_in'
else 'unknown'
)
- is_google = account_type == 'Google Play'
-
- show_local_signed_in_as = False
- local_signed_in_as_space = 50.0
+ # We expose GPGS-specific functionality only if it is 'active'
+ # (meaning the current GPGS player matches one of our account's
+ # logins).
+ 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
signed_in_as_space = 95.0
@@ -244,40 +260,42 @@ class AccountSettingsWindow(ba.Window):
show_sign_in_benefits = not self._signed_in
sign_in_benefits_space = 80.0
- show_signing_in_text = account_state == 'signing_in'
+ show_signing_in_text = (
+ v1_state == 'signing_in' or self._signing_in_adapter is not None
+ )
signing_in_text_space = 80.0
show_google_play_sign_in_button = (
- account_state == 'signed_out'
+ v1_state == 'signed_out'
+ and self._signing_in_adapter is None
and 'Google Play' in self._show_sign_in_buttons
)
- show_device_sign_in_button = (
- account_state == 'signed_out'
- and 'Local' in self._show_sign_in_buttons
+ show_v2_proxy_sign_in_button = (
+ v1_state == 'signed_out'
+ and self._signing_in_adapter is None
+ and 'V2Proxy' in self._show_sign_in_buttons
)
- show_v2_sign_in_button = (
- account_state == 'signed_out' and 'V2' 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
+ deprecated_space = 60
- 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_service_button_space = 60.0
- show_linked_accounts_text = (
- self._signed_in
- and ba.internal.get_v1_account_misc_read_val(
- 'allowAccountLinking2', False
- )
- )
+ show_what_is_v2 = self._signed_in and v1_account_type == 'V2'
+
+ show_linked_accounts_text = self._signed_in
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',
- 'Alibaba',
'Local',
- 'OUYA',
'V2',
)
achievements_button_space = 60.0
@@ -287,7 +305,7 @@ class AccountSettingsWindow(ba.Window):
)
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
show_campaign_progress = self._signed_in
@@ -300,8 +318,8 @@ class AccountSettingsWindow(ba.Window):
reset_progress_button_space = 70.0
show_manage_v2_account_button = (
- self._signed_in and account_type == 'V2' and bool(False)
- ) # Disabled for now.
+ self._signed_in and v1_account_type == 'V2'
+ )
manage_v2_account_button_space = 100.0
show_player_profiles_button = self._signed_in
@@ -309,45 +327,46 @@ class AccountSettingsWindow(ba.Window):
70.0 if show_manage_v2_account_button else 100.0
)
- show_link_accounts_button = (
- self._signed_in
- and ba.internal.get_v1_account_misc_read_val(
- 'allowAccountLinking2', False
- )
+ show_link_accounts_button = self._signed_in and (
+ primary_v2_account is None or FORCE_ENABLE_V1_LINKING
)
link_accounts_button_space = 70.0
show_unlink_accounts_button = show_link_accounts_button
unlink_accounts_button_space = 90.0
- show_sign_out_button = self._signed_in and account_type in [
+ show_v2_link_info = self._signed_in and not show_link_accounts_button
+ v2_link_info_space = 70.0
+
+ show_sign_out_button = self._signed_in and v1_account_type in [
'Local',
'Google Play',
'V2',
]
sign_out_button_space = 70.0
- show_cancel_v2_sign_in_button = (
- account_state == 'signing_in'
- and ba.app.accounts_v2.have_primary_credentials()
+ # We can show cancel if we're either waiting on an adapter to
+ # provide us with v2 credentials or waiting for those credentials
+ # to be verified.
+ show_cancel_sign_in_button = self._signing_in_adapter is not None or (
+ ba.app.accounts_v2.have_primary_credentials()
+ and primary_v2_account is None
)
- cancel_v2_sign_in_button_space = 70.0
+ cancel_sign_in_button_space = 70.0
if self._subcontainer is not None:
self._subcontainer.delete()
self._sub_height = 60.0
- if show_local_signed_in_as:
- self._sub_height += local_signed_in_as_space
if show_signed_in_as:
self._sub_height += signed_in_as_space
if show_signing_in_text:
self._sub_height += signing_in_text_space
if show_google_play_sign_in_button:
self._sub_height += sign_in_button_space
+ if show_v2_proxy_sign_in_button:
+ self._sub_height += sign_in_button_space
if show_device_sign_in_button:
- self._sub_height += sign_in_button_space
- if show_v2_sign_in_button:
- self._sub_height += sign_in_button_space
+ self._sub_height += sign_in_button_space + deprecated_space
if show_game_service_button:
self._sub_height += game_service_button_space
if show_linked_accounts_text:
@@ -374,10 +393,12 @@ class AccountSettingsWindow(ba.Window):
self._sub_height += link_accounts_button_space
if show_unlink_accounts_button:
self._sub_height += unlink_accounts_button_space
+ if show_v2_link_info:
+ self._sub_height += v2_link_info_space
if show_sign_out_button:
self._sub_height += sign_out_button_space
- if show_cancel_v2_sign_in_button:
- self._sub_height += cancel_v2_sign_in_button_space
+ if show_cancel_sign_in_button:
+ self._sub_height += cancel_sign_in_button_space
self._subcontainer = ba.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
@@ -390,27 +411,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = None
v = self._sub_height - 10.0
- if show_local_signed_in_as:
- v -= local_signed_in_as_space * 0.6
- 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_what_is_text: ba.Widget | None
+ self._account_name_what_is_y = 0.0
self._account_name_text: ba.Widget | None
if show_signed_in_as:
v -= signed_in_as_space * 0.2
@@ -429,7 +431,7 @@ class AccountSettingsWindow(ba.Window):
h_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(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v),
@@ -441,10 +443,39 @@ class AccountSettingsWindow(ba.Window):
h_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()
+
v -= signed_in_as_space * 0.4
+
else:
self._account_name_text = None
+ self._account_name_what_is_text = None
if self._back_button is None:
bbtn = ba.internal.get_special_widget('back_button')
@@ -528,7 +559,7 @@ class AccountSettingsWindow(ba.Window):
),
],
),
- on_activate_call=lambda: self._sign_in_press('Google Play'),
+ on_activate_call=lambda: self._sign_in_press(LoginType.GPGS),
)
if first_selectable is None:
first_selectable = btn
@@ -541,16 +572,16 @@ class AccountSettingsWindow(ba.Window):
ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None
- if show_v2_sign_in_button:
+ if show_v2_proxy_sign_in_button:
button_width = 350
v -= sign_in_button_space
- self._sign_in_v2_button = btn = ba.buttonwidget(
+ self._sign_in_v2_proxy_button = btn = ba.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v - 20),
autoselect=True,
size=(button_width, 60),
label='',
- on_activate_call=self._v2_sign_in_press,
+ on_activate_call=self._v2_proxy_sign_in_press,
)
ba.textwidget(
parent=self._subcontainer,
@@ -598,7 +629,7 @@ class AccountSettingsWindow(ba.Window):
if show_device_sign_in_button:
button_width = 350
- v -= sign_in_button_space
+ v -= sign_in_button_space + deprecated_space
self._sign_in_device_button = btn = ba.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v - 20),
@@ -607,6 +638,18 @@ class AccountSettingsWindow(ba.Window):
label='',
on_activate_call=lambda: self._sign_in_press('Local'),
)
+ ba.textwidget(
+ parent=self._subcontainer,
+ h_align='center',
+ v_align='center',
+ size=(0, 0),
+ position=(self._sub_width * 0.5, v + 60),
+ text=ba.Lstr(resource='deprecatedText'),
+ scale=0.8,
+ maxwidth=300,
+ color=(0.6, 0.55, 0.45),
+ )
+
ba.textwidget(
parent=self._subcontainer,
draw_controller=btn,
@@ -663,9 +706,7 @@ class AccountSettingsWindow(ba.Window):
color=(0.55, 0.5, 0.6),
icon=ba.gettexture('settingsIcon'),
textcolor=(0.75, 0.7, 0.8),
- on_activate_call=lambda: ba.open_url(
- 'https://ballistica.net/accountsettings'
- ),
+ on_activate_call=ba.WeakCall(self._on_manage_account_press),
)
if first_selectable is None:
first_selectable = btn
@@ -703,12 +744,12 @@ class AccountSettingsWindow(ba.Window):
if show_game_service_button:
button_width = 300
v -= game_service_button_space * 0.85
- account_type = ba.internal.get_v1_account_type()
- if account_type == 'Game Center':
- account_type_name = ba.Lstr(resource='gameCenterText')
+ v1_account_type = ba.internal.get_v1_account_type()
+ if v1_account_type == 'Game Center':
+ v1_account_type_name = ba.Lstr(resource='gameCenterText')
else:
raise ValueError(
- "unknown account type: '" + str(account_type) + "'"
+ "unknown account type: '" + str(v1_account_type) + "'"
)
self._game_service_button = btn = ba.buttonwidget(
parent=self._subcontainer,
@@ -718,7 +759,7 @@ class AccountSettingsWindow(ba.Window):
autoselect=True,
on_activate_call=ba.internal.show_online_score_ui,
size=(button_width, 50),
- label=account_type_name,
+ label=v1_account_type_name,
)
if first_selectable is None:
first_selectable = btn
@@ -761,11 +802,15 @@ class AccountSettingsWindow(ba.Window):
autoselect=True,
icon=ba.gettexture(
'googlePlayAchievementsIcon'
- if is_google
+ if is_gpgs
else 'achievementsIcon'
),
- icon_color=(0.8, 0.95, 0.7) if is_google else (0.85, 0.8, 0.9),
- on_activate_call=self._on_achievements_press,
+ icon_color=(0.8, 0.95, 0.7) if is_gpgs else (0.85, 0.8, 0.9),
+ on_activate_call=(
+ self._on_custom_achievements_press
+ if is_gpgs
+ else self._on_achievements_press
+ ),
size=(button_width, 50),
label='',
)
@@ -897,6 +942,7 @@ class AccountSettingsWindow(ba.Window):
scale=0.9,
color=(0.75, 0.7, 0.8),
maxwidth=self._sub_width * 0.95,
+ text=ba.Lstr(resource=self._r + '.linkedAccountsText'),
h_align='center',
v_align='center',
)
@@ -905,6 +951,8 @@ class AccountSettingsWindow(ba.Window):
else:
self._linked_accounts_text = None
+ # Show link/unlink buttons only for V1 accounts.
+
if show_link_accounts_button:
v -= link_accounts_button_space
self._link_accounts_button = btn = ba.buttonwidget(
@@ -984,6 +1032,21 @@ class AccountSettingsWindow(ba.Window):
else:
self._unlink_accounts_button = None
+ if show_v2_link_info:
+ v -= v2_link_info_space
+ ba.textwidget(
+ parent=self._subcontainer,
+ h_align='center',
+ v_align='center',
+ size=(0, 0),
+ position=(self._sub_width * 0.5, v + v2_link_info_space - 20),
+ text=ba.Lstr(resource='v2AccountLinkingInfoText'),
+ flatness=1.0,
+ scale=0.8,
+ maxwidth=450,
+ color=(0.5, 0.45, 0.55),
+ )
+
if show_sign_out_button:
v -= sign_out_button_space
self._sign_out_button = btn = ba.buttonwidget(
@@ -1005,9 +1068,9 @@ class AccountSettingsWindow(ba.Window):
)
ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
- if show_cancel_v2_sign_in_button:
- v -= cancel_v2_sign_in_button_space
- self._cancel_v2_sign_in_button = btn = ba.buttonwidget(
+ if show_cancel_sign_in_button:
+ v -= cancel_sign_in_button_space
+ self._cancel_sign_in_button = btn = ba.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v),
size=(button_width, 60),
@@ -1015,7 +1078,7 @@ class AccountSettingsWindow(ba.Window):
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
autoselect=True,
- on_activate_call=self._cancel_v2_sign_in_press,
+ on_activate_call=self._cancel_sign_in_press,
)
if first_selectable is None:
first_selectable = btn
@@ -1038,34 +1101,52 @@ class AccountSettingsWindow(ba.Window):
)
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:
# pylint: disable=cyclic-import
from bastd.ui import achievements
- account_state = ba.internal.get_v1_account_state()
- account_type = (
- ba.internal.get_v1_account_type()
- if account_state == 'signed_in'
- else 'unknown'
+ assert self._achievements_button is not None
+ achievements.AchievementsWindow(
+ position=self._achievements_button.get_screen_space_center()
)
- # for google play we use the built-in UI; otherwise pop up our own
- if account_type == 'Google Play':
- ba.timer(
- 0.15,
- ba.Call(ba.internal.show_online_score_ui, 'achievements'),
- timetype=ba.TimeType.REAL,
+
+ def _on_what_is_v2_press(self) -> None:
+ show_what_is_v2_page()
+
+ def _on_manage_account_press(self) -> None:
+ ba.screenmessage(ba.Lstr(resource='oneMomentText'))
+
+ # We expect to have a v2 account signed in if we get here.
+ if ba.app.accounts_v2.primary is None:
+ logging.exception(
+ 'got manage-account press without v2 account present'
)
- 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,
+ return
+
+ with ba.app.accounts_v2.primary:
+ ba.app.cloud.send_message_cb(
+ bacommon.cloud.ManageAccountMessage(),
+ on_response=ba.WeakCall(self._on_manage_account_response),
)
+ def _on_manage_account_response(
+ self, response: bacommon.cloud.ManageAccountResponse | Exception
+ ) -> None:
+
+ if isinstance(response, Exception) or response.url is None:
+ ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0))
+ ba.playsound(ba.getsound('error'))
+ return
+
+ ba.open_url(response.url)
+
def _on_leaderboards_press(self) -> None:
ba.timer(
0.15,
@@ -1096,6 +1177,14 @@ class AccountSettingsWindow(ba.Window):
if self._linked_accounts_text is None:
return
+ # Disable this by default when signed in to a V2 account
+ # (since this shows V1 links which we should no longer care about).
+ if (
+ ba.app.accounts_v2.primary is not None
+ and not FORCE_ENABLE_V1_LINKING
+ ):
+ return
+
# if this is not present, we haven't had contact from the server so
# let's not proceed..
if ba.internal.get_public_login_id() is None:
@@ -1105,13 +1194,9 @@ class AccountSettingsWindow(ba.Window):
accounts = ba.internal.get_v1_account_misc_read_val_2(
'linkedAccounts', []
)
- # our_account = _bs.get_v1_account_display_string()
- # accounts = [a for a in accounts if a != our_account]
- # accounts_str = u', '.join(accounts) if accounts else
- # ba.Lstr(translate=('settingNames', 'None'))
# UPDATE - we now just print the number here; not the actual
- # accounts
- # (they can see that in the unlink section if they're curious)
+ # accounts (they can see that in the unlink section if they're
+ # curious)
accounts_str = str(max(0, len(accounts) - 1))
ba.textwidget(
edit=self._linked_accounts_text,
@@ -1162,6 +1247,7 @@ class AccountSettingsWindow(ba.Window):
)
def _refresh_account_name_text(self) -> None:
+
if self._account_name_text is None:
return
try:
@@ -1169,7 +1255,20 @@ class AccountSettingsWindow(ba.Window):
except Exception:
ba.print_exception()
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:
if (
@@ -1214,16 +1313,27 @@ class AccountSettingsWindow(ba.Window):
origin_widget=self._player_profiles_button
)
- def _cancel_v2_sign_in_press(self) -> None:
- # Just say we don't wanna be signed in anymore.
+ def _cancel_sign_in_press(self) -> None:
+
+ # If we're waiting on an adapter to give us credentials, abort.
+ self._signing_in_adapter = None
+
+ # Say we don't wanna be signed in anymore if we are.
ba.app.accounts_v2.set_primary_credentials(None)
+ self._needs_refresh = True
+
# Speed UI updates along.
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
def _sign_out_press(self) -> None:
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)
else:
ba.internal.sign_out_v1()
@@ -1242,25 +1352,90 @@ class AccountSettingsWindow(ba.Window):
# Speed UI updates along.
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
- def _sign_in_press(
- self, account_type: str, show_test_warning: bool = True
- ) -> None:
- del show_test_warning # unused
- ba.internal.sign_in_v1(account_type)
+ def _sign_in_press(self, login_type: str | LoginType) -> None:
- # Make note of the type account we're *wanting* to be signed in with.
- cfg = ba.app.config
- cfg['Auto Account State'] = account_type
- cfg.commit()
+ # V1 login types are strings.
+ if isinstance(login_type, str):
+ ba.internal.sign_in_v1(login_type)
+
+ # Make note of the type account we're *wanting*
+ # to be signed in with.
+ cfg = ba.app.config
+ cfg['Auto Account State'] = login_type
+ cfg.commit()
+ self._needs_refresh = True
+ ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
+ return
+
+ # V2 login sign-in buttons generally go through adapters.
+ adapter = ba.app.accounts_v2.login_adapters.get(login_type)
+ if adapter is not None:
+ self._signing_in_adapter = adapter
+ adapter.sign_in(
+ result_cb=ba.WeakCall(self._on_adapter_sign_in_result)
+ )
+ # Will get 'Signing in...' to show.
+ self._needs_refresh = True
+ ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
+ else:
+ ba.screenmessage(f'Unsupported login_type: {login_type.name}')
+
+ def _on_adapter_sign_in_result(
+ self,
+ adapter: LoginAdapter,
+ result: LoginAdapter.SignInResult | Exception,
+ ) -> None:
+ is_us = self._signing_in_adapter is adapter
+
+ # If this isn't our current one we don't care.
+ if not is_us:
+ return
+
+ # If it is us, note that we're done.
+ self._signing_in_adapter = None
+
+ if isinstance(result, Exception):
+ # For now just make a bit of noise if anything went wrong;
+ # can get more specific as needed later.
+ ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0))
+ ba.playsound(ba.getsound('error'))
+ else:
+ # Success! Plug in these credentials which will begin
+ # verifying them and set our primary account-handle
+ # when finished.
+ 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.
self._needs_refresh = True
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
- def _v2_sign_in_press(self) -> None:
+ def _v2_proxy_sign_in_press(self) -> None:
# pylint: disable=cyclic-import
- from bastd.ui.account.v2 import V2SignInWindow
+ from bastd.ui.account.v2proxy import V2ProxySignInWindow
- assert self._sign_in_v2_button is not None
- V2SignInWindow(origin_widget=self._sign_in_v2_button)
+ assert self._sign_in_v2_proxy_button is not None
+ V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button)
def _reset_progress(self) -> None:
try:
@@ -1319,3 +1494,9 @@ class AccountSettingsWindow(ba.Window):
ba.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
ba.print_exception(f'Error restoring state for {self}.')
+
+
+def show_what_is_v2_page() -> None:
+ """Show the webpage describing V2 accounts."""
+ bamasteraddr = ba.internal.get_master_server_address(version=2)
+ ba.open_url(f'{bamasteraddr}/whatisv2')
diff --git a/assets/src/ba_data/python/bastd/ui/account/v2.py b/assets/src/ba_data/python/bastd/ui/account/v2proxy.py
similarity index 98%
rename from assets/src/ba_data/python/bastd/ui/account/v2.py
rename to assets/src/ba_data/python/bastd/ui/account/v2proxy.py
index 7f155e8a..a2d4fb76 100644
--- a/assets/src/ba_data/python/bastd/ui/account/v2.py
+++ b/assets/src/ba_data/python/bastd/ui/account/v2proxy.py
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
STATUS_CHECK_INTERVAL_SECONDS = 2.0
-class V2SignInWindow(ba.Window):
+class V2ProxySignInWindow(ba.Window):
"""A window allowing signing in to a v2 account."""
def __init__(self, origin_widget: ba.Widget):
@@ -95,7 +95,7 @@ class V2SignInWindow(ba.Window):
)
return
- # Show link(s) the user can use to log in.
+ # Show link(s) the user can use to sign in.
address = (
ba.internal.get_master_server_address(version=2) + response.url
)
diff --git a/assets/src/ba_data/python/bastd/ui/appinvite.py b/assets/src/ba_data/python/bastd/ui/appinvite.py
index bd1fd41e..525e8711 100644
--- a/assets/src/ba_data/python/bastd/ui/appinvite.py
+++ b/assets/src/ba_data/python/bastd/ui/appinvite.py
@@ -413,6 +413,9 @@ def handle_app_invites_press(force_code: bool = False) -> None:
and ba.internal.get_v1_account_misc_read_val('enableAppInvites', False)
and not app.on_tv
)
+ # Update: google's app invites are deprecated.
+ do_app_invites = False
+
if force_code:
do_app_invites = False
diff --git a/assets/src/ba_data/python/bastd/ui/configerror.py b/assets/src/ba_data/python/bastd/ui/configerror.py
index 129e5eaa..3165bfc3 100644
--- a/assets/src/ba_data/python/bastd/ui/configerror.py
+++ b/assets/src/ba_data/python/bastd/ui/configerror.py
@@ -20,12 +20,12 @@ class ConfigErrorWindow(ba.Window):
self._config_file_path = ba.app.config_file_path
width = 800
super().__init__(
- ba.containerwidget(size=(width, 300), transition='in_right')
+ ba.containerwidget(size=(width, 400), transition='in_right')
)
padding = 20
ba.textwidget(
parent=self._root_widget,
- position=(padding, 220),
+ position=(padding, 220 + 60),
size=(width - 2 * padding, 100 - 2 * padding),
h_align='center',
v_align='top',
@@ -41,7 +41,7 @@ class ConfigErrorWindow(ba.Window):
)
ba.textwidget(
parent=self._root_widget,
- position=(padding, 198),
+ position=(padding, 198 + 60),
size=(width - 2 * padding, 100 - 2 * padding),
h_align='center',
v_align='top',
diff --git a/assets/src/ba_data/python/bastd/ui/gather/__init__.py b/assets/src/ba_data/python/bastd/ui/gather/__init__.py
index 0b3ed076..42688276 100644
--- a/assets/src/ba_data/python/bastd/ui/gather/__init__.py
+++ b/assets/src/ba_data/python/bastd/ui/gather/__init__.py
@@ -44,6 +44,7 @@ class GatherTab:
The tab should create and return a container widget covering the
specified region.
"""
+ raise RuntimeError('Should not get here.')
def on_deactivate(self) -> None:
"""Called when the tab will no longer be the active one."""
diff --git a/assets/src/ba_data/python/bastd/ui/playlist/browser.py b/assets/src/ba_data/python/bastd/ui/playlist/browser.py
index 1ee3f119..d5644525 100644
--- a/assets/src/ba_data/python/bastd/ui/playlist/browser.py
+++ b/assets/src/ba_data/python/bastd/ui/playlist/browser.py
@@ -63,7 +63,7 @@ class PlaylistBrowserWindow(ba.Window):
)
uiscale = ba.app.ui.uiscale
- self._width = 900 if uiscale is ba.UIScale.SMALL else 800
+ self._width = 900.0 if uiscale is ba.UIScale.SMALL else 800.0
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
self._height = (
480
@@ -365,7 +365,7 @@ class PlaylistBrowserWindow(ba.Window):
self._sub_width = self._scroll_width
self._sub_height = (
- 40 + rows * (button_height + 2 * button_buffer_v) + 90
+ 40.0 + rows * (button_height + 2 * button_buffer_v) + 90
)
assert self._sub_width is not None
assert self._sub_height is not None
diff --git a/assets/src/ba_data/python/bastd/ui/v2upgrade.py b/assets/src/ba_data/python/bastd/ui/v2upgrade.py
new file mode 100644
index 00000000..ed61e170
--- /dev/null
+++ b/assets/src/ba_data/python/bastd/ui/v2upgrade.py
@@ -0,0 +1,111 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""UI for upgrading V1 accounts to V2."""
+
+from __future__ import annotations
+
+import ba
+import ba.internal
+
+
+class V2UpgradeWindow(ba.Window):
+ """A window presenting a URL to the user visually."""
+
+ def __init__(self, login_name: str, code: str):
+ from bastd.ui.account.settings import show_what_is_v2_page
+
+ app = ba.app
+ uiscale = app.ui.uiscale
+
+ self._code = code
+
+ self._width = 700
+ self._height = 270
+ super().__init__(
+ root_widget=ba.containerwidget(
+ size=(self._width, self._height + 40),
+ transition='in_right',
+ scale=(
+ 1.25
+ if uiscale is ba.UIScale.SMALL
+ else 1.25
+ if uiscale is ba.UIScale.MEDIUM
+ else 1.25
+ ),
+ )
+ )
+ ba.playsound(ba.getsound('error'))
+
+ ba.textwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5, self._height - 46),
+ size=(0, 0),
+ color=ba.app.ui.title_color,
+ h_align='center',
+ v_align='center',
+ text=ba.Lstr(
+ resource='deviceAccountUpgradeText',
+ subs=[('${NAME}', login_name)],
+ ),
+ maxwidth=self._width * 0.95,
+ )
+ ba.textwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5, 125),
+ size=(0, 0),
+ scale=0.8,
+ color=(0.7, 0.8, 0.7),
+ h_align='center',
+ v_align='center',
+ text=(
+ ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT)
+ + login_name
+ + ' ----> '
+ + ba.charstr(ba.SpecialChar.V2_LOGO)
+ + login_name
+ ),
+ maxwidth=self._width * 0.95,
+ )
+ button_width = 200
+
+ cancel_button = ba.buttonwidget(
+ parent=self._root_widget,
+ position=(20, 25),
+ size=(button_width, 65),
+ autoselect=True,
+ label=ba.Lstr(resource='notNowText'),
+ on_activate_call=self._done,
+ )
+
+ _what_is_this_button = ba.buttonwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5 - button_width * 0.5, 25),
+ size=(button_width, 65),
+ autoselect=True,
+ label=ba.Lstr(resource='whatIsThisText'),
+ color=(0.55, 0.5, 0.6),
+ textcolor=(0.75, 0.7, 0.8),
+ on_activate_call=show_what_is_v2_page,
+ )
+
+ bamasteraddr = ba.internal.get_master_server_address(version=2)
+ upgrade_button = ba.buttonwidget(
+ parent=self._root_widget,
+ position=(self._width - button_width - 20, 25),
+ size=(button_width, 65),
+ autoselect=True,
+ label=ba.Lstr(resource='upgradeText'),
+ on_activate_call=ba.Call(
+ ba.open_url,
+ f'{bamasteraddr}/v2uda/{self._code}',
+ ),
+ )
+
+ ba.containerwidget(
+ edit=self._root_widget,
+ selected_child=upgrade_button,
+ cancel_button=cancel_button,
+ )
+
+ def _done(self) -> None:
+ ba.containerwidget(edit=self._root_widget, transition='out_left')
diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
index b0de6664..de0fbb15 100644
--- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
@@ -42,11 +42,13 @@
airborn
alext
alibaba
+ alibname
allerrors
allobjc
allobjs
allocs
allwarnings
+ alogins
alot
alphaimg
alphapixels
@@ -216,6 +218,7 @@
cend
centiseconds
certifi
+ cfgdict
cfgdir
cfgpath
changeme
@@ -1005,6 +1008,7 @@
pflag
pflags
pgmout
+ phandle
phasescriptexecution
piplist
pipvers
@@ -1332,6 +1336,7 @@
subtypestr
successmsg
sval
+ swidth
swiftc
symbolification
syscalls
@@ -1343,6 +1348,9 @@
targs
tasklabel
tbegin
+ tbfile
+ tbfiles
+ tbpath
tcls
tdels
tdiff
@@ -1506,6 +1514,7 @@
weeeird
welp
whaaaaaaa
+ whatisv
wheee
wheeee
wiimote
diff --git a/ballisticacore-cmake/CMakeLists.txt b/ballisticacore-cmake/CMakeLists.txt
index 77deaf38..4b79cb15 100644
--- a/ballisticacore-cmake/CMakeLists.txt
+++ b/ballisticacore-cmake/CMakeLists.txt
@@ -464,7 +464,6 @@ add_executable(ballisticacore
${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_host.h
${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_host_udp.cc
${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_host_udp.h
- ${BA_SRC_ROOT}/ballistica/logic/friend_score_set.h
${BA_SRC_ROOT}/ballistica/logic/host_activity.cc
${BA_SRC_ROOT}/ballistica/logic/host_activity.h
${BA_SRC_ROOT}/ballistica/logic/logic.cc
diff --git a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj b/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj
index 8f4b7c5a..18e36464 100644
--- a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj
+++ b/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj
@@ -455,7 +455,6 @@
-
diff --git a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters b/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters
index 4430ba75..75bed1a1 100644
--- a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters
+++ b/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters
@@ -799,9 +799,6 @@
ballistica\logic\connection
-
- ballistica\logic
-
ballistica\logic
diff --git a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj b/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj
index f1654fcf..78353bdb 100644
--- a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj
+++ b/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj
@@ -450,7 +450,6 @@
-
diff --git a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters b/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters
index 4430ba75..75bed1a1 100644
--- a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters
+++ b/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters
@@ -799,9 +799,6 @@
ballistica\logic\connection
-
- ballistica\logic
-
ballistica\logic
diff --git a/src/ballistica/app/app_flavor.cc b/src/ballistica/app/app_flavor.cc
index d238d854..d6a2b1ed 100644
--- a/src/ballistica/app/app_flavor.cc
+++ b/src/ballistica/app/app_flavor.cc
@@ -156,7 +156,6 @@ void AppFlavor::UpdatePauseResume() {
void AppFlavor::OnPause() {
assert(InMainThread());
- // Avoid reading gyro values for a short time to avoid hitches when restored.
g_graphics->SetGyroEnabled(false);
// 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() {
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@"
+ std::to_string(Platform::GetCurrentMilliseconds()));
assert(!sys_paused_app_);
sys_paused_app_ = true;
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() {
assert(InMainThread());
+ millisecs_t start_time{Platform::GetCurrentMilliseconds()};
Platform::DebugLog("ResumeApp@"
+ std::to_string(Platform::GetCurrentMilliseconds()));
assert(sys_paused_app_);
sys_paused_app_ = false;
UpdatePauseResume();
+ Log(LogLevel::kDebug,
+ "ResumeApp() completed in "
+ + std::to_string(Platform::GetCurrentMilliseconds() - start_time)
+ + "ms.");
}
void AppFlavor::DidFinishRenderingFrame(FrameDef* frame) {}
@@ -265,6 +303,7 @@ void AppFlavor::PrimeEventPump() {
#pragma mark Push-Calls
+// FIXME - move this call to Platform.
void AppFlavor::PushShowOnlineScoreUICall(const std::string& show,
const std::string& game,
const std::string& game_version) {
@@ -316,14 +355,6 @@ void AppFlavor::PushOpenURLCall(const std::string& 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,
const std::string& game_version,
int64_t score) {
diff --git a/src/ballistica/app/app_flavor.h b/src/ballistica/app/app_flavor.h
index fb1f013a..c0693b48 100644
--- a/src/ballistica/app/app_flavor.h
+++ b/src/ballistica/app/app_flavor.h
@@ -43,16 +43,17 @@ class AppFlavor {
/// Should process any pending OS events, etc.
virtual auto RunEvents() -> void;
- // These should be called by the window, view-controller, sdl,
- // or whatever is driving the app. They must be called from the main thread.
-
- /// Should be called on mobile when the app is backgrounded.
- /// Pauses threads, closes network sockets, etc.
+ /// Put the app into a paused state. Should be called from the main
+ /// thread. Pauses work, closes network sockets, etc.
+ /// Corresponds to being backgrounded on mobile, etc.
+ /// It is assumed that, as soon as this call returns, all work is
+ /// finished and all threads can be suspended by the OS without any
+ /// negative side effects.
auto PauseApp() -> void;
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.
auto ResumeApp() -> void;
@@ -101,9 +102,6 @@ class AppFlavor {
auto PushShowOnlineScoreUICall(const std::string& show,
const std::string& game,
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,
const std::string& game_version, int64_t score)
-> void;
diff --git a/src/ballistica/app/stress_test.cc b/src/ballistica/app/stress_test.cc
index dd63eb23..cbc6524c 100644
--- a/src/ballistica/app/stress_test.cc
+++ b/src/ballistica/app/stress_test.cc
@@ -50,11 +50,6 @@ void StressTest::Update() {
"time,averageFps,nodes,models,collide_models,textures,sounds,"
"pssMem,sharedDirtyMem,privateDirtyMem\n");
fflush(stress_test_stats_file_);
- if (g_buildconfig.ostype_android()) {
- // On android, let the OS know we've added or removed a file
- // (limit to android or we'll get an unimplemented warning).
- g_platform->AndroidRefreshFile(f_name);
- }
}
}
if (stress_test_stats_file_ != nullptr) {
diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc
index fc3b176a..7e901c60 100644
--- a/src/ballistica/ballistica.cc
+++ b/src/ballistica/ballistica.cc
@@ -32,7 +32,7 @@
namespace ballistica {
// These are set automatically via script; don't modify them here.
-const int kAppBuildNumber = 20921;
+const int kAppBuildNumber = 20956;
const char* kAppVersion = "1.7.14";
// Our standalone globals.
diff --git a/src/ballistica/core/object.cc b/src/ballistica/core/object.cc
index ee3280ee..6efc0098 100644
--- a/src/ballistica/core/object.cc
+++ b/src/ballistica/core/object.cc
@@ -11,7 +11,7 @@
namespace ballistica {
-void Object::PrintObjects() {
+void Object::LsObjects() {
#if BA_DEBUG_BUILD
std::string s;
{
@@ -47,7 +47,7 @@ void Object::PrintObjects() {
for (auto&& i : obj_map) {
sorted.emplace_back(i.second, i.first);
}
- std::sort(sorted.begin(), sorted.end());
+ std::sort(sorted.rbegin(), sorted.rend());
for (auto&& i : sorted) {
s += "\n " + std::to_string(i.first) + ": " + i.second;
}
@@ -56,7 +56,7 @@ void Object::PrintObjects() {
}
Log(LogLevel::kInfo, s);
#else
- Log(LogLevel::kInfo, "PrintObjects() only functions in debug builds.");
+ Log(LogLevel::kInfo, "LsObjects() only functions in debug builds.");
#endif // BA_DEBUG_BUILD
}
diff --git a/src/ballistica/core/object.h b/src/ballistica/core/object.h
index 6847e43b..5cb03e1b 100644
--- a/src/ballistica/core/object.h
+++ b/src/ballistica/core/object.h
@@ -21,8 +21,8 @@ class Object {
Object();
virtual ~Object();
- /// Prints a tally of object types and counts (debug build only).
- static void PrintObjects();
+ /// Logs a tally of ba::Object types and counts (debug build only).
+ static void LsObjects();
// Object classes can provide descriptive names for themselves;
// these are used for debugging and other purposes.
diff --git a/src/ballistica/core/thread.cc b/src/ballistica/core/thread.cc
index de2b5985..c1fae4a8 100644
--- a/src/ballistica/core/thread.cc
+++ b/src/ballistica/core/thread.cc
@@ -91,7 +91,7 @@ auto Thread::RunAssetsThreadP(void* data) -> void* {
return nullptr;
}
-void Thread::SetPaused(bool paused) {
+void Thread::PushSetPaused(bool paused) {
// Can be toggled from the main thread only.
assert(std::this_thread::get_id() == g_app->main_thread_id);
PushThreadMessage(ThreadMessage(paused ? ThreadMessage::Type::kPause
@@ -101,13 +101,24 @@ void Thread::SetPaused(bool paused) {
void Thread::WaitForNextEvent(bool single_cycle) {
// If we're running a single cycle we never stop to wait.
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;
}
- // We also never wait if we have pending runnables.
- // (we run all existing runnables in each loop cycle, but one of those
- // may have enqueued more).
- if (has_pending_runnables()) {
+ // We also never wait if we have pending runnables; we wan't to run
+ // things as soon as we can. We chew through all runnables at the end
+ // of the loop so it might seem like there should never be any here,
+ // 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;
}
@@ -422,15 +433,17 @@ Thread::~Thread() = default;
#pragma clang diagnostic push
#pragma ide diagnostic ignored "ConstantConditionsOC"
-void Thread::LogThreadMessageTally() {
+void Thread::LogThreadMessageTally(
+ std::vector>* log_entries) {
// Prevent recursion.
if (!writing_tally_) {
writing_tally_ = true;
std::unordered_map tally;
- Log(LogLevel::kError, "Thread message tally ("
+ log_entries->emplace_back(std::make_pair(
+ LogLevel::kError, "Thread message tally ("
+ std::to_string(thread_messages_.size())
- + " in list):");
+ + " in list):"));
for (auto&& m : thread_messages_) {
std::string s;
switch (m.type) {
@@ -464,8 +477,9 @@ void Thread::LogThreadMessageTally() {
}
int entry = 1;
for (auto&& i : tally) {
- Log(LogLevel::kError, " #" + std::to_string(entry++) + " ("
- + std::to_string(i.second) + "x): " + i.first);
+ log_entries->emplace_back(std::make_pair(
+ LogLevel::kError, " #" + std::to_string(entry++) + " ("
+ + std::to_string(i.second) + "x): " + i.first));
}
writing_tally_ = false;
}
@@ -473,15 +487,18 @@ void Thread::LogThreadMessageTally() {
#pragma clang diagnostic pop
void Thread::PushThreadMessage(const ThreadMessage& t) {
+ // We don't want to make log calls while holding this mutex;
+ // log calls acquire the GIL and if the GIL-holder (generally
+ // the logic thread) is trying to send a thread message to the
+ // thread doing the logging we would get deadlock.
+ // So tally up any logs and send them after.
+ std::vector> log_entries;
{
std::unique_lock lock(thread_message_mutex_);
// Plop the data on to the list; we're assuming the mutex is locked.
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.
if (explicit_bool(false)) {
static int one_off = 0;
@@ -498,8 +515,9 @@ void Thread::PushThreadMessage(const ThreadMessage& t) {
// Show count periodically.
if ((std::this_thread::get_id() == g_app->main_thread_id) && foo > 100) {
foo = 0;
- Log(LogLevel::kInfo,
- "MSG COUNT " + std::to_string(thread_messages_.size()));
+ log_entries.emplace_back(std::make_pair(
+ LogLevel::kInfo,
+ "MSG COUNT " + std::to_string(thread_messages_.size())));
}
}
@@ -507,9 +525,11 @@ void Thread::PushThreadMessage(const ThreadMessage& t) {
static bool sent_error = false;
if (!sent_error) {
sent_error = true;
- Log(LogLevel::kError,
- "ThreadMessage list > 1000 in thread: " + GetCurrentThreadName());
- LogThreadMessageTally();
+ log_entries.emplace_back(std::make_pair(
+ LogLevel::kError,
+ "ThreadMessage list > 1000 in thread: " + GetCurrentThreadName()));
+
+ LogThreadMessageTally(&log_entries);
}
}
@@ -523,15 +543,36 @@ void Thread::PushThreadMessage(const ThreadMessage& t) {
// available.
}
thread_message_cv_.notify_all();
+
+ // Now log anything we accumulated safely outside of the locked section.
+ for (auto&& log_entry : log_entries) {
+ Log(log_entry.first, log_entry.second);
+ }
}
-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;
for (auto&& i : g_app->pausable_threads) {
- i->SetPaused(paused);
+ i->PushSetPaused(paused);
}
}
+auto Thread::GetStillPausingThreads() -> std::vector {
+ std::vector 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::NewTimer(millisecs_t length, bool repeat,
@@ -553,6 +594,7 @@ auto Thread::GetCurrentThreadName() -> std::string {
}
}
+ // Ask pthread for the thread name if we don't have one.
// FIXME - move this to platform.
#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS || BA_OSTYPE_LINUX
std::string name = "unknown (sys-name=";
diff --git a/src/ballistica/core/thread.h b/src/ballistica/core/thread.h
index d5a63d01..a0c5c594 100644
--- a/src/ballistica/core/thread.h
+++ b/src/ballistica/core/thread.h
@@ -45,7 +45,8 @@ class Thread {
void SetAcquiresPythonGIL();
- void SetPaused(bool paused);
+ void PushSetPaused(bool paused);
+
auto thread_id() const -> std::thread::id { return thread_id_; }
// Needed in rare cases where we jump physical threads.
@@ -97,6 +98,10 @@ class Thread {
/// the app through a flood of packets.
auto CheckPushSafety() -> bool;
+ static auto GetStillPausingThreads() -> std::vector;
+
+ auto paused() { return paused_; }
+
private:
struct ThreadMessage {
enum class Type { kShutdown = 999, kRunnable, kPause, kResume };
@@ -113,7 +118,8 @@ class Thread {
auto SetInternalThreadName(const std::string& name) -> void;
auto WaitForNextEvent(bool single_cycle) -> void;
auto LoopUpkeep(bool once) -> void;
- auto LogThreadMessageTally() -> void;
+ auto LogThreadMessageTally(
+ std::vector>* log_entries) -> void;
auto PushLocalRunnable(Runnable* runnable, bool* completion_flag) -> void;
auto PushCrossThreadRunnable(Runnable* runnable, bool* completion_flag)
-> void;
diff --git a/src/ballistica/core/types.h b/src/ballistica/core/types.h
index 663d0ad9..956d63f8 100644
--- a/src/ballistica/core/types.h
+++ b/src/ballistica/core/types.h
@@ -80,7 +80,6 @@ class Data;
class DataData;
class Dynamics;
class FrameDef;
-struct FriendScoreSet;
class GLContext;
class GlobalsNode;
class Graphics;
diff --git a/src/ballistica/generic/json.cc b/src/ballistica/generic/json.cc
index d90fecfb..cd7ba946 100644
--- a/src/ballistica/generic/json.cc
+++ b/src/ballistica/generic/json.cc
@@ -155,17 +155,19 @@ static auto print_number(cJSON* item) -> char* {
double d = item->valuedouble;
if (fabs(((double)item->valueint) - d) <= DBL_EPSILON && d <= INT_MAX
&& d >= INT_MIN) {
- str = (char*)cJSON_malloc(21); /* 2^64+1 can be represented in 21 chars. */
- if (str) sprintf(str, "%d", item->valueint);
+ size_t sz{21};
+ str = (char*)cJSON_malloc(sz); /* 2^64+1 can be represented in 21 chars. */
+ if (str) snprintf(str, sz, "%d", item->valueint);
} else {
- str = (char*)cJSON_malloc(64); /* This is a nice tradeoff. */
+ size_t sz{64};
+ str = (char*)cJSON_malloc(sz); /* This is a nice tradeoff. */
if (str) {
if (fabs(floor(d) - d) <= DBL_EPSILON && fabs(d) < 1.0e60)
- sprintf(str, "%.0f", d);
+ snprintf(str, sz, "%.0f", d);
else if (fabs(d) < 1.0e-6 || fabs(d) > 1.0e9)
- sprintf(str, "%e", d);
+ snprintf(str, sz, "%e", d);
else
- sprintf(str, "%f", d);
+ snprintf(str, sz, "%f", d);
}
}
return str;
@@ -379,7 +381,7 @@ static auto print_string_ptr(const char* str) -> char* {
*ptr2++ = 't';
break;
default:
- sprintf(ptr2, "u%04x", token);
+ snprintf(ptr2, 20, "u%04x", token);
ptr2 += 5;
break; /* escape and print */
}
diff --git a/src/ballistica/graphics/gl/gl_sys.h b/src/ballistica/graphics/gl/gl_sys.h
index 8ffa9ad8..154e0ce7 100644
--- a/src/ballistica/graphics/gl/gl_sys.h
+++ b/src/ballistica/graphics/gl/gl_sys.h
@@ -61,9 +61,9 @@
#if BA_OSTYPE_MACOS
#if BA_XCODE_BUILD
#include
+#include
#include
#endif // BA_XCODE_BUILD
-#include
#endif // BA_OSTYPE_MACOS
#endif // BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID
diff --git a/src/ballistica/input/input.cc b/src/ballistica/input/input.cc
index bebef567..a6a1b289 100644
--- a/src/ballistica/input/input.cc
+++ b/src/ballistica/input/input.cc
@@ -1818,4 +1818,48 @@ auto Input::GetKeyName(int keycode) -> std::string {
#pragma clang diagnostic pop
#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
diff --git a/src/ballistica/input/input.h b/src/ballistica/input/input.h
index 1ece106a..3c3659c7 100644
--- a/src/ballistica/input/input.h
+++ b/src/ballistica/input/input.h
@@ -126,6 +126,7 @@ class Input {
auto PushTouchEvent(const TouchEvent& touch_event) -> void;
auto PushDestroyKeyboardInputDevices() -> void;
auto PushCreateKeyboardInputDevices() -> void;
+ auto LsInputDevices() -> void;
/// Roughly how long in milliseconds have all input devices been idle.
auto input_idle_time() const { return input_idle_time_; }
@@ -170,7 +171,6 @@ class Input {
bool have_non_touch_inputs_{};
float cursor_pos_x_{};
float cursor_pos_y_{};
- // millisecs_t last_input_time_{};
millisecs_t last_click_time_{};
millisecs_t double_click_time_{200};
millisecs_t last_mouse_move_time_{};
diff --git a/src/ballistica/logic/friend_score_set.h b/src/ballistica/logic/friend_score_set.h
deleted file mode 100644
index e90dd619..00000000
--- a/src/ballistica/logic/friend_score_set.h
+++ /dev/null
@@ -1,30 +0,0 @@
-// Released under the MIT License. See LICENSE for details.
-
-#ifndef BALLISTICA_LOGIC_FRIEND_SCORE_SET_H_
-#define BALLISTICA_LOGIC_FRIEND_SCORE_SET_H_
-
-#include
-#include
-#include
-
-namespace ballistica {
-
-// Used by game-center/etc when reporting friend scores to the game.
-struct FriendScoreSet {
- FriendScoreSet(bool success, void* user_data)
- : success(success), user_data(user_data) {}
- struct Entry {
- Entry(int score, std::string name, bool is_me)
- : score(score), name(std::move(name)), is_me(is_me) {}
- int score;
- std::string name;
- bool is_me;
- };
- std::list entries;
- bool success;
- void* user_data;
-};
-
-} // namespace ballistica
-
-#endif // BALLISTICA_LOGIC_FRIEND_SCORE_SET_H_
diff --git a/src/ballistica/logic/logic.cc b/src/ballistica/logic/logic.cc
index 7e43bc65..8ac784af 100644
--- a/src/ballistica/logic/logic.cc
+++ b/src/ballistica/logic/logic.cc
@@ -19,7 +19,6 @@
#include "ballistica/logic/connection/connection_set.h"
#include "ballistica/logic/connection/connection_to_client_udp.h"
#include "ballistica/logic/connection/connection_to_host_udp.h"
-#include "ballistica/logic/friend_score_set.h"
#include "ballistica/logic/host_activity.h"
#include "ballistica/logic/player.h"
#include "ballistica/logic/session/client_session.h"
@@ -1138,11 +1137,6 @@ void Logic::PushPlaySoundCall(SystemSoundID sound) {
[sound] { g_audio->PlaySound(g_assets->GetSound(sound)); });
}
-void Logic::PushFriendScoreSetCall(const FriendScoreSet& score_set) {
- thread()->PushCall(
- [score_set] { g_python->HandleFriendScoresCB(score_set); });
-}
-
void Logic::PushConfirmQuitCall() {
thread()->PushCall([this] {
assert(InLogicThread());
diff --git a/src/ballistica/logic/logic.h b/src/ballistica/logic/logic.h
index 5a5c0bfa..1baa65d0 100644
--- a/src/ballistica/logic/logic.h
+++ b/src/ballistica/logic/logic.h
@@ -88,7 +88,6 @@ class Logic {
auto PushConfirmQuitCall() -> void;
auto PushStringEditSetCall(const std::string& value) -> void;
auto PushStringEditCancelCall() -> void;
- auto PushFriendScoreSetCall(const FriendScoreSet& score_set) -> void;
auto PushShowURLCall(const std::string& url) -> void;
auto PushOnAppResumeCall() -> void;
auto PushFrameDefRequest() -> void;
diff --git a/src/ballistica/platform/apple/platform_apple.h b/src/ballistica/platform/apple/platform_apple.h
index 6aa86d56..5c819577 100644
--- a/src/ballistica/platform/apple/platform_apple.h
+++ b/src/ballistica/platform/apple/platform_apple.h
@@ -37,8 +37,6 @@ class PlatformApple : public Platform {
const std::vector& widths, float scale)
-> void* 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,
int64_t score) -> void override;
auto ReportAchievement(const std::string& achievement) -> void override;
diff --git a/src/ballistica/platform/platform.cc b/src/ballistica/platform/platform.cc
index a43aecb0..0bdb8a89 100644
--- a/src/ballistica/platform/platform.cc
+++ b/src/ballistica/platform/platform.cc
@@ -28,12 +28,12 @@
#include "ballistica/graphics/mesh/sprite_mesh.h"
#include "ballistica/graphics/vr_graphics.h"
#include "ballistica/input/input.h"
-#include "ballistica/logic/friend_score_set.h"
#include "ballistica/logic/logic.h"
#include "ballistica/networking/networking_sys.h"
#include "ballistica/platform/sdl/sdl_app.h"
#include "ballistica/platform/stdio_console.h"
#include "ballistica/python/python.h"
+#include "ballistica/python/python_sys.h"
#if BA_HEADLESS_BUILD
#include "ballistica/app/app_flavor_headless.h"
@@ -183,6 +183,23 @@ auto Platform::GetLegacyDeviceUUID() -> const std::string& {
return legacy_device_uuid_;
}
+auto Platform::LoginAdapterGetSignInToken(const std::string& login_type,
+ int attempt_id) -> void {
+ // Default implementation simply calls completion callback immediately.
+ g_logic->thread()->PushCall([login_type, attempt_id] {
+ PythonRef args(Py_BuildValue("(sss)", login_type.c_str(),
+ std::to_string(attempt_id).c_str(), ""),
+ PythonRef::kSteal);
+ g_python->obj(Python::ObjID::kLoginAdapterGetSignInTokenResponseCall)
+ .Call(args);
+ });
+}
+
+auto Platform::LoginAdapterBackEndActiveChange(const std::string& login_type,
+ bool active) -> void {
+ // Default is no-op.
+}
+
auto Platform::GetDeviceV1AccountUUIDPrefix() -> std::string {
Log(LogLevel::kError, "GetDeviceV1AccountUUIDPrefix() unimplemented");
return "u";
@@ -849,13 +866,6 @@ auto Platform::ConvertIncomingLeaderboardScore(
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,
int64_t score) {
Log(LogLevel::kError, "FIXME: SubmitScore() unimplemented");
@@ -1030,10 +1040,6 @@ void Platform::SubmitAnalyticsCounts() {}
void Platform::SetPlatformMiscReadVals(const std::string& vals) {}
-void Platform::AndroidRefreshFile(const std::string& file) {
- Log(LogLevel::kError, "AndroidRefreshFile() unimplemented");
-}
-
void Platform::ShowAd(const std::string& purpose) {
Log(LogLevel::kError, "ShowAd() unimplemented");
}
diff --git a/src/ballistica/platform/platform.h b/src/ballistica/platform/platform.h
index 463c1ada..27b03ded 100644
--- a/src/ballistica/platform/platform.h
+++ b/src/ballistica/platform/platform.h
@@ -267,7 +267,6 @@ class Platform {
virtual auto AndroidShowAppInvite(const std::string& title,
const std::string& message,
const std::string& code) -> void;
- virtual auto AndroidRefreshFile(const std::string& file) -> void;
virtual auto AndroidShowWifiSettings() -> void;
virtual auto AndroidGetExternalFilesDir() -> std::string;
@@ -337,6 +336,14 @@ class Platform {
/// Return the prefix to use for device-account ids on this platform.
virtual auto GetDeviceV1AccountUUIDPrefix() -> std::string;
+ /// Called when a Python LoginAdapter is requesting an explicit sign-in.
+ virtual auto LoginAdapterGetSignInToken(const std::string& login_type,
+ 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 ----------------------------------------------------
// FIXME: currently these are wired up on Android; need to generalize
@@ -365,9 +372,6 @@ class Platform {
virtual auto ConvertIncomingLeaderboardScore(
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,
int64_t score) -> void;
virtual auto ReportAchievement(const std::string& achievement) -> void;
diff --git a/src/ballistica/python/methods/python_methods_app.cc b/src/ballistica/python/methods/python_methods_app.cc
index d2460ea9..2de0a6dc 100644
--- a/src/ballistica/python/methods/python_methods_app.cc
+++ b/src/ballistica/python/methods/python_methods_app.cc
@@ -6,6 +6,7 @@
#include "ballistica/app/app_flavor.h"
#include "ballistica/assets/component/texture.h"
#include "ballistica/core/logging.h"
+#include "ballistica/core/thread.h"
#include "ballistica/graphics/graphics.h"
#include "ballistica/logic/connection/connection_set.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 suppress_warning{};
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",
- "other_thread_use_fg_context", nullptr};
- if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|ppp",
+ "other_thread_use_fg_context",
+ "raw",
+ nullptr};
+ if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|pppp",
const_cast(kwlist), &call_obj,
&from_other_thread, &suppress_warning,
- &other_thread_use_fg_context)) {
+ &other_thread_use_fg_context, &raw)) {
return nullptr;
}
- // The from-other-thread case is basically a different call.
- if (from_other_thread) {
+ // 'raw' mode does no thread checking and no context saves/restores.
+ 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
// save/restore context.
if (!suppress_warning && InLogicThread()) {
@@ -1132,15 +1144,12 @@ auto PythonMethodsApp::GetMethods() -> std::vector {
{"pushcall", (PyCFunction)PyPushCall, METH_VARARGS | METH_KEYWORDS,
"pushcall(call: Callable, from_other_thread: bool = False,\n"
" suppress_other_thread_warning: bool = False,\n"
- " other_thread_use_fg_context: bool = False) -> None\n"
- "\n"
- "Pushes a call onto the event loop to be run during the next cycle.\n"
+ " other_thread_use_fg_context: bool = False,\n"
+ " raw: bool = False) -> None\n"
"\n"
+ "Push a call to the logic event-loop.\n"
"Category: **General Utility Functions**\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 "
"automatically\n"
"save and restore the ba.Context to behave seamlessly.\n"
@@ -1149,8 +1158,9 @@ auto PythonMethodsApp::GetMethods() -> std::vector {
"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"
"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,
METH_VARARGS | METH_KEYWORDS,
"getactivity(doraise: bool = True) -> \n"
diff --git a/src/ballistica/python/methods/python_methods_system.cc b/src/ballistica/python/methods/python_methods_system.cc
index 1cb80409..1d9f879b 100644
--- a/src/ballistica/python/methods/python_methods_system.cc
+++ b/src/ballistica/python/methods/python_methods_system.cc
@@ -622,6 +622,38 @@ auto PySetAnalyticsScreen(PyObject* self, PyObject* args, PyObject* keywds)
BA_PYTHON_CATCH;
}
+auto PyLoginAdapterGetSignInToken(PyObject* self, PyObject* args,
+ PyObject* keywds) -> PyObject* {
+ BA_PYTHON_TRY;
+ const char* login_type;
+ int attempt_id;
+ static const char* kwlist[] = {"login_type", "attempt_id", nullptr};
+ if (!PyArg_ParseTupleAndKeywords(args, keywds, "si",
+ const_cast(kwlist), &login_type,
+ &attempt_id)) {
+ return nullptr;
+ }
+ g_platform->LoginAdapterGetSignInToken(login_type, attempt_id);
+ Py_RETURN_NONE;
+ 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(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* {
BA_PYTHON_TRY;
PyObject* list_obj;
@@ -659,26 +691,6 @@ auto PySetInternalLanguageKeys(PyObject* self, PyObject* args) -> PyObject* {
BA_PYTHON_CATCH;
}
-auto PyIsOuyaBuild(PyObject* self, PyObject* args) -> PyObject* {
- BA_PYTHON_TRY;
- Py_RETURN_FALSE;
- BA_PYTHON_CATCH;
-}
-
-auto PyAndroidMediaScanFile(PyObject* self, PyObject* args, PyObject* keywds)
- -> PyObject* {
- BA_PYTHON_TRY;
- const char* file_name;
- static const char* kwlist[] = {"file_name", nullptr};
- if (!PyArg_ParseTupleAndKeywords(args, keywds, "s",
- const_cast(kwlist), &file_name)) {
- return nullptr;
- }
- g_platform->AndroidRefreshFile(file_name);
- Py_RETURN_NONE;
- BA_PYTHON_CATCH;
-}
-
#pragma clang diagnostic push
#pragma ide diagnostic ignored "ConstantFunctionResult"
@@ -719,10 +731,18 @@ auto PyAndroidShowWifiSettings(PyObject* self, PyObject* args, PyObject* keywds)
BA_PYTHON_CATCH;
}
-auto PyPrintObjects(PyObject* self, PyObject* args, PyObject* keywds)
+auto PyLsObjects(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
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;
BA_PYTHON_CATCH;
}
@@ -760,6 +780,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector {
"\n"
"If this returns False, UIs should not show 'copy to clipboard'\n"
"buttons, etc."},
+
{"clipboard_has_text", (PyCFunction)PyClipboardHasText, METH_NOARGS,
"clipboard_has_text() -> bool\n"
"\n"
@@ -769,6 +790,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector {
"\n"
"This will return False if no system clipboard is available; no need\n"
" to call ba.clipboard_is_supported() separately."},
+
{"clipboard_set_text", (PyCFunction)PyClipboardSetText,
METH_VARARGS | METH_KEYWORDS,
"clipboard_set_text(value: str) -> None\n"
@@ -779,6 +801,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector {
"\n"
"Ensure that ba.clipboard_is_supported() returns True before adding\n"
" buttons/etc. that make use of this functionality."},
+
{"clipboard_get_text", (PyCFunction)PyClipboardGetText, METH_NOARGS,
"clipboard_get_text() -> str\n"
"\n"
@@ -788,9 +811,20 @@ auto PythonMethodsSystem::GetMethods() -> std::vector {
"\n"
"Ensure that ba.clipboard_has_text() returns True before calling\n"
" 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,
- "printobjects() -> None\n"
+ "ls_input_devices() -> None\n"
"\n"
"Print debugging info about game objects.\n"
"\n"
@@ -822,15 +856,6 @@ auto PythonMethodsSystem::GetMethods() -> std::vector {
"\n"
"(internal)"},
- {"android_media_scan_file", (PyCFunction)PyAndroidMediaScanFile,
- METH_VARARGS | METH_KEYWORDS,
- "android_media_scan_file(file_name: str) -> None\n"
- "\n"
- "(internal)\n"
- "\n"
- "Refreshes Android MTP Index for a file; use this to get file\n"
- "modifications to be reflected in Android File Transfer."},
-
{"android_get_external_files_dir",
(PyCFunction)PyAndroidGetExternalFilesDir, METH_VARARGS | METH_KEYWORDS,
"android_get_external_files_dir() -> str\n"
@@ -847,13 +872,6 @@ auto PythonMethodsSystem::GetMethods() -> std::vector {
"\n"
"(internal)"},
- {"is_ouya_build", PyIsOuyaBuild, METH_VARARGS,
- "is_ouya_build() -> bool\n"
- "\n"
- "(internal)\n"
- "\n"
- "Returns whether we're running the ouya-specific version"},
-
{"set_internal_language_keys", PySetInternalLanguageKeys, METH_VARARGS,
"set_internal_language_keys(listobj: list[tuple[str, str]],\n"
" random_names_list: list[tuple[str, str]]) -> None\n"
@@ -872,6 +890,21 @@ auto PythonMethodsSystem::GetMethods() -> std::vector {
"'screen' should be a string description of an app location\n"
"('Main Menu', etc.)"},
+ {"login_adapter_get_sign_in_token",
+ (PyCFunction)PyLoginAdapterGetSignInToken, METH_VARARGS | METH_KEYWORDS,
+ "login_adapter_get_sign_in_token(login_type: str, attempt_id: int)"
+ " -> None\n"
+ "\n"
+ "(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,
METH_VARARGS | METH_KEYWORDS,
"submit_analytics_counts() -> None\n"
diff --git a/src/ballistica/python/python.cc b/src/ballistica/python/python.cc
index 4ca762fe..4d2c57cc 100644
--- a/src/ballistica/python/python.cc
+++ b/src/ballistica/python/python.cc
@@ -14,7 +14,6 @@
#include "ballistica/input/device/joystick.h"
#include "ballistica/input/device/keyboard_input.h"
#include "ballistica/internal/app_internal.h"
-#include "ballistica/logic/friend_score_set.h"
#include "ballistica/logic/host_activity.h"
#include "ballistica/logic/player.h"
#include "ballistica/logic/v1_account.h"
@@ -2144,40 +2143,6 @@ void Python::CaptureKeyboardInput(PyObject* obj) {
}
void Python::ReleaseKeyboardInput() { keyboard_call_.Release(); }
-void Python::HandleFriendScoresCB(const FriendScoreSet& score_set) {
- // This is the initial strong-ref to this pointer
- // so it will be cleaned up properly.
- Object::Ref cb(
- static_cast(score_set.user_data));
-
- // We pass None on error.
- if (!score_set.success) {
- PythonRef args(Py_BuildValue("(O)", Py_None), PythonRef::kSteal);
- cb->Run(args);
- } else {
- // Otherwise convert it to a python list and pass that.
- PyObject* py_list = PyList_New(0);
- std::string icon_str;
-#if BA_USE_GOOGLE_PLAY_GAME_SERVICES
- icon_str = g_logic->CharStr(SpecialChar::kGooglePlayGamesLogo);
-#elif BA_USE_GAME_CIRCLE
- icon_str = g_logic->CharStr(SpecialChar::kGameCircleLogo);
-#elif BA_USE_GAME_CENTER
- icon_str = g_logic->CharStr(SpecialChar::kGameCenterLogo);
-#endif
- for (auto&& i : score_set.entries) {
- PyObject* obj =
- Py_BuildValue("[isi]", i.score, (icon_str + i.name).c_str(),
- static_cast(i.is_me));
- PyList_Append(py_list, obj);
- Py_DECREF(obj);
- }
- PythonRef args(Py_BuildValue("(O)", py_list), PythonRef::kSteal);
- Py_DECREF(py_list);
- cb->Run(args);
- }
-}
-
auto Python::HandleKeyPressEvent(const SDL_Keysym& keysym) -> bool {
assert(InLogicThread());
if (!keyboard_call_.exists()) {
diff --git a/src/ballistica/python/python.h b/src/ballistica/python/python.h
index a48e847b..35df1fe0 100644
--- a/src/ballistica/python/python.h
+++ b/src/ballistica/python/python.h
@@ -114,7 +114,6 @@ class Python {
auto ReleaseGamePadInput() -> void;
auto CaptureKeyboardInput(PyObject* obj) -> void;
auto ReleaseKeyboardInput() -> void;
- auto HandleFriendScoresCB(const FriendScoreSet& ss) -> void;
auto IssueCallInLogicThreadWarning(PyObject* call) -> void;
/// Borrowed from python's source code: used in overriding of objects' dir()
@@ -318,6 +317,7 @@ class Python {
kSetLastAdNetworkCall,
kNoGameCircleMessageCall,
kGooglePlayPurchasesNotAvailableMessageCall,
+ kGooglePlayServicesNotAvailableMessageCall,
kEmptyCall,
kLevelIconPressCall,
kTrophyIconPressCall,
@@ -365,6 +365,9 @@ class Python {
kLoggingWarningCall,
kLoggingErrorCall,
kLoggingCriticalCall,
+ kImplicitSignInCall,
+ kImplicitSignOutCall,
+ kLoginAdapterGetSignInTokenResponseCall,
kLast // Sentinel; must be at end.
};
diff --git a/src/meta/bameta/python_embedded/binding.py b/src/meta/bameta/python_embedded/binding.py
index 9408b71d..df969526 100644
--- a/src/meta/bameta/python_embedded/binding.py
+++ b/src/meta/bameta/python_embedded/binding.py
@@ -66,6 +66,7 @@ def get_binding_values() -> tuple[Any, ...]:
_hooks.set_last_ad_network, # kSetLastAdNetworkCall
_hooks.no_game_circle_message, # kNoGameCircleMessageCall
_hooks.google_play_purchases_not_available_message, # kGooglePlayPurchasesNotAvailableMessageCall
+ _hooks.google_play_services_not_available_message, # kGooglePlayServicesNotAvailableMessageCall
_hooks.empty_call, # kEmptyCall
_hooks.level_icon_press, # kLevelIconPressCall
_hooks.trophy_icon_press, # kTrophyIconPressCall
@@ -142,4 +143,7 @@ def get_binding_values() -> tuple[Any, ...]:
logging.warning, # kLoggingWarningCall
logging.error, # kLoggingErrorCall
logging.critical, # kLoggingCriticalCall
+ _hooks.implicit_sign_in, # kImplicitSignInCall
+ _hooks.implicit_sign_out, # kImplicitSignOutCall
+ _hooks.login_adapter_get_sign_in_token_response, # kLoginAdapterGetSignInTokenResponseCall
) # yapf: disable
diff --git a/tools/bacommon/cloud.py b/tools/bacommon/cloud.py
index 431dac5a..bb36f6c1 100644
--- a/tools/bacommon/cloud.py
+++ b/tools/bacommon/cloud.py
@@ -10,6 +10,7 @@ from enum import Enum
from efro.message import Message, Response
from efro.dataclassio import ioprepped, IOAttrs
from bacommon.transfer import DirectoryManifest
+from bacommon.login import LoginType
if TYPE_CHECKING:
pass
@@ -154,3 +155,42 @@ class WorkspaceFetchResponse(Response):
] = field(default_factory=dict)
done: Annotated[bool, IOAttrs('d')] = False
+
+
+@ioprepped
+@dataclass
+class SignInMessage(Message):
+ """Can I sign in please?"""
+
+ login_type: Annotated[LoginType, IOAttrs('l')]
+ sign_in_token: Annotated[str, IOAttrs('t')]
+
+ @classmethod
+ def get_response_types(cls) -> list[type[Response] | None]:
+ return [SignInResponse]
+
+
+@ioprepped
+@dataclass
+class SignInResponse(Response):
+ """Here's that sign-in result you asked for, boss."""
+
+ credentials: Annotated[str | None, IOAttrs('c')]
+
+
+@ioprepped
+@dataclass
+class ManageAccountMessage(Message):
+ """Message asking for a manage-account url."""
+
+ @classmethod
+ def get_response_types(cls) -> list[type[Response] | None]:
+ return [ManageAccountResponse]
+
+
+@ioprepped
+@dataclass
+class ManageAccountResponse(Response):
+ """Here's that sign-in result you asked for, boss."""
+
+ url: Annotated[str | None, IOAttrs('u')]
diff --git a/tools/bacommon/login.py b/tools/bacommon/login.py
new file mode 100644
index 00000000..c8ec0057
--- /dev/null
+++ b/tools/bacommon/login.py
@@ -0,0 +1,31 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Functionality related to cloud based assets."""
+
+from __future__ import annotations
+
+from enum import Enum
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ pass
+
+
+class LoginType(Enum):
+ """Types of logins available."""
+
+ # Email/password
+ EMAIL = 'email'
+
+ # Google Play Game Services
+ 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'
diff --git a/tools/batools/pcommand.py b/tools/batools/pcommand.py
index 2caa95e7..23191a88 100644
--- a/tools/batools/pcommand.py
+++ b/tools/batools/pcommand.py
@@ -334,11 +334,11 @@ def gen_fulltest_buildfile_linux() -> None:
batools.build.gen_fulltest_buildfile_linux()
-def python_version_build_base() -> None:
+def python_version_android_base() -> None:
"""Print built Python base version."""
- from efrotools.pybuild import PY_VER
+ from efrotools.pybuild import PY_VER_ANDROID
- print(PY_VER, end='')
+ print(PY_VER_ANDROID, end='')
def python_version_android() -> None:
@@ -444,7 +444,25 @@ def python_gather() -> None:
from efrotools import pybuild
os.chdir(PROJROOT)
- pybuild.gather()
+ pybuild.gather(do_android=True, do_apple=True)
+
+
+def python_gather_android() -> None:
+ """python_gather but only android bits."""
+ import os
+ from efrotools import pybuild
+
+ os.chdir(PROJROOT)
+ pybuild.gather(do_android=True, do_apple=False)
+
+
+def python_gather_apple() -> None:
+ """python_gather but only apple bits."""
+ import os
+ from efrotools import pybuild
+
+ os.chdir(PROJROOT)
+ pybuild.gather(do_android=False, do_apple=True)
def python_winprune() -> None:
diff --git a/tools/batools/project.py b/tools/batools/project.py
index 48c4d561..f4d82d7f 100755
--- a/tools/batools/project.py
+++ b/tools/batools/project.py
@@ -729,8 +729,10 @@ class Updater:
def _check_misc(self) -> None:
# 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(
'src/ballistica/internal/master_server_config.h',
encoding='utf-8',
diff --git a/tools/efro/error.py b/tools/efro/error.py
index 7f13148c..456fc6f9 100644
--- a/tools/efro/error.py
+++ b/tools/efro/error.py
@@ -80,6 +80,15 @@ class IntegrityError(ValueError):
"""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:
"""Is the provided exception from urllib a communication-related error?
@@ -188,6 +197,7 @@ def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
firewall/connectivity issues, etc. These issues can often be safely
ignored or presented to the user as general 'connection-lost' events.
"""
+ # pylint: disable=too-many-return-statements
import ssl
if isinstance(
@@ -227,4 +237,8 @@ def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
if 'SSL: WRONG_VERSION_NUMBER' in excstr:
return True
+ # And seeing this very rarely; assuming its just data corruption?
+ if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr:
+ return True
+
return False
diff --git a/tools/efro/message/_message.py b/tools/efro/message/_message.py
index 9343a396..0697ad53 100644
--- a/tools/efro/message/_message.py
+++ b/tools/efro/message/_message.py
@@ -43,6 +43,20 @@ class SysResponse:
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:
diff --git a/tools/efro/message/_sender.py b/tools/efro/message/_sender.py
index bd9e94db..ee439e01 100644
--- a/tools/efro/message/_sender.py
+++ b/tools/efro/message/_sender.py
@@ -6,7 +6,6 @@ Supports static typing for message types and possible return types.
from __future__ import annotations
-import logging
from typing import TYPE_CHECKING
from efro.error import CleanError, RemoteError, CommunicationError
@@ -158,17 +157,18 @@ class MessageSender:
bound_obj, msg_encoded
)
except Exception as exc:
- # Any error in the raw send call gets recorded as either
- # a local or communication error.
- return ErrorSysResponse(
- error_message=f'Error in MessageSender @send_method'
- f' ({type(exc)}): {exc}',
+ response = ErrorSysResponse(
+ error_message='Error in MessageSender @send_method.',
error_type=(
ErrorSysResponse.ErrorType.COMMUNICATION
if isinstance(exc, CommunicationError)
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)
async def fetch_raw_response_async(
@@ -193,17 +193,18 @@ class MessageSender:
bound_obj, msg_encoded
)
except Exception as exc:
- # Any error in the raw send call gets recorded as either
- # a local or communication error.
- return ErrorSysResponse(
- error_message=f'Error in MessageSender @send_async_method'
- f' ({type(exc)}): {exc}',
+ response = ErrorSysResponse(
+ error_message='Error in MessageSender @send_async_method.',
error_type=(
ErrorSysResponse.ErrorType.COMMUNICATION
if isinstance(exc, CommunicationError)
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)
def unpack_raw_response(
@@ -250,18 +251,14 @@ class MessageSender:
self._decode_filter_call(
bound_obj, message, response_dict, response
)
- except Exception:
- # 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')
+ except Exception as exc:
response = ErrorSysResponse(
- error_message='Error decoding raw response;'
- ' see log for details.',
+ error_message='Error decoding raw response.',
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
def _unpack_raw_response(
@@ -282,16 +279,24 @@ class MessageSender:
# Some error occurred. Raise a local Exception for it.
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 (
raw_response.error_type
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,
# don't say it was a remote error.
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 (
@@ -299,14 +304,18 @@ class MessageSender:
and raw_response.error_type
is ErrorSysResponse.ErrorType.REMOTE_CLEAN
):
- raise CleanError(raw_response.error_message)
+ raise CleanError(
+ raw_response.error_message
+ ) from local_exception
if (
self.protocol.forward_communication_errors
and raw_response.error_type
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.
raise RemoteError(
@@ -316,7 +325,7 @@ class MessageSender:
if self._peer_desc_call is None
else self._peer_desc_call(bound_obj)
),
- )
+ ) from local_exception
assert isinstance(raw_response, Response)
return raw_response
diff --git a/tools/efro/rpc.py b/tools/efro/rpc.py
index c6412bfd..4fe7e639 100644
--- a/tools/efro/rpc.py
+++ b/tools/efro/rpc.py
@@ -628,7 +628,8 @@ class RPCEndpoint:
# Now just sit and handle stuff as it comes in.
while True:
- assert not self._closing
+ if self._closing:
+ return
# Read message type.
mtype = _PacketType(await self._read_int_8())
diff --git a/tools/efro/util.py b/tools/efro/util.py
index 699fb068..553120f0 100644
--- a/tools/efro/util.py
+++ b/tools/efro/util.py
@@ -273,13 +273,14 @@ class DispatchMethodWrapper(Generic[ArgT, RetT]):
"""Type-aware standin for the dispatch func returned by dispatchmethod."""
def __call__(self, arg: ArgT) -> RetT:
- pass
+ raise RuntimeError('Should not get here')
@staticmethod
def register(
func: Callable[[Any, Any], RetT]
) -> Callable[[Any, Any], RetT]:
"""Register a new dispatch handler for this dispatch-method."""
+ raise RuntimeError('Should not get here')
registry: dict[Any, Callable]
diff --git a/tools/efrotools/pcommand.py b/tools/efrotools/pcommand.py
index 8a5a66a1..7e8c2ede 100644
--- a/tools/efrotools/pcommand.py
+++ b/tools/efrotools/pcommand.py
@@ -162,6 +162,13 @@ def _spelling(words: list[str]) -> None:
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:
"""Add all misspellings from a pycharm run."""
import subprocess
diff --git a/tools/efrotools/pybuild.py b/tools/efrotools/pybuild.py
index 4dc0f5be..3ac5a137 100644
--- a/tools/efrotools/pybuild.py
+++ b/tools/efrotools/pybuild.py
@@ -14,9 +14,10 @@ if TYPE_CHECKING:
from typing import Any
# Python version we build here (not necessarily same as we use in repo).
-PY_VER = '3.10'
-PY_VER_EXACT_ANDROID = '3.10.7'
-PY_VER_EXACT_APPLE = '3.10.7'
+PY_VER_ANDROID = '3.11'
+PY_VER_EXACT_ANDROID = '3.11.0'
+PY_VER_APPLE = '3.10'
+PY_VER_EXACT_APPLE = '3.10.5'
ANDROID_PYTHON_REPO = 'https://github.com/GRRedWings/python3-android'
@@ -91,7 +92,7 @@ def build_apple(arch: str, debug: bool = False) -> None:
# broke in the underlying build even on old commits so keeping it
# locked for now...
# run('git checkout bf1ed73d0d5ff46862ba69dd5eb2ffaeff6f19b6')
- subprocess.run(['git', 'checkout', PY_VER], check=True)
+ subprocess.run(['git', 'checkout', PY_VER_APPLE], check=True)
txt = readfile('Makefile')
@@ -240,7 +241,7 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None:
# after it is extracted.
ftxt = readfile('build.sh')
- ftxt = replace_exact(ftxt, 'PYVER=3.10.5', f'PYVER={PY_VER_EXACT_ANDROID}')
+ ftxt = replace_exact(ftxt, 'PYVER=3.11.0', f'PYVER={PY_VER_EXACT_ANDROID}')
ftxt = replace_exact(
ftxt,
' popd\n',
@@ -252,7 +253,10 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None:
# Ok; let 'er rip!
exargs = ' --with-pydebug' if debug else ''
subprocess.run(
- f'ARCH={arch} ANDROID_API=21 ./build.sh{exargs}', shell=True, check=True
+ f'ARCH={arch} ANDROID_API=21 ./build.sh{exargs} --without-ensurepip'
+ f' --with-build-python=/home/ubuntu/.py311/bin/python3.11',
+ shell=True,
+ check=True,
)
print('python build complete! (android/' + arch + ')')
@@ -302,30 +306,34 @@ def android_patch_ssl() -> None:
)
writefile(fname, txt)
- # Getting a lot of crashes in _armv7_tick, which seems to be a
- # somewhat known issue with certain arm7 devices. Sounds like
- # there are no major downsides to disabling this feature, so doing that.
- # (Sounds like its possible to somehow disable it through an env var
- # but let's just be sure and #ifdef it out in the source.
- # see https://github.com/openssl/openssl/issues/17465
- fname = 'crypto/armcap.c'
- txt = readfile(fname)
- txt = replace_exact(
- txt,
- ' /* Things that getauxval didn\'t tell us */\n'
- ' if (sigsetjmp(ill_jmp, 1) == 0) {\n'
- ' _armv7_tick();\n'
- ' OPENSSL_armcap_P |= ARMV7_TICK;\n'
- ' }\n',
- '# if 0 // ericf disabled; causing crashes on some android devices.\n'
- ' /* Things that getauxval didn\'t tell us */\n'
- ' if (sigsetjmp(ill_jmp, 1) == 0) {\n'
- ' _armv7_tick();\n'
- ' OPENSSL_armcap_P |= ARMV7_TICK;\n'
- ' }\n'
- '# endif // 0\n',
- )
- writefile(fname, txt)
+ # Update: looks like this might have been disabled by default for
+ # newer SSL builds used by 3.11+
+ if bool(False):
+ # Getting a lot of crashes in _armv7_tick, which seems to be a
+ # somewhat known issue with certain arm7 devices. Sounds like
+ # there are no major downsides to disabling this feature, so doing that.
+ # (Sounds like its possible to somehow disable it through an env var
+ # but let's just be sure and #ifdef it out in the source.
+ # see https://github.com/openssl/openssl/issues/17465
+ fname = 'crypto/armcap.c'
+ txt = readfile(fname)
+ txt = replace_exact(
+ txt,
+ ' /* Things that getauxval didn\'t tell us */\n'
+ ' if (sigsetjmp(ill_jmp, 1) == 0) {\n'
+ ' _armv7_tick();\n'
+ ' OPENSSL_armcap_P |= ARMV7_TICK;\n'
+ ' }\n',
+ '# if 0 // ericf disabled; causing crashes'
+ ' on some android devices.\n'
+ ' /* Things that getauxval didn\'t tell us */\n'
+ ' if (sigsetjmp(ill_jmp, 1) == 0) {\n'
+ ' _armv7_tick();\n'
+ ' OPENSSL_armcap_P |= ARMV7_TICK;\n'
+ ' }\n'
+ '# endif // 0\n',
+ )
+ writefile(fname, txt)
def _patch_py_ssl() -> None:
@@ -373,6 +381,7 @@ def _patch_setup_file(platform: str, arch: str, slc: str) -> None:
if platform == 'android':
prefix = '$(srcdir)/Android/sysroot/usr'
uuid_ex = f' -L{prefix}/lib -luuid'
+ uuid_ex += f' -I{prefix}/include/uuid' # Testing
zlib_ex = f' -I{prefix}/include -L{prefix}/lib -lz'
bz2_ex = f' -I{prefix}/include -L{prefix}/lib -lbz2'
ssl_ex = f' -DUSE_SSL -I{prefix}/include -L{prefix}/lib -lssl -lcrypto'
@@ -552,7 +561,7 @@ def _patch_setup_file(platform: str, arch: str, slc: str) -> None:
ftxt += (
f'_sqlite3'
- f' _sqlite/cache.c'
+ f' _sqlite/blob.c'
f' _sqlite/connection.c'
f' _sqlite/cursor.c'
f' _sqlite/microprotocols.c'
@@ -642,7 +651,7 @@ def winprune() -> None:
print('Win-prune successful.')
-def gather() -> None:
+def gather(do_android: bool, do_apple: bool) -> None:
"""Gather per-platform python headers, libs, and modules together.
This assumes all embeddable py builds have been run successfully,
@@ -651,8 +660,6 @@ def gather() -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
- do_android = True
-
# First off, clear out any existing output.
existing_dirs = [
os.path.join('src/external', d)
@@ -675,7 +682,7 @@ def gather() -> None:
debug = buildtype == 'debug'
bsuffix = '_debug' if buildtype == 'debug' else ''
bsuffix2 = '-debug' if buildtype == 'debug' else ''
- libname = 'python' + PY_VER + ('d' if debug else '')
+ alibname = 'python' + PY_VER_ANDROID + ('d' if debug else '')
bases = {
'mac': f'build/python_apple_mac{bsuffix}/build/macOS',
@@ -756,9 +763,9 @@ def gather() -> None:
{
'name': 'android_arm',
'group': 'android',
- 'headers': bases['android_arm'] + f'/usr/include/{libname}',
+ 'headers': bases['android_arm'] + f'/usr/include/{alibname}',
'libs': [
- bases['android_arm'] + f'/usr/lib/lib{libname}.a',
+ bases['android_arm'] + f'/usr/lib/lib{alibname}.a',
bases2['android_arm'] + '/usr/lib/libssl.a',
bases2['android_arm'] + '/usr/lib/libcrypto.a',
bases2['android_arm'] + '/usr/lib/liblzma.a',
@@ -767,14 +774,16 @@ def gather() -> None:
bases2['android_arm'] + '/usr/lib/libuuid.a',
],
'libinst': 'android_armeabi-v7a',
- 'pylib': (bases['android_arm'] + '/usr/lib/python' + PY_VER),
+ 'pylib': (
+ bases['android_arm'] + '/usr/lib/python' + PY_VER_ANDROID
+ ),
},
{
'name': 'android_arm64',
'group': 'android',
- 'headers': bases['android_arm64'] + f'/usr/include/{libname}',
+ 'headers': bases['android_arm64'] + f'/usr/include/{alibname}',
'libs': [
- bases['android_arm64'] + f'/usr/lib/lib{libname}.a',
+ bases['android_arm64'] + f'/usr/lib/lib{alibname}.a',
bases2['android_arm64'] + '/usr/lib/libssl.a',
bases2['android_arm64'] + '/usr/lib/libcrypto.a',
bases2['android_arm64'] + '/usr/lib/liblzma.a',
@@ -787,9 +796,9 @@ def gather() -> None:
{
'name': 'android_x86',
'group': 'android',
- 'headers': bases['android_x86'] + f'/usr/include/{libname}',
+ 'headers': bases['android_x86'] + f'/usr/include/{alibname}',
'libs': [
- bases['android_x86'] + f'/usr/lib/lib{libname}.a',
+ bases['android_x86'] + f'/usr/lib/lib{alibname}.a',
bases2['android_x86'] + '/usr/lib/libssl.a',
bases2['android_x86'] + '/usr/lib/libcrypto.a',
bases2['android_x86'] + '/usr/lib/liblzma.a',
@@ -802,9 +811,9 @@ def gather() -> None:
{
'name': 'android_x86_64',
'group': 'android',
- 'headers': bases['android_x86_64'] + f'/usr/include/{libname}',
+ 'headers': bases['android_x86_64'] + f'/usr/include/{alibname}',
'libs': [
- bases['android_x86_64'] + f'/usr/lib/lib{libname}.a',
+ bases['android_x86_64'] + f'/usr/lib/lib{alibname}.a',
bases2['android_x86_64'] + '/usr/lib/libssl.a',
bases2['android_x86_64'] + '/usr/lib/libcrypto.a',
bases2['android_x86_64'] + '/usr/lib/liblzma.a',
@@ -820,6 +829,8 @@ def gather() -> None:
grp = build['group']
if not do_android and grp == 'android':
continue
+ if not do_apple and grp == 'apple':
+ continue
builddir = f'src/external/python-{grp}{bsuffix2}'
header_dst = os.path.join(builddir, 'include')
lib_dst = os.path.join(builddir, 'lib')
diff --git a/tools/pcommand b/tools/pcommand
index 5a7b0725..e372f9af 100755
--- a/tools/pcommand
+++ b/tools/pcommand
@@ -67,7 +67,7 @@ from batools.pcommand import (
python_version_android,
python_version_apple,
python_build_apple,
- python_version_build_base,
+ python_version_android_base,
python_build_apple_debug,
python_build_android,
python_build_android_debug,
@@ -75,6 +75,8 @@ from batools.pcommand import (
python_android_patch_ssl,
python_apple_patch,
python_gather,
+ python_gather_apple,
+ python_gather_android,
python_winprune,
capitalize,
upper,