diff --git a/.efrocachemap b/.efrocachemap
index f8e226f7..3a7ce8e9 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -420,28 +420,28 @@
"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/89/ec/d472036fbb09f310891761beb39a",
+ "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/chinese.json": "https://files.ballistica.net/cache/ba1/1f/7f/af259ba9b41556e5e667ad4c646d",
"assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/3c/22/78a56fc40426ab19ad4e76924b78",
"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/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/english.json": "https://files.ballistica.net/cache/ba1/a0/1d/5fbc922d01521142c2a347b1b024",
"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/gibberish.json": "https://files.ballistica.net/cache/ba1/5f/51/c15d74d2fe4e88ee1e3db0986500",
"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/hindi.json": "https://files.ballistica.net/cache/ba1/09/55/b50104638f60636af2263877bb7f",
"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/indonesian.json": "https://files.ballistica.net/cache/ba1/3a/6b/34714586cb4e9f1b12f8ae54cac8",
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/12/62/862228b229057877e89fb195d41d",
"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/persian.json": "https://files.ballistica.net/cache/ba1/10/13/1228836444f7557211f0058ef9bd",
"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/romanian.json": "https://files.ballistica.net/cache/ba1/d7/06/9d70642d0a4d1e3b1c2149d7a17c",
@@ -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/20/fc/441aca31b3c8d5e9dcd0d71f6634",
+ "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/76/42/ebdcc0eda90030c41496c53885b4",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2a/c3/c1694a56da0e053d4dff2f203684",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0e/b8/9bbb04ec920dab1e32cbb752ab12",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/63/9a/85a2953ff4282728fac8bc9b92aa",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/da/6b/53941679695bef614fe94906f8c7",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/91/5b/5b4871e3e18e2cf046f8411ac41c",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b1/e8/b471efa43d7bb2be88ec4882d1ec",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/20/30/0567f04d0ed9662d84907d73ccff",
+ "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/61/3e/3bd9d4b36f8e7977902f5830eef7",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6f/55/f9e2afacd2d1e55622d42102f987",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/38/78/c94ca8ab70e99a64fe7d9b77b7fd",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/81/3e/ce018d8ffb1ce8b795461f177926",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/4f/5e/2b753f317848edb49a1228b48669",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e6/09/5fb14bb97f99c12509d986482a9e",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/31/9d/c21fd262d8e8bf7357982d3919ab",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/b4/03/1a4dd6757d0f02f536b01446aa70",
+ "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a7/e5/ed98da5d3d1d101498cdb78a3e66",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/8e/be/4c06b72d6cfe9d7c918e49e5dade",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/d6/e0/7e696374bf682d8de0fa761fa280",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ab/36/ab405201f69f1396c7393d272145",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b1/f7/4d23bfb3b40e3abf944d03b293fb",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ea/32/46199c5adad2591f3335cc3788a9",
+ "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b8/9d/10dd3fee2be55f0fb05160208cc3",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d3/0c/dfd26af11b14dea706d03d32bc39",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/02/80/b5fde15cc6e79f9b243b1e6c7035",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f6/07/5cee0f1e1e5362d84f1af9ed2503",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3c/66/432193098a405483f64a78ad3139",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d3/56/804cd3f543b05891f4f7d85ca457",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2a/95/62f8f30e3829d95358f603344aca",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/bd/c1/db89136718610fbab8ebeb29e62c",
+ "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/09/2a/71b05acd74b7f18af9b6eb0de3a3",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d2/37/08d73f46704304b24b38bf93b046",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d7/17/6220f7a35fbddb711c605903f491",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c0/73/ae5ad1b4170b2bba3f31a95a9b3d",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2f/d2/8aa3dc377561ad74448c7e3f5a44",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/8e/ff/159b3a7f288f6fc7bd5fb7091a52",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/2d/68/dc944091445ecff5ff5173acb17f",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/18/17/f5fe60e049a1bece575f6df1c4b2",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/09/43/a868bb9683aac2a9bec62c8f7c89",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/1d/47/d08b1b7383d837271fd1089f3b98",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/ca/f1/f82e547b5f72cfb2c082f88523f8",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/7b/5e/81fbec9eebd1137251df2dcc2953",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/ce/7f/f34ea80406a491e5c71080ea9639",
+ "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/87/8c/22c79120b24ffa5d790bb6f72120",
"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..89615536 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -65,6 +65,7 @@
aiomain
alarmsound
alibaba
+ alibname
allerrors
allobjc
allobjs
@@ -1103,6 +1104,7 @@
googlevr
goosey
gotresponse
+ gpgs
gpio
gprev
gpsui
@@ -1866,6 +1868,7 @@
pentry
perma
perrdetail
+ phandle
phasers
phasescriptexecution
phello
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff0b82a9..98c4246a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,10 @@
-### 1.7.14 (build 20921, api 7, 2022-11-03)
+### 1.7.14 (build 20928, api 7, 2022-11-15)
+- 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'.
### 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..d6172785 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",
@@ -358,12 +362,12 @@
"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",
diff --git a/assets/Makefile b/assets/Makefile
index 92750a22..63833361 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 \
@@ -425,6 +426,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 +539,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 \
@@ -654,6 +656,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 +689,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..6c7e41b9 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
+90228160524159856290785480177185335343
\ 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..671c8d6d 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,12 @@ def lock_all_input() -> None:
return None
+def login_adapter_get_sign_in_token(login_type: str, attempt_id: int) -> None:
+
+ """(internal)"""
+ return None
+
+
def mac_music_app_get_library_source() -> None:
"""(internal)"""
diff --git a/assets/src/ba_data/python/ba/_accountv2.py b/assets/src/ba_data/python/ba/_accountv2.py
index 77b4ecdd..b93caa0a 100644
--- a/assets/src/ba_data/python/ba/_accountv2.py
+++ b/assets/src/ba_data/python/ba/_accountv2.py
@@ -4,6 +4,7 @@
from __future__ import annotations
+import logging
from typing import TYPE_CHECKING
import _ba
@@ -11,6 +12,9 @@ import _ba
if TYPE_CHECKING:
from typing import Any
+ from bacommon.login import LoginType
+ from ba._login import LoginAdapter
+
class AccountV2Subsystem:
"""Subsystem for modern account handling in the app.
@@ -21,6 +25,7 @@ class AccountV2Subsystem:
"""
def __init__(self) -> None:
+ from bacommon.login import LoginType
# Whether or not everything related to an initial login
# (or lack thereof) has completed. This includes things like
@@ -30,9 +35,24 @@ class AccountV2Subsystem:
self._kicked_off_workspace_load = False
+ self.login_adapters: dict[LoginType, LoginAdapter] = {}
+
+ self._implicit_signed_in_adapter: LoginAdapter | None = None
+ self._auto_signed_in = False
+
+ if _ba.app.platform == 'android' and _ba.app.subplatform == 'google':
+ from ba._login import LoginAdapterGPGS
+
+ self.login_adapters[LoginType.GPGS] = LoginAdapterGPGS(
+ LoginType.GPGS
+ )
+
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 +69,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 +80,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 +121,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_login(
+ self, login_type: LoginType, login_id: str, display_name: str
+ ) -> None:
+ """An implicit login happened."""
+ 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_logout(self, login_type: LoginType) -> None:
+ """An implicit logout happened."""
+ 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 +156,63 @@ class AccountV2Subsystem:
self._initial_login_completed = True
_ba.app.on_initial_login_completed()
+ def on_implicit_login_state_changed(
+ self,
+ login_type: LoginType,
+ state: LoginAdapter.ImplicitLoginState | None,
+ ) -> None:
+ """Called when implicit login state changes.
+
+ Logins 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.
+ """
+ assert _ba.in_logic_thread()
+
+ # Store which (if any) adapter is currently implicitly signed in.
+ if state is None:
+ self._implicit_signed_in_adapter = None
+ else:
+ self._implicit_signed_in_adapter = self.login_adapters[login_type]
+
+ # 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
+
+ # We attempt auto-sign-in only once.
+ if self._auto_signed_in:
+ return
+
+ # If we're not currently signed in, we have connectivity, and
+ # we have an available implicit adapter, do an auto-sign-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
+ ):
+ self._auto_signed_in = True
+ self._implicit_signed_in_adapter.sign_in(self._on_sign_in_completed)
+
+ def _on_sign_in_completed(
+ self, result: LoginAdapter.SignInResult | Exception
+ ) -> None:
+ logging.debug('GOT SIGN-IN COMPLETED WITH %s', result)
+
def _on_set_active_workspace_completed(self) -> None:
if not self._initial_login_completed:
self._initial_login_completed = True
@@ -129,8 +232,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/_bootstrap.py b/assets/src/ba_data/python/ba/_bootstrap.py
index 5766328a..6320a45a 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 = 20928
running_build: int = env['build_number']
if running_build != expected_build:
print(
diff --git a/assets/src/ba_data/python/ba/_cloud.py b/assets/src/ba_data/python/ba/_cloud.py
index 029ed482..19202b55 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,15 @@ class CloudSubsystem:
"""
return False # Needs to be overridden
+ 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 +78,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 +142,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/_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..f9e10817 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,39 @@ 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_login(
+ 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_login(
+ login_type=LoginType(login_type_str),
+ login_id=login_id,
+ display_name=display_name,
+ )
+
+
+def implicit_logout(login_type_str: str) -> None:
+ """An implicit logout happened."""
+ from bacommon.login import LoginType
+
+ _ba.app.accounts_v2.on_implicit_logout(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 LoginAdapterGPGS
+
+ 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, LoginAdapterGPGS)
+ 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..4d6ab704 100644
--- a/assets/src/ba_data/python/ba/_internal.py
+++ b/assets/src/ba_data/python/ba/_internal.py
@@ -321,7 +321,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..b9041e4b
--- /dev/null
+++ b/assets/src/ba_data/python/ba/_login.py
@@ -0,0 +1,298 @@
+# 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
+
+import _ba
+
+if TYPE_CHECKING:
+ from typing import Callable
+
+ from bacommon.login import LoginType
+
+DEBUG_LOG = True
+
+
+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."""
+
+ @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()
+
+ 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,
+ logins,
+ )
+
+ 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[[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
+
+ del result_cb # Unused.
+
+ 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, 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:
+ from ba._language import Lstr
+
+ if DEBUG_LOG:
+ logging.debug(
+ 'LoginAdapter: %s adapter got sign-in response %s...',
+ self.login_type.name,
+ response,
+ )
+ if isinstance(response, Exception):
+ _ba.screenmessage(
+ Lstr(resource='errorText'), color=(1, 0, 0)
+ )
+ _ba.playsound(_ba.getsound('error'))
+ else:
+ _ba.app.accounts_v2.set_primary_credentials(
+ response.credentials
+ )
+
+ _ba.app.cloud.send_message_cb(
+ bacommon.cloud.SignInMessage(self.login_type, result),
+ on_response=_got_sign_in_response,
+ )
+
+ self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
+
+ 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 LoginAdapterGPGS(LoginAdapter):
+ """Google Play Game Services adapter."""
+
+ def __init__(self, login_type: LoginType):
+ super().__init__(login_type)
+
+ # 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_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)
diff --git a/assets/src/ba_data/python/ba/internal.py b/assets/src/ba_data/python/ba/internal.py
index cab4bab7..e96cb0aa 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,
@@ -178,6 +179,7 @@ from ba._internal import (
)
__all__ = [
+ 'LoginAdapter',
'show_online_score_ui',
'set_ui_input_device',
'is_party_icon_visible',
@@ -247,7 +249,6 @@ __all__ = [
'set_telnet_access_enabled',
'new_replay_session',
'get_replays_dir',
- # DIVIDER
'get_unowned_maps',
'get_unowned_game_types',
'get_map_class',
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/ui/account/settings.py b/assets/src/ba_data/python/bastd/ui/account/settings.py
index 3aae9917..f0b1e85f 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,16 @@
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
class AccountSettingsWindow(ba.Window):
@@ -27,7 +30,7 @@ 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._close_once_signed_in = close_once_signed_in
@@ -92,16 +95,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('Local')
top_extra = 15 if uiscale is ba.UIScale.SMALL else 0
super().__init__(
@@ -255,8 +257,9 @@ class AccountSettingsWindow(ba.Window):
account_state == 'signed_out'
and 'Local' in self._show_sign_in_buttons
)
- show_v2_sign_in_button = (
- account_state == 'signed_out' and 'V2' in self._show_sign_in_buttons
+ show_v2_proxy_sign_in_button = (
+ account_state == 'signed_out'
+ and 'V2Proxy' in self._show_sign_in_buttons
)
sign_in_button_space = 70.0
@@ -275,9 +278,7 @@ class AccountSettingsWindow(ba.Window):
show_achievements_button = self._signed_in and account_type in (
'Google Play',
- 'Alibaba',
'Local',
- 'OUYA',
'V2',
)
achievements_button_space = 60.0
@@ -299,9 +300,7 @@ class AccountSettingsWindow(ba.Window):
show_reset_progress_button = False
reset_progress_button_space = 70.0
- show_manage_v2_account_button = (
- self._signed_in and account_type == 'V2' and bool(False)
- ) # Disabled for now.
+ show_manage_v2_account_button = self._signed_in and account_type == 'V2'
manage_v2_account_button_space = 100.0
show_player_profiles_button = self._signed_in
@@ -327,11 +326,11 @@ class AccountSettingsWindow(ba.Window):
]
sign_out_button_space = 70.0
- show_cancel_v2_sign_in_button = (
+ show_cancel_sign_in_button = (
account_state == 'signing_in'
and ba.app.accounts_v2.have_primary_credentials()
)
- cancel_v2_sign_in_button_space = 70.0
+ cancel_sign_in_button_space = 70.0
if self._subcontainer is not None:
self._subcontainer.delete()
@@ -346,7 +345,7 @@ class AccountSettingsWindow(ba.Window):
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:
+ if show_v2_proxy_sign_in_button:
self._sub_height += sign_in_button_space
if show_game_service_button:
self._sub_height += game_service_button_space
@@ -376,8 +375,8 @@ class AccountSettingsWindow(ba.Window):
self._sub_height += unlink_accounts_button_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),
@@ -528,7 +527,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 +540,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,
@@ -663,9 +662,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
@@ -1005,9 +1002,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 +1012,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
@@ -1066,6 +1063,33 @@ class AccountSettingsWindow(ba.Window):
account_type,
)
+ 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'
+ )
+ 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,
@@ -1214,7 +1238,7 @@ class AccountSettingsWindow(ba.Window):
origin_widget=self._player_profiles_button
)
- def _cancel_v2_sign_in_press(self) -> None:
+ def _cancel_sign_in_press(self) -> None:
# Just say we don't wanna be signed in anymore.
ba.app.accounts_v2.set_primary_credentials(None)
@@ -1242,25 +1266,42 @@ 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
+ def _sign_in_press(self, login_type: str | LoginType) -> None:
+
+ # 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:
+ adapter.sign_in(
+ result_cb=ba.WeakCall(self._on_adapter_sign_in_result)
+ )
+ else:
+ ba.screenmessage(f'Unsupported login_type: {login_type.name}')
+
+ def _on_adapter_sign_in_result(
+ self, result: LoginAdapter.SignInResult | Exception
) -> None:
- del show_test_warning # unused
- ba.internal.sign_in_v1(account_type)
+ ba.screenmessage('GOT SIGN IN RESULT')
+ logging.debug('GOT SIGN IN RESULT %s', result)
- # 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()
- 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:
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 99%
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..97c95ca4 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):
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/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/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
index b0de6664..60593a9f 100644
--- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
@@ -42,6 +42,7 @@
airborn
alext
alibaba
+ alibname
allerrors
allobjc
allobjs
@@ -1005,6 +1006,7 @@
pflag
pflags
pgmout
+ phandle
phasescriptexecution
piplist
pipvers
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..df8407fd 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 = 20928;
const char* kAppVersion = "1.7.14";
// Our standalone globals.
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/platform/platform.cc b/src/ballistica/platform/platform.cc
index a43aecb0..04adb2dc 100644
--- a/src/ballistica/platform/platform.cc
+++ b/src/ballistica/platform/platform.cc
@@ -34,6 +34,7 @@
#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 +184,18 @@ 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::GetDeviceV1AccountUUIDPrefix() -> std::string {
Log(LogLevel::kError, "GetDeviceV1AccountUUIDPrefix() unimplemented");
return "u";
@@ -1030,10 +1043,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..8edcade4 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,10 @@ 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;
+
#pragma mark MUSIC PLAYBACK ----------------------------------------------------
// FIXME: currently these are wired up on Android; need to generalize
diff --git a/src/ballistica/python/methods/python_methods_system.cc b/src/ballistica/python/methods/python_methods_system.cc
index 1cb80409..b4a161b0 100644
--- a/src/ballistica/python/methods/python_methods_system.cc
+++ b/src/ballistica/python/methods/python_methods_system.cc
@@ -622,6 +622,22 @@ 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 PySetInternalLanguageKeys(PyObject* self, PyObject* args) -> PyObject* {
BA_PYTHON_TRY;
PyObject* list_obj;
@@ -659,26 +675,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"
@@ -822,15 +818,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 +834,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 +852,13 @@ 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)"},
+
{"submit_analytics_counts", (PyCFunction)PySubmitAnalyticsCounts,
METH_VARARGS | METH_KEYWORDS,
"submit_analytics_counts() -> None\n"
diff --git a/src/ballistica/python/python.h b/src/ballistica/python/python.h
index a48e847b..8b86b79c 100644
--- a/src/ballistica/python/python.h
+++ b/src/ballistica/python/python.h
@@ -318,6 +318,7 @@ class Python {
kSetLastAdNetworkCall,
kNoGameCircleMessageCall,
kGooglePlayPurchasesNotAvailableMessageCall,
+ kGooglePlayServicesNotAvailableMessageCall,
kEmptyCall,
kLevelIconPressCall,
kTrophyIconPressCall,
@@ -365,6 +366,9 @@ class Python {
kLoggingWarningCall,
kLoggingErrorCall,
kLoggingCriticalCall,
+ kImplicitLoginCall,
+ kImplicitLogoutCall,
+ 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..5289f236 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_login, # kImplicitLoginCall
+ _hooks.implicit_logout, # kImplicitLogoutCall
+ _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..957997da
--- /dev/null
+++ b/tools/bacommon/login.py
@@ -0,0 +1,21 @@
+# 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'
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/efro/error.py b/tools/efro/error.py
index 7f13148c..d3353708 100644
--- a/tools/efro/error.py
+++ b/tools/efro/error.py
@@ -188,6 +188,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 +228,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/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/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,