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,