version bump

This commit is contained in:
Eric 2023-01-17 13:32:28 -08:00
parent e0ea183ef4
commit 667da7eef2
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
19 changed files with 427 additions and 237 deletions

View File

@ -420,10 +420,10 @@
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/60/ad/38269b7f1c7dc20cb9a506cd0681", "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/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/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/98/28/2d0235ac9ccd0b800f832ab7fbb3", "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/9b/ec/d11f3e0238ff648bce3657fe5d50",
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/92/43/36b34307575f6d6219bdf4898e18", "assets/build/ba_data/data/languages/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/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/aa/ed/4bd02af3cffbd4c9c4be532fb1fe", "assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/96/96/1390940b8457b477113194acbb41",
"assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/f7/b0/191439142c6d6da4a056edc98b38", "assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/f7/b0/191439142c6d6da4a056edc98b38",
"assets/build/ba_data/data/languages/croatian.json": "https://files.ballistica.net/cache/ba1/c9/73/01a1343af814131b1ee96af0b687", "assets/build/ba_data/data/languages/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/4e/8c/72ddb584856a15dfb11df95f9283", "assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/4e/8c/72ddb584856a15dfb11df95f9283",
@ -449,11 +449,11 @@
"assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/6c/62/06869ed55a656b6e51b4d22e6fa8", "assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/6c/62/06869ed55a656b6e51b4d22e6fa8",
"assets/build/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/4e/91/6f2a9a3ce733908e91377a6ddb9a", "assets/build/ba_data/data/languages/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/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/f5/35/1dea74424ee613611ae3f7deec99", "assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/0b/21/a4d09ca1fea8bbf347ed7091c8a2",
"assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/3b/9f/d40c1423d260784970fd7364ca30", "assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/3b/9f/d40c1423d260784970fd7364ca30",
"assets/build/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/3d/83/e1bb0a664d1c14c41b1a083acf0d", "assets/build/ba_data/data/languages/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/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/2a/0e/b164149e76efd0e6d591a9bf04bb", "assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/63/c8/6cfbfd6702c80aa9df490e4629d7",
"assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/3e/b6/052f1faed0264bf7135feb5c4cc3", "assets/build/ba_data/data/languages/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/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/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/1f/ae/abe3f105b3c4b51f6b7942773305",
@ -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/__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", "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", "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/a0/31/79276810bb7e7a63b16a813762ed", "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/93/f0/feed13859ce5e9ea067b0e0529ff",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8b/ff/ffe14193d7ecc7be7bd4b8f0c1df", "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/0d/88/056f8877eb83cfa87fb581d09a52",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1b/35/085b6877ca6132f22363935c556f", "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f3/1f/14bf78a27530eec8d30e54392788",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ea/6d/812e4be178b1bfac568dfbef1cd5", "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d5/f9/9acf2f6b0a04f122d937b671337b",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/01/81/44c58651a270c66621fac233af03", "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/21/b5/65486e588021c5a551a7faf60490",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/bc/7f/fcb81a65171d668c02b43ad8a0ca", "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/4f/3f/39a86bf3e6556628bac7e038eb51",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/97/d3/a39c9949110c3258662e37a01218", "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6c/6f/7efeb16dffd3f1e205feb2ce99fa",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/34/c6/b9f70ebc862ea8c0824c20c21a8c", "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/21/e5/6e5bcfbf995ae8579638fc5d1488",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d4/1d/5fce150d3ffb8286eccc3c932070", "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/a0/7c/7a09ecb3beedfa91cde84e964bb9",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7d/22/88ba10738a5fa80022fd4e43cd63", "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/5a/fa/8491b8dac79d172c33747862e77f",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/14/01/f113a6e70aa0f69753513c890d2e", "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4c/8f/90292472906d259c86c8a38bcbc7",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4c/16/0a6993916d2b486cf1e3a96b5855", "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/18/c3/92d5a2465283e17bcf7af2bf26e8",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/8d/54/7a2f804e29e8c43de5d17f3fb303", "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f2/4a/222725049d0619e8c8d97e60d079",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/98/aa/4decee9932b26500b9db74f68115", "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/60/e4/d6798a2d698da031a1f97b2fd008",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e7/e2/8b1ec39a67b0ac7c23c76ea2b71d", "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4e/96/d35f6076e1e01b7a959df4379b76",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/59/ab/2756b60a87a798b89450d4ead9c6", "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f9/56/d0f8e1eb5f17c33a99ee032a42ff",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/43/f4/5289b9660fed0381c1d98626c0a6", "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e6/fc/95cab0bca71d81826a30b7621852",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/7f/9a/fde700fa2c7a32cd9c3695fe8e8d", "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/99/93/23154000c5d940cd943deed2692f",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/ba/9a/81f247b9f980e7907b28633605e1", "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/ae/5b/3ccd55688d48429a7d2a4bd32919",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/c1/83/499a6c6d1cf150ce0370680db482", "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/ac/ef/5ed349d3c42d58a98a79c94404b0",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e4/1b/a2ab5dd8c59cd90f5cd28f451884", "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cb/3b/83e6150eebf4fa7b5e8a7b863219",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a5/48/1e25219edd0a78e40c0f3093396d", "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/70/2878a73f2d55849cd6a75e401575",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/dd/5d/51313d43cafea4898cc2ca7aaf3d", "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/24/49/4baca475df5c7f87e6ed17d46696",
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b3/ca/9a7f4019bf2e57d668171c42c876", "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b9/46/1c6b679ef9db6807100bc0bba261",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4e/c8/478035d9e79dabc2a0cff7dc5c07", "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/da/b9/427dfd7ae8efbe6009964fe34beb",
"build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/06/e3/5cac56d9f4e96a800cd424ecb01a", "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ac/2c/fc0a576c3d957896bfd3de792af6",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2d/f2/f3afa4635c55863e2996ee110414", "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/39/08/4033ab823798c48b3446c032a72c",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/52/2e/3fef9d99b533a1ae5e1be3c63e21", "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3a/c6/189744027136a7411d5dfa5c5cf4",
"build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/17/d4/3578de762544210f09f9ef6b60a4", "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7a/c6/bb533c59368fdf45d65812f37a27",
"build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/18/44/f6a99fe7cf886f8a1b77b1878c7a", "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3c/6e/88180b8e905df7453e0f989da027",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/84/3a/a7f7d3b3ee3bd7b1806bf77f2859", "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2c/99/e35a71c9c410da4035a0456314b6",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e9/58/63afc99ce47eed08527f2dfc8f4d", "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/47/b2/bb092304cd5e5f3bdb6e085197de",
"build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/43/ed/f49af3eb3646c69c5312ef8000dc", "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9c/ce/523a32e2dce1174df21373ab5765",
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e4/c5/091b0d55220c1c7a6939e00563d7", "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b2/ee/b908410d85c763d5bca09a3bc893",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b3/f4/69436953e88c28d027b7c8004ba3", "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fd/24/8c51e5752064d4b487dff42a7ffa",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/37/d3/599f3904f5ee987d540164237703", "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/93/c6/40e9e47dd33c88646510212ff321",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/5b/0e/a571f0b1d167f3a0d3f07ff6e633", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/a1/25/ca39d9309b63ed38bee3be1f9e67",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/86/3e/abdb5280c7e24e145bcf4cfc73d4", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/b3/ef/a6240c67194508ac7bd58ba73391",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/15/b7/76299570cd56fac624ec810c575c", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/c2/cc/b081c59aa873304086d2e44c1f30",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/39/07/b32bb5e8a4458adc0df92dad82f6", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/d3/cb/8c1ed9ee3e8f1b0d866160257506",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/28/46/700cc357d8c776c3a34dbdeb31c8", "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/f0/f8/fb0aa403b29d5c5a9ebb5aec66fb",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/71/f0/d3d64e44f03a10536649e000c836", "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/0b/03/ed35c58e80013f47b57c838c12d4",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/42/9a/961f50a8d8d7b1d704a2616d76ca", "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/18/1d/cfc42a4939904783c897322207aa",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/4a/da/85bb42cb764293be0b4c515268ad", "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/68/82/c02d830bdb12f57ae976c5acc4fb",
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/23/ce/68396b1b7ec6d2f8425902148140", "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/23/ce/68396b1b7ec6d2f8425902148140",
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/2d/4f/f4fe67827f36cd59cd5193333a02", "src/ballistica/generated/python_embedded/bootstrap.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" "src/ballistica/generated/python_embedded/bootstrap_monolithic.inc": "https://files.ballistica.net/cache/ba1/ef/c1/aa5f1aa10af89f5c0b1e616355fd"

View File

@ -185,6 +185,7 @@
<w>availplug</w> <w>availplug</w>
<w>aval</w> <w>aval</w>
<w>awaitable</w> <w>awaitable</w>
<w>awaitables</w>
<w>axismotion</w> <w>axismotion</w>
<w>bacfg</w> <w>bacfg</w>
<w>backgrounded</w> <w>backgrounded</w>
@ -863,6 +864,7 @@
<w>fcontents</w> <w>fcontents</w>
<w>fcount</w> <w>fcount</w>
<w>fdata</w> <w>fdata</w>
<w>fdcount</w>
<w>fdesc</w> <w>fdesc</w>
<w>fdict</w> <w>fdict</w>
<w>fdout</w> <w>fdout</w>
@ -1276,6 +1278,7 @@
<w>iprof</w> <w>iprof</w>
<w>isatty</w> <w>isatty</w>
<w>iscale</w> <w>iscale</w>
<w>iscoroutinefunction</w>
<w>iserverget</w> <w>iserverget</w>
<w>iserverput</w> <w>iserverput</w>
<w>ispunch</w> <w>ispunch</w>

View File

@ -1,4 +1,6 @@
### 1.7.18 (build 20984, api 7, 2023-01-12) ### 1.7.18 (build 20991, api 7, 2023-01-17)
- Reworked some low level asynchronous messaging functionality in efro.message and efro.rpc. Previously these were a little *too* asynchronous which could lead to messages being received in a different order than they were sent, which is not desirable.
- Added a way to suppress 'Your build is outdated' messages at launch (see `ba._hooks.show_client_too_old_error()`).
### 1.7.17 (build 20983, api 7, 2023-01-09) ### 1.7.17 (build 20983, api 7, 2023-01-09)
- V2 accounts now show a 'Unlink Legacy (V1) Accounts' button in account settings if they have any old V1 links present. This can be used to clear out old links to replace them with V2 links which work correctly with V2 accounts. - 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.

View File

@ -36,7 +36,7 @@ class AccountV2Subsystem:
# (or lack thereof) has completed. This includes things like # (or lack thereof) has completed. This includes things like
# workspace syncing. Completion of this is what flips the app # workspace syncing. Completion of this is what flips the app
# into 'running' state. # into 'running' state.
self._initial_login_completed = False self._initial_sign_in_completed = False
self._kicked_off_workspace_load = False self._kicked_off_workspace_load = False
@ -98,7 +98,7 @@ class AccountV2Subsystem:
if account.workspaceid is not None: if account.workspaceid is not None:
assert account.workspacename is not None assert account.workspacename is not None
if ( if (
not self._initial_login_completed not self._initial_sign_in_completed
and not self._kicked_off_workspace_load and not self._kicked_off_workspace_load
): ):
self._kicked_off_workspace_load = True self._kicked_off_workspace_load = True
@ -121,9 +121,9 @@ class AccountV2Subsystem:
return return
# Ok; no workspace to worry about; carry on. # Ok; no workspace to worry about; carry on.
if not self._initial_login_completed: if not self._initial_sign_in_completed:
self._initial_login_completed = True self._initial_sign_in_completed = True
_ba.app.on_initial_login_completed() _ba.app.on_initial_sign_in_completed()
def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None: def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
"""Should be called when logins for the active account change.""" """Should be called when logins for the active account change."""
@ -156,9 +156,9 @@ class AccountV2Subsystem:
within a few seconds of app launch; the app can move forward within a few seconds of app launch; the app can move forward
with the startup sequence at that point. with the startup sequence at that point.
""" """
if not self._initial_login_completed: if not self._initial_sign_in_completed:
self._initial_login_completed = True self._initial_sign_in_completed = True
_ba.app.on_initial_login_completed() _ba.app.on_initial_sign_in_completed()
@staticmethod @staticmethod
def _hashstr(val: str) -> str: def _hashstr(val: str) -> str:
@ -271,7 +271,7 @@ class AccountV2Subsystem:
self._implicit_state_changed = False self._implicit_state_changed = False
# Once we've made a move here we don't want to # Once we've made a move here we don't want to
# do any more automatic ones. # do any more automatic stuff.
self._can_do_auto_sign_in = False self._can_do_auto_sign_in = False
else: else:
@ -290,22 +290,23 @@ class AccountV2Subsystem:
' of implicit state change...', ' of implicit state change...',
) )
self._implicit_signed_in_adapter.sign_in( self._implicit_signed_in_adapter.sign_in(
self._on_explicit_sign_in_completed self._on_explicit_sign_in_completed,
description='implicit state change',
) )
self._implicit_state_changed = False self._implicit_state_changed = False
# Once we've made a move here we don't want to # Once we've made a move here we don't want to
# do any more automatic ones. # do any more automatic stuff.
self._can_do_auto_sign_in = False self._can_do_auto_sign_in = False
if not self._can_do_auto_sign_in: if not self._can_do_auto_sign_in:
return return
# If we're not currently signed in, we have connectivity, and # If we're not currently signed in, we have connectivity, and
# we have an available implicit login, auto-sign-in with it. # we have an available implicit login, auto-sign-in with it once.
# The implicit-state-change logic above should keep things # The implicit-state-change logic above should keep things
# mostly in-sync, but due to connectivity or other issues that # mostly in-sync, but that might not always be the case due to
# might not always be the case. We prefer to keep people signed # connectivity or other issues. We prefer to keep people signed
# in as a rule, even if there are corner cases where this might # in as a rule, even if there are corner cases where this might
# not be what they want (A user signing out and then restarting # not be what they want (A user signing out and then restarting
# may be auto-signed back in). # may be auto-signed back in).
@ -324,7 +325,7 @@ class AccountV2Subsystem:
) )
self._can_do_auto_sign_in = False # Only ATTEMPT once self._can_do_auto_sign_in = False # Only ATTEMPT once
self._implicit_signed_in_adapter.sign_in( self._implicit_signed_in_adapter.sign_in(
self._on_implicit_sign_in_completed self._on_implicit_sign_in_completed, description='auto-sign-in'
) )
def _on_explicit_sign_in_completed( def _on_explicit_sign_in_completed(
@ -337,8 +338,8 @@ class AccountV2Subsystem:
del adapter # Unused. del adapter # Unused.
# Make some noise on errors since the user knows # Make some noise on errors since the user knows a
# a sign-in attempt is happening in this case. # sign-in attempt is happening in this case (the 'explicit' part).
if isinstance(result, Exception): if isinstance(result, Exception):
# We expect the occasional communication errors; # We expect the occasional communication errors;
# Log a full exception for anything else though. # Log a full exception for anything else though.
@ -347,6 +348,8 @@ class AccountV2Subsystem:
'Error on explicit accountv2 sign in attempt.', 'Error on explicit accountv2 sign in attempt.',
exc_info=result, exc_info=result,
) )
# For now just show 'error'. Should do better than this.
with _ba.Context('ui'): with _ba.Context('ui'):
_ba.screenmessage( _ba.screenmessage(
Lstr(resource='internal.signInErrorText'), Lstr(resource='internal.signInErrorText'),
@ -395,9 +398,9 @@ class AccountV2Subsystem:
_ba.app.accounts_v2.set_primary_credentials(result.credentials) _ba.app.accounts_v2.set_primary_credentials(result.credentials)
def _on_set_active_workspace_completed(self) -> None: def _on_set_active_workspace_completed(self) -> None:
if not self._initial_login_completed: if not self._initial_sign_in_completed:
self._initial_login_completed = True self._initial_sign_in_completed = True
_ba.app.on_initial_login_completed() _ba.app.on_initial_sign_in_completed()
class AccountV2Handle: class AccountV2Handle:

View File

@ -235,7 +235,7 @@ class App:
self.state = self.State.LAUNCHING self.state = self.State.LAUNCHING
self._launch_completed = False self._launch_completed = False
self._initial_login_completed = False self._initial_sign_in_completed = False
self._meta_scan_completed = False self._meta_scan_completed = False
self._called_on_app_running = False self._called_on_app_running = False
self._app_paused = False self._app_paused = False
@ -511,7 +511,7 @@ class App:
self.plugins.on_app_resume() self.plugins.on_app_resume()
self.health_monitor.on_app_resume() self.health_monitor.on_app_resume()
if self._initial_login_completed and self._meta_scan_completed: if self._initial_sign_in_completed and self._meta_scan_completed:
self.state = self.State.RUNNING self.state = self.State.RUNNING
if not self._called_on_app_running: if not self._called_on_app_running:
self._called_on_app_running = True self._called_on_app_running = True
@ -724,8 +724,8 @@ class App:
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
_ba.playsound(_ba.getsound('error')) _ba.playsound(_ba.getsound('error'))
def on_initial_login_completed(self) -> None: def on_initial_sign_in_completed(self) -> None:
"""Callback to be run after initial login process (or lack thereof). """Callback to be run after initial sign-in (or lack thereof).
This period includes things such as syncing account workspaces This period includes things such as syncing account workspaces
or other data so it may take a substantial amount of time. or other data so it may take a substantial amount of time.
@ -736,5 +736,5 @@ class App:
# (account workspaces). # (account workspaces).
self.meta.start_extra_scan() self.meta.start_extra_scan()
self._initial_login_completed = True self._initial_sign_in_completed = True
self._update_state() self._update_state()

View File

@ -476,35 +476,18 @@ def on_too_many_file_descriptors() -> None:
real_time = _ba.time(TimeType.REAL) real_time = _ba.time(TimeType.REAL)
def _do_log() -> None: def _do_log() -> None:
import subprocess
pid = os.getpid() pid = os.getpid()
out = f'TOO MANY FDS at {real_time}.\nWe are pid {pid}\n' try:
fdcount: int | str = len(os.listdir(f'/proc/{pid}/fd'))
out += ( except Exception as exc:
'FD Count: ' fdcount = f'? ({exc})'
+ subprocess.run( logging.warning(
f'ls -l /proc/{pid}/fd | wc -l', 'TOO MANY FDS at %.2f. We are pid %d. FDCount is %s.',
shell=True, real_time,
check=False, pid,
capture_output=True, fdcount,
).stdout.decode()
+ '\n'
) )
out += (
'lsof output:\n'
+ subprocess.run(
f'lsof -p {pid}',
shell=True,
check=False,
capture_output=True,
).stdout.decode()
+ '\n'
)
logging.warning(out)
Thread(target=_do_log, daemon=True).start() Thread(target=_do_log, daemon=True).start()
# import io # import io

View File

@ -47,7 +47,7 @@ def bootstrap() -> None:
# Give a soft warning if we're being used with a different binary # Give a soft warning if we're being used with a different binary
# version than we expect. # version than we expect.
expected_build = 20984 expected_build = 20991
running_build: int = env['build_number'] running_build: int = env['build_number']
if running_build != expected_build: if running_build != expected_build:
print( print(

View File

@ -1,13 +1,13 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Snippets of code for use by the internal C++ layer. """Snippets of code for use by the internal layer.
History: originally I would dynamically compile/eval bits of Python text History: originally the engine would dynamically compile/eval various Python
from within C++ code, but the major downside there was that none of that was code from within C++ code, but the major downside there was that none of it
type-checked so if names or arguments changed I would never catch code breakage was type-checked so if names or arguments changed it would go unnoticed
until the code was next run. By defining all snippets I use here and then until it broke at runtime. By instead defining such snippets here and then
capturing references to them all at launch I can immediately verify everything capturing references to them all at launch it is possible to allow linting
I'm looking for exists and pylint/mypy can do their magic on this file. and type-checking magic to happen and most issues will be caught immediately.
""" """
# (most of these are self-explanatory) # (most of these are self-explanatory)
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
@ -470,3 +470,33 @@ def login_adapter_get_sign_in_token_response(
adapter = _ba.app.accounts_v2.login_adapters[login_type] adapter = _ba.app.accounts_v2.login_adapters[login_type]
assert isinstance(adapter, LoginAdapterNative) assert isinstance(adapter, LoginAdapterNative)
adapter.on_sign_in_complete(attempt_id=attempt_id, result=result) adapter.on_sign_in_complete(attempt_id=attempt_id, result=result)
def show_client_too_old_error() -> None:
"""Called at launch if the server tells us we're too old to talk to it."""
from ba._language import Lstr
# If you are using an old build of the app and would like to stop
# seeing this error at launch, do:
# ba.app.config['SuppressClientTooOldErrorForBuild'] = ba.app.build_number
# ba.app.config.commit()
# Note that you will have to do that again later if you update to
# a newer build.
if (
_ba.app.config.get('SuppressClientTooOldErrorForBuild')
== _ba.app.build_number
):
return
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(
Lstr(
translate=(
'serverResponses',
'Server functionality is no longer supported'
' in this version of the game;\n'
'Please update to a newer version.',
)
),
color=(1, 0, 0),
)

View File

@ -4,6 +4,7 @@
from __future__ import annotations from __future__ import annotations
import time
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, final from typing import TYPE_CHECKING, final
@ -57,6 +58,9 @@ class LoginAdapter:
# current active primary account. # current active primary account.
self._active_login_id: str | None = None self._active_login_id: str | None = None
self._last_sign_in_time: float | None = None
self._last_sign_in_desc: str | None = None
def on_app_launch(self) -> None: def on_app_launch(self) -> None:
"""Should be called for each adapter in on_app_launch.""" """Should be called for each adapter in on_app_launch."""
@ -142,6 +146,7 @@ class LoginAdapter:
def sign_in( def sign_in(
self, self,
result_cb: Callable[[LoginAdapter, SignInResult | Exception], None], result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
description: str,
) -> None: ) -> None:
"""Attempt an explicit sign in via this adapter. """Attempt an explicit sign in via this adapter.
@ -151,6 +156,38 @@ class LoginAdapter:
""" """
assert _ba.in_logic_thread() assert _ba.in_logic_thread()
from ba._general import Call from ba._general import Call
from ba._generated.enums import TimeType
# Have been seeing multiple sign-in attempts come through
# nearly simultaneously which can be problematic server-side.
# Let's error if a sign-in attempt is made within a few seconds
# of the last one to address this.
now = time.monotonic()
appnow = _ba.time(TimeType.REAL)
if self._last_sign_in_time is not None:
since_last = now - self._last_sign_in_time
if since_last < 1.0:
logging.warning(
'LoginAdapter: %s adapter sign_in() called too soon'
' (%.2fs) after last; this-desc="%s", last-desc="%s",'
' ba-real-time=%.2f.',
self.login_type.name,
since_last,
description,
self._last_sign_in_desc,
appnow,
)
_ba.pushcall(
Call(
result_cb,
self,
RuntimeError('sign_in called too soon after last.'),
)
)
return
self._last_sign_in_desc = description
self._last_sign_in_time = now
if DEBUG_LOG: if DEBUG_LOG:
logging.debug( logging.debug(
@ -223,7 +260,12 @@ class LoginAdapter:
_ba.pushcall(Call(result_cb, self, result2)) _ba.pushcall(Call(result_cb, self, result2))
_ba.app.cloud.send_message_cb( _ba.app.cloud.send_message_cb(
bacommon.cloud.SignInMessage(self.login_type, result), bacommon.cloud.SignInMessage(
self.login_type,
result,
description=description,
apptime=appnow,
),
on_response=_got_sign_in_response, on_response=_got_sign_in_response,
) )

View File

@ -1403,7 +1403,8 @@ class AccountSettingsWindow(ba.Window):
if adapter is not None: if adapter is not None:
self._signing_in_adapter = adapter self._signing_in_adapter = adapter
adapter.sign_in( adapter.sign_in(
result_cb=ba.WeakCall(self._on_adapter_sign_in_result) result_cb=ba.WeakCall(self._on_adapter_sign_in_result),
description='account settings button',
) )
# Will get 'Signing in...' to show. # Will get 'Signing in...' to show.
self._needs_refresh = True self._needs_refresh = True

View File

@ -94,6 +94,7 @@
<w>avel</w> <w>avel</w>
<w>avels</w> <w>avels</w>
<w>awaitable</w> <w>awaitable</w>
<w>awaitables</w>
<w>axismotion</w> <w>axismotion</w>
<w>backgrounded</w> <w>backgrounded</w>
<w>backgrounding</w> <w>backgrounding</w>
@ -464,6 +465,7 @@
<w>fbos</w> <w>fbos</w>
<w>fcntl</w> <w>fcntl</w>
<w>fdata</w> <w>fdata</w>
<w>fdcount</w>
<w>fdirx</w> <w>fdirx</w>
<w>fdiry</w> <w>fdiry</w>
<w>fdirz</w> <w>fdirz</w>
@ -672,6 +674,7 @@
<w>ioprepped</w> <w>ioprepped</w>
<w>ioprepping</w> <w>ioprepping</w>
<w>ioreg</w> <w>ioreg</w>
<w>iscoroutinefunction</w>
<w>iserverget</w> <w>iserverget</w>
<w>iserverput</w> <w>iserverput</w>
<w>isinst</w> <w>isinst</w>

View File

@ -32,7 +32,7 @@
namespace ballistica { namespace ballistica {
// These are set automatically via script; don't modify them here. // These are set automatically via script; don't modify them here.
const int kAppBuildNumber = 20984; const int kAppBuildNumber = 20991;
const char* kAppVersion = "1.7.18"; const char* kAppVersion = "1.7.18";
// Our standalone globals. // Our standalone globals.

View File

@ -198,9 +198,9 @@ class _BoundTestMessageSenderAsync(BoundMessageSender):
async def send_async(self, message: _TMsg3) -> None: async def send_async(self, message: _TMsg3) -> None:
... ...
async def send_async(self, message: Message) -> Response | None: def send_async(self, message: Message) -> Awaitable[Response | None]:
"""Send a message asynchronously.""" """Send a message asynchronously."""
return await self._sender.send_async(self._obj, message) return self._sender.send_async(self._obj, message)
# SEND_ASYNC_CODE_TEST_END # SEND_ASYNC_CODE_TEST_END
@ -261,9 +261,9 @@ class _BoundTestMessageSenderBBoth(BoundMessageSender):
async def send_async(self, message: _TMsg4) -> None: async def send_async(self, message: _TMsg4) -> None:
... ...
async def send_async(self, message: Message) -> Response | None: def send_async(self, message: Message) -> Awaitable[Response | None]:
"""Send a message asynchronously.""" """Send a message asynchronously."""
return await self._sender.send_async(self._obj, message) return self._sender.send_async(self._obj, message)
# SEND_BOTH_CODE_TEST_END # SEND_BOTH_CODE_TEST_END
@ -424,11 +424,11 @@ class _TestAsyncMessageReceiver(MessageReceiver):
class _BoundTestAsyncMessageReceiver(BoundMessageReceiver): class _BoundTestAsyncMessageReceiver(BoundMessageReceiver):
"""Protocol-specific bound receiver.""" """Protocol-specific bound receiver."""
async def handle_raw_message( def handle_raw_message(
self, message: str, raise_unregistered: bool = False self, message: str, raise_unregistered: bool = False
) -> str: ) -> Awaitable[str]:
"""Asynchronously handle a raw incoming message.""" """Asynchronously handle a raw incoming message."""
return await self._receiver.handle_raw_message_async( return self._receiver.handle_raw_message_async(
self._obj, message, raise_unregistered self._obj, message, raise_unregistered
) )

View File

@ -183,6 +183,10 @@ class SignInMessage(Message):
login_type: Annotated[LoginType, IOAttrs('l')] login_type: Annotated[LoginType, IOAttrs('l')]
sign_in_token: Annotated[str, IOAttrs('t')] sign_in_token: Annotated[str, IOAttrs('t')]
# For debugging. Can remove soft_default once build 20988+ is ubiquitous.
description: Annotated[str, IOAttrs('d', soft_default='-')]
apptime: Annotated[float, IOAttrs('at', soft_default=-1.0)]
@classmethod @classmethod
def get_response_types(cls) -> list[type[Response] | None]: def get_response_types(cls) -> list[type[Response] | None]:
return [SignInResponse] return [SignInResponse]

View File

@ -266,6 +266,9 @@ class LogHandler(logging.Handler):
# didn't expect to be stringified. # didn't expect to be stringified.
msg = self.format(record) msg = self.format(record)
if __debug__:
formattime = time.monotonic()
# Also immediately print pretty colored output to our echo file # Also immediately print pretty colored output to our echo file
# (generally stderr). We do this part here instead of in our bg # (generally stderr). We do this part here instead of in our bg
# thread because the delay can throw off command line prompts or # thread because the delay can throw off command line prompts or
@ -277,6 +280,9 @@ class LogHandler(logging.Handler):
else: else:
self._echofile.write(f'{msg}\n') self._echofile.write(f'{msg}\n')
if __debug__:
echotime = time.monotonic()
self._event_loop.call_soon_threadsafe( self._event_loop.call_soon_threadsafe(
tpartial( tpartial(
self._emit_in_thread, self._emit_in_thread,
@ -295,6 +301,10 @@ class LogHandler(logging.Handler):
now = time.monotonic() now = time.monotonic()
# noinspection PyUnboundLocalVariable # noinspection PyUnboundLocalVariable
duration = now - starttime duration = now - starttime
# noinspection PyUnboundLocalVariable
format_duration = formattime - starttime
# noinspection PyUnboundLocalVariable
echo_duration = echotime - formattime
if duration > 0.05 and ( if duration > 0.05 and (
self._last_slow_emit_warning_time is None self._last_slow_emit_warning_time is None
or now > self._last_slow_emit_warning_time + 10.0 or now > self._last_slow_emit_warning_time + 10.0
@ -307,8 +317,10 @@ class LogHandler(logging.Handler):
tpartial( tpartial(
logging.warning, logging.warning,
'efro.log.LogHandler emit took too long' 'efro.log.LogHandler emit took too long'
' (%.2f seconds).', ' (%.2fs total; %.2fs format, %.2fs echo).',
duration, duration,
format_duration,
echo_duration,
) )
) )

View File

@ -282,10 +282,13 @@ class MessageProtocol:
def _get_module_header( def _get_module_header(
self, self,
part: Literal['sender', 'receiver'], part: Literal['sender', 'receiver'],
extra_import_code: str | None = None, extra_import_code: str | None,
enable_async_sends: bool,
) -> str: ) -> str:
"""Return common parts of generated modules.""" """Return common parts of generated modules."""
# pylint: disable=too-many-locals, too-many-branches # pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
import textwrap import textwrap
tpimports: dict[str, list[str]] = {} tpimports: dict[str, list[str]] = {}
@ -342,7 +345,7 @@ class MessageProtocol:
if part == 'sender': if part == 'sender':
import_lines += ( import_lines += (
'from efro.message import MessageSender,' ' BoundMessageSender' 'from efro.message import MessageSender, BoundMessageSender'
) )
tpimport_typing_extras = '' tpimport_typing_extras = ''
else: else:
@ -362,11 +365,18 @@ class MessageProtocol:
import_lines += f'\n{extra_import_code}\n' import_lines += f'\n{extra_import_code}\n'
ovld = ', overload' if not single_message_type else '' ovld = ', overload' if not single_message_type else ''
ovld2 = (
', cast, Awaitable'
if (single_message_type and part == 'sender' and enable_async_sends)
else ''
)
tpimport_lines = textwrap.indent(tpimport_lines, ' ') tpimport_lines = textwrap.indent(tpimport_lines, ' ')
baseimps = ['Any'] baseimps = ['Any']
if part == 'receiver': if part == 'receiver':
baseimps.append('Callable') baseimps.append('Callable')
if part == 'sender' and enable_async_sends:
baseimps.append('Awaitable')
baseimps_s = ', '.join(baseimps) baseimps_s = ', '.join(baseimps)
out = ( out = (
'# Released under the MIT License. See LICENSE for details.\n' '# Released under the MIT License. See LICENSE for details.\n'
@ -375,7 +385,7 @@ class MessageProtocol:
f'\n' f'\n'
f'from __future__ import annotations\n' f'from __future__ import annotations\n'
f'\n' f'\n'
f'from typing import TYPE_CHECKING{ovld}\n' f'from typing import TYPE_CHECKING{ovld}{ovld2}\n'
f'\n' f'\n'
f'{import_lines}\n' f'{import_lines}\n'
f'\n' f'\n'
@ -399,13 +409,16 @@ class MessageProtocol:
) -> str: ) -> str:
"""Used by create_sender_module(); do not call directly.""" """Used by create_sender_module(); do not call directly."""
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
# pylint: disable=too-many-branches
import textwrap import textwrap
msgtypes = list(self.message_ids_by_type.keys()) msgtypes = list(self.message_ids_by_type.keys())
ppre = '_' if private else '' ppre = '_' if private else ''
out = self._get_module_header( out = self._get_module_header(
'sender', extra_import_code=protocol_module_level_import_code 'sender',
extra_import_code=protocol_module_level_import_code,
enable_async_sends=enable_async_sends,
) )
ccind = textwrap.indent(protocol_create_code, ' ') ccind = textwrap.indent(protocol_create_code, ' ')
out += ( out += (
@ -438,7 +451,8 @@ class MessageProtocol:
continue continue
pfx = 'async ' if async_pass else '' pfx = 'async ' if async_pass else ''
sfx = '_async' if async_pass else '' sfx = '_async' if async_pass else ''
awt = 'await ' if async_pass else '' # awt = 'await ' if async_pass else ''
awt = ''
how = 'asynchronously' if async_pass else 'synchronously' how = 'asynchronously' if async_pass else 'synchronously'
if len(msgtypes) == 1: if len(msgtypes) == 1:
@ -451,22 +465,29 @@ class MessageProtocol:
rtypevar = ' | '.join(_filt_tp_name(t) for t in rtypes) rtypevar = ' | '.join(_filt_tp_name(t) for t in rtypes)
else: else:
rtypevar = _filt_tp_name(rtypes[0]) rtypevar = _filt_tp_name(rtypes[0])
if async_pass:
rtypevar = f'Awaitable[{rtypevar}]'
out += ( out += (
f'\n' f'\n'
f' {pfx}def send{sfx}(self,' f' def send{sfx}(self,'
f' message: {msgtypevar})' f' message: {msgtypevar})'
f' -> {rtypevar}:\n' f' -> {rtypevar}:\n'
f' """Send a message {how}."""\n' f' """Send a message {how}."""\n'
f' out = {awt}self._sender.' f' out = {awt}self._sender.'
f'send{sfx}(self._obj, message)\n' f'send{sfx}(self._obj, message)\n'
f' assert isinstance(out, {rtypevar})\n'
f' return out\n'
) )
if not async_pass:
out += (
f' assert isinstance(out, {rtypevar})\n'
' return out\n'
)
else:
out += f' return cast({rtypevar}, out)\n'
else: else:
for msgtype in msgtypes: for msgtype in msgtypes:
msgtypevar = msgtype.__name__ msgtypevar = msgtype.__name__
# rtypes = msgtype.get_response_types()
rtypes = msgtype.get_response_types() rtypes = msgtype.get_response_types()
if len(rtypes) > 1: if len(rtypes) > 1:
rtypevar = ' | '.join( rtypevar = ' | '.join(
@ -482,10 +503,13 @@ class MessageProtocol:
f' -> {rtypevar}:\n' f' -> {rtypevar}:\n'
f' ...\n' f' ...\n'
) )
rtypevar = 'Response | None'
if async_pass:
rtypevar = f'Awaitable[{rtypevar}]'
out += ( out += (
f'\n' f'\n'
f' {pfx}def send{sfx}(self, message: Message)' f' def send{sfx}(self, message: Message)'
f' -> Response | None:\n' f' -> {rtypevar}:\n'
f' """Send a message {how}."""\n' f' """Send a message {how}."""\n'
f' return {awt}self._sender.' f' return {awt}self._sender.'
f'send{sfx}(self._obj, message)\n' f'send{sfx}(self._obj, message)\n'
@ -509,7 +533,9 @@ class MessageProtocol:
ppre = '_' if private else '' ppre = '_' if private else ''
msgtypes = list(self.message_ids_by_type.keys()) msgtypes = list(self.message_ids_by_type.keys())
out = self._get_module_header( out = self._get_module_header(
'receiver', extra_import_code=protocol_module_level_import_code 'receiver',
extra_import_code=protocol_module_level_import_code,
enable_async_sends=False,
) )
ccind = textwrap.indent(protocol_create_code, ' ') ccind = textwrap.indent(protocol_create_code, ' ')
out += ( out += (
@ -602,11 +628,11 @@ class MessageProtocol:
if is_async: if is_async:
out += ( out += (
'\n' '\n'
' async def handle_raw_message(\n' ' def handle_raw_message(\n'
' self, message: str, raise_unregistered: bool = False\n' ' self, message: str, raise_unregistered: bool = False\n'
' ) -> str:\n' ' ) -> Awaitable[str]:\n'
' """Asynchronously handle a raw incoming message."""\n' ' """Asynchronously handle a raw incoming message."""\n'
' return await self._receiver.' ' return self._receiver.'
'handle_raw_message_async(\n' 'handle_raw_message_async(\n'
' self._obj, message, raise_unregistered\n' ' self._obj, message, raise_unregistered\n'
' )\n' ' )\n'

View File

@ -62,12 +62,6 @@ class MessageReceiver:
[Any, Message | None, Response | SysResponse, dict], None [Any, Message | None, Response | SysResponse, dict], None
] | None = None ] | None = None
# TODO: don't currently have async encode equivalent
# or either for sender; can add as needed.
self._decode_filter_async_call: Callable[
[Any, dict, Message], Awaitable[None]
] | None = None
# noinspection PyProtectedMember # noinspection PyProtectedMember
def register_handler( def register_handler(
self, call: Callable[[Any, Message], Response | None] self, call: Callable[[Any, Message], Response | None]
@ -96,14 +90,17 @@ class MessageReceiver:
# Make sure we are only given async methods if we are an async handler # Make sure we are only given async methods if we are an async handler
# and sync ones otherwise. # and sync ones otherwise.
is_async = inspect.iscoroutinefunction(call) # UPDATE - can't do this anymore since we now sometimes use
if self.is_async != is_async: # regular functions which return awaitables instead of having
msg = ( # the entire function be async.
'Expected a sync method; found an async one.' # is_async = inspect.iscoroutinefunction(call)
if is_async # if self.is_async != is_async:
else 'Expected an async method; found a sync one.' # msg = (
) # 'Expected a sync method; found an async one.'
raise ValueError(msg) # if is_async
# else 'Expected an async method; found a sync one.'
# )
# raise ValueError(msg)
# Check annotation types to determine what message types we handle. # Check annotation types to determine what message types we handle.
# Return-type annotation can be a Union, but we probably don't # Return-type annotation can be a Union, but we probably don't
@ -189,19 +186,6 @@ class MessageReceiver:
self._decode_filter_call = call self._decode_filter_call = call
return call return call
def decode_filter_async_method(
self, call: Callable[[Any, dict, Message], Awaitable[None]]
) -> Callable[[Any, dict, Message], Awaitable[None]]:
"""Function decorator for defining a decode filter.
Decode filters can be used to extract extra data from incoming
message dicts. Note that this version will only work with
handle_raw_message_async().
"""
assert self._decode_filter_async_call is None
self._decode_filter_async_call = call
return call
def encode_filter_method( def encode_filter_method(
self, self,
call: Callable[ call: Callable[
@ -247,24 +231,6 @@ class MessageReceiver:
bound_obj, _msg_dict, msg_decoded = self._decode_incoming_message_base( bound_obj, _msg_dict, msg_decoded = self._decode_incoming_message_base(
bound_obj=bound_obj, msg=msg bound_obj=bound_obj, msg=msg
) )
# If they've set an async filter but are calling sync
# handle_raw_message() its likely a bug.
assert self._decode_filter_async_call is None
return msg_decoded
async def _decode_incoming_message_async(
self, bound_obj: Any, msg: str
) -> Message:
bound_obj, msg_dict, msg_decoded = self._decode_incoming_message_base(
bound_obj=bound_obj, msg=msg
)
if self._decode_filter_async_call is not None:
await self._decode_filter_async_call(
bound_obj, msg_dict, msg_decoded
)
return msg_decoded return msg_decoded
def encode_user_response( def encode_user_response(
@ -346,46 +312,83 @@ class MessageReceiver:
logging.exception('Error in efro.message handling.') logging.exception('Error in efro.message handling.')
return rstr return rstr
async def handle_raw_message_async( def handle_raw_message_async(
self, bound_obj: Any, msg: str, raise_unregistered: bool = False self, bound_obj: Any, msg: str, raise_unregistered: bool = False
) -> str: ) -> Awaitable[str]:
"""Should be called when the receiver gets a message. """Should be called when the receiver gets a message.
The return value is the raw response to the message. The return value is the raw response to the message.
""" """
# Note: This call is synchronous so that the first part of it can
# happen synchronously. If the whole call were async we wouldn't be
# able to guarantee that messages handlers would be called in the
# order the messages were received.
assert self.is_async, "can't call async handler on sync receiver" assert self.is_async, "can't call async handler on sync receiver"
msg_decoded: Message | None = None msg_decoded: Message | None = None
msgtype: type[Message] | None = None msgtype: type[Message] | None = None
try: try:
msg_decoded = await self._decode_incoming_message_async( msg_decoded = self._decode_incoming_message(bound_obj, msg)
bound_obj, msg
)
msgtype = type(msg_decoded) msgtype = type(msg_decoded)
handler = self._handlers.get(msgtype) handler = self._handlers.get(msgtype)
if handler is None: if handler is None:
raise RuntimeError(f'Got unhandled message type: {msgtype}.') raise RuntimeError(f'Got unhandled message type: {msgtype}.')
response = await handler(bound_obj, msg_decoded) handler_awaitable = handler(bound_obj, msg_decoded)
assert isinstance(response, Response | None)
return self.encode_user_response(bound_obj, msg_decoded, response)
except Exception as exc: except Exception as exc:
if raise_unregistered and isinstance( if raise_unregistered and isinstance(
exc, UnregisteredMessageIDError exc, UnregisteredMessageIDError
): ):
raise raise
rstr, dolog = self.encode_error_response( return self._handle_raw_message_async_error(
bound_obj, msg_decoded, exc bound_obj, msg_decoded, msgtype, exc
)
# Return an awaitable to handle the rest asynchronously.
return self._handle_raw_message_async(
bound_obj, msg_decoded, msgtype, handler_awaitable
)
async def _handle_raw_message_async_error(
self,
bound_obj: Any,
msg_decoded: Message | None,
msgtype: type[Message] | None,
exc: Exception,
) -> str:
rstr, dolog = self.encode_error_response(bound_obj, msg_decoded, exc)
if dolog:
if msgtype is not None:
logging.exception(
'Error handling %s.%s message.',
msgtype.__module__,
msgtype.__qualname__,
)
else:
logging.exception('Error in efro.message handling.')
return rstr
async def _handle_raw_message_async(
self,
bound_obj: Any,
msg_decoded: Message,
msgtype: type[Message] | None,
handler_awaitable: Awaitable[Response | None],
) -> str:
"""Should be called when the receiver gets a message.
The return value is the raw response to the message.
"""
try:
response = await handler_awaitable
assert isinstance(response, Response | None)
return self.encode_user_response(bound_obj, msg_decoded, response)
except Exception as exc:
return await self._handle_raw_message_async_error(
bound_obj, msg_decoded, msgtype, exc
) )
if dolog:
if msgtype is not None:
logging.exception(
'Error handling %s.%s message.',
msgtype.__module__,
msgtype.__qualname__,
)
else:
logging.exception('Error in efro.message handling.')
return rstr
class BoundMessageReceiver: class BoundMessageReceiver:

View File

@ -78,6 +78,14 @@ class MessageSender:
CommunicationErrors raised here will be returned to the sender CommunicationErrors raised here will be returned to the sender
as such; all other exceptions will result in a RuntimeError for as such; all other exceptions will result in a RuntimeError for
the sender. the sender.
IMPORTANT: Generally async send methods should not be implemented
as 'async' methods, but instead should be regular methods that
return awaitable objects. This way it can be guaranteed that
outgoing messages are synchronously enqueued in the correct
order, and then async calls can be returned which finish each
send. If the entire call is async, they may be enqueued out of
order in rare cases.
""" """
assert self._send_async_raw_message_call is None assert self._send_async_raw_message_call is None
self._send_async_raw_message_call = call self._send_async_raw_message_call = call
@ -88,8 +96,9 @@ class MessageSender:
) -> Callable[[Any, str, Message], Awaitable[str]]: ) -> Callable[[Any, str, Message], Awaitable[str]]:
"""Function decorator for extended send-async method. """Function decorator for extended send-async method.
This version of the method also is passed the original unencoded Version of send_async_method which is also is passed the original
message. unencoded message; can be useful for cases where metadata is sent
along with messages referring to their payloads/etc.
""" """
assert self._send_async_raw_message_ex_call is None assert self._send_async_raw_message_ex_call is None
self._send_async_raw_message_ex_call = call self._send_async_raw_message_ex_call = call
@ -141,17 +150,34 @@ class MessageSender:
), ),
) )
async def send_async( def send_async(
self, bound_obj: Any, message: Message self, bound_obj: Any, message: Message
) -> Response | None: ) -> Awaitable[Response | None]:
"""Send a message asynchronously.""" """Send a message asynchronously."""
# Note: This call is synchronous so that the first part of it can
# happen synchronously. If the whole call were async we wouldn't be
# able to guarantee that messages sent in order would actually go
# out in order.
raw_response_awaitable = self.fetch_raw_response_async(
bound_obj=bound_obj,
message=message,
)
# Now return an awaitable that will finish the send.
return self._send_async_awaitable(
bound_obj, message, raw_response_awaitable
)
async def _send_async_awaitable(
self,
bound_obj: Any,
message: Message,
raw_response_awaitable: Awaitable[Response | SysResponse],
) -> Response | None:
return self.unpack_raw_response( return self.unpack_raw_response(
bound_obj=bound_obj, bound_obj=bound_obj,
message=message, message=message,
raw_response=await self.fetch_raw_response_async( raw_response=await raw_response_awaitable,
bound_obj=bound_obj,
message=message,
),
) )
def fetch_raw_response( def fetch_raw_response(
@ -186,19 +212,23 @@ class MessageSender:
return response return response
return self._decode_raw_response(bound_obj, message, response_encoded) return self._decode_raw_response(bound_obj, message, response_encoded)
async def fetch_raw_response_async( def fetch_raw_response_async(
self, bound_obj: Any, message: Message self, bound_obj: Any, message: Message
) -> Response | SysResponse: ) -> Awaitable[Response | SysResponse]:
"""Fetch a raw message response. """Fetch a raw message response awaitable.
The result of this should be passed to unpack_raw_response() to The result of this should be awaited and then passed to
produce the final message result. unpack_raw_response() to produce the final message result.
Generally you can just call send(); calling fetch and unpack Generally you can just call send(); calling fetch and unpack
manually is for when message sending and response handling need manually is for when message sending and response handling need
to happen in different contexts/threads. to happen in different contexts/threads.
""" """
# Note: This call is synchronous so that the first part of it can
# happen synchronously. If the whole call were async we wouldn't be
# able to guarantee that messages sent in order would actually go
# out in order.
if ( if (
self._send_async_raw_message_call is None self._send_async_raw_message_call is None
and self._send_async_raw_message_ex_call is None and self._send_async_raw_message_ex_call is None
@ -208,14 +238,42 @@ class MessageSender:
msg_encoded = self._encode_message(bound_obj, message) msg_encoded = self._encode_message(bound_obj, message)
try: try:
if self._send_async_raw_message_ex_call is not None: if self._send_async_raw_message_ex_call is not None:
response_encoded = await self._send_async_raw_message_ex_call( send_awaitable = self._send_async_raw_message_ex_call(
bound_obj, msg_encoded, message bound_obj, msg_encoded, message
) )
else: else:
assert self._send_async_raw_message_call is not None assert self._send_async_raw_message_call is not None
response_encoded = await self._send_async_raw_message_call( send_awaitable = self._send_async_raw_message_call(
bound_obj, msg_encoded bound_obj, msg_encoded
) )
except Exception as exc:
return self._error_awaitable(exc)
# Now return an awaitable to finish the job.
return self._fetch_raw_response_awaitable(
bound_obj, message, send_awaitable
)
async def _error_awaitable(self, exc: Exception) -> SysResponse:
response = ErrorSysResponse(
error_message='Error in MessageSender @send_async_method.',
error_type=(
ErrorSysResponse.ErrorType.COMMUNICATION
if isinstance(exc, CommunicationError)
else ErrorSysResponse.ErrorType.LOCAL
),
)
# Can include the actual exception since we'll be looking at
# this locally; might be helpful.
response.set_local_exception(exc)
return response
async def _fetch_raw_response_awaitable(
self, bound_obj: Any, message: Message, send_awaitable: Awaitable[str]
) -> Response | SysResponse:
try:
response_encoded = await send_awaitable
except Exception as exc: except Exception as exc:
response = ErrorSysResponse( response = ErrorSysResponse(
error_message='Error in MessageSender @send_async_method.', error_message='Error in MessageSender @send_async_method.',
@ -378,23 +436,23 @@ class BoundMessageSender:
assert self._obj is not None assert self._obj is not None
return self._sender.send(bound_obj=self._obj, message=message) return self._sender.send(bound_obj=self._obj, message=message)
async def send_async_untyped(self, message: Message) -> Response | None: def send_async_untyped(
self, message: Message
) -> Awaitable[Response | None]:
"""Send a message asynchronously. """Send a message asynchronously.
Whenever possible, use the send_async() call provided by generated Whenever possible, use the send_async() call provided by generated
subclasses instead of this; it will provide better type safety. subclasses instead of this; it will provide better type safety.
""" """
assert self._obj is not None assert self._obj is not None
return await self._sender.send_async( return self._sender.send_async(bound_obj=self._obj, message=message)
bound_obj=self._obj, message=message
)
async def fetch_raw_response_async_untyped( def fetch_raw_response_async_untyped(
self, message: Message self, message: Message
) -> Response | SysResponse: ) -> Awaitable[Response | SysResponse]:
"""Split send (part 1 of 2).""" """Split send (part 1 of 2)."""
assert self._obj is not None assert self._obj is not None
return await self._sender.fetch_raw_response_async( return self._sender.fetch_raw_response_async(
bound_obj=self._obj, message=message bound_obj=self._obj, message=message
) )

View File

@ -323,12 +323,12 @@ class RPCEndpoint:
if self.debug_print: if self.debug_print:
self.debug_print_call(f'{self._label}: finished.') self.debug_print_call(f'{self._label}: finished.')
async def send_message( def send_message(
self, self,
message: bytes, message: bytes,
timeout: float | None = None, timeout: float | None = None,
close_on_error: bool = True, close_on_error: bool = True,
) -> bytes: ) -> Awaitable[bytes]:
"""Send a message to the peer and return a response. """Send a message to the peer and return a response.
If timeout is not provided, the default will be used. If timeout is not provided, the default will be used.
@ -340,7 +340,10 @@ class RPCEndpoint:
respect to a given endpoint. Pass close_on_error=False to respect to a given endpoint. Pass close_on_error=False to
override this for a particular message. override this for a particular message.
""" """
# pylint: disable=too-many-branches # Note: This call is synchronous so that the first part of it
# (enqueueing outgoing messages) happens synchronously. If it were
# a pure async call it could be possible for send order to vary
# based on how the async tasks get processed.
if self.debug_print_io: if self.debug_print_io:
self.debug_print_call( self.debug_print_call(
@ -358,16 +361,6 @@ class RPCEndpoint:
f'{self._label}: have peerinfo? {self._peer_info is not None}.' f'{self._label}: have peerinfo? {self._peer_info is not None}.'
) )
# We need to know their protocol, so if we haven't gotten a handshake
# from them yet, just wait.
while self._peer_info is None:
await asyncio.sleep(0.01)
assert self._peer_info is not None
if self._peer_info.protocol == 1:
if len(message) > 65535:
raise RuntimeError('Message cannot be larger than 65535 bytes')
# message_id is a 16 bit looping value. # message_id is a 16 bit looping value.
message_id = self._next_message_id message_id = self._next_message_id
self._next_message_id = (self._next_message_id + 1) % 65536 self._next_message_id = (self._next_message_id + 1) % 65536
@ -420,8 +413,35 @@ class RPCEndpoint:
if timeout is None: if timeout is None:
timeout = self.DEFAULT_MESSAGE_TIMEOUT timeout = self.DEFAULT_MESSAGE_TIMEOUT
assert timeout is not None assert timeout is not None
bytes_awaitable = msgobj.wait_task
# Now complete the send asynchronously.
return self._send_message(
message, timeout, close_on_error, bytes_awaitable, message_id
)
async def _send_message(
self,
message: bytes,
timeout: float | None,
close_on_error: bool,
bytes_awaitable: asyncio.Task[bytes],
message_id: int,
) -> bytes:
# We need to know their protocol, so if we haven't gotten a handshake
# from them yet, just wait.
while self._peer_info is None:
await asyncio.sleep(0.01)
assert self._peer_info is not None
if self._peer_info.protocol == 1:
if len(message) > 65535:
raise RuntimeError('Message cannot be larger than 65535 bytes')
try: try:
return await asyncio.wait_for(msgobj.wait_task, timeout=timeout) return await asyncio.wait_for(bytes_awaitable, timeout=timeout)
except asyncio.CancelledError as exc: except asyncio.CancelledError as exc:
# Question: we assume this means the above wait_for() was # Question: we assume this means the above wait_for() was
# cancelled; how do we distinguish between this and *us* being # cancelled; how do we distinguish between this and *us* being
@ -449,7 +469,7 @@ class RPCEndpoint:
) )
# Stop waiting on the response. # Stop waiting on the response.
msgobj.wait_task.cancel() bytes_awaitable.cancel()
# Remove the record of this message. # Remove the record of this message.
del self._in_flight_messages[message_id] del self._in_flight_messages[message_id]