initial 1.7 build

This commit is contained in:
Eric Froemling 2022-05-28 11:54:29 -07:00
parent b506c2c9b3
commit a7a32a3f63
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
95 changed files with 2147 additions and 859 deletions

View File

@ -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"
}

View File

@ -21,8 +21,11 @@
<w>abouttab</w>
<w>abtn</w>
<w>accesstime</w>
<w>accountclientv</w>
<w>accountname</w>
<w>accountui</w>
<w>accountv</w>
<w>accountvalues</w>
<w>accum</w>
<w>accumkillcount</w>
<w>accumkilledcount</w>
@ -42,6 +45,7 @@
<w>activityteam</w>
<w>activitytypes</w>
<w>activityutils</w>
<w>actool</w>
<w>actorclass</w>
<w>adbcfaca</w>
<w>adbpath</w>
@ -111,6 +115,7 @@
<w>argh</w>
<w>argparse</w>
<w>argsjoined</w>
<w>argstr</w>
<w>argtypes</w>
<w>argval</w>
<w>armeabi</w>
@ -206,6 +211,7 @@
<w>bastd</w>
<w>batools</w>
<w>batoolsinternal</w>
<w>baworker</w>
<w>bbbb</w>
<w>bblk</w>
<w>bblu</w>
@ -276,6 +282,7 @@
<w>bsstd</w>
<w>bstat</w>
<w>bstournament</w>
<w>bstr</w>
<w>bsuffix</w>
<w>bsui</w>
<w>btnh</w>
@ -374,6 +381,7 @@
<w>checkins</w>
<w>checkpaths</w>
<w>checkroundover</w>
<w>checksummed</w>
<w>checksums</w>
<w>checktype</w>
<w>childanntype</w>
@ -443,7 +451,13 @@
<w>comms</w>
<w>compat</w>
<w>compileall</w>
<w>compileassetcatalog</w>
<w>compilec</w>
<w>compilelocations</w>
<w>compilemetalfile</w>
<w>compilestoryboard</w>
<w>compileswift</w>
<w>compileswiftsources</w>
<w>completeargs</w>
<w>completecmd</w>
<w>compounddict</w>
@ -475,9 +489,12 @@
<w>coopscorescreen</w>
<w>coopsession</w>
<w>coords</w>
<w>copypng</w>
<w>copyreg</w>
<w>copyrightline</w>
<w>copyrule</w>
<w>copystringsfile</w>
<w>copyswiftlibs</w>
<w>cornerpin</w>
<w>coroutines</w>
<w>countdownsounds</w>
@ -492,6 +509,7 @@
<w>cpus</w>
<w>cpython</w>
<w>crashlytics</w>
<w>createbuilddirectory</w>
<w>createtime</w>
<w>creationflags</w>
<w>creditslist</w>
@ -505,6 +523,7 @@
<w>csspbt</w>
<w>cstr</w>
<w>csum</w>
<w>csval</w>
<w>ctest</w>
<w>ctex</w>
<w>ctracker</w>
@ -530,6 +549,7 @@
<w>darwiin</w>
<w>darwiinremote</w>
<w>datab</w>
<w>databytes</w>
<w>dataclassio</w>
<w>dataclassutils</w>
<w>datamodule</w>
@ -1102,11 +1122,15 @@
<w>hurtiness</w>
<w>hval</w>
<w>iasset</w>
<w>ibtool</w>
<w>ibtoold</w>
<w>icls</w>
<w>icns</w>
<w>iconpicker</w>
<w>iconscale</w>
<w>iconset</w>
<w>iconsstorename</w>
<w>iconutil</w>
<w>ident</w>
<w>idevices</w>
<w>ifeq</w>
@ -1231,6 +1255,7 @@
<w>keyfilt</w>
<w>keyint</w>
<w>keylayout</w>
<w>keylen</w>
<w>keypresses</w>
<w>keystr</w>
<w>keytype</w>
@ -1319,6 +1344,7 @@
<w>linearstep</w>
<w>linebits</w>
<w>lineheight</w>
<w>linemax</w>
<w>lineno</w>
<w>linenum</w>
<w>linenumber</w>
@ -1326,6 +1352,7 @@
<w>linetype</w>
<w>linetypes</w>
<w>linflav</w>
<w>linkstoryboards</w>
<w>linkto</w>
<w>lintable</w>
<w>lintcode</w>
@ -1377,6 +1404,7 @@
<w>lshort</w>
<w>lsprof</w>
<w>lsqlite</w>
<w>lsregister</w>
<w>lssl</w>
<w>lstart</w>
<w>lstr</w>
@ -1442,6 +1470,7 @@
<w>memfunctions</w>
<w>menubar</w>
<w>messagetype</w>
<w>metallink</w>
<w>metamakefile</w>
<w>metaprogramming</w>
<w>metascan</w>
@ -1472,6 +1501,7 @@
<w>mkflags</w>
<w>mlen</w>
<w>mline</w>
<w>mmacosx</w>
<w>mmapmodule</w>
<w>mmult</w>
<w>mname</w>
@ -1581,6 +1611,7 @@
<w>newdbpath</w>
<w>newnode</w>
<w>newpath</w>
<w>newtoken</w>
<w>nextcall</w>
<w>nextfilenum</w>
<w>nextlevel</w>
@ -1655,6 +1686,7 @@
<w>okbtn</w>
<w>oldbook</w>
<w>oldlady</w>
<w>oldtoken</w>
<w>onln</w>
<w>onscreencountdown</w>
<w>onscreenkeyboard</w>
@ -1685,10 +1717,12 @@
<w>ourself</w>
<w>outdata</w>
<w>outdelay</w>
<w>outdict</w>
<w>outext</w>
<w>outfilename</w>
<w>outfilepath</w>
<w>outhashpath</w>
<w>outmsg</w>
<w>outname</w>
<w>outpath</w>
<w>outputter</w>
@ -1742,6 +1776,7 @@
<w>perma</w>
<w>perrdetail</w>
<w>phasers</w>
<w>phasescriptexecution</w>
<w>phello</w>
<w>photoshop</w>
<w>phrea</w>
@ -1856,6 +1891,9 @@
<w>privatetab</w>
<w>proactor</w>
<w>proc</w>
<w>processinfoplistfile</w>
<w>processpch</w>
<w>processpchplusplus</w>
<w>procs</w>
<w>profileindex</w>
<w>profilekey</w>
@ -1871,6 +1909,7 @@
<w>projroot</w>
<w>projs</w>
<w>promocode</w>
<w>proxykey</w>
<w>prunedir</w>
<w>prval</w>
<w>pstats</w>
@ -1999,6 +2038,8 @@
<w>redist</w>
<w>redistributables</w>
<w>regionid</w>
<w>registerexecutionpolicyexception</w>
<w>registerwithlaunchservices</w>
<w>regtp</w>
<w>reimported</w>
<w>relfut</w>
@ -2094,6 +2135,7 @@
<w>sbtn</w>
<w>sbwht</w>
<w>sbylw</w>
<w>sbytes</w>
<w>scenefile</w>
<w>scenefiles</w>
<w>scenename</w>
@ -2123,6 +2165,7 @@
<w>sdkcheck</w>
<w>sdkutils</w>
<w>sdtk</w>
<w>sectionchanged</w>
<w>selchild</w>
<w>selectmodule</w>
<w>selindex</w>
@ -2278,6 +2321,7 @@
<w>starscale</w>
<w>startercache</w>
<w>startscan</w>
<w>startsplits</w>
<w>starttime</w>
<w>statictest</w>
<w>statictestfiles</w>
@ -2349,6 +2393,7 @@
<w>svne</w>
<w>svvv</w>
<w>swht</w>
<w>swiftc</w>
<w>swip</w>
<w>swipsound</w>
<w>sylw</w>
@ -2450,10 +2495,12 @@
<w>textwidgets</w>
<w>tfin</w>
<w>thanvannispen</w>
<w>thats</w>
<w>thelaststand</w>
<w>themself</w>
<w>thingie</w>
<w>this'll</w>
<w>thislinelen</w>
<w>thismodule</w>
<w>threadpool</w>
<w>threadtype</w>
@ -2699,6 +2746,7 @@
<w>wpath</w>
<w>wprjp</w>
<w>wref</w>
<w>writeauxiliaryfile</w>
<w>writeclasses</w>
<w>writefuncs</w>
<w>wslpath</w>
@ -2711,7 +2759,10 @@
<w>xbox</w>
<w>xcarchive</w>
<w>xcassets</w>
<w>xcframework</w>
<w>xcodebuild</w>
<w>xcodebuildverbose</w>
<w>xcoderun</w>
<w>xcpretty</w>
<w>xcprojpath</w>
<w>xcrun</w>
@ -2733,6 +2784,7 @@
<w>xmore</w>
<w>xoffs</w>
<w>xoffset</w>
<w>xors</w>
<w>xpos</w>
<w>xres</w>
<w>xscl</w>
@ -2758,6 +2810,7 @@
<w>zaggy</w>
<w>zimbot</w>
<w>zipapp</w>
<w>zipdata</w>
<w>zlib</w>
<w>zlibmodule</w>
<w>zoneid</w>

View File

@ -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)

View File

@ -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",

View File

@ -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 \

View File

@ -1 +1 @@
263249044076897294312459199917994463006
14398100813069830297938811166218395775

View File

@ -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

View File

@ -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)"""

View File

@ -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 = '?'

View File

@ -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,

View File

@ -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

View File

@ -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'))

View File

@ -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

View File

@ -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']))

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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')

View File

@ -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']:

View File

@ -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.')))

View File

@ -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 = (

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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(

View File

@ -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

View File

@ -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'],

View File

@ -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')

View File

@ -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(

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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])

View File

@ -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',

View File

@ -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(

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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(
{

View File

@ -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()

View File

@ -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,

View File

@ -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',

View File

@ -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:

View File

@ -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,

View File

@ -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'])

View File

@ -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

View File

@ -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',

View File

@ -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:

View File

@ -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:

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -8,7 +8,10 @@
<w>abouttab</w>
<w>absval</w>
<w>accel</w>
<w>accountclientv</w>
<w>accountid</w>
<w>accountv</w>
<w>accountvalues</w>
<w>achs</w>
<w>acinstance</w>
<w>ack'ed</w>
@ -17,6 +20,7 @@
<w>aclass</w>
<w>aclass's</w>
<w>activityplayer</w>
<w>actool</w>
<w>addcall</w>
<w>addchars</w>
<w>addr</w>
@ -62,6 +66,7 @@
<w>appspot</w>
<w>appstate</w>
<w>argsjoined</w>
<w>argstr</w>
<w>asci</w>
<w>assetsmakefile</w>
<w>assigninput</w>
@ -99,6 +104,7 @@
<w>basntoclient</w>
<w>bastd</w>
<w>batoolsinternal</w>
<w>baworker</w>
<w>bbbb</w>
<w>bbbbb</w>
<w>bbbbbb</w>
@ -150,6 +156,7 @@
<w>bsmhi</w>
<w>bsstd</w>
<w>bstat</w>
<w>bstr</w>
<w>bsuuid</w>
<w>btnlabel</w>
<w>bucketnum</w>
@ -197,6 +204,7 @@
<w>checkarglist</w>
<w>checkboxwidget</w>
<w>checkchisel</w>
<w>checksummed</w>
<w>childanntype</w>
<w>childanntypes</w>
<w>childtype</w>
@ -225,11 +233,20 @@
<w>collider</w>
<w>columnwidget</w>
<w>comms</w>
<w>compileassetcatalog</w>
<w>compilec</w>
<w>compilemetalfile</w>
<w>compilestoryboard</w>
<w>compileswift</w>
<w>compileswiftsources</w>
<w>connectattr</w>
<w>containerwidget</w>
<w>controlfp</w>
<w>cooldown</w>
<w>coopscore</w>
<w>copypng</w>
<w>copystringsfile</w>
<w>copyswiftlibs</w>
<w>coreaudio</w>
<w>coulda</w>
<w>cout</w>
@ -239,6 +256,7 @@
<w>cpuid</w>
<w>crashenv</w>
<w>crashlytics</w>
<w>createbuilddirectory</w>
<w>createtime</w>
<w>cresult</w>
<w>crom</w>
@ -252,6 +270,7 @@
<w>cstdint</w>
<w>cstdlib</w>
<w>cstring</w>
<w>csval</w>
<w>ctargetref</w>
<w>ctracker</w>
<w>cubemap</w>
@ -260,6 +279,7 @@
<w>cutef</w>
<w>cvar</w>
<w>data</w>
<w>databytes</w>
<w>dataclassio</w>
<w>datadata</w>
<w>dataout</w>
@ -546,9 +566,13 @@
<w>htonf</w>
<w>htonl</w>
<w>htons</w>
<w>ibtool</w>
<w>ibtoold</w>
<w>ibuf</w>
<w>icloud</w>
<w>iconscale</w>
<w>iconset</w>
<w>iconutil</w>
<w>ieeefp</w>
<w>ifaddr</w>
<w>ifaddrs</w>
@ -615,6 +639,7 @@
<w>keycode</w>
<w>keyfilt</w>
<w>keyint</w>
<w>keylen</w>
<w>keysyms</w>
<w>keywds</w>
<w>khronos</w>
@ -648,6 +673,8 @@
<w>lightshad</w>
<w>linearsize</w>
<w>linearstep</w>
<w>linemax</w>
<w>linkstoryboards</w>
<w>listobj</w>
<w>llock</w>
<w>localns</w>
@ -667,6 +694,7 @@
<w>lrintf</w>
<w>lscope</w>
<w>lshort</w>
<w>lsregister</w>
<w>lstr</w>
<w>lsync</w>
<w>ltypes</w>
@ -709,6 +737,7 @@
<w>meshdata</w>
<w>messagebox</w>
<w>messagetype</w>
<w>metallink</w>
<w>metamakefile</w>
<w>meth</w>
<w>mhbegin</w>
@ -722,6 +751,7 @@
<w>mipmaps</w>
<w>mkflags</w>
<w>mlen</w>
<w>mmacosx</w>
<w>mmask</w>
<w>mmdevapi</w>
<w>modder</w>
@ -782,6 +812,7 @@
<w>newitem</w>
<w>newname</w>
<w>newnode</w>
<w>newtoken</w>
<w>nextchar</w>
<w>nitpicky</w>
<w>nlpos</w>
@ -828,6 +859,7 @@
<w>okbtn</w>
<w>oldbook</w>
<w>oldname</w>
<w>oldtoken</w>
<w>oooo</w>
<w>ooooooo</w>
<w>ooooooooo</w>
@ -859,6 +891,8 @@
<w>ourname</w>
<w>ourself</w>
<w>ourstanding</w>
<w>outdict</w>
<w>outmsg</w>
<w>outpath</w>
<w>outputter</w>
<w>outval</w>
@ -880,6 +914,7 @@
<w>pflag</w>
<w>pflags</w>
<w>pgmout</w>
<w>phasescriptexecution</w>
<w>piplist</w>
<w>pipvers</w>
<w>pixelformat</w>
@ -918,6 +953,9 @@
<w>printobjects</w>
<w>priv</w>
<w>privatetab</w>
<w>processinfoplistfile</w>
<w>processpch</w>
<w>processpchplusplus</w>
<w>profilers</w>
<w>prog</w>
<w>proj</w>
@ -925,6 +963,7 @@
<w>projpath</w>
<w>projprefix</w>
<w>prolly</w>
<w>proxykey</w>
<w>psmx</w>
<w>pspec</w>
<w>psps</w>
@ -992,6 +1031,8 @@
<w>refcounted</w>
<w>refl</w>
<w>regionid</w>
<w>registerexecutionpolicyexception</w>
<w>registerwithlaunchservices</w>
<w>regtp</w>
<w>rehel</w>
<w>reimported</w>
@ -1042,6 +1083,7 @@
<w>sapspace</w>
<w>savebtn</w>
<w>savebutton</w>
<w>sbytes</w>
<w>scancode</w>
<w>scenetime</w>
<w>screenmessage</w>
@ -1050,6 +1092,7 @@
<w>sdkcheck</w>
<w>sdl's</w>
<w>sdlk</w>
<w>sectionchanged</w>
<w>selchild</w>
<w>selindex</w>
<w>selwidget</w>
@ -1137,6 +1180,7 @@
<w>standin</w>
<w>startedptr</w>
<w>startpos</w>
<w>startsplits</w>
<w>starttime</w>
<w>startx</w>
<w>starty</w>
@ -1171,6 +1215,7 @@
<w>subscr</w>
<w>subtypestr</w>
<w>sval</w>
<w>swiftc</w>
<w>symbolification</w>
<w>syscalls</w>
<w>tabdefs</w>
@ -1194,8 +1239,10 @@
<w>textcolor</w>
<w>textwidget</w>
<w>thang</w>
<w>thats</w>
<w>thecommand</w>
<w>theres</w>
<w>thislinelen</w>
<w>thismodule</w>
<w>threadname</w>
<w>threadpool</w>
@ -1330,10 +1377,14 @@
<w>worldspace</w>
<w>woutdir</w>
<w>wprjp</w>
<w>writeauxiliaryfile</w>
<w>wsroot</w>
<w>wunused</w>
<w>wvmpth</w>
<w>xcframework</w>
<w>xclamped</w>
<w>xcodebuildverbose</w>
<w>xcoderun</w>
<w>xcrun</w>
<w>xdiff</w>
<w>xdist</w>
@ -1342,6 +1393,7 @@
<w>xmin</w>
<w>xmmintrin</w>
<w>xoffset</w>
<w>xors</w>
<w>xtweak</w>
<w>xxlimited</w>
<w>xxsubinterpreters</w>
@ -1354,6 +1406,7 @@
<w>yoffs</w>
<w>yooooooo</w>
<w>ytweak</w>
<w>zipdata</w>
<w>zmax</w>
<w>zmin</w>
<w>zoffset</w>

View File

@ -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.

View File

@ -1018,7 +1018,8 @@ enum class V1AccountType {
kServer,
kOculus,
kSteam,
kNvidiaChina
kNvidiaChina,
kV2
};
enum class GraphicsQuality {

View File

@ -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;

View File

@ -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;

View File

@ -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");

View File

@ -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 ----------------------------------------------------

View File

@ -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<std::string> override;
auto GenerateUUID() -> std::string override;
auto GetDefaultConfigDir() -> std::string override;

View File

@ -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"

View File

@ -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*;

View File

@ -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"

View File

@ -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*;

View File

@ -350,6 +350,7 @@ class Python {
kLstrFromJsonCall,
kUUIDStrCall,
kHashStringsCall,
kHaveAccountV2CredentialsCall,
kLast // Sentinel; must be at end.
};

View File

@ -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

View File

@ -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

View File

@ -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,

103
tools/bacommon/cloud.py Normal file
View File

@ -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')]

View File

@ -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')

View File

@ -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'

View File

@ -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: <xcode project path> <configuration name>')
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

View File

@ -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

View File

@ -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?

View File

@ -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'],

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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: <xcode project path> <configuration name>')
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})')

View File

@ -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'

570
tools/efrotools/xcode.py Normal file
View File

@ -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)

View File

@ -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: