diff --git a/.efrocachemap b/.efrocachemap index 27400acf..0f40217f 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -420,7 +420,7 @@ "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/7d/d4/6a32da2a6a5d1f8d71f65ac65792", + "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/36/57/76a60b261de061886357dbf2c0db", "assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/e2/24/5e7ea9ca5c9de4d3b7a28e53564d", "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/f8/15/e1a2fa38697417bcf2cf19cd34ef", @@ -454,7 +454,7 @@ "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/9a/90/8e2ed626def09f88c3b9ab5215a3", "assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/ab/35/644e4239cfa62a597a905412b90c", - "assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/53/9e/068074156b38bab7f732977a4031", + "assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/22/8e/3fccd3a8c9761c9e60ee4f5ecd85", "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", @@ -3995,50 +3995,50 @@ "assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e", "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34", "ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a", - "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/b4/b2/d9d81b227c329f77198d96ee2ae1", - "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/06/0d/c5503c7e9b4e8c5465b7df36c3ab", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2e/57/6c8e84496af8bcfc60b6030f9008", - "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f1/37/e421b0f64743dd33ff9e3db54838", - "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/3a/83/153f01e88ee01db4e2dd234ce189", - "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/0f/9b/66b3b2c089137e81496868f9c829", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/5a/2e/0b387964a1cbe7658f353b0c3d58", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/77/52/ae35cb8cfea02c60417968702b3f", - "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/cf/55/d502cfa9bef1142b7cc240759c07", - "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c7/59/4d6c76a8e57478a81804b446cec7", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ed/5c/68871f87dd6353ca6693b24c895c", - "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/69/34/d573542acf00286337a8a7d4070e", - "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/54/21/ad891fc900eb06eb307401887479", - "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/10/6d/b079707d97db261651539cf42be4", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9f/ec/d6f090057a354d6f27778a79151d", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2f/8d/7076055efd990269b20af15d092a", - "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/b6/9b/04419fdb2911f107ba2cf3d1daef", - "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/89/c0/d3b177978d11d0283b0290e88960", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/32/23/ce5919233ff0438442f6e6e66a62", - "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/99/51/13dbd2e177dd17314b4bc86fe200", - "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e2/88/53757bc9fd92d49bd35dc6d3be0e", - "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/06/69/078f7eef49a1127a1492db4703f6", - "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/82/f1/2b13fe77164f72d2bf57453bb8e5", - "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6a/b0/a853b61ab794706bbf395ecd2a80", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/76/3d/b0d2913a1650bdc35b2ca0d81154", - "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cb/b3/39a0642a376e1f131172f9500353", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7a/07/4804a222e0d92f0fab8b279ce4c1", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/55/09/bf8e7d6ce41962163411c6bbd884", - "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1d/be/5e0b2be7272c4e443cc974d5b182", - "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5e/ab/075e9137d21e6110d29b67210533", - "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/54/2a/c5e1d5ed4328c40821695db2cd84", - "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b7/dd/55210b3f3c075d9b237e0c6aa733", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a2/00/d01842e8b0777f7e6ea47c912b16", - "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a6/91/f9cb15d0876750e28abe3b0d221c", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b7/11/1ecfe322ae997772b71538664cad", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8d/d6/1e83dba73d581cfb2b2f6eb31f22", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/50/6a/ca5e49b3cad047b541648dc9914d", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/6f/f7/b38282cfbd3cb0cda89d5a458176", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/97/e5/1d2e76fcadbe022d4c522c7d2135", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/54/d6/2d642477837c34c946b70e27014d", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/79/2b/f17b81bdf03719098a558305f0a3", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/9a/5a/3ca5679187f8dd5f89d8bb68ed84", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/4b/88/d016d4059ea7b334e95d1e6ef258", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/1b/35/b11d851fc912b7bcd57766981fa2", + "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/7c/40/4d11b867dbfdab90a1601f31b792", + "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c1/64/71a9087e53146cc5377135b42b73", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/32/bd/f82f8059a47fd84e5d86d8814ecb", + "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/68/a5/24e48c50d5325869c9ce56b691ba", + "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f5/0b/9694a6942b8f6580eb041040b7f4", + "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a8/53/ab21700e2be712f046b6c11e6061", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a5/bd/6a29dc921daec7c8508fbf17b24a", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/24/71/80d7091f262bff97ff18a4bb8948", + "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f1/0e/c3738bcfce8a0ff51140f95ec9a5", + "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d8/7e/3ecd464aa666da920625a8336ccf", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c4/12/244ec02b1e210441832b9265d96b", + "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1a/42/7ff1c8244886f3a8a3772ffe82e1", + "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/25/50/b37e83853bc4a5549686f79d4346", + "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/fa/80/4e12c28bd617bcee0b3402f6bfcc", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/dd/64/9c8a9a811af36110da0d875ef694", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/43/ba/6d7956bc670c75758e74615a9f3f", + "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/7c/29/68fdb792ec55c6ba404b09bba49c", + "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/bb/1b/2d3c1944086c8e70fe396aeb73ff", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/b4/e5/a46580be7e047d2f0d12587b3e30", + "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/f5/a5/98b120cfcc070679e032ee684ca3", + "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9d/a6/c963859c531bb19f507f405cf589", + "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/df/dc/79349a169d3b00964d9f35853f84", + "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a7/63/d057e19cb7806302b9570b91c573", + "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/17/c0/3da5a81581aa9275b6c32bb03fc8", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3f/84/d66202e8a15d5518f876dccdd6d6", + "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5a/ba/3a6f95b9e4a9c310ac63d0bc0a8c", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2f/58/9462e34601e2f442eb429185ee35", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/50/09/6e8feb718cb60ea80ecaab4ba9a0", + "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7d/a3/f8fd8ad1037e5f5b47b72a6d1edf", + "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9d/c6/3902a717b71f9f8781d724c8ca23", + "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e4/5a/3b25cbbca51ef2a6036d8d1805aa", + "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fd/11/f0fb88f01753350f88f068b6c6a0", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e4/22/d3700b99ec02b9bd99b8801ad69b", + "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8b/d8/4b2e840ace5be8dd8fc9d6841cdd", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/46/07/545eec0e6bde25bba8b3857d7e9b", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b4/ae/d9a2b38dc9824ac6acc79d520404", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/4a/d3/b01764e89eafd5b9a3ffc2e2e540", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/9a/0e/ce412ed759cb5275b647f19b33ff", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/96/6d/d2aa5bbb9d9f2d3f5fd51042d50b", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/bc/ed/250948d00952c6e690e3e24511f6", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/59/2e/2b4b7140eb237e6ce00b7ec975db", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/7f/0a/1ee8835881fb9af7a3c3576de59a", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/b6/3d/6a800860c35bd41f4aad2199b10f", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/d3/7d/3fee0a9e0b2d4ad3601722def1c8", "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/c0/32/b7907e3859a5c5013a3d97b6b523", "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 17bdf891..7d8742fa 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -153,6 +153,7 @@ assigninput astc astcenc + astr astroid asus asynchat @@ -261,6 +262,8 @@ blocksize bluetooth bmag + bmas + bmasl bname bndl boffs @@ -343,6 +346,7 @@ campaignname cancelbtn capb + caplog capturetheflag carentity cashregistersound @@ -604,6 +608,8 @@ depsets depsval dereferencing + deregistering + deregistration descpos desctype dest @@ -679,6 +685,7 @@ dropdir drumroll dsqlite + dstabs dstattr dstbase dstdata @@ -1725,6 +1732,7 @@ nvidia nyko obj's + objb objid objname objs @@ -2375,6 +2383,7 @@ splitlen splitnumstr squadcore + srcabs srcattr srcdata srcdir @@ -2498,6 +2507,7 @@ sysconfigdata sysctl syslogmodule + sysresponse tabdefs tabtype tabtypes @@ -2594,6 +2604,7 @@ this'll thislinelen thismodule + threadlocals threadpool threadtype throwiness @@ -2925,6 +2936,7 @@ zlib zlibmodule zoneid + zoneids zoneinfo zoomtext zpings diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b9923e8..17c25573 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ -### 1.7.10 (build 20887, api 7, 2022-09-29) +### 1.7.10 (build 20889, api 7, 2022-10-07) - Added eval support for cloud-console. This means you can type something like '1+1' in the console and see '2' printed. This is how Python behaves in the stdin console or in-game console or the standard Python interpreter. - Exceptions in the cloud-console now print to stderr instead of logging.exception(). This means they aren't a pretty red color anymore, but this will keep cloud-console behaving well with things like servers where logging.exception() might trigger alarms or otherwise. This is also consistent with standard interactive Python behavior. - Cloud console now shows the device name at the top instead of simply 'Console' while connected. - Moved the function that actually runs cloud console code to `ba._cloud.cloud_console_exec()`. - Added efro.debug which contains useful functionality for debugging object reference issues and memory leaks on live app instances (via cloud shell or whatever). +- Lots of reworking/polishing in general on communication between the game and v2 regional/master servers in preparation of upgrading Google Play accounts to V2. Please holler if anything is not working smoothly with a V2 account. +- When establishing V2 master-server communication, if the closest regional server is down or too busy, will now fall back to farther ones instead of giving up. You can follow this process by setting env var `BA_DEBUG_PRINT_V2_TRANSPORT` to 1 when running the app. +- Network testing now skips the alternate v1 master server addr if the primary succeeded. The alternate often fails which makes things look broken even though the game is ok as long as primary works. +- The v2-transport system will now properly reestablish account connectivity when asked to refresh its connection (the cloud does this periodically so regional cloud servers can be restarted as needed). Practically this means your app won't stop showing up under the ballistica.net devices section after its been running for a while; a problem previous builds had. +- The v2-transport system can now establish more than one connection at a time, which allows the app to gracefully transition to a new connection when the old is about to expire without any period of no connectivity. To test this functionality, set env var `BA_DEBUG_PRINT_V2_TRANSPORT=1` to see transport debug messages and `BA_DEBUG_V2_TRANSPORT_SHORT_DURATION=1` to cause the cloud to request a connection-refresh every 30 seconds or so. +- V2 accounts now consider themselves instantly signed in if they were signed in when the app last ran. They still need to contact the master-server before anything important can happen, but this should help keep things feel faster in general. +- Due to v2-transport improvements, pressing the 'End Session Now' button in ballistica.net account settings should now instantly log you out of all apps using that session. ### 1.7.9 (build 20880, api 7, 2022-09-24) - Cleaned up the efro.message system to isolate response types that are used purely internally (via a new SysResponse type). diff --git a/assets/src/ba_data/python/ba/_accountv2.py b/assets/src/ba_data/python/ba/_accountv2.py index b5758ff2..4d34af45 100644 --- a/assets/src/ba_data/python/ba/_accountv2.py +++ b/assets/src/ba_data/python/ba/_accountv2.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING import _ba if TYPE_CHECKING: - pass + from typing import Any class AccountV2Subsystem: @@ -111,10 +111,21 @@ class AccountV2Subsystem: class AccountV2Handle: - """Handle for interacting with a v2 account.""" + """Handle for interacting with a V2 account. + + This class supports the 'with' statement, which is how it is + used with some operations such as cloud messaging. + """ def __init__(self) -> None: self.tag = '?' self.workspacename: str | None = None self.workspaceid: str | None = None + + def __enter__(self) -> None: + """Support for "with" statement. + """ + + def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: + """Support for "with" statement.""" diff --git a/assets/src/ba_data/python/ba/_bootstrap.py b/assets/src/ba_data/python/ba/_bootstrap.py index 6fe2b550..f91e53e2 100644 --- a/assets/src/ba_data/python/ba/_bootstrap.py +++ b/assets/src/ba_data/python/ba/_bootstrap.py @@ -45,7 +45,7 @@ def bootstrap() -> None: # Give a soft warning if we're being used with a different binary # version than we expect. - expected_build = 20887 + expected_build = 20889 running_build: int = env['build_number'] if running_build != expected_build: print( diff --git a/assets/src/ba_data/python/bastd/ui/mainmenu.py b/assets/src/ba_data/python/bastd/ui/mainmenu.py index 39f38371..52919c19 100644 --- a/assets/src/ba_data/python/bastd/ui/mainmenu.py +++ b/assets/src/ba_data/python/bastd/ui/mainmenu.py @@ -56,10 +56,11 @@ class MainMenuWindow(ba.Window): self._gather_button: ba.Widget | None = None self._start_button: ba.Widget | None = None self._watch_button: ba.Widget | None = None - self._gc_button: ba.Widget | None = None + self._account_button: ba.Widget | None = None self._how_to_play_button: ba.Widget | None = None self._credits_button: ba.Widget | None = None self._settings_button: ba.Widget | None = None + self._next_refresh_allow_time = 0.0 self._store_char_tex = self._get_store_char_tex() @@ -71,7 +72,7 @@ class MainMenuWindow(ba.Window): self._account_state_num = ba.internal.get_v1_account_state_num() self._account_type = (ba.internal.get_v1_account_type() if self._account_state == 'signed_in' else None) - self._refresh_timer = ba.Timer(1.0, + self._refresh_timer = ba.Timer(0.27, ba.WeakCall(self._check_refresh), repeat=True, timetype=ba.TimeType.REAL) @@ -132,11 +133,15 @@ class MainMenuWindow(ba.Window): if not self._root_widget: return + now = ba.time(ba.TimeType.REAL) + if now < self._next_refresh_allow_time: + return + # Don't refresh for the first few seconds the game is up so we don't # interrupt the transition in. - ba.app.main_menu_window_refresh_check_count += 1 - if ba.app.main_menu_window_refresh_check_count < 4: - return + # ba.app.main_menu_window_refresh_check_count += 1 + # if ba.app.main_menu_window_refresh_check_count < 4: + # return store_char_tex = self._get_store_char_tex() account_state_num = ba.internal.get_v1_account_state_num() @@ -251,11 +256,11 @@ class MainMenuWindow(ba.Window): size=(self._button_width, self._button_height), autoselect=self._use_autoselect, label=ba.Lstr(resource=self._r + - ('.endTestText' if self._is_benchmark() - else '.endGameText')), + ('.endTestText' if self._is_benchmark( + ) else '.endGameText')), on_activate_call=(self._confirm_end_test - if self._is_benchmark() - else self._confirm_end_game)) + if self._is_benchmark() else + self._confirm_end_game)) # Assume we're in a client-session. else: ba.buttonwidget( @@ -428,6 +433,7 @@ 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 ba.app.did_menu_intro = True self._width = 400.0 self._height = 200.0 @@ -609,7 +615,7 @@ class MainMenuWindow(ba.Window): this_b_width = self._button_width h, v, scale = positions[self._p_index] self._p_index += 1 - self._gc_button = ba.buttonwidget( + self._account_button = ba.buttonwidget( parent=self._root_widget, position=(h - this_b_width * 0.5 * scale, v), size=(this_b_width, self._button_height), @@ -637,7 +643,7 @@ class MainMenuWindow(ba.Window): tilt_scale=0.0) self._tdelay += self._t_delay_inc else: - self._gc_button = None + self._account_button = None # How-to-play button. h, v, scale = positions[self._p_index] @@ -852,7 +858,7 @@ class MainMenuWindow(ba.Window): ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.set_main_menu_window( AccountSettingsWindow( - origin_widget=self._gc_button).get_root_widget()) + origin_widget=self._account_button).get_root_widget()) def _on_store_pressed(self) -> None: # pylint: disable=cyclic-import @@ -870,7 +876,7 @@ class MainMenuWindow(ba.Window): def _is_benchmark(self) -> bool: session = ba.internal.get_foreground_host_session() return (getattr(session, 'benchmark_type', None) == 'cpu' - or ba.app.stress_test_reset_timer is not None) + or ba.app.stress_test_reset_timer is not None) def _confirm_end_game(self) -> None: # pylint: disable=cyclic-import @@ -984,8 +990,8 @@ class MainMenuWindow(ba.Window): ba.app.ui.main_menu_selection = 'Credits' elif sel == self._settings_button: ba.app.ui.main_menu_selection = 'Settings' - elif sel == self._gc_button: - ba.app.ui.main_menu_selection = 'GameService' + elif sel == self._account_button: + ba.app.ui.main_menu_selection = 'Account' elif sel == self._store_button: ba.app.ui.main_menu_selection = 'Store' elif sel == self._quit_button: @@ -1016,8 +1022,8 @@ class MainMenuWindow(ba.Window): sel = self._credits_button elif sel_name == 'Settings': sel = self._settings_button - elif sel_name == 'GameService': - sel = self._gc_button + elif sel_name == 'Account': + sel = self._account_button elif sel_name == 'Store': sel = self._store_button elif sel_name == 'Quit': diff --git a/assets/src/ba_data/python/bastd/ui/settings/nettesting.py b/assets/src/ba_data/python/bastd/ui/settings/nettesting.py index 9ea4c9b5..1f6032be 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/nettesting.py +++ b/assets/src/ba_data/python/bastd/ui/settings/nettesting.py @@ -146,13 +146,14 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: ba.pushcall(_print_in_logic_thread, from_other_thread=True) - def _print_test_results(call: Callable[[], Any]) -> None: - """Run the provided call; return success/fail text & color.""" + def _print_test_results(call: Callable[[], Any]) -> bool: + """Run the provided call, print result, & return success.""" starttime = time.monotonic() try: call() duration = time.monotonic() - starttime _print(f'Succeeded in {duration:.2f}s.', color=(0, 1, 0)) + return True except Exception as exc: import traceback duration = time.monotonic() - starttime @@ -161,6 +162,7 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: _print(msg, color=(1.0, 1.0, 0.3)) _print(f'Failed in {duration:.2f}s.', color=(1, 0, 0)) have_error[0] = True + return False try: _print(f'Running network diagnostics...\n' @@ -177,14 +179,23 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: # V1 ping baseaddr = ba.internal.get_master_server_address(source=0, version=1) _print(f'\nContacting V1 master-server src0 ({baseaddr})...') - _print_test_results(lambda: _test_fetch(baseaddr)) + v1worked = _print_test_results(lambda: _test_fetch(baseaddr)) - # V1 alternate ping - baseaddr = ba.internal.get_master_server_address(source=1, version=1) - _print(f'\nContacting V1 master-server src1 ({baseaddr})...') - _print_test_results(lambda: _test_fetch(baseaddr)) + # V1 alternate ping (only if primary fails since this often fails). + if v1worked: + _print('\nSkipping V1 master-server src1 test since src0 worked.') + else: + baseaddr = ba.internal.get_master_server_address(source=1, + version=1) + _print(f'\nContacting V1 master-server src1 ({baseaddr})...') + _print_test_results(lambda: _test_fetch(baseaddr)) - _print(f'\nV1-test-log: {ba.app.net.v1_test_log}') + if 'none succeeded' in ba.app.net.v1_test_log: + _print(f'\nV1-test-log failed: {ba.app.net.v1_test_log}', + color=(1, 0, 0)) + have_error[0] = True + else: + _print(f'\nV1-test-log ok: {ba.app.net.v1_test_log}') for srcid, result in sorted(ba.app.net.v1_ctest_results.items()): _print(f'\nV1 src{srcid} result: {result}') diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index ad6d8d05..c38c0473 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -79,6 +79,7 @@ asci assetsmakefile assigninput + astr atest athome attrobj @@ -144,6 +145,8 @@ blockwidth bluetooth blurscale + bmas + bmasl bname bodyid bodypart @@ -197,6 +200,7 @@ camerashake cancelbtn capitan + caplog cargs cbegin cbgn @@ -329,6 +333,8 @@ demangled demangling denom + deregistering + deregistration dernit desctype destdir @@ -364,6 +370,7 @@ drpt dsize dsound + dstabs dstattr dstnode dstpath @@ -906,6 +913,7 @@ nval nvidia nyffenegger + objb objexists objid objtoyaml @@ -1234,6 +1242,7 @@ sphrand spinup spivak + srcabs srcattr srcfolder srcid @@ -1303,6 +1312,7 @@ swiftc symbolification syscalls + sysresponse tabdefs tabtype tabtypes @@ -1335,6 +1345,7 @@ theres thislinelen thismodule + threadlocals threadname threadpool threadtype @@ -1522,6 +1533,7 @@ zoffset zomg zoneid + zoneids zoneinfo zoomable zpings diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index 99446c21..623efecd 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -32,7 +32,7 @@ namespace ballistica { // These are set automatically via script; don't modify them here. -const int kAppBuildNumber = 20887; +const int kAppBuildNumber = 20889; const char* kAppVersion = "1.7.10"; // Our standalone globals. diff --git a/src/ballistica/logic/v1_account.cc b/src/ballistica/logic/v1_account.cc index 399211cc..7a9fa184 100644 --- a/src/ballistica/logic/v1_account.cc +++ b/src/ballistica/logic/v1_account.cc @@ -152,6 +152,8 @@ void V1Account::SetLogin(V1AccountType account_type, V1LoginState login_state, // We call out to Python so need to be in logic thread. assert(InLogicThread()); + + // We want redundant sets to be no-ops. if (login_state_ != login_state || g_app->account_type != account_type || login_id_ != login_id || login_name_ != login_name) { // Special case: if they sent a sign-out for an account type that is diff --git a/tests/test_efro/test_message.py b/tests/test_efro/test_message.py index a88e90bb..84f3fce7 100644 --- a/tests/test_efro/test_message.py +++ b/tests/test_efro/test_message.py @@ -1,10 +1,12 @@ # Released under the MIT License. See LICENSE for details. # """Testing message functionality.""" +# pylint: disable=too-many-lines from __future__ import annotations import os +import logging import asyncio from typing import TYPE_CHECKING, overload from dataclasses import dataclass @@ -198,7 +200,7 @@ class _TestMessageSenderBBoth(MessageSender): """Protocol-specific sender.""" def __init__(self) -> None: - protocol = TEST_PROTOCOL_B + protocol = TEST_PROTOCOL_EVOLVED super().__init__(protocol) def __get__(self, @@ -427,12 +429,14 @@ TEST_PROTOCOL = MessageProtocol( 1: _TResp2, }, forward_clean_errors=True, + forward_communication_errors=True, remote_errors_include_stack_traces=True, ) -# Represents an 'evolved' TEST_PROTOCOL (one extra message type added). -# (so we can test communication failures talking to older protocols) -TEST_PROTOCOL_B = MessageProtocol( +# Represents an 'evolved' TEST_PROTOCOL (the same as TEST_PROTOCOL; just +# one extra message type added). +# This way we can test communication failures talking to older protocols. +TEST_PROTOCOL_EVOLVED = MessageProtocol( message_types={ 0: _TMsg1, 1: _TMsg2, @@ -444,6 +448,7 @@ TEST_PROTOCOL_B = MessageProtocol( 1: _TResp2, }, forward_clean_errors=True, + forward_communication_errors=True, remote_errors_include_stack_traces=True, ) @@ -581,9 +586,9 @@ def test_sender_module_both_emb() -> None: # here, but it requires us to pass code which imports this test module # to get at the protocol, and that currently fails in our static mypy # tests. - smod = TEST_PROTOCOL_B.do_create_sender_module( + smod = TEST_PROTOCOL_EVOLVED.do_create_sender_module( 'TestMessageSenderBBoth', - protocol_create_code='protocol = TEST_PROTOCOL_B', + protocol_create_code='protocol = TEST_PROTOCOL_EVOLVED', enable_sync_sends=True, enable_async_sends=True, private=True, @@ -741,7 +746,7 @@ def test_receiver_creation() -> None: receiver.validate() -def test_full_pipeline() -> None: +def test_full_pipeline(caplog: pytest.LogCaptureFixture) -> None: """Test the full pipeline.""" # pylint: disable=too-many-locals @@ -816,6 +821,45 @@ def test_full_pipeline() -> None: if self.test_sidecar: setattr(response, '_sidecar_data', indata['_sidecar_data']) + # Alternate sender for testing other protocol options. + class TestClassSAlt: + """Test class incorporating send functionality.""" + + msg = _TestMessageSenderSingle() + + test_handling_unregistered = False + test_send_method_exceptions = False + test_send_method_exceptions_comm = False + + def __init__(self, target: TestClassRAlt) -> None: + self.test_sidecar = False + self._target = target + + @msg.send_method + def _send_raw_message(self, data: str) -> str: + """Handle synchronous sending of raw json message data.""" + + # Test throwing exceptions in send methods. + if self.test_send_method_exceptions: + raise (CommunicationError() + if self.test_send_method_exceptions_comm else + RuntimeError()) + + # Just talk directly to the receiver for this example. + # (currently only support synchronous receivers) + assert isinstance(self._target, TestClassRAlt) + try: + return self._target.receiver.handle_raw_message( + data, raise_unregistered=self.test_handling_unregistered) + except UnregisteredMessageIDError: + if self.test_handling_unregistered: + # Emulate forwarding unregistered messages on to some + # other handler... + response_dict = self.msg.protocol.response_to_dict( + EmptySysResponse()) + return self.msg.protocol.encode_dict(response_dict) + raise + class TestClassRSync: """Test class incorporating synchronous receive functionality.""" @@ -831,6 +875,8 @@ def test_full_pipeline() -> None: raise CleanError('Testing Clean Error') if msg.ival == 2: raise RuntimeError('Testing Runtime Error') + if msg.ival == 3: + raise CommunicationError('Testing Communication Error') out = _TResp1(bval=True) if self.test_sidecar: setattr(out, '_sidecar_data', getattr(msg, '_sidecar_data')) @@ -864,6 +910,30 @@ def test_full_pipeline() -> None: receiver.validate() + class TestClassRAlt: + """Test class incorporating synchronous receive functionality.""" + + receiver = _TestSingleMessageReceiver() + + def __init__(self) -> None: + self.test_sidecar = False + + @receiver.handler + def handle_test_message_1(self, msg: _TMsg1) -> _TResp1: + """Test.""" + if msg.ival == 1: + raise CleanError('Testing Clean Error') + if msg.ival == 2: + raise RuntimeError('Testing Runtime Error') + if msg.ival == 3: + raise CommunicationError('Testing Communication Error') + out = _TResp1(bval=True) + if self.test_sidecar: + setattr(out, '_sidecar_data', getattr(msg, '_sidecar_data')) + return out + + receiver.validate() + class TestClassRAsync: """Test class incorporating asynchronous receive functionality.""" @@ -876,6 +946,8 @@ def test_full_pipeline() -> None: raise CleanError('Testing Clean Error') if msg.ival == 2: raise RuntimeError('Testing Runtime Error') + if msg.ival == 3: + raise CommunicationError('Testing Communication Error') return _TResp1(bval=True) @receiver.handler @@ -897,35 +969,76 @@ def test_full_pipeline() -> None: obj = TestClassS(target=obj_r_sync) obj2 = TestClassS(target=obj_r_async) + obj_rb = TestClassRAlt() + objb = TestClassSAlt(target=obj_rb) + # Test sends (of sync and async varieties). response1 = obj.msg.send(_TMsg1(ival=0)) - response2 = obj.msg.send(_TMsg2(sval='rah')) - response3 = obj.msg.send(_TMsg3(sval='rah')) - response4 = asyncio.run(obj.msg.send_async(_TMsg1(ival=0))) - - # Make sure static typing lines up with what we expect. - if os.environ.get('EFRO_TEST_MESSAGE_FAST') != '1': - # assert static_type_equals(response1, _TResp1) - assert_type(response1, _TResp1) - # assert static_type_equals(response3, None) - assert_type(response3, None) - + assert_type(response1, _TResp1) assert isinstance(response1, _TResp1) + + response1b = objb.msg.send(_TMsg1(ival=0)) + assert_type(response1b, _TResp1) + + response2 = obj.msg.send(_TMsg2(sval='rah')) assert isinstance(response2, (_TResp1, _TResp2)) + + response3 = obj.msg.send(_TMsg3(sval='rah')) + assert_type(response3, None) assert response3 is None + + response4 = asyncio.run(obj.msg.send_async(_TMsg1(ival=0))) assert isinstance(response4, _TResp1) - # Remote CleanErrors should come across locally as the same - # (provided our protocol has enabled support for them). + # Nothing up to this point should have logged any warnings/errors/etc. + assert not any(r.levelno >= logging.WARNING for r in caplog.records) + + # Remote CleanErrors should come across locally as the same and + # no errors should be logged. + # (since our protocol has forward_clean_errors enabled). + caplog.clear() try: _response5 = obj.msg.send(_TMsg1(ival=1)) except Exception as exc: assert isinstance(exc, CleanError) assert str(exc) == 'Testing Clean Error' + assert not caplog.records - # Other remote errors should result in RemoteError. + # Same using a protocol *without* forward_clean_errors should + # give us a generic RemoteError and log the error. + caplog.clear() + with pytest.raises(RemoteError): + _response5 = objb.msg.send(_TMsg1(ival=1)) + assert (len(caplog.records) == 1 + and caplog.records[0].levelno == logging.ERROR) + + # Same with CommunicationErrors occurring on the peer; they should + # come back to us intact if forward_communication_errors is enabled + # and no errors should have been logged. + caplog.clear() + try: + _response5 = obj.msg.send(_TMsg1(ival=3)) + except Exception as exc: + assert isinstance(exc, CommunicationError) + assert str(exc) == 'Testing Communication Error' + assert not caplog.records + + # Same using a protocol *without* forward_clean_errors should + # give us a generic RemoteError and log an error. + caplog.clear() + with pytest.raises(RemoteError): + _response5 = objb.msg.send(_TMsg1(ival=3)) + assert (len(caplog.records) == 1 + and caplog.records[0].levelno == logging.ERROR) + + # Misc other error types happening on peer should result in + # RemoteError and log message. + caplog.clear() with pytest.raises(RemoteError): _response5 = obj.msg.send(_TMsg1(ival=2)) + # This should have logged a single error message. + assert (len(caplog.records) == 1 + and caplog.records[0].levelno == logging.ERROR) # Now test sends to async handlers. response6 = asyncio.run(obj2.msg.send_async(_TMsg1(ival=0))) diff --git a/tests/test_efro/test_rpc.py b/tests/test_efro/test_rpc.py index 8aedbc43..123a8242 100644 --- a/tests/test_efro/test_rpc.py +++ b/tests/test_efro/test_rpc.py @@ -71,11 +71,14 @@ class _ServerClientCommon: async def send_message(self, message: _Message, - timeout: float | None = None) -> _Message: + timeout: float | None = None, + close_on_error: bool = True) -> _Message: """Send high level messages.""" assert self._endpoint is not None response = await self._endpoint.send_message( - dataclass_to_json(message).encode(), timeout=timeout) + dataclass_to_json(message).encode(), + timeout=timeout, + close_on_error=close_on_error) return dataclass_from_json(_Message, response.decode()) async def handle_message(self, msg: _Message) -> _Message: @@ -364,12 +367,23 @@ def test_message_timeout() -> None: _Message(_MessageType.TEST_SLOW)) assert resp.messagetype is _MessageType.RESPONSE_SLOW - # This message should time out. + # This message should time out but not close the connection. with pytest.raises(CommunicationError): resp = await tester.server.send_message( _Message(_MessageType.TEST_SLOW), timeout=0.5, + close_on_error=False, ) + assert not tester.server.endpoint.is_closing() + + # This message should time out and close the connection as a result. + with pytest.raises(CommunicationError): + resp = await tester.server.send_message( + _Message(_MessageType.TEST_SLOW), + timeout=0.5, + close_on_error=True, + ) + assert tester.server.endpoint.is_closing() tester.run(_do_it()) diff --git a/tools/efro/message/_message.py b/tools/efro/message/_message.py index 7a00070c..b0d03265 100644 --- a/tools/efro/message/_message.py +++ b/tools/efro/message/_message.py @@ -61,6 +61,7 @@ class ErrorSysResponse(SysResponse): REMOTE_CLEAN = 1 LOCAL = 2 COMMUNICATION = 3 + REMOTE_COMMUNICATION = 4 error_message: Annotated[str, IOAttrs('m')] error_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.REMOTE diff --git a/tools/efro/message/_protocol.py b/tools/efro/message/_protocol.py index 86387deb..094f924f 100644 --- a/tools/efro/message/_protocol.py +++ b/tools/efro/message/_protocol.py @@ -8,10 +8,9 @@ from __future__ import annotations from typing import TYPE_CHECKING import traceback -import logging import json -from efro.error import CleanError +from efro.error import CleanError, CommunicationError from efro.dataclassio import (is_ioprepped_dataclass, dataclass_to_dict, dataclass_from_dict) from efro.message._message import (Message, Response, SysResponse, @@ -34,24 +33,49 @@ class MessageProtocol: def __init__(self, message_types: dict[int, type[Message]], response_types: dict[int, type[Response]], + forward_communication_errors: bool = False, forward_clean_errors: bool = False, - remote_errors_include_stack_traces: bool = False) -> None: + remote_errors_include_stack_traces: bool = False, + log_remote_errors: bool = True) -> None: """Create a protocol with a given configuration. - Note that common response types are automatically registered - with (unchanging negative ids) so they don't need to be passed - explicitly (but can be if a different id is desired). + If 'forward_communication_errors' is True, + efro.error.CommunicationErrors raised on the receiver end will + result in a matching error raised back on the sender. This can + be useful if the receiver will be in some way forwarding + messages along and the sender doesn't need to know where + communication breakdowns occurred; only that they did. If 'forward_clean_errors' is True, efro.error.CleanError exceptions raised on the receiver end will result in a matching - CleanError raised back on the sender. All other Exception types - come across as efro.error.RemoteError. + CleanError raised back on the sender. - If 'remote_errors_include_stack_traces' is True, stringified stack - traces will be returned to the sender for exceptions occurring - on the receiver end. This can make debugging easier but should - only be used when the client is trusted to see such info. + When an exception is not covered by the optional forwarding + mechanisms above, it will come across as efro.error.RemoteError + and the exception will be logged on the receiver end. + + If 'remote_errors_include_stack_traces' is True, stringified + stack traces will be returned with efro.error.RemoteError + exceptions. This is useful for debugging but should only be + enabled in cases where the sender is trusted to see internal + details of the receiver. + + By default, when a message-handling exception will result in an + efro.error.RemoteError being returned to the sender, the + exception will be logged on the receiver. This is because the + goal is usually to avoid returning opaque RemoteErrors and to + instead return something meaningful as part of an expected + response type (even if that value itself represents a logical + error state). If 'log_remote_errors' is False, however, such + exceptions will not be logged on the receiver. This can be + useful in combination with 'remote_errors_include_stack_traces' + and 'forward_clean_errors' in situations where all error + logging/management will be happening on the sender end. Be + aware, however, that in that case + efro.error.CommunicationErrors can possibly prevent such error + messages from ever being seen. """ + # pylint: disable=too-many-locals self.message_types_by_id: dict[int, type[Message]] = {} self.message_ids_by_type: dict[type[Message], int] = {} self.response_types_by_id: dict[int, type[Response] @@ -123,8 +147,10 @@ class MessageProtocol: ' all types are required to have unique names.') self.forward_clean_errors = forward_clean_errors + self.forward_communication_errors = forward_communication_errors self.remote_errors_include_stack_traces = ( remote_errors_include_stack_traces) + self.log_remote_errors = log_remote_errors @staticmethod def encode_dict(obj: dict) -> str: @@ -139,23 +165,31 @@ class MessageProtocol: """Encode a response to a json ready dict.""" return self._to_dict(response, self.response_ids_by_type, 'response') - def error_to_response(self, exc: Exception) -> SysResponse: - """Translate an error to a response.""" + def error_to_response(self, exc: Exception) -> tuple[SysResponse, bool]: + """Translate an Exception to a SysResponse. - # Log any errors we got during handling. - logging.exception('Error in efro.message handling.') + Also returns whether the error should be logged if this happened + within handle_raw_message(). + """ # If anything goes wrong, return a ErrorSysResponse instead. # (either CLEAN or generic REMOTE) - if isinstance(exc, CleanError) and self.forward_clean_errors: - return ErrorSysResponse( + if self.forward_clean_errors and isinstance(exc, CleanError): + return (ErrorSysResponse( error_message=str(exc), - error_type=ErrorSysResponse.ErrorType.REMOTE_CLEAN) - return ErrorSysResponse( + error_type=ErrorSysResponse.ErrorType.REMOTE_CLEAN), False) + if self.forward_communication_errors and isinstance( + exc, CommunicationError): + return (ErrorSysResponse( + error_message=str(exc), + error_type=ErrorSysResponse.ErrorType.REMOTE_COMMUNICATION), + False) + return (ErrorSysResponse( error_message=(traceback.format_exc() if self.remote_errors_include_stack_traces else 'An internal error has occurred.'), - error_type=ErrorSysResponse.ErrorType.REMOTE) + error_type=ErrorSysResponse.ErrorType.REMOTE), + self.log_remote_errors) def _to_dict(self, message: Any, ids_by_type: dict[type, int], opname: str) -> dict: diff --git a/tools/efro/message/_receiver.py b/tools/efro/message/_receiver.py index 5b23a80c..b9870d39 100644 --- a/tools/efro/message/_receiver.py +++ b/tools/efro/message/_receiver.py @@ -260,14 +260,14 @@ class MessageReceiver: return self.protocol.encode_dict(response_dict) def encode_error_response(self, bound_obj: Any, message: Message | None, - exc: Exception) -> str: - """Given an error, return a response ready for sending.""" - response = self.protocol.error_to_response(exc) + exc: Exception) -> tuple[str, bool]: + """Given an error, return sysresponse str and whether to log.""" + response, dolog = self.protocol.error_to_response(exc) response_dict = self.protocol.response_to_dict(response) if self._encode_filter_call is not None: self._encode_filter_call(bound_obj, message, response, response_dict) - return self.protocol.encode_dict(response_dict) + return self.protocol.encode_dict(response_dict), dolog def handle_raw_message(self, bound_obj: Any, @@ -296,7 +296,11 @@ class MessageReceiver: if (raise_unregistered and isinstance(exc, UnregisteredMessageIDError)): raise - return self.encode_error_response(bound_obj, msg_decoded, exc) + rstr, dolog = self.encode_error_response(bound_obj, msg_decoded, + exc) + if dolog: + logging.exception('Error in efro.message handling.') + return rstr async def handle_raw_message_async( self, @@ -324,7 +328,11 @@ class MessageReceiver: if (raise_unregistered and isinstance(exc, UnregisteredMessageIDError)): raise - return self.encode_error_response(bound_obj, msg_decoded, exc) + rstr, dolog = self.encode_error_response(bound_obj, msg_decoded, + exc) + if dolog: + logging.exception('Error in efro.message handling.') + return rstr class BoundMessageReceiver: @@ -348,9 +356,9 @@ class BoundMessageReceiver: """Given an error, return a response ready to send. This should be used for any errors that happen outside of - of standard handle_raw_message calls. Any errors within those - calls should be automatically returned as encoded strings. + standard handle_raw_message calls. Any errors within those + calls will be automatically returned as encoded strings. """ # Passing None for Message here; we would only have that available # for things going wrong in the handler (which this is not for). - return self._receiver.encode_error_response(self._obj, None, exc) + return self._receiver.encode_error_response(self._obj, None, exc)[0] diff --git a/tools/efro/message/_sender.py b/tools/efro/message/_sender.py index 47e1dc58..a0bc714a 100644 --- a/tools/efro/message/_sender.py +++ b/tools/efro/message/_sender.py @@ -272,6 +272,11 @@ class MessageSender: is ErrorSysResponse.ErrorType.REMOTE_CLEAN): raise CleanError(raw_response.error_message) + if (self.protocol.forward_communication_errors + and raw_response.error_type is + ErrorSysResponse.ErrorType.REMOTE_COMMUNICATION): + raise CommunicationError(raw_response.error_message) + # Everything else gets lumped in as a remote error. raise RemoteError(raw_response.error_message, peer_desc=('peer' if self._peer_desc_call is None diff --git a/tools/efro/rpc.py b/tools/efro/rpc.py index b7a14d03..d535a62b 100644 --- a/tools/efro/rpc.py +++ b/tools/efro/rpc.py @@ -290,17 +290,24 @@ class RPCEndpoint: async def send_message(self, message: bytes, - timeout: float | None = None) -> bytes: + timeout: float | None = None, + close_on_error: bool = True) -> bytes: """Send a message to the peer and return a response. If timeout is not provided, the default will be used. Raises a CommunicationError if the round trip is not completed for any reason. + + By default, the entire endpoint will go down in the case of + errors. This allows messages to be treated as 'reliable' with + respect to a given endpoint. Pass close_on_error=False to + override this for a particular message. """ + # pylint: disable=too-many-branches self._check_env() if self._closing: - raise CommunicationError('Endpoint is closed') + raise CommunicationError('Endpoint is closed.') # We need to know their protocol, so if we haven't gotten a handshake # from them yet, just wait. @@ -358,6 +365,8 @@ class RPCEndpoint: if self._debug_print: self._debug_print_call( f'{self._label}: message {message_id} was cancelled.') + if close_on_error: + self.close() raise CommunicationError() from exc except asyncio.TimeoutError as exc: if self._debug_print: @@ -370,6 +379,9 @@ class RPCEndpoint: # Remove the record of this message. del self._in_flight_messages[message_id] + if close_on_error: + self.close() + # Let the user know something went wrong. raise CommunicationError() from exc @@ -407,7 +419,11 @@ class RPCEndpoint: return self._closing async def wait_closed(self) -> None: - """I said seagulls; mmmm; stop it now.""" + """I said seagulls; mmmm; stop it now. + + Wait for the endpoint to finish closing. This is called by run() + so generally does not need to be explicitly called. + """ # pylint: disable=too-many-branches self._check_env() @@ -729,8 +745,9 @@ class RPCEndpoint: def _check_env(self) -> None: # I was seeing that asyncio stuff wasn't working as expected if - # created in one thread and used in another, so let's enforce - # a single thread for all use of an instance. + # created in one thread and used in another (and have verified + # that this is part of the design), so let's enforce a single + # thread for all use of an instance. if current_thread() is not self._thread: raise RuntimeError('This must be called from the same thread' ' that the endpoint was created in.') diff --git a/tools/efrotools/pcommand.py b/tools/efrotools/pcommand.py index 787a47a4..4524bd60 100644 --- a/tools/efrotools/pcommand.py +++ b/tools/efrotools/pcommand.py @@ -433,11 +433,11 @@ def _filter_tool_config(cfg: str) -> str: # Short project name. short_names = { - 'ballistica-internal': 'ba-int', + 'ballistica-internal': 'ba-i', 'ballistica': 'ba', - 'ballistica-master-server': 'bamaster', - 'ballistica-master-server-legacy': 'bamasterlegacy', - 'ballistica-server-node': 'baservnode', + 'ballistica-master-server': 'bmas', + 'ballistica-master-server-legacy': 'bmasl', + 'ballistica-server-node': 'basn', } shortname = short_names.get(PROJROOT.name, PROJROOT.name) cfg = cfg.replace('__EFRO_PROJECT_SHORTNAME__', shortname) diff --git a/tools/efrotools/sync.py b/tools/efrotools/sync.py index cb532378..299f75f8 100644 --- a/tools/efrotools/sync.py +++ b/tools/efrotools/sync.py @@ -205,8 +205,10 @@ def sync_paths(src_proj: str, src: Path, dst: Path, mode: Mode) -> int: continue # Src/dst hashes don't match and marker doesn't match either. # We give up. + srcabs = os.path.abspath(srcfile) + dstabs = os.path.abspath(dstfile) raise RuntimeError( - f'both src and dst sync files changed: {srcfile} {dstfile}' + f'both src and dst sync files changed: {srcabs} {dstabs}' '; this must be resolved manually.') # (if we got here this file should be healthy..)