diff --git a/.efrocachemap b/.efrocachemap index 19cce70e..b054ad5d 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -420,41 +420,41 @@ "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/3c/3f/dc185a4ea8f2512770558ce0ff90", - "assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/c5/81/e386a592e4fdf702b9089f106610", + "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/a8/a2/8c6816daac85d0b899a324509cf6", + "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/8e/4f/992f2a50dbe9514d774c1bb4fd28", + "assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/aa/ed/4bd02af3cffbd4c9c4be532fb1fe", "assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/c2/a4/993d5f0f30d0670d7053466929b6", "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/cb/df/f2d54d3146a159c678a47d3ff01b", + "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/7e/e7/836562182c2056902a326f5c7b9d", + "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/65/3f/6f1b739feac015dc39e97675b49d", + "assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/8e/90/3ba17e62e21919bc2fa7e46884be", "assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/5d/2a/a5363cbe713fd67ea0baaa44403a", - "assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/27/48/4cf2f4a3dd35260ef9b3b188ccde", - "assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/63/64/8073ab02f55940292099513d341b", + "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/b4/51/0f993429723480691d5289c7bc5d", + "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/72/35/c40aee4c2f7f64fa030f83eb08d5", - "assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/7c/38/d4a44c481757d355836f292ede48", + "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/66/32/92ae734c4358397d2e90c1c85325", "assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/db/14/2f441ccb65023775dc8139a63f3f", "assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/67/93/372c2a2428a830056e9ba22bbf95", - "assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/e6/fb/dfef0975c9c58cbcc621c022ec4b", + "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/a4/08/5e70701b63899cddf21733b0eea6", + "assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/b7/98/2523d11d09614bcfad8fa50be100", "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/20/a9/163d189884edf802636bf291e432", - "assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/e9/6d/62090311ae9aabaaae3918ea2d5b", + "assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/d8/19/21882f76375ade330ef5a6e791a4", "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/48/2c/78cb542498a59e3023b4838df515", + "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/c3/d2/d075ad07ec8d8aec3616f8760534", - "assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/73/7b/b8822b5632a8d3c7cf134e2859cc", + "assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/2a/0e/b164149e76efd0e6d591a9bf04bb", + "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", @@ -4008,50 +4008,50 @@ "assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e", "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/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/6f/3c/b6d4e92dcbac32aa3093d9139b9d", - "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a1/b8/31ad72ffe5df6bbedbd59eb3f02b", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d2/d9/084996631fa94337e0a92c93b753", - "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/89/0a/4483e91a808e4724dfbc382d6f60", - "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ec/4d/6c58d9a9c5b13837d87d3d871868", - "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/dd/12/2e3164bd4770a6c648e2e7dbc4f0", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fa/41/63a57ecf884cecf7ee1c8b9b2dcf", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/64/2b/0ebcdfd2f61027dcfff899d8894f", - "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/8f/e7/3ca7ed13d4a03013ecbb80ecb78c", - "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ec/e2/ebe20cc007e87d0ede6e0ec169af", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3a/77/32299795fdb8b0c0fbc3130b7193", - "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c2/1a/bfaeccd961429479947ea1cb2c47", - "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/43/74/b5749a746e72007f2f464b2941c6", - "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/eb/e2/f97727387a3f9707b079b7b2acd0", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/5f/da/6a048012e6b94a303e59b8dae61b", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f5/11/e75118e0a8f821e030f82e79c1bc", - "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d2/ad/4ee541d9d19b2d6fc1dab4947a26", - "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/10/2b/133d1cf611800e518d5353a10a94", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/85/53/fde7e925ac80da650a02ee078ad0", - "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/96/8e/c8292f6fc6967003affeb6ca32b7", - "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/28/f6/26327365481296a963bb37d58d82", - "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6d/b6/4e4a46d553d1949c956e72a3617d", - "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/81/57/d0a2b72a86d6f7f542d07e4fba29", - "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ac/f5/a6350f397911c2833c35fb86aa6c", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/91/87/78eab8de6f5bc52ff10f75e9d0b5", - "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a9/29/9cf3b260aae600ccafb535309b62", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/56/c2/75943b307817b0f23807b4f36632", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/93/51/6cf630d6ab82f8946569b96f01b5", - "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e4/ba/31a54b98463bc261835ac9cc7c51", - "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/64/25/bba77bfd767e14a9df46795c93bb", - "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f2/17/02a1053141eac72b24c323b10003", - "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/98/ca/dcbb87a7c32120a42ab6ca1e7194", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ca/dc/4a0fcdee7fc3c57960d5381a0299", - "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6c/a3/b70ad508a305efc2a10b5ba4508e", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/98/a9/1426b7eadc8be33f98f103fb12f0", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/38/8f/50753e457af4c626e2285c3171ac", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/fe/4d/77331e69f4fc0ef288df17377b42", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/9b/07/37e3a24a6ee588d0e5d4ba7b5335", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/0a/60/0109e5badaf8d6610cc9a4a61aa2", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/6d/e7/4e5bf2f3c16ca59770d5789c2481", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/b5/de/6914b0c6ab3142ad99547e930d88", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/3e/a4/e22b4cc12099dc6863a418d186f9", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/bd/c7/c97d29a73c75b3e699f14bcd6e37", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/44/36/de0a2f89ccd4e4728301cecd1033", + "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/b2/8a/31e68e8aeb4a440a98cdb37e688e", + "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7e/c2/ed0e710cd33e9a96c7f54fdf400a", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/62/35/37d0f605248971a7d270914c0e61", + "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/91/a1/5da5cec236aa862f7f92f89783cd", + "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/37/bb/b6bfe02bbd14a9c2d02fb782e4a9", + "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/05/38/a9adca329b4a147d58dd81a714ac", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2c/03/e562ae1a5cc7be268e1bf0ccc4c0", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c9/d5/066f05c777e200dd73084f0a1c25", + "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f7/6f/239efdb4e9e06cc0256f74da3aa8", + "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d7/77/48d470740edb84a271bfb18c2a16", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e1/2f/d8e39443dd972ea41fb0c241e8fd", + "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/32/9b/9d76eef13898e654b088698512d7", + "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/34/94/4abfe29e9b0443b292c53659a133", + "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/43/b1/2a8098e867f5d2a2cc086a4fb3f5", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/19/b6/0700636b968985ecc9ff1d3bd28e", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/db/ee/569470160da9dff657538c06ce7b", + "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/91/f9/6ec6bf4f8496ec4f71b3716db983", + "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/66/8a/7f50eede3fb6b6d7a655e4613f2e", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/d4/40/1121fb6089b698565347acec66f6", + "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/06/3b/0d289dd9a1170f750859e1c35b99", + "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f5/70/38d78023e0b99fdbe6ee29edd809", + "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/35/92/f2e39484904878e7eb9f5b3aff60", + "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6e/c5/6a81e0fc6027de9eca1c4a63143d", + "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/88/b5/e58450cec94d586f3d824f53c580", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/66/af/cf144d78359363ba027f2bd98a55", + "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9d/96/78f5d0d1e9249c71f018d84d9b24", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/58/d1/0f90ecfd6631b5476da915a76c55", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7c/bc/ec9ef218fb8065b58182493c981d", + "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4c/6b/b2a4eb26f043bb474e7e7e958470", + "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/64/e1/93f3afbc17ba6405cc970b559bb2", + "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/79/d2/f3b45d1aca8056c6cb951adc99ba", + "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4b/40/2ad8e00744d0b821e86fc42fc1f6", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8d/55/9876fecbe5d5076879b08d441665", + "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b4/f1/6c795ec50a26c24c734c5e0168b4", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7d/44/03c734fb003671a15182e8446a44", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7b/65/cadda8309ad9a4360a6930f05df8", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/41/a5/13590a172a2bf749bf419f67ce9f", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/76/26/0f89328b9e4554a5e656b1ead2d1", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/4b/da/b914c9e05be25d475155727046f4", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/c3/fb/78fe613f999fc6a9291c7141aeb8", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/f8/b2/cf697fd62a3f0bdbd248173551f0", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/d3/56/62b48ba16afb6023e730c9a42e75", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/4c/6e/009f37e5b619ce32e398b8039f62", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/26/03/2ee19e3130a7455efe5c9f78cd93", "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/25/77/8093dfffddaa80cd513ddaa61867", "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" diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 780866a1..7f276301 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -721,6 +721,7 @@ dummyret dummytoken dummyval + dumpminlog dups dval dxml @@ -1376,6 +1377,7 @@ levelmodule levelname levelno + levelnos levelstr lfull lfval @@ -1542,6 +1544,7 @@ mdiv mdocs mdocslines + mdpath mdpi megalint memfunctions @@ -1569,6 +1572,7 @@ minigame minigames miniplayer + minlog minping minusbutton minval @@ -2342,6 +2346,7 @@ shobs shortname shouldn + showbuffer showpoints showstats showsubseconds @@ -2869,6 +2874,7 @@ webpage webpages weeeird + whatarev whatevs whatisv wheee diff --git a/CHANGELOG.md b/CHANGELOG.md index a99f4fac..798e5152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -### 1.7.16 (build 20967, api 7, 2022-12-16) +### 1.7.17 (build 20973, api 7, 2022-12-20) +- 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. diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py index 89a6184f..c7b09622 100644 --- a/assets/src/ba_data/python/ba/_app.py +++ b/assets/src/ba_data/python/ba/_app.py @@ -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.""" @@ -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,11 +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_tracebacks + 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 @@ -402,7 +409,7 @@ 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. @@ -461,7 +468,7 @@ class App: ) # If any traceback dumps happened last run, log and clear them. - log_dumped_tracebacks() + log_dumped_app_state() self._launch_completed = True self._update_state() @@ -493,6 +500,7 @@ class App: self.cloud.on_app_pause() self.accounts_v1.on_app_pause() self.plugins.on_app_pause() + self.health_monitor.on_app_pause() else: # Leaving paused state: if self.state is self.State.PAUSED: @@ -501,6 +509,7 @@ class App: 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_login_completed and self._meta_scan_completed: self.state = self.State.RUNNING diff --git a/assets/src/ba_data/python/ba/_apputils.py b/assets/src/ba_data/python/ba/_apputils.py index 9633b078..024ad189 100644 --- a/assets/src/ba_data/python/ba/_apputils.py +++ b/assets/src/ba_data/python/ba/_apputils.py @@ -6,8 +6,12 @@ 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: @@ -266,22 +270,67 @@ def print_corrupt_file_error() -> None: _tbfiles: list[TextIO] = [] -def dump_tracebacks(delay: float) -> None: - """Dump a traceback of all Python threads after a delay in seconds. +@ioprepped +@dataclass +class DumpedAppStateMetadata: + """High level info about a dumped app state.""" - Can be used for debugging deadlock situations. Will dump to a preset - file location in the app config dir. Will attempt to log and clear - the results after dumping. It will be done at next launch otherwise, - or can be done explicitly via log_dumped_tracebacks(). + reason: str + app_time: float + log_level: LogLevel - Do not use this call during regular operation of the app; it is only - intended for debugging as it can leak file descriptors/etc. + +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 - tbpath = os.path.join(os.path.dirname(_ba.app.config_file_path), '_tbdump') + # 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 @@ -289,23 +338,132 @@ def dump_tracebacks(delay: float) -> None: tbfile = open(tbpath, 'w', encoding='utf-8') _tbfiles.append(tbfile) - faulthandler.dump_traceback_later(delay, file=tbfile) + if delay > 0.0: + faulthandler.dump_traceback_later(delay, file=tbfile) + else: + faulthandler.dump_traceback(file=tbfile) # Attempt to log shortly after dumping. - with _ba.Context('ui'): - _ba.timer(delay + 1.0, log_dumped_tracebacks, timetype=TimeType.REAL) + # 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_tracebacks() -> None: - """If a traceback dump exists, log it and clear it. No-op otherwise.""" +def log_dumped_app_state() -> None: + """If an app-state dump exists, log it and clear it. No-op otherwise.""" try: - tbpath = os.path.join( - os.path.dirname(_ba.app.config_file_path), '_tbdump' + out = '' + mdpath = os.path.join( + os.path.dirname(_ba.app.config_file_path), '_appstate_dump_md' ) - if os.path.exists(tbpath): - with open(tbpath, 'r', encoding='utf-8') as infile: - logging.info('Dumped tracebacks:\n%s', infile.read()) - os.unlink(tbpath) + 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 tracebacks.') + 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._bg_thread_main, daemon=True) + self._thread.start() + self._response = False + self._first_check = True + + def _bg_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 diff --git a/assets/src/ba_data/python/ba/_bootstrap.py b/assets/src/ba_data/python/ba/_bootstrap.py index 53091f8f..d7a037be 100644 --- a/assets/src/ba_data/python/ba/_bootstrap.py +++ b/assets/src/ba_data/python/ba/_bootstrap.py @@ -47,7 +47,7 @@ def bootstrap() -> None: # Give a soft warning if we're being used with a different binary # version than we expect. - expected_build = 20967 + expected_build = 20973 running_build: int = env['build_number'] if running_build != expected_build: print( diff --git a/assets/src/ba_data/python/ba/_store.py b/assets/src/ba_data/python/ba/_store.py index 2dfc07c1..d3274de5 100644 --- a/assets/src/ba_data/python/ba/_store.py +++ b/assets/src/ba_data/python/ba/_store.py @@ -53,7 +53,9 @@ def get_store_item_display_size(item_name: str) -> tuple[float, float]: if item_name.startswith('characters.'): return 340 * 0.6, 430 * 0.6 if item_name in ['pro', 'upgrades.pro', 'merch']: - return 650 * 0.9, 500 * 0.85 + return 650 * 0.9, 500 * ( + 0.72 if _ba.app.config.get('Merch Link') else 0.85 + ) if item_name.startswith('maps.'): return 510 * 0.6, 450 * 0.6 if item_name.startswith('icons.'): diff --git a/assets/src/ba_data/python/ba/internal.py b/assets/src/ba_data/python/ba/internal.py index 7cf9a06e..4b04a9a7 100644 --- a/assets/src/ba_data/python/ba/internal.py +++ b/assets/src/ba_data/python/ba/internal.py @@ -101,8 +101,8 @@ from ba._apputils import ( is_browser_likely_available, get_remote_app_name, should_submit_debug_info, - dump_tracebacks, - log_dumped_tracebacks, + dump_app_state, + log_dumped_app_state, ) from ba._benchmark import ( run_gpu_benchmark, @@ -334,6 +334,6 @@ __all__ = [ 'sign_out_v1', 'sign_in_v1', 'mark_config_dirty', - 'dump_tracebacks', - 'log_dumped_tracebacks', + 'dump_app_state', + 'log_dumped_app_state', ] diff --git a/assets/src/ba_data/python/bastd/ui/account/settings.py b/assets/src/ba_data/python/bastd/ui/account/settings.py index 149e89d0..ea43f5df 100644 --- a/assets/src/ba_data/python/bastd/ui/account/settings.py +++ b/assets/src/ba_data/python/bastd/ui/account/settings.py @@ -38,6 +38,8 @@ 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') @@ -57,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), @@ -73,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 @@ -190,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 @@ -253,10 +243,10 @@ class AccountSettingsWindow(ba.Window): False if gpgs_adapter is None else gpgs_adapter.is_back_end_active() ) - 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 = ( @@ -282,17 +272,17 @@ class AccountSettingsWindow(ba.Window): sign_in_button_space = 70.0 deprecated_space = 60 - show_game_service_button = self._signed_in and v1_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_what_is_v2 = self._signed_in and v1_account_type == 'V2' + show_what_is_v2 = self._v1_signed_in and v1_account_type == 'V2' - show_linked_accounts_text = self._signed_in + show_linked_accounts_text = self._v1_signed_in linked_accounts_text_space = 60.0 - show_achievements_button = self._signed_in and v1_account_type in ( + show_achievements_button = self._v1_signed_in and v1_account_type in ( 'Google Play', 'Local', 'V2', @@ -300,33 +290,33 @@ 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_gpgs + 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 v1_account_type == 'V2' + 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 ( + 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 @@ -334,10 +324,12 @@ class AccountSettingsWindow(ba.Window): show_unlink_accounts_button = show_link_accounts_button unlink_accounts_button_space = 90.0 - show_v2_link_info = self._signed_in and not show_link_accounts_button + show_v2_link_info = self._v1_signed_in and not show_link_accounts_button v2_link_info_space = 70.0 - show_sign_out_button = self._signed_in and v1_account_type in [ + legacy_unlink_button_space = 120.0 + + show_sign_out_button = self._v1_signed_in and v1_account_type in [ 'Local', 'Google Play', 'V2', @@ -394,6 +386,8 @@ class AccountSettingsWindow(ba.Window): 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: @@ -460,7 +454,7 @@ class AccountSettingsWindow(ba.Window): v_align='center', autoselect=True, selectable=True, - on_activate_call=ba.WeakCall(self._on_what_is_v2_press), + on_activate_call=show_what_is_v2_page, click_activate=True, ) if first_selectable is None: @@ -1046,6 +1040,35 @@ class AccountSettingsWindow(ba.Window): 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( @@ -1153,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: @@ -1166,12 +1189,21 @@ 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 @@ -1297,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) @@ -1499,3 +1531,9 @@ 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') diff --git a/assets/src/ba_data/python/bastd/ui/mainmenu.py b/assets/src/ba_data/python/bastd/ui/mainmenu.py index a3d67738..b4369f0a 100644 --- a/assets/src/ba_data/python/bastd/ui/mainmenu.py +++ b/assets/src/ba_data/python/bastd/ui/mainmenu.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/ui/store/browser.py b/assets/src/ba_data/python/bastd/ui/store/browser.py index 3582629f..22977e4c 100644 --- a/assets/src/ba_data/python/bastd/ui/store/browser.py +++ b/assets/src/ba_data/python/bastd/ui/store/browser.py @@ -907,7 +907,11 @@ class StoreBrowserWindow(ba.Window): dummy_name ) section['v_spacing'] = ( - -17 if self._tab == 'characters' else 0 + -25 + if self._tab == 'extras' + else -17 + if self._tab == 'characters' + else 0 ) if 'title' not in section: section['title'] = '' @@ -919,7 +923,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 diff --git a/assets/src/ba_data/python/bastd/ui/store/item.py b/assets/src/ba_data/python/bastd/ui/store/item.py index 078f3c9c..6c8b827e 100644 --- a/assets/src/ba_data/python/bastd/ui/store/item.py +++ b/assets/src/ba_data/python/bastd/ui/store/item.py @@ -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 @@ -170,11 +174,11 @@ def instantiate_store_item_display( ) if item_name == 'merch': - frame_size = b_width * 0.7 + 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.5 - im_dim * 0.5, + b_pos[1] + b_height * 0.47 - im_dim * 0.5, ) ba.imagewidget( parent=parent_widget, diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index d5ef5e70..45705ff7 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -388,6 +388,7 @@ dummyret dummyval dummyvalid + dumpminlog dval dxgi dynamicdata @@ -732,6 +733,7 @@ leaderboards lenval levelno + levelnos levelstr lgui lhalf @@ -807,6 +809,7 @@ maxtries maxwait maxwidth + mdpath mediump memalign memchr @@ -824,6 +827,7 @@ millisecs minelem miniplayer + minlog minping minsdl mipmapcount @@ -1245,6 +1249,7 @@ shhh shifthigh shouldnt + showbuffer shufflable signsubscale sigsetjmp @@ -1518,6 +1523,7 @@ weeeird welp whaaaaaaa + whatarev whatisv wheee wheeee diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index 51a2e010..7ed2cdc6 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -32,8 +32,8 @@ namespace ballistica { // These are set automatically via script; don't modify them here. -const int kAppBuildNumber = 20967; -const char* kAppVersion = "1.7.16"; +const int kAppBuildNumber = 20973; +const char* kAppVersion = "1.7.17"; // Our standalone globals. // These are separated out for easy access. diff --git a/src/ballistica/ui/widget/container_widget.cc b/src/ballistica/ui/widget/container_widget.cc index 5d82083b..5cdd262e 100644 --- a/src/ballistica/ui/widget/container_widget.cc +++ b/src/ballistica/ui/widget/container_widget.cc @@ -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_; diff --git a/tools/efro/log.py b/tools/efro/log.py index 6ee2e896..b4ea9649 100644 --- a/tools/efro/log.py +++ b/tools/efro/log.py @@ -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,