diff --git a/.efrocachemap b/.efrocachemap
index f2d000a2..d2780876 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -4001,52 +4001,52 @@
"assets/build/workspace/onslaughtplug.py": "https://files.ballistica.net/cache/ba1/08/ed/d671c39a3ece6362a6d985112c8e",
"assets/build/workspace/runaroundplug.py": "https://files.ballistica.net/cache/ba1/4d/71/1292911f5369bdb83ef6a34921c0",
"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",
+ "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/1c/77/ac670a5118abdf8a7687af0e159b",
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
- "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c0/b2/ff130457d068b900d961e2354adc",
- "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3e/6f/e5eb8e0646c8a56d81a744b83033",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/bc/a0/8801adcaae0ebf8e065e50df88e5",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/60/0d/a08147b59d213ef1a61b6a393997",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c0/16/798fbe8f0e563c2a1158f612df18",
- "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b0/f6/6db11e5da130e85b59c1e99f34d2",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c1/95/30cb68910669c012bf5f3e0f5d8d",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9d/53/124cd00020647d0ea73114368d9f",
- "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e4/a0/14debf6938db7d2db3e6a66dd51a",
- "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/af/4d/9d01c0b54f17152439ff229585a8",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d3/ef/35e69f1dc7e0d2ec40814169ee16",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/46/77/675fc277cb7d0b4362b4ad002dbd",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/75/de/899f4b4c7864a775a918735d2050",
- "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ac/31/75bab23c1617aaf9c07c428ee477",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/18/0c/ff933dc5901512c7bcd8fc9d0c2a",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/5d/ad/992a5b10cfd2fb13ce402d64e672",
- "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/3f/3e/d7ffee3c48c5e7e61432274c303c",
- "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d2/aa/ac8b35f42058ce4accd33f689366",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/f7/d9/037846d00c1491e4d09609cbbec6",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/ae/47/d79b5b8c9168944641514072c56c",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a0/2a/df32b0c34525af7de212e9a2cf30",
- "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8f/28/f103463730f785ba4fe6a91f8943",
- "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/29/4a/0df587009671edf52d6939c8f966",
- "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c8/df/12ea6b0a703568e52a84001ca2db",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fb/29/5a74e41f2aa46aaa4ebb62b2612f",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d0/89/1995ef2194458c78c546698de761",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7c/7e/01b2fbad142edaac0c0af7853874",
- "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9c/2f/3d0f4b2439b7da9def68597c14c8",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/26/d8/2a352f23af0375e97752da915d38",
- "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a6/0f/72032ea268956736a6eabce738c3",
- "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/92/5d/a8e2b0137a4e67821fc0c233704f",
- "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/01/7c/c21ceaa90f0a0794979056c0a5dc",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/60/4a/238ad7b1d8b5cac2376e58cdd6de",
- "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/27/b6/676cee2d1996ab592e1006ba4721",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b1/fe/bf79be22fc78040b28b60aa2a8e3",
- "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/37/3e/689be0ffce83ac46775c5dec6446",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/ff/e0/707a7f27737a95cd484a8105cb1e",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/22/40/9c60cea175b1685b6ceb49825753",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/b7/2a/6bf91a3cce672678d4313535e5f8",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/31/97/77ee5fbf99340b8a5368b691fffb",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/de/9c/7232b3307214ae9b18c7c7673afa",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/ec/e8/a98b515b312ab875ee58464050e8",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/21/f0/41a934523b6b34ba5f9e9cee96c6",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/43/97/158153da90bd41bba18cd245a6be",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e8/7b/1dbe9dbf1819b08421362d3203cc",
+ "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8a/16/4ab8922ba3524473860b9e3c3fd0",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ee/55/a23141dd32ae2849664267fc24b3",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/af/c7/5e8d3e6873382d7f9b2e6ecb8273",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/2e/8e/b4561b47b90255ac660612516890",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/05/c0/dd90b84f7fe0a31fdaebd4e5532e",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3d/29/52ca62a01b5df84af8c2578646d2",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/88/17/f7891bf7ba6158598260160df7ba",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/13/7c/ec7e0bebd3608bde2dc6434cc3d1",
+ "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d3/16/ccefa4342be01801c1e152f76aee",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ef/45/e1f2aa107b62245c23ae613022fe",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/aa/d7/65771c73cea67f0549c1a5f69b0b",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/56/a2/f0ad7ca2cd6a99c33dab76b6f90a",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/48/a6/dbe19639b711ff8c5030eec054b4",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0f/c8/b332d9bc4dd72347613a0a50aacf",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a3/9b/26a032924048ae2a2d94eec53c78",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/b4/04/7be7602bcb91eee479402875feb7",
+ "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/52/ba/0aba5f9247a0e7af5738d6487df3",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/42/45/4cc46885f0d29c62b4b08be440c6",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/4d/a0/37cfb3c48f7a7026638d68e07855",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fc/d7/66e38e026edcd53ab470c74d7224",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9a/42/7ddd583f9c874837a4a7f902953f",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/22/4a/7771e45d6f8316d4c738d186062e",
+ "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/6b/09b134e2f9b8bd276ff6351b74ee",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4e/d2/43048339ea0b88895e0e6c206f20",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/78/cd/021300c2cacc307bdda1763d9845",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f4/b2/9d68ec3b7c0981efed766e0dfdd7",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1a/f2/4bbf3362f5735492e1bb27606013",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e6/df/d9e68d3b7b1593a3400b1d2b14c7",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e0/e0/58c6709938ed40089213be8c7e37",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/29/d9/2c1234aaccc1c0d50962d7d61f9f",
+ "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5d/ae/45084efc0b90744bcfed29112223",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/62/92/0bd650555ce0042a9c56c4a0751f",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/be/8b/c9423cd7566404e2e74d0db50506",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cf/15/c8ce7acd9c5a973d6886771c099e",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fa/59/26dc8860ecd9644b725e9428a312",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/91/39/6ff63efde3ae2c98474712f78b56",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/18/f6/bdf379aadd643ac3ae46094d8545",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/91/c3/9fe6f6916de5e8086f3e1c9c0c1f",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/41/c0/730d363fbccc0639ac24f7318d32",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/18/82/af34d2eececb1b43018d3016f54c",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/86/2f/a7abcfde5205ba2fdbf844decffb",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/a7/ab/854144e721101e2eb404ff0a6d51",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/f4/92/e786733e776c2b63e9f5340f88d8",
"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 e82d8857..f7ed8256 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -1063,6 +1063,7 @@
getopt
getplayer
getpt
+ getr
getrefs
getremote
getres
@@ -1738,6 +1739,7 @@
objid
objname
objs
+ objsizes
objt
objtoyaml
objtype
@@ -1851,6 +1853,7 @@
pdoc
pedit
peditui
+ peerinfo
peername
pentry
perma
@@ -1972,6 +1975,7 @@
printobjects
printpaths
printrefs
+ printsizes
printtypes
priv
privatetab
@@ -2335,6 +2339,8 @@
sitebuiltins
skey
sline
+ slist
+ slists
slval
smag
smallscale
@@ -2646,6 +2652,8 @@
toplevel
toplevelfiles
totaldudes
+ totalobjmb
+ totalobjsize
totalpts
totaltime
totalwaves
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d517798..b28a7eb3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
-### 1.7.11 (build 20899, api 7, 2022-10-10)
+### 1.7.11 (build 20909, api 7, 2022-10-15)
- Switched our Python autoformatting from yapf to black. The yapf project seems to be mostly dead whereas black seems to be thriving. The final straw was yapf not supporting the `match` statement in Python 3.10.
+- Added `has_settings_ui()` and `show_settings_ui()` methods to ba.Plugin. Plugins can use these to enable a 'Settings' button next to them in the plugin manager that brings up a custom UI.
+- Fixed workspaces functionality, which I broke rather terribly in 1.7.10 when I forgot to test it against all the internal changes there (sorry). Note that there is a slight downside to having workspace syncing enabled now in that it turns off the fast-v2-relaunch-login optimization from 1.7.10.
+- App should now show a message when workspace has been changed and a restart is needed for it to take effect.
+- Fixed an issue where `ba.open_url()` would fall back to internal url display window on some newer Android versions instead of opening a browser. It should now correctly open a browser on regular Android. On AndroidTV/iiRcade/VR it will now always display the internal pop-up. It was trying to use fancy logic before to determine if a browser was available but this seemed to be flaky. Holler if this is not working well on your device/situation.
+- The internal 'fallback' `ba.open_url()` window which shows a url string when a system browser is not available now has a qrcode and a copy button (where copy/paste is supported).
+- Added a 'force_internal' arg to `ba.open_url()` if you would like to always use the internal window instead of attempting to open a browser. Now that we show a copy button and qr code there are some cases where this may be desirable.
### 1.7.10 (build 20895, api 7, 2022-10-09)
- 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.
diff --git a/assets/src/ba_data/python/._ba_sources_hash b/assets/src/ba_data/python/._ba_sources_hash
index 8067bbe6..62f1f485 100644
--- a/assets/src/ba_data/python/._ba_sources_hash
+++ b/assets/src/ba_data/python/._ba_sources_hash
@@ -1 +1 @@
-180157672676216986210895241045962795292
\ No newline at end of file
+31242320059036633417109806113241486230
\ No newline at end of file
diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py
index 7b3bdcb5..b1ef6d75 100644
--- a/assets/src/ba_data/python/_ba.py
+++ b/assets/src/ba_data/python/_ba.py
@@ -2609,14 +2609,15 @@ def open_file_externally(path: str) -> None:
return None
-def open_url(address: str) -> None:
+def open_url(address: str, force_internal: bool = False) -> None:
"""Open a provided URL.
Category: **General Utility Functions**
Open the provided url in a web-browser, or display the URL
- string in a window if that isn't possible.
+ string in a window if that isn't possible (or if force_internal
+ is True).
"""
return None
diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py
index 34e33c98..290d8482 100644
--- a/assets/src/ba_data/python/ba/__init__.py
+++ b/assets/src/ba_data/python/ba/__init__.py
@@ -64,6 +64,7 @@ from _ba import (
getdata,
in_logic_thread,
)
+from ba._accountv2 import AccountV2Handle
from ba._activity import Activity
from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem
from ba._actor import Actor
@@ -187,6 +188,7 @@ from ba._collision import Collision, getcollision
app: App
__all__ = [
+ 'AccountV2Handle',
'Achievement',
'AchievementSubsystem',
'Activity',
diff --git a/assets/src/ba_data/python/ba/_accountv2.py b/assets/src/ba_data/python/ba/_accountv2.py
index e05a1e6f..77b4ecdd 100644
--- a/assets/src/ba_data/python/ba/_accountv2.py
+++ b/assets/src/ba_data/python/ba/_accountv2.py
@@ -77,6 +77,7 @@ class AccountV2Subsystem:
):
self._kicked_off_workspace_load = True
_ba.app.workspaces.set_active_workspace(
+ account=account,
workspaceid=account.workspaceid,
workspacename=account.workspacename,
on_completed=self._on_set_active_workspace_completed,
diff --git a/assets/src/ba_data/python/ba/_bootstrap.py b/assets/src/ba_data/python/ba/_bootstrap.py
index e3238519..a9fc7436 100644
--- a/assets/src/ba_data/python/ba/_bootstrap.py
+++ b/assets/src/ba_data/python/ba/_bootstrap.py
@@ -47,7 +47,7 @@ def bootstrap() -> None:
# Give a soft warning if we're being used with a different binary
# version than we expect.
- expected_build = 20899
+ expected_build = 20909
running_build: int = env['build_number']
if running_build != expected_build:
print(
diff --git a/assets/src/ba_data/python/ba/_plugin.py b/assets/src/ba_data/python/ba/_plugin.py
index efe1ef69..3e896887 100644
--- a/assets/src/ba_data/python/ba/_plugin.py
+++ b/assets/src/ba_data/python/ba/_plugin.py
@@ -222,3 +222,10 @@ class Plugin:
def on_app_shutdown(self) -> None:
"""Called before closing the application."""
+
+ def has_settings_ui(self) -> bool:
+ """Called to ask if we have settings UI we can show."""
+ return False
+
+ def show_settings_ui(self, source_widget: ba.Widget | None) -> None:
+ """Called to show our settings UI."""
diff --git a/assets/src/ba_data/python/ba/_workspace.py b/assets/src/ba_data/python/ba/_workspace.py
index 33b3b702..c0997396 100644
--- a/assets/src/ba_data/python/ba/_workspace.py
+++ b/assets/src/ba_data/python/ba/_workspace.py
@@ -36,6 +36,7 @@ class WorkspaceSubsystem:
def set_active_workspace(
self,
+ account: ba.AccountV2Handle,
workspaceid: str,
workspacename: str,
on_completed: Callable[[], None],
@@ -46,6 +47,7 @@ class WorkspaceSubsystem:
# interactivity.
Thread(
target=lambda: self._set_active_workspace_bg(
+ account=account,
workspaceid=workspaceid,
workspacename=workspacename,
on_completed=on_completed,
@@ -63,6 +65,7 @@ class WorkspaceSubsystem:
def _set_active_workspace_bg(
self,
+ account: ba.AccountV2Handle,
workspaceid: str,
workspacename: str,
on_completed: Callable[[], None],
@@ -91,11 +94,12 @@ class WorkspaceSubsystem:
state = bacommon.cloud.WorkspaceFetchState(manifest=manifest)
while True:
- response = _ba.app.cloud.send_message(
- bacommon.cloud.WorkspaceFetchMessage(
- workspaceid=workspaceid, state=state
+ with account:
+ response = _ba.app.cloud.send_message(
+ bacommon.cloud.WorkspaceFetchMessage(
+ workspaceid=workspaceid, state=state
+ )
)
- )
state = response.state
self._handle_deletes(
workspace_dir=wspath, deletes=response.deletes
diff --git a/assets/src/ba_data/python/bastd/ui/settings/plugins.py b/assets/src/ba_data/python/bastd/ui/settings/plugins.py
index 8213e42a..a8197551 100644
--- a/assets/src/ba_data/python/bastd/ui/settings/plugins.py
+++ b/assets/src/ba_data/python/bastd/ui/settings/plugins.py
@@ -21,6 +21,7 @@ class PluginSettingsWindow(ba.Window):
origin_widget: ba.Widget | None = None,
):
# pylint: disable=too-many-locals
+ # pylint: disable=too-many-statements
app = ba.app
# If they provided an origin-widget, scale up from that.
@@ -113,11 +114,9 @@ class PluginSettingsWindow(ba.Window):
highlight=False,
size=(self._scroll_width, self._scroll_height),
selection_loops_to_parent=True,
+ claims_left_right=True,
)
ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
- self._subcontainer = ba.columnwidget(
- parent=self._scrollwidget, selection_loops_to_parent=True
- )
if ba.app.meta.scanresults is None:
ba.screenmessage(
@@ -127,17 +126,32 @@ class PluginSettingsWindow(ba.Window):
pluglist = ba.app.plugins.potential_plugins
plugstates: dict[str, dict] = ba.app.config.setdefault('Plugins', {})
assert isinstance(plugstates, dict)
+
+ plug_line_height = 50
+ sub_width = self._scroll_width
+ sub_height = len(pluglist) * plug_line_height
+ self._subcontainer = ba.containerwidget(
+ parent=self._scrollwidget,
+ size=(sub_width, sub_height),
+ background=False,
+ )
+
for i, availplug in enumerate(pluglist):
- active = availplug.class_path in ba.app.plugins.active_plugins
+ plugin = ba.app.plugins.active_plugins.get(availplug.class_path)
+ active = plugin is not None
plugstate = plugstates.setdefault(availplug.class_path, {})
checked = plugstate.get('enabled', False)
assert isinstance(checked, bool)
+
+ item_y = sub_height - (i + 1) * plug_line_height
check = ba.checkboxwidget(
parent=self._subcontainer,
text=availplug.display_name,
+ autoselect=True,
value=checked,
- maxwidth=self._scroll_width - 100,
+ maxwidth=self._scroll_width - 200,
+ position=(10, item_y),
size=(self._scroll_width - 40, 50),
on_value_change_call=ba.Call(
self._check_value_changed, availplug
@@ -150,14 +164,35 @@ class PluginSettingsWindow(ba.Window):
else (0.6, 0.6, 0.6)
),
)
+ if plugin is not None and plugin.has_settings_ui():
+ button = ba.buttonwidget(
+ parent=self._subcontainer,
+ label=ba.Lstr(resource='mainMenu.settingsText'),
+ autoselect=True,
+ size=(100, 40),
+ position=(sub_width - 130, item_y + 6),
+ )
+ ba.buttonwidget(
+ edit=button,
+ on_activate_call=ba.Call(plugin.show_settings_ui, button),
+ )
+ else:
+ button = None
+
+ # Allow getting back to back button.
+ if i == 0:
+ ba.widget(
+ edit=check,
+ up_widget=self._back_button,
+ left_widget=self._back_button,
+ )
+ if button is not None:
+ ba.widget(edit=button, up_widget=self._back_button)
# Make sure we scroll all the way to the end when using
# keyboard/button nav.
ba.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40)
- # Keep last from looping to back button when down is pressed.
- if i == len(pluglist) - 1:
- ba.widget(edit=check, down_widget=check)
ba.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
diff --git a/assets/src/ba_data/python/bastd/ui/url.py b/assets/src/ba_data/python/bastd/ui/url.py
index 7b87b2d7..bcecbb95 100644
--- a/assets/src/ba_data/python/bastd/ui/url.py
+++ b/assets/src/ba_data/python/bastd/ui/url.py
@@ -17,52 +17,12 @@ class ShowURLWindow(ba.Window):
# (for long URLs especially)
app = ba.app
uiscale = app.ui.uiscale
- if app.platform == 'android' and app.subplatform == 'alibaba':
- self._width = 500
- self._height = 500
- super().__init__(
- root_widget=ba.containerwidget(
- size=(self._width, self._height),
- transition='in_right',
- scale=(
- 1.25
- if uiscale is ba.UIScale.SMALL
- else 1.25
- if uiscale is ba.UIScale.MEDIUM
- else 1.25
- ),
- )
- )
- self._cancel_button = ba.buttonwidget(
- parent=self._root_widget,
- position=(50, self._height - 30),
- size=(50, 50),
- scale=0.6,
- label='',
- color=(0.6, 0.5, 0.6),
- on_activate_call=self._done,
- autoselect=True,
- icon=ba.gettexture('crossOut'),
- iconscale=1.2,
- )
- qr_size = 400
- ba.imagewidget(
- parent=self._root_widget,
- position=(
- self._width * 0.5 - qr_size * 0.5,
- self._height * 0.5 - qr_size * 0.5,
- ),
- size=(qr_size, qr_size),
- texture=ba.internal.get_qrcode_texture(address),
- )
- ba.containerwidget(
- edit=self._root_widget, cancel_button=self._cancel_button
- )
- else:
- # show it as a simple string...
- self._width = 800
- self._height = 200
- self._root_widget = ba.containerwidget(
+ self._address = address
+
+ self._width = 800
+ self._height = 450
+ super().__init__(
+ root_widget=ba.containerwidget(
size=(self._width, self._height + 40),
transition='in_right',
scale=(
@@ -73,43 +33,77 @@ class ShowURLWindow(ba.Window):
else 1.25
),
)
- ba.textwidget(
- parent=self._root_widget,
- position=(self._width * 0.5, self._height - 10),
- size=(0, 0),
- color=ba.app.ui.title_color,
- h_align='center',
- v_align='center',
- text=ba.Lstr(resource='directBrowserToURLText'),
- maxwidth=self._width * 0.95,
- )
- ba.textwidget(
- parent=self._root_widget,
- position=(self._width * 0.5, self._height * 0.5 + 29),
- size=(0, 0),
- scale=1.3,
- color=ba.app.ui.infotextcolor,
- h_align='center',
- v_align='center',
- text=address,
- maxwidth=self._width * 0.95,
- )
- button_width = 200
+ )
+ ba.textwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5, self._height - 10),
+ size=(0, 0),
+ color=ba.app.ui.title_color,
+ h_align='center',
+ v_align='center',
+ text=ba.Lstr(resource='directBrowserToURLText'),
+ maxwidth=self._width * 0.95,
+ )
+ ba.textwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5, self._height - 60),
+ size=(0, 0),
+ scale=1.3,
+ color=ba.app.ui.infotextcolor,
+ h_align='center',
+ v_align='center',
+ text=address,
+ maxwidth=self._width * 0.95,
+ )
+ button_width = 200
+
+ qr_size = 220
+ ba.imagewidget(
+ parent=self._root_widget,
+ position=(
+ self._width * 0.5 - qr_size * 0.5,
+ self._height * 0.5 - qr_size * 0.5 + 10,
+ ),
+ size=(qr_size, qr_size),
+ texture=ba.internal.get_qrcode_texture(address),
+ )
+
+ xoffs = 0
+ if ba.clipboard_is_supported():
+ xoffs = -150
btn = ba.buttonwidget(
parent=self._root_widget,
- position=(self._width * 0.5 - button_width * 0.5, 20),
+ position=(
+ self._width * 0.5 - button_width * 0.5 + xoffs,
+ 20,
+ ),
size=(button_width, 65),
- label=ba.Lstr(resource='doneText'),
- on_activate_call=self._done,
- )
- # we have no 'cancel' button but still want to be able to
- # hit back/escape/etc to leave..
- ba.containerwidget(
- edit=self._root_widget,
- selected_child=btn,
- start_button=btn,
- on_cancel_call=btn.activate,
+ autoselect=True,
+ label=ba.Lstr(resource='copyText'),
+ on_activate_call=self._copy,
)
+ xoffs = 150
+
+ btn = ba.buttonwidget(
+ parent=self._root_widget,
+ position=(self._width * 0.5 - button_width * 0.5 + xoffs, 20),
+ size=(button_width, 65),
+ autoselect=True,
+ label=ba.Lstr(resource='doneText'),
+ on_activate_call=self._done,
+ )
+ # we have no 'cancel' button but still want to be able to
+ # hit back/escape/etc to leave..
+ ba.containerwidget(
+ edit=self._root_widget,
+ selected_child=btn,
+ start_button=btn,
+ on_cancel_call=btn.activate,
+ )
+
+ def _copy(self) -> None:
+ ba.clipboard_set_text(self._address)
+ ba.screenmessage(ba.Lstr(resource='copyConfirmText'), color=(0, 1, 0))
def _done(self) -> None:
ba.containerwidget(edit=self._root_widget, transition='out_left')
diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
index dd33b099..d2f7fe75 100644
--- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
@@ -567,6 +567,7 @@
getpublicpartyenabled
getpublicpartymaxsize
getqrcodetexture
+ getr
getrefs
getres
getsession
@@ -918,6 +919,7 @@
objb
objexists
objid
+ objsizes
objtoyaml
objtypes
obstack
@@ -988,6 +990,7 @@
pdataclass
pdoc
pdst
+ peerinfo
peername
persp
pflag
@@ -1035,6 +1038,7 @@
printnodes
printobjects
printrefs
+ printsizes
printtypes
priv
privatetab
@@ -1230,6 +1234,8 @@
simpletype
sisssssssss
sixteenbits
+ slist
+ slists
smod
smoothering
smoothstep
@@ -1369,6 +1375,8 @@
tmpmat
tomer
topos
+ totalobjmb
+ totalobjsize
touchpad
toucs
tournamentbutton
diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc
index 4838d50d..ab54da9c 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 = 20899;
+const int kAppBuildNumber = 20909;
const char* kAppVersion = "1.7.11";
// Our standalone globals.
diff --git a/src/ballistica/python/methods/python_methods_ui.cc b/src/ballistica/python/methods/python_methods_ui.cc
index f5a7a6cc..cf1cc08e 100644
--- a/src/ballistica/python/methods/python_methods_ui.cc
+++ b/src/ballistica/python/methods/python_methods_ui.cc
@@ -2159,13 +2159,19 @@ auto PyBackPress(PyObject* self, PyObject* args, PyObject* keywds)
auto PyOpenURL(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* {
BA_PYTHON_TRY;
const char* address = nullptr;
- static const char* kwlist[] = {"address", nullptr};
- if (!PyArg_ParseTupleAndKeywords(args, keywds, "s",
- const_cast(kwlist), &address)) {
+ int force_internal{0};
+ static const char* kwlist[] = {"address", "force_internal", nullptr};
+ if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|p",
+ const_cast(kwlist), &address,
+ &force_internal)) {
return nullptr;
}
assert(g_app_flavor);
- g_app_flavor->PushOpenURLCall(address);
+ if (force_internal) {
+ g_logic->PushShowURLCall(address);
+ } else {
+ g_app_flavor->PushOpenURLCall(address);
+ }
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
@@ -2271,14 +2277,15 @@ auto PythonMethodsUI::GetMethods() -> std::vector {
"Open the provided file in the default external app."},
{"open_url", (PyCFunction)PyOpenURL, METH_VARARGS | METH_KEYWORDS,
- "open_url(address: str) -> None\n"
+ "open_url(address: str, force_internal: bool = False) -> None\n"
"\n"
"Open a provided URL.\n"
"\n"
"Category: **General Utility Functions**\n"
"\n"
"Open the provided url in a web-browser, or display the URL\n"
- "string in a window if that isn't possible.\n"},
+ "string in a window if that isn't possible (or if force_internal\n"
+ "is True).\n"},
{"back_press", (PyCFunction)PyBackPress, METH_VARARGS | METH_KEYWORDS,
"back_press() -> None\n"
diff --git a/tests/test_efro/test_rpc.py b/tests/test_efro/test_rpc.py
index 6f23de65..aa5a795a 100644
--- a/tests/test_efro/test_rpc.py
+++ b/tests/test_efro/test_rpc.py
@@ -432,7 +432,7 @@ def test_server_interrupt() -> None:
await asyncio.sleep(0.2)
tester.server.endpoint.close()
- asyncio.create_task(_kill_connection())
+ _task = asyncio.create_task(_kill_connection())
with pytest.raises(CommunicationError):
await tester.server.send_message(_Message(_MessageType.TEST_SLOW))
@@ -448,7 +448,7 @@ def test_client_interrupt() -> None:
await asyncio.sleep(0.2)
tester.client.endpoint.close()
- asyncio.create_task(_kill_connection())
+ _task = asyncio.create_task(_kill_connection())
with pytest.raises(CommunicationError):
await tester.server.send_message(_Message(_MessageType.TEST_SLOW))
diff --git a/tools/batools/assetstaging.py b/tools/batools/assetstaging.py
index 481fb47a..7d6e542c 100755
--- a/tools/batools/assetstaging.py
+++ b/tools/batools/assetstaging.py
@@ -429,15 +429,12 @@ def _sync_server_files(cfg: Config) -> None:
outfilename=os.path.join(cfg.serverdst, 'config_template.yaml'),
)
if cfg.win_type is not None:
+ fname = 'launch_ballisticacore_server.bat'
stage_server_file(
projroot=cfg.projroot,
mode=modeval,
- infilename=(
- f'{cfg.src}/../src/server/launch_ballisticacore_server.bat'
- ),
- outfilename=os.path.join(
- cfg.serverdst, 'launch_ballisticacore_server.bat'
- ),
+ infilename=f'{cfg.src}/../src/server/{fname}',
+ outfilename=os.path.join(cfg.serverdst, fname),
)
diff --git a/tools/batools/pcommand.py b/tools/batools/pcommand.py
index 3552838b..2caa95e7 100644
--- a/tools/batools/pcommand.py
+++ b/tools/batools/pcommand.py
@@ -183,9 +183,11 @@ def lazy_increment_build() -> None:
except FileNotFoundError:
lasthash = ''
if codehash != lasthash:
- print(f'{Clr.SMAG}Source(s) changed; incrementing build...{Clr.RST}')
if not update_hash_only:
+ print(
+ f'{Clr.SMAG}Source(s) changed; incrementing build...{Clr.RST}'
+ )
# Just go ahead and bless; this will increment the build as needed.
# subprocess.run(['make', 'bless'], check=True)
subprocess.run(
@@ -1029,11 +1031,10 @@ def win_ci_install_prereqs() -> None:
# build to succeed. Normally this would happen through our Makefile
# targets but we can't use them under raw window so we need to just
# hard-code whatever we need here.
+ lib_dbg_win32 = 'build/prefab/lib/windows/Debug_Win32'
needed_targets: set[str] = {
- 'build/prefab/lib/windows/Debug_Win32/'
- 'BallisticaCoreGenericInternal.lib',
- 'build/prefab/lib/windows/Debug_Win32/'
- 'BallisticaCoreGenericInternal.pdb',
+ f'{lib_dbg_win32}/BallisticaCoreGenericInternal.lib',
+ f'{lib_dbg_win32}/BallisticaCoreGenericInternal.pdb',
'ballisticacore-windows/Generic/BallisticaCore.ico',
}
diff --git a/tools/batools/project.py b/tools/batools/project.py
index ee54eb59..48c4d561 100755
--- a/tools/batools/project.py
+++ b/tools/batools/project.py
@@ -703,8 +703,7 @@ class Updater:
# CMake android components:
fname = (
- 'ballisticacore-android/BallisticaCore'
- '/src/main/cpp/CMakeLists.txt'
+ 'ballisticacore-android/BallisticaCore/src/main/cpp/CMakeLists.txt'
)
if not self._public:
self._update_cmake_file(fname)
diff --git a/tools/batools/pythonenumsmodule.py b/tools/batools/pythonenumsmodule.py
index 3ea4de93..bdc7c176 100755
--- a/tools/batools/pythonenumsmodule.py
+++ b/tools/batools/pythonenumsmodule.py
@@ -44,7 +44,7 @@ def _gen_enums(infilename: str) -> str:
out += f'\n\nclass {enum_name}(Enum):\n """'
out += '\n '.join(doclines)
if len(doclines) > 1:
- out += '\n """\n'
+ out += '\n """\n\n'
else:
out += '"""\n'
diff --git a/tools/efro/debug.py b/tools/efro/debug.py
index e5372863..74a72e0e 100644
--- a/tools/efro/debug.py
+++ b/tools/efro/debug.py
@@ -27,7 +27,9 @@ ABS_MAX_LEVEL = 10
# we're showing some temporary objects that we should be ignoring.
-def getobjs(cls: type | str, contains: str | None = None) -> list[Any]:
+def getobjs(
+ cls: type | str, contains: str | None = None, expanded: bool = False
+) -> list[Any]:
"""Return all garbage-collected objects matching criteria.
'type' can be an actual type or a string in which case objects
@@ -45,17 +47,51 @@ def getobjs(cls: type | str, contains: str | None = None) -> list[Any]:
if not isinstance(contains, str | None):
raise TypeError('Expected a string or None for contains')
+ allobjs = _get_all_objects(expanded=expanded)
+
if isinstance(cls, str):
- objs = [o for o in gc.get_objects() if cls in str(type(o))]
+ objs = [o for o in allobjs if cls in str(type(o))]
else:
- objs = [o for o in gc.get_objects() if isinstance(o, cls)]
+ objs = [o for o in allobjs if isinstance(o, cls)]
if contains is not None:
objs = [o for o in objs if contains in str(o)]
return objs
-def getobj(objid: int) -> Any:
+# Recursively expand slists objects into olist, using seen to track
+# already processed objects.
+def _getr(slist: list[Any], olist: list[Any], seen: set[int]) -> None:
+ for obj in slist:
+ if id(obj) in seen:
+ continue
+ seen.add(id(obj))
+ olist.append(obj)
+ tll = gc.get_referents(obj)
+ if tll:
+ _getr(tll, olist, seen)
+
+
+def _get_all_objects(expanded: bool) -> list[Any]:
+ """Return an expanded list of all objects.
+
+ See https://utcc.utoronto.ca/~cks/space/blog/python/GetAllObjects
+ """
+ gcl = gc.get_objects()
+ if not expanded:
+ return gcl
+ olist: list[Any] = []
+ seen: set[int] = set()
+ # Just in case:
+ seen.add(id(gcl))
+ seen.add(id(olist))
+ seen.add(id(seen))
+ # _getr does the real work.
+ _getr(gcl, olist, seen)
+ return olist
+
+
+def getobj(objid: int, expanded: bool = False) -> Any:
"""Return a garbage-collected object by its id.
Remember that this is VERY inefficient and should only ever be used
@@ -65,7 +101,10 @@ def getobj(objid: int) -> Any:
raise TypeError(f'Expected an int for objid; got a {type(objid)}.')
# Don't wanna return stuff waiting to be garbage-collected.
- for obj in gc.get_objects():
+ gc.collect()
+
+ allobjs = _get_all_objects(expanded=expanded)
+ for obj in allobjs:
if id(obj) == objid:
return obj
raise RuntimeError(f'Object with id {objid} not found.')
@@ -145,12 +184,14 @@ def printrefs(
)
-def printtypes(limit: int = 50, file: TextIO | None = None) -> None:
+def printtypes(
+ limit: int = 50, file: TextIO | None = None, expanded: bool = False
+) -> None:
"""Print a human readable list of which types have the most instances."""
assert limit > 0
objtypes: dict[str, int] = {}
gc.collect() # Recommended before get_objects().
- allobjs = gc.get_objects()
+ allobjs = _get_all_objects(expanded=expanded)
allobjc = len(allobjs)
for obj in allobjs:
modname = type(obj).__module__
@@ -175,6 +216,38 @@ def printtypes(limit: int = 50, file: TextIO | None = None) -> None:
print(f'{i+1}: {tpname}: {tpval} ({percent:.2f}%)', file=file)
+def printsizes(
+ limit: int = 50, file: TextIO | None = None, expanded: bool = False
+) -> None:
+ """Print total allocated sizes of different types."""
+ assert limit > 0
+ objsizes: dict[str, int] = {}
+ gc.collect() # Recommended before get_objects().
+ allobjs = _get_all_objects(expanded=expanded)
+ totalobjsize = 0
+
+ for obj in allobjs:
+ modname = type(obj).__module__
+ tpname = type(obj).__qualname__
+ if modname != 'builtins':
+ tpname = f'{modname}.{tpname}'
+ objsize = sys.getsizeof(obj)
+ objsizes[tpname] = objsizes.get(tpname, 0) + objsize
+ totalobjsize += objsize
+
+ totalobjmb = totalobjsize / (1024 * 1024)
+ print(
+ f'Types with most allocated bytes ({totalobjmb:.2f} mb total):',
+ file=file,
+ )
+ for i, tpitem in enumerate(
+ sorted(objsizes.items(), key=lambda x: x[1], reverse=True)[:limit]
+ ):
+ tpname, tpval = tpitem
+ percent = tpval / totalobjsize * 100.0
+ print(f'{i+1}: {tpname}: {tpval} ({percent:.2f}%)', file=file)
+
+
def _desctype(obj: Any) -> str:
cls = type(obj)
if cls is types.ModuleType:
diff --git a/tools/efro/log.py b/tools/efro/log.py
index 10f2a5f9..3cc9230f 100644
--- a/tools/efro/log.py
+++ b/tools/efro/log.py
@@ -292,7 +292,8 @@ class LogHandler(logging.Handler):
self._file_chunk_ship_task[
name
] = self._event_loop.create_task(
- self._ship_chunks_task(name)
+ self._ship_chunks_task(name),
+ name='log ship file chunks',
)
except Exception:
@@ -321,7 +322,6 @@ class LogHandler(logging.Handler):
traceback.print_exc(file=self._echofile)
async def _ship_chunks_task(self, name: str) -> None:
- await asyncio.sleep(0.1)
self._ship_file_chunks(name, cancel_ship_task=False)
def _ship_file_chunks(self, name: str, cancel_ship_task: bool) -> None:
diff --git a/tools/efro/rpc.py b/tools/efro/rpc.py
index 051555b4..ba5d1d22 100644
--- a/tools/efro/rpc.py
+++ b/tools/efro/rpc.py
@@ -88,6 +88,10 @@ def ssl_stream_writer_force_close_check(writer: asyncio.StreamWriter) -> None:
from efro.call import tpartial
from threading import Thread
+ # Disabling for now..
+ if bool(True):
+ return
+
# Hopefully can remove this in Python 3.11?...
# see issue with is_closing() below for more details.
transport = getattr(writer, '_transport', None)
@@ -128,7 +132,9 @@ class _InFlightMessage:
def __init__(self) -> None:
self._response: bytes | None = None
self._got_response = asyncio.Event()
- self.wait_task = asyncio.create_task(self._wait())
+ self.wait_task = asyncio.create_task(
+ self._wait(), name='rpc in flight msg wait'
+ )
async def _wait(self) -> bytes:
await self._got_response.wait()
@@ -185,11 +191,11 @@ class RPCEndpoint:
self._handle_raw_message_call = handle_raw_message_call
self._reader = reader
self._writer = writer
- self._debug_print = debug_print
- self._debug_print_io = debug_print_io
+ self.debug_print = debug_print
+ self.debug_print_io = debug_print_io
if debug_print_call is None:
debug_print_call = print
- self._debug_print_call: Callable[[str], None] = debug_print_call
+ self.debug_print_call: Callable[[str], None] = debug_print_call
self._label = label
self._thread = current_thread()
self._closing = False
@@ -207,7 +213,7 @@ class RPCEndpoint:
# Need to hold weak-refs to these otherwise it creates dep-loops
# which keeps us alive.
- self._tasks: list[weakref.ref[asyncio.Task]] = []
+ self._tasks: list[asyncio.Task] = []
# When we last got a keepalive or equivalent (time.monotonic value)
self._last_keepalive_receive_time: float | None = None
@@ -217,9 +223,9 @@ class RPCEndpoint:
self._in_flight_messages: dict[int, _InFlightMessage] = {}
- if self._debug_print:
+ if self.debug_print:
peername = self._writer.get_extra_info('peername')
- self._debug_print_call(
+ self.debug_print_call(
f'{self._label}: connected to {peername} at {self._tm()}.'
)
@@ -270,16 +276,19 @@ class RPCEndpoint:
core_tasks = [
asyncio.create_task(
- self._run_core_task('keepalive', self._run_keepalive_task())
+ self._run_core_task('keepalive', self._run_keepalive_task()),
+ name='rpc keepalive',
),
asyncio.create_task(
- self._run_core_task('read', self._run_read_task())
+ self._run_core_task('read', self._run_read_task()),
+ name='rpc read',
),
asyncio.create_task(
- self._run_core_task('write', self._run_write_task())
+ self._run_core_task('write', self._run_write_task()),
+ name='rpc write',
),
]
- self._tasks += [weakref.ref(t) for t in core_tasks]
+ self._tasks += core_tasks
# Run our core tasks until they all complete.
results = await asyncio.gather(*core_tasks, return_exceptions=True)
@@ -309,8 +318,8 @@ class RPCEndpoint:
except Exception:
logging.exception('Error closing %s.', self._label)
- if self._debug_print:
- self._debug_print_call(f'{self._label}: finished.')
+ if self.debug_print:
+ self.debug_print_call(f'{self._label}: finished.')
async def send_message(
self,
@@ -330,11 +339,23 @@ class RPCEndpoint:
override this for a particular message.
"""
# pylint: disable=too-many-branches
+
+ if self.debug_print_io:
+ self.debug_print_call(
+ f'{self._label}: sending message of size {len(message)}'
+ f' at {self._tm()}.'
+ )
+
self._check_env()
if self._closing:
raise CommunicationError('Endpoint is closed.')
+ if self.debug_print_io:
+ self.debug_print_call(
+ 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:
@@ -349,6 +370,11 @@ class RPCEndpoint:
message_id = self._next_message_id
self._next_message_id = (self._next_message_id + 1) % 65536
+ if self.debug_print_io:
+ self.debug_print_call(
+ f'{self._label}: will enqueue at {self._tm()}.'
+ )
+
# FIXME - should handle backpressure (waiting here if there are
# enough packets already enqueued).
@@ -371,13 +397,19 @@ class RPCEndpoint:
+ message
)
+ if self.debug_print_io:
+ self.debug_print_call(
+ f'{self._label}: enqueued message of size {len(message)}'
+ f' at {self._tm()}.'
+ )
+
# Make an entry so we know this message is out there.
assert message_id not in self._in_flight_messages
msgobj = self._in_flight_messages[message_id] = _InFlightMessage()
# Also add its task to our list so we properly cancel it if we die.
self._prune_tasks() # Keep our list from filling with dead tasks.
- self._tasks.append(weakref.ref(msgobj.wait_task))
+ self._tasks.append(msgobj.wait_task)
# Note: we always want to incorporate a timeout. Individual
# messages may hang or error on the other end and this ensures
@@ -392,16 +424,17 @@ class RPCEndpoint:
# Question: we assume this means the above wait_for() was
# cancelled; what happens if a task running *us* is cancelled
# though?
- if self._debug_print:
- self._debug_print_call(
+ 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:
- self._debug_print_call(
+ if self.debug_print:
+ self.debug_print_call(
f'{self._label}: message {message_id} timed out.'
)
@@ -424,21 +457,21 @@ class RPCEndpoint:
if self._closing:
return
- if self._debug_print:
- self._debug_print_call(f'{self._label}: closing...')
+ if self.debug_print:
+ self.debug_print_call(f'{self._label}: closing...')
self._closing = True
# Kill all of our in-flight tasks.
- if self._debug_print:
- self._debug_print_call(f'{self._label}: cancelling tasks...')
+ if self.debug_print:
+ self.debug_print_call(f'{self._label}: cancelling tasks...')
for task in self._get_live_tasks():
task.cancel()
# Close our writer.
assert not self._did_close_writer
- if self._debug_print:
- self._debug_print_call(f'{self._label}: closing writer...')
+ if self.debug_print:
+ self.debug_print_call(f'{self._label}: closing writer...')
self._writer.close()
self._did_close_writer = True
@@ -474,8 +507,13 @@ class RPCEndpoint:
)
live_tasks = self._get_live_tasks()
- if self._debug_print:
- self._debug_print_call(
+
+ # Don't need our task list anymore; this should
+ # break any cyclical refs from tasks referring to us.
+ self._tasks = []
+
+ if self.debug_print:
+ self.debug_print_call(
f'{self._label}: waiting for tasks to finish: '
f' ({live_tasks=})...'
)
@@ -498,8 +536,8 @@ class RPCEndpoint:
id(self),
)
- if self._debug_print:
- self._debug_print_call(
+ if self.debug_print:
+ self.debug_print_call(
f'{self._label}: tasks finished; waiting for writer close...'
)
@@ -515,15 +553,19 @@ class RPCEndpoint:
# indefinitely. See https://github.com/python/cpython/issues/83939
# It sounds like this should be fixed in 3.11 but for now just
# forcing the issue with a timeout here.
- await asyncio.wait_for(self._writer.wait_closed(), timeout=30.0)
+ await asyncio.wait_for(
+ self._writer.wait_closed(),
+ # timeout=60.0 * 6.0,
+ timeout=30.0,
+ )
except asyncio.TimeoutError:
logging.info(
'Timeout on _writer.wait_closed() for %s rpc (transport=%s).',
self._label,
ssl_stream_writer_underlying_transport_info(self._writer),
)
- if self._debug_print:
- self._debug_print_call(
+ if self.debug_print:
+ self.debug_print_call(
f'{self._label}: got timeout in _writer.wait_closed();'
' This should be fixed in future Python versions.'
)
@@ -531,8 +573,8 @@ class RPCEndpoint:
if not self._is_expected_connection_error(exc):
logging.exception('Error closing _writer for %s.', self._label)
else:
- if self._debug_print:
- self._debug_print_call(
+ if self.debug_print:
+ self.debug_print_call(
f'{self._label}: silently ignoring error in'
f' _writer.wait_closed(): {exc}.'
)
@@ -555,14 +597,18 @@ class RPCEndpoint:
self._check_env()
assert self._peer_info is None
+ # Bug fix: if we don't have this set we will never time out
+ # if we never receive any data from the other end.
+ self._last_keepalive_receive_time = time.monotonic()
+
# The first thing they should send us is their handshake; then
# we'll know if/how we can talk to them.
mlen = await self._read_int_32()
message = await self._reader.readexactly(mlen)
self._peer_info = dataclass_from_json(_PeerInfo, message.decode())
self._last_keepalive_receive_time = time.monotonic()
- if self._debug_print:
- self._debug_print_call(
+ if self.debug_print:
+ self.debug_print_call(
f'{self._label}: received handshake at {self._tm()}.'
)
@@ -576,8 +622,8 @@ class RPCEndpoint:
raise RuntimeError('Got multiple handshakes')
if mtype is _PacketType.KEEPALIVE:
- if self._debug_print_io:
- self._debug_print_call(
+ if self.debug_print_io:
+ self.debug_print_call(
f'{self._label}: received keepalive'
f' at {self._tm()}.'
)
@@ -606,8 +652,8 @@ class RPCEndpoint:
else:
msglen = await self._read_int_16()
msg = await self._reader.readexactly(msglen)
- if self._debug_print_io:
- self._debug_print_call(
+ if self.debug_print_io:
+ self.debug_print_call(
f'{self._label}: received message {msgid}'
f' of size {msglen} at {self._tm()}.'
)
@@ -617,14 +663,13 @@ class RPCEndpoint:
assert not self._closing
self._prune_tasks() # Keep from filling with dead tasks.
self._tasks.append(
- weakref.ref(
- asyncio.create_task(
- self._handle_raw_message(message_id=msgid, message=msg)
- )
+ asyncio.create_task(
+ self._handle_raw_message(message_id=msgid, message=msg),
+ name='efro rpc message handle',
)
)
- if self._debug_print:
- self._debug_print_call(
+ if self.debug_print:
+ self.debug_print_call(
f'{self._label}: done handling message at {self._tm()}.'
)
@@ -636,8 +681,8 @@ class RPCEndpoint:
rsplen = await self._read_int_32()
else:
rsplen = await self._read_int_16()
- if self._debug_print_io:
- self._debug_print_call(
+ if self.debug_print_io:
+ self.debug_print_call(
f'{self._label}: received response {msgid}'
f' of size {rsplen} at {self._tm()}.'
)
@@ -647,8 +692,8 @@ class RPCEndpoint:
# It's possible for us to get a response to a message
# that has timed out. In this case we will have no local
# record of it.
- if self._debug_print:
- self._debug_print_call(
+ if self.debug_print:
+ self.debug_print_call(
f'{self._label}: got response for nonexistent'
f' message id {msgid}; perhaps it timed out?'
)
@@ -730,9 +775,9 @@ class RPCEndpoint:
and now - self._last_keepalive_receive_time
> self._keepalive_timeout
):
- if self._debug_print:
+ if self.debug_print:
since = now - self._last_keepalive_receive_time
- self._debug_print_call(
+ self.debug_print_call(
f'{self._label}: reached keepalive time-out'
f' ({since:.1f}s).'
)
@@ -751,15 +796,15 @@ class RPCEndpoint:
tasklabel,
)
else:
- if self._debug_print:
- self._debug_print_call(
+ if self.debug_print:
+ self.debug_print_call(
f'{self._label}: {tasklabel} task will exit cleanly'
f' due to {exc!r}.'
)
finally:
# Any core task exiting triggers shutdown.
- if self._debug_print:
- self._debug_print_call(
+ if self.debug_print:
+ self.debug_print_call(
f'{self._label}: {tasklabel} task exiting...'
)
self.close()
@@ -836,8 +881,8 @@ class RPCEndpoint:
"""Enqueue a raw packet to be sent. Must be called from our loop."""
self._check_env()
- if self._debug_print_io:
- self._debug_print_call(
+ if self.debug_print_io:
+ self.debug_print_call(
f'{self._label}: enqueueing outgoing packet'
f' {data[:50]!r} at {self._tm()}.'
)
@@ -847,17 +892,7 @@ class RPCEndpoint:
self._have_out_packets.set()
def _prune_tasks(self) -> None:
- out: list[weakref.ref[asyncio.Task]] = []
- for task_weak_ref in self._tasks:
- task = task_weak_ref()
- if task is not None and not task.done():
- out.append(task_weak_ref)
- self._tasks = out
+ self._tasks = self._get_live_tasks()
def _get_live_tasks(self) -> list[asyncio.Task]:
- out: list[asyncio.Task] = []
- for task_weak_ref in self._tasks:
- task = task_weak_ref()
- if task is not None and not task.done():
- out.append(task)
- return out
+ return [t for t in self._tasks if not t.done()]