mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-26 17:03:14 +08:00
Merge branch 'master' into save-last-manual-connect-port-to-config
This commit is contained in:
commit
f762d75024
153
.efrocachemap
153
.efrocachemap
@ -420,41 +420,42 @@
|
||||
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/60/ad/38269b7f1c7dc20cb9a506cd0681",
|
||||
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/72/85/d6fc4d16b7081d91fba2850b5b10",
|
||||
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/e9/ae/1d674d0c086eaa0bd1c3b1db0505",
|
||||
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/89/ec/d472036fbb09f310891761beb39a",
|
||||
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/b0/05/e530acaba539f040ce61e22561dc",
|
||||
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/9b/ec/d11f3e0238ff648bce3657fe5d50",
|
||||
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/92/43/36b34307575f6d6219bdf4898e18",
|
||||
"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/1f/7f/af259ba9b41556e5e667ad4c646d",
|
||||
"assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/3c/22/78a56fc40426ab19ad4e76924b78",
|
||||
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/96/96/1390940b8457b477113194acbb41",
|
||||
"assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/f7/b0/191439142c6d6da4a056edc98b38",
|
||||
"assets/build/ba_data/data/languages/croatian.json": "https://files.ballistica.net/cache/ba1/c9/73/01a1343af814131b1ee96af0b687",
|
||||
"assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/61/20/01291c2cb72b22f204730c0d7574",
|
||||
"assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/4e/8c/72ddb584856a15dfb11df95f9283",
|
||||
"assets/build/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/6a/fa/fcf4a804beaff927b0f12c179eaa",
|
||||
"assets/build/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/68/93/da8e9874f41a786edf52ba4ccaad",
|
||||
"assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/a0/1d/5fbc922d01521142c2a347b1b024",
|
||||
"assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/4c/c7/0184b8178869d1a3827a1bfcd5bb",
|
||||
"assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/c7/2e/e0520f58206da01b829e02ff4576",
|
||||
"assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/e8/84/6c9f123e9a0d82fc595c8f55ac7c",
|
||||
"assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/8a/09/3e0fa9e44913b53f4dab195d3fae",
|
||||
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/5f/51/c15d74d2fe4e88ee1e3db0986500",
|
||||
"assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/aa/da/dfc8d710af960d7300c7090faeab",
|
||||
"assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/09/55/b50104638f60636af2263877bb7f",
|
||||
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/d8/f2/aa16bc336bd7660cc86c3264bfc4",
|
||||
"assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/3a/6b/34714586cb4e9f1b12f8ae54cac8",
|
||||
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/12/62/862228b229057877e89fb195d41d",
|
||||
"assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/7c/38/d4a44c481757d355836f292ede48",
|
||||
"assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/10/13/1228836444f7557211f0058ef9bd",
|
||||
"assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/19/e9/59c891b1fb85f3ba9f19283c233d",
|
||||
"assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/61/5b/847c03407d1c3a85866833323676",
|
||||
"assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/ad/85/3caf0c981389e46835862248386a",
|
||||
"assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/ac/f5/c0922a99e40dfc9f5e026d43b533",
|
||||
"assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/dc/50/c109f475599608a7e8fdacea667c",
|
||||
"assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/4f/4d/b259b145a69db1d34052281bb3bb",
|
||||
"assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/cb/7b/b881356f22e5f8293a5e4b8976ae",
|
||||
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/ee/fa/6eb38e7d4cfd13702c66d37b9723",
|
||||
"assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/8a/82/392f2a4e0e6e5a5e367f19e49538",
|
||||
"assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/2e/d1/b506ddf3d27af7be1ae6159531a3",
|
||||
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/b2/81/53c8cd7617d649403e539c3a6171",
|
||||
"assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/30/91/9dc870d35ddca657bf07d0f88a82",
|
||||
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/dd/be/1ae65f073d563e69c0c5246d20d7",
|
||||
"assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/32/97/4c61425d7c200a5c4c9ae3a94a0e",
|
||||
"assets/build/ba_data/data/languages/malay.json": "https://files.ballistica.net/cache/ba1/b4/02/f686ce008312e74d5953a511c8b2",
|
||||
"assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/70/07/7864c07c7177dd751780ccc85b34",
|
||||
"assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/38/31/fd930431c7612d0ab0e73e6bc74f",
|
||||
"assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/cb/96/86bb139f20e126593f7fdba9e9e1",
|
||||
"assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/d7/06/9d70642d0a4d1e3b1c2149d7a17c",
|
||||
"assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/34/ed/b97350983272e4b23bf140d7a5f4",
|
||||
"assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/6c/62/06869ed55a656b6e51b4d22e6fa8",
|
||||
"assets/build/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/4e/91/6f2a9a3ce733908e91377a6ddb9a",
|
||||
"assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/f9/4b/d9f01814224066856695452ef57c",
|
||||
"assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/ce/be/2f06c3436871fd464ff3a62597d9",
|
||||
"assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/91/0a/35c4baf539d5951fc03a794c0e0b",
|
||||
"assets/build/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/64/22/7bc899ecbec52cf978a1faf1c127",
|
||||
"assets/build/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/f7/df/7ba5f99c5c2c4c86fc0503fcf0b7",
|
||||
"assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/ee/08/1f77c7c320d8d8504a11ee495db3",
|
||||
"assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/f3/92/fd7ee5fa8a92fcc8fd2219a88a2f",
|
||||
"assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/2e/86/10d3e39d35014d039cc9ea886ca7",
|
||||
"assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/20/a9/163d189884edf802636bf291e432",
|
||||
"assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/0b/21/a4d09ca1fea8bbf347ed7091c8a2",
|
||||
"assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/3b/9f/d40c1423d260784970fd7364ca30",
|
||||
"assets/build/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/3d/83/e1bb0a664d1c14c41b1a083acf0d",
|
||||
"assets/build/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/d6/16/523c643358880b03b233ed88e557",
|
||||
"assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/63/c8/6cfbfd6702c80aa9df490e4629d7",
|
||||
"assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/3e/b6/052f1faed0264bf7135feb5c4cc3",
|
||||
"assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/a6/ed/416638d46950c9ab4f6155b9c334",
|
||||
"assets/build/ba_data/data/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/1f/ae/abe3f105b3c4b51f6b7942773305",
|
||||
"assets/build/ba_data/data/maps/big_g.json": "https://files.ballistica.net/cache/ba1/47/0a/a617cc85d927b576c4e6fc1091ed",
|
||||
"assets/build/ba_data/data/maps/bridgit.json": "https://files.ballistica.net/cache/ba1/03/4b/57ee9b42854b26f23f81bd8c58ef",
|
||||
@ -1858,6 +1859,10 @@
|
||||
"assets/build/ba_data/textures/menuIcon.ktx": "https://files.ballistica.net/cache/ba1/e7/8c/dde40b132e096841299505e2b665",
|
||||
"assets/build/ba_data/textures/menuIcon.pvr": "https://files.ballistica.net/cache/ba1/91/d3/6e3b38a70caa2bc68ddff50e3421",
|
||||
"assets/build/ba_data/textures/menuIcon_preview.png": "https://files.ballistica.net/cache/ba1/1f/22/69a013729949501405ea342a1a8b",
|
||||
"assets/build/ba_data/textures/merch.dds": "https://files.ballistica.net/cache/ba1/da/6a/87779603f295cf30fcec2d3a4e9f",
|
||||
"assets/build/ba_data/textures/merch.ktx": "https://files.ballistica.net/cache/ba1/5b/c4/2df30594a9e7c1988d92435d7bd0",
|
||||
"assets/build/ba_data/textures/merch.pvr": "https://files.ballistica.net/cache/ba1/20/76/b8da7a2dded8c137c8388bcb6986",
|
||||
"assets/build/ba_data/textures/merch_preview.png": "https://files.ballistica.net/cache/ba1/31/ad/9ffc829f1fcff718106d129ac062",
|
||||
"assets/build/ba_data/textures/meter.dds": "https://files.ballistica.net/cache/ba1/52/a8/00244236f0e6126e2e14bd1f3106",
|
||||
"assets/build/ba_data/textures/meter.ktx": "https://files.ballistica.net/cache/ba1/b5/e7/3ba3d4c7c7e05081eb033ae5da8f",
|
||||
"assets/build/ba_data/textures/meter.pvr": "https://files.ballistica.net/cache/ba1/58/de/ab83ad9e0d9b434ca42811030ef9",
|
||||
@ -4003,51 +4008,51 @@
|
||||
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
|
||||
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/1c/77/ac670a5118abdf8a7687af0e159b",
|
||||
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
|
||||
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/bd/82/98ea775b22a1113323a1ddf12b6a",
|
||||
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e1/0d/0a431edcdee394a4e4d5b18608d7",
|
||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e8/94/162a944636170ac881d3f3dfd805",
|
||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f8/96/fea047474c276064176b65f7e48a",
|
||||
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/12/eb/226aba01e295a807614c64c44d40",
|
||||
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/23/71/f60c8e90699d887979c4ad26a2e2",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8d/80/05e3d712c67d0fcde0e8605d0be7",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7f/05/498a538fd28fcd1de964c046b8f2",
|
||||
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d1/6e/01e46632d17db0597d1aef3394d1",
|
||||
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/aa/12/7992a25feae2b0bf2c8fe2023187",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/99/a9/2b251e46b2e9e4ef143a0308d9f3",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6a/93/faee77acd35111d083998df65aaa",
|
||||
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/25/c1/9b6efb09c364beae30a40510bfcf",
|
||||
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/18/76/3f8e144f6727ee8f5f5f4b0b6ddc",
|
||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3c/e8/02284ba36b4de9ca68cdd7c3e689",
|
||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/15/cf/c02041bdc6fa5d0042408e591dcf",
|
||||
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/62/98/ee64d80a6332d7d9fc57a2605a2d",
|
||||
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/54/e6/c4c75d29a7c19c34ba5876c4c350",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/69/3c/96b8690702f596182a305b5b4489",
|
||||
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/b4/b4/81668b3afad33372276a46545aae",
|
||||
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c3/1a/ae199ada4bf5a649f73fe1663868",
|
||||
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a2/4f/2cf4047fdbac4a661ca99d4aedb8",
|
||||
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/06/f5/b0fdcf55008fc53e1660f7bc841f",
|
||||
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2c/af/159cc0021a3751da19f4d6832602",
|
||||
"build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cc/4d/0586cbf47105ba1224a445cd72f4",
|
||||
"build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f8/33/ae7f84447a19e465c134355b359e",
|
||||
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/40/eb/004a4ea3094210114fd739cb9fcb",
|
||||
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/45/ed/36918bf061396d6e1f9814afce8c",
|
||||
"build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8f/28/7c06af16187bf2db92cf99763f4e",
|
||||
"build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/69/aeba19cb88e6b57c4ab9325f5877",
|
||||
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/69/4e/b8354e50de6f2afb45b342919868",
|
||||
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/70/65/f35430e7328bc7ac30de3960dfc7",
|
||||
"build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d8/47/c89b62ea5a71854b303f9e85e5d7",
|
||||
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f2/19/280f6773b3563e012ad6bfad33dc",
|
||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1e/17/e5ef6ce0e41360a43c63ef9c0974",
|
||||
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cd/78/1adf82e5c3e456d2ea2d4290c61c",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/6b/5f/c5dc0b2a2809bc1d3ea57fb985d0",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/56/ee/49a4a2d7940fd239a66c04657c90",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/39/72/fb9b3400c5d128ad156818ede03d",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/22/65/550d27a960822b8846c0c0a440be",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/33/3b/9b38515580edd4616f9955f7e33c",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/fe/1a/80ddcd73df9985ab768e617a6c2b",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/1d/27/0d69901bb721f986fdbfc490100f",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/e9/59/ab5278ceeae3656f91c6d3c68c83",
|
||||
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/25/77/8093dfffddaa80cd513ddaa61867",
|
||||
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/a2/53/595db696c112a17513214a62eee7",
|
||||
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/27/db/57461eaa99ad29c86aab36b26a82",
|
||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/63/39/ad16cba9e1efb90fb90ae4d564d2",
|
||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/53/1e/40e9ac482c8d04a56e340100fba6",
|
||||
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/52/6c/5cf1aa6b12d251faea83d2f63377",
|
||||
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/65/91/1e7e14afff621cb2070644f57bbc",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0e/81/2cd5d6fbe320802b90d81768bd73",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9a/02/d7393257500edfd7879baeea431d",
|
||||
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/96/a2/6d45db766e7299b743156cce615b",
|
||||
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/52/b8/99e7cb5fa71615795f272ed2c518",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/5b/c7/6d474e85577975383f1ae1fc7377",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a1/df/9bd1a6b76c8bf77917753959195f",
|
||||
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/cb/da/9d073b52363b6bc0240fe5e79bf1",
|
||||
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c9/23/7060ae125ba189320190b321d947",
|
||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0f/43/49d1bee57cb412e0b7ba59dd9769",
|
||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e0/38/826f664787217ca44d569a5723d7",
|
||||
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/36/57/0c4fc5b9294b04a312ccd8832254",
|
||||
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/98/90/f34fd62f7d512cffd3c9534da10b",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/cb/a5/5d01935a74a86efcf1885975cfbf",
|
||||
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/c7/78/a8765f7463f3651c71cd5ca71cb6",
|
||||
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cb/3b/83e6150eebf4fa7b5e8a7b863219",
|
||||
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/70/2878a73f2d55849cd6a75e401575",
|
||||
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/24/49/4baca475df5c7f87e6ed17d46696",
|
||||
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b9/46/1c6b679ef9db6807100bc0bba261",
|
||||
"build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/da/b9/427dfd7ae8efbe6009964fe34beb",
|
||||
"build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ac/2c/fc0a576c3d957896bfd3de792af6",
|
||||
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/39/08/4033ab823798c48b3446c032a72c",
|
||||
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3a/c6/189744027136a7411d5dfa5c5cf4",
|
||||
"build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7a/c6/bb533c59368fdf45d65812f37a27",
|
||||
"build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3c/6e/88180b8e905df7453e0f989da027",
|
||||
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2c/99/e35a71c9c410da4035a0456314b6",
|
||||
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/47/b2/bb092304cd5e5f3bdb6e085197de",
|
||||
"build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9c/ce/523a32e2dce1174df21373ab5765",
|
||||
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b2/ee/b908410d85c763d5bca09a3bc893",
|
||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fd/24/8c51e5752064d4b487dff42a7ffa",
|
||||
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/93/c6/40e9e47dd33c88646510212ff321",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/a1/25/ca39d9309b63ed38bee3be1f9e67",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/b3/ef/a6240c67194508ac7bd58ba73391",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/c2/cc/b081c59aa873304086d2e44c1f30",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/d3/cb/8c1ed9ee3e8f1b0d866160257506",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/f0/f8/fb0aa403b29d5c5a9ebb5aec66fb",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/0b/03/ed35c58e80013f47b57c838c12d4",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/18/1d/cfc42a4939904783c897322207aa",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/68/82/c02d830bdb12f57ae976c5acc4fb",
|
||||
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/23/ce/68396b1b7ec6d2f8425902148140",
|
||||
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/2d/4f/f4fe67827f36cd59cd5193333a02",
|
||||
"src/ballistica/generated/python_embedded/bootstrap_monolithic.inc": "https://files.ballistica.net/cache/ba1/ef/c1/aa5f1aa10af89f5c0b1e616355fd"
|
||||
}
|
||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -16,9 +16,9 @@ jobs:
|
||||
check_linux:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
@ -31,9 +31,9 @@ jobs:
|
||||
compile_linux:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Compile binary
|
||||
@ -47,9 +47,9 @@ jobs:
|
||||
check_and_compile_windows:
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
|
||||
22
.idea/dictionaries/ericf.xml
generated
22
.idea/dictionaries/ericf.xml
generated
@ -73,6 +73,7 @@
|
||||
<w>allsettings</w>
|
||||
<w>allteams</w>
|
||||
<w>allwarnings</w>
|
||||
<w>alogins</w>
|
||||
<w>aman</w>
|
||||
<w>amazonaws</w>
|
||||
<w>aname</w>
|
||||
@ -114,6 +115,7 @@
|
||||
<w>appmode</w>
|
||||
<w>appname</w>
|
||||
<w>appnameupper</w>
|
||||
<w>appnow</w>
|
||||
<w>appspot</w>
|
||||
<w>appstate</w>
|
||||
<w>appstore</w>
|
||||
@ -184,6 +186,7 @@
|
||||
<w>availplug</w>
|
||||
<w>aval</w>
|
||||
<w>awaitable</w>
|
||||
<w>awaitables</w>
|
||||
<w>axismotion</w>
|
||||
<w>bacfg</w>
|
||||
<w>backgrounded</w>
|
||||
@ -374,6 +377,7 @@
|
||||
<w>certifi</w>
|
||||
<w>cfconfig</w>
|
||||
<w>cfenv</w>
|
||||
<w>cfgdict</w>
|
||||
<w>cfgdir</w>
|
||||
<w>cfgkey</w>
|
||||
<w>cfgkeys</w>
|
||||
@ -677,6 +681,7 @@
|
||||
<w>dosomething</w>
|
||||
<w>dostar</w>
|
||||
<w>dostuff</w>
|
||||
<w>downcasting</w>
|
||||
<w>downmix</w>
|
||||
<w>dpad</w>
|
||||
<w>dpath</w>
|
||||
@ -718,6 +723,7 @@
|
||||
<w>dummyret</w>
|
||||
<w>dummytoken</w>
|
||||
<w>dummyval</w>
|
||||
<w>dumpminlog</w>
|
||||
<w>dups</w>
|
||||
<w>dval</w>
|
||||
<w>dxml</w>
|
||||
@ -726,6 +732,7 @@
|
||||
<w>eaddrnotavail</w>
|
||||
<w>easteregghunt</w>
|
||||
<w>echofile</w>
|
||||
<w>echotime</w>
|
||||
<w>edcc</w>
|
||||
<w>editcontroller</w>
|
||||
<w>editgame</w>
|
||||
@ -848,6 +855,7 @@
|
||||
<w>fakeshake</w>
|
||||
<w>fallbacks</w>
|
||||
<w>farthestpt</w>
|
||||
<w>faulthandler</w>
|
||||
<w>fback</w>
|
||||
<w>fbase</w>
|
||||
<w>fclose</w>
|
||||
@ -858,6 +866,7 @@
|
||||
<w>fcontents</w>
|
||||
<w>fcount</w>
|
||||
<w>fdata</w>
|
||||
<w>fdcount</w>
|
||||
<w>fdesc</w>
|
||||
<w>fdict</w>
|
||||
<w>fdout</w>
|
||||
@ -1271,6 +1280,7 @@
|
||||
<w>iprof</w>
|
||||
<w>isatty</w>
|
||||
<w>iscale</w>
|
||||
<w>iscoroutinefunction</w>
|
||||
<w>iserverget</w>
|
||||
<w>iserverput</w>
|
||||
<w>ispunch</w>
|
||||
@ -1372,6 +1382,7 @@
|
||||
<w>levelmodule</w>
|
||||
<w>levelname</w>
|
||||
<w>levelno</w>
|
||||
<w>levelnos</w>
|
||||
<w>levelstr</w>
|
||||
<w>lfull</w>
|
||||
<w>lfval</w>
|
||||
@ -1522,6 +1533,7 @@
|
||||
<w>masterhash</w>
|
||||
<w>masterver</w>
|
||||
<w>mastervers</w>
|
||||
<w>matchlines</w>
|
||||
<w>mathmodule</w>
|
||||
<w>mathnode</w>
|
||||
<w>mathutils</w>
|
||||
@ -1537,6 +1549,7 @@
|
||||
<w>mdiv</w>
|
||||
<w>mdocs</w>
|
||||
<w>mdocslines</w>
|
||||
<w>mdpath</w>
|
||||
<w>mdpi</w>
|
||||
<w>megalint</w>
|
||||
<w>memfunctions</w>
|
||||
@ -1564,6 +1577,7 @@
|
||||
<w>minigame</w>
|
||||
<w>minigames</w>
|
||||
<w>miniplayer</w>
|
||||
<w>minlog</w>
|
||||
<w>minping</w>
|
||||
<w>minusbutton</w>
|
||||
<w>minval</w>
|
||||
@ -2337,6 +2351,7 @@
|
||||
<w>shobs</w>
|
||||
<w>shortname</w>
|
||||
<w>shouldn</w>
|
||||
<w>showbuffer</w>
|
||||
<w>showpoints</w>
|
||||
<w>showstats</w>
|
||||
<w>showsubseconds</w>
|
||||
@ -2513,6 +2528,7 @@
|
||||
<w>svne</w>
|
||||
<w>svvv</w>
|
||||
<w>swht</w>
|
||||
<w>swidth</w>
|
||||
<w>swiftc</w>
|
||||
<w>swip</w>
|
||||
<w>swipsound</w>
|
||||
@ -2549,6 +2565,9 @@
|
||||
<w>targs</w>
|
||||
<w>tasklabel</w>
|
||||
<w>tbegin</w>
|
||||
<w>tbfile</w>
|
||||
<w>tbfiles</w>
|
||||
<w>tbpath</w>
|
||||
<w>tbtcolor</w>
|
||||
<w>tbtn</w>
|
||||
<w>tbttxt</w>
|
||||
@ -2850,6 +2869,7 @@
|
||||
<w>wanttype</w>
|
||||
<w>warntype</w>
|
||||
<w>wasdead</w>
|
||||
<w>wasn</w>
|
||||
<w>wavenum</w>
|
||||
<w>weakref</w>
|
||||
<w>weakrefs</w>
|
||||
@ -2859,7 +2879,9 @@
|
||||
<w>webpage</w>
|
||||
<w>webpages</w>
|
||||
<w>weeeird</w>
|
||||
<w>whatarev</w>
|
||||
<w>whatevs</w>
|
||||
<w>whatisv</w>
|
||||
<w>wheee</w>
|
||||
<w>whos</w>
|
||||
<w>widgetdeathtime</w>
|
||||
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@ -1,12 +1,46 @@
|
||||
### 1.7.14 (build 20934, api 7, 2022-11-16)
|
||||
### 1.7.19 (build 20993, api 7, 2023-01-17)
|
||||
|
||||
### 1.7.18 (build 20989, api 7, 2023-01-16)
|
||||
- Reworked some low level asynchronous messaging functionality in efro.message and efro.rpc. Previously these were a little *too* asynchronous which could lead to messages being received in a different order than they were sent, which is not desirable.
|
||||
- Added a way to suppress 'Your build is outdated' messages at launch (see `ba._hooks.show_client_too_old_error()`).
|
||||
|
||||
### 1.7.17 (build 20983, api 7, 2023-01-09)
|
||||
- V2 accounts now show a 'Unlink Legacy (V1) Accounts' button in account settings if they have any old V1 links present. This can be used to clear out old links to replace them with V2 links which work correctly with V2 accounts.
|
||||
- `ba.internal.dump_tracebacks()` is now `ba.internal.dump_app_state()` and `ba.internal.log_dumped_tracebacks()` is now `ba.internal.log_dumped_app_state()`. This reflects the fact that these calls may be expanded to include other app state in the future (C++ layer thread states, etc.).
|
||||
- Added `ba.app.health_monitor` which will dump app state if the logic thread ever stops responding for 5+ seconds while the app is running (to help diagnose deadlock situations).
|
||||
- Various extra logging and bug fixes related to V2 accounts and master server communication (trying to get this stuff working as smoothly as possible now that it is feature-complete).
|
||||
|
||||
### 1.7.16 (build 20969, api 7, 2022-12-18)
|
||||
- Fixed a bug where profile names encased in curly brackets could cause harmless error messages.
|
||||
- Android will no longer log errors on ba.open_url() calls if a browser is not available (it still just falls back to the in-app dialog in that case).
|
||||
- The 'Upgrade' button for device accounts now signs you out and closes the upgrade window to hopefully make it more clear that you need to sign in with your newly created/upgraded BombSquad account.
|
||||
- Fixed a bug where the remote app could not connect for the first 5 seconds after launching the app.
|
||||
- Added Malay language. Ick; apparently its been sitting done for a while and I hadn't realized it wasn't added to the game yet. Apologies!. And thanks to all contributors!
|
||||
- Added 'enable_queue' server config setting. This defaults to True but can be turned off as a workaround for server owners targeted by queue spam attacks.
|
||||
- The public party list no longer sorts servers without queues at the end of the list. This sorting was put there long ago to prioritize fancy new queue-supporting servers but now it would just make the few that opt out of queues hard to find. Doh. So opting out of queues is probably not a great idea until this build is widespread.
|
||||
- Public uuids now only change once every 6 months or so instead of with every version bump. This way periods of heavy development won't put added strain on server owners trying to keep ban lists up to date and whatnot.
|
||||
- Added a merch button in the in-game store that goes to the ballistica.net merch page (though it only shows up in the few countries where merch is available).
|
||||
|
||||
### 1.7.15 (build 20960, api 7, 2022-12-04)
|
||||
- The cancel button on the 'Sign in with a Bombsquad Account' popup no longer respond to system cancel buttons (escape key, android back button, etc). Turns out some Android people were pressing back repeatedly to come back from a browser after signing in and immediately canceling their sign in attempts in the game before they completed. Hopefully this will avoid some frustration.
|
||||
- Fixed an issue where back presses could result in multiple main menu windows appearing.
|
||||
|
||||
### 1.7.14 (build 20958, api 7, 2022-12-03)
|
||||
- Android Google Play logins now provide V2 accounts with access to all V2 features such as a globally-unique account tag, cloud-console, and workspaces. They should still retain their V1 data as well.
|
||||
- V2 accounts now have a 'Manage Account' button in the app account window which will sign you into a browser with your current account.
|
||||
- Removed Google App Invite functionality which has been deprecated for a while now. Google Play users can still get tickets by sharing the app via codes (same as other platforms).
|
||||
- Updated Android root-detection library to the latest version. Please holler if you are getting new false 'your device is rooted' errors when trying to play tournaments or anything like that.
|
||||
- Removed a few obsolete internal functions: `_ba.is_ouya_build()`, `_ba.android_media_scan_file()`.
|
||||
- Renaming some methods/data to disambiguate 'login' vs 'sign-in', both in the app and on ballistica.net. Those two terms are somewhat ambiguous and interchangeable in English and can either be a verb or a noun. I'd like to keep things clear in Ballistica by always using 'sign-in' for the verb form and 'login' for the noun. For example: 'You can now sign in to your account using your Google Play login'.
|
||||
- WARNING: There are currently some rough edges with Google Play V2 accounts; for example Google Play achievements and leaderboards UIs are not currently showing up. I will be cleaning all of this up before the official 1.7.14 release.
|
||||
- Commit Last Manual Party Connect Port to config. Previously, it always assumed the port to be 43210.
|
||||
- Fixed the 'your config is broken' dialog that shows on desktop builds if the game's config file is corrupt and can't be read. It should let you edit the config or replace it with a default.
|
||||
- `ba.printobjects()` is now `ba.ls_objects()`. It technically logs and doesn't print so the former name was a bit misleading.
|
||||
- Added `ba.ls_input_devices()` to dump debug info about the current set of input devices. Can be helpful to diagnose mysterious devices joining games unintentionally and things like that.
|
||||
- Added 'raw' bool arg to `ba.pushcall()`. Passing True for it disables context save/restore and thread checks.
|
||||
- Added `ba.internal.dump_tracebacks()` which can be used to dump the stack state of all Python threads after some delay. Useful for debugging deadlock; just call right before said deadlock occurs. Results will be logged on the next app launch if they cannot be immediately.
|
||||
- Fixed a low level event-loop issue that in some cases was preventing the Android version from properly pausing/resuming the app or managing connections while in the background. If you look at the devices section on ballistica.net you should now see your device disappear when you background the app and reappear when you foreground it. Please holler if not.
|
||||
- Device accounts are now marked as deprecated, and signing in with one now brings up an 'upgrade' UI which allows converting it to a V2 account. It is my hope to push the entire client ecosystem to V2 accounts as quickly as possible since trying to support both independent V1 accounts and V2 accounts is a substantial technical burden.
|
||||
- Fixed an issue where Log calls made within `Thread::PushThreadMessage()` could result in deadlock.
|
||||
- Fixed an issue where some Android hardware buttons could theoretically cause rogue game controller button presses (due to downcasting int values > 255 into a uint8 value).
|
||||
|
||||
### 1.7.13 (build 20919, api 7, 2022-11-03)
|
||||
- Android target-sdk has been updated to 33 (Android 13). Please holler if anything seems broken or is behaving differently than before on Android.
|
||||
|
||||
@ -38,4 +38,8 @@
|
||||
|
||||
### Vishal332008
|
||||
- Bug Fixer
|
||||
- Modder
|
||||
- Modder
|
||||
|
||||
### Era0S
|
||||
- Fixed a single bug
|
||||
- Modder
|
||||
|
||||
@ -441,6 +441,7 @@
|
||||
"ba_data/data/languages/indonesian.json",
|
||||
"ba_data/data/languages/italian.json",
|
||||
"ba_data/data/languages/korean.json",
|
||||
"ba_data/data/languages/malay.json",
|
||||
"ba_data/data/languages/persian.json",
|
||||
"ba_data/data/languages/polish.json",
|
||||
"ba_data/data/languages/portuguese.json",
|
||||
@ -1880,6 +1881,10 @@
|
||||
"ba_data/textures/menuIcon.ktx",
|
||||
"ba_data/textures/menuIcon.pvr",
|
||||
"ba_data/textures/menuIcon_preview.png",
|
||||
"ba_data/textures/merch.dds",
|
||||
"ba_data/textures/merch.ktx",
|
||||
"ba_data/textures/merch.pvr",
|
||||
"ba_data/textures/merch_preview.png",
|
||||
"ba_data/textures/meter.dds",
|
||||
"ba_data/textures/meter.ktx",
|
||||
"ba_data/textures/meter.pvr",
|
||||
|
||||
@ -356,6 +356,7 @@
|
||||
"ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/bastd/ui/__pycache__/trophies.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/bastd/ui/__pycache__/url.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/bastd/ui/__pycache__/v2upgrade.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/bastd/ui/__pycache__/watch.cpython-310.opt-1.pyc",
|
||||
"ba_data/python/bastd/ui/account/__init__.py",
|
||||
"ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-310.opt-1.pyc",
|
||||
@ -514,6 +515,7 @@
|
||||
"ba_data/python/bastd/ui/tournamentscores.py",
|
||||
"ba_data/python/bastd/ui/trophies.py",
|
||||
"ba_data/python/bastd/ui/url.py",
|
||||
"ba_data/python/bastd/ui/v2upgrade.py",
|
||||
"ba_data/python/bastd/ui/watch.py",
|
||||
"ba_data/python/efro/__init__.py",
|
||||
"ba_data/python/efro/__pycache__/__init__.cpython-310.opt-1.pyc",
|
||||
|
||||
@ -381,6 +381,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
|
||||
build/ba_data/python/bastd/ui/tournamentscores.py \
|
||||
build/ba_data/python/bastd/ui/trophies.py \
|
||||
build/ba_data/python/bastd/ui/url.py \
|
||||
build/ba_data/python/bastd/ui/v2upgrade.py \
|
||||
build/ba_data/python/bastd/ui/watch.py \
|
||||
build/server/ballisticacore_server.py
|
||||
|
||||
@ -633,6 +634,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
|
||||
build/ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-310.opt-1.pyc \
|
||||
build/ba_data/python/bastd/ui/__pycache__/trophies.cpython-310.opt-1.pyc \
|
||||
build/ba_data/python/bastd/ui/__pycache__/url.cpython-310.opt-1.pyc \
|
||||
build/ba_data/python/bastd/ui/__pycache__/v2upgrade.cpython-310.opt-1.pyc \
|
||||
build/ba_data/python/bastd/ui/__pycache__/watch.cpython-310.opt-1.pyc \
|
||||
build/server/__pycache__/ballisticacore_server.cpython-310.opt-1.pyc
|
||||
|
||||
@ -4915,6 +4917,7 @@ DATA_TARGETS = \
|
||||
build/ba_data/data/languages/indonesian.json \
|
||||
build/ba_data/data/languages/italian.json \
|
||||
build/ba_data/data/languages/korean.json \
|
||||
build/ba_data/data/languages/malay.json \
|
||||
build/ba_data/data/languages/persian.json \
|
||||
build/ba_data/data/languages/polish.json \
|
||||
build/ba_data/data/languages/portuguese.json \
|
||||
@ -5595,6 +5598,7 @@ TEX2D_DDS_TARGETS = \
|
||||
build/ba_data/textures/menuBG.dds \
|
||||
build/ba_data/textures/menuButton.dds \
|
||||
build/ba_data/textures/menuIcon.dds \
|
||||
build/ba_data/textures/merch.dds \
|
||||
build/ba_data/textures/meter.dds \
|
||||
build/ba_data/textures/monkeyFaceLevelColor.dds \
|
||||
build/ba_data/textures/monkeyFacePreview.dds \
|
||||
@ -6000,6 +6004,7 @@ TEX2D_PVR_TARGETS = \
|
||||
build/ba_data/textures/menuBG.pvr \
|
||||
build/ba_data/textures/menuButton.pvr \
|
||||
build/ba_data/textures/menuIcon.pvr \
|
||||
build/ba_data/textures/merch.pvr \
|
||||
build/ba_data/textures/meter.pvr \
|
||||
build/ba_data/textures/monkeyFaceLevelColor.pvr \
|
||||
build/ba_data/textures/monkeyFacePreview.pvr \
|
||||
@ -6405,6 +6410,7 @@ TEX2D_KTX_TARGETS = \
|
||||
build/ba_data/textures/menuBG.ktx \
|
||||
build/ba_data/textures/menuButton.ktx \
|
||||
build/ba_data/textures/menuIcon.ktx \
|
||||
build/ba_data/textures/merch.ktx \
|
||||
build/ba_data/textures/meter.ktx \
|
||||
build/ba_data/textures/monkeyFaceLevelColor.ktx \
|
||||
build/ba_data/textures/monkeyFacePreview.ktx \
|
||||
@ -6810,6 +6816,7 @@ TEX2D_PREVIEW_PNG_TARGETS = \
|
||||
build/ba_data/textures/menuBG_preview.png \
|
||||
build/ba_data/textures/menuButton_preview.png \
|
||||
build/ba_data/textures/menuIcon_preview.png \
|
||||
build/ba_data/textures/merch_preview.png \
|
||||
build/ba_data/textures/meter_preview.png \
|
||||
build/ba_data/textures/monkeyFaceLevelColor_preview.png \
|
||||
build/ba_data/textures/monkeyFacePreview_preview.png \
|
||||
|
||||
@ -1 +1 @@
|
||||
136821726394202151644063370854718971574
|
||||
71864217068887285722858773141608052966
|
||||
@ -2429,12 +2429,42 @@ def lock_all_input() -> None:
|
||||
return None
|
||||
|
||||
|
||||
def login_adapter_back_end_active_change(login_type: str, active: bool) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def login_adapter_get_sign_in_token(login_type: str, attempt_id: int) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def ls_input_devices() -> None:
|
||||
|
||||
"""Print debugging info about game objects.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
This call only functions in debug builds of the game.
|
||||
It prints various info about the current object count, etc.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
def ls_objects() -> None:
|
||||
|
||||
"""Log debugging info about C++ level objects.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
This call only functions in debug builds of the game.
|
||||
It prints various info about the current object count, etc.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
def mac_music_app_get_library_source() -> None:
|
||||
|
||||
"""(internal)"""
|
||||
@ -2653,32 +2683,17 @@ def printnodes() -> None:
|
||||
return None
|
||||
|
||||
|
||||
def printobjects() -> None:
|
||||
|
||||
"""Print debugging info about game objects.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
This call only functions in debug builds of the game.
|
||||
It prints various info about the current object count, etc.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
def pushcall(
|
||||
call: Callable,
|
||||
from_other_thread: bool = False,
|
||||
suppress_other_thread_warning: bool = False,
|
||||
other_thread_use_fg_context: bool = False,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
|
||||
"""Pushes a call onto the event loop to be run during the next cycle.
|
||||
|
||||
"""Push a call to the logic event-loop.
|
||||
Category: **General Utility Functions**
|
||||
|
||||
This can be handy for calls that are disallowed from within other
|
||||
callbacks, etc.
|
||||
|
||||
This call expects to be used in the logic thread, and will automatically
|
||||
save and restore the ba.Context to behave seamlessly.
|
||||
|
||||
@ -2687,6 +2702,7 @@ def pushcall(
|
||||
the call will always run in the UI context on the logic thread
|
||||
or whichever context is in the foreground if
|
||||
other_thread_use_fg_context is True.
|
||||
Passing raw=True will disable thread checks and context sets/restores.
|
||||
"""
|
||||
return None
|
||||
|
||||
@ -3009,6 +3025,12 @@ def set_public_party_name(name: str) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def set_public_party_queue_enabled(max_size: bool) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def set_public_party_stats_url(url: str | None) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
|
||||
@ -252,7 +252,6 @@ def submit_score(
|
||||
name: Any,
|
||||
score: int | None,
|
||||
callback: Callable,
|
||||
friend_callback: Callable | None,
|
||||
order: str = 'increasing',
|
||||
tournament_id: str | None = None,
|
||||
score_type: str = 'points',
|
||||
|
||||
@ -43,7 +43,8 @@ from _ba import (
|
||||
newnode,
|
||||
playsound,
|
||||
printnodes,
|
||||
printobjects,
|
||||
ls_objects,
|
||||
ls_input_devices,
|
||||
pushcall,
|
||||
quit,
|
||||
rowwidget,
|
||||
@ -316,7 +317,8 @@ __all__ = [
|
||||
'print_error',
|
||||
'print_exception',
|
||||
'printnodes',
|
||||
'printobjects',
|
||||
'ls_objects',
|
||||
'ls_input_devices',
|
||||
'pushcall',
|
||||
'quit',
|
||||
'rowwidget',
|
||||
|
||||
@ -48,6 +48,9 @@ class AccountV1Subsystem:
|
||||
|
||||
_ba.pushcall(do_auto_sign_in)
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Should be called when app is pausing."""
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Should be called when the app is resumed."""
|
||||
|
||||
|
||||
@ -4,17 +4,24 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.call import tpartial
|
||||
from efro.error import CommunicationError
|
||||
from bacommon.login import LoginType
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from bacommon.login import LoginType
|
||||
from ba._login import LoginAdapter
|
||||
|
||||
|
||||
DEBUG_LOG = False
|
||||
|
||||
|
||||
class AccountV2Subsystem:
|
||||
"""Subsystem for modern account handling in the app.
|
||||
|
||||
@ -24,20 +31,20 @@ class AccountV2Subsystem:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
from bacommon.login import LoginType
|
||||
|
||||
# Whether or not everything related to an initial login
|
||||
# (or lack thereof) has completed. This includes things like
|
||||
# workspace syncing. Completion of this is what flips the app
|
||||
# into 'running' state.
|
||||
self._initial_login_completed = False
|
||||
self._initial_sign_in_completed = False
|
||||
|
||||
self._kicked_off_workspace_load = False
|
||||
|
||||
self.login_adapters: dict[LoginType, LoginAdapter] = {}
|
||||
|
||||
self._implicit_signed_in_adapter: LoginAdapter | None = None
|
||||
self._auto_signed_in = False
|
||||
self._implicit_state_changed = False
|
||||
self._can_do_auto_sign_in = True
|
||||
|
||||
if _ba.app.platform == 'android' and _ba.app.subplatform == 'google':
|
||||
from ba._login import LoginAdapterGPGS
|
||||
@ -91,7 +98,7 @@ class AccountV2Subsystem:
|
||||
if account.workspaceid is not None:
|
||||
assert account.workspacename is not None
|
||||
if (
|
||||
not self._initial_login_completed
|
||||
not self._initial_sign_in_completed
|
||||
and not self._kicked_off_workspace_load
|
||||
):
|
||||
self._kicked_off_workspace_load = True
|
||||
@ -114,9 +121,9 @@ class AccountV2Subsystem:
|
||||
return
|
||||
|
||||
# Ok; no workspace to worry about; carry on.
|
||||
if not self._initial_login_completed:
|
||||
self._initial_login_completed = True
|
||||
_ba.app.on_initial_login_completed()
|
||||
if not self._initial_sign_in_completed:
|
||||
self._initial_sign_in_completed = True
|
||||
_ba.app.on_initial_sign_in_completed()
|
||||
|
||||
def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
|
||||
"""Should be called when logins for the active account change."""
|
||||
@ -127,7 +134,7 @@ class AccountV2Subsystem:
|
||||
def on_implicit_sign_in(
|
||||
self, login_type: LoginType, login_id: str, display_name: str
|
||||
) -> None:
|
||||
"""An implicit login happened."""
|
||||
"""An implicit sign-in happened (called by native layer)."""
|
||||
from ba._login import LoginAdapter
|
||||
|
||||
with _ba.Context('ui'):
|
||||
@ -138,7 +145,7 @@ class AccountV2Subsystem:
|
||||
)
|
||||
|
||||
def on_implicit_sign_out(self, login_type: LoginType) -> None:
|
||||
"""An implicit logout happened."""
|
||||
"""An implicit sign-out happened (called by native layer)."""
|
||||
with _ba.Context('ui'):
|
||||
self.login_adapters[login_type].set_implicit_login_state(None)
|
||||
|
||||
@ -149,9 +156,15 @@ class AccountV2Subsystem:
|
||||
within a few seconds of app launch; the app can move forward
|
||||
with the startup sequence at that point.
|
||||
"""
|
||||
if not self._initial_login_completed:
|
||||
self._initial_login_completed = True
|
||||
_ba.app.on_initial_login_completed()
|
||||
if not self._initial_sign_in_completed:
|
||||
self._initial_sign_in_completed = True
|
||||
_ba.app.on_initial_sign_in_completed()
|
||||
|
||||
@staticmethod
|
||||
def _hashstr(val: str) -> str:
|
||||
md5 = hashlib.md5()
|
||||
md5.update(val.encode())
|
||||
return md5.hexdigest()
|
||||
|
||||
def on_implicit_login_state_changed(
|
||||
self,
|
||||
@ -160,18 +173,75 @@ class AccountV2Subsystem:
|
||||
) -> None:
|
||||
"""Called when implicit login state changes.
|
||||
|
||||
Logins that tend to sign themselves in/out in the background are
|
||||
considered implicit. We may choose to honor or ignore their states,
|
||||
allowing the user to opt for other login types even if the default
|
||||
implicit one can't be explicitly logged out or otherwise controlled.
|
||||
Login systems that tend to sign themselves in/out in the
|
||||
background are considered implicit. We may choose to honor or
|
||||
ignore their states, allowing the user to opt for other login
|
||||
types even if the default implicit one can't be explicitly
|
||||
logged out or otherwise controlled.
|
||||
"""
|
||||
from ba._language import Lstr
|
||||
|
||||
assert _ba.in_logic_thread()
|
||||
|
||||
cfg = _ba.app.config
|
||||
cfgkey = 'ImplicitLoginStates'
|
||||
cfgdict = _ba.app.config.setdefault(cfgkey, {})
|
||||
|
||||
# Store which (if any) adapter is currently implicitly signed in.
|
||||
# Making the assumption there will only ever be one implicit
|
||||
# adapter at a time; may need to update this if that changes.
|
||||
prev_state = cfgdict.get(login_type.value)
|
||||
if state is None:
|
||||
self._implicit_signed_in_adapter = None
|
||||
new_state = cfgdict[login_type.value] = None
|
||||
else:
|
||||
self._implicit_signed_in_adapter = self.login_adapters[login_type]
|
||||
new_state = cfgdict[login_type.value] = self._hashstr(
|
||||
state.login_id
|
||||
)
|
||||
|
||||
# Special case: if the user is already signed in but not with
|
||||
# this implicit login, we may want to let them know that the
|
||||
# 'Welcome back FOO' they likely just saw is not actually
|
||||
# accurate.
|
||||
if (
|
||||
self.primary is not None
|
||||
and not self.login_adapters[login_type].is_back_end_active()
|
||||
):
|
||||
if login_type is LoginType.GPGS:
|
||||
service_str = Lstr(resource='googlePlayText')
|
||||
else:
|
||||
service_str = None
|
||||
if service_str is not None:
|
||||
_ba.timer(
|
||||
2.0,
|
||||
tpartial(
|
||||
_ba.screenmessage,
|
||||
Lstr(
|
||||
resource='notUsingAccountText',
|
||||
subs=[
|
||||
('${ACCOUNT}', state.display_name),
|
||||
('${SERVICE}', service_str),
|
||||
],
|
||||
),
|
||||
(1, 0.5, 0),
|
||||
),
|
||||
)
|
||||
|
||||
cfg.commit()
|
||||
|
||||
# We want to respond any time the implicit state changes;
|
||||
# generally this means the user has explicitly signed in/out or
|
||||
# switched accounts within that back-end.
|
||||
if prev_state != new_state:
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'AccountV2: Implicit state changed (%s -> %s);'
|
||||
' will update app sign-in state accordingly.',
|
||||
prev_state,
|
||||
new_state,
|
||||
)
|
||||
self._implicit_state_changed = True
|
||||
|
||||
# We may want to auto-sign-in based on this new state.
|
||||
self._update_auto_sign_in()
|
||||
@ -187,12 +257,59 @@ class AccountV2Subsystem:
|
||||
def _update_auto_sign_in(self) -> None:
|
||||
from ba._internal import get_v1_account_state
|
||||
|
||||
# We attempt auto-sign-in only once.
|
||||
if self._auto_signed_in:
|
||||
# If implicit state has changed, try to respond.
|
||||
if self._implicit_state_changed:
|
||||
if self._implicit_signed_in_adapter is None:
|
||||
# If implicit back-end is signed out, follow suit
|
||||
# immediately; no need to wait for network connectivity.
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'AccountV2: Signing out as result'
|
||||
' of implicit state change...',
|
||||
)
|
||||
_ba.app.accounts_v2.set_primary_credentials(None)
|
||||
self._implicit_state_changed = False
|
||||
|
||||
# Once we've made a move here we don't want to
|
||||
# do any more automatic stuff.
|
||||
self._can_do_auto_sign_in = False
|
||||
|
||||
else:
|
||||
# Ok; we've got a new implicit state. If we've got
|
||||
# connectivity, let's attempt to sign in with it.
|
||||
# Consider this an 'explicit' sign in because the
|
||||
# implicit-login state change presumably was triggered
|
||||
# by some user action (signing in, signing out, or
|
||||
# switching accounts via the back-end).
|
||||
# NOTE: should test case where we don't have
|
||||
# connectivity here.
|
||||
if _ba.app.cloud.is_connected():
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'AccountV2: Signing in as result'
|
||||
' of implicit state change...',
|
||||
)
|
||||
self._implicit_signed_in_adapter.sign_in(
|
||||
self._on_explicit_sign_in_completed,
|
||||
description='implicit state change',
|
||||
)
|
||||
self._implicit_state_changed = False
|
||||
|
||||
# Once we've made a move here we don't want to
|
||||
# do any more automatic stuff.
|
||||
self._can_do_auto_sign_in = False
|
||||
|
||||
if not self._can_do_auto_sign_in:
|
||||
return
|
||||
|
||||
# If we're not currently signed in, we have connectivity, and
|
||||
# we have an available implicit adapter, do an auto-sign-in.
|
||||
# we have an available implicit login, auto-sign-in with it once.
|
||||
# The implicit-state-change logic above should keep things
|
||||
# mostly in-sync, but that might not always be the case due to
|
||||
# connectivity or other issues. We prefer to keep people signed
|
||||
# in as a rule, even if there are corner cases where this might
|
||||
# not be what they want (A user signing out and then restarting
|
||||
# may be auto-signed back in).
|
||||
connected = _ba.app.cloud.is_connected()
|
||||
signed_in_v1 = get_v1_account_state() == 'signed_in'
|
||||
signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials()
|
||||
@ -202,24 +319,78 @@ class AccountV2Subsystem:
|
||||
and not signed_in_v2
|
||||
and self._implicit_signed_in_adapter is not None
|
||||
):
|
||||
self._auto_signed_in = True # Only attempt this once
|
||||
self._implicit_signed_in_adapter.sign_in(self._on_sign_in_completed)
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'AccountV2: Signing in due to on-launch-auto-sign-in...',
|
||||
)
|
||||
self._can_do_auto_sign_in = False # Only ATTEMPT once
|
||||
self._implicit_signed_in_adapter.sign_in(
|
||||
self._on_implicit_sign_in_completed, description='auto-sign-in'
|
||||
)
|
||||
|
||||
def _on_sign_in_completed(
|
||||
def _on_explicit_sign_in_completed(
|
||||
self,
|
||||
adapter: LoginAdapter,
|
||||
result: LoginAdapter.SignInResult | Exception,
|
||||
) -> None:
|
||||
"""A sign-in has completed that the user asked for explicitly."""
|
||||
from ba._language import Lstr
|
||||
|
||||
del adapter # Unused.
|
||||
|
||||
# Make some noise on errors since the user knows a
|
||||
# sign-in attempt is happening in this case (the 'explicit' part).
|
||||
if isinstance(result, Exception):
|
||||
# We expect the occasional communication errors;
|
||||
# Log a full exception for anything else though.
|
||||
if not isinstance(result, CommunicationError):
|
||||
logging.warning(
|
||||
'Error on explicit accountv2 sign in attempt.',
|
||||
exc_info=result,
|
||||
)
|
||||
|
||||
# For now just show 'error'. Should do better than this.
|
||||
with _ba.Context('ui'):
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.signInErrorText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
|
||||
# Also I suppose we should sign them out in this case since
|
||||
# it could be misleading to be still signed in with the old
|
||||
# account.
|
||||
_ba.app.accounts_v2.set_primary_credentials(None)
|
||||
return
|
||||
|
||||
_ba.app.accounts_v2.set_primary_credentials(result.credentials)
|
||||
|
||||
def _on_implicit_sign_in_completed(
|
||||
self,
|
||||
adapter: LoginAdapter,
|
||||
result: LoginAdapter.SignInResult | Exception,
|
||||
) -> None:
|
||||
"""A sign-in has completed that the user didn't ask for explicitly."""
|
||||
from ba._internal import get_v1_account_state
|
||||
|
||||
del adapter # Unused.
|
||||
|
||||
# Silently ignore errors.
|
||||
# Log errors but don't inform the user; they're not aware of this
|
||||
# attempt and ignorance is bliss.
|
||||
if isinstance(result, Exception):
|
||||
# We expect the occasional communication errors;
|
||||
# Log a full exception for anything else though.
|
||||
if not isinstance(result, CommunicationError):
|
||||
logging.warning(
|
||||
'Error on implicit accountv2 sign in attempt.',
|
||||
exc_info=result,
|
||||
)
|
||||
return
|
||||
|
||||
# If we're still connected and still not signed in,
|
||||
# plug in the credentials we got.
|
||||
# plug in the credentials we got. We want to be extra cautious
|
||||
# in case the user has since explicitly signed in since we
|
||||
# kicked off.
|
||||
connected = _ba.app.cloud.is_connected()
|
||||
signed_in_v1 = get_v1_account_state() == 'signed_in'
|
||||
signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials()
|
||||
@ -227,9 +398,9 @@ class AccountV2Subsystem:
|
||||
_ba.app.accounts_v2.set_primary_credentials(result.credentials)
|
||||
|
||||
def _on_set_active_workspace_completed(self) -> None:
|
||||
if not self._initial_login_completed:
|
||||
self._initial_login_completed = True
|
||||
_ba.app.on_initial_login_completed()
|
||||
if not self._initial_sign_in_completed:
|
||||
self._initial_sign_in_completed = True
|
||||
_ba.app.on_initial_sign_in_completed()
|
||||
|
||||
|
||||
class AccountV2Handle:
|
||||
|
||||
@ -32,6 +32,7 @@ if TYPE_CHECKING:
|
||||
from bastd.actor import spazappearance
|
||||
from ba._accountv2 import AccountV2Subsystem
|
||||
from ba._level import Level
|
||||
from ba._apputils import AppHealthMonitor
|
||||
|
||||
|
||||
class App:
|
||||
@ -50,7 +51,9 @@ class App:
|
||||
# Implementations for these will be filled in by internal libs.
|
||||
accounts_v2: AccountV2Subsystem
|
||||
cloud: CloudSubsystem
|
||||
|
||||
log_handler: efro.log.LogHandler
|
||||
health_monitor: AppHealthMonitor
|
||||
|
||||
class State(Enum):
|
||||
"""High level state the app can be in."""
|
||||
@ -232,7 +235,7 @@ class App:
|
||||
self.state = self.State.LAUNCHING
|
||||
|
||||
self._launch_completed = False
|
||||
self._initial_login_completed = False
|
||||
self._initial_sign_in_completed = False
|
||||
self._meta_scan_completed = False
|
||||
self._called_on_app_running = False
|
||||
self._app_paused = False
|
||||
@ -346,7 +349,6 @@ class App:
|
||||
# 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
|
||||
from ba import _campaign
|
||||
@ -354,10 +356,16 @@ class App:
|
||||
from bastd import maps as stdmaps
|
||||
from bastd.actor import spazappearance
|
||||
from ba._generated.enums import TimeType
|
||||
from ba._apputils import (
|
||||
log_dumped_app_state,
|
||||
handle_leftover_v1_cloud_log_file,
|
||||
AppHealthMonitor,
|
||||
)
|
||||
|
||||
assert _ba.in_logic_thread()
|
||||
|
||||
self._aioloop = _asyncio.setup_asyncio()
|
||||
self.health_monitor = AppHealthMonitor()
|
||||
|
||||
cfg = self.config
|
||||
|
||||
@ -401,15 +409,15 @@ class App:
|
||||
|
||||
# If there's a leftover log file, attempt to upload it to the
|
||||
# master-server and/or get rid of it.
|
||||
_apputils.handle_leftover_v1_cloud_log_file()
|
||||
handle_leftover_v1_cloud_log_file()
|
||||
|
||||
# Only do this stuff if our config file is healthy so we don't
|
||||
# overwrite a broken one or whatnot and wipe out data.
|
||||
if not self.config_file_healthy:
|
||||
if self.platform in ('mac', 'linux', 'windows'):
|
||||
from bastd.ui import configerror
|
||||
from bastd.ui.configerror import ConfigErrorWindow
|
||||
|
||||
configerror.ConfigErrorWindow()
|
||||
_ba.pushcall(ConfigErrorWindow)
|
||||
return
|
||||
|
||||
# For now on other systems we just overwrite the bum config.
|
||||
@ -459,6 +467,9 @@ class App:
|
||||
'on_app_launch found state %s; expected LAUNCHING.', self.state
|
||||
)
|
||||
|
||||
# If any traceback dumps happened last run, log and clear them.
|
||||
log_dumped_app_state()
|
||||
|
||||
self._launch_completed = True
|
||||
self._update_state()
|
||||
|
||||
@ -483,9 +494,24 @@ class App:
|
||||
assert _ba.in_logic_thread()
|
||||
|
||||
if self._app_paused:
|
||||
self.state = self.State.PAUSED
|
||||
# Entering paused state:
|
||||
if self.state is not self.State.PAUSED:
|
||||
self.state = self.State.PAUSED
|
||||
self.cloud.on_app_pause()
|
||||
self.accounts_v1.on_app_pause()
|
||||
self.plugins.on_app_pause()
|
||||
self.health_monitor.on_app_pause()
|
||||
else:
|
||||
if self._initial_login_completed and self._meta_scan_completed:
|
||||
# Leaving paused state:
|
||||
if self.state is self.State.PAUSED:
|
||||
self.fg_state += 1
|
||||
self.cloud.on_app_resume()
|
||||
self.accounts_v1.on_app_resume()
|
||||
self.music.on_app_resume()
|
||||
self.plugins.on_app_resume()
|
||||
self.health_monitor.on_app_resume()
|
||||
|
||||
if self._initial_sign_in_completed and self._meta_scan_completed:
|
||||
self.state = self.State.RUNNING
|
||||
if not self._called_on_app_running:
|
||||
self._called_on_app_running = True
|
||||
@ -498,19 +524,16 @@ class App:
|
||||
def on_app_pause(self) -> None:
|
||||
"""Called when the app goes to a suspended state."""
|
||||
|
||||
assert not self._app_paused # Should avoid redundant calls.
|
||||
self._app_paused = True
|
||||
self._update_state()
|
||||
self.plugins.on_app_pause()
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Run when the app resumes from a suspended state."""
|
||||
|
||||
assert self._app_paused # Should avoid redundant calls.
|
||||
self._app_paused = False
|
||||
self._update_state()
|
||||
self.fg_state += 1
|
||||
self.accounts_v1.on_app_resume()
|
||||
self.music.on_app_resume()
|
||||
self.plugins.on_app_resume()
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""(internal)"""
|
||||
@ -701,8 +724,8 @@ class App:
|
||||
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
|
||||
def on_initial_login_completed(self) -> None:
|
||||
"""Callback to be run after initial login process (or lack thereof).
|
||||
def on_initial_sign_in_completed(self) -> None:
|
||||
"""Callback to be run after initial sign-in (or lack thereof).
|
||||
|
||||
This period includes things such as syncing account workspaces
|
||||
or other data so it may take a substantial amount of time.
|
||||
@ -713,5 +736,5 @@ class App:
|
||||
# (account workspaces).
|
||||
self.meta.start_extra_scan()
|
||||
|
||||
self._initial_login_completed = True
|
||||
self._initial_sign_in_completed = True
|
||||
self._update_state()
|
||||
|
||||
@ -5,12 +5,17 @@ from __future__ import annotations
|
||||
|
||||
import gc
|
||||
import os
|
||||
import logging
|
||||
from threading import Thread
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.log import LogLevel
|
||||
from efro.dataclassio import ioprepped, dataclass_to_json, dataclass_from_json
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from typing import Any, TextIO
|
||||
import ba
|
||||
|
||||
|
||||
@ -260,3 +265,245 @@ def print_corrupt_file_error() -> None:
|
||||
_ba.timer(
|
||||
2.0, Call(_ba.playsound, _ba.getsound('error')), timetype=TimeType.REAL
|
||||
)
|
||||
|
||||
|
||||
_tbfiles: list[TextIO] = []
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class DumpedAppStateMetadata:
|
||||
"""High level info about a dumped app state."""
|
||||
|
||||
reason: str
|
||||
app_time: float
|
||||
log_level: LogLevel
|
||||
|
||||
|
||||
def dump_app_state(
|
||||
delay: float = 0.0,
|
||||
reason: str = 'Unspecified',
|
||||
log_level: LogLevel = LogLevel.WARNING,
|
||||
) -> None:
|
||||
"""Dump various app state for debugging purposes.
|
||||
|
||||
This includes stack traces for all Python threads (and potentially
|
||||
other info in the future).
|
||||
|
||||
This is intended for use debugging deadlock situations. It will dump
|
||||
to preset file location(s) in the app config dir, and will attempt to
|
||||
log and clear the results after dumping. If that should fail (due to
|
||||
a hung app, etc.), then the results will be logged and cleared on the
|
||||
next app run.
|
||||
|
||||
Do not use this call during regular smooth operation of the app; it
|
||||
is should only be used for debugging or in response to confirmed
|
||||
problems as it can leak file descriptors, cause hitches, etc.
|
||||
"""
|
||||
# pylint: disable=consider-using-with
|
||||
import faulthandler
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
# Dump our metadata immediately. If a delay is passed, it generally
|
||||
# means we expect things to hang momentarily, so we should not delay
|
||||
# writing our metadata or it will likely not happen. Though we
|
||||
# should remember that metadata doesn't line up perfectly in time with
|
||||
# the dump in that case.
|
||||
try:
|
||||
mdpath = os.path.join(
|
||||
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_md'
|
||||
)
|
||||
with open(mdpath, 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(
|
||||
dataclass_to_json(
|
||||
DumpedAppStateMetadata(
|
||||
reason=reason,
|
||||
app_time=_ba.time(TimeType.REAL),
|
||||
log_level=log_level,
|
||||
)
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
# Abandon whole dump if we can't write metadata.
|
||||
logging.exception('Error writing app state dump metadata.')
|
||||
return
|
||||
|
||||
tbpath = os.path.join(
|
||||
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_tb'
|
||||
)
|
||||
|
||||
# faulthandler needs the raw file descriptor to still be valid when
|
||||
# it fires, so stuff this into a global var to make sure it doesn't get
|
||||
# cleaned up.
|
||||
tbfile = open(tbpath, 'w', encoding='utf-8')
|
||||
_tbfiles.append(tbfile)
|
||||
|
||||
if delay > 0.0:
|
||||
faulthandler.dump_traceback_later(delay, file=tbfile)
|
||||
else:
|
||||
faulthandler.dump_traceback(file=tbfile)
|
||||
|
||||
# Attempt to log shortly after dumping.
|
||||
# Allow sufficient time since we don't know how long the dump takes.
|
||||
# We want this to work from any thread, so need to kick this part
|
||||
# over to the logic thread so timer works.
|
||||
_ba.pushcall(
|
||||
lambda: _ba.timer(
|
||||
delay + 1.0, log_dumped_app_state, timetype=TimeType.REAL
|
||||
),
|
||||
from_other_thread=True,
|
||||
suppress_other_thread_warning=True,
|
||||
)
|
||||
|
||||
|
||||
def log_dumped_app_state() -> None:
|
||||
"""If an app-state dump exists, log it and clear it. No-op otherwise."""
|
||||
|
||||
try:
|
||||
out = ''
|
||||
mdpath = os.path.join(
|
||||
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_md'
|
||||
)
|
||||
if os.path.exists(mdpath):
|
||||
with open(mdpath, 'r', encoding='utf-8') as infile:
|
||||
metadata = dataclass_from_json(
|
||||
DumpedAppStateMetadata, infile.read()
|
||||
)
|
||||
os.unlink(mdpath)
|
||||
out += (
|
||||
f'App state dump:\nReason: {metadata.reason}\n'
|
||||
f'Time: {metadata.app_time:.2f}'
|
||||
)
|
||||
tbpath = os.path.join(
|
||||
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_tb'
|
||||
)
|
||||
if os.path.exists(tbpath):
|
||||
with open(tbpath, 'r', encoding='utf-8') as infile:
|
||||
out += '\nPython tracebacks:\n' + infile.read()
|
||||
os.unlink(tbpath)
|
||||
logging.log(metadata.log_level.python_logging_level, out)
|
||||
except Exception:
|
||||
logging.exception('Error logging dumped app state.')
|
||||
|
||||
|
||||
class AppHealthMonitor:
|
||||
"""Logs things like app-not-responding issues."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
assert _ba.in_logic_thread()
|
||||
self._running = True
|
||||
self._thread = Thread(target=self._app_monitor_thread_main, daemon=True)
|
||||
self._thread.start()
|
||||
self._response = False
|
||||
self._first_check = True
|
||||
|
||||
def _app_monitor_thread_main(self) -> None:
|
||||
|
||||
try:
|
||||
self._monitor_app()
|
||||
except Exception:
|
||||
logging.exception('Error in AppHealthMonitor thread.')
|
||||
|
||||
def _set_response(self) -> None:
|
||||
assert _ba.in_logic_thread()
|
||||
self._response = True
|
||||
|
||||
def _check_running(self) -> bool:
|
||||
# Workaround for the fact that mypy assumes _running
|
||||
# doesn't change during the course of a function.
|
||||
return self._running
|
||||
|
||||
def _monitor_app(self) -> None:
|
||||
import time
|
||||
|
||||
while bool(True):
|
||||
|
||||
# Always sleep a bit between checks.
|
||||
time.sleep(1.234)
|
||||
|
||||
# Do nothing while backgrounded.
|
||||
while not self._running:
|
||||
time.sleep(2.3456)
|
||||
|
||||
# Wait for the logic thread to run something we send it.
|
||||
starttime = time.monotonic()
|
||||
self._response = False
|
||||
_ba.pushcall(self._set_response, raw=True)
|
||||
while not self._response:
|
||||
|
||||
# Abort this check if we went into the background.
|
||||
if not self._check_running():
|
||||
break
|
||||
|
||||
# Wait a bit longer the first time through since the app
|
||||
# could still be starting up; we generally don't want to
|
||||
# report that.
|
||||
threshold = 10 if self._first_check else 5
|
||||
|
||||
# If we've been waiting too long (and the app is running)
|
||||
# dump the app state and bail. Make an exception for the
|
||||
# first check though since the app could just be taking
|
||||
# a while to get going; we don't want to report that.
|
||||
duration = time.monotonic() - starttime
|
||||
if duration > threshold:
|
||||
dump_app_state(
|
||||
reason=f'Logic thread unresponsive'
|
||||
f' for {threshold} seconds.'
|
||||
)
|
||||
|
||||
# We just do one alert for now.
|
||||
return
|
||||
|
||||
time.sleep(1.042)
|
||||
|
||||
self._first_check = False
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Should be called when the app pauses."""
|
||||
assert _ba.in_logic_thread()
|
||||
self._running = False
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Should be called when the app resumes."""
|
||||
assert _ba.in_logic_thread()
|
||||
self._running = True
|
||||
|
||||
|
||||
def on_too_many_file_descriptors() -> None:
|
||||
"""Called when too many file descriptors are open; trying to debug."""
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
real_time = _ba.time(TimeType.REAL)
|
||||
|
||||
def _do_log() -> None:
|
||||
pid = os.getpid()
|
||||
try:
|
||||
fdcount: int | str = len(os.listdir(f'/proc/{pid}/fd'))
|
||||
except Exception as exc:
|
||||
fdcount = f'? ({exc})'
|
||||
logging.warning(
|
||||
'TOO MANY FDS at %.2f. We are pid %d. FDCount is %s.',
|
||||
real_time,
|
||||
pid,
|
||||
fdcount,
|
||||
)
|
||||
|
||||
Thread(target=_do_log, daemon=True).start()
|
||||
|
||||
# import io
|
||||
# from efro.debug import printtypes
|
||||
|
||||
# with io.StringIO() as fstr:
|
||||
# fstr.write('Too many FDs.\n')
|
||||
# printtypes(file=fstr)
|
||||
# fstr.seek(0)
|
||||
# logging.warning(fstr.read())
|
||||
# import socket
|
||||
|
||||
# objs: list[Any] = []
|
||||
# for obj in gc.get_objects():
|
||||
# if isinstance(obj, socket.socket):
|
||||
# objs.append(obj)
|
||||
# test = open('/Users/ericf/.zshrc', 'r', encoding='utf-8')
|
||||
# reveal_type(test)
|
||||
# print('FOUND', len(objs))
|
||||
|
||||
@ -47,7 +47,7 @@ def bootstrap() -> None:
|
||||
|
||||
# Give a soft warning if we're being used with a different binary
|
||||
# version than we expect.
|
||||
expected_build = 20934
|
||||
expected_build = 20993
|
||||
running_build: int = env['build_number']
|
||||
if running_build != expected_build:
|
||||
print(
|
||||
@ -120,7 +120,8 @@ def bootstrap() -> None:
|
||||
import __main__
|
||||
|
||||
# Clear out the standard quit/exit messages since they don't
|
||||
# work for us.
|
||||
# work in our embedded situation (should revisit this once we're
|
||||
# usable from a standard interpreter).
|
||||
del __main__.__builtins__.quit
|
||||
del __main__.__builtins__.exit
|
||||
|
||||
|
||||
@ -33,6 +33,12 @@ class CloudSubsystem:
|
||||
"""
|
||||
return False # Needs to be overridden
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Should be called when the app pauses."""
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Should be called when the app resumes."""
|
||||
|
||||
def on_connectivity_changed(self, connected: bool) -> None:
|
||||
"""Called when cloud connectivity state changes."""
|
||||
if DEBUG_LOG:
|
||||
@ -125,6 +131,12 @@ class CloudSubsystem:
|
||||
) -> bacommon.cloud.WorkspaceFetchResponse:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self, msg: bacommon.cloud.MerchAvailabilityMessage
|
||||
) -> bacommon.cloud.MerchAvailabilityResponse:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self, msg: bacommon.cloud.TestMessage
|
||||
|
||||
@ -11,7 +11,7 @@ from ba._gameactivity import GameActivity
|
||||
from ba._general import WeakCall
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
from typing import Sequence
|
||||
from bastd.actor.playerspaz import PlayerSpaz
|
||||
import ba
|
||||
|
||||
@ -56,56 +56,6 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
||||
# Preload achievement images in case we get some.
|
||||
_ba.timer(2.0, WeakCall(self._preload_achievements))
|
||||
|
||||
def _show_standard_scores_to_beat_ui(
|
||||
self, scores: list[dict[str, Any]]
|
||||
) -> None:
|
||||
from efro.util import asserttype
|
||||
from ba._gameutils import timestring, animate
|
||||
from ba._nodeactor import NodeActor
|
||||
from ba._generated.enums import TimeFormat
|
||||
|
||||
display_type = self.get_score_type()
|
||||
if scores is not None:
|
||||
|
||||
# Sort by originating date so that the most recent is first.
|
||||
scores.sort(reverse=True, key=lambda s: asserttype(s['time'], int))
|
||||
|
||||
# Now make a display for the most recent challenge.
|
||||
for score in scores:
|
||||
if score['type'] == 'score_challenge':
|
||||
tval = (
|
||||
score['player']
|
||||
+ ': '
|
||||
+ timestring(
|
||||
int(score['value']) * 10,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
).evaluate()
|
||||
if display_type == 'time'
|
||||
else str(score['value'])
|
||||
)
|
||||
hattach = 'center' if display_type == 'time' else 'left'
|
||||
halign = 'center' if display_type == 'time' else 'left'
|
||||
pos = (20, -70) if display_type == 'time' else (20, -130)
|
||||
txt = NodeActor(
|
||||
_ba.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'v_attach': 'top',
|
||||
'h_attach': hattach,
|
||||
'h_align': halign,
|
||||
'color': (0.7, 0.4, 1, 1),
|
||||
'shadow': 0.5,
|
||||
'flatness': 1.0,
|
||||
'position': pos,
|
||||
'scale': 0.6,
|
||||
'text': tval,
|
||||
},
|
||||
)
|
||||
).autoretain()
|
||||
assert txt.node is not None
|
||||
animate(txt.node, 'scale', {1.0: 0.0, 1.1: 0.7, 1.2: 0.6})
|
||||
break
|
||||
|
||||
# FIXME: this is now redundant with activityutils.getscoreconfig();
|
||||
# need to kill this.
|
||||
def get_score_type(self) -> str:
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Snippets of code for use by the internal C++ layer.
|
||||
"""Snippets of code for use by the internal layer.
|
||||
|
||||
History: originally I would dynamically compile/eval bits of Python text
|
||||
from within C++ code, but the major downside there was that none of that was
|
||||
type-checked so if names or arguments changed I would never catch code breakage
|
||||
until the code was next run. By defining all snippets I use here and then
|
||||
capturing references to them all at launch I can immediately verify everything
|
||||
I'm looking for exists and pylint/mypy can do their magic on this file.
|
||||
History: originally the engine would dynamically compile/eval various Python
|
||||
code from within C++ code, but the major downside there was that none of it
|
||||
was type-checked so if names or arguments changed it would go unnoticed
|
||||
until it broke at runtime. By instead defining such snippets here and then
|
||||
capturing references to them all at launch it is possible to allow linting
|
||||
and type-checking magic to happen and most issues will be caught immediately.
|
||||
"""
|
||||
# (most of these are self-explanatory)
|
||||
# pylint: disable=missing-function-docstring
|
||||
@ -461,12 +461,42 @@ def login_adapter_get_sign_in_token_response(
|
||||
) -> None:
|
||||
"""Login adapter do-sign-in completed."""
|
||||
from bacommon.login import LoginType
|
||||
from ba._login import LoginAdapterGPGS
|
||||
from ba._login import LoginAdapterNative
|
||||
|
||||
login_type = LoginType(login_type_str)
|
||||
attempt_id = int(attempt_id_str)
|
||||
result = None if result_str == '' else result_str
|
||||
with _ba.Context('ui'):
|
||||
adapter = _ba.app.accounts_v2.login_adapters[login_type]
|
||||
assert isinstance(adapter, LoginAdapterGPGS)
|
||||
assert isinstance(adapter, LoginAdapterNative)
|
||||
adapter.on_sign_in_complete(attempt_id=attempt_id, result=result)
|
||||
|
||||
|
||||
def show_client_too_old_error() -> None:
|
||||
"""Called at launch if the server tells us we're too old to talk to it."""
|
||||
from ba._language import Lstr
|
||||
|
||||
# If you are using an old build of the app and would like to stop
|
||||
# seeing this error at launch, do:
|
||||
# ba.app.config['SuppressClientTooOldErrorForBuild'] = ba.app.build_number
|
||||
# ba.app.config.commit()
|
||||
# Note that you will have to do that again later if you update to
|
||||
# a newer build.
|
||||
if (
|
||||
_ba.app.config.get('SuppressClientTooOldErrorForBuild')
|
||||
== _ba.app.build_number
|
||||
):
|
||||
return
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
translate=(
|
||||
'serverResponses',
|
||||
'Server functionality is no longer supported'
|
||||
' in this version of the game;\n'
|
||||
'Please update to a newer version.',
|
||||
)
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
||||
@ -104,7 +104,6 @@ def submit_score(
|
||||
name: Any,
|
||||
score: int | None,
|
||||
callback: Callable,
|
||||
friend_callback: Callable | None,
|
||||
order: str = 'increasing',
|
||||
tournament_id: str | None = None,
|
||||
score_type: str = 'points',
|
||||
@ -125,7 +124,6 @@ def submit_score(
|
||||
name=name,
|
||||
score=score,
|
||||
callback=callback,
|
||||
friend_callback=friend_callback,
|
||||
order=order,
|
||||
tournament_id=tournament_id,
|
||||
score_type=score_type,
|
||||
|
||||
@ -67,38 +67,39 @@ class LanguageSubsystem:
|
||||
|
||||
def _get_default_language(self) -> str:
|
||||
languages = {
|
||||
'ar': 'Arabic',
|
||||
'be': 'Belarussian',
|
||||
'zh': 'Chinese',
|
||||
'hr': 'Croatian',
|
||||
'cs': 'Czech',
|
||||
'da': 'Danish',
|
||||
'nl': 'Dutch',
|
||||
'eo': 'Esperanto',
|
||||
'fil': 'Filipino',
|
||||
'fr': 'French',
|
||||
'de': 'German',
|
||||
'el': 'Greek',
|
||||
'hi': 'Hindi',
|
||||
'hu': 'Hungarian',
|
||||
'id': 'Indonesian',
|
||||
'it': 'Italian',
|
||||
'ko': 'Korean',
|
||||
'ms': 'Malay',
|
||||
'fa': 'Persian',
|
||||
'pl': 'Polish',
|
||||
'pt': 'Portuguese',
|
||||
'ro': 'Romanian',
|
||||
'ru': 'Russian',
|
||||
'sr': 'Serbian',
|
||||
'es': 'Spanish',
|
||||
'sk': 'Slovak',
|
||||
'it': 'Italian',
|
||||
'nl': 'Dutch',
|
||||
'da': 'Danish',
|
||||
'pt': 'Portuguese',
|
||||
'fr': 'French',
|
||||
'el': 'Greek',
|
||||
'ru': 'Russian',
|
||||
'pl': 'Polish',
|
||||
'sv': 'Swedish',
|
||||
'eo': 'Esperanto',
|
||||
'cs': 'Czech',
|
||||
'hr': 'Croatian',
|
||||
'hu': 'Hungarian',
|
||||
'be': 'Belarussian',
|
||||
'ro': 'Romanian',
|
||||
'ko': 'Korean',
|
||||
'fa': 'Persian',
|
||||
'ar': 'Arabic',
|
||||
'zh': 'Chinese',
|
||||
'tr': 'Turkish',
|
||||
'th': 'Thai',
|
||||
'id': 'Indonesian',
|
||||
'sr': 'Serbian',
|
||||
'uk': 'Ukrainian',
|
||||
'vi': 'Vietnamese',
|
||||
'vec': 'Venetian',
|
||||
'hi': 'Hindi',
|
||||
'ta': 'Tamil',
|
||||
'fil': 'Filipino',
|
||||
'th': 'Thai',
|
||||
'tr': 'Turkish',
|
||||
'uk': 'Ukrainian',
|
||||
'vec': 'Venetian',
|
||||
'vi': 'Vietnamese',
|
||||
}
|
||||
|
||||
# Special case for Chinese: map specific variations to traditional.
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, final
|
||||
@ -57,6 +58,9 @@ class LoginAdapter:
|
||||
# current active primary account.
|
||||
self._active_login_id: str | None = None
|
||||
|
||||
self._last_sign_in_time: float | None = None
|
||||
self._last_sign_in_desc: str | None = None
|
||||
|
||||
def on_app_launch(self) -> None:
|
||||
"""Should be called for each adapter in on_app_launch."""
|
||||
|
||||
@ -102,6 +106,9 @@ class LoginAdapter:
|
||||
# (possibly) push it to the app for handling.
|
||||
self._update_implicit_login_state()
|
||||
|
||||
# This might affect whether we consider that back-end as 'active'.
|
||||
self._update_back_end_active()
|
||||
|
||||
def set_active_logins(self, logins: dict[LoginType, str]) -> None:
|
||||
"""Keep the adapter informed of actively used logins.
|
||||
|
||||
@ -116,7 +123,7 @@ class LoginAdapter:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter got active logins %s.',
|
||||
self.login_type.name,
|
||||
logins,
|
||||
{k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
|
||||
)
|
||||
|
||||
self._active_login_id = logins.get(self.login_type)
|
||||
@ -139,6 +146,7 @@ class LoginAdapter:
|
||||
def sign_in(
|
||||
self,
|
||||
result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
|
||||
description: str,
|
||||
) -> None:
|
||||
"""Attempt an explicit sign in via this adapter.
|
||||
|
||||
@ -148,6 +156,38 @@ class LoginAdapter:
|
||||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
# Have been seeing multiple sign-in attempts come through
|
||||
# nearly simultaneously which can be problematic server-side.
|
||||
# Let's error if a sign-in attempt is made within a few seconds
|
||||
# of the last one to address this.
|
||||
now = time.monotonic()
|
||||
appnow = _ba.time(TimeType.REAL)
|
||||
if self._last_sign_in_time is not None:
|
||||
since_last = now - self._last_sign_in_time
|
||||
if since_last < 1.0:
|
||||
logging.warning(
|
||||
'LoginAdapter: %s adapter sign_in() called too soon'
|
||||
' (%.2fs) after last; this-desc="%s", last-desc="%s",'
|
||||
' ba-real-time=%.2f.',
|
||||
self.login_type.name,
|
||||
since_last,
|
||||
description,
|
||||
self._last_sign_in_desc,
|
||||
appnow,
|
||||
)
|
||||
_ba.pushcall(
|
||||
Call(
|
||||
result_cb,
|
||||
self,
|
||||
RuntimeError('sign_in called too soon after last.'),
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self._last_sign_in_desc = description
|
||||
self._last_sign_in_time = now
|
||||
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
@ -171,7 +211,7 @@ class LoginAdapter:
|
||||
Call(
|
||||
result_cb,
|
||||
self,
|
||||
RuntimeError('fetch-sign-in-token failed'),
|
||||
RuntimeError('fetch-sign-in-token failed.'),
|
||||
)
|
||||
)
|
||||
return
|
||||
@ -189,7 +229,6 @@ class LoginAdapter:
|
||||
def _got_sign_in_response(
|
||||
response: bacommon.cloud.SignInResponse | Exception,
|
||||
) -> None:
|
||||
# from ba._language import Lstr
|
||||
|
||||
if isinstance(response, Exception):
|
||||
if DEBUG_LOG:
|
||||
@ -199,10 +238,6 @@ class LoginAdapter:
|
||||
self.login_type.name,
|
||||
response,
|
||||
)
|
||||
# _ba.screenmessage(
|
||||
# Lstr(resource='errorText'), color=(1, 0, 0)
|
||||
# )
|
||||
# _ba.playsound(_ba.getsound('error'))
|
||||
_ba.pushcall(Call(result_cb, self, response))
|
||||
else:
|
||||
if DEBUG_LOG:
|
||||
@ -213,7 +248,10 @@ class LoginAdapter:
|
||||
)
|
||||
if response.credentials is None:
|
||||
result2: LoginAdapter.SignInResult | Exception = (
|
||||
RuntimeError('No credentials returned.')
|
||||
RuntimeError(
|
||||
'No credentials returned after'
|
||||
' submitting sign-in-token.'
|
||||
)
|
||||
)
|
||||
else:
|
||||
result2 = self.SignInResult(
|
||||
@ -222,13 +260,22 @@ class LoginAdapter:
|
||||
_ba.pushcall(Call(result_cb, self, result2))
|
||||
|
||||
_ba.app.cloud.send_message_cb(
|
||||
bacommon.cloud.SignInMessage(self.login_type, result),
|
||||
bacommon.cloud.SignInMessage(
|
||||
self.login_type,
|
||||
result,
|
||||
description=description,
|
||||
apptime=appnow,
|
||||
),
|
||||
on_response=_got_sign_in_response,
|
||||
)
|
||||
|
||||
# Kick off the process by fetching a sign-in token.
|
||||
self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
|
||||
|
||||
def is_back_end_active(self) -> bool:
|
||||
"""Is this adapter's back-end currently active?"""
|
||||
return self._back_end_active
|
||||
|
||||
def get_sign_in_token(
|
||||
self, completion_cb: Callable[[str | None], None]
|
||||
) -> None:
|
||||
@ -289,8 +336,8 @@ class LoginAdapter:
|
||||
self._back_end_active = is_active
|
||||
|
||||
|
||||
class LoginAdapterGPGS(LoginAdapter):
|
||||
"""Google Play Game Services adapter."""
|
||||
class LoginAdapterNative(LoginAdapter):
|
||||
"""A login adapter that does its work in the native layer."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(LoginType.GPGS)
|
||||
@ -308,6 +355,9 @@ class LoginAdapterGPGS(LoginAdapter):
|
||||
self._sign_in_attempt_num += 1
|
||||
_ba.login_adapter_get_sign_in_token(self.login_type.value, attempt_id)
|
||||
|
||||
def on_back_end_active_change(self, active: bool) -> None:
|
||||
_ba.login_adapter_back_end_active_change(self.login_type.value, active)
|
||||
|
||||
def on_sign_in_complete(self, attempt_id: int, result: str | None) -> None:
|
||||
"""Called by the native layer on a completed attempt."""
|
||||
assert _ba.in_logic_thread()
|
||||
@ -316,3 +366,7 @@ class LoginAdapterGPGS(LoginAdapter):
|
||||
return
|
||||
callback = self._sign_in_attempts.pop(attempt_id)
|
||||
callback(result)
|
||||
|
||||
|
||||
class LoginAdapterGPGS(LoginAdapterNative):
|
||||
"""Google Play Game Services adapter."""
|
||||
|
||||
@ -173,7 +173,7 @@ class PluginSubsystem:
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
plugnames = ', '.join(disappeared_plugs)
|
||||
logging.warning(
|
||||
logging.info(
|
||||
'%d plugin(s) no longer found: %s.',
|
||||
len(disappeared_plugs),
|
||||
plugnames,
|
||||
|
||||
@ -418,6 +418,7 @@ class ServerController:
|
||||
|
||||
# Call set-enabled last (will push state to the cloud).
|
||||
_ba.set_public_party_max_size(self._config.max_party_size)
|
||||
_ba.set_public_party_queue_enabled(self._config.enable_queue)
|
||||
_ba.set_public_party_name(self._config.party_name)
|
||||
_ba.set_public_party_stats_url(self._config.stats_url)
|
||||
_ba.set_public_party_enabled(self._config.party_is_public)
|
||||
|
||||
@ -30,6 +30,8 @@ def get_store_item_name_translated(item_name: str) -> ba.Lstr:
|
||||
return _language.Lstr(
|
||||
translate=('characterNames', item_info['character'])
|
||||
)
|
||||
if item_name in ['merch']:
|
||||
return _language.Lstr(resource='merchText')
|
||||
if item_name in ['upgrades.pro', 'pro']:
|
||||
return _language.Lstr(
|
||||
resource='store.bombSquadProNameText',
|
||||
@ -50,8 +52,17 @@ def get_store_item_display_size(item_name: str) -> tuple[float, float]:
|
||||
"""(internal)"""
|
||||
if item_name.startswith('characters.'):
|
||||
return 340 * 0.6, 430 * 0.6
|
||||
if item_name in ['pro', 'upgrades.pro']:
|
||||
return 650 * 0.9, 500 * 0.85
|
||||
if item_name in ['pro', 'upgrades.pro', 'merch']:
|
||||
from ba._generated.enums import UIScale
|
||||
|
||||
return 650 * 0.9, 500 * (
|
||||
0.72
|
||||
if (
|
||||
_ba.app.config.get('Merch Link')
|
||||
and _ba.app.ui.uiscale is UIScale.SMALL
|
||||
)
|
||||
else 0.85
|
||||
)
|
||||
if item_name.startswith('maps.'):
|
||||
return 510 * 0.6, 450 * 0.6
|
||||
if item_name.startswith('icons.'):
|
||||
@ -96,6 +107,7 @@ def get_store_items() -> dict[str, dict]:
|
||||
'characters.taobaomascot': {'character': 'Taobao Mascot'},
|
||||
'characters.santa': {'character': 'Santa Claus'},
|
||||
'characters.bunny': {'character': 'Easter Bunny'},
|
||||
'merch': {},
|
||||
'pro': {},
|
||||
'maps.lake_frigid': {'map_type': maps.LakeFrigid},
|
||||
'games.ninja_fight': {
|
||||
@ -193,9 +205,7 @@ def get_store_items() -> dict[str, dict]:
|
||||
'icons.fireball': {'icon': _ba.charstr(SpecialChar.FIREBALL)},
|
||||
'icons.mikirog': {'icon': _ba.charstr(SpecialChar.MIKIROG)},
|
||||
}
|
||||
store_items = _ba.app.store_items
|
||||
assert store_items is not None
|
||||
return store_items
|
||||
return _ba.app.store_items
|
||||
|
||||
|
||||
def get_store_layout() -> dict[str, list[dict[str, Any]]]:
|
||||
@ -261,7 +271,6 @@ def get_store_layout() -> dict[str, list[dict[str, Any]]]:
|
||||
],
|
||||
}
|
||||
store_layout = _ba.app.store_layout
|
||||
assert store_layout is not None
|
||||
store_layout['characters'] = [
|
||||
{
|
||||
'items': [
|
||||
@ -302,6 +311,12 @@ def get_store_layout() -> dict[str, list[dict[str, Any]]]:
|
||||
'items': ['games.easter_egg_hunt'],
|
||||
}
|
||||
)
|
||||
|
||||
# This will cause merch to show only if the master-server has
|
||||
# given us a link (which means merch is available in our region).
|
||||
store_layout['extras'] = [{'items': ['pro']}]
|
||||
if _ba.app.config.get('Merch Link'):
|
||||
store_layout['extras'][0]['items'].append('merch')
|
||||
return store_layout
|
||||
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ from _ba import (
|
||||
get_public_party_max_size,
|
||||
set_public_party_name,
|
||||
set_public_party_max_size,
|
||||
set_public_party_queue_enabled,
|
||||
set_authenticate_clients,
|
||||
set_public_party_enabled,
|
||||
reset_random_player_names,
|
||||
@ -100,6 +101,8 @@ from ba._apputils import (
|
||||
is_browser_likely_available,
|
||||
get_remote_app_name,
|
||||
should_submit_debug_info,
|
||||
dump_app_state,
|
||||
log_dumped_app_state,
|
||||
)
|
||||
from ba._benchmark import (
|
||||
run_gpu_benchmark,
|
||||
@ -211,6 +214,7 @@ __all__ = [
|
||||
'get_public_party_max_size',
|
||||
'set_public_party_name',
|
||||
'set_public_party_max_size',
|
||||
'set_public_party_queue_enabled',
|
||||
'set_authenticate_clients',
|
||||
'set_public_party_enabled',
|
||||
'reset_random_player_names',
|
||||
@ -330,4 +334,6 @@ __all__ = [
|
||||
'sign_out_v1',
|
||||
'sign_in_v1',
|
||||
'mark_config_dirty',
|
||||
'dump_app_state',
|
||||
'log_dumped_app_state',
|
||||
]
|
||||
|
||||
@ -116,7 +116,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||
self._newly_complete: bool | None = None
|
||||
self._is_more_levels: bool | None = None
|
||||
self._next_level_name: str | None = None
|
||||
self._show_friend_scores: bool | None = None
|
||||
self._show_info: dict[str, Any] | None = None
|
||||
self._name_str: str | None = None
|
||||
self._friends_loading_status: ba.Actor | None = None
|
||||
@ -177,12 +176,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||
.replace(' ', '_')
|
||||
)
|
||||
|
||||
# If game-center/etc scores are available we show our friends'
|
||||
# scores. Otherwise we show our local high scores.
|
||||
self._show_friend_scores = ba.internal.game_service_has_leaderboard(
|
||||
self._game_name_str, self._game_config_str
|
||||
)
|
||||
|
||||
try:
|
||||
self._old_best_rank = self._campaign.getlevel(
|
||||
self._level_name
|
||||
@ -366,21 +359,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||
|
||||
ba.internal.set_ui_input_device(None) # Menu is up for grabs.
|
||||
|
||||
if self._show_friend_scores:
|
||||
ba.buttonwidget(
|
||||
parent=rootc,
|
||||
color=(0.45, 0.4, 0.5),
|
||||
position=(h_offs - 520, v_offs + 480),
|
||||
size=(300, 60),
|
||||
label=ba.Lstr(resource='topFriendsText'),
|
||||
on_activate_call=ba.WeakCall(self._ui_gc),
|
||||
transition_delay=delay + 0.5,
|
||||
icon=self._game_service_leaderboards_texture,
|
||||
icon_color=self._game_service_icon_color,
|
||||
autoselect=True,
|
||||
selectable=can_select_extra_buttons,
|
||||
)
|
||||
|
||||
if self._have_achievements and self._account_has_achievements:
|
||||
ba.buttonwidget(
|
||||
parent=rootc,
|
||||
@ -773,18 +751,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||
[p.name for p in self._playerinfos]
|
||||
)
|
||||
|
||||
if self._show_friend_scores:
|
||||
self._friends_loading_status = Text(
|
||||
ba.Lstr(
|
||||
value='${A}...',
|
||||
subs=[('${A}', ba.Lstr(resource='loadingText'))],
|
||||
),
|
||||
position=(-405, 150 + 30),
|
||||
color=(1, 1, 1, 0.4),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
scale=0.7,
|
||||
transition_delay=2.0,
|
||||
)
|
||||
self._score_loading_status = Text(
|
||||
ba.Lstr(
|
||||
value='${A}...',
|
||||
@ -850,8 +816,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||
# We expect this only in kiosk mode; complain otherwise.
|
||||
if not (ba.app.demo_mode or ba.app.arcade_mode):
|
||||
print('got not-signed-in at score-submit; unexpected')
|
||||
if self._show_friend_scores:
|
||||
ba.pushcall(ba.WeakCall(self._got_friend_score_results, None))
|
||||
ba.pushcall(ba.WeakCall(self._got_score_results, None))
|
||||
else:
|
||||
assert self._game_name_str is not None
|
||||
@ -862,9 +826,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||
name_str,
|
||||
self._score,
|
||||
ba.WeakCall(self._got_score_results),
|
||||
ba.WeakCall(self._got_friend_score_results)
|
||||
if self._show_friend_scores
|
||||
else None,
|
||||
order=self._score_order,
|
||||
tournament_id=self.session.tournament_id,
|
||||
score_type=self._score_type,
|
||||
@ -899,138 +860,118 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||
assert txt.node
|
||||
txt.node.client_only = True
|
||||
|
||||
# If we have no friend scores, display local best scores.
|
||||
if self._show_friend_scores:
|
||||
ts_height = 300
|
||||
ts_h_offs = -480
|
||||
v_offs = 40
|
||||
Text(
|
||||
ba.Lstr(resource='yourBestScoresText')
|
||||
if self._score_type == 'points'
|
||||
else ba.Lstr(resource='yourBestTimesText'),
|
||||
maxwidth=210,
|
||||
position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
|
||||
transition=Text.Transition.IN_RIGHT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
scale=1.2,
|
||||
transition_delay=1.8,
|
||||
).autoretain()
|
||||
|
||||
# Host has a button, so we need client-only text.
|
||||
ts_height = 300
|
||||
ts_h_offs = -480
|
||||
v_offs = 40
|
||||
txt = Text(
|
||||
ba.Lstr(resource='topFriendsText'),
|
||||
maxwidth=210,
|
||||
position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
|
||||
transition=Text.Transition.IN_RIGHT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
scale=1.2,
|
||||
transition_delay=1.8,
|
||||
).autoretain()
|
||||
assert txt.node
|
||||
txt.node.client_only = True
|
||||
else:
|
||||
display_scores = list(our_high_scores)
|
||||
display_count = 5
|
||||
|
||||
ts_height = 300
|
||||
ts_h_offs = -480
|
||||
v_offs = 40
|
||||
Text(
|
||||
ba.Lstr(resource='yourBestScoresText')
|
||||
if self._score_type == 'points'
|
||||
else ba.Lstr(resource='yourBestTimesText'),
|
||||
maxwidth=210,
|
||||
position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
|
||||
transition=Text.Transition.IN_RIGHT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
scale=1.2,
|
||||
transition_delay=1.8,
|
||||
).autoretain()
|
||||
while len(display_scores) < display_count:
|
||||
display_scores.append((0, None))
|
||||
|
||||
display_scores = list(our_high_scores)
|
||||
display_count = 5
|
||||
|
||||
while len(display_scores) < display_count:
|
||||
display_scores.append((0, None))
|
||||
|
||||
showed_ours = False
|
||||
h_offs_extra = 85 if self._score_type == 'points' else 130
|
||||
v_offs_extra = 20
|
||||
v_offs_names = 0
|
||||
scale = 1.0
|
||||
p_count = len(self._playerinfos)
|
||||
h_offs_extra -= 75
|
||||
if p_count > 1:
|
||||
h_offs_extra -= 20
|
||||
if p_count == 2:
|
||||
scale = 0.9
|
||||
elif p_count == 3:
|
||||
scale = 0.65
|
||||
elif p_count == 4:
|
||||
scale = 0.5
|
||||
times: list[tuple[float, float]] = []
|
||||
for i in range(display_count):
|
||||
times.insert(
|
||||
random.randrange(0, len(times) + 1),
|
||||
(1.9 + i * 0.05, 2.3 + i * 0.05),
|
||||
)
|
||||
for i in range(display_count):
|
||||
try:
|
||||
if display_scores[i][1] is None:
|
||||
name_str = '-'
|
||||
else:
|
||||
# noinspection PyUnresolvedReferences
|
||||
name_str = ', '.join(
|
||||
[p['name'] for p in display_scores[i][1]['players']]
|
||||
)
|
||||
except Exception:
|
||||
ba.print_exception(
|
||||
f'Error calcing name_str for {display_scores}'
|
||||
)
|
||||
showed_ours = False
|
||||
h_offs_extra = 85 if self._score_type == 'points' else 130
|
||||
v_offs_extra = 20
|
||||
v_offs_names = 0
|
||||
scale = 1.0
|
||||
p_count = len(self._playerinfos)
|
||||
h_offs_extra -= 75
|
||||
if p_count > 1:
|
||||
h_offs_extra -= 20
|
||||
if p_count == 2:
|
||||
scale = 0.9
|
||||
elif p_count == 3:
|
||||
scale = 0.65
|
||||
elif p_count == 4:
|
||||
scale = 0.5
|
||||
times: list[tuple[float, float]] = []
|
||||
for i in range(display_count):
|
||||
times.insert(
|
||||
random.randrange(0, len(times) + 1),
|
||||
(1.9 + i * 0.05, 2.3 + i * 0.05),
|
||||
)
|
||||
for i in range(display_count):
|
||||
try:
|
||||
if display_scores[i][1] is None:
|
||||
name_str = '-'
|
||||
if display_scores[i] == our_score and not showed_ours:
|
||||
flash = True
|
||||
color0 = (0.6, 0.4, 0.1, 1.0)
|
||||
color1 = (0.6, 0.6, 0.6, 1.0)
|
||||
tdelay1 = 3.7
|
||||
tdelay2 = 3.7
|
||||
showed_ours = True
|
||||
else:
|
||||
flash = False
|
||||
color0 = (0.6, 0.4, 0.1, 1.0)
|
||||
color1 = (0.6, 0.6, 0.6, 1.0)
|
||||
tdelay1 = times[i][0]
|
||||
tdelay2 = times[i][1]
|
||||
Text(
|
||||
str(display_scores[i][0])
|
||||
if self._score_type == 'points'
|
||||
else ba.timestring(
|
||||
display_scores[i][0] * 10,
|
||||
timeformat=ba.TimeFormat.MILLISECONDS,
|
||||
suppress_format_warning=True,
|
||||
),
|
||||
position=(
|
||||
ts_h_offs + 20 + h_offs_extra,
|
||||
v_offs_extra
|
||||
+ ts_height / 2
|
||||
+ -ts_height * (i + 1) / 10
|
||||
+ v_offs
|
||||
+ 11.0,
|
||||
),
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=color0,
|
||||
flash=flash,
|
||||
transition=Text.Transition.IN_RIGHT,
|
||||
transition_delay=tdelay1,
|
||||
).autoretain()
|
||||
# noinspection PyUnresolvedReferences
|
||||
name_str = ', '.join(
|
||||
[p['name'] for p in display_scores[i][1]['players']]
|
||||
)
|
||||
except Exception:
|
||||
ba.print_exception(
|
||||
f'Error calcing name_str for {display_scores}'
|
||||
)
|
||||
name_str = '-'
|
||||
if display_scores[i] == our_score and not showed_ours:
|
||||
flash = True
|
||||
color0 = (0.6, 0.4, 0.1, 1.0)
|
||||
color1 = (0.6, 0.6, 0.6, 1.0)
|
||||
tdelay1 = 3.7
|
||||
tdelay2 = 3.7
|
||||
showed_ours = True
|
||||
else:
|
||||
flash = False
|
||||
color0 = (0.6, 0.4, 0.1, 1.0)
|
||||
color1 = (0.6, 0.6, 0.6, 1.0)
|
||||
tdelay1 = times[i][0]
|
||||
tdelay2 = times[i][1]
|
||||
Text(
|
||||
str(display_scores[i][0])
|
||||
if self._score_type == 'points'
|
||||
else ba.timestring(
|
||||
display_scores[i][0] * 10,
|
||||
timeformat=ba.TimeFormat.MILLISECONDS,
|
||||
suppress_format_warning=True,
|
||||
),
|
||||
position=(
|
||||
ts_h_offs + 20 + h_offs_extra,
|
||||
v_offs_extra
|
||||
+ ts_height / 2
|
||||
+ -ts_height * (i + 1) / 10
|
||||
+ v_offs
|
||||
+ 11.0,
|
||||
),
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=color0,
|
||||
flash=flash,
|
||||
transition=Text.Transition.IN_RIGHT,
|
||||
transition_delay=tdelay1,
|
||||
).autoretain()
|
||||
|
||||
Text(
|
||||
ba.Lstr(value=name_str),
|
||||
position=(
|
||||
ts_h_offs + 35 + h_offs_extra,
|
||||
v_offs_extra
|
||||
+ ts_height / 2
|
||||
+ -ts_height * (i + 1) / 10
|
||||
+ v_offs_names
|
||||
+ v_offs
|
||||
+ 11.0,
|
||||
),
|
||||
maxwidth=80.0 + 100.0 * len(self._playerinfos),
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=color1,
|
||||
flash=flash,
|
||||
scale=scale,
|
||||
transition=Text.Transition.IN_RIGHT,
|
||||
transition_delay=tdelay2,
|
||||
).autoretain()
|
||||
Text(
|
||||
ba.Lstr(value=name_str),
|
||||
position=(
|
||||
ts_h_offs + 35 + h_offs_extra,
|
||||
v_offs_extra
|
||||
+ ts_height / 2
|
||||
+ -ts_height * (i + 1) / 10
|
||||
+ v_offs_names
|
||||
+ v_offs
|
||||
+ 11.0,
|
||||
),
|
||||
maxwidth=80.0 + 100.0 * len(self._playerinfos),
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=color1,
|
||||
flash=flash,
|
||||
scale=scale,
|
||||
transition=Text.Transition.IN_RIGHT,
|
||||
transition_delay=tdelay2,
|
||||
).autoretain()
|
||||
|
||||
# Show achievements for this level.
|
||||
ts_height = -150
|
||||
|
||||
@ -180,6 +180,7 @@ class Spaz(ba.Actor):
|
||||
self._bomb_wear_off_flash_timer: ba.Timer | None = None
|
||||
self._multi_bomb_wear_off_timer: ba.Timer | None = None
|
||||
self._multi_bomb_wear_off_flash_timer: ba.Timer | None = None
|
||||
self._curse_timer: ba.Timer | None = None
|
||||
self.bomb_count = self.default_bomb_count
|
||||
self._max_bomb_count = self.default_bomb_count
|
||||
self.bomb_type_default = self.default_bomb_type
|
||||
@ -620,7 +621,8 @@ class Spaz(ba.Actor):
|
||||
self.node.curse_death_time = int(
|
||||
1000.0 * (tval + self.curse_time)
|
||||
)
|
||||
ba.timer(5.0, ba.WeakCall(self.curse_explode))
|
||||
self._curse_timer = ba.Timer(5.0,
|
||||
ba.WeakCall(self.curse_explode))
|
||||
|
||||
def equip_boxing_gloves(self) -> None:
|
||||
"""
|
||||
|
||||
@ -616,9 +616,6 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
|
||||
for bottype in self._bot_types_initial:
|
||||
self._spawn_bot(bottype)
|
||||
|
||||
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
|
||||
self._show_standard_scores_to_beat_ui(scores)
|
||||
|
||||
def _on_bot_spawn(self, spaz: SpazBot) -> None:
|
||||
# We want to move to the left by default.
|
||||
spaz.target_point_default = ba.Vec3(0, 0, 0)
|
||||
|
||||
@ -682,9 +682,6 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]):
|
||||
self._bots = SpazBotSet()
|
||||
ba.timer(4.0, self._start_updating_waves)
|
||||
|
||||
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
|
||||
self._show_standard_scores_to_beat_ui(scores)
|
||||
|
||||
def _get_dist_grp_totals(self, grps: list[Any]) -> tuple[int, int]:
|
||||
totalpts = 0
|
||||
totaldudes = 0
|
||||
|
||||
@ -684,9 +684,6 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]):
|
||||
},
|
||||
)
|
||||
|
||||
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
|
||||
self._show_standard_scores_to_beat_ui(scores)
|
||||
|
||||
def _update_waves(self) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
|
||||
@ -326,9 +326,6 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
|
||||
self._show_standard_scores_to_beat_ui(scores)
|
||||
|
||||
def end_game(self) -> None:
|
||||
# Tell our bots to celebrate just to rub it in.
|
||||
self._bots.final_celebrate()
|
||||
|
||||
@ -17,6 +17,11 @@ import ba.internal
|
||||
if TYPE_CHECKING:
|
||||
from ba.internal import LoginAdapter
|
||||
|
||||
# These days we're directing people to the web based account settings
|
||||
# for V2 account linking and trying to get them to disconnect remaining
|
||||
# V1 links, but leaving this escape hatch here in case needed.
|
||||
FORCE_ENABLE_V1_LINKING = False
|
||||
|
||||
|
||||
class AccountSettingsWindow(ba.Window):
|
||||
"""Window for account related functionality."""
|
||||
@ -33,10 +38,14 @@ class AccountSettingsWindow(ba.Window):
|
||||
self._sign_in_v2_proxy_button: ba.Widget | None = None
|
||||
self._sign_in_device_button: ba.Widget | None = None
|
||||
|
||||
self._show_legacy_unlink_button = False
|
||||
|
||||
self._signing_in_adapter: LoginAdapter | None = None
|
||||
self._close_once_signed_in = close_once_signed_in
|
||||
ba.set_analytics_screen('Account Window')
|
||||
|
||||
self._explicitly_signed_out_of_gpgs = False
|
||||
|
||||
# If they provided an origin-widget, scale up from that.
|
||||
scale_origin: tuple[float, float] | None
|
||||
if origin_widget is not None:
|
||||
@ -50,14 +59,8 @@ class AccountSettingsWindow(ba.Window):
|
||||
self._r = 'accountSettingsWindow'
|
||||
self._modal = modal
|
||||
self._needs_refresh = False
|
||||
self._signed_in = ba.internal.get_v1_account_state() == 'signed_in'
|
||||
self._account_state_num = ba.internal.get_v1_account_state_num()
|
||||
self._show_linked = (
|
||||
self._signed_in
|
||||
and ba.internal.get_v1_account_misc_read_val(
|
||||
'allowAccountLinking2', False
|
||||
)
|
||||
)
|
||||
self._v1_signed_in = ba.internal.get_v1_account_state() == 'signed_in'
|
||||
self._v1_account_state_num = ba.internal.get_v1_account_state_num()
|
||||
self._check_sign_in_timer = ba.Timer(
|
||||
1.0,
|
||||
ba.WeakCall(self._update),
|
||||
@ -66,12 +69,12 @@ class AccountSettingsWindow(ba.Window):
|
||||
)
|
||||
|
||||
# Currently we can only reset achievements on game-center.
|
||||
account_type: str | None
|
||||
if self._signed_in:
|
||||
account_type = ba.internal.get_v1_account_type()
|
||||
v1_account_type: str | None
|
||||
if self._v1_signed_in:
|
||||
v1_account_type = ba.internal.get_v1_account_type()
|
||||
else:
|
||||
account_type = None
|
||||
self._can_reset_achievements = account_type == 'Game Center'
|
||||
v1_account_type = None
|
||||
self._can_reset_achievements = v1_account_type == 'Game Center'
|
||||
|
||||
app = ba.app
|
||||
uiscale = app.ui.uiscale
|
||||
@ -104,7 +107,7 @@ class AccountSettingsWindow(ba.Window):
|
||||
|
||||
# Legacy v1 device accounts are currently always available
|
||||
# (though we need to start phasing them out at some point).
|
||||
self._show_sign_in_buttons.append('Local')
|
||||
self._show_sign_in_buttons.append('Device')
|
||||
|
||||
top_extra = 15 if uiscale is ba.UIScale.SMALL else 0
|
||||
super().__init__(
|
||||
@ -183,31 +186,25 @@ class AccountSettingsWindow(ba.Window):
|
||||
def _update(self) -> None:
|
||||
|
||||
# If they want us to close once we're signed in, do so.
|
||||
if self._close_once_signed_in and self._signed_in:
|
||||
if self._close_once_signed_in and self._v1_signed_in:
|
||||
self._back()
|
||||
return
|
||||
|
||||
# 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.internal.get_v1_account_state_num()
|
||||
account_state = ba.internal.get_v1_account_state()
|
||||
|
||||
show_linked = (
|
||||
self._signed_in
|
||||
and ba.internal.get_v1_account_misc_read_val(
|
||||
'allowAccountLinking2', False
|
||||
)
|
||||
)
|
||||
v1_account_state_num = ba.internal.get_v1_account_state_num()
|
||||
v1_account_state = ba.internal.get_v1_account_state()
|
||||
show_legacy_unlink_button = self._should_show_legacy_unlink_button()
|
||||
|
||||
if (
|
||||
account_state_num != self._account_state_num
|
||||
or self._show_linked != show_linked
|
||||
v1_account_state_num != self._v1_account_state_num
|
||||
or show_legacy_unlink_button != self._show_legacy_unlink_button
|
||||
or self._needs_refresh
|
||||
):
|
||||
self._show_linked = show_linked
|
||||
self._account_state_num = account_state_num
|
||||
self._signed_in = account_state == 'signed_in'
|
||||
self._v1_account_state_num = v1_account_state_num
|
||||
self._v1_signed_in = v1_account_state == 'signed_in'
|
||||
self._show_legacy_unlink_button = show_legacy_unlink_button
|
||||
self._refresh()
|
||||
|
||||
# Go ahead and refresh some individual things
|
||||
@ -229,22 +226,27 @@ class AccountSettingsWindow(ba.Window):
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui import confirm
|
||||
|
||||
primary_v2_account = ba.app.accounts_v2.primary
|
||||
|
||||
v1_state = ba.internal.get_v1_account_state()
|
||||
account_type = (
|
||||
v1_account_type = (
|
||||
ba.internal.get_v1_account_type()
|
||||
if v1_state == 'signed_in'
|
||||
else 'unknown'
|
||||
)
|
||||
|
||||
is_google = account_type == 'Google Play'
|
||||
# We expose GPGS-specific functionality only if it is 'active'
|
||||
# (meaning the current GPGS player matches one of our account's
|
||||
# logins).
|
||||
gpgs_adapter = ba.app.accounts_v2.login_adapters.get(LoginType.GPGS)
|
||||
is_gpgs = (
|
||||
False if gpgs_adapter is None else gpgs_adapter.is_back_end_active()
|
||||
)
|
||||
|
||||
show_local_signed_in_as = False
|
||||
local_signed_in_as_space = 50.0
|
||||
|
||||
show_signed_in_as = self._signed_in
|
||||
show_signed_in_as = self._v1_signed_in
|
||||
signed_in_as_space = 95.0
|
||||
|
||||
show_sign_in_benefits = not self._signed_in
|
||||
show_sign_in_benefits = not self._v1_signed_in
|
||||
sign_in_benefits_space = 80.0
|
||||
|
||||
show_signing_in_text = (
|
||||
@ -257,32 +259,30 @@ class AccountSettingsWindow(ba.Window):
|
||||
and self._signing_in_adapter is None
|
||||
and 'Google Play' in self._show_sign_in_buttons
|
||||
)
|
||||
show_device_sign_in_button = (
|
||||
v1_state == 'signed_out'
|
||||
and self._signing_in_adapter is None
|
||||
and 'Local' in self._show_sign_in_buttons
|
||||
)
|
||||
show_v2_proxy_sign_in_button = (
|
||||
v1_state == 'signed_out'
|
||||
and self._signing_in_adapter is None
|
||||
and 'V2Proxy' in self._show_sign_in_buttons
|
||||
)
|
||||
show_device_sign_in_button = (
|
||||
v1_state == 'signed_out'
|
||||
and self._signing_in_adapter is None
|
||||
and 'Device' in self._show_sign_in_buttons
|
||||
)
|
||||
sign_in_button_space = 70.0
|
||||
deprecated_space = 60
|
||||
|
||||
show_game_service_button = self._signed_in and account_type in [
|
||||
show_game_service_button = self._v1_signed_in and v1_account_type in [
|
||||
'Game Center'
|
||||
]
|
||||
game_service_button_space = 60.0
|
||||
|
||||
show_linked_accounts_text = (
|
||||
self._signed_in
|
||||
and ba.internal.get_v1_account_misc_read_val(
|
||||
'allowAccountLinking2', False
|
||||
)
|
||||
)
|
||||
show_what_is_v2 = self._v1_signed_in and v1_account_type == 'V2'
|
||||
|
||||
show_linked_accounts_text = self._v1_signed_in
|
||||
linked_accounts_text_space = 60.0
|
||||
|
||||
show_achievements_button = self._signed_in and account_type in (
|
||||
show_achievements_button = self._v1_signed_in and v1_account_type in (
|
||||
'Google Play',
|
||||
'Local',
|
||||
'V2',
|
||||
@ -290,42 +290,46 @@ class AccountSettingsWindow(ba.Window):
|
||||
achievements_button_space = 60.0
|
||||
|
||||
show_achievements_text = (
|
||||
self._signed_in and not show_achievements_button
|
||||
self._v1_signed_in and not show_achievements_button
|
||||
)
|
||||
achievements_text_space = 27.0
|
||||
|
||||
show_leaderboards_button = self._signed_in and is_google
|
||||
show_leaderboards_button = self._v1_signed_in and is_gpgs
|
||||
leaderboards_button_space = 60.0
|
||||
|
||||
show_campaign_progress = self._signed_in
|
||||
show_campaign_progress = self._v1_signed_in
|
||||
campaign_progress_space = 27.0
|
||||
|
||||
show_tickets = self._signed_in
|
||||
show_tickets = self._v1_signed_in
|
||||
tickets_space = 27.0
|
||||
|
||||
show_reset_progress_button = False
|
||||
reset_progress_button_space = 70.0
|
||||
|
||||
show_manage_v2_account_button = self._signed_in and account_type == 'V2'
|
||||
show_manage_v2_account_button = (
|
||||
self._v1_signed_in and v1_account_type == 'V2'
|
||||
)
|
||||
manage_v2_account_button_space = 100.0
|
||||
|
||||
show_player_profiles_button = self._signed_in
|
||||
show_player_profiles_button = self._v1_signed_in
|
||||
player_profiles_button_space = (
|
||||
70.0 if show_manage_v2_account_button else 100.0
|
||||
)
|
||||
|
||||
show_link_accounts_button = (
|
||||
self._signed_in
|
||||
and ba.internal.get_v1_account_misc_read_val(
|
||||
'allowAccountLinking2', False
|
||||
)
|
||||
show_link_accounts_button = self._v1_signed_in and (
|
||||
primary_v2_account is None or FORCE_ENABLE_V1_LINKING
|
||||
)
|
||||
link_accounts_button_space = 70.0
|
||||
|
||||
show_unlink_accounts_button = show_link_accounts_button
|
||||
unlink_accounts_button_space = 90.0
|
||||
|
||||
show_sign_out_button = self._signed_in and account_type in [
|
||||
show_v2_link_info = self._v1_signed_in and not show_link_accounts_button
|
||||
v2_link_info_space = 70.0
|
||||
|
||||
legacy_unlink_button_space = 120.0
|
||||
|
||||
show_sign_out_button = self._v1_signed_in and v1_account_type in [
|
||||
'Local',
|
||||
'Google Play',
|
||||
'V2',
|
||||
@ -337,25 +341,23 @@ class AccountSettingsWindow(ba.Window):
|
||||
# to be verified.
|
||||
show_cancel_sign_in_button = self._signing_in_adapter is not None or (
|
||||
ba.app.accounts_v2.have_primary_credentials()
|
||||
and ba.app.accounts_v2.primary is None
|
||||
and primary_v2_account is None
|
||||
)
|
||||
cancel_sign_in_button_space = 70.0
|
||||
|
||||
if self._subcontainer is not None:
|
||||
self._subcontainer.delete()
|
||||
self._sub_height = 60.0
|
||||
if show_local_signed_in_as:
|
||||
self._sub_height += local_signed_in_as_space
|
||||
if show_signed_in_as:
|
||||
self._sub_height += signed_in_as_space
|
||||
if show_signing_in_text:
|
||||
self._sub_height += signing_in_text_space
|
||||
if show_google_play_sign_in_button:
|
||||
self._sub_height += sign_in_button_space
|
||||
if show_device_sign_in_button:
|
||||
self._sub_height += sign_in_button_space
|
||||
if show_v2_proxy_sign_in_button:
|
||||
self._sub_height += sign_in_button_space
|
||||
if show_device_sign_in_button:
|
||||
self._sub_height += sign_in_button_space + deprecated_space
|
||||
if show_game_service_button:
|
||||
self._sub_height += game_service_button_space
|
||||
if show_linked_accounts_text:
|
||||
@ -382,6 +384,10 @@ class AccountSettingsWindow(ba.Window):
|
||||
self._sub_height += link_accounts_button_space
|
||||
if show_unlink_accounts_button:
|
||||
self._sub_height += unlink_accounts_button_space
|
||||
if show_v2_link_info:
|
||||
self._sub_height += v2_link_info_space
|
||||
if self._show_legacy_unlink_button:
|
||||
self._sub_height += legacy_unlink_button_space
|
||||
if show_sign_out_button:
|
||||
self._sub_height += sign_out_button_space
|
||||
if show_cancel_sign_in_button:
|
||||
@ -398,27 +404,8 @@ class AccountSettingsWindow(ba.Window):
|
||||
first_selectable = None
|
||||
v = self._sub_height - 10.0
|
||||
|
||||
if show_local_signed_in_as:
|
||||
v -= local_signed_in_as_space * 0.6
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(self._sub_width * 0.5, v),
|
||||
size=(0, 0),
|
||||
text=ba.Lstr(
|
||||
resource='accountSettingsWindow.deviceSpecificAccountText',
|
||||
subs=[
|
||||
('${NAME}', ba.internal.get_v1_account_display_string())
|
||||
],
|
||||
),
|
||||
scale=0.7,
|
||||
color=(0.5, 0.5, 0.6),
|
||||
maxwidth=self._sub_width * 0.9,
|
||||
flatness=1.0,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
v -= local_signed_in_as_space * 0.4
|
||||
|
||||
self._account_name_what_is_text: ba.Widget | None
|
||||
self._account_name_what_is_y = 0.0
|
||||
self._account_name_text: ba.Widget | None
|
||||
if show_signed_in_as:
|
||||
v -= signed_in_as_space * 0.2
|
||||
@ -437,7 +424,7 @@ class AccountSettingsWindow(ba.Window):
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
v -= signed_in_as_space * 0.4
|
||||
v -= signed_in_as_space * 0.5
|
||||
self._account_name_text = ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(self._sub_width * 0.5, v),
|
||||
@ -449,10 +436,39 @@ class AccountSettingsWindow(ba.Window):
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
|
||||
if show_what_is_v2:
|
||||
self._account_name_what_is_y = v - 23.0
|
||||
self._account_name_what_is_text = ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(0.0, self._account_name_what_is_y),
|
||||
size=(200.0, 60),
|
||||
text=ba.Lstr(
|
||||
value='${WHAT} -->',
|
||||
subs=[('${WHAT}', ba.Lstr(resource='whatIsThisText'))],
|
||||
),
|
||||
scale=0.6,
|
||||
color=(0.3, 0.7, 0.05),
|
||||
maxwidth=200.0,
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
autoselect=True,
|
||||
selectable=True,
|
||||
on_activate_call=show_what_is_v2_page,
|
||||
click_activate=True,
|
||||
)
|
||||
if first_selectable is None:
|
||||
first_selectable = self._account_name_what_is_text
|
||||
else:
|
||||
self._account_name_what_is_text = None
|
||||
|
||||
self._refresh_account_name_text()
|
||||
|
||||
v -= signed_in_as_space * 0.4
|
||||
|
||||
else:
|
||||
self._account_name_text = None
|
||||
self._account_name_what_is_text = None
|
||||
|
||||
if self._back_button is None:
|
||||
bbtn = ba.internal.get_special_widget('back_button')
|
||||
@ -606,7 +622,7 @@ class AccountSettingsWindow(ba.Window):
|
||||
|
||||
if show_device_sign_in_button:
|
||||
button_width = 350
|
||||
v -= sign_in_button_space
|
||||
v -= sign_in_button_space + deprecated_space
|
||||
self._sign_in_device_button = btn = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
position=((self._sub_width - button_width) * 0.5, v - 20),
|
||||
@ -615,6 +631,18 @@ class AccountSettingsWindow(ba.Window):
|
||||
label='',
|
||||
on_activate_call=lambda: self._sign_in_press('Local'),
|
||||
)
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
size=(0, 0),
|
||||
position=(self._sub_width * 0.5, v + 60),
|
||||
text=ba.Lstr(resource='deprecatedText'),
|
||||
scale=0.8,
|
||||
maxwidth=300,
|
||||
color=(0.6, 0.55, 0.45),
|
||||
)
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
draw_controller=btn,
|
||||
@ -709,12 +737,12 @@ class AccountSettingsWindow(ba.Window):
|
||||
if show_game_service_button:
|
||||
button_width = 300
|
||||
v -= game_service_button_space * 0.85
|
||||
account_type = ba.internal.get_v1_account_type()
|
||||
if account_type == 'Game Center':
|
||||
account_type_name = ba.Lstr(resource='gameCenterText')
|
||||
v1_account_type = ba.internal.get_v1_account_type()
|
||||
if v1_account_type == 'Game Center':
|
||||
v1_account_type_name = ba.Lstr(resource='gameCenterText')
|
||||
else:
|
||||
raise ValueError(
|
||||
"unknown account type: '" + str(account_type) + "'"
|
||||
"unknown account type: '" + str(v1_account_type) + "'"
|
||||
)
|
||||
self._game_service_button = btn = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
@ -724,7 +752,7 @@ class AccountSettingsWindow(ba.Window):
|
||||
autoselect=True,
|
||||
on_activate_call=ba.internal.show_online_score_ui,
|
||||
size=(button_width, 50),
|
||||
label=account_type_name,
|
||||
label=v1_account_type_name,
|
||||
)
|
||||
if first_selectable is None:
|
||||
first_selectable = btn
|
||||
@ -767,11 +795,15 @@ class AccountSettingsWindow(ba.Window):
|
||||
autoselect=True,
|
||||
icon=ba.gettexture(
|
||||
'googlePlayAchievementsIcon'
|
||||
if is_google
|
||||
if is_gpgs
|
||||
else 'achievementsIcon'
|
||||
),
|
||||
icon_color=(0.8, 0.95, 0.7) if is_google else (0.85, 0.8, 0.9),
|
||||
on_activate_call=self._on_achievements_press,
|
||||
icon_color=(0.8, 0.95, 0.7) if is_gpgs else (0.85, 0.8, 0.9),
|
||||
on_activate_call=(
|
||||
self._on_custom_achievements_press
|
||||
if is_gpgs
|
||||
else self._on_achievements_press
|
||||
),
|
||||
size=(button_width, 50),
|
||||
label='',
|
||||
)
|
||||
@ -903,6 +935,7 @@ class AccountSettingsWindow(ba.Window):
|
||||
scale=0.9,
|
||||
color=(0.75, 0.7, 0.8),
|
||||
maxwidth=self._sub_width * 0.95,
|
||||
text=ba.Lstr(resource=self._r + '.linkedAccountsText'),
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
@ -911,6 +944,8 @@ class AccountSettingsWindow(ba.Window):
|
||||
else:
|
||||
self._linked_accounts_text = None
|
||||
|
||||
# Show link/unlink buttons only for V1 accounts.
|
||||
|
||||
if show_link_accounts_button:
|
||||
v -= link_accounts_button_space
|
||||
self._link_accounts_button = btn = ba.buttonwidget(
|
||||
@ -990,6 +1025,50 @@ class AccountSettingsWindow(ba.Window):
|
||||
else:
|
||||
self._unlink_accounts_button = None
|
||||
|
||||
if show_v2_link_info:
|
||||
v -= v2_link_info_space
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
size=(0, 0),
|
||||
position=(self._sub_width * 0.5, v + v2_link_info_space - 20),
|
||||
text=ba.Lstr(resource='v2AccountLinkingInfoText'),
|
||||
flatness=1.0,
|
||||
scale=0.8,
|
||||
maxwidth=450,
|
||||
color=(0.5, 0.45, 0.55),
|
||||
)
|
||||
|
||||
if self._show_legacy_unlink_button:
|
||||
v -= legacy_unlink_button_space
|
||||
button_width_w = button_width * 1.5
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(self._sub_width * 0.5 - 150.0, v + 75),
|
||||
size=(300.0, 60),
|
||||
text=ba.Lstr(resource='whatIsThisText'),
|
||||
scale=0.8,
|
||||
color=(0.3, 0.7, 0.05),
|
||||
maxwidth=200.0,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
autoselect=True,
|
||||
selectable=True,
|
||||
on_activate_call=show_what_is_legacy_unlinking_page,
|
||||
click_activate=True,
|
||||
)
|
||||
btn = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
position=((self._sub_width - button_width_w) * 0.5, v + 25),
|
||||
autoselect=True,
|
||||
size=(button_width_w, 60),
|
||||
label=ba.Lstr(resource=self._r + '.unlinkLegacyV1AccountsText'),
|
||||
textcolor=(0.8, 0.4, 0),
|
||||
color=(0.55, 0.5, 0.6),
|
||||
on_activate_call=self._unlink_accounts_press,
|
||||
)
|
||||
|
||||
if show_sign_out_button:
|
||||
v -= sign_out_button_space
|
||||
self._sign_out_button = btn = ba.buttonwidget(
|
||||
@ -1044,33 +1123,24 @@ class AccountSettingsWindow(ba.Window):
|
||||
)
|
||||
self._needs_refresh = False
|
||||
|
||||
def _on_custom_achievements_press(self) -> None:
|
||||
ba.timer(
|
||||
0.15,
|
||||
ba.Call(ba.internal.show_online_score_ui, 'achievements'),
|
||||
timetype=ba.TimeType.REAL,
|
||||
)
|
||||
|
||||
def _on_achievements_press(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui import achievements
|
||||
|
||||
account_state = ba.internal.get_v1_account_state()
|
||||
account_type = (
|
||||
ba.internal.get_v1_account_type()
|
||||
if account_state == 'signed_in'
|
||||
else 'unknown'
|
||||
assert self._achievements_button is not None
|
||||
achievements.AchievementsWindow(
|
||||
position=self._achievements_button.get_screen_space_center()
|
||||
)
|
||||
# for google play we use the built-in UI; otherwise pop up our own
|
||||
if account_type == 'Google Play':
|
||||
ba.timer(
|
||||
0.15,
|
||||
ba.Call(ba.internal.show_online_score_ui, 'achievements'),
|
||||
timetype=ba.TimeType.REAL,
|
||||
)
|
||||
elif account_type != 'unknown':
|
||||
assert self._achievements_button is not None
|
||||
achievements.AchievementsWindow(
|
||||
position=self._achievements_button.get_screen_space_center()
|
||||
)
|
||||
else:
|
||||
print(
|
||||
'ERROR: unknown account type in on_achievements_press:',
|
||||
account_type,
|
||||
)
|
||||
|
||||
def _on_what_is_v2_press(self) -> None:
|
||||
show_what_is_v2_page()
|
||||
|
||||
def _on_manage_account_press(self) -> None:
|
||||
ba.screenmessage(ba.Lstr(resource='oneMomentText'))
|
||||
@ -1106,7 +1176,7 @@ class AccountSettingsWindow(ba.Window):
|
||||
timetype=ba.TimeType.REAL,
|
||||
)
|
||||
|
||||
def _have_unlinkable_accounts(self) -> bool:
|
||||
def _have_unlinkable_v1_accounts(self) -> bool:
|
||||
# if this is not present, we haven't had contact from the server so
|
||||
# let's not proceed..
|
||||
if ba.internal.get_public_login_id() is None:
|
||||
@ -1119,16 +1189,33 @@ class AccountSettingsWindow(ba.Window):
|
||||
def _update_unlink_accounts_button(self) -> None:
|
||||
if self._unlink_accounts_button is None:
|
||||
return
|
||||
if self._have_unlinkable_accounts():
|
||||
if self._have_unlinkable_v1_accounts():
|
||||
clr = (0.75, 0.7, 0.8, 1.0)
|
||||
else:
|
||||
clr = (1.0, 1.0, 1.0, 0.25)
|
||||
ba.textwidget(edit=self._unlink_accounts_button_label, color=clr)
|
||||
|
||||
def _should_show_legacy_unlink_button(self) -> bool:
|
||||
|
||||
# Only show this when fully signed in to a v2 account.
|
||||
if not self._v1_signed_in or ba.app.accounts_v2.primary is None:
|
||||
return False
|
||||
|
||||
out = self._have_unlinkable_v1_accounts()
|
||||
return out
|
||||
|
||||
def _update_linked_accounts_text(self) -> None:
|
||||
if self._linked_accounts_text is None:
|
||||
return
|
||||
|
||||
# Disable this by default when signed in to a V2 account
|
||||
# (since this shows V1 links which we should no longer care about).
|
||||
if (
|
||||
ba.app.accounts_v2.primary is not None
|
||||
and not FORCE_ENABLE_V1_LINKING
|
||||
):
|
||||
return
|
||||
|
||||
# if this is not present, we haven't had contact from the server so
|
||||
# let's not proceed..
|
||||
if ba.internal.get_public_login_id() is None:
|
||||
@ -1138,13 +1225,9 @@ class AccountSettingsWindow(ba.Window):
|
||||
accounts = ba.internal.get_v1_account_misc_read_val_2(
|
||||
'linkedAccounts', []
|
||||
)
|
||||
# our_account = _bs.get_v1_account_display_string()
|
||||
# accounts = [a for a in accounts if a != our_account]
|
||||
# accounts_str = u', '.join(accounts) if accounts else
|
||||
# ba.Lstr(translate=('settingNames', 'None'))
|
||||
# UPDATE - we now just print the number here; not the actual
|
||||
# accounts
|
||||
# (they can see that in the unlink section if they're curious)
|
||||
# accounts (they can see that in the unlink section if they're
|
||||
# curious)
|
||||
accounts_str = str(max(0, len(accounts) - 1))
|
||||
ba.textwidget(
|
||||
edit=self._linked_accounts_text,
|
||||
@ -1195,6 +1278,7 @@ class AccountSettingsWindow(ba.Window):
|
||||
)
|
||||
|
||||
def _refresh_account_name_text(self) -> None:
|
||||
|
||||
if self._account_name_text is None:
|
||||
return
|
||||
try:
|
||||
@ -1202,7 +1286,20 @@ class AccountSettingsWindow(ba.Window):
|
||||
except Exception:
|
||||
ba.print_exception()
|
||||
name_str = '??'
|
||||
|
||||
ba.textwidget(edit=self._account_name_text, text=name_str)
|
||||
if self._account_name_what_is_text is not None:
|
||||
swidth = ba.internal.get_string_width(
|
||||
name_str, suppress_warning=True
|
||||
)
|
||||
# Eww; number-fudging. Need to recalibrate this if
|
||||
# account name scaling changes.
|
||||
x = self._sub_width * 0.5 - swidth * 0.75 - 170
|
||||
|
||||
ba.textwidget(
|
||||
edit=self._account_name_what_is_text,
|
||||
position=(x, self._account_name_what_is_y),
|
||||
)
|
||||
|
||||
def _refresh_achievements(self) -> None:
|
||||
if (
|
||||
@ -1232,7 +1329,7 @@ class AccountSettingsWindow(ba.Window):
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.account import unlink
|
||||
|
||||
if not self._have_unlinkable_accounts():
|
||||
if not self._have_unlinkable_v1_accounts():
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
unlink.AccountUnlinkWindow(origin_widget=self._unlink_accounts_button)
|
||||
@ -1263,6 +1360,11 @@ class AccountSettingsWindow(ba.Window):
|
||||
def _sign_out_press(self) -> None:
|
||||
|
||||
if ba.app.accounts_v2.have_primary_credentials():
|
||||
if (
|
||||
ba.app.accounts_v2.primary is not None
|
||||
and LoginType.GPGS in ba.app.accounts_v2.primary.logins
|
||||
):
|
||||
self._explicitly_signed_out_of_gpgs = True
|
||||
ba.app.accounts_v2.set_primary_credentials(None)
|
||||
else:
|
||||
ba.internal.sign_out_v1()
|
||||
@ -1301,7 +1403,8 @@ class AccountSettingsWindow(ba.Window):
|
||||
if adapter is not None:
|
||||
self._signing_in_adapter = adapter
|
||||
adapter.sign_in(
|
||||
result_cb=ba.WeakCall(self._on_adapter_sign_in_result)
|
||||
result_cb=ba.WeakCall(self._on_adapter_sign_in_result),
|
||||
description='account settings button',
|
||||
)
|
||||
# Will get 'Signing in...' to show.
|
||||
self._needs_refresh = True
|
||||
@ -1334,6 +1437,27 @@ class AccountSettingsWindow(ba.Window):
|
||||
# when finished.
|
||||
ba.app.accounts_v2.set_primary_credentials(result.credentials)
|
||||
|
||||
# Special case - if the user has explicitly logged out and
|
||||
# logged in again with GPGS via this button, warn them that
|
||||
# they need to use the app if they want to switch to a
|
||||
# different GPGS account.
|
||||
if (
|
||||
self._explicitly_signed_out_of_gpgs
|
||||
and adapter.login_type is LoginType.GPGS
|
||||
):
|
||||
# Delay this slightly so it hopefully pops up after
|
||||
# credentials go through and the account name shows up.
|
||||
ba.timer(
|
||||
1.5,
|
||||
ba.Call(
|
||||
ba.screenmessage,
|
||||
ba.Lstr(
|
||||
resource=self._r
|
||||
+ '.googlePlayGamesAccountSwitchText'
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Speed any UI updates along.
|
||||
self._needs_refresh = True
|
||||
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
|
||||
@ -1402,3 +1526,15 @@ class AccountSettingsWindow(ba.Window):
|
||||
ba.containerwidget(edit=self._root_widget, selected_child=sel)
|
||||
except Exception:
|
||||
ba.print_exception(f'Error restoring state for {self}.')
|
||||
|
||||
|
||||
def show_what_is_v2_page() -> None:
|
||||
"""Show the webpage describing V2 accounts."""
|
||||
bamasteraddr = ba.internal.get_master_server_address(version=2)
|
||||
ba.open_url(f'{bamasteraddr}/whatisv2')
|
||||
|
||||
|
||||
def show_what_is_legacy_unlinking_page() -> None:
|
||||
"""Show the webpage describing legacy unlinking."""
|
||||
bamasteraddr = ba.internal.get_master_server_address(version=2)
|
||||
ba.open_url(f'{bamasteraddr}/whatarev1links')
|
||||
|
||||
@ -67,11 +67,14 @@ class V2ProxySignInWindow(ba.Window):
|
||||
label=ba.Lstr(resource='cancelText'),
|
||||
on_activate_call=self._done,
|
||||
autoselect=True,
|
||||
color=(0.55, 0.5, 0.6),
|
||||
textcolor=(0.75, 0.7, 0.8),
|
||||
)
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, cancel_button=self._cancel_button
|
||||
)
|
||||
|
||||
if bool(False):
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, cancel_button=self._cancel_button
|
||||
)
|
||||
|
||||
self._update_timer: ba.Timer | None = None
|
||||
|
||||
|
||||
@ -20,12 +20,12 @@ class ConfigErrorWindow(ba.Window):
|
||||
self._config_file_path = ba.app.config_file_path
|
||||
width = 800
|
||||
super().__init__(
|
||||
ba.containerwidget(size=(width, 300), transition='in_right')
|
||||
ba.containerwidget(size=(width, 400), transition='in_right')
|
||||
)
|
||||
padding = 20
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(padding, 220),
|
||||
position=(padding, 220 + 60),
|
||||
size=(width - 2 * padding, 100 - 2 * padding),
|
||||
h_align='center',
|
||||
v_align='top',
|
||||
@ -41,7 +41,7 @@ class ConfigErrorWindow(ba.Window):
|
||||
)
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(padding, 198),
|
||||
position=(padding, 198 + 60),
|
||||
size=(width - 2 * padding, 100 - 2 * padding),
|
||||
h_align='center',
|
||||
v_align='top',
|
||||
|
||||
@ -1094,7 +1094,6 @@ class PublicGatherTab(GatherTab):
|
||||
|
||||
self._parties_sorted.sort(
|
||||
key=lambda p: (
|
||||
p[1].queue is None, # Show non-queued last.
|
||||
p[1].ping if p[1].ping is not None else 999999.0,
|
||||
p[1].index,
|
||||
)
|
||||
|
||||
@ -515,7 +515,20 @@ class MainMenuWindow(ba.Window):
|
||||
self._tdelay = 2.0
|
||||
self._t_delay_inc = 0.02
|
||||
self._t_delay_play = 1.7
|
||||
self._next_refresh_allow_time = ba.time(ba.TimeType.REAL) + 2.01
|
||||
|
||||
def _set_allow_time() -> None:
|
||||
self._next_refresh_allow_time = ba.time(ba.TimeType.REAL) + 2.5
|
||||
|
||||
# Slight hack: widget transitions currently only progress when
|
||||
# frames are being drawn, but this tends to get called before
|
||||
# frame drawing even starts, meaning we don't know exactly how
|
||||
# long we should wait before refreshing to avoid interrupting
|
||||
# the transition. To make things a bit better, let's do a
|
||||
# redundant set of the time in a deferred call which hopefully
|
||||
# happens closer to actual frame draw times.
|
||||
_set_allow_time()
|
||||
ba.pushcall(_set_allow_time)
|
||||
|
||||
ba.app.did_menu_intro = True
|
||||
self._width = 400.0
|
||||
self._height = 200.0
|
||||
|
||||
@ -473,7 +473,6 @@ class PlaylistEditGameWindow(ba.Window):
|
||||
|
||||
# Ok now wire up the column.
|
||||
try:
|
||||
# pylint: disable=unsubscriptable-object
|
||||
prev_widgets: list[ba.Widget] | None = None
|
||||
for cwdg in widget_column:
|
||||
if prev_widgets is not None:
|
||||
|
||||
@ -4,18 +4,25 @@
|
||||
# pylint: disable=too-many-lines
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import copy
|
||||
import math
|
||||
import logging
|
||||
import weakref
|
||||
from enum import Enum
|
||||
from threading import Thread
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.error import CommunicationError
|
||||
import bacommon.cloud
|
||||
import ba
|
||||
import ba.internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable, Sequence
|
||||
|
||||
MERCH_LINK_KEY = 'Merch Link'
|
||||
|
||||
|
||||
class StoreBrowserWindow(ba.Window):
|
||||
"""Window for browsing the store."""
|
||||
@ -593,8 +600,14 @@ class StoreBrowserWindow(ba.Window):
|
||||
else:
|
||||
self._last_buy_time = curtime
|
||||
|
||||
# Pro is an actual IAP; the rest are ticket purchases.
|
||||
if item == 'pro':
|
||||
# Merch is a special case - just a link.
|
||||
if item == 'merch':
|
||||
url = ba.app.config.get('Merch Link')
|
||||
if isinstance(url, str):
|
||||
ba.open_url(url)
|
||||
|
||||
# Pro is an actual IAP, and the rest are ticket purchases.
|
||||
elif item == 'pro':
|
||||
ba.playsound(ba.getsound('click01'))
|
||||
|
||||
# Purchase either pro or pro_sale depending on whether
|
||||
@ -681,7 +694,9 @@ class StoreBrowserWindow(ba.Window):
|
||||
assert self.button_infos is not None
|
||||
for b_type, b_info in self.button_infos.items():
|
||||
|
||||
if b_type in ['upgrades.pro', 'pro']:
|
||||
if b_type == 'merch':
|
||||
purchased = False
|
||||
elif b_type in ['upgrades.pro', 'pro']:
|
||||
purchased = ba.app.accounts_v1.have_pro()
|
||||
else:
|
||||
purchased = ba.internal.get_purchased(b_type)
|
||||
@ -707,7 +722,11 @@ class StoreBrowserWindow(ba.Window):
|
||||
color = (0.4, 0.8, 0.1)
|
||||
extra_image_opacity = 1.0
|
||||
call = b_info['call'] if 'call' in b_info else None
|
||||
if b_type in ['upgrades.pro', 'pro']:
|
||||
if b_type == 'merch':
|
||||
price_text = ''
|
||||
price_text_left = ''
|
||||
price_text_right = ''
|
||||
elif b_type in ['upgrades.pro', 'pro']:
|
||||
sale_time = get_available_sale_time('extras')
|
||||
if sale_time is not None:
|
||||
priceraw = ba.internal.get_price('pro')
|
||||
@ -888,7 +907,14 @@ class StoreBrowserWindow(ba.Window):
|
||||
dummy_name
|
||||
)
|
||||
section['v_spacing'] = (
|
||||
-17 if self._tab == 'characters' else 0
|
||||
-25
|
||||
if (
|
||||
self._tab == 'extras'
|
||||
and uiscale is ba.UIScale.SMALL
|
||||
)
|
||||
else -17
|
||||
if self._tab == 'characters'
|
||||
else 0
|
||||
)
|
||||
if 'title' not in section:
|
||||
section['title'] = ''
|
||||
@ -900,7 +926,13 @@ class StoreBrowserWindow(ba.Window):
|
||||
else 0
|
||||
)
|
||||
section['y_offs'] = (
|
||||
55
|
||||
20
|
||||
if (
|
||||
self._tab == 'extras'
|
||||
and uiscale is ba.UIScale.SMALL
|
||||
and ba.app.config.get('Merch Link')
|
||||
)
|
||||
else 55
|
||||
if (
|
||||
self._tab == 'extras'
|
||||
and uiscale is ba.UIScale.SMALL
|
||||
@ -917,7 +949,9 @@ class StoreBrowserWindow(ba.Window):
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
from bastd.ui.store import item as storeitemui
|
||||
from bastd.ui.store.item import (
|
||||
instantiate_store_item_display,
|
||||
)
|
||||
|
||||
title_spacing = 40
|
||||
button_border = 20
|
||||
@ -1102,7 +1136,7 @@ class StoreBrowserWindow(ba.Window):
|
||||
+ (b_width + button_spacing) * col,
|
||||
v - b_height + boffs_v2,
|
||||
)
|
||||
storeitemui.instantiate_store_item_display(
|
||||
instantiate_store_item_display(
|
||||
item_name,
|
||||
item,
|
||||
parent_widget=cnt2,
|
||||
@ -1121,7 +1155,6 @@ class StoreBrowserWindow(ba.Window):
|
||||
# Wire this button to the equivalent in the
|
||||
# previous row.
|
||||
if prev_row_buttons is not None:
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if len(prev_row_buttons) > col:
|
||||
ba.widget(
|
||||
edit=btn,
|
||||
@ -1325,3 +1358,41 @@ class StoreBrowserWindow(ba.Window):
|
||||
)
|
||||
if self._on_close_call is not None:
|
||||
self._on_close_call()
|
||||
|
||||
|
||||
def _check_merch_availability_in_bg_thread() -> None:
|
||||
# pylint: disable=cell-var-from-loop
|
||||
|
||||
# Merch is available from some countries only.
|
||||
# Make a reasonable check to ask the master-server about this at
|
||||
# launch and store the results.
|
||||
for _i in range(15):
|
||||
try:
|
||||
if ba.app.cloud.is_connected():
|
||||
response = ba.app.cloud.send_message(
|
||||
bacommon.cloud.MerchAvailabilityMessage()
|
||||
)
|
||||
|
||||
def _store_in_logic_thread() -> None:
|
||||
cfg = ba.app.config
|
||||
current: str | None = cfg.get(MERCH_LINK_KEY)
|
||||
if not isinstance(current, str | None):
|
||||
current = None
|
||||
if current != response.url:
|
||||
cfg[MERCH_LINK_KEY] = response.url
|
||||
cfg.commit()
|
||||
|
||||
# If we successfully get a response, kick it over to the
|
||||
# logic thread to store and we're done.
|
||||
ba.pushcall(_store_in_logic_thread, from_other_thread=True)
|
||||
return
|
||||
except CommunicationError:
|
||||
pass
|
||||
except Exception:
|
||||
logging.warning(
|
||||
'Unexpected error in merch-availability-check.', exc_info=True
|
||||
)
|
||||
time.sleep(1.1934) # A bit randomized to avoid aliasing.
|
||||
|
||||
|
||||
Thread(target=_check_merch_availability_in_bg_thread, daemon=True).start()
|
||||
|
||||
@ -45,19 +45,23 @@ def instantiate_store_item_display(
|
||||
item['name'] = title = get_store_item_name_translated(item_name)
|
||||
|
||||
btn: ba.Widget | None
|
||||
|
||||
# Hack; showbuffer stuff isn't working well when we're showing merch.
|
||||
showbuffer = 10 if item_name in {'merch', 'pro', 'pro_sale'} else 76.0
|
||||
|
||||
if button:
|
||||
item['button'] = btn = ba.buttonwidget(
|
||||
parent=parent_widget,
|
||||
position=b_pos,
|
||||
transition_delay=delay,
|
||||
show_buffer_top=76.0,
|
||||
show_buffer_top=showbuffer,
|
||||
enable_sound=False,
|
||||
button_type='square',
|
||||
size=(b_width, b_height),
|
||||
autoselect=True,
|
||||
label='',
|
||||
)
|
||||
ba.widget(edit=btn, show_buffer_bottom=76.0)
|
||||
ba.widget(edit=btn, show_buffer_bottom=showbuffer)
|
||||
else:
|
||||
btn = None
|
||||
|
||||
@ -92,6 +96,10 @@ def instantiate_store_item_display(
|
||||
tint_tex = character.icon_mask_texture
|
||||
title_v = 0.255
|
||||
price_v = 0.145
|
||||
elif item_name == 'merch':
|
||||
base_text_scale = 0.6
|
||||
title_v = 0.85
|
||||
price_v = 0.15
|
||||
elif item_name in ['upgrades.pro', 'pro']:
|
||||
base_text_scale = 0.6
|
||||
title_v = 0.85
|
||||
@ -165,6 +173,23 @@ def instantiate_store_item_display(
|
||||
tint2_color=tint2_color,
|
||||
)
|
||||
|
||||
if item_name == 'merch':
|
||||
frame_size = b_width * 0.65
|
||||
im_dim = frame_size * (100.0 / 113.0)
|
||||
im_pos = (
|
||||
b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
|
||||
b_pos[1] + b_height * 0.47 - im_dim * 0.5,
|
||||
)
|
||||
ba.imagewidget(
|
||||
parent=parent_widget,
|
||||
position=im_pos,
|
||||
size=(im_dim, im_dim),
|
||||
transition_delay=delay,
|
||||
draw_controller=btn,
|
||||
opacity=1.0,
|
||||
texture=ba.gettexture('merch'),
|
||||
)
|
||||
|
||||
if item_name in ['pro', 'upgrades.pro']:
|
||||
frame_size = b_width * 0.5
|
||||
im_dim = frame_size * (100.0 / 113.0)
|
||||
@ -184,7 +209,6 @@ def instantiate_store_item_display(
|
||||
)
|
||||
txt = ba.Lstr(resource='store.bombSquadProNewDescriptionText')
|
||||
|
||||
# t = 'foo\nfoo\nfoo\nfoo\nfoo\nfoo'
|
||||
item['descriptionText'] = ba.textwidget(
|
||||
parent=parent_widget,
|
||||
text=txt,
|
||||
|
||||
117
assets/src/ba_data/python/bastd/ui/v2upgrade.py
Normal file
117
assets/src/ba_data/python/bastd/ui/v2upgrade.py
Normal file
@ -0,0 +1,117 @@
|
||||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""UI for upgrading V1 accounts to V2."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ba
|
||||
import ba.internal
|
||||
|
||||
|
||||
class V2UpgradeWindow(ba.Window):
|
||||
"""A window presenting a URL to the user visually."""
|
||||
|
||||
def __init__(self, login_name: str, code: str):
|
||||
from bastd.ui.account.settings import show_what_is_v2_page
|
||||
|
||||
app = ba.app
|
||||
uiscale = app.ui.uiscale
|
||||
|
||||
self._code = code
|
||||
|
||||
self._width = 700
|
||||
self._height = 270
|
||||
super().__init__(
|
||||
root_widget=ba.containerwidget(
|
||||
size=(self._width, self._height + 40),
|
||||
transition='in_right',
|
||||
scale=(
|
||||
1.25
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.25
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.25
|
||||
),
|
||||
)
|
||||
)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width * 0.5, self._height - 46),
|
||||
size=(0, 0),
|
||||
color=ba.app.ui.title_color,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
text=ba.Lstr(
|
||||
resource='deviceAccountUpgradeText',
|
||||
subs=[('${NAME}', login_name)],
|
||||
),
|
||||
maxwidth=self._width * 0.95,
|
||||
)
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width * 0.5, 125),
|
||||
size=(0, 0),
|
||||
scale=0.8,
|
||||
color=(0.7, 0.8, 0.7),
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
text=(
|
||||
ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT)
|
||||
+ login_name
|
||||
+ ' ----> '
|
||||
+ ba.charstr(ba.SpecialChar.V2_LOGO)
|
||||
+ login_name
|
||||
),
|
||||
maxwidth=self._width * 0.95,
|
||||
)
|
||||
button_width = 200
|
||||
|
||||
cancel_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(20, 25),
|
||||
size=(button_width, 65),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource='notNowText'),
|
||||
on_activate_call=self._done,
|
||||
)
|
||||
|
||||
_what_is_this_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width * 0.5 - button_width * 0.5, 25),
|
||||
size=(button_width, 65),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource='whatIsThisText'),
|
||||
color=(0.55, 0.5, 0.6),
|
||||
textcolor=(0.75, 0.7, 0.8),
|
||||
on_activate_call=show_what_is_v2_page,
|
||||
)
|
||||
|
||||
upgrade_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width - button_width - 20, 25),
|
||||
size=(button_width, 65),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource='upgradeText'),
|
||||
on_activate_call=self._upgrade_press,
|
||||
)
|
||||
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget,
|
||||
selected_child=upgrade_button,
|
||||
cancel_button=cancel_button,
|
||||
)
|
||||
|
||||
def _upgrade_press(self) -> None:
|
||||
# Get rid of the window and sign out before kicking the
|
||||
# user over to a browser to do the upgrade. This hopefully
|
||||
# makes it more clear when they come back that they need to
|
||||
# sign in with the 'BombSquad account' option.
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
ba.internal.sign_out_v1()
|
||||
bamasteraddr = ba.internal.get_master_server_address(version=2)
|
||||
ba.open_url(f'{bamasteraddr}/v2uda/{self._code}')
|
||||
|
||||
def _done(self) -> None:
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
22
ballisticacore-cmake/.idea/dictionaries/ericf.xml
generated
22
ballisticacore-cmake/.idea/dictionaries/ericf.xml
generated
@ -48,6 +48,7 @@
|
||||
<w>allobjs</w>
|
||||
<w>allocs</w>
|
||||
<w>allwarnings</w>
|
||||
<w>alogins</w>
|
||||
<w>alot</w>
|
||||
<w>alphaimg</w>
|
||||
<w>alphapixels</w>
|
||||
@ -71,6 +72,7 @@
|
||||
<w>appconfig</w>
|
||||
<w>appname</w>
|
||||
<w>appnameupper</w>
|
||||
<w>appnow</w>
|
||||
<w>appspot</w>
|
||||
<w>appstate</w>
|
||||
<w>apptime</w>
|
||||
@ -93,6 +95,7 @@
|
||||
<w>avel</w>
|
||||
<w>avels</w>
|
||||
<w>awaitable</w>
|
||||
<w>awaitables</w>
|
||||
<w>axismotion</w>
|
||||
<w>backgrounded</w>
|
||||
<w>backgrounding</w>
|
||||
@ -217,6 +220,7 @@
|
||||
<w>cend</w>
|
||||
<w>centiseconds</w>
|
||||
<w>certifi</w>
|
||||
<w>cfgdict</w>
|
||||
<w>cfgdir</w>
|
||||
<w>cfgpath</w>
|
||||
<w>changeme</w>
|
||||
@ -367,6 +371,7 @@
|
||||
<w>doraise</w>
|
||||
<w>dosomething</w>
|
||||
<w>dout</w>
|
||||
<w>downcasting</w>
|
||||
<w>downsample</w>
|
||||
<w>dpad</w>
|
||||
<w>dpads</w>
|
||||
@ -385,11 +390,13 @@
|
||||
<w>dummyret</w>
|
||||
<w>dummyval</w>
|
||||
<w>dummyvalid</w>
|
||||
<w>dumpminlog</w>
|
||||
<w>dval</w>
|
||||
<w>dxgi</w>
|
||||
<w>dynamicdata</w>
|
||||
<w>echidna</w>
|
||||
<w>echofile</w>
|
||||
<w>echotime</w>
|
||||
<w>edef</w>
|
||||
<w>effmult</w>
|
||||
<w>efro</w>
|
||||
@ -456,9 +463,11 @@
|
||||
<w>fastldlt</w>
|
||||
<w>fastlsolve</w>
|
||||
<w>fastltsolve</w>
|
||||
<w>faulthandler</w>
|
||||
<w>fbos</w>
|
||||
<w>fcntl</w>
|
||||
<w>fdata</w>
|
||||
<w>fdcount</w>
|
||||
<w>fdirx</w>
|
||||
<w>fdiry</w>
|
||||
<w>fdirz</w>
|
||||
@ -667,6 +676,7 @@
|
||||
<w>ioprepped</w>
|
||||
<w>ioprepping</w>
|
||||
<w>ioreg</w>
|
||||
<w>iscoroutinefunction</w>
|
||||
<w>iserverget</w>
|
||||
<w>iserverput</w>
|
||||
<w>isinst</w>
|
||||
@ -728,6 +738,7 @@
|
||||
<w>leaderboards</w>
|
||||
<w>lenval</w>
|
||||
<w>levelno</w>
|
||||
<w>levelnos</w>
|
||||
<w>levelstr</w>
|
||||
<w>lgui</w>
|
||||
<w>lhalf</w>
|
||||
@ -797,11 +808,13 @@
|
||||
<w>maskuv</w>
|
||||
<w>masterver</w>
|
||||
<w>mastervers</w>
|
||||
<w>matchlines</w>
|
||||
<w>maximus</w>
|
||||
<w>maxpathlen</w>
|
||||
<w>maxtries</w>
|
||||
<w>maxwait</w>
|
||||
<w>maxwidth</w>
|
||||
<w>mdpath</w>
|
||||
<w>mediump</w>
|
||||
<w>memalign</w>
|
||||
<w>memchr</w>
|
||||
@ -819,6 +832,7 @@
|
||||
<w>millisecs</w>
|
||||
<w>minelem</w>
|
||||
<w>miniplayer</w>
|
||||
<w>minlog</w>
|
||||
<w>minping</w>
|
||||
<w>minsdl</w>
|
||||
<w>mipmapcount</w>
|
||||
@ -1240,6 +1254,7 @@
|
||||
<w>shhh</w>
|
||||
<w>shifthigh</w>
|
||||
<w>shouldnt</w>
|
||||
<w>showbuffer</w>
|
||||
<w>shufflable</w>
|
||||
<w>signsubscale</w>
|
||||
<w>sigsetjmp</w>
|
||||
@ -1334,6 +1349,7 @@
|
||||
<w>subtypestr</w>
|
||||
<w>successmsg</w>
|
||||
<w>sval</w>
|
||||
<w>swidth</w>
|
||||
<w>swiftc</w>
|
||||
<w>symbolification</w>
|
||||
<w>syscalls</w>
|
||||
@ -1345,6 +1361,9 @@
|
||||
<w>targs</w>
|
||||
<w>tasklabel</w>
|
||||
<w>tbegin</w>
|
||||
<w>tbfile</w>
|
||||
<w>tbfiles</w>
|
||||
<w>tbpath</w>
|
||||
<w>tcls</w>
|
||||
<w>tdels</w>
|
||||
<w>tdiff</w>
|
||||
@ -1500,6 +1519,7 @@
|
||||
<w>wakeups</w>
|
||||
<w>walisser</w>
|
||||
<w>wasdebug</w>
|
||||
<w>wasn</w>
|
||||
<w>watte</w>
|
||||
<w>wdeprecated</w>
|
||||
<w>weakref</w>
|
||||
@ -1508,6 +1528,8 @@
|
||||
<w>weeeird</w>
|
||||
<w>welp</w>
|
||||
<w>whaaaaaaa</w>
|
||||
<w>whatarev</w>
|
||||
<w>whatisv</w>
|
||||
<w>wheee</w>
|
||||
<w>wheeee</w>
|
||||
<w>wiimote</w>
|
||||
|
||||
@ -464,7 +464,6 @@ add_executable(ballisticacore
|
||||
${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_host.h
|
||||
${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_host_udp.cc
|
||||
${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_host_udp.h
|
||||
${BA_SRC_ROOT}/ballistica/logic/friend_score_set.h
|
||||
${BA_SRC_ROOT}/ballistica/logic/host_activity.cc
|
||||
${BA_SRC_ROOT}/ballistica/logic/host_activity.h
|
||||
${BA_SRC_ROOT}/ballistica/logic/logic.cc
|
||||
|
||||
@ -455,7 +455,6 @@
|
||||
<ClInclude Include="..\..\src\ballistica\logic\connection\connection_to_host.h" />
|
||||
<ClCompile Include="..\..\src\ballistica\logic\connection\connection_to_host_udp.cc" />
|
||||
<ClInclude Include="..\..\src\ballistica\logic\connection\connection_to_host_udp.h" />
|
||||
<ClInclude Include="..\..\src\ballistica\logic\friend_score_set.h" />
|
||||
<ClCompile Include="..\..\src\ballistica\logic\host_activity.cc" />
|
||||
<ClInclude Include="..\..\src\ballistica\logic\host_activity.h" />
|
||||
<ClCompile Include="..\..\src\ballistica\logic\logic.cc" />
|
||||
|
||||
@ -799,9 +799,6 @@
|
||||
<ClInclude Include="..\..\src\ballistica\logic\connection\connection_to_host_udp.h">
|
||||
<Filter>ballistica\logic\connection</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\src\ballistica\logic\friend_score_set.h">
|
||||
<Filter>ballistica\logic</Filter>
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ballistica\logic\host_activity.cc">
|
||||
<Filter>ballistica\logic</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@ -450,7 +450,6 @@
|
||||
<ClInclude Include="..\..\src\ballistica\logic\connection\connection_to_host.h" />
|
||||
<ClCompile Include="..\..\src\ballistica\logic\connection\connection_to_host_udp.cc" />
|
||||
<ClInclude Include="..\..\src\ballistica\logic\connection\connection_to_host_udp.h" />
|
||||
<ClInclude Include="..\..\src\ballistica\logic\friend_score_set.h" />
|
||||
<ClCompile Include="..\..\src\ballistica\logic\host_activity.cc" />
|
||||
<ClInclude Include="..\..\src\ballistica\logic\host_activity.h" />
|
||||
<ClCompile Include="..\..\src\ballistica\logic\logic.cc" />
|
||||
|
||||
@ -799,9 +799,6 @@
|
||||
<ClInclude Include="..\..\src\ballistica\logic\connection\connection_to_host_udp.h">
|
||||
<Filter>ballistica\logic\connection</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\src\ballistica\logic\friend_score_set.h">
|
||||
<Filter>ballistica\logic</Filter>
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ballistica\logic\host_activity.cc">
|
||||
<Filter>ballistica\logic</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@ -156,7 +156,6 @@ void AppFlavor::UpdatePauseResume() {
|
||||
void AppFlavor::OnPause() {
|
||||
assert(InMainThread());
|
||||
|
||||
// Avoid reading gyro values for a short time to avoid hitches when restored.
|
||||
g_graphics->SetGyroEnabled(false);
|
||||
|
||||
// IMPORTANT: Any on-pause related stuff that threads need to do must
|
||||
@ -234,20 +233,64 @@ void AppFlavor::SetProductPrice(const std::string& product,
|
||||
|
||||
void AppFlavor::PauseApp() {
|
||||
assert(InMainThread());
|
||||
millisecs_t start_time{Platform::GetCurrentMilliseconds()};
|
||||
|
||||
// Apple mentioned 5 seconds to run stuff once backgrounded or
|
||||
// they bring down the hammer. Let's aim to stay under 2.
|
||||
millisecs_t max_duration{2000};
|
||||
|
||||
Platform::DebugLog("PauseApp@"
|
||||
+ std::to_string(Platform::GetCurrentMilliseconds()));
|
||||
assert(!sys_paused_app_);
|
||||
sys_paused_app_ = true;
|
||||
UpdatePauseResume();
|
||||
|
||||
// We assume that the OS will completely suspend our process the moment
|
||||
// we return from this call (though this is not technically true on all
|
||||
// platforms). So we want to spin and wait for threads to actually
|
||||
// process the pause message.
|
||||
size_t running_thread_count{};
|
||||
while (std::abs(Platform::GetCurrentMilliseconds() - start_time)
|
||||
< max_duration) {
|
||||
// If/when we get to a point with no threads waiting to be paused,
|
||||
// we're good to go.
|
||||
auto threads{Thread::GetStillPausingThreads()};
|
||||
running_thread_count = threads.size();
|
||||
if (running_thread_count == 0) {
|
||||
if (g_buildconfig.debug_build()) {
|
||||
Log(LogLevel::kDebug,
|
||||
"PauseApp() completed in "
|
||||
+ std::to_string(Platform::GetCurrentMilliseconds()
|
||||
- start_time)
|
||||
+ "ms.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we made it here, we timed out. Complain.
|
||||
Log(LogLevel::kError,
|
||||
std::string("PauseApp() took too long; ")
|
||||
+ std::to_string(running_thread_count)
|
||||
+ " threads not yet paused after "
|
||||
+ std::to_string(Platform::GetCurrentMilliseconds() - start_time)
|
||||
+ " ms.");
|
||||
}
|
||||
|
||||
void AppFlavor::ResumeApp() {
|
||||
assert(InMainThread());
|
||||
millisecs_t start_time{Platform::GetCurrentMilliseconds()};
|
||||
Platform::DebugLog("ResumeApp@"
|
||||
+ std::to_string(Platform::GetCurrentMilliseconds()));
|
||||
assert(sys_paused_app_);
|
||||
sys_paused_app_ = false;
|
||||
UpdatePauseResume();
|
||||
if (g_buildconfig.debug_build()) {
|
||||
Log(LogLevel::kDebug,
|
||||
"ResumeApp() completed in "
|
||||
+ std::to_string(Platform::GetCurrentMilliseconds() - start_time)
|
||||
+ "ms.");
|
||||
}
|
||||
}
|
||||
|
||||
void AppFlavor::DidFinishRenderingFrame(FrameDef* frame) {}
|
||||
@ -265,6 +308,7 @@ void AppFlavor::PrimeEventPump() {
|
||||
|
||||
#pragma mark Push-Calls
|
||||
|
||||
// FIXME - move this call to Platform.
|
||||
void AppFlavor::PushShowOnlineScoreUICall(const std::string& show,
|
||||
const std::string& game,
|
||||
const std::string& game_version) {
|
||||
@ -316,14 +360,6 @@ void AppFlavor::PushOpenURLCall(const std::string& url) {
|
||||
thread()->PushCall([url] { g_platform->OpenURL(url); });
|
||||
}
|
||||
|
||||
void AppFlavor::PushGetFriendScoresCall(const std::string& game,
|
||||
const std::string& game_version,
|
||||
void* data) {
|
||||
thread()->PushCall([game, game_version, data] {
|
||||
g_platform->GetFriendScores(game, game_version, data);
|
||||
});
|
||||
}
|
||||
|
||||
void AppFlavor::PushSubmitScoreCall(const std::string& game,
|
||||
const std::string& game_version,
|
||||
int64_t score) {
|
||||
|
||||
@ -43,16 +43,17 @@ class AppFlavor {
|
||||
/// Should process any pending OS events, etc.
|
||||
virtual auto RunEvents() -> void;
|
||||
|
||||
// These should be called by the window, view-controller, sdl,
|
||||
// or whatever is driving the app. They must be called from the main thread.
|
||||
|
||||
/// Should be called on mobile when the app is backgrounded.
|
||||
/// Pauses threads, closes network sockets, etc.
|
||||
/// Put the app into a paused state. Should be called from the main
|
||||
/// thread. Pauses work, closes network sockets, etc.
|
||||
/// Corresponds to being backgrounded on mobile, etc.
|
||||
/// It is assumed that, as soon as this call returns, all work is
|
||||
/// finished and all threads can be suspended by the OS without any
|
||||
/// negative side effects.
|
||||
auto PauseApp() -> void;
|
||||
|
||||
auto paused() const -> bool { return actually_paused_; }
|
||||
|
||||
/// Should be called on mobile when the app is foregrounded.
|
||||
/// Resume the app; corresponds to returning to foreground on mobile/etc.
|
||||
/// Spins threads back up, re-opens network sockets, etc.
|
||||
auto ResumeApp() -> void;
|
||||
|
||||
@ -101,9 +102,6 @@ class AppFlavor {
|
||||
auto PushShowOnlineScoreUICall(const std::string& show,
|
||||
const std::string& game,
|
||||
const std::string& game_version) -> void;
|
||||
auto PushGetFriendScoresCall(const std::string& game,
|
||||
const std::string& game_version, void* data)
|
||||
-> void;
|
||||
auto PushSubmitScoreCall(const std::string& game,
|
||||
const std::string& game_version, int64_t score)
|
||||
-> void;
|
||||
|
||||
@ -32,8 +32,8 @@
|
||||
namespace ballistica {
|
||||
|
||||
// These are set automatically via script; don't modify them here.
|
||||
const int kAppBuildNumber = 20934;
|
||||
const char* kAppVersion = "1.7.14";
|
||||
const int kAppBuildNumber = 20993;
|
||||
const char* kAppVersion = "1.7.19";
|
||||
|
||||
// Our standalone globals.
|
||||
// These are separated out for easy access.
|
||||
|
||||
@ -70,11 +70,11 @@ auto Logging::V1CloudLog(const std::string& msg) -> void {
|
||||
std::scoped_lock lock(g_app->v1_cloud_log_mutex);
|
||||
if (!g_app->v1_cloud_log_full) {
|
||||
(g_app->v1_cloud_log) += (msg + "\n");
|
||||
if ((g_app->v1_cloud_log).size() > 10000) {
|
||||
if ((g_app->v1_cloud_log).size() > 25000) {
|
||||
// Allow some reasonable overflow for last statement.
|
||||
if ((g_app->v1_cloud_log).size() > 100000) {
|
||||
if ((g_app->v1_cloud_log).size() > 250000) {
|
||||
// FIXME: This could potentially chop up utf-8 chars.
|
||||
(g_app->v1_cloud_log).resize(100000);
|
||||
(g_app->v1_cloud_log).resize(250000);
|
||||
}
|
||||
g_app->v1_cloud_log += "\n<max log size reached>\n";
|
||||
g_app->v1_cloud_log_full = true;
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
void Object::PrintObjects() {
|
||||
void Object::LsObjects() {
|
||||
#if BA_DEBUG_BUILD
|
||||
std::string s;
|
||||
{
|
||||
@ -47,7 +47,7 @@ void Object::PrintObjects() {
|
||||
for (auto&& i : obj_map) {
|
||||
sorted.emplace_back(i.second, i.first);
|
||||
}
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
std::sort(sorted.rbegin(), sorted.rend());
|
||||
for (auto&& i : sorted) {
|
||||
s += "\n " + std::to_string(i.first) + ": " + i.second;
|
||||
}
|
||||
@ -56,7 +56,7 @@ void Object::PrintObjects() {
|
||||
}
|
||||
Log(LogLevel::kInfo, s);
|
||||
#else
|
||||
Log(LogLevel::kInfo, "PrintObjects() only functions in debug builds.");
|
||||
Log(LogLevel::kInfo, "LsObjects() only functions in debug builds.");
|
||||
#endif // BA_DEBUG_BUILD
|
||||
}
|
||||
|
||||
|
||||
@ -21,8 +21,8 @@ class Object {
|
||||
Object();
|
||||
virtual ~Object();
|
||||
|
||||
/// Prints a tally of object types and counts (debug build only).
|
||||
static void PrintObjects();
|
||||
/// Logs a tally of ba::Object types and counts (debug build only).
|
||||
static void LsObjects();
|
||||
|
||||
// Object classes can provide descriptive names for themselves;
|
||||
// these are used for debugging and other purposes.
|
||||
|
||||
@ -91,7 +91,7 @@ auto Thread::RunAssetsThreadP(void* data) -> void* {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Thread::SetPaused(bool paused) {
|
||||
void Thread::PushSetPaused(bool paused) {
|
||||
// Can be toggled from the main thread only.
|
||||
assert(std::this_thread::get_id() == g_app->main_thread_id);
|
||||
PushThreadMessage(ThreadMessage(paused ? ThreadMessage::Type::kPause
|
||||
@ -101,13 +101,24 @@ void Thread::SetPaused(bool paused) {
|
||||
void Thread::WaitForNextEvent(bool single_cycle) {
|
||||
// If we're running a single cycle we never stop to wait.
|
||||
if (single_cycle) {
|
||||
// Need to revisit this if we ever do single-cycle for
|
||||
// the gil-holding thread so we don't starve other Python threads.
|
||||
assert(!acquires_python_gil_);
|
||||
return;
|
||||
}
|
||||
|
||||
// We also never wait if we have pending runnables.
|
||||
// (we run all existing runnables in each loop cycle, but one of those
|
||||
// may have enqueued more).
|
||||
if (has_pending_runnables()) {
|
||||
// We also never wait if we have pending runnables; we wan't to run
|
||||
// things as soon as we can. We chew through all runnables at the end
|
||||
// of the loop so it might seem like there should never be any here,
|
||||
// but runnables can add other runnables that won't get processed until
|
||||
// the next time through.
|
||||
// BUG FIX: We now skip this if we're paused since we don't run runnables
|
||||
// in that case. This was preventing us from releasing the GIL while paused
|
||||
// (and I assume causing us to spin full-speed through the loop; ugh).
|
||||
// NOTE: It is theoretically possible for a runnable to add another runnable
|
||||
// each time through the loop which would effectively starve the GIL as
|
||||
// well; do we need to worry about that case?
|
||||
if (has_pending_runnables() && !paused_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -422,15 +433,17 @@ Thread::~Thread() = default;
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "ConstantConditionsOC"
|
||||
|
||||
void Thread::LogThreadMessageTally() {
|
||||
void Thread::LogThreadMessageTally(
|
||||
std::vector<std::pair<LogLevel, std::string>>* log_entries) {
|
||||
// Prevent recursion.
|
||||
if (!writing_tally_) {
|
||||
writing_tally_ = true;
|
||||
|
||||
std::unordered_map<std::string, int> tally;
|
||||
Log(LogLevel::kError, "Thread message tally ("
|
||||
log_entries->emplace_back(std::make_pair(
|
||||
LogLevel::kError, "Thread message tally ("
|
||||
+ std::to_string(thread_messages_.size())
|
||||
+ " in list):");
|
||||
+ " in list):"));
|
||||
for (auto&& m : thread_messages_) {
|
||||
std::string s;
|
||||
switch (m.type) {
|
||||
@ -464,8 +477,9 @@ void Thread::LogThreadMessageTally() {
|
||||
}
|
||||
int entry = 1;
|
||||
for (auto&& i : tally) {
|
||||
Log(LogLevel::kError, " #" + std::to_string(entry++) + " ("
|
||||
+ std::to_string(i.second) + "x): " + i.first);
|
||||
log_entries->emplace_back(std::make_pair(
|
||||
LogLevel::kError, " #" + std::to_string(entry++) + " ("
|
||||
+ std::to_string(i.second) + "x): " + i.first));
|
||||
}
|
||||
writing_tally_ = false;
|
||||
}
|
||||
@ -473,15 +487,18 @@ void Thread::LogThreadMessageTally() {
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
void Thread::PushThreadMessage(const ThreadMessage& t) {
|
||||
// We don't want to make log calls while holding this mutex;
|
||||
// log calls acquire the GIL and if the GIL-holder (generally
|
||||
// the logic thread) is trying to send a thread message to the
|
||||
// thread doing the logging we would get deadlock.
|
||||
// So tally up any logs and send them after.
|
||||
std::vector<std::pair<LogLevel, std::string>> log_entries;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(thread_message_mutex_);
|
||||
|
||||
// Plop the data on to the list; we're assuming the mutex is locked.
|
||||
thread_messages_.push_back(t);
|
||||
|
||||
// Keep our own count; apparently size() on an stl list involves iterating.
|
||||
// FIXME: Actually I don't think this is the case anymore; should check.
|
||||
|
||||
// Debugging: show message count states.
|
||||
if (explicit_bool(false)) {
|
||||
static int one_off = 0;
|
||||
@ -498,8 +515,9 @@ void Thread::PushThreadMessage(const ThreadMessage& t) {
|
||||
// Show count periodically.
|
||||
if ((std::this_thread::get_id() == g_app->main_thread_id) && foo > 100) {
|
||||
foo = 0;
|
||||
Log(LogLevel::kInfo,
|
||||
"MSG COUNT " + std::to_string(thread_messages_.size()));
|
||||
log_entries.emplace_back(std::make_pair(
|
||||
LogLevel::kInfo,
|
||||
"MSG COUNT " + std::to_string(thread_messages_.size())));
|
||||
}
|
||||
}
|
||||
|
||||
@ -507,9 +525,11 @@ void Thread::PushThreadMessage(const ThreadMessage& t) {
|
||||
static bool sent_error = false;
|
||||
if (!sent_error) {
|
||||
sent_error = true;
|
||||
Log(LogLevel::kError,
|
||||
"ThreadMessage list > 1000 in thread: " + GetCurrentThreadName());
|
||||
LogThreadMessageTally();
|
||||
log_entries.emplace_back(std::make_pair(
|
||||
LogLevel::kError,
|
||||
"ThreadMessage list > 1000 in thread: " + GetCurrentThreadName()));
|
||||
|
||||
LogThreadMessageTally(&log_entries);
|
||||
}
|
||||
}
|
||||
|
||||
@ -523,15 +543,36 @@ void Thread::PushThreadMessage(const ThreadMessage& t) {
|
||||
// available.
|
||||
}
|
||||
thread_message_cv_.notify_all();
|
||||
|
||||
// Now log anything we accumulated safely outside of the locked section.
|
||||
for (auto&& log_entry : log_entries) {
|
||||
Log(log_entry.first, log_entry.second);
|
||||
}
|
||||
}
|
||||
|
||||
void Thread::SetThreadsPaused(bool paused) {
|
||||
auto Thread::SetThreadsPaused(bool paused) -> void {
|
||||
assert(std::this_thread::get_id() == g_app->main_thread_id);
|
||||
g_app->threads_paused = paused;
|
||||
for (auto&& i : g_app->pausable_threads) {
|
||||
i->SetPaused(paused);
|
||||
i->PushSetPaused(paused);
|
||||
}
|
||||
}
|
||||
|
||||
auto Thread::GetStillPausingThreads() -> std::vector<Thread*> {
|
||||
std::vector<Thread*> threads;
|
||||
assert(std::this_thread::get_id() == g_app->main_thread_id);
|
||||
|
||||
// Only return results if an actual pause is in effect.
|
||||
if (g_app->threads_paused) {
|
||||
for (auto&& i : g_app->pausable_threads) {
|
||||
if (!i->paused()) {
|
||||
threads.push_back(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return threads;
|
||||
}
|
||||
|
||||
auto Thread::AreThreadsPaused() -> bool { return g_app->threads_paused; }
|
||||
|
||||
auto Thread::NewTimer(millisecs_t length, bool repeat,
|
||||
@ -553,6 +594,7 @@ auto Thread::GetCurrentThreadName() -> std::string {
|
||||
}
|
||||
}
|
||||
|
||||
// Ask pthread for the thread name if we don't have one.
|
||||
// FIXME - move this to platform.
|
||||
#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS || BA_OSTYPE_LINUX
|
||||
std::string name = "unknown (sys-name=";
|
||||
|
||||
@ -45,7 +45,8 @@ class Thread {
|
||||
|
||||
void SetAcquiresPythonGIL();
|
||||
|
||||
void SetPaused(bool paused);
|
||||
void PushSetPaused(bool paused);
|
||||
|
||||
auto thread_id() const -> std::thread::id { return thread_id_; }
|
||||
|
||||
// Needed in rare cases where we jump physical threads.
|
||||
@ -97,6 +98,10 @@ class Thread {
|
||||
/// the app through a flood of packets.
|
||||
auto CheckPushSafety() -> bool;
|
||||
|
||||
static auto GetStillPausingThreads() -> std::vector<Thread*>;
|
||||
|
||||
auto paused() { return paused_; }
|
||||
|
||||
private:
|
||||
struct ThreadMessage {
|
||||
enum class Type { kShutdown = 999, kRunnable, kPause, kResume };
|
||||
@ -113,7 +118,8 @@ class Thread {
|
||||
auto SetInternalThreadName(const std::string& name) -> void;
|
||||
auto WaitForNextEvent(bool single_cycle) -> void;
|
||||
auto LoopUpkeep(bool once) -> void;
|
||||
auto LogThreadMessageTally() -> void;
|
||||
auto LogThreadMessageTally(
|
||||
std::vector<std::pair<LogLevel, std::string>>* log_entries) -> void;
|
||||
auto PushLocalRunnable(Runnable* runnable, bool* completion_flag) -> void;
|
||||
auto PushCrossThreadRunnable(Runnable* runnable, bool* completion_flag)
|
||||
-> void;
|
||||
|
||||
@ -80,7 +80,6 @@ class Data;
|
||||
class DataData;
|
||||
class Dynamics;
|
||||
class FrameDef;
|
||||
struct FriendScoreSet;
|
||||
class GLContext;
|
||||
class GlobalsNode;
|
||||
class Graphics;
|
||||
|
||||
@ -61,9 +61,9 @@
|
||||
#if BA_OSTYPE_MACOS
|
||||
#if BA_XCODE_BUILD
|
||||
#include <OpenGL/gl.h>
|
||||
#include <OpenGL/glext.h>
|
||||
#include <OpenGL/glu.h>
|
||||
#endif // BA_XCODE_BUILD
|
||||
#include <OpenGL/glext.h>
|
||||
#endif // BA_OSTYPE_MACOS
|
||||
|
||||
#endif // BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID
|
||||
|
||||
@ -1818,4 +1818,48 @@ auto Input::GetKeyName(int keycode) -> std::string {
|
||||
#pragma clang diagnostic pop
|
||||
#endif // BA_SDL2_BUILD || BA_MINSDL_BUILD
|
||||
|
||||
auto Input::LsInputDevices() -> void {
|
||||
BA_PRECONDITION(InLogicThread());
|
||||
|
||||
std::string out;
|
||||
|
||||
std::string ind{" "};
|
||||
int index{0};
|
||||
for (auto& device : input_devices_) {
|
||||
if (index != 0) {
|
||||
out += "\n";
|
||||
}
|
||||
out += std::to_string(index + 1) + ":\n";
|
||||
out += ind + "name: " + device->GetDeviceName() + "\n";
|
||||
out += ind + "index: " + std::to_string(device->index()) + "\n";
|
||||
out += (ind + "is-controller: " + std::to_string(device->IsController())
|
||||
+ "\n");
|
||||
out += (ind + "is-sdl-controller: "
|
||||
+ std::to_string(device->IsSDLController()) + "\n");
|
||||
out += (ind + "is-touch-screen: " + std::to_string(device->IsTouchScreen())
|
||||
+ "\n");
|
||||
out += (ind + "is-remote-control: "
|
||||
+ std::to_string(device->IsRemoteControl()) + "\n");
|
||||
out += (ind + "is-test-input: " + std::to_string(device->IsTestInput())
|
||||
+ "\n");
|
||||
out +=
|
||||
(ind + "is-keyboard: " + std::to_string(device->IsKeyboard()) + "\n");
|
||||
out += (ind + "is-mfi-controller: "
|
||||
+ std::to_string(device->IsMFiController()) + "\n");
|
||||
out += (ind + "is-local: " + std::to_string(device->IsLocal()) + "\n");
|
||||
out += (ind + "is-ui-only: " + std::to_string(device->IsUIOnly()) + "\n");
|
||||
out += (ind + "is-remote-app: " + std::to_string(device->IsRemoteApp())
|
||||
+ "\n");
|
||||
|
||||
out += ind + "attached-to: "
|
||||
+ (device->GetRemotePlayer() != nullptr ? "remote-player"
|
||||
: device->GetPlayer() != nullptr ? "local-player"
|
||||
: "nothing");
|
||||
|
||||
++index;
|
||||
}
|
||||
|
||||
Log(LogLevel::kInfo, out);
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
@ -126,6 +126,7 @@ class Input {
|
||||
auto PushTouchEvent(const TouchEvent& touch_event) -> void;
|
||||
auto PushDestroyKeyboardInputDevices() -> void;
|
||||
auto PushCreateKeyboardInputDevices() -> void;
|
||||
auto LsInputDevices() -> void;
|
||||
|
||||
/// Roughly how long in milliseconds have all input devices been idle.
|
||||
auto input_idle_time() const { return input_idle_time_; }
|
||||
@ -170,7 +171,6 @@ class Input {
|
||||
bool have_non_touch_inputs_{};
|
||||
float cursor_pos_x_{};
|
||||
float cursor_pos_y_{};
|
||||
// millisecs_t last_input_time_{};
|
||||
millisecs_t last_click_time_{};
|
||||
millisecs_t double_click_time_{200};
|
||||
millisecs_t last_mouse_move_time_{};
|
||||
|
||||
@ -377,12 +377,14 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr,
|
||||
}
|
||||
}
|
||||
|
||||
// Don't reuse a slot for 5 seconds.
|
||||
// Don't reuse a slot for 5 seconds (if its been heard from since this time).
|
||||
millisecs_t cooldown_time = GetRealTime() - 5000;
|
||||
|
||||
// Ok, not there already.. now look for a non-taken one and return that.
|
||||
for (int i = 0; i < kMaxRemoteAppClients; i++) {
|
||||
if (!clients_[i].in_use && clients_[i].last_contact_time < cooldown_time) {
|
||||
if (!clients_[i].in_use
|
||||
&& (clients_[i].last_contact_time == 0
|
||||
|| clients_[i].last_contact_time < cooldown_time)) {
|
||||
// Ok lets fill out the client.
|
||||
clients_[i].in_use = true;
|
||||
clients_[i].next_state_id = 0;
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_LOGIC_FRIEND_SCORE_SET_H_
|
||||
#define BALLISTICA_LOGIC_FRIEND_SCORE_SET_H_
|
||||
|
||||
#include <list>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// Used by game-center/etc when reporting friend scores to the game.
|
||||
struct FriendScoreSet {
|
||||
FriendScoreSet(bool success, void* user_data)
|
||||
: success(success), user_data(user_data) {}
|
||||
struct Entry {
|
||||
Entry(int score, std::string name, bool is_me)
|
||||
: score(score), name(std::move(name)), is_me(is_me) {}
|
||||
int score;
|
||||
std::string name;
|
||||
bool is_me;
|
||||
};
|
||||
std::list<Entry> entries;
|
||||
bool success;
|
||||
void* user_data;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_LOGIC_FRIEND_SCORE_SET_H_
|
||||
@ -19,7 +19,6 @@
|
||||
#include "ballistica/logic/connection/connection_set.h"
|
||||
#include "ballistica/logic/connection/connection_to_client_udp.h"
|
||||
#include "ballistica/logic/connection/connection_to_host_udp.h"
|
||||
#include "ballistica/logic/friend_score_set.h"
|
||||
#include "ballistica/logic/host_activity.h"
|
||||
#include "ballistica/logic/player.h"
|
||||
#include "ballistica/logic/session/client_session.h"
|
||||
@ -1138,11 +1137,6 @@ void Logic::PushPlaySoundCall(SystemSoundID sound) {
|
||||
[sound] { g_audio->PlaySound(g_assets->GetSound(sound)); });
|
||||
}
|
||||
|
||||
void Logic::PushFriendScoreSetCall(const FriendScoreSet& score_set) {
|
||||
thread()->PushCall(
|
||||
[score_set] { g_python->HandleFriendScoresCB(score_set); });
|
||||
}
|
||||
|
||||
void Logic::PushConfirmQuitCall() {
|
||||
thread()->PushCall([this] {
|
||||
assert(InLogicThread());
|
||||
@ -2114,6 +2108,20 @@ void Logic::SetPublicPartySize(int count) {
|
||||
}
|
||||
}
|
||||
|
||||
auto Logic::SetPublicPartyQueueEnabled(bool enabled) -> void {
|
||||
assert(InLogicThread());
|
||||
if (enabled == public_party_queue_enabled_) {
|
||||
return;
|
||||
}
|
||||
public_party_queue_enabled_ = enabled;
|
||||
|
||||
// Push our new state to the server *ONLY* if public-party is turned on
|
||||
// (wasteful otherwise).
|
||||
if (public_party_enabled_) {
|
||||
g_app_internal->PushPublicPartyState();
|
||||
}
|
||||
}
|
||||
|
||||
void Logic::SetPublicPartyMaxSize(int count) {
|
||||
assert(InLogicThread());
|
||||
if (count == public_party_max_size_) {
|
||||
|
||||
@ -88,7 +88,6 @@ class Logic {
|
||||
auto PushConfirmQuitCall() -> void;
|
||||
auto PushStringEditSetCall(const std::string& value) -> void;
|
||||
auto PushStringEditCancelCall() -> void;
|
||||
auto PushFriendScoreSetCall(const FriendScoreSet& score_set) -> void;
|
||||
auto PushShowURLCall(const std::string& url) -> void;
|
||||
auto PushOnAppResumeCall() -> void;
|
||||
auto PushFrameDefRequest() -> void;
|
||||
@ -215,6 +214,10 @@ class Logic {
|
||||
auto public_party_size() const { return public_party_size_; }
|
||||
auto SetPublicPartySize(int count) -> void;
|
||||
auto public_party_max_size() const { return public_party_max_size_; }
|
||||
auto SetPublicPartyQueueEnabled(bool enabled) -> void;
|
||||
auto public_party_queue_enabled() const {
|
||||
return public_party_queue_enabled_;
|
||||
}
|
||||
auto public_party_max_player_count() const {
|
||||
return public_party_max_player_count_;
|
||||
}
|
||||
@ -323,6 +326,7 @@ class Logic {
|
||||
bool public_party_enabled_{};
|
||||
int public_party_size_{1}; // Always count ourself (is that what we want?).
|
||||
int public_party_max_size_{8};
|
||||
bool public_party_queue_enabled_{true};
|
||||
int public_party_player_count_{0};
|
||||
int public_party_max_player_count_{8};
|
||||
std::string public_party_name_;
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include "ballistica/networking/network_reader.h"
|
||||
|
||||
#include "ballistica/core/thread.h"
|
||||
#include "ballistica/generic/json.h"
|
||||
#include "ballistica/input/remote_app.h"
|
||||
#include "ballistica/logic/connection/connection_set.h"
|
||||
@ -175,6 +176,25 @@ static auto HandleGameQuery(const char* buffer, size_t size,
|
||||
}
|
||||
}
|
||||
|
||||
auto NetworkReader::CheckFDThreshold(int val) -> void {
|
||||
if (passed_fd_threshold_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Let's trigger when we pass 2/3 of the FD limit.
|
||||
if (val < FD_SETSIZE * 2 / 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we pass the threshold, do a one-time dump of info
|
||||
// to try and debug it.
|
||||
passed_fd_threshold_ = true;
|
||||
g_logic->thread()->PushCall([val] {
|
||||
assert(InLogicThread());
|
||||
g_python->obj(Python::ObjID::kOnTooManyFileDescriptorsCall).Call();
|
||||
});
|
||||
}
|
||||
|
||||
auto NetworkReader::RunThread() -> int {
|
||||
if (!HeadlessMode()) {
|
||||
remote_server_ = std::make_unique<RemoteAppServer>();
|
||||
@ -202,6 +222,7 @@ auto NetworkReader::RunThread() -> int {
|
||||
// Try to get a clean error instead of a crash if we exceed our
|
||||
// open file descriptor limit (except on windows where FD_SETSIZE
|
||||
// is apparently a dummy value).
|
||||
CheckFDThreshold(sd4_);
|
||||
if (sd4_ < 0 || sd4_ >= FD_SETSIZE) {
|
||||
FatalError("Socket/File Descriptor Overflow (sd4="
|
||||
+ std::to_string(sd4_) + ", FD_SETSIZE="
|
||||
@ -216,6 +237,7 @@ auto NetworkReader::RunThread() -> int {
|
||||
// Try to get a clean error instead of a crash if we exceed our
|
||||
// open file descriptor limit (except on windows where FD_SETSIZE
|
||||
// is apparently a dummy value).
|
||||
CheckFDThreshold(sd6_);
|
||||
if (sd6_ < 0 || sd6_ >= FD_SETSIZE) {
|
||||
FatalError("Socket/File Descriptor Overflow (sd6="
|
||||
+ std::to_string(sd6_) + ", FD_SETSIZE="
|
||||
|
||||
@ -32,6 +32,7 @@ class NetworkReader {
|
||||
auto sd6() const { return sd6_; }
|
||||
|
||||
private:
|
||||
auto CheckFDThreshold(int val) -> void;
|
||||
auto OpenSockets() -> void;
|
||||
auto PokeSelf() -> void;
|
||||
auto RunThread() -> int;
|
||||
@ -53,6 +54,7 @@ class NetworkReader {
|
||||
bool paused_{};
|
||||
std::mutex paused_mutex_;
|
||||
std::condition_variable paused_cv_;
|
||||
bool passed_fd_threshold_{};
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
@ -37,8 +37,6 @@ class PlatformApple : public Platform {
|
||||
const std::vector<float>& widths, float scale)
|
||||
-> void* override;
|
||||
auto GetTextTextureData(void* tex) -> uint8_t* override;
|
||||
auto GetFriendScores(const std::string& game, const std::string& game_version,
|
||||
void* py_callback) -> void override;
|
||||
auto SubmitScore(const std::string& game, const std::string& version,
|
||||
int64_t score) -> void override;
|
||||
auto ReportAchievement(const std::string& achievement) -> void override;
|
||||
|
||||
@ -28,7 +28,6 @@
|
||||
#include "ballistica/graphics/mesh/sprite_mesh.h"
|
||||
#include "ballistica/graphics/vr_graphics.h"
|
||||
#include "ballistica/input/input.h"
|
||||
#include "ballistica/logic/friend_score_set.h"
|
||||
#include "ballistica/logic/logic.h"
|
||||
#include "ballistica/networking/networking_sys.h"
|
||||
#include "ballistica/platform/sdl/sdl_app.h"
|
||||
@ -196,6 +195,11 @@ auto Platform::LoginAdapterGetSignInToken(const std::string& login_type,
|
||||
});
|
||||
}
|
||||
|
||||
auto Platform::LoginAdapterBackEndActiveChange(const std::string& login_type,
|
||||
bool active) -> void {
|
||||
// Default is no-op.
|
||||
}
|
||||
|
||||
auto Platform::GetDeviceV1AccountUUIDPrefix() -> std::string {
|
||||
Log(LogLevel::kError, "GetDeviceV1AccountUUIDPrefix() unimplemented");
|
||||
return "u";
|
||||
@ -217,8 +221,15 @@ auto Platform::GetPublicDeviceUUID() -> std::string {
|
||||
// This UUID is supposed to change periodically, so let's plug in
|
||||
// some stuff to enforce that.
|
||||
inputs.emplace_back(GetOSVersionString());
|
||||
inputs.emplace_back(kAppVersion);
|
||||
inputs.emplace_back("kerploople");
|
||||
|
||||
// This part gets shuffled periodically by my version-increment tools.
|
||||
// We used to plug version in directly here, but that caused uuids to
|
||||
// shuffle too rapidly during periods of rapid development. This
|
||||
// keeps it more constant.
|
||||
// __last_rand_uuid_component_shuffle_date__ 2022 12 17
|
||||
auto rand_uuid_component{"BMCJPHH0SC22KB0WVJ1RAYD68TPEXL58"};
|
||||
|
||||
inputs.emplace_back(rand_uuid_component);
|
||||
auto gil{Python::ScopedInterpreterLock()};
|
||||
auto pylist{g_python->StringList(inputs)};
|
||||
auto args{g_python->SingleMemberTuple(pylist)};
|
||||
@ -862,13 +873,6 @@ auto Platform::ConvertIncomingLeaderboardScore(
|
||||
return score;
|
||||
}
|
||||
|
||||
void Platform::GetFriendScores(const std::string& game,
|
||||
const std::string& game_version, void* data) {
|
||||
// As a default, just fail gracefully.
|
||||
Log(LogLevel::kError, "FIXME: GetFriendScores unimplemented");
|
||||
g_logic->PushFriendScoreSetCall(FriendScoreSet(false, data));
|
||||
}
|
||||
|
||||
void Platform::SubmitScore(const std::string& game, const std::string& version,
|
||||
int64_t score) {
|
||||
Log(LogLevel::kError, "FIXME: SubmitScore() unimplemented");
|
||||
|
||||
@ -339,6 +339,10 @@ class Platform {
|
||||
/// Called when a Python LoginAdapter is requesting an explicit sign-in.
|
||||
virtual auto LoginAdapterGetSignInToken(const std::string& login_type,
|
||||
int attempt_id) -> void;
|
||||
/// Called when a Python LoginAdapter is informing us that a back-end is
|
||||
/// active/inactive.
|
||||
virtual auto LoginAdapterBackEndActiveChange(const std::string& login_type,
|
||||
bool active) -> void;
|
||||
|
||||
#pragma mark MUSIC PLAYBACK ----------------------------------------------------
|
||||
|
||||
@ -368,9 +372,6 @@ class Platform {
|
||||
virtual auto ConvertIncomingLeaderboardScore(
|
||||
const std::string& leaderboard_id, int score) -> int;
|
||||
|
||||
virtual auto GetFriendScores(const std::string& game,
|
||||
const std::string& game_version,
|
||||
void* py_callback) -> void;
|
||||
virtual auto SubmitScore(const std::string& game, const std::string& version,
|
||||
int64_t score) -> void;
|
||||
virtual auto ReportAchievement(const std::string& achievement) -> void;
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include "ballistica/app/app_flavor.h"
|
||||
#include "ballistica/assets/component/texture.h"
|
||||
#include "ballistica/core/logging.h"
|
||||
#include "ballistica/core/thread.h"
|
||||
#include "ballistica/graphics/graphics.h"
|
||||
#include "ballistica/logic/connection/connection_set.h"
|
||||
#include "ballistica/logic/host_activity.h"
|
||||
@ -308,18 +309,29 @@ auto PyPushCall(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* {
|
||||
int from_other_thread{};
|
||||
int suppress_warning{};
|
||||
int other_thread_use_fg_context{};
|
||||
static const char* kwlist[] = {"call", "from_other_thread",
|
||||
int raw{0};
|
||||
static const char* kwlist[] = {"call",
|
||||
"from_other_thread",
|
||||
"suppress_other_thread_warning",
|
||||
"other_thread_use_fg_context", nullptr};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|ppp",
|
||||
"other_thread_use_fg_context",
|
||||
"raw",
|
||||
nullptr};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|pppp",
|
||||
const_cast<char**>(kwlist), &call_obj,
|
||||
&from_other_thread, &suppress_warning,
|
||||
&other_thread_use_fg_context)) {
|
||||
&other_thread_use_fg_context, &raw)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The from-other-thread case is basically a different call.
|
||||
if (from_other_thread) {
|
||||
// 'raw' mode does no thread checking and no context saves/restores.
|
||||
if (raw) {
|
||||
Py_INCREF(call_obj);
|
||||
g_logic->thread()->PushCall([call_obj] {
|
||||
assert(InLogicThread());
|
||||
|
||||
PythonRef(call_obj, PythonRef::kSteal).Call();
|
||||
});
|
||||
} else if (from_other_thread) {
|
||||
// Warn the user not to use this from the logic thread since it doesnt
|
||||
// save/restore context.
|
||||
if (!suppress_warning && InLogicThread()) {
|
||||
@ -1132,15 +1144,12 @@ auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
|
||||
{"pushcall", (PyCFunction)PyPushCall, METH_VARARGS | METH_KEYWORDS,
|
||||
"pushcall(call: Callable, from_other_thread: bool = False,\n"
|
||||
" suppress_other_thread_warning: bool = False,\n"
|
||||
" other_thread_use_fg_context: bool = False) -> None\n"
|
||||
"\n"
|
||||
"Pushes a call onto the event loop to be run during the next cycle.\n"
|
||||
" other_thread_use_fg_context: bool = False,\n"
|
||||
" raw: bool = False) -> None\n"
|
||||
"\n"
|
||||
"Push a call to the logic event-loop.\n"
|
||||
"Category: **General Utility Functions**\n"
|
||||
"\n"
|
||||
"This can be handy for calls that are disallowed from within other\n"
|
||||
"callbacks, etc.\n"
|
||||
"\n"
|
||||
"This call expects to be used in the logic thread, and will "
|
||||
"automatically\n"
|
||||
"save and restore the ba.Context to behave seamlessly.\n"
|
||||
@ -1149,8 +1158,9 @@ auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
|
||||
"however, you can pass 'from_other_thread' as True. In this case\n"
|
||||
"the call will always run in the UI context on the logic thread\n"
|
||||
"or whichever context is in the foreground if\n"
|
||||
"other_thread_use_fg_context is True."},
|
||||
|
||||
"other_thread_use_fg_context is True.\n"
|
||||
"Passing raw=True will disable thread checks and context"
|
||||
" sets/restores."},
|
||||
{"getactivity", (PyCFunction)PyGetActivity,
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"getactivity(doraise: bool = True) -> <varies>\n"
|
||||
|
||||
@ -114,6 +114,21 @@ auto PySetPublicPartyMaxSize(PyObject* self, PyObject* args, PyObject* keywds)
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
|
||||
auto PySetPublicPartyQueueEnabled(PyObject* self, PyObject* args,
|
||||
PyObject* keywds) -> PyObject* {
|
||||
BA_PYTHON_TRY;
|
||||
int enabled;
|
||||
static const char* kwlist[] = {"enabled", nullptr};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, keywds, "p",
|
||||
const_cast<char**>(kwlist), &enabled)) {
|
||||
return nullptr;
|
||||
}
|
||||
assert(g_python);
|
||||
g_logic->SetPublicPartyQueueEnabled(enabled);
|
||||
Py_RETURN_NONE;
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
|
||||
auto PySetAuthenticateClients(PyObject* self, PyObject* args, PyObject* keywds)
|
||||
-> PyObject* {
|
||||
BA_PYTHON_TRY;
|
||||
@ -498,6 +513,13 @@ auto PythonMethodsNetworking::GetMethods() -> std::vector<PyMethodDef> {
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"set_public_party_max_size(max_size: int) -> None\n"
|
||||
"\n"
|
||||
|
||||
"(internal)"},
|
||||
|
||||
{"set_public_party_queue_enabled",
|
||||
(PyCFunction)PySetPublicPartyQueueEnabled, METH_VARARGS | METH_KEYWORDS,
|
||||
"set_public_party_queue_enabled(max_size: bool) -> None\n"
|
||||
"\n"
|
||||
"(internal)"},
|
||||
|
||||
{"get_public_party_max_size", (PyCFunction)PyGetPublicPartyMaxSize,
|
||||
|
||||
@ -638,6 +638,22 @@ auto PyLoginAdapterGetSignInToken(PyObject* self, PyObject* args,
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
|
||||
auto PyLoginAdapterBackEndActiveChange(PyObject* self, PyObject* args,
|
||||
PyObject* keywds) -> PyObject* {
|
||||
BA_PYTHON_TRY;
|
||||
const char* login_type;
|
||||
int active;
|
||||
static const char* kwlist[] = {"login_type", "active", nullptr};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, keywds, "sp",
|
||||
const_cast<char**>(kwlist), &login_type,
|
||||
&active)) {
|
||||
return nullptr;
|
||||
}
|
||||
g_platform->LoginAdapterBackEndActiveChange(login_type, active);
|
||||
Py_RETURN_NONE;
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
|
||||
auto PySetInternalLanguageKeys(PyObject* self, PyObject* args) -> PyObject* {
|
||||
BA_PYTHON_TRY;
|
||||
PyObject* list_obj;
|
||||
@ -715,10 +731,18 @@ auto PyAndroidShowWifiSettings(PyObject* self, PyObject* args, PyObject* keywds)
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
|
||||
auto PyPrintObjects(PyObject* self, PyObject* args, PyObject* keywds)
|
||||
auto PyLsObjects(PyObject* self, PyObject* args, PyObject* keywds)
|
||||
-> PyObject* {
|
||||
BA_PYTHON_TRY;
|
||||
Object::PrintObjects();
|
||||
Object::LsObjects();
|
||||
Py_RETURN_NONE;
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
|
||||
auto PyLsInputDevices(PyObject* self, PyObject* args, PyObject* keywds)
|
||||
-> PyObject* {
|
||||
BA_PYTHON_TRY;
|
||||
g_input->LsInputDevices();
|
||||
Py_RETURN_NONE;
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
@ -756,6 +780,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
|
||||
"\n"
|
||||
"If this returns False, UIs should not show 'copy to clipboard'\n"
|
||||
"buttons, etc."},
|
||||
|
||||
{"clipboard_has_text", (PyCFunction)PyClipboardHasText, METH_NOARGS,
|
||||
"clipboard_has_text() -> bool\n"
|
||||
"\n"
|
||||
@ -765,6 +790,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
|
||||
"\n"
|
||||
"This will return False if no system clipboard is available; no need\n"
|
||||
" to call ba.clipboard_is_supported() separately."},
|
||||
|
||||
{"clipboard_set_text", (PyCFunction)PyClipboardSetText,
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"clipboard_set_text(value: str) -> None\n"
|
||||
@ -775,6 +801,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
|
||||
"\n"
|
||||
"Ensure that ba.clipboard_is_supported() returns True before adding\n"
|
||||
" buttons/etc. that make use of this functionality."},
|
||||
|
||||
{"clipboard_get_text", (PyCFunction)PyClipboardGetText, METH_NOARGS,
|
||||
"clipboard_get_text() -> str\n"
|
||||
"\n"
|
||||
@ -784,9 +811,20 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
|
||||
"\n"
|
||||
"Ensure that ba.clipboard_has_text() returns True before calling\n"
|
||||
" this function."},
|
||||
{"printobjects", (PyCFunction)PyPrintObjects,
|
||||
|
||||
{"ls_objects", (PyCFunction)PyLsObjects, METH_VARARGS | METH_KEYWORDS,
|
||||
"ls_objects() -> None\n"
|
||||
"\n"
|
||||
"Log debugging info about C++ level objects.\n"
|
||||
"\n"
|
||||
"Category: **General Utility Functions**\n"
|
||||
"\n"
|
||||
"This call only functions in debug builds of the game.\n"
|
||||
"It prints various info about the current object count, etc."},
|
||||
|
||||
{"ls_input_devices", (PyCFunction)PyLsInputDevices,
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"printobjects() -> None\n"
|
||||
"ls_input_devices() -> None\n"
|
||||
"\n"
|
||||
"Print debugging info about game objects.\n"
|
||||
"\n"
|
||||
@ -859,6 +897,14 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
|
||||
"\n"
|
||||
"(internal)"},
|
||||
|
||||
{"login_adapter_back_end_active_change",
|
||||
(PyCFunction)PyLoginAdapterBackEndActiveChange,
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"login_adapter_back_end_active_change(login_type: str, active: bool)"
|
||||
" -> None\n"
|
||||
"\n"
|
||||
"(internal)"},
|
||||
|
||||
{"submit_analytics_counts", (PyCFunction)PySubmitAnalyticsCounts,
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
"submit_analytics_counts() -> None\n"
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
#include "ballistica/input/device/joystick.h"
|
||||
#include "ballistica/input/device/keyboard_input.h"
|
||||
#include "ballistica/internal/app_internal.h"
|
||||
#include "ballistica/logic/friend_score_set.h"
|
||||
#include "ballistica/logic/host_activity.h"
|
||||
#include "ballistica/logic/player.h"
|
||||
#include "ballistica/logic/v1_account.h"
|
||||
@ -2144,40 +2143,6 @@ void Python::CaptureKeyboardInput(PyObject* obj) {
|
||||
}
|
||||
void Python::ReleaseKeyboardInput() { keyboard_call_.Release(); }
|
||||
|
||||
void Python::HandleFriendScoresCB(const FriendScoreSet& score_set) {
|
||||
// This is the initial strong-ref to this pointer
|
||||
// so it will be cleaned up properly.
|
||||
Object::Ref<PythonContextCall> cb(
|
||||
static_cast<PythonContextCall*>(score_set.user_data));
|
||||
|
||||
// We pass None on error.
|
||||
if (!score_set.success) {
|
||||
PythonRef args(Py_BuildValue("(O)", Py_None), PythonRef::kSteal);
|
||||
cb->Run(args);
|
||||
} else {
|
||||
// Otherwise convert it to a python list and pass that.
|
||||
PyObject* py_list = PyList_New(0);
|
||||
std::string icon_str;
|
||||
#if BA_USE_GOOGLE_PLAY_GAME_SERVICES
|
||||
icon_str = g_logic->CharStr(SpecialChar::kGooglePlayGamesLogo);
|
||||
#elif BA_USE_GAME_CIRCLE
|
||||
icon_str = g_logic->CharStr(SpecialChar::kGameCircleLogo);
|
||||
#elif BA_USE_GAME_CENTER
|
||||
icon_str = g_logic->CharStr(SpecialChar::kGameCenterLogo);
|
||||
#endif
|
||||
for (auto&& i : score_set.entries) {
|
||||
PyObject* obj =
|
||||
Py_BuildValue("[isi]", i.score, (icon_str + i.name).c_str(),
|
||||
static_cast<int>(i.is_me));
|
||||
PyList_Append(py_list, obj);
|
||||
Py_DECREF(obj);
|
||||
}
|
||||
PythonRef args(Py_BuildValue("(O)", py_list), PythonRef::kSteal);
|
||||
Py_DECREF(py_list);
|
||||
cb->Run(args);
|
||||
}
|
||||
}
|
||||
|
||||
auto Python::HandleKeyPressEvent(const SDL_Keysym& keysym) -> bool {
|
||||
assert(InLogicThread());
|
||||
if (!keyboard_call_.exists()) {
|
||||
|
||||
@ -114,7 +114,6 @@ class Python {
|
||||
auto ReleaseGamePadInput() -> void;
|
||||
auto CaptureKeyboardInput(PyObject* obj) -> void;
|
||||
auto ReleaseKeyboardInput() -> void;
|
||||
auto HandleFriendScoresCB(const FriendScoreSet& ss) -> void;
|
||||
auto IssueCallInLogicThreadWarning(PyObject* call) -> void;
|
||||
|
||||
/// Borrowed from python's source code: used in overriding of objects' dir()
|
||||
@ -369,6 +368,7 @@ class Python {
|
||||
kImplicitSignInCall,
|
||||
kImplicitSignOutCall,
|
||||
kLoginAdapterGetSignInTokenResponseCall,
|
||||
kOnTooManyFileDescriptorsCall,
|
||||
kLast // Sentinel; must be at end.
|
||||
};
|
||||
|
||||
|
||||
@ -446,7 +446,9 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool {
|
||||
CheckLayout();
|
||||
|
||||
// Ignore mouse stuff while transitioning out.
|
||||
if (transitioning_ && transitioning_out_) break;
|
||||
if (transitioning_ && transitioning_out_) {
|
||||
break;
|
||||
}
|
||||
|
||||
float x = m.fval1;
|
||||
float y = m.fval2;
|
||||
@ -505,7 +507,9 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool {
|
||||
CheckLayout();
|
||||
|
||||
// Ignore mouse stuff while transitioning.
|
||||
if (transitioning_ && transitioning_out_) break;
|
||||
if (transitioning_ && transitioning_out_) {
|
||||
break;
|
||||
}
|
||||
|
||||
float x = m.fval1;
|
||||
float y = m.fval2;
|
||||
@ -546,7 +550,9 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool {
|
||||
CheckLayout();
|
||||
|
||||
// Ignore mouse stuff while transitioning.
|
||||
if (transitioning_ && transitioning_out_) break;
|
||||
if (transitioning_ && transitioning_out_) {
|
||||
break;
|
||||
}
|
||||
|
||||
float x = m.fval1;
|
||||
float y = m.fval2;
|
||||
@ -909,7 +915,9 @@ void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) {
|
||||
}
|
||||
|
||||
// Don't draw if we've fully transitioned out.
|
||||
if (transitioning_out_ && !transitioning_) return;
|
||||
if (transitioning_out_ && !transitioning_) {
|
||||
return;
|
||||
}
|
||||
|
||||
float l = transition_offset_x_smoothed_ + transition_scale_offset_x_;
|
||||
float r = l + width_;
|
||||
|
||||
@ -1108,7 +1108,15 @@ void RootWidget::SetOverlayWidget(StackWidget* w) {
|
||||
overlay_stack_widget_ = w;
|
||||
}
|
||||
|
||||
void RootWidget::OnCancelCustom() { g_ui->PushBackButtonCall(nullptr); }
|
||||
void RootWidget::OnCancelCustom() {
|
||||
// Need to revisit this. If the cancel event it pushes is not handled, it will
|
||||
// wind up back here where it pushes another back call. This cycle repeats
|
||||
// forever until something comes along which does handle cancel events and
|
||||
// then it gets them all. Current repro case is Sign-in-with-BombSquad-Account
|
||||
// window - press escape a few times while that is up and then click cancel;
|
||||
// This code is only used for toolbar mode so should be safe to leave it
|
||||
// disabled for now. g_ui->PushBackButtonCall(nullptr);
|
||||
}
|
||||
|
||||
auto RootWidget::GetSpecialWidget(const std::string& s) const -> Widget* {
|
||||
if (s == "party_button") {
|
||||
|
||||
@ -441,20 +441,23 @@ void TextWidget::SetText(const std::string& text_in_raw) {
|
||||
bool do_format_check{};
|
||||
bool print_false_positives{};
|
||||
|
||||
if (g_buildconfig.debug_build()) {
|
||||
do_format_check = explicit_bool(true);
|
||||
} else {
|
||||
if (text_in_raw.size() > 1 && text_in_raw[0] == '{'
|
||||
&& text_in_raw[text_in_raw.size() - 1] == '}') {
|
||||
// Ok, its got bounds like json; now if its either missing quotes or a
|
||||
// colon then let's check it.
|
||||
if (!strstr(text_in_raw.c_str(), "\"")
|
||||
|| !strstr(text_in_raw.c_str(), ":")) {
|
||||
do_format_check = true;
|
||||
// Only non-editable text support resource-strings.
|
||||
if (!editable_) {
|
||||
if (g_buildconfig.debug_build()) {
|
||||
do_format_check = explicit_bool(true);
|
||||
} else {
|
||||
if (text_in_raw.size() > 1 && text_in_raw[0] == '{'
|
||||
&& text_in_raw[text_in_raw.size() - 1] == '}') {
|
||||
// Ok, its got bounds like json; now if its either missing quotes or a
|
||||
// colon then let's check it.
|
||||
if (!strstr(text_in_raw.c_str(), "\"")
|
||||
|| !strstr(text_in_raw.c_str(), ":")) {
|
||||
do_format_check = true;
|
||||
|
||||
// We wanna avoid doing this check when we don't have to.
|
||||
// so lets print if we get a false positive
|
||||
print_false_positives = true;
|
||||
// We wanna avoid doing this check when we don't have to.
|
||||
// so lets print if we get a false positive
|
||||
print_false_positives = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,4 +146,5 @@ def get_binding_values() -> tuple[Any, ...]:
|
||||
_hooks.implicit_sign_in, # kImplicitSignInCall
|
||||
_hooks.implicit_sign_out, # kImplicitSignOutCall
|
||||
_hooks.login_adapter_get_sign_in_token_response, # kLoginAdapterGetSignInTokenResponseCall
|
||||
_apputils.on_too_many_file_descriptors, # kOnTooManyFileDescriptorsCall
|
||||
) # yapf: disable
|
||||
|
||||
@ -198,9 +198,9 @@ class _BoundTestMessageSenderAsync(BoundMessageSender):
|
||||
async def send_async(self, message: _TMsg3) -> None:
|
||||
...
|
||||
|
||||
async def send_async(self, message: Message) -> Response | None:
|
||||
def send_async(self, message: Message) -> Awaitable[Response | None]:
|
||||
"""Send a message asynchronously."""
|
||||
return await self._sender.send_async(self._obj, message)
|
||||
return self._sender.send_async(self._obj, message)
|
||||
|
||||
|
||||
# SEND_ASYNC_CODE_TEST_END
|
||||
@ -261,9 +261,9 @@ class _BoundTestMessageSenderBBoth(BoundMessageSender):
|
||||
async def send_async(self, message: _TMsg4) -> None:
|
||||
...
|
||||
|
||||
async def send_async(self, message: Message) -> Response | None:
|
||||
def send_async(self, message: Message) -> Awaitable[Response | None]:
|
||||
"""Send a message asynchronously."""
|
||||
return await self._sender.send_async(self._obj, message)
|
||||
return self._sender.send_async(self._obj, message)
|
||||
|
||||
|
||||
# SEND_BOTH_CODE_TEST_END
|
||||
@ -424,11 +424,11 @@ class _TestAsyncMessageReceiver(MessageReceiver):
|
||||
class _BoundTestAsyncMessageReceiver(BoundMessageReceiver):
|
||||
"""Protocol-specific bound receiver."""
|
||||
|
||||
async def handle_raw_message(
|
||||
def handle_raw_message(
|
||||
self, message: str, raise_unregistered: bool = False
|
||||
) -> str:
|
||||
) -> Awaitable[str]:
|
||||
"""Asynchronously handle a raw incoming message."""
|
||||
return await self._receiver.handle_raw_message_async(
|
||||
return self._receiver.handle_raw_message_async(
|
||||
self._obj, message, raise_unregistered
|
||||
)
|
||||
|
||||
|
||||
@ -157,6 +157,24 @@ class WorkspaceFetchResponse(Response):
|
||||
done: Annotated[bool, IOAttrs('d')] = False
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class MerchAvailabilityMessage(Message):
|
||||
"""Can we show merch link?"""
|
||||
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [MerchAvailabilityResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class MerchAvailabilityResponse(Response):
|
||||
"""About that merch..."""
|
||||
|
||||
url: Annotated[str | None, IOAttrs('u')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class SignInMessage(Message):
|
||||
@ -165,6 +183,10 @@ class SignInMessage(Message):
|
||||
login_type: Annotated[LoginType, IOAttrs('l')]
|
||||
sign_in_token: Annotated[str, IOAttrs('t')]
|
||||
|
||||
# For debugging. Can remove soft_default once build 20988+ is ubiquitous.
|
||||
description: Annotated[str, IOAttrs('d', soft_default='-')]
|
||||
apptime: Annotated[float, IOAttrs('at', soft_default=-1.0)]
|
||||
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [SignInResponse]
|
||||
|
||||
@ -19,3 +19,13 @@ class LoginType(Enum):
|
||||
|
||||
# Google Play Game Services
|
||||
GPGS = 'gpgs'
|
||||
|
||||
@property
|
||||
def displayname(self) -> str:
|
||||
"""Human readable name for this value."""
|
||||
cls = type(self)
|
||||
match self:
|
||||
case cls.EMAIL:
|
||||
return 'Email/Password'
|
||||
case cls.GPGS:
|
||||
return 'Google Play Games'
|
||||
|
||||
@ -138,6 +138,11 @@ class ServerConfig:
|
||||
tuple[float, float, float], tuple[float, float, float]
|
||||
] | None = None
|
||||
|
||||
# Whether to enable the queue where players can line up before entering
|
||||
# your server. Disabling this can be used as a workaround to deal with
|
||||
# queue spamming attacks.
|
||||
enable_queue: bool = True
|
||||
|
||||
# (internal) stress-testing mode.
|
||||
stress_test_players: int | None = None
|
||||
|
||||
|
||||
@ -40,22 +40,22 @@ class PyRequirement:
|
||||
# as manually-installed bits, pip itself must have some way to allow for
|
||||
# that, right?...
|
||||
PY_REQUIREMENTS = [
|
||||
PyRequirement(modulename='pylint', minversion=[2, 14, 5]),
|
||||
PyRequirement(modulename='mypy', minversion=[0, 971]),
|
||||
PyRequirement(modulename='pylint', minversion=[2, 15, 9]),
|
||||
PyRequirement(modulename='mypy', minversion=[0, 991]),
|
||||
PyRequirement(modulename='cpplint', minversion=[1, 6, 1]),
|
||||
PyRequirement(modulename='pytest', minversion=[7, 1, 2]),
|
||||
PyRequirement(modulename='pytest', minversion=[7, 2, 0]),
|
||||
PyRequirement(modulename='pytz'),
|
||||
PyRequirement(modulename='ansiwrap'),
|
||||
PyRequirement(modulename='yaml', pipname='PyYAML'),
|
||||
PyRequirement(modulename='requests'),
|
||||
PyRequirement(modulename='pdoc'),
|
||||
PyRequirement(pipname='black', minversion=[22, 10, 0]),
|
||||
PyRequirement(pipname='typing_extensions', minversion=[4, 3, 0]),
|
||||
PyRequirement(pipname='black', minversion=[22, 12, 0]),
|
||||
PyRequirement(pipname='typing_extensions', minversion=[4, 4, 0]),
|
||||
PyRequirement(pipname='types-filelock', minversion=[3, 2, 7]),
|
||||
PyRequirement(pipname='types-requests', minversion=[2, 28, 9]),
|
||||
PyRequirement(pipname='types-requests', minversion=[2, 28, 11, 7]),
|
||||
PyRequirement(pipname='types-pytz', minversion=[2022, 2, 1, 0]),
|
||||
PyRequirement(pipname='types-PyYAML', minversion=[6, 0, 11]),
|
||||
PyRequirement(pipname='certifi', minversion=[2022, 6, 15]),
|
||||
PyRequirement(pipname='types-PyYAML', minversion=[6, 0, 12, 2]),
|
||||
PyRequirement(pipname='certifi', minversion=[2022, 12, 7]),
|
||||
PyRequirement(pipname='types-certifi', minversion=[2021, 10, 8, 3]),
|
||||
]
|
||||
|
||||
|
||||
@ -729,8 +729,10 @@ class Updater:
|
||||
|
||||
def _check_misc(self) -> None:
|
||||
# Misc sanity checks.
|
||||
if not self._public:
|
||||
# Make sure we're set to prod master server.
|
||||
|
||||
# Make sure we're set to prod master server.
|
||||
# (but ONLY when checking; still want to be able to run updates).
|
||||
if not self._public and self._check:
|
||||
with open(
|
||||
'src/ballistica/internal/master_server_config.h',
|
||||
encoding='utf-8',
|
||||
|
||||
@ -274,6 +274,44 @@ if TYPE_CHECKING:
|
||||
) -> _CallNoArgs[OutT]:
|
||||
...
|
||||
|
||||
# 4 arg call; 3 args bundled.
|
||||
# noinspection PyPep8Naming
|
||||
@overload
|
||||
def Call(
|
||||
call: Callable[[In1T, In2T, In3T, In4T], OutT],
|
||||
arg1: In1T,
|
||||
arg2: In2T,
|
||||
arg3: In3T,
|
||||
) -> _Call1Arg[In4T, OutT]:
|
||||
...
|
||||
|
||||
# 4 arg call; 2 args bundled.
|
||||
# noinspection PyPep8Naming
|
||||
@overload
|
||||
def Call(
|
||||
call: Callable[[In1T, In2T, In3T, In4T], OutT],
|
||||
arg1: In1T,
|
||||
arg2: In2T,
|
||||
) -> _Call2Args[In3T, In4T, OutT]:
|
||||
...
|
||||
|
||||
# 4 arg call; 1 arg bundled.
|
||||
# noinspection PyPep8Naming
|
||||
@overload
|
||||
def Call(
|
||||
call: Callable[[In1T, In2T, In3T, In4T], OutT],
|
||||
arg1: In1T,
|
||||
) -> _Call3Args[In2T, In3T, In4T, OutT]:
|
||||
...
|
||||
|
||||
# 4 arg call; no args bundled.
|
||||
# noinspection PyPep8Naming
|
||||
@overload
|
||||
def Call(
|
||||
call: Callable[[In1T, In2T, In3T, In4T], OutT],
|
||||
) -> _Call4Args[In1T, In2T, In3T, In4T, OutT]:
|
||||
...
|
||||
|
||||
# 5 arg call; 5 args bundled.
|
||||
# noinspection PyPep8Naming
|
||||
@overload
|
||||
|
||||
@ -111,6 +111,8 @@ class IOAttrs:
|
||||
boundaries (see efro.util.utc_today()).
|
||||
'whole_hours', if True, requires datetime values to lie exactly on hour
|
||||
boundaries (see efro.util.utc_this_hour()).
|
||||
'whole_minutes', if True, requires datetime values to lie exactly on minute
|
||||
boundaries (see efro.util.utc_this_minute()).
|
||||
'soft_default', if passed, injects a default value into dataclass
|
||||
instantiation when the field is not present in the input data.
|
||||
This allows dataclasses to add new non-optional fields while
|
||||
@ -136,6 +138,7 @@ class IOAttrs:
|
||||
store_default: bool = True
|
||||
whole_days: bool = False
|
||||
whole_hours: bool = False
|
||||
whole_minutes: bool = False
|
||||
soft_default: Any = MISSING
|
||||
soft_default_factory: Callable[[], Any] | _MissingType = MISSING
|
||||
|
||||
@ -145,6 +148,7 @@ class IOAttrs:
|
||||
store_default: bool = store_default,
|
||||
whole_days: bool = whole_days,
|
||||
whole_hours: bool = whole_hours,
|
||||
whole_minutes: bool = whole_minutes,
|
||||
soft_default: Any = MISSING,
|
||||
soft_default_factory: Callable[[], Any] | _MissingType = MISSING,
|
||||
):
|
||||
@ -160,6 +164,8 @@ class IOAttrs:
|
||||
self.whole_days = whole_days
|
||||
if whole_hours != cls.whole_hours:
|
||||
self.whole_hours = whole_hours
|
||||
if whole_minutes != cls.whole_minutes:
|
||||
self.whole_minutes = whole_minutes
|
||||
if soft_default is not cls.soft_default:
|
||||
|
||||
# Do what dataclasses does with its default types and
|
||||
@ -216,13 +222,18 @@ class IOAttrs:
|
||||
raise ValueError(
|
||||
f'Value {value} at {fieldpath} is not a whole day.'
|
||||
)
|
||||
if self.whole_hours:
|
||||
elif self.whole_hours:
|
||||
if any(
|
||||
x != 0 for x in (value.minute, value.second, value.microsecond)
|
||||
):
|
||||
raise ValueError(
|
||||
f'Value {value} at {fieldpath}' f' is not a whole hour.'
|
||||
)
|
||||
elif self.whole_minutes:
|
||||
if any(x != 0 for x in (value.second, value.microsecond)):
|
||||
raise ValueError(
|
||||
f'Value {value} at {fieldpath}' f' is not a whole minute.'
|
||||
)
|
||||
|
||||
|
||||
def _get_origin(anntype: Any) -> Any:
|
||||
|
||||
@ -80,6 +80,15 @@ class IntegrityError(ValueError):
|
||||
"""Data has been tampered with or corrupted in some form."""
|
||||
|
||||
|
||||
class AuthenticationError(Exception):
|
||||
"""Authentication has failed for some operation.
|
||||
|
||||
This can be raised if server-side-verification does not match
|
||||
client-supplied credentials, if an invalid password is supplied
|
||||
for a sign-in attempt, etc.
|
||||
"""
|
||||
|
||||
|
||||
def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
|
||||
"""Is the provided exception from urllib a communication-related error?
|
||||
|
||||
|
||||
@ -37,7 +37,27 @@ class LogLevel(Enum):
|
||||
ERROR = 3
|
||||
CRITICAL = 4
|
||||
|
||||
@property
|
||||
def python_logging_level(self) -> int:
|
||||
"""Give the corresponding logging level."""
|
||||
return LOG_LEVEL_LEVELNOS[self]
|
||||
|
||||
@classmethod
|
||||
def from_python_logging_level(cls, levelno: int) -> LogLevel:
|
||||
"""Given a Python logging level, return a LogLevel."""
|
||||
return LEVELNO_LOG_LEVELS[levelno]
|
||||
|
||||
|
||||
# Python logging levels from LogLevels
|
||||
LOG_LEVEL_LEVELNOS = {
|
||||
LogLevel.DEBUG: logging.DEBUG,
|
||||
LogLevel.INFO: logging.INFO,
|
||||
LogLevel.WARNING: logging.WARNING,
|
||||
LogLevel.ERROR: logging.ERROR,
|
||||
LogLevel.CRITICAL: logging.CRITICAL,
|
||||
}
|
||||
|
||||
# LogLevels from Python logging levels
|
||||
LEVELNO_LOG_LEVELS = {
|
||||
logging.DEBUG: LogLevel.DEBUG,
|
||||
logging.INFO: LogLevel.INFO,
|
||||
@ -128,7 +148,9 @@ class LogHandler(logging.Handler):
|
||||
self._cache_lock = Lock()
|
||||
self._printed_callback_error = False
|
||||
self._thread_bootstrapped = False
|
||||
self._thread = Thread(target=self._thread_main, daemon=True)
|
||||
self._thread = Thread(target=self._log_thread_main, daemon=True)
|
||||
if __debug__:
|
||||
self._last_slow_emit_warning_time: float | None = None
|
||||
self._thread.start()
|
||||
|
||||
# Spin until our thread is up and running; otherwise we could
|
||||
@ -145,7 +167,7 @@ class LogHandler(logging.Handler):
|
||||
with self._callbacks_lock:
|
||||
self._callbacks.append(call)
|
||||
|
||||
def _thread_main(self) -> None:
|
||||
def _log_thread_main(self) -> None:
|
||||
self._event_loop = asyncio.new_event_loop()
|
||||
# NOTE: if we ever use default threadpool at all we should allow
|
||||
# setting it for our loop.
|
||||
@ -172,20 +194,15 @@ class LogHandler(logging.Handler):
|
||||
now = utc_now()
|
||||
with self._cache_lock:
|
||||
|
||||
# Quick out: if oldest cache entry is still valid,
|
||||
# don't touch anything.
|
||||
if (
|
||||
# Prune the oldest entry as long as there is a first one that
|
||||
# is too old.
|
||||
while (
|
||||
self._cache
|
||||
and (now - self._cache[0][1].time) < self._cache_time_limit
|
||||
and (now - self._cache[0][1].time) >= self._cache_time_limit
|
||||
):
|
||||
continue
|
||||
|
||||
# Ok; full prune.
|
||||
self._cache = [
|
||||
e
|
||||
for e in self._cache
|
||||
if (now - e[1].time) < self._cache_time_limit
|
||||
]
|
||||
popped = self._cache.pop(0)
|
||||
self._cache_size -= popped[0]
|
||||
self._cache_index_offset += 1
|
||||
|
||||
def get_cached(
|
||||
self, start_index: int = 0, max_entries: int | None = None
|
||||
@ -223,6 +240,9 @@ class LogHandler(logging.Handler):
|
||||
)
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
if __debug__:
|
||||
starttime = time.monotonic()
|
||||
|
||||
# Called by logging to send us records.
|
||||
# We simply package them up and ship them to our thread.
|
||||
# UPDATE: turns out we CAN get log messages from this thread
|
||||
@ -246,6 +266,9 @@ class LogHandler(logging.Handler):
|
||||
# didn't expect to be stringified.
|
||||
msg = self.format(record)
|
||||
|
||||
if __debug__:
|
||||
formattime = time.monotonic()
|
||||
|
||||
# Also immediately print pretty colored output to our echo file
|
||||
# (generally stderr). We do this part here instead of in our bg
|
||||
# thread because the delay can throw off command line prompts or
|
||||
@ -257,6 +280,9 @@ class LogHandler(logging.Handler):
|
||||
else:
|
||||
self._echofile.write(f'{msg}\n')
|
||||
|
||||
if __debug__:
|
||||
echotime = time.monotonic()
|
||||
|
||||
self._event_loop.call_soon_threadsafe(
|
||||
tpartial(
|
||||
self._emit_in_thread,
|
||||
@ -267,6 +293,37 @@ class LogHandler(logging.Handler):
|
||||
)
|
||||
)
|
||||
|
||||
if __debug__:
|
||||
# Make noise if we're taking a significant amount of time here.
|
||||
# Limit the noise to once every so often though; otherwise we
|
||||
# could get a feedback loop where every log emit results in a
|
||||
# warning log which results in another, etc.
|
||||
now = time.monotonic()
|
||||
# noinspection PyUnboundLocalVariable
|
||||
duration = now - starttime
|
||||
# noinspection PyUnboundLocalVariable
|
||||
format_duration = formattime - starttime
|
||||
# noinspection PyUnboundLocalVariable
|
||||
echo_duration = echotime - formattime
|
||||
if duration > 0.05 and (
|
||||
self._last_slow_emit_warning_time is None
|
||||
or now > self._last_slow_emit_warning_time + 10.0
|
||||
):
|
||||
# Logging calls from *within* a logging handler
|
||||
# sounds sketchy, so let's just kick this over to
|
||||
# the bg event loop thread we've already got.
|
||||
self._last_slow_emit_warning_time = now
|
||||
self._event_loop.call_soon_threadsafe(
|
||||
tpartial(
|
||||
logging.warning,
|
||||
'efro.log.LogHandler emit took too long'
|
||||
' (%.2fs total; %.2fs format, %.2fs echo).',
|
||||
duration,
|
||||
format_duration,
|
||||
echo_duration,
|
||||
)
|
||||
)
|
||||
|
||||
def _emit_in_thread(
|
||||
self, name: str, levelno: int, created: float, message: str
|
||||
) -> None:
|
||||
|
||||
@ -43,6 +43,20 @@ class SysResponse:
|
||||
users of the api never see them.
|
||||
"""
|
||||
|
||||
def set_local_exception(self, exc: Exception) -> None:
|
||||
"""Attach a local exception to facilitate better logging/handling.
|
||||
|
||||
Be aware that this data does not get serialized and only
|
||||
exists on the local object.
|
||||
"""
|
||||
setattr(self, '_sr_local_exception', exc)
|
||||
|
||||
def get_local_exception(self) -> Exception | None:
|
||||
"""Fetch a local attached exception."""
|
||||
value = getattr(self, '_sr_local_exception', None)
|
||||
assert isinstance(value, Exception | None)
|
||||
return value
|
||||
|
||||
|
||||
# Some standard response types:
|
||||
|
||||
|
||||
@ -282,10 +282,13 @@ class MessageProtocol:
|
||||
def _get_module_header(
|
||||
self,
|
||||
part: Literal['sender', 'receiver'],
|
||||
extra_import_code: str | None = None,
|
||||
extra_import_code: str | None,
|
||||
enable_async_sends: bool,
|
||||
) -> str:
|
||||
"""Return common parts of generated modules."""
|
||||
# pylint: disable=too-many-locals, too-many-branches
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
import textwrap
|
||||
|
||||
tpimports: dict[str, list[str]] = {}
|
||||
@ -342,7 +345,7 @@ class MessageProtocol:
|
||||
|
||||
if part == 'sender':
|
||||
import_lines += (
|
||||
'from efro.message import MessageSender,' ' BoundMessageSender'
|
||||
'from efro.message import MessageSender, BoundMessageSender'
|
||||
)
|
||||
tpimport_typing_extras = ''
|
||||
else:
|
||||
@ -362,11 +365,18 @@ class MessageProtocol:
|
||||
import_lines += f'\n{extra_import_code}\n'
|
||||
|
||||
ovld = ', overload' if not single_message_type else ''
|
||||
ovld2 = (
|
||||
', cast, Awaitable'
|
||||
if (single_message_type and part == 'sender' and enable_async_sends)
|
||||
else ''
|
||||
)
|
||||
tpimport_lines = textwrap.indent(tpimport_lines, ' ')
|
||||
|
||||
baseimps = ['Any']
|
||||
if part == 'receiver':
|
||||
baseimps.append('Callable')
|
||||
if part == 'sender' and enable_async_sends:
|
||||
baseimps.append('Awaitable')
|
||||
baseimps_s = ', '.join(baseimps)
|
||||
out = (
|
||||
'# Released under the MIT License. See LICENSE for details.\n'
|
||||
@ -375,7 +385,7 @@ class MessageProtocol:
|
||||
f'\n'
|
||||
f'from __future__ import annotations\n'
|
||||
f'\n'
|
||||
f'from typing import TYPE_CHECKING{ovld}\n'
|
||||
f'from typing import TYPE_CHECKING{ovld}{ovld2}\n'
|
||||
f'\n'
|
||||
f'{import_lines}\n'
|
||||
f'\n'
|
||||
@ -399,13 +409,16 @@ class MessageProtocol:
|
||||
) -> str:
|
||||
"""Used by create_sender_module(); do not call directly."""
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-branches
|
||||
import textwrap
|
||||
|
||||
msgtypes = list(self.message_ids_by_type.keys())
|
||||
|
||||
ppre = '_' if private else ''
|
||||
out = self._get_module_header(
|
||||
'sender', extra_import_code=protocol_module_level_import_code
|
||||
'sender',
|
||||
extra_import_code=protocol_module_level_import_code,
|
||||
enable_async_sends=enable_async_sends,
|
||||
)
|
||||
ccind = textwrap.indent(protocol_create_code, ' ')
|
||||
out += (
|
||||
@ -438,7 +451,8 @@ class MessageProtocol:
|
||||
continue
|
||||
pfx = 'async ' if async_pass else ''
|
||||
sfx = '_async' if async_pass else ''
|
||||
awt = 'await ' if async_pass else ''
|
||||
# awt = 'await ' if async_pass else ''
|
||||
awt = ''
|
||||
how = 'asynchronously' if async_pass else 'synchronously'
|
||||
|
||||
if len(msgtypes) == 1:
|
||||
@ -451,22 +465,29 @@ class MessageProtocol:
|
||||
rtypevar = ' | '.join(_filt_tp_name(t) for t in rtypes)
|
||||
else:
|
||||
rtypevar = _filt_tp_name(rtypes[0])
|
||||
if async_pass:
|
||||
rtypevar = f'Awaitable[{rtypevar}]'
|
||||
out += (
|
||||
f'\n'
|
||||
f' {pfx}def send{sfx}(self,'
|
||||
f' def send{sfx}(self,'
|
||||
f' message: {msgtypevar})'
|
||||
f' -> {rtypevar}:\n'
|
||||
f' """Send a message {how}."""\n'
|
||||
f' out = {awt}self._sender.'
|
||||
f'send{sfx}(self._obj, message)\n'
|
||||
f' assert isinstance(out, {rtypevar})\n'
|
||||
f' return out\n'
|
||||
)
|
||||
if not async_pass:
|
||||
out += (
|
||||
f' assert isinstance(out, {rtypevar})\n'
|
||||
' return out\n'
|
||||
)
|
||||
else:
|
||||
out += f' return cast({rtypevar}, out)\n'
|
||||
|
||||
else:
|
||||
|
||||
for msgtype in msgtypes:
|
||||
msgtypevar = msgtype.__name__
|
||||
# rtypes = msgtype.get_response_types()
|
||||
rtypes = msgtype.get_response_types()
|
||||
if len(rtypes) > 1:
|
||||
rtypevar = ' | '.join(
|
||||
@ -482,10 +503,13 @@ class MessageProtocol:
|
||||
f' -> {rtypevar}:\n'
|
||||
f' ...\n'
|
||||
)
|
||||
rtypevar = 'Response | None'
|
||||
if async_pass:
|
||||
rtypevar = f'Awaitable[{rtypevar}]'
|
||||
out += (
|
||||
f'\n'
|
||||
f' {pfx}def send{sfx}(self, message: Message)'
|
||||
f' -> Response | None:\n'
|
||||
f' def send{sfx}(self, message: Message)'
|
||||
f' -> {rtypevar}:\n'
|
||||
f' """Send a message {how}."""\n'
|
||||
f' return {awt}self._sender.'
|
||||
f'send{sfx}(self._obj, message)\n'
|
||||
@ -509,7 +533,9 @@ class MessageProtocol:
|
||||
ppre = '_' if private else ''
|
||||
msgtypes = list(self.message_ids_by_type.keys())
|
||||
out = self._get_module_header(
|
||||
'receiver', extra_import_code=protocol_module_level_import_code
|
||||
'receiver',
|
||||
extra_import_code=protocol_module_level_import_code,
|
||||
enable_async_sends=False,
|
||||
)
|
||||
ccind = textwrap.indent(protocol_create_code, ' ')
|
||||
out += (
|
||||
@ -602,11 +628,11 @@ class MessageProtocol:
|
||||
if is_async:
|
||||
out += (
|
||||
'\n'
|
||||
' async def handle_raw_message(\n'
|
||||
' def handle_raw_message(\n'
|
||||
' self, message: str, raise_unregistered: bool = False\n'
|
||||
' ) -> str:\n'
|
||||
' ) -> Awaitable[str]:\n'
|
||||
' """Asynchronously handle a raw incoming message."""\n'
|
||||
' return await self._receiver.'
|
||||
' return self._receiver.'
|
||||
'handle_raw_message_async(\n'
|
||||
' self._obj, message, raise_unregistered\n'
|
||||
' )\n'
|
||||
|
||||
@ -62,12 +62,6 @@ class MessageReceiver:
|
||||
[Any, Message | None, Response | SysResponse, dict], None
|
||||
] | None = None
|
||||
|
||||
# TODO: don't currently have async encode equivalent
|
||||
# or either for sender; can add as needed.
|
||||
self._decode_filter_async_call: Callable[
|
||||
[Any, dict, Message], Awaitable[None]
|
||||
] | None = None
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
def register_handler(
|
||||
self, call: Callable[[Any, Message], Response | None]
|
||||
@ -96,14 +90,17 @@ class MessageReceiver:
|
||||
|
||||
# Make sure we are only given async methods if we are an async handler
|
||||
# and sync ones otherwise.
|
||||
is_async = inspect.iscoroutinefunction(call)
|
||||
if self.is_async != is_async:
|
||||
msg = (
|
||||
'Expected a sync method; found an async one.'
|
||||
if is_async
|
||||
else 'Expected an async method; found a sync one.'
|
||||
)
|
||||
raise ValueError(msg)
|
||||
# UPDATE - can't do this anymore since we now sometimes use
|
||||
# regular functions which return awaitables instead of having
|
||||
# the entire function be async.
|
||||
# is_async = inspect.iscoroutinefunction(call)
|
||||
# if self.is_async != is_async:
|
||||
# msg = (
|
||||
# 'Expected a sync method; found an async one.'
|
||||
# if is_async
|
||||
# else 'Expected an async method; found a sync one.'
|
||||
# )
|
||||
# raise ValueError(msg)
|
||||
|
||||
# Check annotation types to determine what message types we handle.
|
||||
# Return-type annotation can be a Union, but we probably don't
|
||||
@ -189,19 +186,6 @@ class MessageReceiver:
|
||||
self._decode_filter_call = call
|
||||
return call
|
||||
|
||||
def decode_filter_async_method(
|
||||
self, call: Callable[[Any, dict, Message], Awaitable[None]]
|
||||
) -> Callable[[Any, dict, Message], Awaitable[None]]:
|
||||
"""Function decorator for defining a decode filter.
|
||||
|
||||
Decode filters can be used to extract extra data from incoming
|
||||
message dicts. Note that this version will only work with
|
||||
handle_raw_message_async().
|
||||
"""
|
||||
assert self._decode_filter_async_call is None
|
||||
self._decode_filter_async_call = call
|
||||
return call
|
||||
|
||||
def encode_filter_method(
|
||||
self,
|
||||
call: Callable[
|
||||
@ -247,24 +231,6 @@ class MessageReceiver:
|
||||
bound_obj, _msg_dict, msg_decoded = self._decode_incoming_message_base(
|
||||
bound_obj=bound_obj, msg=msg
|
||||
)
|
||||
|
||||
# If they've set an async filter but are calling sync
|
||||
# handle_raw_message() its likely a bug.
|
||||
assert self._decode_filter_async_call is None
|
||||
|
||||
return msg_decoded
|
||||
|
||||
async def _decode_incoming_message_async(
|
||||
self, bound_obj: Any, msg: str
|
||||
) -> Message:
|
||||
bound_obj, msg_dict, msg_decoded = self._decode_incoming_message_base(
|
||||
bound_obj=bound_obj, msg=msg
|
||||
)
|
||||
|
||||
if self._decode_filter_async_call is not None:
|
||||
await self._decode_filter_async_call(
|
||||
bound_obj, msg_dict, msg_decoded
|
||||
)
|
||||
return msg_decoded
|
||||
|
||||
def encode_user_response(
|
||||
@ -316,6 +282,7 @@ class MessageReceiver:
|
||||
"""
|
||||
assert not self.is_async, "can't call sync handler on async receiver"
|
||||
msg_decoded: Message | None = None
|
||||
msgtype: type[Message] | None = None
|
||||
try:
|
||||
msg_decoded = self._decode_incoming_message(bound_obj, msg)
|
||||
msgtype = type(msg_decoded)
|
||||
@ -335,41 +302,93 @@ class MessageReceiver:
|
||||
bound_obj, msg_decoded, exc
|
||||
)
|
||||
if dolog:
|
||||
logging.exception('Error in efro.message handling.')
|
||||
if msgtype is not None:
|
||||
logging.exception(
|
||||
'Error handling %s.%s message.',
|
||||
msgtype.__module__,
|
||||
msgtype.__qualname__,
|
||||
)
|
||||
else:
|
||||
logging.exception('Error in efro.message handling.')
|
||||
return rstr
|
||||
|
||||
async def handle_raw_message_async(
|
||||
def handle_raw_message_async(
|
||||
self, bound_obj: Any, msg: str, raise_unregistered: bool = False
|
||||
) -> str:
|
||||
) -> Awaitable[str]:
|
||||
"""Should be called when the receiver gets a message.
|
||||
|
||||
The return value is the raw response to the message.
|
||||
"""
|
||||
|
||||
# Note: This call is synchronous so that the first part of it can
|
||||
# happen synchronously. If the whole call were async we wouldn't be
|
||||
# able to guarantee that messages handlers would be called in the
|
||||
# order the messages were received.
|
||||
|
||||
assert self.is_async, "can't call async handler on sync receiver"
|
||||
msg_decoded: Message | None = None
|
||||
msgtype: type[Message] | None = None
|
||||
try:
|
||||
msg_decoded = await self._decode_incoming_message_async(
|
||||
bound_obj, msg
|
||||
)
|
||||
msg_decoded = self._decode_incoming_message(bound_obj, msg)
|
||||
msgtype = type(msg_decoded)
|
||||
handler = self._handlers.get(msgtype)
|
||||
if handler is None:
|
||||
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
|
||||
response = await handler(bound_obj, msg_decoded)
|
||||
assert isinstance(response, Response | None)
|
||||
return self.encode_user_response(bound_obj, msg_decoded, response)
|
||||
handler_awaitable = handler(bound_obj, msg_decoded)
|
||||
|
||||
except Exception as exc:
|
||||
if raise_unregistered and isinstance(
|
||||
exc, UnregisteredMessageIDError
|
||||
):
|
||||
raise
|
||||
rstr, dolog = self.encode_error_response(
|
||||
bound_obj, msg_decoded, exc
|
||||
return self._handle_raw_message_async_error(
|
||||
bound_obj, msg_decoded, msgtype, exc
|
||||
)
|
||||
if dolog:
|
||||
|
||||
# Return an awaitable to handle the rest asynchronously.
|
||||
return self._handle_raw_message_async(
|
||||
bound_obj, msg_decoded, msgtype, handler_awaitable
|
||||
)
|
||||
|
||||
async def _handle_raw_message_async_error(
|
||||
self,
|
||||
bound_obj: Any,
|
||||
msg_decoded: Message | None,
|
||||
msgtype: type[Message] | None,
|
||||
exc: Exception,
|
||||
) -> str:
|
||||
rstr, dolog = self.encode_error_response(bound_obj, msg_decoded, exc)
|
||||
if dolog:
|
||||
if msgtype is not None:
|
||||
logging.exception(
|
||||
'Error handling %s.%s message.',
|
||||
msgtype.__module__,
|
||||
msgtype.__qualname__,
|
||||
)
|
||||
else:
|
||||
logging.exception('Error in efro.message handling.')
|
||||
return rstr
|
||||
return rstr
|
||||
|
||||
async def _handle_raw_message_async(
|
||||
self,
|
||||
bound_obj: Any,
|
||||
msg_decoded: Message,
|
||||
msgtype: type[Message] | None,
|
||||
handler_awaitable: Awaitable[Response | None],
|
||||
) -> str:
|
||||
"""Should be called when the receiver gets a message.
|
||||
|
||||
The return value is the raw response to the message.
|
||||
"""
|
||||
try:
|
||||
response = await handler_awaitable
|
||||
assert isinstance(response, Response | None)
|
||||
return self.encode_user_response(bound_obj, msg_decoded, response)
|
||||
|
||||
except Exception as exc:
|
||||
return await self._handle_raw_message_async_error(
|
||||
bound_obj, msg_decoded, msgtype, exc
|
||||
)
|
||||
|
||||
|
||||
class BoundMessageReceiver:
|
||||
|
||||
@ -6,7 +6,6 @@ Supports static typing for message types and possible return types.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.error import CleanError, RemoteError, CommunicationError
|
||||
@ -45,6 +44,9 @@ class MessageSender:
|
||||
self._send_async_raw_message_call: Callable[
|
||||
[Any, str], Awaitable[str]
|
||||
] | None = None
|
||||
self._send_async_raw_message_ex_call: Callable[
|
||||
[Any, str, Message], Awaitable[str]
|
||||
] | None = None
|
||||
self._encode_filter_call: Callable[
|
||||
[Any, Message, dict], None
|
||||
] | None = None
|
||||
@ -76,11 +78,32 @@ class MessageSender:
|
||||
CommunicationErrors raised here will be returned to the sender
|
||||
as such; all other exceptions will result in a RuntimeError for
|
||||
the sender.
|
||||
|
||||
IMPORTANT: Generally async send methods should not be implemented
|
||||
as 'async' methods, but instead should be regular methods that
|
||||
return awaitable objects. This way it can be guaranteed that
|
||||
outgoing messages are synchronously enqueued in the correct
|
||||
order, and then async calls can be returned which finish each
|
||||
send. If the entire call is async, they may be enqueued out of
|
||||
order in rare cases.
|
||||
"""
|
||||
assert self._send_async_raw_message_call is None
|
||||
self._send_async_raw_message_call = call
|
||||
return call
|
||||
|
||||
def send_async_ex_method(
|
||||
self, call: Callable[[Any, str, Message], Awaitable[str]]
|
||||
) -> Callable[[Any, str, Message], Awaitable[str]]:
|
||||
"""Function decorator for extended send-async method.
|
||||
|
||||
Version of send_async_method which is also is passed the original
|
||||
unencoded message; can be useful for cases where metadata is sent
|
||||
along with messages referring to their payloads/etc.
|
||||
"""
|
||||
assert self._send_async_raw_message_ex_call is None
|
||||
self._send_async_raw_message_ex_call = call
|
||||
return call
|
||||
|
||||
def encode_filter_method(
|
||||
self, call: Callable[[Any, Message, dict], None]
|
||||
) -> Callable[[Any, Message, dict], None]:
|
||||
@ -127,17 +150,34 @@ class MessageSender:
|
||||
),
|
||||
)
|
||||
|
||||
async def send_async(
|
||||
def send_async(
|
||||
self, bound_obj: Any, message: Message
|
||||
) -> Response | None:
|
||||
) -> Awaitable[Response | None]:
|
||||
"""Send a message asynchronously."""
|
||||
|
||||
# Note: This call is synchronous so that the first part of it can
|
||||
# happen synchronously. If the whole call were async we wouldn't be
|
||||
# able to guarantee that messages sent in order would actually go
|
||||
# out in order.
|
||||
raw_response_awaitable = self.fetch_raw_response_async(
|
||||
bound_obj=bound_obj,
|
||||
message=message,
|
||||
)
|
||||
# Now return an awaitable that will finish the send.
|
||||
return self._send_async_awaitable(
|
||||
bound_obj, message, raw_response_awaitable
|
||||
)
|
||||
|
||||
async def _send_async_awaitable(
|
||||
self,
|
||||
bound_obj: Any,
|
||||
message: Message,
|
||||
raw_response_awaitable: Awaitable[Response | SysResponse],
|
||||
) -> Response | None:
|
||||
return self.unpack_raw_response(
|
||||
bound_obj=bound_obj,
|
||||
message=message,
|
||||
raw_response=await self.fetch_raw_response_async(
|
||||
bound_obj=bound_obj,
|
||||
message=message,
|
||||
),
|
||||
raw_response=await raw_response_awaitable,
|
||||
)
|
||||
|
||||
def fetch_raw_response(
|
||||
@ -158,52 +198,95 @@ class MessageSender:
|
||||
bound_obj, msg_encoded
|
||||
)
|
||||
except Exception as exc:
|
||||
# Any error in the raw send call gets recorded as either
|
||||
# a local or communication error.
|
||||
return ErrorSysResponse(
|
||||
error_message=f'Error in MessageSender @send_method'
|
||||
f' ({type(exc)}): {exc}',
|
||||
response = ErrorSysResponse(
|
||||
error_message='Error in MessageSender @send_method.',
|
||||
error_type=(
|
||||
ErrorSysResponse.ErrorType.COMMUNICATION
|
||||
if isinstance(exc, CommunicationError)
|
||||
else ErrorSysResponse.ErrorType.LOCAL
|
||||
),
|
||||
)
|
||||
# Can include the actual exception since we'll be looking at
|
||||
# this locally; might be helpful.
|
||||
response.set_local_exception(exc)
|
||||
return response
|
||||
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||
|
||||
async def fetch_raw_response_async(
|
||||
def fetch_raw_response_async(
|
||||
self, bound_obj: Any, message: Message
|
||||
) -> Response | SysResponse:
|
||||
"""Fetch a raw message response.
|
||||
) -> Awaitable[Response | SysResponse]:
|
||||
"""Fetch a raw message response awaitable.
|
||||
|
||||
The result of this should be passed to unpack_raw_response() to
|
||||
produce the final message result.
|
||||
The result of this should be awaited and then passed to
|
||||
unpack_raw_response() to produce the final message result.
|
||||
|
||||
Generally you can just call send(); calling fetch and unpack
|
||||
manually is for when message sending and response handling need
|
||||
to happen in different contexts/threads.
|
||||
"""
|
||||
|
||||
if self._send_async_raw_message_call is None:
|
||||
# Note: This call is synchronous so that the first part of it can
|
||||
# happen synchronously. If the whole call were async we wouldn't be
|
||||
# able to guarantee that messages sent in order would actually go
|
||||
# out in order.
|
||||
if (
|
||||
self._send_async_raw_message_call is None
|
||||
and self._send_async_raw_message_ex_call is None
|
||||
):
|
||||
raise RuntimeError('send_async() is unimplemented for this type.')
|
||||
|
||||
msg_encoded = self._encode_message(bound_obj, message)
|
||||
try:
|
||||
response_encoded = await self._send_async_raw_message_call(
|
||||
bound_obj, msg_encoded
|
||||
)
|
||||
if self._send_async_raw_message_ex_call is not None:
|
||||
send_awaitable = self._send_async_raw_message_ex_call(
|
||||
bound_obj, msg_encoded, message
|
||||
)
|
||||
else:
|
||||
assert self._send_async_raw_message_call is not None
|
||||
send_awaitable = self._send_async_raw_message_call(
|
||||
bound_obj, msg_encoded
|
||||
)
|
||||
except Exception as exc:
|
||||
# Any error in the raw send call gets recorded as either
|
||||
# a local or communication error.
|
||||
return ErrorSysResponse(
|
||||
error_message=f'Error in MessageSender @send_async_method'
|
||||
f' ({type(exc)}): {exc}',
|
||||
return self._error_awaitable(exc)
|
||||
|
||||
# Now return an awaitable to finish the job.
|
||||
return self._fetch_raw_response_awaitable(
|
||||
bound_obj, message, send_awaitable
|
||||
)
|
||||
|
||||
async def _error_awaitable(self, exc: Exception) -> SysResponse:
|
||||
response = ErrorSysResponse(
|
||||
error_message='Error in MessageSender @send_async_method.',
|
||||
error_type=(
|
||||
ErrorSysResponse.ErrorType.COMMUNICATION
|
||||
if isinstance(exc, CommunicationError)
|
||||
else ErrorSysResponse.ErrorType.LOCAL
|
||||
),
|
||||
)
|
||||
# Can include the actual exception since we'll be looking at
|
||||
# this locally; might be helpful.
|
||||
response.set_local_exception(exc)
|
||||
return response
|
||||
|
||||
async def _fetch_raw_response_awaitable(
|
||||
self, bound_obj: Any, message: Message, send_awaitable: Awaitable[str]
|
||||
) -> Response | SysResponse:
|
||||
|
||||
try:
|
||||
response_encoded = await send_awaitable
|
||||
except Exception as exc:
|
||||
response = ErrorSysResponse(
|
||||
error_message='Error in MessageSender @send_async_method.',
|
||||
error_type=(
|
||||
ErrorSysResponse.ErrorType.COMMUNICATION
|
||||
if isinstance(exc, CommunicationError)
|
||||
else ErrorSysResponse.ErrorType.LOCAL
|
||||
),
|
||||
)
|
||||
# Can include the actual exception since we'll be looking at
|
||||
# this locally; might be helpful.
|
||||
response.set_local_exception(exc)
|
||||
return response
|
||||
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||
|
||||
def unpack_raw_response(
|
||||
@ -250,18 +333,14 @@ class MessageSender:
|
||||
self._decode_filter_call(
|
||||
bound_obj, message, response_dict, response
|
||||
)
|
||||
except Exception:
|
||||
# If we got to this point, we successfully communicated
|
||||
# with the other end so errors represent protocol mismatches
|
||||
# or other invalid data. For now let's just log it but perhaps
|
||||
# we'd want to somehow embed it in the ErrorSysResponse to be
|
||||
# available directly to the user later.
|
||||
logging.exception('Error decoding raw response')
|
||||
except Exception as exc:
|
||||
response = ErrorSysResponse(
|
||||
error_message='Error decoding raw response;'
|
||||
' see log for details.',
|
||||
error_message='Error decoding raw response.',
|
||||
error_type=ErrorSysResponse.ErrorType.LOCAL,
|
||||
)
|
||||
# Since we'll be looking at this locally, we can include
|
||||
# extra info for logging/etc.
|
||||
response.set_local_exception(exc)
|
||||
return response
|
||||
|
||||
def _unpack_raw_response(
|
||||
@ -282,16 +361,24 @@ class MessageSender:
|
||||
# Some error occurred. Raise a local Exception for it.
|
||||
if isinstance(raw_response, ErrorSysResponse):
|
||||
|
||||
# Errors that happened locally can attach their exceptions
|
||||
# here for extra logging goodness.
|
||||
local_exception = raw_response.get_local_exception()
|
||||
|
||||
if (
|
||||
raw_response.error_type
|
||||
is ErrorSysResponse.ErrorType.COMMUNICATION
|
||||
):
|
||||
raise CommunicationError(raw_response.error_message)
|
||||
raise CommunicationError(
|
||||
raw_response.error_message
|
||||
) from local_exception
|
||||
|
||||
# If something went wrong on *our* end of the connection,
|
||||
# don't say it was a remote error.
|
||||
if raw_response.error_type is ErrorSysResponse.ErrorType.LOCAL:
|
||||
raise RuntimeError(raw_response.error_message)
|
||||
raise RuntimeError(
|
||||
raw_response.error_message
|
||||
) from local_exception
|
||||
|
||||
# If they want to support clean errors, do those.
|
||||
if (
|
||||
@ -299,14 +386,18 @@ class MessageSender:
|
||||
and raw_response.error_type
|
||||
is ErrorSysResponse.ErrorType.REMOTE_CLEAN
|
||||
):
|
||||
raise CleanError(raw_response.error_message)
|
||||
raise CleanError(
|
||||
raw_response.error_message
|
||||
) from local_exception
|
||||
|
||||
if (
|
||||
self.protocol.forward_communication_errors
|
||||
and raw_response.error_type
|
||||
is ErrorSysResponse.ErrorType.REMOTE_COMMUNICATION
|
||||
):
|
||||
raise CommunicationError(raw_response.error_message)
|
||||
raise CommunicationError(
|
||||
raw_response.error_message
|
||||
) from local_exception
|
||||
|
||||
# Everything else gets lumped in as a remote error.
|
||||
raise RemoteError(
|
||||
@ -316,7 +407,7 @@ class MessageSender:
|
||||
if self._peer_desc_call is None
|
||||
else self._peer_desc_call(bound_obj)
|
||||
),
|
||||
)
|
||||
) from local_exception
|
||||
|
||||
assert isinstance(raw_response, Response)
|
||||
return raw_response
|
||||
@ -345,23 +436,23 @@ class BoundMessageSender:
|
||||
assert self._obj is not None
|
||||
return self._sender.send(bound_obj=self._obj, message=message)
|
||||
|
||||
async def send_async_untyped(self, message: Message) -> Response | None:
|
||||
def send_async_untyped(
|
||||
self, message: Message
|
||||
) -> Awaitable[Response | None]:
|
||||
"""Send a message asynchronously.
|
||||
|
||||
Whenever possible, use the send_async() call provided by generated
|
||||
subclasses instead of this; it will provide better type safety.
|
||||
"""
|
||||
assert self._obj is not None
|
||||
return await self._sender.send_async(
|
||||
bound_obj=self._obj, message=message
|
||||
)
|
||||
return self._sender.send_async(bound_obj=self._obj, message=message)
|
||||
|
||||
async def fetch_raw_response_async_untyped(
|
||||
def fetch_raw_response_async_untyped(
|
||||
self, message: Message
|
||||
) -> Response | SysResponse:
|
||||
) -> Awaitable[Response | SysResponse]:
|
||||
"""Split send (part 1 of 2)."""
|
||||
assert self._obj is not None
|
||||
return await self._sender.fetch_raw_response_async(
|
||||
return self._sender.fetch_raw_response_async(
|
||||
bound_obj=self._obj, message=message
|
||||
)
|
||||
|
||||
|
||||
@ -323,12 +323,12 @@ class RPCEndpoint:
|
||||
if self.debug_print:
|
||||
self.debug_print_call(f'{self._label}: finished.')
|
||||
|
||||
async def send_message(
|
||||
def send_message(
|
||||
self,
|
||||
message: bytes,
|
||||
timeout: float | None = None,
|
||||
close_on_error: bool = True,
|
||||
) -> bytes:
|
||||
) -> Awaitable[bytes]:
|
||||
"""Send a message to the peer and return a response.
|
||||
|
||||
If timeout is not provided, the default will be used.
|
||||
@ -340,7 +340,10 @@ class RPCEndpoint:
|
||||
respect to a given endpoint. Pass close_on_error=False to
|
||||
override this for a particular message.
|
||||
"""
|
||||
# pylint: disable=too-many-branches
|
||||
# Note: This call is synchronous so that the first part of it
|
||||
# (enqueueing outgoing messages) happens synchronously. If it were
|
||||
# a pure async call it could be possible for send order to vary
|
||||
# based on how the async tasks get processed.
|
||||
|
||||
if self.debug_print_io:
|
||||
self.debug_print_call(
|
||||
@ -358,16 +361,6 @@ class RPCEndpoint:
|
||||
f'{self._label}: have peerinfo? {self._peer_info is not None}.'
|
||||
)
|
||||
|
||||
# We need to know their protocol, so if we haven't gotten a handshake
|
||||
# from them yet, just wait.
|
||||
while self._peer_info is None:
|
||||
await asyncio.sleep(0.01)
|
||||
assert self._peer_info is not None
|
||||
|
||||
if self._peer_info.protocol == 1:
|
||||
if len(message) > 65535:
|
||||
raise RuntimeError('Message cannot be larger than 65535 bytes')
|
||||
|
||||
# message_id is a 16 bit looping value.
|
||||
message_id = self._next_message_id
|
||||
self._next_message_id = (self._next_message_id + 1) % 65536
|
||||
@ -420,8 +413,35 @@ class RPCEndpoint:
|
||||
if timeout is None:
|
||||
timeout = self.DEFAULT_MESSAGE_TIMEOUT
|
||||
assert timeout is not None
|
||||
|
||||
bytes_awaitable = msgobj.wait_task
|
||||
|
||||
# Now complete the send asynchronously.
|
||||
return self._send_message(
|
||||
message, timeout, close_on_error, bytes_awaitable, message_id
|
||||
)
|
||||
|
||||
async def _send_message(
|
||||
self,
|
||||
message: bytes,
|
||||
timeout: float | None,
|
||||
close_on_error: bool,
|
||||
bytes_awaitable: asyncio.Task[bytes],
|
||||
message_id: int,
|
||||
) -> bytes:
|
||||
|
||||
# We need to know their protocol, so if we haven't gotten a handshake
|
||||
# from them yet, just wait.
|
||||
while self._peer_info is None:
|
||||
await asyncio.sleep(0.01)
|
||||
assert self._peer_info is not None
|
||||
|
||||
if self._peer_info.protocol == 1:
|
||||
if len(message) > 65535:
|
||||
raise RuntimeError('Message cannot be larger than 65535 bytes')
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(msgobj.wait_task, timeout=timeout)
|
||||
return await asyncio.wait_for(bytes_awaitable, timeout=timeout)
|
||||
except asyncio.CancelledError as exc:
|
||||
# Question: we assume this means the above wait_for() was
|
||||
# cancelled; how do we distinguish between this and *us* being
|
||||
@ -449,7 +469,7 @@ class RPCEndpoint:
|
||||
)
|
||||
|
||||
# Stop waiting on the response.
|
||||
msgobj.wait_task.cancel()
|
||||
bytes_awaitable.cancel()
|
||||
|
||||
# Remove the record of this message.
|
||||
del self._in_flight_messages[message_id]
|
||||
@ -628,7 +648,8 @@ class RPCEndpoint:
|
||||
|
||||
# Now just sit and handle stuff as it comes in.
|
||||
while True:
|
||||
assert not self._closing
|
||||
if self._closing:
|
||||
return
|
||||
|
||||
# Read message type.
|
||||
mtype = _PacketType(await self._read_int_8())
|
||||
|
||||
@ -39,6 +39,7 @@ class _EmptyObj:
|
||||
pass
|
||||
|
||||
|
||||
# TODO: kill this and just use efro.call.tpartial
|
||||
if TYPE_CHECKING:
|
||||
Call = Call
|
||||
else:
|
||||
|
||||
@ -134,6 +134,7 @@ def _add_build_to_xcarchive(
|
||||
MODES['debug']['configuration'],
|
||||
'-archivePath',
|
||||
str(archivepathbase),
|
||||
'-allowProvisioningUpdates',
|
||||
]
|
||||
subprocess.run(args, check=True, capture_output=False)
|
||||
|
||||
|
||||
@ -162,6 +162,13 @@ def _spelling(words: list[str]) -> None:
|
||||
print(f'Modified {num_modded_dictionaries} dictionaries.')
|
||||
|
||||
|
||||
def pur() -> None:
|
||||
"""Run pur using project's Python version."""
|
||||
import subprocess
|
||||
|
||||
subprocess.run([sys.executable, '-m', 'pur'] + sys.argv[2:], check=True)
|
||||
|
||||
|
||||
def spelling_all() -> None:
|
||||
"""Add all misspellings from a pycharm run."""
|
||||
import subprocess
|
||||
|
||||
@ -45,6 +45,19 @@ def ignore_type_check_filter(if_node: nc.NodeNG) -> nc.NodeNG:
|
||||
and isinstance(if_node.parent, astroid.Module)
|
||||
):
|
||||
|
||||
# Special case: some third party modules are starting to contain
|
||||
# code that we don't handle cleanly which results in pylint runs
|
||||
# breaking. For now just ignoring them as they pop up.
|
||||
# We should try to figure out how to disable this filtering
|
||||
# for third party modules altogether or make our filtering more
|
||||
# robust.
|
||||
if if_node.parent.name in {
|
||||
'filelock',
|
||||
'aiohttp.web_app',
|
||||
'aiohttp.web_response',
|
||||
}:
|
||||
return if_node
|
||||
|
||||
module_node = if_node.parent
|
||||
|
||||
# Remove any locals getting defined under this if statement.
|
||||
@ -177,7 +190,7 @@ def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG:
|
||||
# Future behavior:
|
||||
# Annotated assigns under functions are not evaluated.
|
||||
# Class and module vars are normally not either. However we
|
||||
# do evaluate if we come across an 'ioprepped' dataclass
|
||||
# *do* evaluate if we come across an 'ioprepped' dataclass
|
||||
# decorator. (the 'ioprepped' decorator explicitly evaluates
|
||||
# dataclass annotations).
|
||||
|
||||
@ -230,7 +243,8 @@ def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG:
|
||||
break
|
||||
fnode = fnode.parent
|
||||
|
||||
# If this annotation won't be eval'ed, replace it with a dummy string.
|
||||
# If this annotation won't be eval'ed, replace its annotation with
|
||||
# a dummy value.
|
||||
if not willeval:
|
||||
dummyval = astroid.Const(parent=node, value='dummyval')
|
||||
node.annotation = dummyval
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user