diff --git a/.efrocachemap b/.efrocachemap index 2bf0a4b0..668692be 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -420,15 +420,15 @@ "assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/51/eb/0a567253cc08c94c5d315a64d9af", "assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/bc/8f/a9c51a09c418136e386b7fdf21c7", "assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/02/e5/84916e123f47ccf11ddda380d699", - "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/f9/ed/20354b5a613c3e168d9a3b92ed05", + "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/bd/6c/48e30d0a2215958f8ae1a02805ba", "assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/ca/75/3de74bd6e498113b99bbf9eda645", - "assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/55/8c/8d0a0585e434b94865ae4befc090", + "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/f6/21/951b7ff02b0ad14b1f0ac55763c4", "assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/61/e6/caf06ce99017fdf5d2da0c038445", - "assets/build/ba_data/data/languages/croatian.json": "https://files.ballistica.net/cache/ba1/66/bf/6e98398016da261296b8c306560e", + "assets/build/ba_data/data/languages/croatian.json": "https://files.ballistica.net/cache/ba1/28/46/3a36628a033da4d4b4ea65b78a28", "assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/87/84/9f3d39610453b3bf350698a23316", "assets/build/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/3f/46/e4da3c1d2b0ebf916df55c608b28", - "assets/build/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/97/90/39ba65c2ad714429aec82ea1ae3e", + "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/99/2a/bdcfa0932cf73e5cf63fd8113b1b", "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/de/5c/631a09d9192e40c99c07c6191b7c", @@ -437,25 +437,25 @@ "assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/03/6a/4db89c5bf1ced8eb5a5615a4ae64", "assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/82/eb/37ff44af76812097f9c98f05c730", "assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/08/3b/68cea4d16f7020d932829af85323", - "assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/2d/e5/3737c6c3979cf381321c5472bea5", + "assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/b0/48/e1ebe08bfdfc94fcb61a16b851e5", "assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/75/70/e33e6ee95830052e8f36cd2135f7", "assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/c7/16/e31ce16d1b4150c271401669f24f", "assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/07/37/ab65ccee3a555bd40e9661860c58", "assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/02/ab/e310f81582b6dc2ae93348d45166", "assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/d5/fe/422745cdbe51ccb4f2ced6f5554a", "assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/26/41/f1246ab56c6b7853f605c3a95889", - "assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/82/12/57bf144e12be229a9b70da9c45cb", + "assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/39/2b/27822a4e66093ca8bfb968099507", "assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/b2/46/89ae228342f20ca4937ee254197b", - "assets/build/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/a5/48/47d5eb30535158610cdace1edfcd", - "assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/9f/a6/a2c9d7f3f90a2320aa45ccfd65cd", + "assets/build/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/13/19/828be486951be254445263f36c6e", + "assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/f9/4b/d9f01814224066856695452ef57c", "assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/87/5d/d36a8a2e9cb0f02731a3fd7af000", - "assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/50/9f/be006ba19be6a69a57837eb6dca0", + "assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/91/0a/35c4baf539d5951fc03a794c0e0b", "assets/build/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/cb/11/e11957be752c3dc552898b60ab20", - "assets/build/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/74/3d/c3d40a1e5ee1edf82555da05eda9", + "assets/build/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/9d/51/f699dbd4beb88bc3cff699a287a7", "assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/0a/4f/90fcd63bd12a7648b2a1e9b01586", "assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/7f/bb/6239adeb551be5e09f3457d7b411", "assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/e2/e1/b815d9f2e9b2c3a4daddaf728225", - "assets/build/ba_data/data/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/0b/24/3cc2b5a6ebe4bca1e01b40f8ed09", + "assets/build/ba_data/data/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/f2/af/afd1503c7a10cacaa15bc02369b2", "assets/build/ba_data/data/maps/big_g.json": "https://files.ballistica.net/cache/ba1/47/0a/a617cc85d927b576c4e6fc1091ed", "assets/build/ba_data/data/maps/bridgit.json": "https://files.ballistica.net/cache/ba1/03/4b/57ee9b42854b26f23f81bd8c58ef", "assets/build/ba_data/data/maps/courtyard.json": "https://files.ballistica.net/cache/ba1/03/38/344dd05bfef7bbdf464035ec5aa2", @@ -3971,50 +3971,50 @@ "assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e", "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34", "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/62/59/1d88d726a5a1d8300ff9543910f0", - "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/48/24/e7b9b3c1a082760cfc6db0f10ca3", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1b/a6/92035aea2ab0dd03949256a5349e", - "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/27/07/1d7e2e0a8f9a2ebdf8cc33e56820", - "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/53/7a/c90a65dbb653cc1f311d815aac58", - "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a1/7d/5c334610fe346280e5c6dc372a57", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f9/8f/941afe4781fddf0bd979fd2fd758", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a8/04/1fe1a6bb38814c83097137fa73da", - "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/82/13/65edadabda615923ff4d7627853e", - "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b3/a2/8b5be76d52e33e3628da19fbc1a9", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1f/e6/8531ec0f9a53326891238cffd702", - "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8a/e5/c69c71030ef703a7a0637c7c73a5", - "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/da/68/acf38798fe82e9ead9844ae53757", - "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e4/c6/84d27c2ee2095d6955323bff63f6", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/12/aa/e7201b0067e820d095c8c58db855", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cf/1c/20e854d1a3cdb9640bfb7770f2f7", - "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/aa/11/4d3b2e9761e951a9205a913d6842", - "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/09/ff/8b7133310bbf869f19fc79368ee8", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/56/ec/0e501fe0ba3c0a497795b7bde490", - "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/47/da/a765bbbc77ae3442500e025c4342", - "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/68/d3/a4b1c6366ad425d34ef196f1c49e", - "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2f/42/aff90e076f427bd20e6925d54641", - "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c3/8b/afafe8e0c5896aee9c6177e3619a", - "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c5/7d/ae70ac9d5162235143cd560934f9", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/96/30/3b85c7c4d9af95b1f29899603f71", - "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/31/63/1f22739856a91e9e0c8786f0e5e6", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cb/67/06449e9e5fe93cfa070325619c6f", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/de/e4/2ef7d74099133e0fc7272f852fc3", - "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/00/e5/439c0214aec11ee65be93843e623", - "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/37/00/a75ab04600c3934e11644a63d864", - "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/74/8c/06a79d42b719af94e8c634b1480f", - "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/91/b8/ab7e097db2d0f1c1c58c9e37ca27", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/97/e5/92ca0edbed82a9b2081db73a6a67", - "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/17/ef/6c0510ef98fa3611b09f359f6018", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/be/bc/035279dcad567eb7ad4afefe8ed2", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8f/1a/1349780e52df2d59e9ea46266700", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/7c/29/bd985633e24a27703a80ce7e8cb6", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/a1/69/726b6f61333fffb7862a5c5ad2ad", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/40/fa/425f21d8391bb784eff0e2f84c7a", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/88/7e/b155b80d3f4cf3148d0b3c991a84", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/6d/f6/347590d969e154335b282e8a35aa", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/23/2c/bd8bac99b2dc6232d603d0fd9570", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/27/28/1b0efd8cb83ce3ca96f9612a3112", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/e8/67/cd7f25ef0dbac04538ca3d7bebce", - "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/b3/15/7c6d580b3482870b5b058858624c", + "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/96/42/d33846f67af0646f256aeaa7c30c", + "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/51/a5/ddad12c2da1f7b877250464784b2", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ef/d6/16e0a8dc48749f6e17bda544c445", + "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f3/53/09b469e8a14b6134cf6cd4c35363", + "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ab/6a/e451f155bc9ff8929dad3de8a146", + "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3c/f2/75d582f428ad9ff19833a2db6ac5", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c5/4c/be6956268b8869711533d2904a93", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1a/19/47dbd27b3f00daf33b6dd08cad36", + "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9f/65/d4ea67fc0670e35c594f8b831019", + "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c6/2d/57c333a4a46df3a4e98c549c0b89", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/68/57/3f9909fc12ae0dd0a44e60ce3c9d", + "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3e/d4/28c439dac6a74e0451dbcf6075f1", + "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9d/67/bd41584cfb10f2e205be19b849d9", + "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/4b/20/5f79657c50de3c0a5bdce6469d80", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/99/b3/6399f861f42b57b5d6ec627adc1f", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d1/dd/8d87e134c239e65b63a2e20b925a", + "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/c5/da/6df0f75b231b5b61b4d56f188c85", + "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/37/42/427de40168eb72f75feff3e919da", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/dc/4d/fe90d8c2f0e10688881dd8841a05", + "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/cb/94/746781e8cef4564254c3baeedd2d", + "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cb/a6/560fb2889c78f14c47f7185b284c", + "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/24/29/b23ac44bf2a76fcb5b4920d76bb2", + "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2e/1e/d4b6c04e0f7f277487bc21d88c44", + "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/18/87/1a907cb4fbde86c26b295da615ef", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a8/22/0215aaf316e7841a001840181f30", + "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5b/76/b68f63c60efb32d18bfda5ac2125", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/19/01/ca0d7b90ca48daa28a8ff7b5f9e7", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c0/a7/d748772aa00b604fc43bb9ef0c21", + "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8b/e9/437a4918cf4d7eb6498f24a9ff16", + "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ae/44/9bdd10f4bb0b2fdc67bc57624872", + "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/12/5d/3a5acb1cbf7b390fe561aacecaaa", + "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/16/ae/e32e6075da6ced98216aa83c87e3", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8c/38/d67105c92308fc168e2f82eb89db", + "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f5/9b/e02bc0842ec9422cdd1332928805", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/91/8a/03c904a39698ba5804365bec8ae2", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/a4/f76c3f28464408d10e0b4ba2b33c", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/46/ed/7a596e5d725752af99f08e39878b", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/c1/d7/b2368705dbbe68720c318778afc7", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/0f/4f/f12ef3415aa4339e650330cc310b", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/0f/67/5fea68f30efc01d82687be7fd452", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/da/c0/0707d6d205e9dfe4f613ba672918", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/a8/e1/726ea2e9710a12bb375bd5b8c43d", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/28/4f/38d2d59336874ec660cb909aad4b", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/57/9b/99e8f77f47dd8ce0272d59e990e0", + "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/6e/6f/004b696e9a13b083069374e4bb6a", "src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/d3/db/e73d4dcf1280d5f677c3cf8b47c3" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 9571f4eb..4c254e11 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -21,8 +21,11 @@ abouttab abtn accesstime + accountclientv accountname accountui + accountv + accountvalues accum accumkillcount accumkilledcount @@ -42,6 +45,7 @@ activityteam activitytypes activityutils + actool actorclass adbcfaca adbpath @@ -111,6 +115,7 @@ argh argparse argsjoined + argstr argtypes argval armeabi @@ -206,6 +211,7 @@ bastd batools batoolsinternal + baworker bbbb bblk bblu @@ -276,6 +282,7 @@ bsstd bstat bstournament + bstr bsuffix bsui btnh @@ -374,6 +381,7 @@ checkins checkpaths checkroundover + checksummed checksums checktype childanntype @@ -443,7 +451,13 @@ comms compat compileall + compileassetcatalog + compilec compilelocations + compilemetalfile + compilestoryboard + compileswift + compileswiftsources completeargs completecmd compounddict @@ -475,9 +489,12 @@ coopscorescreen coopsession coords + copypng copyreg copyrightline copyrule + copystringsfile + copyswiftlibs cornerpin coroutines countdownsounds @@ -492,6 +509,7 @@ cpus cpython crashlytics + createbuilddirectory createtime creationflags creditslist @@ -505,6 +523,7 @@ csspbt cstr csum + csval ctest ctex ctracker @@ -530,6 +549,7 @@ darwiin darwiinremote datab + databytes dataclassio dataclassutils datamodule @@ -1102,11 +1122,15 @@ hurtiness hval iasset + ibtool + ibtoold icls icns iconpicker iconscale + iconset iconsstorename + iconutil ident idevices ifeq @@ -1231,6 +1255,7 @@ keyfilt keyint keylayout + keylen keypresses keystr keytype @@ -1319,6 +1344,7 @@ linearstep linebits lineheight + linemax lineno linenum linenumber @@ -1326,6 +1352,7 @@ linetype linetypes linflav + linkstoryboards linkto lintable lintcode @@ -1377,6 +1404,7 @@ lshort lsprof lsqlite + lsregister lssl lstart lstr @@ -1442,6 +1470,7 @@ memfunctions menubar messagetype + metallink metamakefile metaprogramming metascan @@ -1472,6 +1501,7 @@ mkflags mlen mline + mmacosx mmapmodule mmult mname @@ -1581,6 +1611,7 @@ newdbpath newnode newpath + newtoken nextcall nextfilenum nextlevel @@ -1655,6 +1686,7 @@ okbtn oldbook oldlady + oldtoken onln onscreencountdown onscreenkeyboard @@ -1685,10 +1717,12 @@ ourself outdata outdelay + outdict outext outfilename outfilepath outhashpath + outmsg outname outpath outputter @@ -1742,6 +1776,7 @@ perma perrdetail phasers + phasescriptexecution phello photoshop phrea @@ -1856,6 +1891,9 @@ privatetab proactor proc + processinfoplistfile + processpch + processpchplusplus procs profileindex profilekey @@ -1871,6 +1909,7 @@ projroot projs promocode + proxykey prunedir prval pstats @@ -1999,6 +2038,8 @@ redist redistributables regionid + registerexecutionpolicyexception + registerwithlaunchservices regtp reimported relfut @@ -2094,6 +2135,7 @@ sbtn sbwht sbylw + sbytes scenefile scenefiles scenename @@ -2123,6 +2165,7 @@ sdkcheck sdkutils sdtk + sectionchanged selchild selectmodule selindex @@ -2278,6 +2321,7 @@ starscale startercache startscan + startsplits starttime statictest statictestfiles @@ -2349,6 +2393,7 @@ svne svvv swht + swiftc swip swipsound sylw @@ -2450,10 +2495,12 @@ textwidgets tfin thanvannispen + thats thelaststand themself thingie this'll + thislinelen thismodule threadpool threadtype @@ -2699,6 +2746,7 @@ wpath wprjp wref + writeauxiliaryfile writeclasses writefuncs wslpath @@ -2711,7 +2759,10 @@ xbox xcarchive xcassets + xcframework xcodebuild + xcodebuildverbose + xcoderun xcpretty xcprojpath xcrun @@ -2733,6 +2784,7 @@ xmore xoffs xoffset + xors xpos xres xscl @@ -2758,6 +2810,7 @@ zaggy zimbot zipapp + zipdata zlib zlibmodule zoneid diff --git a/CHANGELOG.md b/CHANGELOG.md index 878435cb..bf4031e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +### 1.7.0 (20577, 2022-05-28) +- V2 accounts are now available (woohoo!). These are called 'BombSquad Accounts' in the account section. V2 accounts communicate with a completely new server and will be the foundation for lots of new functionality in the future. However they also function as a V1 account so existing functionality should still work. Note that the new 'workspaces' V2-account is not yet available in this build, but it will be coming very soon. Also note that account types such as GameCenter and Google-Play will be 'upgraded' to V2 accounts in the future so there is no need to try this out if you use one of those. But if you use device-accounts you might want to create yourself a V2 account. You can also reserve a nice account-tag by jumping on this now. +- Legacy account subsystem has been renamed from `ba.app.accounts` to `ba.app.accounts_v1` +- Added `ba.app.accounts_v2` subsystem for working with V2 accounts. +- `ba.SessionPlayer.get_account_id()` is now `ba.SessionPlayer.get_v1_account_id()` +- `ba.InputDevice.get_account_id()` is now `ba.InputDevice.get_v1_account_id()` +- `_ba.sign_in()` is now `_ba.sign_in_v1()` +- `_ba.sign_out()` is now `_ba.sign_out_v1()` +- `_ba.get_account_name()` is now `_ba.get_v1_account_name()` +- `_ba.get_account_type()` is now `_ba.get_v1_account_type()` +- `_ba.get_account_state()` is now `_ba.get_v1_account_state()` +- `_ba.get_account_state_num()` is now `_ba.get_v1_account_state_num()` +- `_ba.get_account_display_string()` is now `_ba.get_v1_account_display_string()` +- `_ba.get_account_misc_val()` is now `_ba.get_v1_account_misc_val()` +- `_ba.get_account_misc_read_val()` is now `_ba.get_v1_account_misc_read_val()` +- `_ba.get_account_misc_read_val_2()` is now `_ba.get_v1_account_misc_read_val_2()` +- `_ba.get_account_ticket_count()` is now `_ba.get_v1_account_ticket_count()` + + ### 1.6.12 (20567, 2022-05-04) - More internal work on V2 master-server communication @@ -106,7 +125,7 @@ - `ba.get_valid_languages()` is now an attr: `ba.app.lang.available_languages` - Achievement functionality has been consolidated into an AchievementSubsystem object at ba.app.ach - Plugin functionality has been consolidated into a PluginSubsystem obj at ba.app.plugins -- Ditto with AccountSubsystem and ba.app.accounts +- Ditto with AccountV1Subsystem and ba.app.accounts - Ditto with MetadataSubsystem and ba.app.meta - Ditto with AdsSubsystem and ba.app.ads - Revamped tab-button functionality into a cleaner type-safe class (bastd.ui.tabs.TabRow) diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json index 743adf58..0205a16a 100644 --- a/assets/.asset_manifest_public.json +++ b/assets/.asset_manifest_public.json @@ -1,7 +1,8 @@ [ "ba_data/python/ba/__init__.py", "ba_data/python/ba/__pycache__/__init__.cpython-39.opt-1.pyc", - "ba_data/python/ba/__pycache__/_account.cpython-39.opt-1.pyc", + "ba_data/python/ba/__pycache__/_accountv1.cpython-39.opt-1.pyc", + "ba_data/python/ba/__pycache__/_accountv2.cpython-39.opt-1.pyc", "ba_data/python/ba/__pycache__/_achievement.cpython-39.opt-1.pyc", "ba_data/python/ba/__pycache__/_activity.cpython-39.opt-1.pyc", "ba_data/python/ba/__pycache__/_activitytypes.cpython-39.opt-1.pyc", @@ -58,12 +59,14 @@ "ba_data/python/ba/__pycache__/_tips.cpython-39.opt-1.pyc", "ba_data/python/ba/__pycache__/_tournament.cpython-39.opt-1.pyc", "ba_data/python/ba/__pycache__/_ui.cpython-39.opt-1.pyc", + "ba_data/python/ba/__pycache__/cloud.cpython-39.opt-1.pyc", "ba_data/python/ba/__pycache__/deprecated.cpython-39.opt-1.pyc", "ba_data/python/ba/__pycache__/internal.cpython-39.opt-1.pyc", "ba_data/python/ba/__pycache__/macmusicapp.cpython-39.opt-1.pyc", "ba_data/python/ba/__pycache__/modutils.cpython-39.opt-1.pyc", "ba_data/python/ba/__pycache__/osmusic.cpython-39.opt-1.pyc", - "ba_data/python/ba/_account.py", + "ba_data/python/ba/_accountv1.py", + "ba_data/python/ba/_accountv2.py", "ba_data/python/ba/_achievement.py", "ba_data/python/ba/_activity.py", "ba_data/python/ba/_activitytypes.py", @@ -124,6 +127,7 @@ "ba_data/python/ba/_tips.py", "ba_data/python/ba/_tournament.py", "ba_data/python/ba/_ui.py", + "ba_data/python/ba/cloud.py", "ba_data/python/ba/deprecated.py", "ba_data/python/ba/internal.py", "ba_data/python/ba/macmusicapp.py", @@ -136,11 +140,13 @@ "ba_data/python/bacommon/__pycache__/assets.cpython-39.opt-1.pyc", "ba_data/python/bacommon/__pycache__/bacloud.cpython-39.opt-1.pyc", "ba_data/python/bacommon/__pycache__/build.cpython-39.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/cloud.cpython-39.opt-1.pyc", "ba_data/python/bacommon/__pycache__/net.cpython-39.opt-1.pyc", "ba_data/python/bacommon/__pycache__/servermanager.cpython-39.opt-1.pyc", "ba_data/python/bacommon/assets.py", "ba_data/python/bacommon/bacloud.py", "ba_data/python/bacommon/build.py", + "ba_data/python/bacommon/cloud.py", "ba_data/python/bacommon/net.py", "ba_data/python/bacommon/servermanager.py", "ba_data/python/bastd/__init__.py", diff --git a/assets/Makefile b/assets/Makefile index b2aab5a7..1969b9d4 100644 --- a/assets/Makefile +++ b/assets/Makefile @@ -133,7 +133,8 @@ endef SCRIPT_TARGETS_PY_PUBLIC = \ build/ba_data/python/ba/__init__.py \ - build/ba_data/python/ba/_account.py \ + build/ba_data/python/ba/_accountv1.py \ + build/ba_data/python/ba/_accountv2.py \ build/ba_data/python/ba/_achievement.py \ build/ba_data/python/ba/_activity.py \ build/ba_data/python/ba/_activitytypes.py \ @@ -192,6 +193,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \ build/ba_data/python/ba/_tips.py \ build/ba_data/python/ba/_tournament.py \ build/ba_data/python/ba/_ui.py \ + build/ba_data/python/ba/cloud.py \ build/ba_data/python/ba/deprecated.py \ build/ba_data/python/ba/internal.py \ build/ba_data/python/ba/macmusicapp.py \ @@ -378,7 +380,8 @@ SCRIPT_TARGETS_PY_PUBLIC = \ SCRIPT_TARGETS_PYC_PUBLIC = \ build/ba_data/python/ba/__pycache__/__init__.cpython-39.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_account.cpython-39.opt-1.pyc \ + build/ba_data/python/ba/__pycache__/_accountv1.cpython-39.opt-1.pyc \ + build/ba_data/python/ba/__pycache__/_accountv2.cpython-39.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_achievement.cpython-39.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_activity.cpython-39.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_activitytypes.cpython-39.opt-1.pyc \ @@ -437,6 +440,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ build/ba_data/python/ba/__pycache__/_tips.cpython-39.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_tournament.cpython-39.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_ui.cpython-39.opt-1.pyc \ + build/ba_data/python/ba/__pycache__/cloud.cpython-39.opt-1.pyc \ build/ba_data/python/ba/__pycache__/deprecated.cpython-39.opt-1.pyc \ build/ba_data/python/ba/__pycache__/internal.cpython-39.opt-1.pyc \ build/ba_data/python/ba/__pycache__/macmusicapp.cpython-39.opt-1.pyc \ @@ -640,6 +644,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \ build/ba_data/python/bacommon/assets.py \ 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/net.py \ build/ba_data/python/bacommon/servermanager.py \ build/ba_data/python/efro/__init__.py \ @@ -668,6 +673,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \ build/ba_data/python/bacommon/__pycache__/assets.cpython-39.opt-1.pyc \ build/ba_data/python/bacommon/__pycache__/bacloud.cpython-39.opt-1.pyc \ build/ba_data/python/bacommon/__pycache__/build.cpython-39.opt-1.pyc \ + build/ba_data/python/bacommon/__pycache__/cloud.cpython-39.opt-1.pyc \ build/ba_data/python/bacommon/__pycache__/net.cpython-39.opt-1.pyc \ build/ba_data/python/bacommon/__pycache__/servermanager.cpython-39.opt-1.pyc \ build/ba_data/python/efro/__pycache__/__init__.cpython-39.opt-1.pyc \ diff --git a/assets/src/ba_data/python/._ba_sources_hash b/assets/src/ba_data/python/._ba_sources_hash index 14642f9c..6fcf3172 100644 --- a/assets/src/ba_data/python/._ba_sources_hash +++ b/assets/src/ba_data/python/._ba_sources_hash @@ -1 +1 @@ -263249044076897294312459199917994463006 \ No newline at end of file +14398100813069830297938811166218395775 \ 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 e70d9afa..85a9ee26 100644 --- a/assets/src/ba_data/python/_ba.py +++ b/assets/src/ba_data/python/_ba.py @@ -263,13 +263,6 @@ class InputDevice: """ return bool() - def get_account_name(self, full: bool) -> str: - """Returns the account name associated with this device. - - (can be used to get account names for remote players) - """ - return str() - def get_axis_name(self, axis_id: int) -> str: """Given an axis ID, return the name of the axis on this device. @@ -297,6 +290,13 @@ class InputDevice: """(internal)""" return dict() + def get_v1_account_name(self, full: bool) -> str: + """Returns the account name associated with this device. + + (can be used to get account names for remote players) + """ + return str() + def is_connected_to_remote_player(self) -> bool: """(internal)""" return bool() @@ -788,16 +788,6 @@ class SessionPlayer: """Return whether the underlying player is still in the game.""" return bool() - def get_account_id(self) -> str: - """Return the Account ID this player is signed in under, if - there is one and it can be determined with relative certainty. - Returns None otherwise. Note that this may require an active - internet connection (especially for network-connected players) - and may return None for a short while after a player initially - joins (while verification occurs). - """ - return str() - def get_icon(self) -> dict[str, Any]: """Returns the character's icon (images, colors, etc contained in a dict. @@ -808,6 +798,16 @@ class SessionPlayer: """(internal)""" return {'foo': 'bar'} + def get_v1_account_id(self) -> str: + """Return the V1 Account ID this player is signed in under, if + there is one and it can be determined with relative certainty. + Returns None otherwise. Note that this may require an active + internet connection (especially for network-connected players) + and may return None for a short while after a player initially + joins (while verification occurs). + """ + return str() + def getname(self, full: bool = False, icon: bool = True) -> str: """Returns the player's name. If icon is True, the long version of the name may include an icon. @@ -1570,54 +1570,6 @@ def game_service_has_leaderboard(game: str, config: str) -> bool: return bool() -def get_account_display_string(full: bool = True) -> str: - """(internal)""" - return str() - - -def get_account_misc_read_val(name: str, default_value: Any) -> Any: - """(internal)""" - return _uninferrable() - - -def get_account_misc_read_val_2(name: str, default_value: Any) -> Any: - """(internal)""" - return _uninferrable() - - -def get_account_misc_val(name: str, default_value: Any) -> Any: - """(internal)""" - return _uninferrable() - - -def get_account_name() -> str: - """(internal)""" - return str() - - -def get_account_state() -> str: - """(internal)""" - return str() - - -def get_account_state_num() -> int: - """(internal)""" - return int() - - -def get_account_ticket_count() -> int: - """(internal) - - Returns the number of tickets for the current account. - """ - return int() - - -def get_account_type() -> str: - """(internal)""" - return str() - - def get_appconfig_builtin_keys() -> list[str]: """(internal)""" return ['blah', 'blah2'] @@ -1920,6 +1872,54 @@ def get_ui_input_device() -> ba.InputDevice: return ba.InputDevice() +def get_v1_account_display_string(full: bool = True) -> str: + """(internal)""" + return str() + + +def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any: + """(internal)""" + return _uninferrable() + + +def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any: + """(internal)""" + return _uninferrable() + + +def get_v1_account_misc_val(name: str, default_value: Any) -> Any: + """(internal)""" + return _uninferrable() + + +def get_v1_account_name() -> str: + """(internal)""" + return str() + + +def get_v1_account_state() -> str: + """(internal)""" + return str() + + +def get_v1_account_state_num() -> int: + """(internal)""" + return int() + + +def get_v1_account_ticket_count() -> int: + """(internal) + + Returns the number of tickets for the current account. + """ + return int() + + +def get_v1_account_type() -> str: + """(internal)""" + return str() + + def get_v2_fleet() -> str: """(internal)""" return str() @@ -2949,7 +2949,7 @@ def show_progress_bar() -> None: return None -def sign_in(account_type: str) -> None: +def sign_in_v1(account_type: str) -> None: """(internal) Category: General Utility Functions @@ -2957,7 +2957,7 @@ def sign_in(account_type: str) -> None: return None -def sign_out() -> None: +def sign_out_v1(v2_embedded: bool = False) -> None: """(internal) Category: General Utility Functions diff --git a/assets/src/ba_data/python/ba/_account.py b/assets/src/ba_data/python/ba/_accountv1.py similarity index 92% rename from assets/src/ba_data/python/ba/_account.py rename to assets/src/ba_data/python/ba/_accountv1.py index fb330641..2f72f19d 100644 --- a/assets/src/ba_data/python/ba/_account.py +++ b/assets/src/ba_data/python/ba/_accountv1.py @@ -14,12 +14,12 @@ if TYPE_CHECKING: from typing import Any, Optional -class AccountSubsystem: - """Subsystem for account handling in the app. +class AccountV1Subsystem: + """Subsystem for legacy account handling in the app. Category: **App Classes** - Access the single shared instance of this class at 'ba.app.plugins'. + Access the single shared instance of this class at 'ba.app.accounts_v1'. """ def __init__(self) -> None: @@ -41,7 +41,7 @@ class AccountSubsystem: def do_auto_sign_in() -> None: if _ba.app.headless_mode or _ba.app.config.get( 'Auto Account State') == 'Local': - _ba.sign_in('Local') + _ba.sign_in_v1('Local') _ba.pushcall(do_auto_sign_in) @@ -108,8 +108,8 @@ class AccountSubsystem: if data['p']: pro_mult = 1.0 + float( - _ba.get_account_misc_read_val('proPowerRankingBoost', - 0.0)) * 0.01 + _ba.get_v1_account_misc_read_val('proPowerRankingBoost', + 0.0)) * 0.01 else: pro_mult = 1.0 @@ -135,7 +135,7 @@ class AccountSubsystem: """(internal)""" # pylint: disable=cyclic-import from ba import _store - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': return [] icons = [] store_items = _store.get_store_items() @@ -152,12 +152,12 @@ class AccountSubsystem: (internal) """ # This only applies when we're signed in. - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': return # If the short version of our account name currently cant be # displayed by the game, cancel. - if not _ba.have_chars(_ba.get_account_display_string(full=False)): + if not _ba.have_chars(_ba.get_v1_account_display_string(full=False)): return config = _ba.app.config @@ -199,7 +199,7 @@ class AccountSubsystem: # or also if we've been grandfathered in or are using ballistica-core # builds. return self.have_pro() or bool( - _ba.get_account_misc_read_val_2('proOptionsUnlocked', False) + _ba.get_v1_account_misc_read_val_2('proOptionsUnlocked', False) or _ba.app.config.get('lc14292', 0) > 1) def show_post_purchase_message(self) -> None: @@ -221,7 +221,8 @@ class AccountSubsystem: from ba._language import Lstr # Run any pending promo codes we had queued up while not signed in. - if _ba.get_account_state() == 'signed_in' and self.pending_promo_codes: + if _ba.get_v1_account_state( + ) == 'signed_in' and self.pending_promo_codes: for code in self.pending_promo_codes: _ba.screenmessage(Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)) @@ -241,7 +242,7 @@ class AccountSubsystem: # If we're not signed in, queue up the code to run the next time we # are and issue a warning if we haven't signed in within the next # few seconds. - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': def check_pending_codes() -> None: """(internal)""" diff --git a/assets/src/ba_data/python/ba/_accountv2.py b/assets/src/ba_data/python/ba/_accountv2.py new file mode 100644 index 00000000..36b776e7 --- /dev/null +++ b/assets/src/ba_data/python/ba/_accountv2.py @@ -0,0 +1,51 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Account related functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional + + +class AccountV2Subsystem: + """Subsystem for modern account handling in the app. + + Category: **App Classes** + + Access the single shared instance of this class at 'ba.app.accounts_v2'. + """ + + def on_app_launch(self) -> None: + """Should be called at standard on_app_launch time.""" + + def set_primary_credentials(self, credentials: Optional[str]) -> None: + """Set credentials for the primary app account.""" + raise RuntimeError('This should be overridden.') + + def have_primary_credentials(self) -> bool: + """Are credentials currently set for the primary app account? + + Note that this does not mean these credentials are currently valid; + only that they exist. If/when credentials are validated, the 'primary' + account handle will be set. + """ + raise RuntimeError('This should be overridden.') + + @property + def primary(self) -> Optional[AccountV2Handle]: + """The primary account for the app, or None if not logged in.""" + return None + + def get_primary(self) -> Optional[AccountV2Handle]: + """Internal - should be overridden by subclass.""" + return None + + +class AccountV2Handle: + """Handle for interacting with a v2 account.""" + + def __init__(self) -> None: + self.tag = '?' diff --git a/assets/src/ba_data/python/ba/_achievement.py b/assets/src/ba_data/python/ba/_achievement.py index 0357c84e..23e5d93e 100644 --- a/assets/src/ba_data/python/ba/_achievement.py +++ b/assets/src/ba_data/python/ba/_achievement.py @@ -409,9 +409,9 @@ def _get_ach_mult(include_pro_bonus: bool = False) -> int: (just for display; changing this here won't affect actual rewards) """ - val: int = _ba.get_account_misc_read_val('achAwardMult', 5) + val: int = _ba.get_v1_account_misc_read_val('achAwardMult', 5) assert isinstance(val, int) - if include_pro_bonus and _ba.app.accounts.have_pro(): + if include_pro_bonus and _ba.app.accounts_v1.have_pro(): val *= 2 return val @@ -496,7 +496,7 @@ class Achievement: # signed in, lets not show them (otherwise we tend to get # confusing 'controller connected' achievements popping up while # waiting to log in which can be confusing). - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': return # If we're being freshly complete, display/report it and whatnot. @@ -592,8 +592,8 @@ class Achievement: def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int: """Get the ticket award value for this achievement.""" - val: int = (_ba.get_account_misc_read_val('achAward.' + self._name, - self._award) * + val: int = (_ba.get_v1_account_misc_read_val('achAward.' + self._name, + self._award) * _get_ach_mult(include_pro_bonus)) assert isinstance(val, int) return val @@ -601,7 +601,7 @@ class Achievement: @property def power_ranking_value(self) -> int: """Get the power-ranking award value for this achievement.""" - val: int = _ba.get_account_misc_read_val( + val: int = _ba.get_v1_account_misc_read_val( 'achLeaguePoints.' + self._name, self._award) assert isinstance(val, int) return val @@ -1176,7 +1176,7 @@ class Achievement: objt.node.host_only = True # Add the 'x 2' if we've got pro. - if app.accounts.have_pro(): + if app.accounts_v1.have_pro(): objt = Text('x 2', position=(-120 - 180 + 45, 80 + y_offs - 50), v_attach=Text.VAttach.BOTTOM, diff --git a/assets/src/ba_data/python/ba/_ads.py b/assets/src/ba_data/python/ba/_ads.py index b7765b74..a44133cc 100644 --- a/assets/src/ba_data/python/ba/_ads.py +++ b/assets/src/ba_data/python/ba/_ads.py @@ -77,7 +77,7 @@ class AdsSubsystem: # No ads without net-connections, etc. if not _ba.can_show_ad(): show = False - if app.accounts.have_pro(): + if app.accounts_v1.have_pro(): show = False # Pro disables interstitials. try: session = _ba.get_foreground_host_session() @@ -93,15 +93,15 @@ class AdsSubsystem: launch_count = app.config.get('launchCount', 0) # If we're seeing short ads we may want to space them differently. - interval_mult = (_ba.get_account_misc_read_val( + interval_mult = (_ba.get_v1_account_misc_read_val( 'ads.shortIntervalMult', 1.0) if self.last_ad_was_short else 1.0) if self.ad_amt is None: if launch_count <= 1: - self.ad_amt = _ba.get_account_misc_read_val( + self.ad_amt = _ba.get_v1_account_misc_read_val( 'ads.startVal1', 0.99) else: - self.ad_amt = _ba.get_account_misc_read_val( + self.ad_amt = _ba.get_v1_account_misc_read_val( 'ads.startVal2', 1.0) interval = None else: @@ -110,15 +110,15 @@ class AdsSubsystem: # (we reach our threshold faster the longer we've been # playing). base = 'ads' if _ba.has_video_ads() else 'ads2' - min_lc = _ba.get_account_misc_read_val(base + '.minLC', 0.0) - max_lc = _ba.get_account_misc_read_val(base + '.maxLC', 5.0) - min_lc_scale = (_ba.get_account_misc_read_val( + min_lc = _ba.get_v1_account_misc_read_val(base + '.minLC', 0.0) + max_lc = _ba.get_v1_account_misc_read_val(base + '.maxLC', 5.0) + min_lc_scale = (_ba.get_v1_account_misc_read_val( base + '.minLCScale', 0.25)) - max_lc_scale = (_ba.get_account_misc_read_val( + max_lc_scale = (_ba.get_v1_account_misc_read_val( base + '.maxLCScale', 0.34)) - min_lc_interval = (_ba.get_account_misc_read_val( + min_lc_interval = (_ba.get_v1_account_misc_read_val( base + '.minLCInterval', 360)) - max_lc_interval = (_ba.get_account_misc_read_val( + max_lc_interval = (_ba.get_v1_account_misc_read_val( base + '.maxLCInterval', 300)) if launch_count < min_lc: lc_amt = 0.0 diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py index dca9096a..edec32f5 100644 --- a/assets/src/ba_data/python/ba/_app.py +++ b/assets/src/ba_data/python/ba/_app.py @@ -7,6 +7,7 @@ import random import logging from enum import Enum from typing import TYPE_CHECKING +from concurrent.futures import ThreadPoolExecutor import _ba from ba._music import MusicSubsystem @@ -14,16 +15,20 @@ from ba._language import LanguageSubsystem from ba._ui import UISubsystem from ba._achievement import AchievementSubsystem from ba._plugin import PluginSubsystem -from ba._account import AccountSubsystem +from ba._accountv1 import AccountV1Subsystem from ba._meta import MetadataSubsystem from ba._ads import AdsSubsystem from ba._net import NetworkSubsystem if TYPE_CHECKING: - import ba - from bastd.actor import spazappearance + import asyncio from typing import Optional, Any, Callable + import ba + from ba.cloud import CloudSubsystem + from bastd.actor import spazappearance + from ba._accountv2 import AccountV2Subsystem + class App: """A class for high level app functionality and state. @@ -38,6 +43,10 @@ class App: # pylint: disable=too-many-public-methods + # Implementations for these will be filled in by internal libs. + accounts_v2: AccountV2Subsystem + cloud: CloudSubsystem + class State(Enum): """High level state the app can be in.""" LAUNCHING = 0 @@ -45,6 +54,20 @@ class App: PAUSED = 2 SHUTTING_DOWN = 3 + @property + def aioloop(self) -> asyncio.AbstractEventLoop: + """The Logic Thread's Asyncio Event Loop. + + This allow async tasks to be run in the logic thread. + Note that, at this time, the asyncio loop is encapsulated + and explicitly stepped by the engine's logic thread loop and + thus things like asyncio.get_running_loop() will not return this + loop from most places in the logic thread; only from within a + task explicitly created in this loop. + """ + assert self._aioloop is not None + return self._aioloop + @property def build_number(self) -> int: """Integer build number. @@ -196,6 +219,8 @@ class App: # refreshed/etc. self.fg_state = 0 + self._aioloop: Optional[asyncio.AbstractEventLoop] = None + self._env = _ba.env() self.protocol_version: int = self._env['protocol_version'] assert isinstance(self.protocol_version, int) @@ -211,6 +236,11 @@ class App: assert isinstance(self.iircade_mode, bool) self.allow_ticket_purchases: bool = not self.iircade_mode + # Default executor which can be used for misc background processing. + # It should also be passed to any asyncio loops we create so that + # everything shares the same single set of threads. + self.threadpool = ThreadPoolExecutor(thread_name_prefix='baworker') + # Misc. self.tips: list[str] = [] self.stress_test_reset_timer: Optional[ba.Timer] = None @@ -235,7 +265,7 @@ class App: self.server: Optional[ba.ServerController] = None self.meta = MetadataSubsystem() - self.accounts = AccountSubsystem() + self.accounts_v1 = AccountV1Subsystem() self.plugins = PluginSubsystem() self.music = MusicSubsystem() self.lang = LanguageSubsystem() @@ -286,6 +316,8 @@ class App: (internal)""" # pylint: disable=cyclic-import + # pylint: disable=too-many-locals + from ba import _asyncio from ba import _apputils from ba import _appconfig from ba import _map @@ -295,6 +327,8 @@ class App: from bastd.actor import spazappearance from ba._generated.enums import TimeType + self._aioloop = _asyncio.setup_asyncio() + cfg = self.config self.delegate = appdelegate.AppDelegate() @@ -365,7 +399,8 @@ class App: _ba.timer(3.0, check_special_offer, timetype=TimeType.REAL) self.meta.on_app_launch() - self.accounts.on_app_launch() + self.accounts_v2.on_app_launch() + self.accounts_v1.on_app_launch() self.plugins.on_app_launch() # See note below in on_app_pause. @@ -403,7 +438,7 @@ class App: self._app_paused = False self._update_state() self.fg_state += 1 - self.accounts.on_app_resume() + self.accounts_v1.on_app_resume() self.music.on_app_resume() self.plugins.on_app_resume() @@ -569,7 +604,7 @@ class App: appname = _ba.appname() if url.startswith(f'{appname}://code/'): code = url.replace(f'{appname}://code/', '') - self.accounts.add_pending_promo_code(code) + self.accounts_v1.add_pending_promo_code(code) else: _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) _ba.playsound(_ba.getsound('error')) diff --git a/assets/src/ba_data/python/ba/_asyncio.py b/assets/src/ba_data/python/ba/_asyncio.py index 79407a79..40e53c92 100644 --- a/assets/src/ba_data/python/ba/_asyncio.py +++ b/assets/src/ba_data/python/ba/_asyncio.py @@ -21,11 +21,12 @@ _asyncio_timer: Optional[ba.Timer] = None _asyncio_event_loop: Optional[asyncio.AbstractEventLoop] = None -def setup_asyncio() -> None: - """Setup asyncio functionality for our game thread.""" +def setup_asyncio() -> asyncio.AbstractEventLoop: + """Setup asyncio functionality for the logic thread.""" # pylint: disable=global-statement import _ba + import ba from ba._generated.enums import TimeType assert _ba.in_game_thread() @@ -40,6 +41,7 @@ def setup_asyncio() -> None: global _asyncio_event_loop # pylint: disable=invalid-name _asyncio_event_loop = asyncio.new_event_loop() + _asyncio_event_loop.set_default_executor(ba.app.threadpool) # Ideally we should integrate asyncio into our C++ Thread class's # low level event loop so that asyncio timers/sockets/etc. could @@ -70,3 +72,5 @@ def setup_asyncio() -> None: print('TEST AIO TASK ENDING') _asyncio_event_loop.create_task(aio_test()) + + return _asyncio_event_loop diff --git a/assets/src/ba_data/python/ba/_gameactivity.py b/assets/src/ba_data/python/ba/_gameactivity.py index 73e9f538..419eae0f 100644 --- a/assets/src/ba_data/python/ba/_gameactivity.py +++ b/assets/src/ba_data/python/ba/_gameactivity.py @@ -239,11 +239,11 @@ class GameActivity(Activity[PlayerType, TeamType]): self._zoom_message_times: dict[int, float] = {} self._is_waiting_for_continue = False - self._continue_cost = _ba.get_account_misc_read_val( + self._continue_cost = _ba.get_v1_account_misc_read_val( 'continueStartCost', 25) - self._continue_cost_mult = _ba.get_account_misc_read_val( + self._continue_cost_mult = _ba.get_v1_account_misc_read_val( 'continuesMult', 2) - self._continue_cost_offset = _ba.get_account_misc_read_val( + self._continue_cost_offset = _ba.get_v1_account_misc_read_val( 'continuesOffset', 0) @property @@ -390,7 +390,7 @@ class GameActivity(Activity[PlayerType, TeamType]): from ba._generated.enums import TimeType try: - if _ba.get_account_misc_read_val('enableContinues', False): + if _ba.get_v1_account_misc_read_val('enableContinues', False): session = self.session # We only support continuing in non-tournament games. @@ -467,7 +467,7 @@ class GameActivity(Activity[PlayerType, TeamType]): data_t = data['t'] # This used to be the whole payload. # Keep our cached tourney info up to date - _ba.app.accounts.cache_tournament_info(data_t) + _ba.app.accounts_v1.cache_tournament_info(data_t) self._setup_tournament_time_limit( max(5, data_t[0]['timeRemaining'])) diff --git a/assets/src/ba_data/python/ba/_hooks.py b/assets/src/ba_data/python/ba/_hooks.py index 11ddae36..03f1331e 100644 --- a/assets/src/ba_data/python/ba/_hooks.py +++ b/assets/src/ba_data/python/ba/_hooks.py @@ -24,12 +24,11 @@ if TYPE_CHECKING: def finish_bootstrapping() -> None: """Do final bootstrapping related bits.""" - from ba._asyncio import setup_asyncio assert _ba.in_game_thread() # Kick off our asyncio event handling, allowing us to use coroutines # in our game thread alongside our internal event handling. - setup_asyncio() + # setup_asyncio() # Ok, bootstrapping is done; time to get the show started. _ba.app.on_app_launch() @@ -362,3 +361,8 @@ def hash_strings(inputs: list[str]) -> str: sha.update(inp.encode()) return sha.hexdigest() + + +def have_account_v2_credentials() -> bool: + """Do we have primary account-v2 credentials set?""" + return _ba.app.accounts_v2.have_primary_credentials() diff --git a/assets/src/ba_data/python/ba/_input.py b/assets/src/ba_data/python/ba/_input.py index fcf96ef5..bfea7280 100644 --- a/assets/src/ba_data/python/ba/_input.py +++ b/assets/src/ba_data/python/ba/_input.py @@ -639,5 +639,5 @@ def get_last_player_name_from_input_device(device: ba.InputDevice) -> str: if profilename == '_random': profilename = device.get_default_player_name() if profilename == '__account__': - profilename = _ba.get_account_display_string() + profilename = _ba.get_v1_account_display_string() return profilename diff --git a/assets/src/ba_data/python/ba/_lobby.py b/assets/src/ba_data/python/ba/_lobby.py index 1d4274d6..bd953b87 100644 --- a/assets/src/ba_data/python/ba/_lobby.py +++ b/assets/src/ba_data/python/ba/_lobby.py @@ -452,7 +452,8 @@ class Chooser: clamp = not full elif name == '__account__': try: - name = self._sessionplayer.inputdevice.get_account_name(full) + name = self._sessionplayer.inputdevice.get_v1_account_name( + full) except Exception: print_exception('Error getting account name for chooser.') name = 'Invalid' @@ -894,7 +895,7 @@ class Lobby: self.character_names_local_unlocked.sort(key=lambda x: x.lower()) # Do any overall prep we need to such as creating account profile. - _ba.app.accounts.ensure_have_account_player_profile() + _ba.app.accounts_v1.ensure_have_account_player_profile() for chooser in self.choosers: try: chooser.reload_profiles() diff --git a/assets/src/ba_data/python/ba/_servermode.py b/assets/src/ba_data/python/ba/_servermode.py index 87e525d7..37fad3c8 100644 --- a/assets/src/ba_data/python/ba/_servermode.py +++ b/assets/src/ba_data/python/ba/_servermode.py @@ -227,7 +227,7 @@ class ServerController: def _prepare_to_serve(self) -> None: """Run in a timer to do prep before beginning to serve.""" - signed_in = _ba.get_account_state() == 'signed_in' + signed_in = _ba.get_v1_account_state() == 'signed_in' if not signed_in: # Signing in to the local server account should not take long; @@ -302,7 +302,7 @@ class ServerController: appcfg = app.config sessiontype = self._get_session_type() - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': print('WARNING: launch_server_session() expects to run ' 'with a signed in server account') diff --git a/assets/src/ba_data/python/ba/_store.py b/assets/src/ba_data/python/ba/_store.py index 0dc91f9c..5c936bd5 100644 --- a/assets/src/ba_data/python/ba/_store.py +++ b/assets/src/ba_data/python/ba/_store.py @@ -366,11 +366,11 @@ def get_store_layout() -> dict[str, list[dict[str, Any]]]: 'games.ninja_fight', 'games.meteor_shower', 'games.target_practice' ] }] - if _ba.get_account_misc_read_val('xmas', False): + if _ba.get_v1_account_misc_read_val('xmas', False): store_layout['characters'][0]['items'].append('characters.santa') store_layout['characters'][0]['items'].append('characters.wizard') store_layout['characters'][0]['items'].append('characters.cyborg') - if _ba.get_account_misc_read_val('easter', False): + if _ba.get_v1_account_misc_read_val('easter', False): store_layout['characters'].append({ 'title': 'store.holidaySpecialText', 'items': ['characters.bunny'] @@ -401,10 +401,10 @@ def get_clean_price(price_string: str) -> str: def get_available_purchase_count(tab: str = None) -> int: """(internal)""" try: - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': return 0 count = 0 - our_tickets = _ba.get_account_ticket_count() + our_tickets = _ba.get_v1_account_ticket_count() store_data = get_store_layout() if tab is not None: tabs = [(tab, store_data[tab])] @@ -425,7 +425,8 @@ def _calc_count_for_tab(tabval: list[dict[str, Any]], our_tickets: int, count: int) -> int: for section in tabval: for item in section['items']: - ticket_cost = _ba.get_account_misc_read_val('price.' + item, None) + ticket_cost = _ba.get_v1_account_misc_read_val( + 'price.' + item, None) if ticket_cost is not None: if (our_tickets >= ticket_cost and not _ba.get_purchased(item)): @@ -447,7 +448,7 @@ def get_available_sale_time(tab: str) -> Optional[int]: # Calc time for our pro sale (old special case). if tab == 'extras': config = app.config - if app.accounts.have_pro(): + if app.accounts_v1.have_pro(): return None # If we haven't calced/loaded start times yet. @@ -462,7 +463,7 @@ def get_available_sale_time(tab: str) -> Optional[int]: # We start the timer once we get the duration from # the server. - start_duration = _ba.get_account_misc_read_val( + start_duration = _ba.get_v1_account_misc_read_val( 'proSaleDurationMinutes', None) if start_duration is not None: app.pro_sale_start_time = int( @@ -488,7 +489,7 @@ def get_available_sale_time(tab: str) -> Optional[int]: sale_times.append(val) # Now look for sales in this tab. - sales_raw = _ba.get_account_misc_read_val('sales', {}) + sales_raw = _ba.get_v1_account_misc_read_val('sales', {}) store_layout = get_store_layout() for section in store_layout[tab]: for item in section['items']: diff --git a/assets/src/ba_data/python/ba/cloud.py b/assets/src/ba_data/python/ba/cloud.py new file mode 100644 index 00000000..da0057a5 --- /dev/null +++ b/assets/src/ba_data/python/ba/cloud.py @@ -0,0 +1,93 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the cloud.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, overload + +import _ba + +if TYPE_CHECKING: + from typing import Union, Callable, Any + + from efro.message import Message + import bacommon.cloud + +# 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. + + +class CloudSubsystem: + """Used for communicating with the cloud.""" + + def is_connected(self) -> bool: + """Return whether a connection to the cloud is present. + + This is a good indicator (though not for certain) that sending + messages will succeed. + """ + return False # Needs to be overridden + + @overload + def send_message( + self, + msg: bacommon.cloud.LoginProxyRequestMessage, + on_response: Callable[ + [Union[bacommon.cloud.LoginProxyRequestResponse, + Exception]], None], + ) -> None: + ... + + @overload + def send_message( + self, + msg: bacommon.cloud.LoginProxyStateQueryMessage, + on_response: Callable[ + [Union[bacommon.cloud.LoginProxyStateQueryResponse, + Exception]], None], + ) -> None: + ... + + @overload + def send_message( + self, + msg: bacommon.cloud.LoginProxyCompleteMessage, + on_response: Callable[[Union[None, Exception]], None], + ) -> None: + ... + + @overload + def send_message( + self, + msg: bacommon.cloud.CredentialsCheckMessage, + on_response: Callable[ + [Union[bacommon.cloud.CredentialsCheckResponse, Exception]], None], + ) -> None: + ... + + @overload + def send_message( + self, + msg: bacommon.cloud.AccountSessionReleaseMessage, + on_response: Callable[[Union[None, Exception]], None], + ) -> None: + ... + + def send_message( + self, + msg: Message, + on_response: Callable[[Any], None], + ) -> None: + """Asynchronously send a message to the cloud from the game thread. + + The provided on_response call will be run in the logic thread + and passed either the response or the error that occurred. + """ + from ba._general import Call + del msg # Unused. + + _ba.pushcall( + Call(on_response, + RuntimeError('Cloud functionality is not available.'))) diff --git a/assets/src/ba_data/python/bastd/activity/coopscore.py b/assets/src/ba_data/python/bastd/activity/coopscore.py index 9dd4e277..f5783903 100644 --- a/assets/src/ba_data/python/bastd/activity/coopscore.py +++ b/assets/src/ba_data/python/bastd/activity/coopscore.py @@ -52,8 +52,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ba.app.ach.achievements_for_coop_level(self._campaign.name + ':' + settings['level'])) - self._account_type = (_ba.get_account_type() if - _ba.get_account_state() == 'signed_in' else None) + self._account_type = (_ba.get_v1_account_type() + if _ba.get_v1_account_state() == 'signed_in' else + None) self._game_service_icon_color: Optional[Sequence[float]] self._game_service_achievements_texture: Optional[ba.Texture] @@ -631,7 +632,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if ba.app.server is None: # If we're running in normal non-headless build, show this text # because only host can continue the game. - adisp = _ba.get_account_display_string() + adisp = _ba.get_v1_account_display_string() txt = Text(ba.Lstr(resource='waitingForHostText', subs=[('${HOST}', adisp)]), maxwidth=300, @@ -732,7 +733,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): 'scoreVersion': sver, 'scores': our_high_scores_all }) - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': # We expect this only in kiosk mode; complain otherwise. if not (ba.app.demo_mode or ba.app.arcade_mode): print('got not-signed-in at score-submit; unexpected') @@ -1260,8 +1261,8 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): try: tournament_id = self.session.tournament_id if tournament_id is not None: - if tournament_id in ba.app.accounts.tournament_info: - tourney_info = ba.app.accounts.tournament_info[ + if tournament_id in ba.app.accounts_v1.tournament_info: + tourney_info = ba.app.accounts_v1.tournament_info[ tournament_id] # pylint: disable=unbalanced-tuple-unpacking pr1, pv1, pr2, pv2, pr3, pv3 = ( diff --git a/assets/src/ba_data/python/bastd/actor/spazfactory.py b/assets/src/ba_data/python/bastd/actor/spazfactory.py index 1a5bb9be..0840d8f5 100644 --- a/assets/src/ba_data/python/bastd/actor/spazfactory.py +++ b/assets/src/ba_data/python/bastd/actor/spazfactory.py @@ -208,14 +208,14 @@ class SpazFactory: # Lets load some basic rules. # (allows them to be tweaked from the master server) - self.shield_decay_rate = _ba.get_account_misc_read_val('rsdr', 10.0) - self.punch_cooldown = _ba.get_account_misc_read_val('rpc', 400) - self.punch_cooldown_gloves = (_ba.get_account_misc_read_val( + self.shield_decay_rate = _ba.get_v1_account_misc_read_val('rsdr', 10.0) + self.punch_cooldown = _ba.get_v1_account_misc_read_val('rpc', 400) + self.punch_cooldown_gloves = (_ba.get_v1_account_misc_read_val( 'rpcg', 300)) - self.punch_power_scale = _ba.get_account_misc_read_val('rpp', 1.2) - self.punch_power_scale_gloves = (_ba.get_account_misc_read_val( + self.punch_power_scale = _ba.get_v1_account_misc_read_val('rpp', 1.2) + self.punch_power_scale_gloves = (_ba.get_v1_account_misc_read_val( 'rppg', 1.4)) - self.max_shield_spillover_damage = (_ba.get_account_misc_read_val( + self.max_shield_spillover_damage = (_ba.get_v1_account_misc_read_val( 'rsms', 500)) def get_style(self, character: str) -> str: diff --git a/assets/src/ba_data/python/bastd/mainmenu.py b/assets/src/ba_data/python/bastd/mainmenu.py index 86818e21..b32a937d 100644 --- a/assets/src/ba_data/python/bastd/mainmenu.py +++ b/assets/src/ba_data/python/bastd/mainmenu.py @@ -67,7 +67,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # host is navigating menus while they're just staring at an # empty-ish screen. tval = ba.Lstr(resource='hostIsNavigatingMenusText', - subs=[('${HOST}', _ba.get_account_display_string())]) + subs=[('${HOST}', _ba.get_v1_account_display_string())]) self._host_is_navigating_text = ba.NodeActor( ba.newnode('text', attrs={ @@ -274,7 +274,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # We now want to wait until we're signed in before fetching news. def _try_fetching_news(self) -> None: - if _ba.get_account_state() == 'signed_in': + if _ba.get_v1_account_state() == 'signed_in': self._fetch_news() self._fetch_timer = None @@ -282,7 +282,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): ba.app.main_menu_last_news_fetch_time = time.time() # UPDATE - We now just pull news from MRVs. - news = _ba.get_account_misc_read_val('n', None) + news = _ba.get_v1_account_misc_read_val('n', None) if news is not None: self._got_news(news) @@ -757,7 +757,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): }) def _get_custom_logo_tex_name(self) -> Optional[str]: - if _ba.get_account_misc_read_val('easter', False): + if _ba.get_v1_account_misc_read_val('easter', False): return 'logoEaster' return None diff --git a/assets/src/ba_data/python/bastd/ui/account/__init__.py b/assets/src/ba_data/python/bastd/ui/account/__init__.py index 7bf7c132..9b562442 100644 --- a/assets/src/ba_data/python/bastd/ui/account/__init__.py +++ b/assets/src/ba_data/python/bastd/ui/account/__init__.py @@ -15,7 +15,7 @@ def show_sign_in_prompt(account_type: str = None) -> None: if account_type == 'Google Play': ConfirmWindow( ba.Lstr(resource='notSignedInGooglePlayErrorText'), - lambda: _ba.sign_in('Google Play'), + lambda: _ba.sign_in_v1('Google Play'), ok_text=ba.Lstr(resource='accountSettingsWindow.signInText'), width=460, height=130) diff --git a/assets/src/ba_data/python/bastd/ui/account/link.py b/assets/src/ba_data/python/bastd/ui/account/link.py index df01fc9a..1e0b2b77 100644 --- a/assets/src/ba_data/python/bastd/ui/account/link.py +++ b/assets/src/ba_data/python/bastd/ui/account/link.py @@ -50,7 +50,7 @@ class AccountLinkWindow(ba.Window): autoselect=True, icon=ba.gettexture('crossOut'), iconscale=1.2) - maxlinks = _ba.get_account_misc_read_val('maxLinkAccounts', 5) + maxlinks = _ba.get_v1_account_misc_read_val('maxLinkAccounts', 5) ba.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.56), @@ -84,7 +84,7 @@ class AccountLinkWindow(ba.Window): def _generate_press(self) -> None: from bastd.ui import account - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return ba.screenmessage( 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 1a7d2641..901e0c89 100644 --- a/assets/src/ba_data/python/bastd/ui/account/settings.py +++ b/assets/src/ba_data/python/bastd/ui/account/settings.py @@ -45,10 +45,11 @@ class AccountSettingsWindow(ba.Window): self._r = 'accountSettingsWindow' self._modal = modal self._needs_refresh = False - self._signed_in = (_ba.get_account_state() == 'signed_in') - self._account_state_num = _ba.get_account_state_num() - self._show_linked = (self._signed_in and _ba.get_account_misc_read_val( - 'allowAccountLinking2', False)) + self._signed_in = (_ba.get_v1_account_state() == 'signed_in') + self._account_state_num = _ba.get_v1_account_state_num() + self._show_linked = (self._signed_in + and _ba.get_v1_account_misc_read_val( + 'allowAccountLinking2', False)) self._check_sign_in_timer = ba.Timer(1.0, ba.WeakCall(self._update), timetype=ba.TimeType.REAL, @@ -57,7 +58,7 @@ class AccountSettingsWindow(ba.Window): # Currently we can only reset achievements on game-center. account_type: Optional[str] if self._signed_in: - account_type = _ba.get_account_type() + account_type = _ba.get_v1_account_type() else: account_type = None self._can_reset_achievements = (account_type == 'Game Center') @@ -91,7 +92,7 @@ class AccountSettingsWindow(ba.Window): self._show_sign_in_buttons.append('Local') # Ditto with shiny new V2 ones. - if bool(False): + if bool(True): self._show_sign_in_buttons.append('V2') top_extra = 15 if uiscale is ba.UIScale.SMALL else 0 @@ -158,10 +159,10 @@ class AccountSettingsWindow(ba.Window): # Hmm should update this to use get_account_state_num. # Theoretically if we switch from one signed-in account to another # in the background this would break. - account_state_num = _ba.get_account_state_num() - account_state = _ba.get_account_state() + account_state_num = _ba.get_v1_account_state_num() + account_state = _ba.get_v1_account_state() - show_linked = (self._signed_in and _ba.get_account_misc_read_val( + show_linked = (self._signed_in and _ba.get_v1_account_misc_read_val( 'allowAccountLinking2', False)) if (account_state_num != self._account_state_num @@ -190,8 +191,8 @@ class AccountSettingsWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui import confirm - account_state = _ba.get_account_state() - account_type = (_ba.get_account_type() + account_state = _ba.get_v1_account_state() + account_type = (_ba.get_v1_account_type() if account_state == 'signed_in' else 'unknown') is_google = account_type == 'Google Play' @@ -225,7 +226,7 @@ class AccountSettingsWindow(ba.Window): game_service_button_space = 60.0 show_linked_accounts_text = (self._signed_in - and _ba.get_account_misc_read_val( + and _ba.get_v1_account_misc_read_val( 'allowAccountLinking2', False)) linked_accounts_text_space = 60.0 @@ -254,17 +255,22 @@ class AccountSettingsWindow(ba.Window): player_profiles_button_space = 100.0 show_link_accounts_button = (self._signed_in - and _ba.get_account_misc_read_val( + and _ba.get_v1_account_misc_read_val( 'allowAccountLinking2', False)) link_accounts_button_space = 70.0 show_unlink_accounts_button = show_link_accounts_button unlink_accounts_button_space = 90.0 - show_sign_out_button = (self._signed_in - and account_type in ['Local', 'Google Play']) + show_sign_out_button = (self._signed_in and account_type + in ['Local', 'Google Play', 'V2']) sign_out_button_space = 70.0 + show_cancel_v2_sign_in_button = ( + account_state == 'signing_in' + and ba.app.accounts_v2.have_primary_credentials()) + cancel_v2_sign_in_button_space = 70.0 + if self._subcontainer is not None: self._subcontainer.delete() self._sub_height = 60.0 @@ -308,6 +314,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 self._subcontainer = ba.containerwidget(parent=self._scrollwidget, size=(self._sub_width, self._sub_height), @@ -327,7 +335,7 @@ class AccountSettingsWindow(ba.Window): size=(0, 0), text=ba.Lstr( resource='accountSettingsWindow.deviceSpecificAccountText', - subs=[('${NAME}', _ba.get_account_display_string())]), + subs=[('${NAME}', _ba.get_v1_account_display_string())]), scale=0.7, color=(0.5, 0.5, 0.6), maxwidth=self._sub_width * 0.9, @@ -581,7 +589,7 @@ class AccountSettingsWindow(ba.Window): if show_game_service_button: button_width = 300 v -= game_service_button_space * 0.85 - account_type = _ba.get_account_type() + account_type = _ba.get_v1_account_type() if account_type == 'Game Center': account_type_name = ba.Lstr(resource='gameCenterText') elif account_type == 'Game Circle': @@ -849,6 +857,24 @@ class AccountSettingsWindow(ba.Window): right_widget=_ba.get_special_widget('party_button')) 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( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + size=(button_width, 60), + label=ba.Lstr(resource='cancelText'), + 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) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) + # Whatever the topmost selectable thing is, we want it to scroll all # the way up when we select it. if first_selectable is not None: @@ -863,8 +889,8 @@ class AccountSettingsWindow(ba.Window): def _on_achievements_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui import achievements - account_state = _ba.get_account_state() - account_type = (_ba.get_account_type() + account_state = _ba.get_v1_account_state() + account_type = (_ba.get_v1_account_type() if account_state == 'signed_in' else 'unknown') # for google play we use the built-in UI; otherwise pop up our own if account_type == 'Google Play': @@ -889,7 +915,7 @@ class AccountSettingsWindow(ba.Window): # let's not proceed.. if _ba.get_public_login_id() is None: return False - accounts = _ba.get_account_misc_read_val_2('linkedAccounts', []) + accounts = _ba.get_v1_account_misc_read_val_2('linkedAccounts', []) return len(accounts) > 1 def _update_unlink_accounts_button(self) -> None: @@ -911,8 +937,8 @@ class AccountSettingsWindow(ba.Window): num = int(time.time()) % 4 accounts_str = num * '.' + (4 - num) * ' ' else: - accounts = _ba.get_account_misc_read_val_2('linkedAccounts', []) - # our_account = _bs.get_account_display_string() + accounts = _ba.get_v1_account_misc_read_val_2('linkedAccounts', []) + # our_account = _bs.get_v1_account_display_string() # accounts = [a for a in accounts if a != our_account] # accounts_str = u', '.join(accounts) if accounts else # ba.Lstr(translate=('settingNames', 'None')) @@ -951,7 +977,7 @@ class AccountSettingsWindow(ba.Window): if self._tickets_text is None: return try: - tc_str = str(_ba.get_account_ticket_count()) + tc_str = str(_ba.get_v1_account_ticket_count()) except Exception: ba.print_exception() tc_str = '-' @@ -963,7 +989,7 @@ class AccountSettingsWindow(ba.Window): if self._account_name_text is None: return try: - name_str = _ba.get_account_display_string() + name_str = _ba.get_v1_account_display_string() except Exception: ba.print_exception() name_str = '??' @@ -1005,22 +1031,37 @@ class AccountSettingsWindow(ba.Window): pbrowser.ProfileBrowserWindow( origin_widget=self._player_profiles_button) + def _cancel_v2_sign_in_press(self) -> None: + # Just say we don't wanna be signed in anymore. + ba.app.accounts_v2.set_primary_credentials(None) + + # Speed UI updates along. + ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) + def _sign_out_press(self) -> None: - _ba.sign_out() + + if ba.app.accounts_v2.have_primary_credentials(): + ba.app.accounts_v2.set_primary_credentials(None) + else: + _ba.sign_out_v1() + cfg = ba.app.config - # Take note that its our *explicit* intention to not be signed in at - # this point. + # Also take note that its our *explicit* intention to not be + # signed in at this point (affects v1 accounts). cfg['Auto Account State'] = 'signed_out' cfg.commit() ba.buttonwidget(edit=self._sign_out_button, label=ba.Lstr(resource=self._r + '.signingOutText')) + # Speed UI updates along. + ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) + def _sign_in_press(self, account_type: str, show_test_warning: bool = True) -> None: del show_test_warning # unused - _ba.sign_in(account_type) + _ba.sign_in_v1(account_type) # Make note of the type account we're *wanting* to be signed in with. cfg = ba.app.config diff --git a/assets/src/ba_data/python/bastd/ui/account/unlink.py b/assets/src/ba_data/python/bastd/ui/account/unlink.py index 8ef04517..83e00ddc 100644 --- a/assets/src/ba_data/python/bastd/ui/account/unlink.py +++ b/assets/src/ba_data/python/bastd/ui/account/unlink.py @@ -81,7 +81,7 @@ class AccountUnlinkWindow(ba.Window): if our_login_id is None: entries = [] else: - account_infos = _ba.get_account_misc_read_val_2( + account_infos = _ba.get_v1_account_misc_read_val_2( 'linkedAccounts2', []) entries = [{ 'name': ai['d'], diff --git a/assets/src/ba_data/python/bastd/ui/account/v2.py b/assets/src/ba_data/python/bastd/ui/account/v2.py index d622b1d7..9acc76a5 100644 --- a/assets/src/ba_data/python/bastd/ui/account/v2.py +++ b/assets/src/ba_data/python/bastd/ui/account/v2.py @@ -4,25 +4,30 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING import ba import _ba +from efro.error import CommunicationError +import bacommon.cloud + if TYPE_CHECKING: - pass + from typing import Union, Optional + +STATUS_CHECK_INTERVAL_SECONDS = 2.0 class V2SignInWindow(ba.Window): """A window allowing signing in to a v2 account.""" def __init__(self, origin_widget: ba.Widget): - from ba.internal import is_browser_likely_available - logincode = '1412345' - address = ( - f'{_ba.get_master_server_address(version=2)}?login={logincode}') self._width = 600 self._height = 500 + self._proxyid: Optional[str] = None + self._proxykey: Optional[str] = None + uiscale = ba.app.ui.uiscale super().__init__(root_widget=ba.containerwidget( size=(self._width, self._height), @@ -31,6 +36,53 @@ class V2SignInWindow(ba.Window): scale=(1.25 if uiscale is ba.UIScale.SMALL else 1.0 if uiscale is ba.UIScale.MEDIUM else 0.85))) + self._loading_text = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + h_align='center', + v_align='center', + size=(0, 0), + maxwidth=0.9 * self._width, + text=ba.Lstr(value='${A}...', + subs=[('${A}', ba.Lstr(resource='loadingText'))]), + ) + + self._cancel_button = ba.buttonwidget( + parent=self._root_widget, + position=(30, self._height - 55), + size=(130, 50), + scale=0.8, + label=ba.Lstr(resource='cancelText'), + on_activate_call=self._done, + autoselect=True, + textcolor=(0.75, 0.7, 0.8), + ) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + self._update_timer: Optional[ba.Timer] = None + + # Ask the cloud for a proxy login id. + ba.app.cloud.send_message(bacommon.cloud.LoginProxyRequestMessage(), + on_response=ba.WeakCall( + self._on_proxy_request_response)) + + def _on_proxy_request_response( + self, response: Union[bacommon.cloud.LoginProxyRequestResponse, + Exception] + ) -> None: + from ba.internal import is_browser_likely_available + + # Something went wrong. Show an error message and that's it. + if isinstance(response, Exception): + ba.textwidget( + edit=self._loading_text, + text=ba.Lstr(resource='internal.unavailableNoConnectionText'), + color=(1, 0, 0)) + return + + # Show link(s) the user can use to log in. + address = _ba.get_master_server_address(version=2) + response.url ba.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 85), @@ -65,22 +117,6 @@ class V2SignInWindow(ba.Window): v_align='center') qroffs = 20.0 - self._cancel_button = ba.buttonwidget( - parent=self._root_widget, - position=(30, self._height - 55), - size=(130, 50), - scale=0.8, - label=ba.Lstr(resource='cancelText'), - # color=(0.6, 0.5, 0.6), - on_activate_call=self._done, - autoselect=True, - textcolor=(0.75, 0.7, 0.8), - # icon=ba.gettexture('crossOut'), - # iconscale=1.2 - ) - ba.containerwidget(edit=self._root_widget, - cancel_button=self._cancel_button) - qr_size = 270 ba.imagewidget(parent=self._root_widget, position=(self._width * 0.5 - qr_size * 0.5, @@ -88,5 +124,68 @@ class V2SignInWindow(ba.Window): size=(qr_size, qr_size), texture=_ba.get_qrcode_texture(address)) + # Start querying for results. + self._proxyid = response.proxyid + self._proxykey = response.proxykey + ba.timer(STATUS_CHECK_INTERVAL_SECONDS, + ba.WeakCall(self._ask_for_status)) + + def _ask_for_status(self) -> None: + assert self._proxyid is not None + assert self._proxykey is not None + ba.app.cloud.send_message(bacommon.cloud.LoginProxyStateQueryMessage( + proxyid=self._proxyid, proxykey=self._proxykey), + on_response=ba.WeakCall(self._got_status)) + + def _got_status( + self, response: Union[bacommon.cloud.LoginProxyStateQueryResponse, + Exception] + ) -> None: + + # For now, if anything goes wrong on the server-side, just abort + # with a vague error message. Can be more verbose later if need be. + if (isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse) + and response.state is response.State.FAIL): + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) + self._done() + return + + # If we got a token, set ourself as signed in. Hooray! + if (isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse) + and response.state is response.State.SUCCESS): + assert response.credentials is not None + ba.app.accounts_v2.set_primary_credentials(response.credentials) + + # As a courtesy, tell the server we're done with this proxy + # so it can clean up (not a huge deal if this fails) + assert self._proxyid is not None + try: + ba.app.cloud.send_message( + bacommon.cloud.LoginProxyCompleteMessage( + proxyid=self._proxyid), + on_response=ba.WeakCall(self._proxy_complete_response)) + except CommunicationError: + pass + except Exception: + logging.warning( + 'Unexpected error sending login-proxy-complete message', + exc_info=True) + + self._done() + return + + # If we're still waiting, ask again soon. + if (isinstance(response, Exception) + or response.state is response.State.WAITING): + ba.timer(STATUS_CHECK_INTERVAL_SECONDS, + ba.WeakCall(self._ask_for_status)) + + def _proxy_complete_response(self, response: Union[None, + Exception]) -> None: + del response # Not used. + # We could do something smart like retry on exceptions here, but + # this isn't critical so we'll just let anything slide. + def _done(self) -> None: ba.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/assets/src/ba_data/python/bastd/ui/account/viewer.py b/assets/src/ba_data/python/bastd/ui/account/viewer.py index 01f18c14..6464da20 100644 --- a/assets/src/ba_data/python/bastd/ui/account/viewer.py +++ b/assets/src/ba_data/python/bastd/ui/account/viewer.py @@ -91,7 +91,7 @@ class AccountViewerWindow(popup.PopupWindow): # In cases where the user most likely has a browser/email, lets # offer a 'report this user' button. - if (is_browser_likely_available() and _ba.get_account_misc_read_val( + if (is_browser_likely_available() and _ba.get_v1_account_misc_read_val( 'showAccountExtrasMenu', False)): self._extras_menu_button = ba.buttonwidget( diff --git a/assets/src/ba_data/python/bastd/ui/appinvite.py b/assets/src/ba_data/python/bastd/ui/appinvite.py index 9d3cb384..262a809f 100644 --- a/assets/src/ba_data/python/bastd/ui/appinvite.py +++ b/assets/src/ba_data/python/bastd/ui/appinvite.py @@ -60,15 +60,14 @@ class AppInviteWindow(ba.Window): resource='gatherWindow.earnTicketsForRecommendingAmountText', fallback_resource=( 'gatherWindow.earnTicketsForRecommendingText'), - subs=[ - ('${COUNT}', - str(_ba.get_account_misc_read_val('friendTryTickets', - 300))), - ('${YOU_COUNT}', - str( - _ba.get_account_misc_read_val('friendTryAwardTickets', - 100))) - ])) + subs=[('${COUNT}', + str( + _ba.get_v1_account_misc_read_val( + 'friendTryTickets', 300))), + ('${YOU_COUNT}', + str( + _ba.get_v1_account_misc_read_val( + 'friendTryAwardTickets', 100)))])) or_text = ba.Lstr(resource='orText', subs=[('${A}', ''), @@ -129,17 +128,18 @@ class AppInviteWindow(ba.Window): ba.playsound(ba.getsound('error')) return - if _ba.get_account_state() == 'signed_in': + if _ba.get_v1_account_state() == 'signed_in': ba.set_analytics_screen('App Invite UI') _ba.show_app_invite( ba.Lstr(resource='gatherWindow.appInviteTitleText', subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) ]).evaluate(), ba.Lstr(resource='gatherWindow.appInviteMessageText', - subs=[('${COUNT}', str(self._data['tickets'])), - ('${NAME}', _ba.get_account_name().split()[0]), - ('${APP_NAME}', ba.Lstr(resource='titleText')) - ]).evaluate(), self._data['code']) + subs=[ + ('${COUNT}', str(self._data['tickets'])), + ('${NAME}', _ba.get_v1_account_name().split()[0]), + ('${APP_NAME}', ba.Lstr(resource='titleText')) + ]).evaluate(), self._data['code']) else: ba.playsound(ba.getsound('error')) @@ -256,7 +256,7 @@ class ShowFriendCodeWindow(ba.Window): ]).evaluate(), ba.Lstr(resource='gatherWindow.appInviteMessageText', subs=[('${COUNT}', str(self._data['tickets'])), - ('${NAME}', _ba.get_account_name().split()[0]), + ('${NAME}', _ba.get_v1_account_name().split()[0]), ('${APP_NAME}', ba.Lstr(resource='titleText')) ]).evaluate(), self._data['code']) @@ -264,7 +264,7 @@ class ShowFriendCodeWindow(ba.Window): import urllib.parse # If somehow we got signed out. - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) @@ -273,7 +273,7 @@ class ShowFriendCodeWindow(ba.Window): ba.set_analytics_screen('Email Friend Code') subject = (ba.Lstr(resource='gatherWindow.friendHasSentPromoCodeText'). evaluate().replace( - '${NAME}', _ba.get_account_name()).replace( + '${NAME}', _ba.get_v1_account_name()).replace( '${APP_NAME}', ba.Lstr(resource='titleText').evaluate()).replace( '${COUNT}', str(self._data['tickets']))) @@ -304,7 +304,7 @@ def handle_app_invites_press(force_code: bool = False) -> None: """(internal)""" app = ba.app do_app_invites = (app.platform == 'android' and app.subplatform == 'google' - and _ba.get_account_misc_read_val( + and _ba.get_v1_account_misc_read_val( 'enableAppInvites', False) and not app.on_tv) if force_code: do_app_invites = False diff --git a/assets/src/ba_data/python/bastd/ui/characterpicker.py b/assets/src/ba_data/python/bastd/ui/characterpicker.py index b60a4767..9fc9bacc 100644 --- a/assets/src/ba_data/python/bastd/ui/characterpicker.py +++ b/assets/src/ba_data/python/bastd/ui/characterpicker.py @@ -156,7 +156,7 @@ class CharacterPicker(popup.PopupWindow): def _on_store_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/colorpicker.py b/assets/src/ba_data/python/bastd/ui/colorpicker.py index 800a3727..3f12ed02 100644 --- a/assets/src/ba_data/python/bastd/ui/colorpicker.py +++ b/assets/src/ba_data/python/bastd/ui/colorpicker.py @@ -94,7 +94,7 @@ class ColorPicker(PopupWindow): on_activate_call=ba.WeakCall(self._select_other)) # Custom colors are limited to pro currently. - if not ba.app.accounts.have_pro(): + if not ba.app.accounts_v1.have_pro(): ba.imagewidget(parent=self.root_widget, position=(50, 12), size=(30, 30), @@ -118,7 +118,7 @@ class ColorPicker(PopupWindow): from bastd.ui import purchase # Requires pro. - if not ba.app.accounts.have_pro(): + if not ba.app.accounts_v1.have_pro(): purchase.PurchaseWindow(items=['pro']) self._transition_out() return diff --git a/assets/src/ba_data/python/bastd/ui/continues.py b/assets/src/ba_data/python/bastd/ui/continues.py index d5644a1a..2635d90a 100644 --- a/assets/src/ba_data/python/bastd/ui/continues.py +++ b/assets/src/ba_data/python/bastd/ui/continues.py @@ -142,9 +142,9 @@ class ContinuesWindow(ba.Window): self._on_cancel() return - if _ba.get_account_state() == 'signed_in': + if _ba.get_v1_account_state() == 'signed_in': sval = (ba.charstr(ba.SpecialChar.TICKET) + - str(_ba.get_account_ticket_count())) + str(_ba.get_v1_account_ticket_count())) else: sval = '?' if self._tickets_text is not None: @@ -176,14 +176,14 @@ class ContinuesWindow(ba.Window): ba.playsound(ba.getsound('error')) else: # If somehow we got signed out... - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) return # If it appears we don't have enough tickets, offer to buy more. - tickets = _ba.get_account_ticket_count() + tickets = _ba.get_v1_account_ticket_count() if tickets < self._cost: # FIXME: Should we start the timer back up again after? self._counting_down = False diff --git a/assets/src/ba_data/python/bastd/ui/coop/browser.py b/assets/src/ba_data/python/bastd/ui/coop/browser.py index ca33075a..d118b503 100644 --- a/assets/src/ba_data/python/bastd/ui/coop/browser.py +++ b/assets/src/ba_data/python/bastd/ui/coop/browser.py @@ -92,7 +92,7 @@ class CoopBrowserWindow(ba.Window): self._tourney_data_up_to_date = False - self._campaign_difficulty = _ba.get_account_misc_val( + self._campaign_difficulty = _ba.get_v1_account_misc_val( 'campaignDifficulty', 'easy') super().__init__(root_widget=ba.containerwidget( @@ -235,7 +235,7 @@ class CoopBrowserWindow(ba.Window): self._subcontainer: Optional[ba.Widget] = None # Take note of our account state; we'll refresh later if this changes. - self._account_state_num = _ba.get_account_state_num() + self._account_state_num = _ba.get_v1_account_state_num() # Same for fg/bg state. self._fg_state = app.fg_state @@ -251,14 +251,14 @@ class CoopBrowserWindow(ba.Window): # If we've got a cached tournament list for our account and info for # each one of those tournaments, go ahead and display it as a # starting point. - if (app.accounts.account_tournament_list is not None - and app.accounts.account_tournament_list[0] - == _ba.get_account_state_num() - and all(t_id in app.accounts.tournament_info - for t_id in app.accounts.account_tournament_list[1])): + if (app.accounts_v1.account_tournament_list is not None + and app.accounts_v1.account_tournament_list[0] + == _ba.get_v1_account_state_num() and all( + t_id in app.accounts_v1.tournament_info + for t_id in app.accounts_v1.account_tournament_list[1])): tourney_data = [ - app.accounts.tournament_info[t_id] - for t_id in app.accounts.account_tournament_list[1] + app.accounts_v1.tournament_info[t_id] + for t_id in app.accounts_v1.account_tournament_list[1] ] self._update_for_data(tourney_data) @@ -300,7 +300,7 @@ class CoopBrowserWindow(ba.Window): self._tourney_data_up_to_date = False # If our account state has changed, do a full request. - account_state_num = _ba.get_account_state_num() + account_state_num = _ba.get_v1_account_state_num() if account_state_num != self._account_state_num: self._account_state_num = account_state_num self._save_state() @@ -358,7 +358,7 @@ class CoopBrowserWindow(ba.Window): try: ba.imagewidget( edit=self._hard_button_lock_image, - opacity=0.0 if ba.app.accounts.have_pro_options() else 1.0) + opacity=0.0 if ba.app.accounts_v1.have_pro_options() else 1.0) except Exception: ba.print_exception('Error updating campaign lock.') @@ -480,7 +480,7 @@ class CoopBrowserWindow(ba.Window): tbtn['required_league'] = (None if 'requiredLeague' not in entry else entry['requiredLeague']) - game = ba.app.accounts.tournament_info[ + game = ba.app.accounts_v1.tournament_info[ tbtn['tournament_id']]['game'] if game is None: @@ -491,7 +491,7 @@ class CoopBrowserWindow(ba.Window): else: campaignname, levelname = game.split(':') campaign = getcampaign(campaignname) - max_players = ba.app.accounts.tournament_info[ + max_players = ba.app.accounts_v1.tournament_info[ tbtn['tournament_id']]['maxPlayers'] txt = ba.Lstr( value='${A} ${B}', @@ -525,7 +525,7 @@ class CoopBrowserWindow(ba.Window): tbtn['allow_ads'] = allow_ads = entry['allowAds'] final_fee: Optional[int] = (None if fee_var is None else - _ba.get_account_misc_read_val( + _ba.get_v1_account_misc_read_val( fee_var, '?')) final_fee_str: Union[str, ba.Lstr] @@ -540,9 +540,9 @@ class CoopBrowserWindow(ba.Window): ba.charstr(ba.SpecialChar.TICKET_BACKING) + str(final_fee)) - ad_tries_remaining = ba.app.accounts.tournament_info[ + ad_tries_remaining = ba.app.accounts_v1.tournament_info[ tbtn['tournament_id']]['adTriesRemaining'] - free_tries_remaining = ba.app.accounts.tournament_info[ + free_tries_remaining = ba.app.accounts_v1.tournament_info[ tbtn['tournament_id']]['freeTriesRemaining'] # Now, if this fee allows ads and we support video ads, show @@ -592,7 +592,7 @@ class CoopBrowserWindow(ba.Window): def _on_tournament_query_response(self, data: Optional[dict[str, Any]]) -> None: - accounts = ba.app.accounts + accounts = ba.app.accounts_v1 if data is not None: tournament_data = data['t'] # This used to be the whole payload. self._last_tournament_query_response_time = ba.time( @@ -606,9 +606,11 @@ class CoopBrowserWindow(ba.Window): accounts.cache_tournament_info(tournament_data) # Also cache the current tourney list/order for this account. - accounts.account_tournament_list = (_ba.get_account_state_num(), [ - e['tournamentID'] for e in tournament_data - ]) + accounts.account_tournament_list = (_ba.get_v1_account_state_num(), + [ + e['tournamentID'] + for e in tournament_data + ]) self._doing_tournament_query = False self._update_for_data(tournament_data) @@ -617,7 +619,8 @@ class CoopBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow if difficulty != self._campaign_difficulty: - if difficulty == 'hard' and not ba.app.accounts.have_pro_options(): + if (difficulty == 'hard' + and not ba.app.accounts_v1.have_pro_options()): PurchaseWindow(items=['pro']) return ba.playsound(ba.getsound('gunCocking')) @@ -872,7 +875,7 @@ class CoopBrowserWindow(ba.Window): # no tournaments). if self._tournament_button_count == 0: unavailable_text = ba.Lstr(resource='unavailableText') - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': unavailable_text = ba.Lstr( value='${A} (${B})', subs=[('${A}', unavailable_text), @@ -943,7 +946,7 @@ class CoopBrowserWindow(ba.Window): ] # Show easter-egg-hunt either if its easter or we own it. - if _ba.get_account_misc_read_val( + if _ba.get_v1_account_misc_read_val( 'easter', False) or _ba.get_purchased('games.easter_egg_hunt'): items = [ 'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt' @@ -1346,7 +1349,7 @@ class CoopBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt from bastd.ui.league.rankwindow import LeagueRankWindow - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() @@ -1363,7 +1366,7 @@ class CoopBrowserWindow(ba.Window): ) -> None: # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() @@ -1427,7 +1430,7 @@ class CoopBrowserWindow(ba.Window): # Do a bit of pre-flight for tournament options. if tournament_button is not None: - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return @@ -1465,7 +1468,7 @@ class CoopBrowserWindow(ba.Window): return # Game is whatever the tournament tells us it is. - game = ba.app.accounts.tournament_info[ + game = ba.app.accounts_v1.tournament_info[ tournament_button['tournament_id']]['game'] if tournament_button is None and game == 'Easy:The Last Stand': @@ -1481,8 +1484,8 @@ class CoopBrowserWindow(ba.Window): if tournament_button is None and game in ( 'Challenges:Infinite Runaround', 'Challenges:Infinite Onslaught' - ) and not ba.app.accounts.have_pro(): - if _ba.get_account_state() != 'signed_in': + ) and not ba.app.accounts_v1.have_pro(): + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow(items=['pro']) @@ -1508,7 +1511,7 @@ class CoopBrowserWindow(ba.Window): if (tournament_button is None and required_purchase is not None and not _ba.get_purchased(required_purchase)): - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow(items=[required_purchase]) diff --git a/assets/src/ba_data/python/bastd/ui/coop/gamebutton.py b/assets/src/ba_data/python/bastd/ui/coop/gamebutton.py index 2a6a75b4..026f4cef 100644 --- a/assets/src/ba_data/python/bastd/ui/coop/gamebutton.py +++ b/assets/src/ba_data/python/bastd/ui/coop/gamebutton.py @@ -199,7 +199,7 @@ class GameButton: # Hard-code games we haven't unlocked. if ((game in ('Challenges:Infinite Runaround', 'Challenges:Infinite Onslaught') - and not ba.app.accounts.have_pro()) + and not ba.app.accounts_v1.have_pro()) or (game in ('Challenges:Meteor Shower', ) and not _ba.get_purchased('games.meteor_shower')) or (game in ('Challenges:Target Practice', 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 8dca6cc5..c8de2ced 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/__init__.py +++ b/assets/src/ba_data/python/bastd/ui/gather/__init__.py @@ -151,7 +151,7 @@ class GatherWindow(ba.Window): tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [ (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText')) ] - if _ba.get_account_misc_read_val('enablePublicParties', True): + if _ba.get_v1_account_misc_read_val('enablePublicParties', True): tabdefs.append((self.TabID.INTERNET, ba.Lstr(resource=self._r + '.publicText'))) tabdefs.append( diff --git a/assets/src/ba_data/python/bastd/ui/gather/abouttab.py b/assets/src/ba_data/python/bastd/ui/gather/abouttab.py index f185b90b..87e09377 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/abouttab.py +++ b/assets/src/ba_data/python/bastd/ui/gather/abouttab.py @@ -52,7 +52,8 @@ class AboutGatherTab(GatherTab): include_invite = True msc_scale = 1.1 c_height_2 = min(region_height, string_height * msc_scale + 100) - try_tickets = _ba.get_account_misc_read_val('friendTryTickets', None) + try_tickets = _ba.get_v1_account_misc_read_val('friendTryTickets', + None) if try_tickets is None: include_invite = False self._container = ba.containerwidget( @@ -106,7 +107,7 @@ class AboutGatherTab(GatherTab): def _invite_to_try_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.appinvite import handle_app_invites_press - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return handle_app_invites_press() diff --git a/assets/src/ba_data/python/bastd/ui/gather/privatetab.py b/assets/src/ba_data/python/bastd/ui/gather/privatetab.py index 26633766..b1d047ff 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/privatetab.py +++ b/assets/src/ba_data/python/bastd/ui/gather/privatetab.py @@ -225,7 +225,7 @@ class PrivateGatherTab(GatherTab): def _update_currency_ui(self) -> None: # Keep currency count up to date if applicable. try: - t_str = str(_ba.get_account_ticket_count()) + t_str = str(_ba.get_v1_account_ticket_count()) except Exception: t_str = '?' if self._get_tickets_button: @@ -245,7 +245,7 @@ class PrivateGatherTab(GatherTab): if self._state.sub_tab is SubTabType.HOST: # If we're not signed in, just refresh to show that. - if (_ba.get_account_state() != 'signed_in' + if (_ba.get_v1_account_state() != 'signed_in' and self._showing_not_signed_in_screen): self._refresh_sub_tab() else: @@ -254,7 +254,7 @@ class PrivateGatherTab(GatherTab): if (self._last_hosting_state_query_time is None or now - self._last_hosting_state_query_time > 15.0): self._debug_server_comm('querying private party state') - if _ba.get_account_state() == 'signed_in': + if _ba.get_v1_account_state() == 'signed_in': _ba.add_transaction( { 'type': 'PRIVATE_PARTY_QUERY', @@ -437,7 +437,7 @@ class PrivateGatherTab(GatherTab): # pylint: disable=too-many-branches # pylint: disable=too-many-statements - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': ba.textwidget(parent=self._container, size=(0, 0), h_align='center', @@ -776,7 +776,7 @@ class PrivateGatherTab(GatherTab): or self._waiting_for_initial_state): return - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInErrorText')) ba.playsound(ba.getsound('error')) self._refresh_sub_tab() @@ -795,7 +795,7 @@ class PrivateGatherTab(GatherTab): if self._hostingstate.tickets_to_host_now > 0: ticket_count: Optional[int] try: - ticket_count = _ba.get_account_ticket_count() + ticket_count = _ba.get_v1_account_ticket_count() except Exception: # FIXME: should add a ba.NotSignedInError we can use here. ticket_count = None diff --git a/assets/src/ba_data/python/bastd/ui/gather/publictab.py b/assets/src/ba_data/python/bastd/ui/gather/publictab.py index 2edb9954..0261cef5 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/publictab.py +++ b/assets/src/ba_data/python/bastd/ui/gather/publictab.py @@ -88,8 +88,8 @@ class UIRow: if party.clean_display_index == index: return - ping_good = _ba.get_account_misc_read_val('pingGood', 100) - ping_med = _ba.get_account_misc_read_val('pingMed', 500) + ping_good = _ba.get_v1_account_misc_read_val('pingGood', 100) + ping_med = _ba.get_v1_account_misc_read_val('pingMed', 500) self._clear() hpos = 20 @@ -122,8 +122,8 @@ class UIRow: if party.stats_addr: url = party.stats_addr.replace( '${ACCOUNT}', - _ba.get_account_misc_read_val_2('resolvedAccountID', - 'UNKNOWN')) + _ba.get_v1_account_misc_read_val_2('resolvedAccountID', + 'UNKNOWN')) self._stats_button = ba.buttonwidget( color=(0.3, 0.6, 0.94), textcolor=(1.0, 1.0, 1.0), @@ -793,7 +793,7 @@ class PublicGatherTab(GatherTab): self._process_pending_party_infos() # Anytime we sign in/out, make sure we refresh our list. - signed_in = _ba.get_account_state() == 'signed_in' + signed_in = _ba.get_v1_account_state() == 'signed_in' if self._signed_in != signed_in: self._signed_in = signed_in self._party_lists_dirty = True @@ -986,7 +986,7 @@ class PublicGatherTab(GatherTab): p[1].index)) # If signed out or errored, show no parties. - if (_ba.get_account_state() != 'signed_in' + if (_ba.get_v1_account_state() != 'signed_in' or not self._have_valid_server_list): self._parties_displayed = {} else: @@ -1023,11 +1023,11 @@ class PublicGatherTab(GatherTab): # Fire off a new public-party query periodically. if (self._last_server_list_query_time is None or now - self._last_server_list_query_time > 0.001 * - _ba.get_account_misc_read_val('pubPartyRefreshMS', 10000)): + _ba.get_v1_account_misc_read_val('pubPartyRefreshMS', 10000)): self._last_server_list_query_time = now if DEBUG_SERVER_COMMUNICATION: print('REQUESTING SERVER LIST') - if _ba.get_account_state() == 'signed_in': + if _ba.get_v1_account_state() == 'signed_in': _ba.add_transaction( { 'type': 'PUBLIC_PARTY_QUERY', @@ -1156,7 +1156,7 @@ class PublicGatherTab(GatherTab): def _on_start_advertizing_press(self) -> None: from bastd.ui.account import show_sign_in_prompt - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return diff --git a/assets/src/ba_data/python/bastd/ui/getcurrency.py b/assets/src/ba_data/python/bastd/ui/getcurrency.py index d2674060..eafe89bc 100644 --- a/assets/src/ba_data/python/bastd/ui/getcurrency.py +++ b/assets/src/ba_data/python/bastd/ui/getcurrency.py @@ -179,22 +179,27 @@ class GetCurrencyWindow(ba.Window): c2txt = ba.Lstr( resource=rsrc, subs=[('${COUNT}', - str(_ba.get_account_misc_read_val('tickets2Amount', 500)))]) + str(_ba.get_v1_account_misc_read_val('tickets2Amount', + 500)))]) c3txt = ba.Lstr( resource=rsrc, - subs=[('${COUNT}', - str(_ba.get_account_misc_read_val('tickets3Amount', - 1500)))]) + subs=[ + ('${COUNT}', + str(_ba.get_v1_account_misc_read_val('tickets3Amount', 1500))) + ]) c4txt = ba.Lstr( resource=rsrc, - subs=[('${COUNT}', - str(_ba.get_account_misc_read_val('tickets4Amount', - 5000)))]) + subs=[ + ('${COUNT}', + str(_ba.get_v1_account_misc_read_val('tickets4Amount', 5000))) + ]) c5txt = ba.Lstr( resource=rsrc, - subs=[('${COUNT}', - str(_ba.get_account_misc_read_val('tickets5Amount', - 15000)))]) + subs=[ + ('${COUNT}', + str(_ba.get_v1_account_misc_read_val('tickets5Amount', + 15000))) + ]) h = 110.0 @@ -261,7 +266,7 @@ class GetCurrencyWindow(ba.Window): label=ba.Lstr(resource=self._r + '.ticketsFromASponsorText', subs=[('${COUNT}', str( - _ba.get_account_misc_read_val( + _ba.get_v1_account_misc_read_val( 'sponsorTickets', 5)))]), tex_name='ticketsMore', enabled=self._enable_ad_button, @@ -301,11 +306,10 @@ class GetCurrencyWindow(ba.Window): size=b_size_3, label=ba.Lstr( resource='gatherWindow.earnTicketsForRecommendingText', - subs=[ - ('${COUNT}', - str(_ba.get_account_misc_read_val( - 'sponsorTickets', 5))) - ]), + subs=[('${COUNT}', + str( + _ba.get_v1_account_misc_read_val( + 'sponsorTickets', 5)))]), tex_name='ticketsMore', enabled=True, tex_opacity=0.6, @@ -427,16 +431,16 @@ class GetCurrencyWindow(ba.Window): import datetime # if we somehow get signed out, just die.. - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': self._back() return - self._ticket_count = _ba.get_account_ticket_count() + self._ticket_count = _ba.get_v1_account_ticket_count() # update our incentivized ad button depending on whether ads are # available if self._ad_button is not None: - next_reward_ad_time = _ba.get_account_misc_read_val_2( + next_reward_ad_time = _ba.get_v1_account_misc_read_val_2( 'nextRewardAdTime', None) if next_reward_ad_time is not None: next_reward_ad_time = datetime.datetime.utcfromtimestamp( @@ -494,8 +498,9 @@ class GetCurrencyWindow(ba.Window): app = ba.app if ((app.test_build or (app.platform == 'android' - and app.subplatform in ['oculus', 'cardboard'])) and - _ba.get_account_misc_read_val('allowAccountLinking2', False)): + and app.subplatform in ['oculus', 'cardboard'])) + and _ba.get_v1_account_misc_read_val('allowAccountLinking2', + False)): ba.screenmessage(ba.Lstr(resource=self._r + '.unavailableLinkAccountText'), color=(1, 0.5, 0)) @@ -509,7 +514,7 @@ class GetCurrencyWindow(ba.Window): from bastd.ui import appinvite from ba.internal import master_server_get if item == 'app_invite': - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return appinvite.handle_app_invites_press() @@ -554,7 +559,7 @@ class GetCurrencyWindow(ba.Window): if item == 'ad': import datetime # if ads are disabled until some time, error.. - next_reward_ad_time = _ba.get_account_misc_read_val_2( + next_reward_ad_time = _ba.get_v1_account_misc_read_val_2( 'nextRewardAdTime', None) if next_reward_ad_time is not None: next_reward_ad_time = datetime.datetime.utcfromtimestamp( diff --git a/assets/src/ba_data/python/bastd/ui/iconpicker.py b/assets/src/ba_data/python/bastd/ui/iconpicker.py index 6c6f07db..a9d5727c 100644 --- a/assets/src/ba_data/python/bastd/ui/iconpicker.py +++ b/assets/src/ba_data/python/bastd/ui/iconpicker.py @@ -40,7 +40,7 @@ class IconPicker(popup.PopupWindow): self._transitioning_out = False self._icons = [ba.charstr(ba.SpecialChar.LOGO) - ] + ba.app.accounts.get_purchased_icons() + ] + ba.app.accounts_v1.get_purchased_icons() count = len(self._icons) columns = 4 rows = int(math.ceil(float(count) / columns)) @@ -137,7 +137,7 @@ class IconPicker(popup.PopupWindow): def _on_store_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/kiosk.py b/assets/src/ba_data/python/bastd/ui/kiosk.py index c75caeca..09f92aa9 100644 --- a/assets/src/ba_data/python/bastd/ui/kiosk.py +++ b/assets/src/ba_data/python/bastd/ui/kiosk.py @@ -360,7 +360,7 @@ class KioskWindow(ba.Window): def _update(self) -> None: # Kiosk-mode is designed to be used signed-out... try for force # the issue. - if _ba.get_account_state() == 'signed_in': + if _ba.get_v1_account_state() == 'signed_in': # _bs.sign_out() # FIXME: Try to delete player profiles here too. pass diff --git a/assets/src/ba_data/python/bastd/ui/league/rankbutton.py b/assets/src/ba_data/python/bastd/ui/league/rankbutton.py index d7ab53e2..3a6ffa54 100644 --- a/assets/src/ba_data/python/bastd/ui/league/rankbutton.py +++ b/assets/src/ba_data/python/bastd/ui/league/rankbutton.py @@ -94,7 +94,7 @@ class LeagueRankButton: self._smooth_update_timer: Optional[ba.Timer] = None # Take note of our account state; we'll refresh later if this changes. - self._account_state_num = _ba.get_account_state_num() + self._account_state_num = _ba.get_v1_account_state_num() self._last_power_ranking_query_time: Optional[float] = None self._doing_power_ranking_query = False self.set_position(position) @@ -106,7 +106,7 @@ class LeagueRankButton: self._update() # If we've got cached power-ranking data already, apply it. - data = ba.app.accounts.get_cached_league_rank_data() + data = ba.app.accounts_v1.get_cached_league_rank_data() if data is not None: self._update_for_league_rank_data(data) @@ -224,7 +224,7 @@ class LeagueRankButton: in_top = data is not None and data['rank'] is not None do_percent = False - if data is None or _ba.get_account_state() != 'signed_in': + if data is None or _ba.get_v1_account_state() != 'signed_in': self._percent = self._rank = None status_text = '-' elif in_top: @@ -248,7 +248,8 @@ class LeagueRankButton: self._percent = self._rank = None status_text = '-' else: - our_points = ba.app.accounts.get_league_rank_points(data) + our_points = ba.app.accounts_v1.get_league_rank_points( + data) progress = float(our_points) / data['scores'][-1][1] self._percent = int(progress * 100.0) self._rank = None @@ -327,14 +328,14 @@ class LeagueRankButton: def _on_power_ranking_query_response( self, data: Optional[dict[str, Any]]) -> None: self._doing_power_ranking_query = False - ba.app.accounts.cache_league_rank_data(data) + ba.app.accounts_v1.cache_league_rank_data(data) self._update_for_league_rank_data(data) def _update(self) -> None: cur_time = ba.time(ba.TimeType.REAL) # If our account state has changed, refresh our UI. - account_state_num = _ba.get_account_state_num() + account_state_num = _ba.get_v1_account_state_num() if account_state_num != self._account_state_num: self._account_state_num = account_state_num diff --git a/assets/src/ba_data/python/bastd/ui/league/rankwindow.py b/assets/src/ba_data/python/bastd/ui/league/rankwindow.py index d1e31c46..e1828adc 100644 --- a/assets/src/ba_data/python/bastd/ui/league/rankwindow.py +++ b/assets/src/ba_data/python/bastd/ui/league/rankwindow.py @@ -118,13 +118,13 @@ class LeagueRankWindow(ba.Window): self._season: Optional[str] = None # take note of our account state; we'll refresh later if this changes - self._account_state = _ba.get_account_state() + self._account_state = _ba.get_v1_account_state() self._refresh() self._restore_state() # if we've got cached power-ranking data already, display it - info = ba.app.accounts.get_cached_league_rank_data() + info = ba.app.accounts_v1.get_cached_league_rank_data() if info is not None: self._update_for_league_rank_data(info) @@ -155,7 +155,8 @@ class LeagueRankWindow(ba.Window): resource='coopSelectWindow.activenessAllTimeInfoText' if self._season == 'a' else 'coopSelectWindow.activenessInfoText', subs=[('${MAX}', - str(_ba.get_account_misc_read_val('activenessMax', 1.0)))]) + str(_ba.get_v1_account_misc_read_val('activenessMax', + 1.0)))]) confirm.ConfirmWindow(txt, cancel_button=False, width=460, @@ -164,17 +165,15 @@ class LeagueRankWindow(ba.Window): def _on_pro_mult_press(self) -> None: from bastd.ui import confirm - txt = ba.Lstr( - resource='coopSelectWindow.proMultInfoText', - subs=[ - ('${PERCENT}', - str(_ba.get_account_misc_read_val('proPowerRankingBoost', - 10))), - ('${PRO}', - ba.Lstr(resource='store.bombSquadProNameText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) - ])) - ]) + txt = ba.Lstr(resource='coopSelectWindow.proMultInfoText', + subs=[('${PERCENT}', + str( + _ba.get_v1_account_misc_read_val( + 'proPowerRankingBoost', 10))), + ('${PRO}', + ba.Lstr(resource='store.bombSquadProNameText', + subs=[('${APP_NAME}', + ba.Lstr(resource='titleText'))]))]) confirm.ConfirmWindow(txt, cancel_button=False, width=460, @@ -196,7 +195,7 @@ class LeagueRankWindow(ba.Window): self._doing_power_ranking_query = False # important: *only* cache this if we requested the current season.. if data is not None and data.get('s', None) is None: - ba.app.accounts.cache_league_rank_data(data) + ba.app.accounts_v1.cache_league_rank_data(data) # always store a copy locally though (even for other seasons) self._league_rank_data = copy.deepcopy(data) self._update_for_league_rank_data(data) @@ -209,7 +208,7 @@ class LeagueRankWindow(ba.Window): cur_time = ba.time(ba.TimeType.REAL) # if our account state has changed, refresh our UI - account_state = _ba.get_account_state() + account_state = _ba.get_v1_account_state() if account_state != self._account_state: self._account_state = account_state self._save_state() @@ -353,7 +352,7 @@ class LeagueRankWindow(ba.Window): maxwidth=200) self._activity_mult_button: Optional[ba.Widget] - if _ba.get_account_misc_read_val('act', False): + if _ba.get_v1_account_misc_read_val('act', False): self._activity_mult_button = ba.buttonwidget( parent=w_parent, position=(h2 - 60, v2 + 10), @@ -594,7 +593,7 @@ class LeagueRankWindow(ba.Window): # pylint: disable=too-many-locals if not self._root_widget: return - accounts = ba.app.accounts + accounts = ba.app.accounts_v1 in_top = (data is not None and data['rank'] is not None) eq_text = self._rdict.powerRankingPointsEqualsText pts_txt = self._rdict.powerRankingPointsText @@ -603,7 +602,7 @@ class LeagueRankWindow(ba.Window): finished_season_unranked = False self._can_do_more_button = True extra_text = '' - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': status_text = '(' + ba.Lstr( resource='notSignedInText').evaluate() + ')' elif in_top: @@ -790,7 +789,8 @@ class LeagueRankWindow(ba.Window): have_pro = False if data is None else data['p'] pro_mult = 1.0 + float( - _ba.get_account_misc_read_val('proPowerRankingBoost', 0.0)) * 0.01 + _ba.get_v1_account_misc_read_val('proPowerRankingBoost', + 0.0)) * 0.01 # pylint: disable=consider-using-f-string ba.textwidget(edit=self._pro_mult_text, text=' -' if diff --git a/assets/src/ba_data/python/bastd/ui/mainmenu.py b/assets/src/ba_data/python/bastd/ui/mainmenu.py index f58df844..155188b2 100644 --- a/assets/src/ba_data/python/bastd/ui/mainmenu.py +++ b/assets/src/ba_data/python/bastd/ui/mainmenu.py @@ -67,9 +67,9 @@ class MainMenuWindow(ba.Window): self._restore_state() # Keep an eye on a few things and refresh if they change. - self._account_state = _ba.get_account_state() - self._account_state_num = _ba.get_account_state_num() - self._account_type = (_ba.get_account_type() + self._account_state = _ba.get_v1_account_state() + self._account_state_num = _ba.get_v1_account_state_num() + self._account_type = (_ba.get_v1_account_type() if self._account_state == 'signed_in' else None) self._refresh_timer = ba.Timer(1.0, ba.WeakCall(self._check_refresh), @@ -122,9 +122,9 @@ class MainMenuWindow(ba.Window): ba.print_exception('Error showing get-remote-app info') def _get_store_char_tex(self) -> str: - return ('storeCharacterXmas' if _ba.get_account_misc_read_val( + return ('storeCharacterXmas' if _ba.get_v1_account_misc_read_val( 'xmas', False) else - 'storeCharacterEaster' if _ba.get_account_misc_read_val( + 'storeCharacterEaster' if _ba.get_v1_account_misc_read_val( 'easter', False) else 'storeCharacter') def _check_refresh(self) -> None: @@ -138,13 +138,13 @@ class MainMenuWindow(ba.Window): return store_char_tex = self._get_store_char_tex() - account_state_num = _ba.get_account_state_num() + account_state_num = _ba.get_v1_account_state_num() if (account_state_num != self._account_state_num or store_char_tex != self._store_char_tex): self._store_char_tex = store_char_tex self._account_state_num = account_state_num - account_state = self._account_state = (_ba.get_account_state()) - self._account_type = (_ba.get_account_type() + account_state = self._account_state = (_ba.get_v1_account_state()) + self._account_type = (_ba.get_v1_account_type() if account_state == 'signed_in' else None) self._save_state() self._refresh() @@ -213,8 +213,8 @@ class MainMenuWindow(ba.Window): on_activate_call=self._settings) # Scattered eggs on easter. - if _ba.get_account_misc_read_val('easter', - False) and not self._in_game: + if _ba.get_v1_account_misc_read_val('easter', + False) and not self._in_game: icon_size = 34 ba.imagewidget(parent=self._root_widget, position=(h - icon_size * 0.5 - 15, @@ -310,7 +310,7 @@ class MainMenuWindow(ba.Window): transition_delay=self._tdelay) # Scattered eggs on easter. - if _ba.get_account_misc_read_val('easter', False): + if _ba.get_v1_account_misc_read_val('easter', False): icon_size = 30 ba.imagewidget(parent=self._root_widget, position=(h - icon_size * 0.5 + 25, @@ -427,8 +427,8 @@ class MainMenuWindow(ba.Window): self._height = 200.0 enable_account_button = True account_type_name: Union[str, ba.Lstr] - if _ba.get_account_state() == 'signed_in': - account_type_name = _ba.get_account_display_string() + if _ba.get_v1_account_state() == 'signed_in': + account_type_name = _ba.get_v1_account_display_string() account_type_icon = None account_textcolor = (1.0, 1.0, 1.0) else: @@ -618,8 +618,8 @@ class MainMenuWindow(ba.Window): enable_sound=account_type_enable_button_sound) # Scattered eggs on easter. - if _ba.get_account_misc_read_val('easter', - False) and not self._in_game: + if _ba.get_v1_account_misc_read_val('easter', + False) and not self._in_game: icon_size = 32 ba.imagewidget(parent=self._root_widget, position=(h - icon_size * 0.5 + 35, @@ -648,8 +648,8 @@ class MainMenuWindow(ba.Window): self._how_to_play_button = btn # Scattered eggs on easter. - if _ba.get_account_misc_read_val('easter', - False) and not self._in_game: + if _ba.get_v1_account_misc_read_val('easter', + False) and not self._in_game: icon_size = 28 ba.imagewidget(parent=self._root_widget, position=(h - icon_size * 0.5 + 30, @@ -851,7 +851,7 @@ class MainMenuWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.store.browser import StoreBrowserWindow from bastd.ui.account import show_sign_in_prompt - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() diff --git a/assets/src/ba_data/python/bastd/ui/partyqueue.py b/assets/src/ba_data/python/bastd/ui/partyqueue.py index 4f4cf9c0..a44f92c6 100644 --- a/assets/src/ba_data/python/bastd/ui/partyqueue.py +++ b/assets/src/ba_data/python/bastd/ui/partyqueue.py @@ -320,8 +320,8 @@ class PartyQueueWindow(ba.Window): if -1 not in self._dudes_by_id: dude = self.Dude( self, response['d'], self._initial_offset, True, - _ba.get_account_misc_read_val_2('resolvedAccountID', None), - _ba.get_account_display_string()) + _ba.get_v1_account_misc_read_val_2('resolvedAccountID', None), + _ba.get_v1_account_display_string()) self._dudes_by_id[-1] = dude self._dudes.append(dude) else: @@ -457,11 +457,11 @@ class PartyQueueWindow(ba.Window): """Boost was pressed.""" from bastd.ui import account from bastd.ui import getcurrency - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return - if _ba.get_account_ticket_count() < self._boost_tickets: + if _ba.get_v1_account_ticket_count() < self._boost_tickets: ba.playsound(ba.getsound('error')) getcurrency.show_get_tickets_prompt() return @@ -498,17 +498,17 @@ class PartyQueueWindow(ba.Window): # Update boost button color based on if we have enough moola. if self._boost_button is not None: can_boost = ( - (_ba.get_account_state() == 'signed_in' - and _ba.get_account_ticket_count() >= self._boost_tickets)) + (_ba.get_v1_account_state() == 'signed_in' + and _ba.get_v1_account_ticket_count() >= self._boost_tickets)) ba.buttonwidget(edit=self._boost_button, color=(0, 1, 0) if can_boost else (0.7, 0.7, 0.7)) # Update ticket-count. if self._tickets_text is not None: if self._boost_button is not None: - if _ba.get_account_state() == 'signed_in': + if _ba.get_v1_account_state() == 'signed_in': val = ba.charstr(ba.SpecialChar.TICKET) + str( - _ba.get_account_ticket_count()) + _ba.get_v1_account_ticket_count()) else: val = ba.charstr(ba.SpecialChar.TICKET) + '???' ba.textwidget(edit=self._tickets_text, text=val) @@ -518,7 +518,7 @@ class PartyQueueWindow(ba.Window): current_time = ba.time(ba.TimeType.REAL) if (self._last_transaction_time is None or current_time - self._last_transaction_time > - 0.001 * _ba.get_account_misc_read_val('pqInt', 5000)): + 0.001 * _ba.get_v1_account_misc_read_val('pqInt', 5000)): self._last_transaction_time = current_time _ba.add_transaction( { diff --git a/assets/src/ba_data/python/bastd/ui/play.py b/assets/src/ba_data/python/bastd/ui/play.py index d5c2b001..5fefb3e3 100644 --- a/assets/src/ba_data/python/bastd/ui/play.py +++ b/assets/src/ba_data/python/bastd/ui/play.py @@ -447,7 +447,7 @@ class PlayWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt from bastd.ui.coop.browser import CoopBrowserWindow - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() diff --git a/assets/src/ba_data/python/bastd/ui/playlist/addgame.py b/assets/src/ba_data/python/bastd/ui/playlist/addgame.py index f12f26bf..607226dd 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/addgame.py +++ b/assets/src/ba_data/python/bastd/ui/playlist/addgame.py @@ -176,7 +176,7 @@ class PlaylistAddGameWindow(ba.Window): def _on_get_more_games_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return StoreBrowserWindow(modal=True, 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 7646b3ff..7ca0b333 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/browser.py +++ b/assets/src/ba_data/python/bastd/ui/playlist/browser.py @@ -140,7 +140,7 @@ class PlaylistBrowserWindow(ba.Window): def _ensure_standard_playlists_exist(self) -> None: # On new installations, go ahead and create a few playlists # besides the hard-coded default one: - if not _ba.get_account_misc_val('madeStandardPlaylists', False): + if not _ba.get_v1_account_misc_val('madeStandardPlaylists', False): _ba.add_transaction({ 'type': 'ADD_PLAYLIST', diff --git a/assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py b/assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py index 917e65ed..e802521c 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py +++ b/assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py @@ -253,7 +253,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): self._update() def _update(self) -> None: - have = ba.app.accounts.have_pro_options() + have = ba.app.accounts_v1.have_pro_options() for lock in self._lock_images: ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0) @@ -383,7 +383,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.playlist.editcontroller import PlaylistEditController from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts.have_pro_options(): + if not ba.app.accounts_v1.have_pro_options(): PurchaseWindow(items=['pro']) return @@ -407,7 +407,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.playlist.editcontroller import PlaylistEditController from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts.have_pro_options(): + if not ba.app.accounts_v1.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_playlist_name is None: @@ -445,7 +445,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): from bastd.ui.playlist import share # Gotta be signed in for this to work. - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) @@ -472,12 +472,12 @@ class PlaylistCustomizeBrowserWindow(ba.Window): def _share_playlist(self) -> None: # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts.have_pro_options(): + if not ba.app.accounts_v1.have_pro_options(): PurchaseWindow(items=['pro']) return # Gotta be signed in for this to work. - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) @@ -508,7 +508,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow from bastd.ui.confirm import ConfirmWindow - if not ba.app.accounts.have_pro_options(): + if not ba.app.accounts_v1.have_pro_options(): PurchaseWindow(items=['pro']) return @@ -534,7 +534,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # pylint: disable=too-many-branches # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts.have_pro_options(): + if not ba.app.accounts_v1.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_playlist_name is None: diff --git a/assets/src/ba_data/python/bastd/ui/playlist/mapselect.py b/assets/src/ba_data/python/bastd/ui/playlist/mapselect.py index fc79ce76..2464e0fd 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/mapselect.py +++ b/assets/src/ba_data/python/bastd/ui/playlist/mapselect.py @@ -210,7 +210,7 @@ class PlaylistMapSelectWindow(ba.Window): def _on_store_press(self) -> None: from bastd.ui import account from bastd.ui.store.browser import StoreBrowserWindow - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return StoreBrowserWindow(modal=True, diff --git a/assets/src/ba_data/python/bastd/ui/playoptions.py b/assets/src/ba_data/python/bastd/ui/playoptions.py index e4b16d5a..2d98d5dd 100644 --- a/assets/src/ba_data/python/bastd/ui/playoptions.py +++ b/assets/src/ba_data/python/bastd/ui/playoptions.py @@ -250,7 +250,7 @@ class PlayOptionsWindow(popup.PopupWindow): autoselect=True, textcolor=(0.8, 0.8, 0.8), label=ba.Lstr(resource='teamNamesColorText')) - if not ba.app.accounts.have_pro(): + if not ba.app.accounts_v1.have_pro(): ba.imagewidget( parent=self.root_widget, size=(30, 30), @@ -348,8 +348,8 @@ class PlayOptionsWindow(popup.PopupWindow): from bastd.ui.account import show_sign_in_prompt from bastd.ui.teamnamescolors import TeamNamesColorsWindow from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts.have_pro(): - if _ba.get_account_state() != 'signed_in': + if not ba.app.accounts_v1.have_pro(): + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow(items=['pro']) diff --git a/assets/src/ba_data/python/bastd/ui/profile/browser.py b/assets/src/ba_data/python/bastd/ui/profile/browser.py index 8d0ac7e7..e9aef0b2 100644 --- a/assets/src/ba_data/python/bastd/ui/profile/browser.py +++ b/assets/src/ba_data/python/bastd/ui/profile/browser.py @@ -51,7 +51,7 @@ class ProfileBrowserWindow(ba.Window): self._r = 'playerProfilesWindow' # Ensure we've got an account-profile in cases where we're signed in. - ba.app.accounts.ensure_have_account_player_profile() + ba.app.accounts_v1.ensure_have_account_player_profile() top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 @@ -174,9 +174,9 @@ class ProfileBrowserWindow(ba.Window): from bastd.ui.purchase import PurchaseWindow # Limit to a handful profiles if they don't have pro-options. - max_non_pro_profiles = _ba.get_account_misc_read_val('mnpp', 5) + max_non_pro_profiles = _ba.get_v1_account_misc_read_val('mnpp', 5) assert self._profiles is not None - if (not ba.app.accounts.have_pro_options() + if (not ba.app.accounts_v1.have_pro_options() and len(self._profiles) >= max_non_pro_profiles): PurchaseWindow(items=['pro'], header_text=ba.Lstr( @@ -283,8 +283,8 @@ class ProfileBrowserWindow(ba.Window): items.sort(key=lambda x: asserttype(x[0], str).lower()) index = 0 account_name: Optional[str] - if _ba.get_account_state() == 'signed_in': - account_name = _ba.get_account_display_string() + if _ba.get_v1_account_state() == 'signed_in': + account_name = _ba.get_v1_account_display_string() else: account_name = None widget_to_select = None diff --git a/assets/src/ba_data/python/bastd/ui/profile/edit.py b/assets/src/ba_data/python/bastd/ui/profile/edit.py index d7df39ac..d0df1374 100644 --- a/assets/src/ba_data/python/bastd/ui/profile/edit.py +++ b/assets/src/ba_data/python/bastd/ui/profile/edit.py @@ -173,8 +173,8 @@ class EditProfileWindow(ba.Window): self._upgrade_button = None if self._is_account_profile: - if _ba.get_account_state() == 'signed_in': - sval = _ba.get_account_display_string() + if _ba.get_v1_account_state() == 'signed_in': + sval = _ba.get_v1_account_display_string() else: sval = '??' ba.textwidget(parent=self._root_widget, @@ -427,7 +427,7 @@ class EditProfileWindow(ba.Window): """Attempt to ugrade the profile to global.""" from bastd.ui import account from bastd.ui.profile import upgrade as pupgrade - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return @@ -593,8 +593,8 @@ class EditProfileWindow(ba.Window): return name = self.getname() if name == '__account__': - name = (_ba.get_account_name() - if _ba.get_account_state() == 'signed_in' else '???') + name = (_ba.get_v1_account_name() + if _ba.get_v1_account_state() == 'signed_in' else '???') if len(name) > 10 and not (self._global or self._is_account_profile): ba.textwidget(edit=self._clipped_name_text, text=ba.Lstr(resource='inGameClippedNameText', diff --git a/assets/src/ba_data/python/bastd/ui/profile/upgrade.py b/assets/src/ba_data/python/bastd/ui/profile/upgrade.py index 5d2c8f16..0dc86dd3 100644 --- a/assets/src/ba_data/python/bastd/ui/profile/upgrade.py +++ b/assets/src/ba_data/python/bastd/ui/profile/upgrade.py @@ -126,7 +126,8 @@ class ProfileUpgradeWindow(ba.Window): 'b': ba.app.build_number }, callback=ba.WeakCall(self._profile_check_result)) - self._cost = _ba.get_account_misc_read_val('price.global_profile', 500) + self._cost = _ba.get_v1_account_misc_read_val('price.global_profile', + 500) self._status: Optional[str] = 'waiting' self._update_timer = ba.Timer(1.0, ba.WeakCall(self._update), @@ -169,7 +170,7 @@ class ProfileUpgradeWindow(ba.Window): from bastd.ui import getcurrency if self._status is None: # If it appears we don't have enough tickets, offer to buy more. - tickets = _ba.get_account_ticket_count() + tickets = _ba.get_v1_account_ticket_count() if tickets < self._cost: ba.playsound(ba.getsound('error')) getcurrency.show_get_tickets_prompt() @@ -204,7 +205,7 @@ class ProfileUpgradeWindow(ba.Window): def _update(self) -> None: try: - t_str = str(_ba.get_account_ticket_count()) + t_str = str(_ba.get_v1_account_ticket_count()) except Exception: t_str = '?' if self._tickets_text is not None: diff --git a/assets/src/ba_data/python/bastd/ui/purchase.py b/assets/src/ba_data/python/bastd/ui/purchase.py index c516b43e..c1a28b11 100644 --- a/assets/src/ba_data/python/bastd/ui/purchase.py +++ b/assets/src/ba_data/python/bastd/ui/purchase.py @@ -72,7 +72,7 @@ class PurchaseWindow(ba.Window): pyoffs = -15 else: pyoffs = 0 - price = self._price = _ba.get_account_misc_read_val( + price = self._price = _ba.get_v1_account_misc_read_val( 'price.' + str(items[0]), -1) price_str = ba.charstr(ba.SpecialChar.TICKET) + str(price) self._price_text = ba.textwidget(parent=self._root_widget, @@ -118,7 +118,7 @@ class PurchaseWindow(ba.Window): # We go away if we see that our target item is owned. if self._items == ['pro']: - if ba.app.accounts.have_pro(): + if ba.app.accounts_v1.have_pro(): can_die = True else: if _ba.get_purchased(self._items[0]): @@ -134,7 +134,7 @@ class PurchaseWindow(ba.Window): else: ticket_count: Optional[int] try: - ticket_count = _ba.get_account_ticket_count() + ticket_count = _ba.get_v1_account_ticket_count() except Exception: ticket_count = None if ticket_count is not None and ticket_count < self._price: diff --git a/assets/src/ba_data/python/bastd/ui/settings/advanced.py b/assets/src/ba_data/python/bastd/ui/settings/advanced.py index ebf384a4..f511a7d7 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/advanced.py +++ b/assets/src/ba_data/python/bastd/ui/settings/advanced.py @@ -339,7 +339,7 @@ class AdvancedSettingsWindow(ba.Window): self._update_lang_status() v -= 40 - lang_inform = _ba.get_account_misc_val('langInform', False) + lang_inform = _ba.get_v1_account_misc_val('langInform', False) self._language_inform_checkbox = cbw = ba.checkboxwidget( parent=self._subcontainer, @@ -550,7 +550,7 @@ class AdvancedSettingsWindow(ba.Window): def _on_friend_promo_code_press(self) -> None: from bastd.ui import appinvite from bastd.ui import account - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return appinvite.handle_app_invites_press() @@ -568,7 +568,7 @@ class AdvancedSettingsWindow(ba.Window): from bastd.ui.account import show_sign_in_prompt # We have to be logged in for promo-codes to work. - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() diff --git a/assets/src/ba_data/python/bastd/ui/settings/nettesting.py b/assets/src/ba_data/python/bastd/ui/settings/nettesting.py index fbd0f127..0cfee590 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/nettesting.py +++ b/assets/src/ba_data/python/bastd/ui/settings/nettesting.py @@ -232,7 +232,7 @@ def _dummy_fail() -> None: def _test_v1_transaction() -> None: """Dummy fail test case.""" - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': raise RuntimeError('Not signed in.') starttime = time.monotonic() diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py b/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py index cc125175..5cf74aaa 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py +++ b/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py @@ -211,7 +211,7 @@ class SoundtrackBrowserWindow(ba.Window): on_cancel_call=self._back) def _update(self) -> None: - have = ba.app.accounts.have_pro_options() + have = ba.app.accounts_v1.have_pro_options() for lock in self._lock_images: ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0) @@ -232,7 +232,7 @@ class SoundtrackBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow from bastd.ui.confirm import ConfirmWindow - if not ba.app.accounts.have_pro_options(): + if not ba.app.accounts_v1.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_soundtrack is None: @@ -251,7 +251,7 @@ class SoundtrackBrowserWindow(ba.Window): def _duplicate_soundtrack(self) -> None: # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts.have_pro_options(): + if not ba.app.accounts_v1.have_pro_options(): PurchaseWindow(items=['pro']) return cfg = ba.app.config @@ -322,7 +322,7 @@ class SoundtrackBrowserWindow(ba.Window): def _edit_soundtrack_with_sound(self) -> None: # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts.have_pro_options(): + if not ba.app.accounts_v1.have_pro_options(): PurchaseWindow(items=['pro']) return ba.playsound(ba.getsound('swish')) @@ -332,7 +332,7 @@ class SoundtrackBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow from bastd.ui.soundtrack.edit import SoundtrackEditWindow - if not ba.app.accounts.have_pro_options(): + if not ba.app.accounts_v1.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_soundtrack is None: @@ -434,7 +434,7 @@ class SoundtrackBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow from bastd.ui.soundtrack.edit import SoundtrackEditWindow - if not ba.app.accounts.have_pro_options(): + if not ba.app.accounts_v1.have_pro_options(): PurchaseWindow(items=['pro']) return self._save_state() diff --git a/assets/src/ba_data/python/bastd/ui/specialoffer.py b/assets/src/ba_data/python/bastd/ui/specialoffer.py index 5b2c2ab6..0457f4b4 100644 --- a/assets/src/ba_data/python/bastd/ui/specialoffer.py +++ b/assets/src/ba_data/python/bastd/ui/specialoffer.py @@ -95,7 +95,7 @@ class SpecialOfferWindow(ba.Window): if ('bonusTickets' in offer and offer['bonusTickets'] is not None): self._is_bundle_sale = True - original_price = _ba.get_account_misc_read_val( + original_price = _ba.get_v1_account_misc_read_val( 'price.' + self._offer_item, 9999) # For pure ticket prices we can show a percent-off. @@ -341,7 +341,7 @@ class SpecialOfferWindow(ba.Window): # We go away if we see that our target item is owned. if self._offer_item == 'pro': - if _ba.app.accounts.have_pro(): + if _ba.app.accounts_v1.have_pro(): can_die = True else: if _ba.get_purchased(self._offer_item): @@ -364,9 +364,9 @@ class SpecialOfferWindow(ba.Window): if not self._root_widget: return sval: Union[str, ba.Lstr] - if _ba.get_account_state() == 'signed_in': + if _ba.get_v1_account_state() == 'signed_in': sval = (ba.charstr(SpecialChar.TICKET) + - str(_ba.get_account_ticket_count())) + str(_ba.get_v1_account_ticket_count())) else: sval = ba.Lstr(resource='getTicketsWindow.titleText') ba.buttonwidget(edit=self._get_tickets_button, label=sval) @@ -374,7 +374,7 @@ class SpecialOfferWindow(ba.Window): def _on_get_more_tickets_press(self) -> None: from bastd.ui import account from bastd.ui import getcurrency - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return getcurrency.GetCurrencyWindow(modal=True).get_root_widget() @@ -393,7 +393,7 @@ class SpecialOfferWindow(ba.Window): else: ticket_count: Optional[int] try: - ticket_count = _ba.get_account_ticket_count() + ticket_count = _ba.get_v1_account_ticket_count() except Exception: ticket_count = None if (ticket_count is not None diff --git a/assets/src/ba_data/python/bastd/ui/store/browser.py b/assets/src/ba_data/python/bastd/ui/store/browser.py index 6d757c2a..9eb9f571 100644 --- a/assets/src/ba_data/python/bastd/ui/store/browser.py +++ b/assets/src/ba_data/python/bastd/ui/store/browser.py @@ -282,7 +282,7 @@ class StoreBrowserWindow(ba.Window): def _restore_purchases(self) -> None: from bastd.ui import account - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() else: _ba.restore_purchases() @@ -323,9 +323,9 @@ class StoreBrowserWindow(ba.Window): if not self._root_widget: return sval: Union[str, ba.Lstr] - if _ba.get_account_state() == 'signed_in': + if _ba.get_v1_account_state() == 'signed_in': sval = ba.charstr(SpecialChar.TICKET) + str( - _ba.get_account_ticket_count()) + _ba.get_v1_account_ticket_count()) else: sval = ba.Lstr(resource='getTicketsWindow.titleText') if self._get_tickets_button: @@ -410,7 +410,7 @@ class StoreBrowserWindow(ba.Window): else: if is_ticket_purchase: if result['allow']: - price = _ba.get_account_misc_read_val( + price = _ba.get_v1_account_misc_read_val( 'price.' + item, None) if (price is None or not isinstance(price, int) or price <= 0): @@ -485,7 +485,7 @@ class StoreBrowserWindow(ba.Window): self._last_buy_time) < 2.0: ba.playsound(ba.getsound('error')) else: - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() else: self._last_buy_time = curtime @@ -499,9 +499,9 @@ class StoreBrowserWindow(ba.Window): self._do_purchase_check('pro' if get_available_sale_time( 'extras') is None else 'pro_sale') else: - price = _ba.get_account_misc_read_val( + price = _ba.get_v1_account_misc_read_val( 'price.' + item, None) - our_tickets = _ba.get_account_ticket_count() + our_tickets = _ba.get_v1_account_ticket_count() if price is not None and our_tickets < price: ba.playsound(ba.getsound('error')) getcurrency.show_get_tickets_prompt() @@ -540,7 +540,7 @@ class StoreBrowserWindow(ba.Window): if not self._root_widget: return import datetime - sales_raw = _ba.get_account_misc_read_val('sales', {}) + sales_raw = _ba.get_v1_account_misc_read_val('sales', {}) sales = {} try: # Look at the current set of sales; filter any with time remaining. @@ -559,7 +559,7 @@ class StoreBrowserWindow(ba.Window): for b_type, b_info in self.button_infos.items(): if b_type in ['upgrades.pro', 'pro']: - purchased = _ba.app.accounts.have_pro() + purchased = _ba.app.accounts_v1.have_pro() else: purchased = _ba.get_purchased(b_type) @@ -606,14 +606,16 @@ class StoreBrowserWindow(ba.Window): price_text_left = '' price_text_right = '' else: - price = _ba.get_account_misc_read_val('price.' + b_type, 0) + price = _ba.get_v1_account_misc_read_val( + 'price.' + b_type, 0) # Color the button differently if we cant afford this. - if _ba.get_account_state() == 'signed_in': - if _ba.get_account_ticket_count() < price: + if _ba.get_v1_account_state() == 'signed_in': + if _ba.get_v1_account_ticket_count() < price: color = (0.6, 0.61, 0.6) price_text = ba.charstr(ba.SpecialChar.TICKET) + str( - _ba.get_account_misc_read_val('price.' + b_type, '?')) + _ba.get_v1_account_misc_read_val( + 'price.' + b_type, '?')) price_text_left = '' price_text_right = '' @@ -1062,7 +1064,7 @@ class StoreBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt from bastd.ui.getcurrency import GetCurrencyWindow - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() diff --git a/assets/src/ba_data/python/bastd/ui/store/button.py b/assets/src/ba_data/python/bastd/ui/store/button.py index 5aec0506..3492563d 100644 --- a/assets/src/ba_data/python/bastd/ui/store/button.py +++ b/assets/src/ba_data/python/bastd/ui/store/button.py @@ -197,7 +197,7 @@ class StoreButton: # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if _ba.get_account_state() != 'signed_in': + if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return StoreBrowserWindow(modal=True, origin_widget=self._button) @@ -216,9 +216,9 @@ class StoreButton: return # Our instance may outlive our UI objects. if self._ticket_text is not None: - if _ba.get_account_state() == 'signed_in': + if _ba.get_v1_account_state() == 'signed_in': sval = ba.charstr(SpecialChar.TICKET) + str( - _ba.get_account_ticket_count()) + _ba.get_v1_account_ticket_count()) else: sval = '-' ba.textwidget(edit=self._ticket_text, text=sval) @@ -230,7 +230,7 @@ class StoreButton: # ..also look for new style sales. if sale_time is None: import datetime - sales_raw = _ba.get_account_misc_read_val('sales', {}) + sales_raw = _ba.get_v1_account_misc_read_val('sales', {}) sale_times = [] try: # Look at the current set of sales; filter any with time diff --git a/assets/src/ba_data/python/bastd/ui/store/item.py b/assets/src/ba_data/python/bastd/ui/store/item.py index ea49deda..f7e6198f 100644 --- a/assets/src/ba_data/python/bastd/ui/store/item.py +++ b/assets/src/ba_data/python/bastd/ui/store/item.py @@ -202,7 +202,7 @@ def instantiate_store_item_display(item_name: str, color=(1, 1, 1), texture=ba.gettexture('ticketsMore'))) bonus_tickets = str( - _ba.get_account_misc_read_val('proBonusTickets', 100)) + _ba.get_v1_account_misc_read_val('proBonusTickets', 100)) extra_texts.append( ba.textwidget(parent=parent_widget, draw_controller=btn, @@ -270,8 +270,8 @@ def instantiate_store_item_display(item_name: str, # If we have a 'total-worth' item-id for this id, show that price so # the user knows how much this is worth. - total_worth_item = _ba.get_account_misc_read_val('twrths', - {}).get(item_name) + total_worth_item = _ba.get_v1_account_misc_read_val('twrths', + {}).get(item_name) total_worth_price: Optional[str] if total_worth_item is not None: price = _ba.get_price(total_worth_item) diff --git a/assets/src/ba_data/python/bastd/ui/tournamententry.py b/assets/src/ba_data/python/bastd/ui/tournamententry.py index 17373870..7419c4c8 100644 --- a/assets/src/ba_data/python/bastd/ui/tournamententry.py +++ b/assets/src/ba_data/python/bastd/ui/tournamententry.py @@ -33,7 +33,7 @@ class TournamentEntryWindow(popup.PopupWindow): self._tournament_id = tournament_id self._tournament_info = ( - ba.app.accounts.tournament_info[self._tournament_id]) + ba.app.accounts_v1.tournament_info[self._tournament_id]) # Set a few vars depending on the tourney fee. self._fee = self._tournament_info['fee'] @@ -274,13 +274,14 @@ class TournamentEntryWindow(popup.PopupWindow): # If there seems to be a relatively-recent valid cached info for this # tournament, use it. Otherwise we'll kick off a query ourselves. - if (self._tournament_id in ba.app.accounts.tournament_info and - ba.app.accounts.tournament_info[self._tournament_id]['valid'] + if (self._tournament_id in ba.app.accounts_v1.tournament_info + and ba.app.accounts_v1.tournament_info[ + self._tournament_id]['valid'] and (ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) - - ba.app.accounts.tournament_info[self._tournament_id] + ba.app.accounts_v1.tournament_info[self._tournament_id] ['timeReceived'] < 1000 * 60 * 5)): try: - info = ba.app.accounts.tournament_info[self._tournament_id] + info = ba.app.accounts_v1.tournament_info[self._tournament_id] self._seconds_remaining = max( 0, info['timeRemaining'] - int( (ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) @@ -304,7 +305,7 @@ class TournamentEntryWindow(popup.PopupWindow): def _on_tournament_query_response(self, data: Optional[dict[str, Any]]) -> None: - accounts = ba.app.accounts + accounts = ba.app.accounts_v1 self._running_query = False if data is not None: data = data['t'] # This used to be the whole payload. @@ -358,7 +359,7 @@ class TournamentEntryWindow(popup.PopupWindow): self._running_query = True # Grab the latest info on our tourney. - self._tournament_info = ba.app.accounts.tournament_info[ + self._tournament_info = ba.app.accounts_v1.tournament_info[ self._tournament_id] # If we don't have valid data always show a '-' for time. @@ -374,7 +375,7 @@ class TournamentEntryWindow(popup.PopupWindow): timeformat=ba.TimeFormat.MILLISECONDS)) # Keep price up-to-date and update the button with it. - self._purchase_price = _ba.get_account_misc_read_val( + self._purchase_price = _ba.get_v1_account_misc_read_val( self._purchase_price_name, None) ba.textwidget( @@ -422,7 +423,7 @@ class TournamentEntryWindow(popup.PopupWindow): color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4)) try: - t_str = str(_ba.get_account_ticket_count()) + t_str = str(_ba.get_v1_account_ticket_count()) except Exception: t_str = '?' if self._get_tickets_button: @@ -512,7 +513,7 @@ class TournamentEntryWindow(popup.PopupWindow): # Deny if we don't have enough tickets. ticket_count: Optional[int] try: - ticket_count = _ba.get_account_ticket_count() + ticket_count = _ba.get_v1_account_ticket_count() except Exception: # FIXME: should add a ba.NotSignedInError we can use here. ticket_count = None diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index f580f813..e951bf03 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -8,7 +8,10 @@ abouttab absval accel + accountclientv accountid + accountv + accountvalues achs acinstance ack'ed @@ -17,6 +20,7 @@ aclass aclass's activityplayer + actool addcall addchars addr @@ -62,6 +66,7 @@ appspot appstate argsjoined + argstr asci assetsmakefile assigninput @@ -99,6 +104,7 @@ basntoclient bastd batoolsinternal + baworker bbbb bbbbb bbbbbb @@ -150,6 +156,7 @@ bsmhi bsstd bstat + bstr bsuuid btnlabel bucketnum @@ -197,6 +204,7 @@ checkarglist checkboxwidget checkchisel + checksummed childanntype childanntypes childtype @@ -225,11 +233,20 @@ collider columnwidget comms + compileassetcatalog + compilec + compilemetalfile + compilestoryboard + compileswift + compileswiftsources connectattr containerwidget controlfp cooldown coopscore + copypng + copystringsfile + copyswiftlibs coreaudio coulda cout @@ -239,6 +256,7 @@ cpuid crashenv crashlytics + createbuilddirectory createtime cresult crom @@ -252,6 +270,7 @@ cstdint cstdlib cstring + csval ctargetref ctracker cubemap @@ -260,6 +279,7 @@ cutef cvar data + databytes dataclassio datadata dataout @@ -546,9 +566,13 @@ htonf htonl htons + ibtool + ibtoold ibuf icloud iconscale + iconset + iconutil ieeefp ifaddr ifaddrs @@ -615,6 +639,7 @@ keycode keyfilt keyint + keylen keysyms keywds khronos @@ -648,6 +673,8 @@ lightshad linearsize linearstep + linemax + linkstoryboards listobj llock localns @@ -667,6 +694,7 @@ lrintf lscope lshort + lsregister lstr lsync ltypes @@ -709,6 +737,7 @@ meshdata messagebox messagetype + metallink metamakefile meth mhbegin @@ -722,6 +751,7 @@ mipmaps mkflags mlen + mmacosx mmask mmdevapi modder @@ -782,6 +812,7 @@ newitem newname newnode + newtoken nextchar nitpicky nlpos @@ -828,6 +859,7 @@ okbtn oldbook oldname + oldtoken oooo ooooooo ooooooooo @@ -859,6 +891,8 @@ ourname ourself ourstanding + outdict + outmsg outpath outputter outval @@ -880,6 +914,7 @@ pflag pflags pgmout + phasescriptexecution piplist pipvers pixelformat @@ -918,6 +953,9 @@ printobjects priv privatetab + processinfoplistfile + processpch + processpchplusplus profilers prog proj @@ -925,6 +963,7 @@ projpath projprefix prolly + proxykey psmx pspec psps @@ -992,6 +1031,8 @@ refcounted refl regionid + registerexecutionpolicyexception + registerwithlaunchservices regtp rehel reimported @@ -1042,6 +1083,7 @@ sapspace savebtn savebutton + sbytes scancode scenetime screenmessage @@ -1050,6 +1092,7 @@ sdkcheck sdl's sdlk + sectionchanged selchild selindex selwidget @@ -1137,6 +1180,7 @@ standin startedptr startpos + startsplits starttime startx starty @@ -1171,6 +1215,7 @@ subscr subtypestr sval + swiftc symbolification syscalls tabdefs @@ -1194,8 +1239,10 @@ textcolor textwidget thang + thats thecommand theres + thislinelen thismodule threadname threadpool @@ -1330,10 +1377,14 @@ worldspace woutdir wprjp + writeauxiliaryfile wsroot wunused wvmpth + xcframework xclamped + xcodebuildverbose + xcoderun xcrun xdiff xdist @@ -1342,6 +1393,7 @@ xmin xmmintrin xoffset + xors xtweak xxlimited xxsubinterpreters @@ -1354,6 +1406,7 @@ yoffs yooooooo ytweak + zipdata zmax zmin zoffset diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index 185e529b..1013624d 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -21,8 +21,8 @@ namespace ballistica { // These are set automatically via script; don't modify them here. -const int kAppBuildNumber = 20567; -const char* kAppVersion = "1.6.12"; +const int kAppBuildNumber = 20577; +const char* kAppVersion = "1.7.0"; // Our standalone globals. // These are separated out for easy access. diff --git a/src/ballistica/core/types.h b/src/ballistica/core/types.h index ffff71ba..ffceef07 100644 --- a/src/ballistica/core/types.h +++ b/src/ballistica/core/types.h @@ -1018,7 +1018,8 @@ enum class V1AccountType { kServer, kOculus, kSteam, - kNvidiaChina + kNvidiaChina, + kV2 }; enum class GraphicsQuality { diff --git a/src/ballistica/platform/apple/platform_apple.h b/src/ballistica/platform/apple/platform_apple.h index 52f1c85a..57702b37 100644 --- a/src/ballistica/platform/apple/platform_apple.h +++ b/src/ballistica/platform/apple/platform_apple.h @@ -16,7 +16,7 @@ namespace ballistica { class PlatformApple : public Platform { public: PlatformApple(); - auto GetDeviceAccountUUIDPrefix() -> std::string override; + auto GetDeviceV1AccountUUIDPrefix() -> std::string override; auto GetRealLegacyDeviceUUID(std::string* uuid) -> bool override; auto GenerateUUID() -> std::string override; auto GetDefaultConfigDir() -> std::string override; diff --git a/src/ballistica/platform/linux/platform_linux.h b/src/ballistica/platform/linux/platform_linux.h index 87c96304..71f89f15 100644 --- a/src/ballistica/platform/linux/platform_linux.h +++ b/src/ballistica/platform/linux/platform_linux.h @@ -13,7 +13,7 @@ namespace ballistica { class PlatformLinux : public Platform { public: PlatformLinux(); - auto GetDeviceAccountUUIDPrefix() -> std::string override { return "l"; } + auto GetDeviceV1AccountUUIDPrefix() -> std::string override { return "l"; } auto GenerateUUID() -> std::string override; auto DoHasTouchScreen() -> bool override; auto DoOpenURL(const std::string& url) -> void override; diff --git a/src/ballistica/platform/platform.cc b/src/ballistica/platform/platform.cc index 960f3514..dfaa32ca 100644 --- a/src/ballistica/platform/platform.cc +++ b/src/ballistica/platform/platform.cc @@ -118,7 +118,7 @@ Platform::~Platform() = default; auto Platform::GetLegacyDeviceUUID() -> const std::string& { if (!have_device_uuid_) { - legacy_device_uuid_ = GetDeviceAccountUUIDPrefix(); + legacy_device_uuid_ = GetDeviceV1AccountUUIDPrefix(); std::string real_unique_uuid; bool have_real_unique_uuid = GetRealLegacyDeviceUUID(&real_unique_uuid); @@ -168,8 +168,8 @@ auto Platform::GetLegacyDeviceUUID() -> const std::string& { return legacy_device_uuid_; } -auto Platform::GetDeviceAccountUUIDPrefix() -> std::string { - Log("GetDeviceAccountUUIDPrefix() unimplemented"); +auto Platform::GetDeviceV1AccountUUIDPrefix() -> std::string { + Log("GetDeviceV1AccountUUIDPrefix() unimplemented"); return "u"; } @@ -804,7 +804,7 @@ auto Platform::IsStdinATerminal() -> bool { #endif } -auto Platform::GetOSVersionString() -> std::string { return "?"; } +auto Platform::GetOSVersionString() -> std::string { return ""; } auto Platform::GetUserAgentString() -> std::string { std::string device = GetDeviceName(); @@ -949,7 +949,7 @@ void Platform::AndroidQuitActivity() { Log("AndroidQuitActivity() unimplemented"); } -auto Platform::GetDeviceAccountID() -> std::string { +auto Platform::GetDeviceV1AccountID() -> std::string { if (HeadlessMode()) { return "S-" + GetLegacyDeviceUUID(); } @@ -1073,15 +1073,15 @@ auto Platform::GetHasVideoAds() -> bool { return GetHasAds(); } -void Platform::SignIn(const std::string& account_type) { - Log("SignIn() unimplemented"); +void Platform::SignInV1(const std::string& account_type) { + Log("SignInV1() unimplemented"); } void Platform::LoginDidChange() { // Default is no-op. } -void Platform::SignOut() { Log("SignOut() unimplemented"); } +void Platform::SignOutV1() { Log("SignOutV1() unimplemented"); } void Platform::AndroidShowWifiSettings() { Log("AndroidShowWifiSettings() unimplemented"); diff --git a/src/ballistica/platform/platform.h b/src/ballistica/platform/platform.h index f6795e07..26e21808 100644 --- a/src/ballistica/platform/platform.h +++ b/src/ballistica/platform/platform.h @@ -304,17 +304,17 @@ class Platform { #pragma mark ACCOUNTS ---------------------------------------------------------- - virtual auto SignIn(const std::string& account_type) -> void; - virtual auto SignOut() -> void; + virtual auto SignInV1(const std::string& account_type) -> void; + virtual auto SignOutV1() -> void; virtual auto GameCenterLogin() -> void; virtual auto LoginDidChange() -> void; /// Returns the ID to use for the device account. - auto GetDeviceAccountID() -> std::string; + auto GetDeviceV1AccountID() -> std::string; /// Return the prefix to use for device-account ids on this platform. - virtual auto GetDeviceAccountUUIDPrefix() -> std::string; + virtual auto GetDeviceV1AccountUUIDPrefix() -> std::string; #pragma mark MUSIC PLAYBACK ---------------------------------------------------- diff --git a/src/ballistica/platform/windows/platform_windows.h b/src/ballistica/platform/windows/platform_windows.h index c41add71..8cd06ce2 100644 --- a/src/ballistica/platform/windows/platform_windows.h +++ b/src/ballistica/platform/windows/platform_windows.h @@ -15,7 +15,7 @@ class PlatformWindows : public Platform { public: PlatformWindows(); void SetupInterruptHandling() override; - auto GetDeviceAccountUUIDPrefix() -> std::string override { return "w"; } + auto GetDeviceV1AccountUUIDPrefix() -> std::string override { return "w"; } auto GetDeviceUUIDInputs() -> std::list override; auto GenerateUUID() -> std::string override; auto GetDefaultConfigDir() -> std::string override; diff --git a/src/ballistica/python/class/python_class_input_device.cc b/src/ballistica/python/class/python_class_input_device.cc index 61635f8e..1a6269b2 100644 --- a/src/ballistica/python/class/python_class_input_device.cc +++ b/src/ballistica/python/class/python_class_input_device.cc @@ -300,8 +300,8 @@ auto PythonClassInputDevice::GetPlayerProfiles(PythonClassInputDevice* self) BA_PYTHON_CATCH; } -auto PythonClassInputDevice::GetAccountName(PythonClassInputDevice* self, - PyObject* args, PyObject* keywds) +auto PythonClassInputDevice::GetV1AccountName(PythonClassInputDevice* self, + PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; int full; @@ -440,9 +440,9 @@ PyMethodDef PythonClassInputDevice::tp_methods[] = { "\n" "Returns the default player name for this device. (used for the 'random'\n" "profile)"}, - {"get_account_name", (PyCFunction)GetAccountName, + {"get_v1_account_name", (PyCFunction)GetV1AccountName, METH_VARARGS | METH_KEYWORDS, // NOLINT (signed bitwise ops) - "get_account_name(full: bool) -> str\n" + "get_v1_account_name(full: bool) -> str\n" "\n" "Returns the account name associated with this device.\n" "\n" diff --git a/src/ballistica/python/class/python_class_input_device.h b/src/ballistica/python/class/python_class_input_device.h index a3508939..d360c876 100644 --- a/src/ballistica/python/class/python_class_input_device.h +++ b/src/ballistica/python/class/python_class_input_device.h @@ -34,8 +34,8 @@ class PythonClassInputDevice : public PythonClass { -> PyObject*; static auto GetDefaultPlayerName(PythonClassInputDevice* self) -> PyObject*; static auto GetPlayerProfiles(PythonClassInputDevice* self) -> PyObject*; - static auto GetAccountName(PythonClassInputDevice* self, PyObject* args, - PyObject* keywds) -> PyObject*; + static auto GetV1AccountName(PythonClassInputDevice* self, PyObject* args, + PyObject* keywds) -> PyObject*; static auto IsConnectedToRemotePlayer(PythonClassInputDevice* self) -> PyObject*; static auto Exists(PythonClassInputDevice* self) -> PyObject*; diff --git a/src/ballistica/python/class/python_class_session_player.cc b/src/ballistica/python/class/python_class_session_player.cc index 8544ced5..953dc563 100644 --- a/src/ballistica/python/class/python_class_session_player.cc +++ b/src/ballistica/python/class/python_class_session_player.cc @@ -481,7 +481,7 @@ auto PythonClassSessionPlayer::GetTeam(PythonClassSessionPlayer* self) // NOTE: this returns their PUBLIC account-id; we want to keep // actual account-ids as hidden as possible for now. -auto PythonClassSessionPlayer::GetAccountID(PythonClassSessionPlayer* self) +auto PythonClassSessionPlayer::GetV1AccountID(PythonClassSessionPlayer* self) -> PyObject* { BA_PYTHON_TRY; assert(InGameThread()); @@ -703,10 +703,11 @@ PyMethodDef PythonClassSessionPlayer::tp_methods[] = { "remove_from_game() -> None\n" "\n" "Removes the player from the game."}, - {"get_account_id", (PyCFunction)GetAccountID, METH_VARARGS | METH_KEYWORDS, - "get_account_id() -> str\n" + {"get_v1_account_id", (PyCFunction)GetV1AccountID, + METH_VARARGS | METH_KEYWORDS, + "get_v1_account_id() -> str\n" "\n" - "Return the Account ID this player is signed in under, if\n" + "Return the V1 Account ID this player is signed in under, if\n" "there is one and it can be determined with relative certainty.\n" "Returns None otherwise. Note that this may require an active\n" "internet connection (especially for network-connected players)\n" diff --git a/src/ballistica/python/class/python_class_session_player.h b/src/ballistica/python/class/python_class_session_player.h index bbfb8223..bedee741 100644 --- a/src/ballistica/python/class/python_class_session_player.h +++ b/src/ballistica/python/class/python_class_session_player.h @@ -40,7 +40,7 @@ class PythonClassSessionPlayer : public PythonClass { PyObject* keywds) -> PyObject*; static auto RemoveFromGame(PythonClassSessionPlayer* self) -> PyObject*; static auto GetTeam(PythonClassSessionPlayer* self) -> PyObject*; - static auto GetAccountID(PythonClassSessionPlayer* self) -> PyObject*; + static auto GetV1AccountID(PythonClassSessionPlayer* self) -> PyObject*; static auto SetData(PythonClassSessionPlayer* self, PyObject* args, PyObject* keywds) -> PyObject*; static auto GetIconInfo(PythonClassSessionPlayer* self) -> PyObject*; diff --git a/src/ballistica/python/python.h b/src/ballistica/python/python.h index 1fbfbbd5..b6e385b0 100644 --- a/src/ballistica/python/python.h +++ b/src/ballistica/python/python.h @@ -350,6 +350,7 @@ class Python { kLstrFromJsonCall, kUUIDStrCall, kHashStringsCall, + kHaveAccountV2CredentialsCall, 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 90c1830e..d779056a 100644 --- a/src/meta/bameta/python_embedded/binding.py +++ b/src/meta/bameta/python_embedded/binding.py @@ -83,7 +83,7 @@ def get_binding_values() -> tuple[Any, ...]: _hooks.do_quit, # kQuitCall _hooks.shutdown, # kShutdownCall _hooks.gc_disable, # kGCDisableCall - ba.app.accounts.show_post_purchase_message, # kShowPostPurchaseMessageCall + ba.app.accounts_v1.show_post_purchase_message, # kShowPostPurchaseMessageCall _hooks.device_menu_press, # kDeviceMenuPressCall _hooks.show_url_window, # kShowURLWindowCall _hooks.party_invite_revoke, # kHandlePartyInviteRevokeCall @@ -134,4 +134,5 @@ def get_binding_values() -> tuple[Any, ...]: _language.Lstr.from_json, # kLstrFromJsonCall _hooks.uuid_str, # kUUIDStrCall _hooks.hash_strings, # kHashStringsCall + _hooks.have_account_v2_credentials, # kHaveAccountV2CredentialsCall ) # yapf: disable diff --git a/tests/test_efro/test_message.py b/tests/test_efro/test_message.py index 8129b97c..eb669650 100644 --- a/tests/test_efro/test_message.py +++ b/tests/test_efro/test_message.py @@ -751,6 +751,7 @@ def test_full_pipeline() -> None: def __init__(self, target: Union[TestClassRSync, TestClassRAsync]) -> None: + self.test_sidecar = False self._target = target @msg.send_method @@ -766,7 +767,9 @@ def test_full_pipeline() -> None: if self.test_handling_unregistered: # Emulate forwarding unregistered messages on to some # other handler... - return self.msg.protocol.encode_response(EmptyResponse()) + response_dict = self.msg.protocol.response_to_dict( + EmptyResponse()) + return self.msg.protocol.encode_dict(response_dict) raise @msg.send_async_method @@ -778,11 +781,26 @@ def test_full_pipeline() -> None: return self._target.receiver.handle_raw_message(data) return await self._target.receiver.handle_raw_message(data) + @msg.encode_filter_method + def _encode_filter(self, msg: Message, outdict: dict) -> None: + """Filter our outgoing messages.""" + if self.test_sidecar: + outdict['_sidecar_data'] = getattr(msg, '_sidecar_data') + + @msg.decode_filter_method + def _decode_filter(self, indata: dict, response: Response) -> None: + """Filter our incoming responses.""" + if self.test_sidecar: + setattr(response, '_sidecar_data', indata['_sidecar_data']) + class TestClassRSync: """Test class incorporating synchronous receive functionality.""" receiver = _TestSyncMessageReceiver() + def __init__(self) -> None: + self.test_sidecar = False + @receiver.handler def handle_test_message_1(self, msg: _TMsg1) -> _TResp1: """Test.""" @@ -790,7 +808,10 @@ def test_full_pipeline() -> None: raise CleanError('Testing Clean Error') if msg.ival == 2: raise RuntimeError('Testing Runtime Error') - return _TResp1(bval=True) + out = _TResp1(bval=True) + if self.test_sidecar: + setattr(out, '_sidecar_data', getattr(msg, '_sidecar_data')) + return out @receiver.handler def handle_test_message_2(self, @@ -804,6 +825,18 @@ def test_full_pipeline() -> None: """Test.""" del msg # Unused + @receiver.decode_filter_method + def _decode_filter(self, indata: dict, message: Message) -> None: + """Filter our incoming messages.""" + if self.test_sidecar: + setattr(message, '_sidecar_data', indata['_sidecar_data']) + + @receiver.encode_filter_method + def _encode_filter(self, response: Response, outdict: dict) -> None: + """Filter our outgoing responses.""" + if self.test_sidecar: + outdict['_sidecar_data'] = getattr(response, '_sidecar_data') + receiver.validate() class TestClassRAsync: @@ -885,3 +918,15 @@ def test_full_pipeline() -> None: # Make sure static typing lines up with what we expect. if os.environ.get('EFRO_TEST_MESSAGE_FAST') != '1': assert static_type_equals(response6, _TResp1) + + # Now test adding extra data to messages. This should be transferred + # into the encoded message, copied to the response, and again back + # through the encoded response using the filter functions we defined. + obj.test_sidecar = True + obj_r_sync.test_sidecar = True + outmsg = _TMsg1(ival=0) + setattr(outmsg, '_sidecar_data', 198) # Our test payload. + response1 = obj.msg.send(outmsg) + assert getattr(response1, '_sidecar_data') == 198 + obj.test_sidecar = False + obj_r_sync.test_sidecar = False diff --git a/tools/bacloud b/tools/bacloud index f142ed6b..c00e0cc1 100755 --- a/tools/bacloud +++ b/tools/bacloud @@ -182,6 +182,7 @@ class App: response_raw_2 = requests.post( (MASTER_SERVER_URL + '/bacloudcmd'), + headers={'User-Agent': f'bacloud/{VERSION}'}, data={ 'c': cmd, 'v': VERSION, diff --git a/tools/bacommon/cloud.py b/tools/bacommon/cloud.py new file mode 100644 index 00000000..0e7b7cef --- /dev/null +++ b/tools/bacommon/cloud.py @@ -0,0 +1,103 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to cloud functionality.""" + +from __future__ import annotations +from dataclasses import dataclass +from typing import TYPE_CHECKING, Annotated, Optional +from enum import Enum + +from efro.message import Message, Response +from efro.dataclassio import ioprepped, IOAttrs + +if TYPE_CHECKING: + pass + + +@ioprepped +@dataclass +class LoginProxyRequestMessage(Message): + """Request send to the cloud to ask for a login-proxy.""" + + @classmethod + def get_response_types(cls) -> list[type[Response]]: + return [LoginProxyRequestResponse] + + +@ioprepped +@dataclass +class LoginProxyRequestResponse(Response): + """Response to a request for a login proxy.""" + + # URL to direct the user to for login. + url: Annotated[str, IOAttrs('u')] + + # Proxy-Login id for querying results. + proxyid: Annotated[str, IOAttrs('p')] + + # Proxy-Login key for querying results. + proxykey: Annotated[str, IOAttrs('k')] + + +@ioprepped +@dataclass +class LoginProxyStateQueryMessage(Message): + """Soo.. how is that login proxy going?""" + proxyid: Annotated[str, IOAttrs('p')] + proxykey: Annotated[str, IOAttrs('k')] + + @classmethod + def get_response_types(cls) -> list[type[Response]]: + return [LoginProxyStateQueryResponse] + + +@ioprepped +@dataclass +class LoginProxyStateQueryResponse(Response): + """Here's the info on that login-proxy you asked about, boss.""" + + class State(Enum): + """States a login-proxy can be in.""" + WAITING = 'waiting' + SUCCESS = 'success' + FAIL = 'fail' + + state: Annotated[State, IOAttrs('s')] + + # On success, these will be filled out. + credentials: Annotated[Optional[str], IOAttrs('tk')] + + +@ioprepped +@dataclass +class LoginProxyCompleteMessage(Message): + """Just so you know, we're done with this proxy.""" + proxyid: Annotated[str, IOAttrs('p')] + + +@ioprepped +@dataclass +class AccountSessionReleaseMessage(Message): + """We're done using this particular session.""" + token: Annotated[str, IOAttrs('tk')] + + +@ioprepped +@dataclass +class CredentialsCheckMessage(Message): + """Are our current credentials valid?""" + + @classmethod + def get_response_types(cls) -> list[type[Response]]: + return [CredentialsCheckResponse] + + +@ioprepped +@dataclass +class CredentialsCheckResponse(Response): + """Info returned when checking credentials.""" + + verified: Annotated[bool, IOAttrs('v')] + + # Current account tag (good time to check if it has changed). + tag: Annotated[str, IOAttrs('t')] diff --git a/tools/batools/assetstaging.py b/tools/batools/assetstaging.py index 773f4400..eda72a39 100755 --- a/tools/batools/assetstaging.py +++ b/tools/batools/assetstaging.py @@ -416,7 +416,7 @@ def stage_server_file(projroot: str, mode: str, infilename: str, outfilename: str) -> None: """Stage files for the server environment with some filtering.""" import batools.build - from efrotools import replace_one + from efrotools import replace_exact if mode not in ('debug', 'release'): raise RuntimeError(f"Invalid server-file-staging mode '{mode}';" f" expected 'debug' or 'release'.") @@ -437,9 +437,9 @@ def stage_server_file(projroot: str, mode: str, infilename: str, with open(infilename, encoding='utf-8') as infile: lines = infile.read().splitlines() if mode == 'release': - lines[0] = replace_one(lines[0], - f'#!/usr/bin/env python{PYVER}', - f'#!/usr/bin/env -S python{PYVER} -O') + lines[0] = replace_exact( + lines[0], f'#!/usr/bin/env python{PYVER}', + f'#!/usr/bin/env -S python{PYVER} -O') _write_if_changed(outfilename, '\n'.join(lines) + '\n', make_executable=True) @@ -452,15 +452,15 @@ def stage_server_file(projroot: str, mode: str, infilename: str, with open(infilename, encoding='utf-8') as infile: lines = infile.read().splitlines() if mode == 'release': - lines[1] = replace_one( + lines[1] = replace_exact( lines[1], ':: Python interpreter.', ':: Python interpreter.' ' (in opt mode so we use bundled .opt-1.pyc files)') - lines[2] = replace_one( + lines[2] = replace_exact( lines[2], 'dist\\\\python.exe ballisticacore_server.py', 'dist\\\\python.exe -O ballisticacore_server.py') else: # In debug mode we use the bundled debug interpreter. - lines[2] = replace_one( + lines[2] = replace_exact( lines[2], 'dist\\\\python.exe ballisticacore_server.py', 'dist\\\\python_d.exe ballisticacore_server.py') diff --git a/tools/batools/build.py b/tools/batools/build.py index 8bd3d98a..5575e79f 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -38,23 +38,23 @@ class PipRequirement: # installing it. And as far as manually-installed bits, pip itself must # have some way to allow for that, right?... PIP_REQUIREMENTS = [ - PipRequirement(modulename='pylint', minversion=[2, 12, 2]), - PipRequirement(modulename='mypy', minversion=[0, 931]), + PipRequirement(modulename='pylint', minversion=[2, 13, 9]), + PipRequirement(modulename='mypy', minversion=[0, 960]), PipRequirement(modulename='yapf', minversion=[0, 32, 0]), - PipRequirement(modulename='cpplint', minversion=[1, 5, 5]), - PipRequirement(modulename='pytest', minversion=[6, 2, 5]), + PipRequirement(modulename='cpplint', minversion=[1, 6, 0]), + PipRequirement(modulename='pytest', minversion=[7, 1, 2]), PipRequirement(modulename='pytz'), PipRequirement(modulename='ansiwrap'), PipRequirement(modulename='yaml', pipname='PyYAML'), PipRequirement(modulename='requests'), PipRequirement(modulename='pdoc'), - PipRequirement(pipname='typing_extensions', minversion=[4, 0, 1]), - PipRequirement(pipname='types-filelock', minversion=[3, 2, 5]), - PipRequirement(pipname='types-requests', minversion=[2, 27, 7]), - PipRequirement(pipname='types-pytz', minversion=[2021, 3, 4]), - PipRequirement(pipname='types-PyYAML', minversion=[6, 0, 3]), - PipRequirement(pipname='certifi', minversion=[2021, 10, 8]), - PipRequirement(pipname='types-certifi', minversion=[2021, 10, 8, 1]), + PipRequirement(pipname='typing_extensions', minversion=[4, 2, 0]), + PipRequirement(pipname='types-filelock', minversion=[3, 2, 6]), + PipRequirement(pipname='types-requests', minversion=[2, 27, 29]), + PipRequirement(pipname='types-pytz', minversion=[2021, 3, 8]), + PipRequirement(pipname='types-PyYAML', minversion=[6, 0, 7]), + PipRequirement(pipname='certifi', minversion=[2022, 5, 18, 1]), + PipRequirement(pipname='types-certifi', minversion=[2021, 10, 8, 2]), ] # Parts of full-tests suite we only run on particular days. @@ -566,7 +566,8 @@ def checkenv() -> None: f' will update all pip requirements.') if minver is not None: vnums = pipvers[pipname] - assert len(vnums) == len(minver) + assert len(vnums) == len(minver), ( + f'unexpected version format for {pipname}: {vnums}') if vnums < minver: raise CleanError( f'{pipname} ver. {_vstr(minver)} or newer' diff --git a/tools/batools/pcommand.py b/tools/batools/pcommand.py index f8cb194d..a9e00912 100644 --- a/tools/batools/pcommand.py +++ b/tools/batools/pcommand.py @@ -1,6 +1,5 @@ # Released under the MIT License. See LICENSE for details. # -# pylint: disable=too-many-lines """A nice collection of ready-to-use pcommands for this package.""" from __future__ import annotations @@ -387,7 +386,10 @@ def python_apple_patch() -> None: """Patches Python to prep for building for Apple platforms.""" from efrotools import pybuild arch = sys.argv[2] - pybuild.apple_patch(arch) + slc = sys.argv[3] + assert slc + assert ' ' not in slc + pybuild.apple_patch(arch, slc) def python_gather() -> None: @@ -954,21 +956,6 @@ def update_meta_makefile() -> None: update(projroot=str(PROJROOT), check='--check' in sys.argv) -def xcode_build_path() -> None: - """Get the build path for an xcode project.""" - import os - from batools.xcode import project_build_path - if len(sys.argv) != 4: - raise Exception( - 'Expected 2 args: ') - project_path = os.path.abspath(sys.argv[2]) - configuration = sys.argv[3] - path = project_build_path(projroot=str(PROJROOT), - project_path=project_path, - configuration=configuration) - print(path) - - def gen_python_enums_module() -> None: """Update our procedurally generated python enums.""" from batools.pythonenumsmodule import generate diff --git a/tools/batools/xcode.py b/tools/batools/xcode.py deleted file mode 100755 index 8d5ce148..00000000 --- a/tools/batools/xcode.py +++ /dev/null @@ -1,91 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Fetch and cache xcode project build paths. - -This saves the few seconds it normally would take to fire up xcodebuild -and filter its output. -""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -import time -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional - - -def project_build_path(projroot: str, project_path: str, - configuration: str) -> str: - """Main script entry point.""" - # pylint: disable=too-many-locals - - config_path = os.path.join(projroot, '.cache', 'xcode_build_path') - out_path = None - config: dict[str, dict[str, Any]] = {} - - build_dir: Optional[str] = None - - try: - if os.path.exists(config_path): - with open(config_path, encoding='utf-8') as infile: - config = json.loads(infile.read()) - if (project_path in config - and configuration in config[project_path]): - - # Ok we've found a build-dir entry for this project; now if it - # exists on disk and all timestamps within it are decently - # close to the one we've got recorded, lets use it. - # (Anything using this script should also be building - # stuff there so mod times should be pretty recent; if not - # then its worth re-caching to be sure.) - build_dir = config[project_path][configuration]['build_dir'] - timestamp = config[project_path][configuration]['timestamp'] - assert build_dir is not None - if os.path.isdir(build_dir): - use_cached = True - - # if its been over a day since we cached this, renew it - now = time.time() - if abs(now - timestamp) > 60 * 60 * 24: - use_cached = False - - if use_cached: - out_path = build_dir - except Exception: - import traceback - print('EXCEPTION checking cached build path', file=sys.stderr) - traceback.print_exc() - out_path = None - - # If we don't have a path at this point we look it up and cache it. - if out_path is None: - print('Caching xcode build path...', file=sys.stderr) - output = subprocess.check_output([ - 'xcodebuild', '-project', project_path, '-showBuildSettings', - '-configuration', configuration - ]).decode('utf-8') - prefix = 'TARGET_BUILD_DIR = ' - lines = [ - l for l in output.splitlines() if l.strip().startswith(prefix) - ] - if len(lines) != 1: - raise Exception( - 'TARGET_BUILD_DIR not found in xcodebuild settings output') - build_dir = lines[0].replace(prefix, '').strip() - if project_path not in config: - config[project_path] = {} - config[project_path][configuration] = { - 'build_dir': build_dir, - 'timestamp': time.time() - } - os.makedirs(os.path.dirname(config_path), exist_ok=True) - with open(config_path, 'w', encoding='utf-8') as outfile: - outfile.write(json.dumps(config)) - - assert build_dir is not None - return build_dir diff --git a/tools/efro/error.py b/tools/efro/error.py index a35bfac8..2809b452 100644 --- a/tools/efro/error.py +++ b/tools/efro/error.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: class CleanError(Exception): - """An error that should be presented to the user as a simple message. + """An error that can be presented to the user as a simple message. These errors should be completely self-explanatory, to the point where a traceback or other context would not be useful. @@ -41,7 +41,7 @@ class CommunicationError(Exception): This covers anything network-related going wrong in the sending of data or receiving of a response. This error does not imply that data was not received on the other end; only that a full - response round trip was not completed. + acknowledgement round trip was not completed. These errors should be gracefully handled whenever possible, as occasional network outages are generally unavoidable. @@ -55,9 +55,9 @@ class RemoteError(Exception): occurs remotely. The error string can consist of a remote stack trace or a simple message depending on the context. - Depending on the situation, more specific error types such as CleanError - may be raised due to the remote error, so this one is considered somewhat - of a catch-all. + Communication systems should raise more specific error types when + more introspection/control is needed; this is intended somewhat as + a catch-all. """ def __str__(self) -> str: @@ -65,6 +65,10 @@ class RemoteError(Exception): return f'Remote Exception Follows:\n{s}' +class IntegrityError(ValueError): + """Data has been tampered with or corrupted in some form.""" + + def is_urllib_network_error(exc: BaseException) -> bool: """Is the provided exception from urllib a network-related error? diff --git a/tools/efro/message/_protocol.py b/tools/efro/message/_protocol.py index b5320fa8..97f6a4fa 100644 --- a/tools/efro/message/_protocol.py +++ b/tools/efro/message/_protocol.py @@ -11,7 +11,7 @@ import traceback import logging import json -from efro.error import CleanError, RemoteError +from efro.error import CleanError from efro.dataclassio import (is_ioprepped_dataclass, dataclass_to_dict, dataclass_from_dict) from efro.message._message import (Message, Response, ErrorResponse, @@ -33,7 +33,6 @@ class MessageProtocol: def __init__(self, message_types: dict[int, type[Message]], response_types: dict[int, type[Response]], - type_key: Optional[str] = None, preserve_clean_errors: bool = True, log_remote_exceptions: bool = True, trusted_sender: bool = False) -> None: @@ -43,11 +42,6 @@ class MessageProtocol: with (unchanging negative ids) so they don't need to be passed explicitly (but can be if a different id is desired). - If 'type_key' is provided, the message type ID is stored as the - provided key in the message dict; otherwise it will be stored as - part of a top level dict with the message payload appearing as a - child dict. This is mainly for backwards compatibility. - If 'preserve_clean_errors' is True, efro.error.CleanError errors on the remote end will result in the same error raised locally. All other Exception types come across as efro.error.RemoteError. @@ -55,7 +49,6 @@ class MessageProtocol: If 'trusted_sender' is True, stringified remote stack traces will be included in the responses if errors occur. """ - # pylint: disable=too-many-locals self.message_types_by_id: dict[int, type[Message]] = {} self.message_ids_by_type: dict[type[Message], int] = {} self.response_types_by_id: dict[int, type[Response]] = {} @@ -93,7 +86,6 @@ class MessageProtocol: _reg_if_not(ErrorResponse, -1) _reg_if_not(EmptyResponse, -2) - # _reg_if_not(BoolResponse, -3) # Some extra-thorough validation in debug mode. if __debug__: @@ -124,84 +116,78 @@ class MessageProtocol: 'message_types contains duplicate __name__s;' ' all types are required to have unique names.') - self._type_key = type_key self.preserve_clean_errors = preserve_clean_errors self.log_remote_exceptions = log_remote_exceptions self.trusted_sender = trusted_sender - def encode_message(self, message: Message) -> str: - """Encode a message to a json string for transport.""" - return self._encode(message, self.message_ids_by_type, 'message') + @staticmethod + def encode_dict(obj: dict) -> str: + """Json-encode a provided dict.""" + return json.dumps(obj, separators=(',', ':')) - def encode_response(self, response: Response) -> str: - """Encode a response to a json string for transport.""" - return self._encode(response, self.response_ids_by_type, 'response') + def message_to_dict(self, message: Message) -> dict: + """Encode a message to a json ready dict.""" + return self._to_dict(message, self.message_ids_by_type, 'message') - def encode_error_response(self, exc: Exception) -> str: - """Return a raw response for an error that occurred during handling.""" + def response_to_dict(self, response: Response) -> dict: + """Encode a response to a json ready dict.""" + return self._to_dict(response, self.response_ids_by_type, 'response') + + def error_to_response(self, exc: Exception) -> Response: + """Translate an error to a response.""" if self.log_remote_exceptions: logging.exception('Error handling message.') # If anything goes wrong, return a ErrorResponse instead. if isinstance(exc, CleanError) and self.preserve_clean_errors: - err_response = ErrorResponse(error_message=str(exc), - error_type=ErrorType.CLEAN) - else: - err_response = ErrorResponse( - error_message=(traceback.format_exc() if self.trusted_sender - else 'An unknown error has occurred.'), - error_type=ErrorType.OTHER) - return self.encode_response(err_response) + return ErrorResponse(error_message=str(exc), + error_type=ErrorType.CLEAN) + return ErrorResponse( + error_message=(traceback.format_exc() if self.trusted_sender else + 'An unknown error has occurred.'), + error_type=ErrorType.OTHER) - def _encode(self, message: Any, ids_by_type: dict[type, int], - opname: str) -> str: + def _to_dict(self, message: Any, ids_by_type: dict[type, int], + opname: str) -> dict: """Encode a message to a json string for transport.""" m_id: Optional[int] = ids_by_type.get(type(message)) if m_id is None: raise TypeError(f'{opname} type is not registered in protocol:' f' {type(message)}') - msgdict = dataclass_to_dict(message) + out = {'t': m_id, 'm': dataclass_to_dict(message)} + return out - # Encode type as part of the message/response dict if desired - # (for legacy compatibility). - if self._type_key is not None: - if self._type_key in msgdict: - raise RuntimeError(f'Type-key {self._type_key}' - f' found in msg of type {type(message)}') - msgdict[self._type_key] = m_id - out = msgdict - else: - out = {'m': msgdict, 't': m_id} - return json.dumps(out, separators=(',', ':')) + @staticmethod + def decode_dict(data: str) -> dict: + """Decode data to a dict.""" + out = json.loads(data) + assert isinstance(out, dict) + return out - def decode_message(self, data: str) -> Message: + def message_from_dict(self, data: dict) -> Message: """Decode a message from a json string.""" - out = self._decode(data, self.message_types_by_id, 'message') + out = self._from_dict(data, self.message_types_by_id, 'message') assert isinstance(out, Message) return out - def decode_response(self, data: str) -> Optional[Response]: + def response_from_dict(self, data: dict) -> Response: """Decode a response from a json string.""" - out = self._decode(data, self.response_types_by_id, 'response') - assert isinstance(out, (Response, type(None))) + out = self._from_dict(data, self.response_types_by_id, 'response') + assert isinstance(out, Response) return out # Weeeird; we get mypy errors returning dict[int, type] but # dict[int, typing.Type] or dict[int, type[Any]] works.. - def _decode(self, data: str, types_by_id: dict[int, type[Any]], - opname: str) -> Any: + def _from_dict(self, data: dict, types_by_id: dict[int, type[Any]], + opname: str) -> Any: """Decode a message from a json string.""" - msgfull = json.loads(data) - assert isinstance(msgfull, dict) msgdict: Optional[dict] - if self._type_key is not None: - m_id = msgfull.pop(self._type_key) - msgdict = msgfull - assert isinstance(m_id, int) - else: - m_id = msgfull.get('t') - msgdict = msgfull.get('m') + + m_id = data.get('t') + # Allow omitting 'm' dict if its empty. + msgdict = data.get('m', {}) + assert isinstance(m_id, int) assert isinstance(msgdict, dict) @@ -210,22 +196,7 @@ class MessageProtocol: if msgtype is None: raise UnregisteredMessageIDError( f'Got unregistered {opname} id of {m_id}.') - out = dataclass_from_dict(msgtype, msgdict) - - # Special case: if we get EmptyResponse, we simply return None. - if isinstance(out, EmptyResponse): - return None - - # Special case: a remote error occurred. Raise a local Exception - # instead of returning the message. - if isinstance(out, ErrorResponse): - assert opname == 'response' - if (self.preserve_clean_errors - and out.error_type is ErrorType.CLEAN): - raise CleanError(out.error_message) - raise RemoteError(out.error_message) - - return out + return dataclass_from_dict(msgtype, msgdict) def _get_module_header(self, part: Literal['sender', 'receiver'], diff --git a/tools/efro/message/_receiver.py b/tools/efro/message/_receiver.py index bffbbe9e..1d114a99 100644 --- a/tools/efro/message/_receiver.py +++ b/tools/efro/message/_receiver.py @@ -49,6 +49,10 @@ class MessageReceiver: def __init__(self, protocol: MessageProtocol) -> None: self.protocol = protocol self._handlers: dict[type[Message], Callable] = {} + self._decode_filter_call: Optional[Callable[[Any, dict, Message], + None]] = None + self._encode_filter_call: Optional[Callable[[Any, Response, dict], + None]] = None # noinspection PyProtectedMember def register_handler( @@ -59,6 +63,9 @@ class MessageReceiver: type annotation. """ # TODO: can use types.GenericAlias in 3.9. + # (hmm though now that we're there, it seems a drop-in + # replace gives us errors. Should re-test in 3.10 as it seems + # that typing_extensions handles it differently in that case) from typing import _GenericAlias # type: ignore from typing import get_type_hints, get_args @@ -136,6 +143,30 @@ class MessageReceiver: # Ok; we're good! self._handlers[msgtype] = call + def decode_filter_method( + self, call: Callable[[Any, dict, Message], None] + ) -> Callable[[Any, dict, Message], None]: + """Function decorator for defining a decode filter. + + Decode filters can be used to extract extra data from incoming + message dicts. + """ + assert self._decode_filter_call is None + self._decode_filter_call = call + return call + + def encode_filter_method( + self, call: Callable[[Any, Response, dict], None] + ) -> Callable[[Any, Response, dict], None]: + """Function decorator for defining an encode filter. + + Encode filters can be used to add extra data to the message + dict before is is encoded to a string and sent out. + """ + assert self._encode_filter_call is None + self._encode_filter_call = call + return call + def validate(self, log_only: bool = False) -> None: """Check for handler completeness, valid types, etc.""" for msgtype in self.protocol.message_ids_by_type.keys(): @@ -149,16 +180,22 @@ class MessageReceiver: else: raise TypeError(msg) - def _decode_incoming_message(self, + def _decode_incoming_message(self, bound_obj: Any, msg: str) -> tuple[Message, type[Message]]: # Decode the incoming message. - msg_decoded = self.protocol.decode_message(msg) + msg_dict = self.protocol.decode_dict(msg) + msg_decoded = self.protocol.message_from_dict(msg_dict) msgtype = type(msg_decoded) assert issubclass(msgtype, Message) + if self._decode_filter_call is not None: + self._decode_filter_call(bound_obj, msg_dict, msg_decoded) + return msg_decoded, msgtype - def _encode_response(self, response: Optional[Response], - msgtype: type[Message]) -> str: + def encode_user_response(self, bound_obj: Any, + response: Optional[Response], + msgtype: type[Message]) -> str: + """Encode a response provided by the user for sending.""" # A return value of None equals EmptyResponse. if response is None: @@ -168,7 +205,18 @@ class MessageReceiver: # (user should never explicitly return error-responses) assert not isinstance(response, ErrorResponse) assert type(response) in msgtype.get_response_types() - return self.protocol.encode_response(response) + response_dict = self.protocol.response_to_dict(response) + if self._encode_filter_call is not None: + self._encode_filter_call(bound_obj, response, response_dict) + return self.protocol.encode_dict(response_dict) + + def encode_error_response(self, bound_obj: Any, exc: Exception) -> str: + """Given an error, return a response ready for sending.""" + response = self.protocol.error_to_response(exc) + response_dict = self.protocol.response_to_dict(response) + if self._encode_filter_call is not None: + self._encode_filter_call(bound_obj, response, response_dict) + return self.protocol.encode_dict(response_dict) def handle_raw_message(self, bound_obj: Any, @@ -183,18 +231,20 @@ class MessageReceiver: """ assert not self.is_async, "can't call sync handler on async receiver" try: - msg_decoded, msgtype = self._decode_incoming_message(msg) + msg_decoded, msgtype = self._decode_incoming_message( + bound_obj, msg) handler = self._handlers.get(msgtype) if handler is None: raise RuntimeError(f'Got unhandled message type: {msgtype}.') - result = handler(bound_obj, msg_decoded) - return self._encode_response(result, msgtype) + response = handler(bound_obj, msg_decoded) + assert isinstance(response, (Response, type(None))) + return self.encode_user_response(bound_obj, response, msgtype) except Exception as exc: if (raise_unregistered and isinstance(exc, UnregisteredMessageIDError)): raise - return self.protocol.encode_error_response(exc) + return self.encode_error_response(bound_obj, exc) async def handle_raw_message_async( self, @@ -207,18 +257,20 @@ class MessageReceiver: """ assert self.is_async, "can't call async handler on sync receiver" try: - msg_decoded, msgtype = self._decode_incoming_message(msg) + msg_decoded, msgtype = self._decode_incoming_message( + bound_obj, msg) handler = self._handlers.get(msgtype) if handler is None: raise RuntimeError(f'Got unhandled message type: {msgtype}.') - result = await handler(bound_obj, msg_decoded) - return self._encode_response(result, msgtype) + response = await handler(bound_obj, msg_decoded) + assert isinstance(response, (Response, type(None))) + return self.encode_user_response(bound_obj, response, msgtype) except Exception as exc: if (raise_unregistered and isinstance(exc, UnregisteredMessageIDError)): raise - return self.protocol.encode_error_response(exc) + return self.encode_error_response(bound_obj, exc) class BoundMessageReceiver: @@ -237,3 +289,7 @@ class BoundMessageReceiver: def protocol(self) -> MessageProtocol: """Protocol associated with this receiver.""" return self._receiver.protocol + + def encode_error_response(self, exc: Exception) -> str: + """Given an error, return a response ready to send.""" + return self._receiver.encode_error_response(self._obj, exc) diff --git a/tools/efro/message/_sender.py b/tools/efro/message/_sender.py index 8cd16beb..0a2c015f 100644 --- a/tools/efro/message/_sender.py +++ b/tools/efro/message/_sender.py @@ -8,12 +8,13 @@ from __future__ import annotations from typing import TYPE_CHECKING, TypeVar -from efro.message._message import Response +from efro.error import CleanError, RemoteError +from efro.message._message import (EmptyResponse, ErrorResponse, ErrorType) if TYPE_CHECKING: from typing import Any, Callable, Optional, Awaitable - from efro.message._message import Message + from efro.message._message import Message, Response from efro.message._protocol import MessageProtocol TM = TypeVar('TM', bound='MessageSender') @@ -44,6 +45,10 @@ class MessageSender: self._send_raw_message_call: Optional[Callable[[Any, str], str]] = None self._send_async_raw_message_call: Optional[Callable[ [Any, str], Awaitable[str]]] = None + self._encode_filter_call: Optional[Callable[[Any, Message, dict], + None]] = None + self._decode_filter_call: Optional[Callable[[Any, dict, Response], + None]] = None def send_method( self, call: Callable[[Any, str], @@ -61,6 +66,30 @@ class MessageSender: self._send_async_raw_message_call = call return call + def encode_filter_method( + self, call: Callable[[Any, Message, dict], None] + ) -> Callable[[Any, Message, dict], None]: + """Function decorator for defining an encode filter. + + Encode filters can be used to add extra data to the message + dict before is is encoded to a string and sent out. + """ + assert self._encode_filter_call is None + self._encode_filter_call = call + return call + + def decode_filter_method( + self, call: Callable[[Any, dict, Response], None] + ) -> Callable[[Any, dict, Response], None]: + """Function decorator for defining a decode filter. + + Decode filters can be used to extract extra data from incoming + message dicts. + """ + assert self._decode_filter_call is None + self._decode_filter_call = call + return call + def send(self, bound_obj: Any, message: Message) -> Optional[Response]: """Send a message and receive a response. @@ -69,14 +98,44 @@ class MessageSender: if self._send_raw_message_call is None: raise RuntimeError('send() is unimplemented for this type.') - msg_encoded = self.protocol.encode_message(message) + msg_encoded = self.encode_message(bound_obj, message) + response_encoded = self._send_raw_message_call(bound_obj, msg_encoded) - response = self.protocol.decode_response(response_encoded) - assert isinstance(response, (Response, type(None))) + + response = self.decode_response(bound_obj, response_encoded) assert (response is None or type(response) in type(message).get_response_types()) return response + def encode_message(self, bound_obj: Any, message: Message) -> str: + """Encode a message for sending.""" + msg_dict = self.protocol.message_to_dict(message) + if self._encode_filter_call is not None: + self._encode_filter_call(bound_obj, message, msg_dict) + return self.protocol.encode_dict(msg_dict) + + def decode_response(self, bound_obj: Any, + response_encoded: str) -> Optional[Response]: + """Decode, filter, and possibly act on raw response data.""" + response_dict = self.protocol.decode_dict(response_encoded) + response = self.protocol.response_from_dict(response_dict) + if self._decode_filter_call is not None: + self._decode_filter_call(bound_obj, response_dict, response) + + # Special case: if we get EmptyResponse, we simply return None. + if isinstance(response, EmptyResponse): + return None + + # Special case: a remote error occurred. Raise a local Exception + # instead of returning the message. + if isinstance(response, ErrorResponse): + if (self.protocol.preserve_clean_errors + and response.error_type is ErrorType.CLEAN): + raise CleanError(response.error_message) + raise RemoteError(response.error_message) + + return response + async def send_async(self, bound_obj: Any, message: Message) -> Optional[Response]: """Send a message asynchronously using asyncio. @@ -87,11 +146,12 @@ class MessageSender: if self._send_async_raw_message_call is None: raise RuntimeError('send_async() is unimplemented for this type.') - msg_encoded = self.protocol.encode_message(message) + msg_encoded = self.encode_message(bound_obj, message) + response_encoded = await self._send_async_raw_message_call( bound_obj, msg_encoded) - response = self.protocol.decode_response(response_encoded) - assert isinstance(response, (Response, type(None))) + + response = self.decode_response(bound_obj, response_encoded) assert (response is None or type(response) in type(message).get_response_types()) return response diff --git a/tools/efro/util.py b/tools/efro/util.py index f1eeb56e..44d098d7 100644 --- a/tools/efro/util.py +++ b/tools/efro/util.py @@ -4,8 +4,9 @@ from __future__ import annotations -import datetime +import os import time +import datetime import weakref import functools from enum import Enum @@ -652,7 +653,6 @@ def unchanging_hostname() -> str: network conditions. (A Mac will tend to go from Foo to Foo.local, Foo.lan etc. throughout its various adventures) """ - import os import platform import subprocess diff --git a/tools/efrotools/__init__.py b/tools/efrotools/__init__.py index 8cc19ddf..161f939f 100644 --- a/tools/efrotools/__init__.py +++ b/tools/efrotools/__init__.py @@ -96,12 +96,16 @@ def writefile(path: Union[str, Path], txt: str) -> None: outfile.write(txt) -def replace_one(opstr: str, old: str, new: str) -> str: - """Replace text ensuring that exactly one occurrence is replaced.""" - count = opstr.count(old) - if count != 1: - raise Exception( - f'expected 1 string occurrence; found {count}. String = {old}') +def replace_exact(opstr: str, old: str, new: str, count: int = 1) -> str: + """Replace text ensuring that exactly x occurrences are replaced. + + Useful when filtering data in some predefined way to ensure the original + has not changed. + """ + found = opstr.count(old) + if found != count: + raise Exception(f'expected {count} string occurrence(s);' + f' found {found}. String = {old}') return opstr.replace(old, new) diff --git a/tools/efrotools/pcommand.py b/tools/efrotools/pcommand.py index 3eaaffde..93f75f96 100644 --- a/tools/efrotools/pcommand.py +++ b/tools/efrotools/pcommand.py @@ -177,6 +177,31 @@ def spelling() -> None: _spelling(sys.argv[2:]) +def xcodebuild() -> None: + """Run xcodebuild with added smarts.""" + from efrotools.xcode import XCodeBuild + XCodeBuild(projroot=str(PROJROOT), args=sys.argv[2:]).run() + + +def xcoderun() -> None: + """Run an xcode build in the terminal.""" + import os + import subprocess + from efro.error import CleanError + from efrotools.xcode import project_build_path + if len(sys.argv) != 5: + raise CleanError( + 'Expected 3 args: ') + project_path = os.path.abspath(sys.argv[2]) + scheme = sys.argv[3] + configuration = sys.argv[4] + path = project_build_path(projroot=str(PROJROOT), + project_path=project_path, + scheme=scheme, + configuration=configuration) + subprocess.run(path, check=True) + + def pyver() -> None: """Prints the Python version used by this project.""" from efrotools import PYVER @@ -241,8 +266,9 @@ def gen_empty_py_init() -> None: Used as part of meta builds. """ from efro.terminal import Clr + from efro.error import CleanError if len(sys.argv) != 3: - raise Exception('Expected a single path arg.') + raise CleanError('Expected a single path arg.') outpath = Path(sys.argv[2]) outpath.parent.mkdir(parents=True, exist_ok=True) print(f'Meta-building {Clr.BLD}{outpath}{Clr.RST}') @@ -374,8 +400,9 @@ def androidstudiocode() -> None: def tool_config_install() -> None: """Install a tool config file (with some filtering).""" from efro.terminal import Clr + from efro.error import CleanError if len(sys.argv) != 4: - raise Exception('expected 2 args') + raise CleanError('expected 2 args') src = Path(sys.argv[2]) dst = Path(sys.argv[3]) @@ -590,6 +617,7 @@ def makefile_target_list() -> None: Takes a single argument: a path to a Makefile. """ from dataclasses import dataclass + from efro.error import CleanError from efro.terminal import Clr @dataclass @@ -599,7 +627,7 @@ def makefile_target_list() -> None: title: str if len(sys.argv) != 3: - raise RuntimeError('Expected exactly one filename arg.') + raise CleanError('Expected exactly one filename arg.') with open(sys.argv[2], encoding='utf-8') as infile: lines = infile.readlines() @@ -663,3 +691,34 @@ def echo() -> None: out.append(arg) out.append(Clr.RST) print(''.join(out)) + + +def urandom_pretty() -> None: + """Spits out urandom bytes formatted for source files.""" + # Note; this is not especially efficient. It should probably be rewritten + # if ever needed in a performance-sensitive context. + import os + from efro.error import CleanError + + if len(sys.argv) not in (3, 4): + raise CleanError( + 'Expected one arg (count) and possibly two (line len).') + size = int(sys.argv[2]) + linemax = 72 if len(sys.argv) < 4 else int(sys.argv[3]) + + val = os.urandom(size) + lines: list[str] = [] + line = b'' + + for i in range(len(val)): + char = val[i:i + 1] + thislinelen = len(repr(line + char)) + if thislinelen > linemax: + lines.append(repr(line)) + line = b'' + line += char + if line: + lines.append(repr(line)) + + bstr = '\n'.join(str(l) for l in lines) + print(f'({bstr})') diff --git a/tools/efrotools/pybuild.py b/tools/efrotools/pybuild.py index ea390171..67d0250c 100644 --- a/tools/efrotools/pybuild.py +++ b/tools/efrotools/pybuild.py @@ -8,16 +8,17 @@ import os import subprocess from typing import TYPE_CHECKING -from efrotools import PYVER, readfile, writefile, replace_one +from efrotools import readfile, writefile, replace_exact if TYPE_CHECKING: from typing import Any -ENABLE_OPENSSL = True -NEWER_PY_TEST = True +PY_VER = '3.10' +PY_VER_EXACT_ANDROID = '3.10.4' +PY_VER_EXACT_APPLE = '3.10.4' -PY_VER_EXACT_ANDROID = '3.9.10' -PY_VER_EXACT_APPLE = '3.9.6' +# ANDROID_PYTHON_REPO = 'https://github.com/yan12125/python3-android.git' +ANDROID_PYTHON_REPO = 'https://github.com/GRRedWings/python3-android' # Filenames we prune from Python lib dirs in source repo to cut down on size. PRUNE_LIB_NAMES = [ @@ -66,66 +67,80 @@ 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', PYVER], check=True) + subprocess.run(['git', 'checkout', PY_VER], check=True) txt = readfile('Makefile') # Fix a bug where spaces in PATH cause errors (darn you vmware fusion!) - txt = replace_one( - txt, '&& PATH=$(PROJECT_DIR)/$(PYTHON_DIR-macOS)/dist/bin:$(PATH) .', - '&& PATH="$(PROJECT_DIR)/$(PYTHON_DIR-macOS)/dist/bin:$(PATH)" .') + txt = replace_exact( + txt, + '\t\tPATH=$(PROJECT_DIR)/$(PYTHON_DIR-macOS)/_install/bin:$(PATH)', + '\t\tPATH="$(PROJECT_DIR)/$(PYTHON_DIR-macOS)/_install/bin:$(PATH)"') # Turn doc strings on; looks like it only adds a few hundred k. - txt = txt.replace('--without-doc-strings', '--with-doc-strings') + txt = replace_exact(txt, + '--without-doc-strings', + '--with-doc-strings', + count=2) - # Set mac/ios version reqs - # (see issue with utimensat and futimens). - txt = replace_one(txt, 'MACOSX_DEPLOYMENT_TARGET=10.8', - 'MACOSX_DEPLOYMENT_TARGET=10.15') - # And equivalent iOS (11+). - txt = replace_one(txt, 'CFLAGS-iOS=-mios-version-min=8.0', - 'CFLAGS-iOS=-mios-version-min=13.0') - # Ditto for tvOS. - txt = replace_one(txt, 'CFLAGS-tvOS=-mtvos-version-min=9.0', - 'CFLAGS-tvOS=-mtvos-version-min=13.0') + # Customize our minimum version requirements + txt = replace_exact( + txt, + 'CFLAGS-macOS=-mmacosx-version-min=10.15\n', + 'CFLAGS-macOS=-mmacosx-version-min=10.15\n', + ) + txt = replace_exact( + txt, + 'CFLAGS-iOS=-mios-version-min=12.0 ', + 'CFLAGS-iOS=-mios-version-min=12.0 ', + ) + txt = replace_exact( + txt, + 'CFLAGS-tvOS=-mtvos-version-min=9.0 ', + 'CFLAGS-tvOS=-mtvos-version-min=9.0 ', + ) + assert '--with-pydebug' not in txt if debug: # Add debug build flag - # (Currently expect to find 2 instances of this). - dline = '--with-doc-strings --enable-ipv6 --without-ensurepip' - splitlen = len(txt.split(dline)) - if splitlen != 3: - raise Exception('unexpected configure lines') - txt = txt.replace(dline, '--with-pydebug ' + dline) + txt = replace_exact( + txt, + '--enable-ipv6 --without-ensurepip ', + '--enable-ipv6 --with-pydebug --without-ensurepip ', + count=2, + ) - # Debug has a different name. - # (Currently expect to replace 12 instances of this). - dline = ('python$(PYTHON_VER)' - if NEWER_PY_TEST else 'python$(PYTHON_VER)m') - splitlen = len(txt.split(dline)) - if splitlen != 13: - raise RuntimeError(f'Unexpected configure line count {splitlen}.') - txt = txt.replace( - dline, 'python$(PYTHON_VER)d' - if NEWER_PY_TEST else 'python$(PYTHON_VER)dm') + # Debug lib has a different name. + txt = replace_exact(txt, + 'python$(PYTHON_VER).a', + 'python$(PYTHON_VER)d.a', + count=2) - # Inject our custom modifications to fire before building. - txt = txt.replace( - ' # Configure target Python\n', - ' cd $$(PYTHON_DIR-$1) && ' - f'../../../../../tools/pcommand python_apple_patch {arch}\n' - ' # Configure target Python\n', - ) + txt = replace_exact(txt, + '/include/python$(PYTHON_VER)', + '/include/python$(PYTHON_VER)d', + count=4) - # Use python3 instead of python for libffi setup script - txt = replace_one( - txt, - 'cd $$(LIBFFI_DIR-$1) && python generate-darwin-source-and-headers.py' - " --only-$(shell echo $1 | tr '[:upper:]' '[:lower:]')", - 'cd $$(LIBFFI_DIR-$1) && python3 generate-darwin-source-and-headers.py' - " --only-$(shell echo $1 | tr '[:upper:]' '[:lower:]')", - ) + # Inject our custom modifications to fire right after their normal + # Setup.local filtering and right before building (and pass the same + # 'slice' value they use so we can use it too). + txt = replace_exact( + txt, '\t\t\tsed -e "s/{{slice}}/$$(SLICE-$$(SDK-$(target)))/g" \\\n' + '\t\t\t> $$(PYTHON_DIR-$(target))/Modules/Setup.local\n', + '\t\t\tsed -e "s/{{slice}}/$$(SLICE-$$(SDK-$(target)))/g" \\\n' + '\t\t\t> $$(PYTHON_DIR-$(target))/Modules/Setup.local\n' + '\tcd $$(PYTHON_DIR-$(target)) && ' + f'../../../../../tools/pcommand python_apple_patch {arch} ' + '"$$(SLICE-$$(SDK-$(target)))"\n') + txt = replace_exact( + txt, '\t\t\tsed -e "s/{{slice}}/$$(SLICE-macosx)/g" \\\n' + '\t\t\t> $$(PYTHON_DIR-$(os))/Modules/Setup.local\n', + '\t\t\tsed -e "s/{{slice}}/$$(SLICE-macosx)/g" \\\n' + '\t\t\t> $$(PYTHON_DIR-$(os))/Modules/Setup.local\n' + '\tcd $$(PYTHON_DIR-$(os)) && ' + f'../../../../../tools/pcommand python_apple_patch {arch} ' + '"$$(SLICE-macosx)"\n') writefile('Makefile', txt) @@ -146,20 +161,6 @@ def build_apple(arch: str, debug: bool = False) -> None: print('python build complete! (apple/' + arch + ')') -def apple_patch(arch: str) -> None: - """Run necessary patches on an apple archive before building.""" - - # Here's the deal: we want our custom static python libraries to - # be as similar as possible on apple platforms and android, so let's - # blow away all the tweaks that this setup does to Setup.local and - # instead apply our very similar ones directly to Setup, just as we - # do for android. - with open('Modules/Setup.local', 'w', encoding='utf-8') as outfile: - outfile.write('# cleared by efrotools build\n') - - _patch_setup_file('apple', arch) - - def build_android(rootdir: str, arch: str, debug: bool = False) -> None: """Run a build for android with the given architecture. @@ -170,14 +171,14 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None: subprocess.run(['rm', '-rf', builddir], check=True) subprocess.run(['mkdir', '-p', 'build'], check=True) subprocess.run( - [ - 'git', 'clone', 'https://github.com/yan12125/python3-android.git', - builddir - ], + ['git', 'clone', ANDROID_PYTHON_REPO, builddir], check=True, ) os.chdir(builddir) + # TEMP - use 3.9.6 branch + # subprocess.run(['git', 'checkout', PY_VER_EXACT_ANDROID], check=True) + # These builds require ANDROID_NDK to be set; make sure that's the case. os.environ['ANDROID_NDK'] = subprocess.check_output( [f'{rootdir}/tools/pcommand', 'android_sdk_utils', @@ -185,9 +186,9 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None: # Disable builds for dependencies we don't use. ftxt = readfile('Android/build_deps.py') - # ftxt = replace_one(ftxt, ' NCurses,\n', + # ftxt = replace_exact(ftxt, ' NCurses,\n', # '# NCurses,\n',) - ftxt = replace_one( + ftxt = replace_exact( ftxt, ' ' 'BZip2, GDBM, LibFFI, LibUUID, OpenSSL, Readline, SQLite, XZ, ZLib,\n', @@ -196,13 +197,14 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None: ) # Older ssl seems to choke on newer ndk layouts. - ftxt = replace_one( - ftxt, - "source = 'https://www.openssl.org/source/openssl-1.1.1h.tar.gz'", - "source = 'https://www.openssl.org/source/openssl-1.1.1l.tar.gz'") + if bool(False): + ftxt = replace_exact( + ftxt, + "source = 'https://www.openssl.org/source/openssl-1.1.1h.tar.gz'", + "source = 'https://www.openssl.org/source/openssl-1.1.1l.tar.gz'") # Give ourselves a handle to patch the OpenSSL build. - ftxt = replace_one( + ftxt = replace_exact( ftxt, ' # OpenSSL handles NDK internal paths by itself', ' # Ericf addition: do some patching:\n' @@ -217,8 +219,9 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None: # of Python and also inject some code to modify bits of python # after it is extracted. ftxt = readfile('build.sh') - ftxt = replace_one(ftxt, 'PYVER=3.9.0', f'PYVER={PY_VER_EXACT_ANDROID}') - ftxt = replace_one( + + ftxt = replace_exact(ftxt, 'PYVER=3.10.4', f'PYVER={PY_VER_EXACT_ANDROID}') + ftxt = replace_exact( ftxt, ' popd\n', f' ../../../tools/pcommand' f' python_android_patch Python-{PY_VER_EXACT_ANDROID}\n popd\n') writefile('build.sh', ftxt) @@ -231,14 +234,29 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None: print('python build complete! (android/' + arch + ')') +def apple_patch(arch: str, slc: str) -> None: + """Run necessary patches on an apple archive before building.""" + + # Here's the deal: we want our custom static python libraries to + # be as similar as possible on apple platforms and android, so let's + # blow away all the tweaks that this setup does to Setup.local and + # instead apply our very similar ones directly to Setup, just as we + # do for android. + with open('Modules/Setup.local', 'w', encoding='utf-8') as outfile: + outfile.write('# cleared by efrotools build\n') + + _patch_setup_file('apple', arch, slc) + + def android_patch() -> None: """Run necessary patches on an android archive before building.""" - _patch_setup_file('android', '?') + _patch_setup_file('android', '?', '?') -def _patch_setup_file(platform: str, arch: str) -> None: +def _patch_setup_file(platform: str, arch: str, slc: str) -> None: # pylint: disable=too-many-locals # pylint: disable=too-many-statements + fname = 'Modules/Setup' ftxt = readfile(fname) @@ -252,18 +270,33 @@ def _patch_setup_file(platform: str, arch: str) -> None: hash_ex = ' -DUSE_SSL -lssl -lcrypto' lzma_ex = ' -llzma' elif platform == 'apple': - prefix = '$(srcdir)/Android/sysroot/usr' + # These should basically match what the Python-Apple-support dist + # does in its patch/Python/Setup.embedded. + # (We do our own thing and ignore the Setup.local it generates + # for the sake of cross-platform consistency, but still need to + # do what we do the same way they do what they do) + + def _slrp(val: str) -> str: + # In the distro, the Makefile does this filtering to Setup.local + return val.replace('{{slice}}', slc) + uuid_ex = '' zlib_ex = ' -I$(prefix)/include -lz' - bz2_ex = (' -I$(srcdir)/../Support/BZip2/Headers' - ' -L$(srcdir)/../Support/BZip2 -lbzip2') - ssl_ex = (' -I$(srcdir)/../Support/OpenSSL/Headers' - ' -L$(srcdir)/../Support/OpenSSL -lOpenSSL -DUSE_SSL') + bz2_ex = _slrp( + ' -I$(srcdir)/../Support/BZip2.xcframework/{{slice}}/Headers' + ' -L$(srcdir)/../Support/BZip2.xcframework/{{slice}} -lbzip2') + ssl_ex = _slrp( + ' -I$(srcdir)/../Support/OpenSSL.xcframework/{{slice}}/Headers' + ' -L$(srcdir)/../Support/OpenSSL.xcframework/{{slice}}' + ' -lOpenSSL -DUSE_SSL') sqlite_ex = ' -I$(srcdir)/Modules/_sqlite' - hash_ex = (' -I$(srcdir)/../Support/OpenSSL/Headers' - ' -L$(srcdir)/../Support/OpenSSL -lOpenSSL -DUSE_SSL') - lzma_ex = (' -I$(srcdir)/../Support/XZ/Headers' - ' -L$(srcdir)/../Support/XZ/ -lxz') + hash_ex = _slrp( + ' -I$(srcdir)/../Support/OpenSSL.xcframework/{{slice}}/Headers' + ' -L$(srcdir)/../Support/OpenSSL.xcframework/{{slice}}' + ' -lOpenSSL -DUSE_SSL') + lzma_ex = _slrp( + ' -I$(srcdir)/../Support/XZ.xcframework/{{slice}}/Headers' + ' -L$(srcdir)/../Support/XZ.xcframework/{{slice}} -lxz') else: raise RuntimeError(f'Unknown platform {platform}') @@ -301,13 +334,13 @@ def _patch_setup_file(platform: str, arch: str) -> None: enables += ['_md5'] for enable in enables: - ftxt = replace_one(ftxt, f'#{enable} ', f'{enable} ') + ftxt = replace_exact(ftxt, f'#{enable} ', f'{enable} ') cmodules.remove(enable) # Disable ones that were enabled: disables = ['xxsubtype'] for disable in disables: - ftxt = replace_one(ftxt, f'\n{disable} ', f'\n#{disable} ') + ftxt = replace_exact(ftxt, f'\n{disable} ', f'\n#{disable} ') # Additions: ftxt += '\n# Additions by efrotools:\n' @@ -374,10 +407,10 @@ def _patch_setup_file(platform: str, arch: str) -> None: fname = 'Modules/makesetup' txt = readfile(fname) if platform == 'android': - txt = replace_one(txt, ' *=*)' - ' DEFS="$line$NL$DEFS"; continue;;', - ' [A-Z]*=*) DEFS="$line$NL$DEFS";' - ' continue;;') + txt = replace_exact(txt, ' *=*)' + ' DEFS="$line$NL$DEFS"; continue;;', + ' [A-Z]*=*) DEFS="$line$NL$DEFS";' + ' continue;;') assert txt.count('[A-Z]*=*') == 1 writefile(fname, txt) @@ -394,7 +427,7 @@ def android_patch_ssl() -> None: # but it seems cleaner to just have things work by default. fname = 'crypto/getenv.c' txt = readfile(fname) - txt = replace_one( + txt = replace_exact( txt, ('char *ossl_safe_getenv(const char *name)\n' '{\n'), @@ -455,7 +488,7 @@ def gather() -> None: debug = buildtype == 'debug' bsuffix = '_debug' if buildtype == 'debug' else '' bsuffix2 = '-debug' if buildtype == 'debug' else '' - libname = 'python' + PYVER + ('d' if debug else '') + libname = 'python' + PY_VER + ('d' if debug else '') bases = { 'mac': f'build/python_apple_mac{bsuffix}/build/macOS', @@ -529,7 +562,7 @@ def gather() -> None: bases2['android_arm'] + '/usr/lib/libuuid.a', ], 'libinst': 'android_armeabi-v7a', - 'pylib': (bases['android_arm'] + '/usr/lib/python' + PYVER), + 'pylib': (bases['android_arm'] + '/usr/lib/python' + PY_VER), }, { 'name': 'android_arm64', 'group': 'android', @@ -616,7 +649,7 @@ def gather() -> None: # so let's skip that. fname = f'{assets_src_dst}/site.py' txt = readfile(fname) - txt = replace_one( + txt = replace_exact( txt, ' known_paths = addusersitepackages(known_paths)', ' # efro tweak: this craps out on ios/tvos.\n' diff --git a/tools/efrotools/xcode.py b/tools/efrotools/xcode.py new file mode 100644 index 00000000..142befa5 --- /dev/null +++ b/tools/efrotools/xcode.py @@ -0,0 +1,570 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to Xcode on Apple platforms.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import time +import shlex +from enum import Enum +from typing import TYPE_CHECKING + +from efro.terminal import Clr +from efro.error import CleanError +from efro.util import assert_never + +if TYPE_CHECKING: + from typing import Any, Optional + + +class _Section(Enum): + COMPILEC = 'CompileC' + MKDIR = 'MkDir' + LD = 'Ld' + COMPILEASSETCATALOG = 'CompileAssetCatalog' + CODESIGN = 'CodeSign' + COMPILESTORYBOARD = 'CompileStoryboard' + LINKSTORYBOARDS = 'LinkStoryboards' + PROCESSINFOPLISTFILE = 'ProcessInfoPlistFile' + COPYSWIFTLIBS = 'CopySwiftLibs' + REGISTEREXECUTIONPOLICYEXCEPTION = 'RegisterExecutionPolicyException' + VALIDATE = 'Validate' + TOUCH = 'Touch' + REGISTERWITHLAUNCHSERVICES = 'RegisterWithLaunchServices' + METALLINK = 'MetalLink' + COMPILESWIFT = 'CompileSwift' + CREATEBUILDDIRECTORY = 'CreateBuildDirectory' + COMPILEMETALFILE = 'CompileMetalFile' + COPY = 'Copy' + COPYSTRINGSFILE = 'CopyStringsFile' + WRITEAUXILIARYFILE = 'WriteAuxiliaryFile' + COMPILESWIFTSOURCES = 'CompileSwiftSources' + PROCESSPCH = 'ProcessPCH' + PROCESSPCHPLUSPLUS = 'ProcessPCH++' + PHASESCRIPTEXECUTION = 'PhaseScriptExecution' + + +class XCodeBuild: + """xcodebuild wrapper with extra bells and whistles.""" + + def __init__(self, projroot: str, args: list[str]): + self._projroot = projroot + self._args = args + self._output: list[str] = [] + self._verbose = os.environ.get('XCODEBUILDVERBOSE', '0') == '1' + self._section: Optional[_Section] = None + self._section_line_count = 0 + self._returncode: Optional[int] = None + self._project: str = self._argstr(args, '-project') + self._scheme: str = self._argstr(args, '-scheme') + self._configuration: str = self._argstr(args, '-configuration') + + def run(self) -> None: + """Do the thing.""" + self._run_cmd(self._build_cmd_args()) + assert self._returncode is not None + + # In some failure cases we may want to run a clean and try again. + if self._returncode != 0: + + # Getting this error sometimes after xcode updates. + if 'error: PCH file built from a different branch' in '\n'.join( + self._output): + print(f'{Clr.MAG}WILL CLEAN AND' + f' RE-ATTEMPT XCODE BUILD{Clr.RST}') + self._run_cmd([ + 'xcodebuild', '-project', self._project, '-scheme', + self._scheme, '-configuration', self._configuration, + 'clean' + ]) + # Now re-run the original build. + print(f'{Clr.MAG}RE-ATTEMPTING XCODE BUILD' + f' AFTER CLEAN{Clr.RST}') + self._run_cmd(self._build_cmd_args()) + + if self._returncode != 0: + raise CleanError(f'Command failed with code {self._returncode}.') + + @staticmethod + def _argstr(args: list[str], flag: str) -> str: + try: + return args[args.index(flag) + 1] + except (ValueError, IndexError) as exc: + raise RuntimeError(f'{flag} value not found') from exc + + def _build_cmd_args(self) -> list[str]: + return ['xcodebuild'] + self._args + + def _run_cmd(self, cmd: list[str]) -> None: + # reset some state + self._output = [] + self._section = None + self._returncode = 0 + print(f'{Clr.BLU}Running build: {Clr.BLD}{cmd}{Clr.RST}') + with subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) as proc: + if proc.stdout is None: + raise RuntimeError('Error running command') + while True: + line = proc.stdout.readline().decode() + if len(line) == 0: + break + self._output.append(line) + self._print_filtered_line(line) + proc.wait() + self._returncode = proc.returncode + + def _print_filtered_line(self, line: str) -> None: + + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + + # NOTE: xcodebuild output can be coming from multiple tasks and + # intermingled, so lets try to be as conservative as possible when + # hiding lines. When we're not 100% sure we know what a line is, + # we should print it to be sure. + + if self._verbose: + sys.stdout.write(line) + return + + # Look for a few special cases regardless of the section we're in: + if line == '** BUILD SUCCEEDED **\n': + sys.stdout.write( + f'{Clr.GRN}{Clr.BLD}XCODE BUILD SUCCEEDED{Clr.RST}\n') + return + + if line == '** CLEAN SUCCEEDED **\n': + sys.stdout.write( + f'{Clr.GRN}{Clr.BLD}XCODE CLEAN SUCCEEDED{Clr.RST}\n') + return + + if 'warning: OpenGL is deprecated.' in line: + return # yes Apple, I know. + + # xcodebuild output generally consists of some high level command + # ('CompileC blah blah blah') followed by a number of related lines. + # Look for particular high level commands to switch us into different + # modes. + sectionchanged = False + for section in _Section: + if line.startswith(f'{section.value} '): + self._section = section + sectionchanged = True + + if sectionchanged: + self._section_line_count = 0 + else: + self._section_line_count += 1 + + # There's a lot of random chatter at the start of builds, + # so let's go ahead and ignore everything before we've got a + # line-mode set. + if self._section is None: + return + if self._section is _Section.COMPILEC: + self._print_compilec_line(line) + elif self._section is _Section.MKDIR: + self._print_mkdir_line(line) + elif self._section is _Section.LD: + self._print_ld_line(line) + elif self._section is _Section.COMPILEASSETCATALOG: + self._print_compile_asset_catalog_line(line) + elif self._section is _Section.CODESIGN: + self._print_code_sign_line(line) + elif self._section is _Section.COMPILESTORYBOARD: + self._print_compile_storyboard_line(line) + elif self._section is _Section.LINKSTORYBOARDS: + self._print_simple_section_line( + line, ignore_line_start_tails=['/ibtool']) + elif self._section is _Section.PROCESSINFOPLISTFILE: + self._print_process_info_plist_file_line(line) + elif self._section is _Section.COPYSWIFTLIBS: + self._print_simple_section_line( + line, ignore_line_starts=['builtin-swiftStdLibTool']) + elif self._section is _Section.REGISTEREXECUTIONPOLICYEXCEPTION: + self._print_simple_section_line( + line, + ignore_line_starts=[ + 'builtin-RegisterExecutionPolicyException' + ]) + elif self._section is _Section.VALIDATE: + self._print_simple_section_line( + line, ignore_line_starts=['builtin-validationUtility']) + elif self._section is _Section.TOUCH: + self._print_simple_section_line( + line, ignore_line_starts=['/usr/bin/touch']) + elif self._section is _Section.REGISTERWITHLAUNCHSERVICES: + self._print_simple_section_line( + line, ignore_line_start_tails=['lsregister']) + elif self._section is _Section.METALLINK: + self._print_simple_section_line(line, + prefix='Linking', + ignore_line_start_tails=['/metal']) + elif self._section is _Section.COMPILESWIFT: + self._print_simple_section_line( + line, + prefix='Compiling', + prefix_index=3, + ignore_line_start_tails=['/swift-frontend', 'EmitSwiftModule']) + elif self._section is _Section.CREATEBUILDDIRECTORY: + self._print_simple_section_line( + line, ignore_line_starts=['builtin-create-build-directory']) + elif self._section is _Section.COMPILEMETALFILE: + self._print_simple_section_line(line, + prefix='Metal-Compiling', + ignore_line_start_tails=['/metal']) + elif self._section is _Section.COPY: + self._print_simple_section_line( + line, ignore_line_starts=['builtin-copy']) + elif self._section is _Section.COPYSTRINGSFILE: + self._print_simple_section_line(line, + ignore_line_starts=[ + 'builtin-copyStrings', + 'CopyPNGFile', + 'ConvertIconsetFile' + ], + ignore_line_start_tails=[ + '/InfoPlist.strings:1:1:', + '/copypng', '/iconutil' + ]) + elif self._section is _Section.WRITEAUXILIARYFILE: + # EW: this spits out our full list of entitlements line by line. + # We should make this smart enough to ignore that whole section + # but just ignoring specific exact lines for now. + self._print_simple_section_line( + line, + ignore_line_starts=[ + 'PhaseScriptExecution', + '/bin/sh -c', + 'write-file', + 'builtin-productPackagingUtility', + 'ProcessProductPackaging', + 'Entitlements:', + '{', + '}', + ');', + '};', + '"com.apple.security.get-task-allow"' + '"com.apple.security.app-sandbox"', + '"com.apple.Music"', + '"com.apple.Music.library.read"', + '"com.apple.Music.playback"', + '"com.apple.security.app-sandbox"', + '"com.apple.security.automation.apple-events"', + '"com.apple.security.device.bluetooth"', + '"com.apple.security.device.usb"', + '"com.apple.security.get-task-allow"', + '"com.apple.security.network.client"', + '"com.apple.security.network.server"', + '"com.apple.security.scripting-targets"', + '"com.apple.Music.library.read",', + ]) + elif self._section is _Section.COMPILESWIFTSOURCES: + self._print_simple_section_line( + line, + prefix='Compiling Swift Sources', + prefix_index=None, + ignore_line_starts=['PrecompileSwiftBridgingHeader'], + ignore_line_start_tails=['/swiftc', '/swift-frontend']) + elif self._section is _Section.PROCESSPCH: + self._print_simple_section_line( + line, + ignore_line_starts=['Precompile of'], + ignore_line_start_tails=['/clang']) + elif self._section is _Section.PROCESSPCHPLUSPLUS: + self._print_simple_section_line( + line, + ignore_line_starts=['Precompile of'], + ignore_line_start_tails=['/clang']) + elif self._section is _Section.PHASESCRIPTEXECUTION: + self._print_simple_section_line(line, + prefix='Running Script', + ignore_line_starts=['/bin/sh']) + else: + assert_never(self._section) + + def _print_compilec_line(self, line: str) -> None: + + # First line of the section. + if self._section_line_count == 0: + fname = os.path.basename(shlex.split(line)[2]) + sys.stdout.write(f'{Clr.BLU}Compiling {Clr.BLD}{fname}{Clr.RST}\n') + return + + # Ignore empty lines or things we expect to be there. + splits = line.split() + if not splits: + return + if splits[0] in ['cd', 'export']: + return + if splits[0].endswith('/clang'): + return + + # Fall back on printing anything we don't recognize. + sys.stdout.write(line) + + def _print_mkdir_line(self, line: str) -> None: + + # First line of the section. + if self._section_line_count == 0: + return + + # Ignore empty lines or things we expect to be there. + splits = line.split() + if not splits: + return + if splits[0] in ['cd', '/bin/mkdir']: + return + + # Fall back on printing anything we don't recognize. + sys.stdout.write(line) + + def _print_ld_line(self, line: str) -> None: + + # First line of the section. + if self._section_line_count == 0: + name = os.path.basename(shlex.split(line)[1]) + sys.stdout.write(f'{Clr.BLU}Linking {Clr.BLD}{name}{Clr.RST}\n') + return + + # Ignore empty lines or things we expect to be there. + splits = line.split() + if not splits: + return + if splits[0] in ['cd']: + return + if splits[0].endswith('/clang++'): + return + + # Fall back on printing anything we don't recognize. + sys.stdout.write(line) + + def _print_compile_asset_catalog_line(self, line: str) -> None: + # pylint: disable=too-many-return-statements + + # First line of the section. + if self._section_line_count == 0: + name = os.path.basename(shlex.split(line)[1]) + sys.stdout.write( + f'{Clr.BLU}Compiling Asset Catalog {Clr.BLD}{name}{Clr.RST}\n') + return + + # Ignore empty lines or things we expect to be there. + line_s = line.strip() + splits = line.split() + if not splits: + return + if splits[0] in ['cd']: + return + if splits[0].endswith('/actool'): + return + if line_s == '/* com.apple.actool.compilation-results */': + return + if (' ibtoold[' in line_s + and 'NSFileCoordinator is doing nothing' in line_s): + return + if any(line_s.endswith(x) for x in ('.plist', '.icns', '.car')): + return + + # Fall back on printing anything we don't recognize. + sys.stdout.write(line) + + def _print_compile_storyboard_line(self, line: str) -> None: + + # First line of the section. + if self._section_line_count == 0: + name = os.path.basename(shlex.split(line)[1]) + sys.stdout.write( + f'{Clr.BLU}Compiling Storyboard {Clr.BLD}{name}{Clr.RST}\n') + return + + # Ignore empty lines or things we expect to be there. + splits = line.split() + if not splits: + return + if splits[0] in ['cd', 'export']: + return + if splits[0].endswith('/ibtool'): + return + + # Fall back on printing anything we don't recognize. + sys.stdout.write(line) + + def _print_code_sign_line(self, line: str) -> None: + + # First line of the section. + if self._section_line_count == 0: + name = os.path.basename(shlex.split(line)[1]) + sys.stdout.write(f'{Clr.BLU}Signing' + f' {Clr.BLD}{name}{Clr.RST}\n') + return + + # Ignore empty lines or things we expect to be there. + splits = line.split() + if not splits: + return + if splits[0] in ['cd', 'export', '/usr/bin/codesign']: + return + if line.strip().startswith('Signing Identity:'): + return + if ': replacing existing signature' in line: + return + + # Fall back on printing anything we don't recognize. + sys.stdout.write(line) + + def _print_process_info_plist_file_line(self, line: str) -> None: + + # First line of the section. + if self._section_line_count == 0: + name = os.path.basename(shlex.split(line)[1]) + sys.stdout.write(f'{Clr.BLU}Processing {Clr.BLD}{name}{Clr.RST}\n') + return + + # Ignore empty lines or things we expect to be there. + splits = line.split() + if not splits: + return + if splits[0] in ['cd', 'export', 'builtin-infoPlistUtility']: + return + + # Fall back on printing anything we don't recognize. + sys.stdout.write(line) + + def _print_simple_section_line( + self, + line: str, + prefix: str = None, + prefix_index: Optional[int] = 1, + ignore_line_starts: list[str] = None, + ignore_line_start_tails: list[str] = None) -> None: + + if ignore_line_starts is None: + ignore_line_starts = [] + if ignore_line_start_tails is None: + ignore_line_start_tails = [] + + # First line of the section. + if self._section_line_count == 0: + if prefix is not None: + if prefix_index is None: + sys.stdout.write(f'{Clr.BLU}{prefix}{Clr.RST}\n') + else: + name = os.path.basename(shlex.split(line)[prefix_index]) + sys.stdout.write(f'{Clr.BLU}{prefix}' + f' {Clr.BLD}{name}{Clr.RST}\n') + return + + # Ignore empty lines or things we expect to be there. + splits = line.split() + if not splits: + return + for start in ['cd', 'export'] + ignore_line_starts: + # The start strings they pass may themselves be splittable so + # we may need to compare more than one string. + startsplits = start.split() + if splits[:len(startsplits)] == startsplits: + return + if any(splits[0].endswith(tail) for tail in ignore_line_start_tails): + return + + # Fall back on printing anything we don't recognize. + if prefix is None: + # If a prefix was not supplied for this section, the user will + # have no way to know what this output relates to. Tack a bit + # on to clarify in that case. + assert self._section is not None + sys.stdout.write(f'{Clr.YLW}Unexpected {self._section.value}' + f' Output:{Clr.RST} {line}') + else: + sys.stdout.write(line) + + +def project_build_path(projroot: str, project_path: str, scheme: str, + configuration: str) -> str: + """Get build paths for an xcode project (cached for efficiency).""" + # pylint: disable=too-many-locals + + config_path = os.path.join(projroot, '.cache', 'xcode_build_path') + config: dict[str, dict[str, Any]] = {} + + build_dir: Optional[str] = None + executable_path: Optional[str] = None + + if os.path.exists(config_path): + with open(config_path, encoding='utf-8') as infile: + config = json.loads(infile.read()) + if (project_path in config and configuration in config[project_path] + and scheme in config[project_path][configuration]): + + # Ok we've found a build-dir entry for this project; now if it + # exists on disk and all timestamps within it are decently + # close to the one we've got recorded, lets use it. + # (Anything using this script should also be building + # stuff there so mod times should be pretty recent; if not + # then its worth re-caching to be sure.) + cached_build_dir = config[project_path][configuration][scheme][ + 'build_dir'] + cached_timestamp = config[project_path][configuration][scheme][ + 'timestamp'] + cached_executable_path = config[project_path][configuration][ + scheme]['executable_path'] + assert isinstance(cached_build_dir, str) + assert isinstance(cached_timestamp, float) + assert isinstance(cached_executable_path, str) + now = time.time() + if (os.path.isdir(cached_build_dir) + and abs(now - cached_timestamp) < 60 * 60 * 24): + build_dir = cached_build_dir + executable_path = cached_executable_path + + # If we don't have a path at this point we look it up and cache it. + if build_dir is None: + print('Caching xcode build path...', file=sys.stderr) + cmd = [ + 'xcodebuild', '-project', project_path, '-showBuildSettings', + '-configuration', configuration, '-scheme', scheme + ] + output = subprocess.run(cmd, check=True, + capture_output=True).stdout.decode() + + prefix = 'TARGET_BUILD_DIR = ' + lines = [ + l for l in output.splitlines() if l.strip().startswith(prefix) + ] + if len(lines) != 1: + raise Exception( + 'TARGET_BUILD_DIR not found in xcodebuild settings output') + build_dir = lines[0].replace(prefix, '').strip() + + prefix = 'EXECUTABLE_PATH = ' + lines = [ + l for l in output.splitlines() if l.strip().startswith(prefix) + ] + if len(lines) != 1: + raise Exception( + 'EXECUTABLE_PATH not found in xcodebuild settings output') + executable_path = lines[0].replace(prefix, '').strip() + + if project_path not in config: + config[project_path] = {} + if configuration not in config[project_path]: + config[project_path][configuration] = {} + config[project_path][configuration][scheme] = { + 'build_dir': build_dir, + 'executable_path': executable_path, + 'timestamp': time.time() + } + os.makedirs(os.path.dirname(config_path), exist_ok=True) + with open(config_path, 'w', encoding='utf-8') as outfile: + outfile.write(json.dumps(config)) + + assert build_dir is not None + assert executable_path is not None + return os.path.join(build_dir, executable_path) diff --git a/tools/pcommand b/tools/pcommand index 70981a97..15bb1b9e 100755 --- a/tools/pcommand +++ b/tools/pcommand @@ -23,7 +23,7 @@ from efrotools.pcommand import ( cpplint, pylint, pylint_files, mypy, runmypy, dmypy, tool_config_install, sync, sync_all, scriptfiles, pycharm, clioncode, androidstudiocode, makefile_target_list, spelling, spelling_all, pytest, echo, - compile_python_files, pyver, try_repeat) + compile_python_files, pyver, try_repeat, xcodebuild, xcoderun) from batools.pcommand import ( stage_server_file, py_examine, resize_image, check_clean_safety, clean_orphaned_assets, archive_old_builds, lazy_increment_build, @@ -41,9 +41,8 @@ from batools.pcommand import ( update_cmake_prefab_lib, cmake_prep_dir, gen_binding_code, gen_flat_data_code, wsl_path_to_win, wsl_build_check_win_drive, win_ci_binary_build, genchangelog, android_sdk_utils, - update_resources_makefile, update_meta_makefile, xcode_build_path, - gen_python_enums_module, gen_python_init_module, update_dummy_module, - win_ci_install_prereqs) + update_resources_makefile, update_meta_makefile, gen_python_enums_module, + gen_python_init_module, update_dummy_module, win_ci_install_prereqs) # pylint: enable=unused-import if TYPE_CHECKING: