diff --git a/.efrocachemap b/.efrocachemap
index 28b3b217..c7838b82 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -945,11 +945,11 @@
"assets/build/ba_data/models/zoeUpperArm.bob": "https://files.ballistica.net/cache/ba1/99/38/b7694cae0804260eeb337aa1676a",
"assets/build/ba_data/models/zoeUpperLeg.bob": "https://files.ballistica.net/cache/ba1/83/4f/28b2202d0109fa93272c0b09fa2d",
"assets/build/ba_data/python-site-packages/_yaml/__init__.py": "https://files.ballistica.net/cache/ba1/0d/45/65ba92f51d411dcffac8835b6130",
- "assets/build/ba_data/python-site-packages/certifi/__init__.py": "https://files.ballistica.net/cache/ba1/f0/50/dbe7d0065006ac12adf2eaa239fc",
+ "assets/build/ba_data/python-site-packages/certifi/__init__.py": "https://files.ballistica.net/cache/ba1/1b/3d/98d25cd57f325c7dcda2d6f9054d",
"assets/build/ba_data/python-site-packages/certifi/__main__.py": "https://files.ballistica.net/cache/ba1/b2/bb/d7d8216212bcf66cdc3067700fb7",
- "assets/build/ba_data/python-site-packages/certifi/cacert.pem": "https://files.ballistica.net/cache/ba1/eb/1c/18ef584961785d002a2550d389e0",
+ "assets/build/ba_data/python-site-packages/certifi/cacert.pem": "https://files.ballistica.net/cache/ba1/33/4f/e92ea306a333f3c82d95b577653e",
"assets/build/ba_data/python-site-packages/certifi/core.py": "https://files.ballistica.net/cache/ba1/ac/28/37f05b52df3806856bce2c0b9772",
- "assets/build/ba_data/python-site-packages/typing_extensions.py": "https://files.ballistica.net/cache/ba1/a5/c3/66c408bfad73af8644f507d8ee17",
+ "assets/build/ba_data/python-site-packages/typing_extensions.py": "https://files.ballistica.net/cache/ba1/77/43/af9b9eedb8efd97e5818a2655a44",
"assets/build/ba_data/python-site-packages/yaml/__init__.py": "https://files.ballistica.net/cache/ba1/e5/47/17715ca7620f3b9749558b9dcb2d",
"assets/build/ba_data/python-site-packages/yaml/composer.py": "https://files.ballistica.net/cache/ba1/3e/aa/d7fcfc4707ad19a6964d72654b82",
"assets/build/ba_data/python-site-packages/yaml/constructor.py": "https://files.ballistica.net/cache/ba1/f4/29/cd8c7f5a2296d8f1715ad49b5797",
@@ -4008,50 +4008,50 @@
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/1c/77/ac670a5118abdf8a7687af0e159b",
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
- "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/82/d3/3715dc8f54d1353bab3493b785bc",
- "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/21/a3/3d39047fbe2d7168fde9323461a3",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fa/ae/0b8e3ea76ead98bcb1f91154adb4",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9a/de/2d3ea7b4b6b8d96fee70bc6ee51e",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/63/73/49be93777a8e46dd3379897eaee0",
- "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/bc/81/80bb7ab35ed92ce9e904874f38b9",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/46/1d/18f4916b42ec3af78ca83e132be4",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9f/38/5a8b30c0b79cecb834eb5348ff8d",
- "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/3e/76/8c1cea0d914d49a64bf721e49070",
- "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a4/a3/189278b9cd02c7c4255f96bc5bc2",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/60/2a/8f637d1dcf6e3b832c4770ed0c16",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/01/28/20484d98b6b88ce8b56e28efcc80",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/50/a2/b54a80f3881e94c7fabeaaec7ad4",
- "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b0/ab/7472dd6f3ae5bb06753f5df250ff",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8c/0a/35c89966441a7dd16f6543d540f3",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e5/9c/43a7f214bacd6eab04ec30ebcafd",
- "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/71/c7/781eae3a95730651fc8fef6e6346",
- "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/b2/a9/7bc2e2f656ddb8505f9f380033af",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/c4/7c/6f8e20e6ec4788047a228a5d896f",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/3b/2e/48440d1ace2bc05e48eb9f56f63b",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/34/66/0a1e0fece6b9ad87ac315a9ba9f3",
- "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6d/a4/4a1357b21f0bc128c9b5948061da",
- "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c1/23/405f68ec8c229549de5b0dad41eb",
- "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/aa/a5/c088252e71a395765f8bc0cdc028",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0f/ba/196bbcd459b59e32e93c15a7c128",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/39/d2/706b2ca75bae629545cc7eb0a8d5",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/89/56/24c6be441e8b120eef698c467ce6",
- "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6b/11/b1c5fc90854821d7e63fafd6f664",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3c/a7/308c08a0ebd82d100505666b85d5",
- "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2e/83/abff53b7982976aa9c6edb39d22b",
- "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/01/d2/b005ef960a6c9ba96c3d77fd005f",
- "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/dc/53/af455ca35433fba5018fb0a4ea10",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e7/91/4c135c16252f54a325ca375a7f79",
- "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/65/95/85ac4196233bb9a6fbef9e10f431",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ac/0d/82b7132a360c91e386b070c38fa2",
- "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7d/43/7112b8b9467213a48880352a4ba2",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/4c/8a/bfeae3274074681c9fcc50625029",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/0e/46/75aff046dbc4ac23208ab9b4774e",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/81/5d/66dfda3a7d4428b8c7a3ed1ad17f",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/41/79/5be0ca4e66ab63ccfdb4f129ddcb",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/f2/0a/e8432743590dd423b723eb3f262f",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/06/13/fc1c3c4b3acb2677a48e28ffae63",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/58/95/3a1c4831cd80b6516ad95301bb7d",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/f3/08/8936bdf7db0abe3c438a1db84445",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/75/cb/d0257897d9230af60b8265dd49b3",
+ "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d9/69/54f46438b5c9e5159c342b2f2efe",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2b/17/0b3ec1eab3ff8be3bf20ac14b048",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/55/af/00171ed0d6e78a72f7b928a2fbea",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/7d/a8/e3e3d130cc77e4070d64b630e2b5",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/af/a2/821b7cd09fa5178a8aa52df3ca39",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/47/09/50585aaa691782553aa2c5675b31",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/27/42/18e141f733dfd0233fce159e05e5",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/a7/77/dd0b4658d5e0de27f1c3900e777a",
+ "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d5/67/53cc21710083adc2318860e81c96",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1e/5d/1ad092e3af8455f4d23cc649a737",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/52/79/94f1e48fe80359b8e84d9f405f5f",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e3/1c/569425522cc0af062aa01d7010eb",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/83/67/2f62e2bcc3126bf09bd6066f51ca",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d7/41/b2afb28d9d6d5eca30a40c25a9cf",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/33/05/894c4682febb13c0862a6dba669c",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/4e/c6/3b0e683a07ec04a1c975ab09dc9b",
+ "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ce/be/450ca52c3de6fb3f9bf37155fea2",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/bb/a4/065dea281d8dac0686ae54ff46bc",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/24/5b/3533fde7a47702d923c963163939",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/22/b9/efe2e0a58bc65d852104552d67cf",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b0/68/12b83d2fb0d9303f77349dcc3e2b",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d3/2c/943673be99c77c8c052845fe79fc",
+ "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2e/07/b739ef23ff55fb3fd45a5434cc46",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/24/eb/8f58ad33181a61dc128229219b55",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/74/90/6b7b9259633c55e1e23041bd6fdd",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/48/52/5d677ac97fd41b6b55627085ab5c",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/01/fd/a66b6ee0cff76d9eb27190ee1f08",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2e/b9/7c47db5c03dd72d015ee19d1fdc8",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3d/c9/52598eebc9079a275963ce372272",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/15/5a/4a90da5f1b681f6bdd6214673e1a",
+ "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ab/97/449b885954e49816dc53e5112839",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/df/4c/e6ac16fd511c196eb6adea36affd",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d6/3f/f865c2ac0183040993db5d68ee71",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ae/31/34f17c4dbd9d0c1036541bb8919a",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/91/56/8cf9ffe11c29cf297fddb4b359d7",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/1b/f0/87ceb3ec480dac9ca6365d6cf4c7",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/ff/d3/a94e83a99a3c6df253a9ce750b00",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/f3/bc/7237ac5bfe9308c39157a5e8648b",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/24/0c/6bab1b51d397c084cf6714cf262e",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/bf/d0/d906b7f2b752ef8b1148ebcc8c13",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/63/d3/bf61061c5270e265f68c4f8d13ee",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/9b/0e/575279f8955d4c36ebcda8eba3bd",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/39/3e/759f28bb0fead4a42132ce9954bf",
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/23/ce/68396b1b7ec6d2f8425902148140",
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/2d/4f/f4fe67827f36cd59cd5193333a02",
"src/ballistica/generated/python_embedded/bootstrap_monolithic.inc": "https://files.ballistica.net/cache/ba1/ef/c1/aa5f1aa10af89f5c0b1e616355fd"
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index 41368e39..50c7601b 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -2330,6 +2330,7 @@
setactivity
setalpha
setbuild
+ setclass
setconfig
setdata
setlanguage
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f9135b9..082b936b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,5 @@
-### 1.7.20 (build 20999, api 7, 2023-01-19)
+### 1.7.20 (build 21005, api 7, 2023-01-21)
+- Started work on the ba.app.components subsystem which will be used by different app-modes, plugins, etc. to override various app functionality.
### 1.7.19 (build 20997, api 7, 2023-01-19)
- Fixes an issue where repeated curses could use incorrect countdown times (Thanks EraOSBeta!).
diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json
index 77b6d030..fb7c3b94 100644
--- a/assets/.asset_manifest_public.json
+++ b/assets/.asset_manifest_public.json
@@ -12,6 +12,7 @@
"ba_data/python/ba/__pycache__/_ads.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_analytics.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_app.cpython-310.opt-1.pyc",
+ "ba_data/python/ba/__pycache__/_appcomponent.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_appconfig.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_appdelegate.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_appmode.cpython-310.opt-1.pyc",
@@ -79,6 +80,7 @@
"ba_data/python/ba/_ads.py",
"ba_data/python/ba/_analytics.py",
"ba_data/python/ba/_app.py",
+ "ba_data/python/ba/_appcomponent.py",
"ba_data/python/ba/_appconfig.py",
"ba_data/python/ba/_appdelegate.py",
"ba_data/python/ba/_appmode.py",
@@ -522,6 +524,7 @@
"ba_data/python/efro/__init__.py",
"ba_data/python/efro/__pycache__/__init__.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/call.cpython-310.opt-1.pyc",
+ "ba_data/python/efro/__pycache__/cloudshell.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/debug.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/error.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/log.cpython-310.opt-1.pyc",
@@ -529,6 +532,7 @@
"ba_data/python/efro/__pycache__/terminal.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/util.cpython-310.opt-1.pyc",
"ba_data/python/efro/call.py",
+ "ba_data/python/efro/cloudshell.py",
"ba_data/python/efro/dataclassio/__init__.py",
"ba_data/python/efro/dataclassio/__pycache__/__init__.cpython-310.opt-1.pyc",
"ba_data/python/efro/dataclassio/__pycache__/_api.cpython-310.opt-1.pyc",
diff --git a/assets/Makefile b/assets/Makefile
index 7d845b7f..019d13e5 100644
--- a/assets/Makefile
+++ b/assets/Makefile
@@ -144,6 +144,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
build/ba_data/python/ba/_ads.py \
build/ba_data/python/ba/_analytics.py \
build/ba_data/python/ba/_app.py \
+ build/ba_data/python/ba/_appcomponent.py \
build/ba_data/python/ba/_appconfig.py \
build/ba_data/python/ba/_appdelegate.py \
build/ba_data/python/ba/_appmode.py \
@@ -398,6 +399,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
build/ba_data/python/ba/__pycache__/_ads.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_analytics.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_app.cpython-310.opt-1.pyc \
+ build/ba_data/python/ba/__pycache__/_appcomponent.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_appconfig.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_appdelegate.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_appmode.cpython-310.opt-1.pyc \
@@ -666,6 +668,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
build/ba_data/python/bacommon/transfer.py \
build/ba_data/python/efro/__init__.py \
build/ba_data/python/efro/call.py \
+ build/ba_data/python/efro/cloudshell.py \
build/ba_data/python/efro/dataclassio/__init__.py \
build/ba_data/python/efro/dataclassio/_api.py \
build/ba_data/python/efro/dataclassio/_base.py \
@@ -699,6 +702,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
build/ba_data/python/bacommon/__pycache__/transfer.cpython-310.opt-1.pyc \
build/ba_data/python/efro/__pycache__/__init__.cpython-310.opt-1.pyc \
build/ba_data/python/efro/__pycache__/call.cpython-310.opt-1.pyc \
+ build/ba_data/python/efro/__pycache__/cloudshell.cpython-310.opt-1.pyc \
build/ba_data/python/efro/dataclassio/__pycache__/__init__.cpython-310.opt-1.pyc \
build/ba_data/python/efro/dataclassio/__pycache__/_api.cpython-310.opt-1.pyc \
build/ba_data/python/efro/dataclassio/__pycache__/_base.cpython-310.opt-1.pyc \
diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py
index 9c44d333..29610d29 100644
--- a/assets/src/ba_data/python/ba/_app.py
+++ b/assets/src/ba_data/python/ba/_app.py
@@ -20,6 +20,7 @@ from ba._meta import MetadataSubsystem
from ba._ads import AdsSubsystem
from ba._net import NetworkSubsystem
from ba._workspace import WorkspaceSubsystem
+from ba._appcomponent import AppComponentSubsystem
from ba import _internal
if TYPE_CHECKING:
@@ -294,6 +295,7 @@ class App:
# Server Mode.
self.server: ba.ServerController | None = None
+ self.components = AppComponentSubsystem()
self.meta = MetadataSubsystem()
self.accounts_v1 = AccountV1Subsystem()
self.plugins = PluginSubsystem()
diff --git a/assets/src/ba_data/python/ba/_appcomponent.py b/assets/src/ba_data/python/ba/_appcomponent.py
new file mode 100644
index 00000000..0e1f635d
--- /dev/null
+++ b/assets/src/ba_data/python/ba/_appcomponent.py
@@ -0,0 +1,90 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Provides the AppComponent class."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, TypeVar, cast
+
+import _ba
+
+if TYPE_CHECKING:
+ from typing import Callable, Any
+
+T = TypeVar('T', bound=type)
+
+
+class AppComponentSubsystem:
+ """Subsystem for wrangling AppComponents.
+
+ Category: **App Classes**
+
+ This subsystem acts as a registry for classes providing particular
+ functionality for the app, and allows plugins or other custom code to
+ easily override said functionality.
+
+ Use ba.app.components to get the single shared instance of this class.
+
+ The general idea with this setup is that a base-class is defined to
+ provide some functionality and then anyone wanting that functionality
+ uses the getclass() method with that base class to return the current
+ registered implementation. The user should not know or care whether
+ they are getting the base class itself or some other implementation.
+
+ Change-callbacks can also be requested for base classes which will
+ fire in a deferred manner when particular base-classes are overridden.
+ """
+
+ def __init__(self) -> None:
+ self._implementations: dict[type, type] = {}
+ self._prev_implementations: dict[type, type] = {}
+ self._dirty_base_classes: set[type] = set()
+ self._change_callbacks: dict[type, list[Callable[[Any], None]]] = {}
+
+ def setclass(self, baseclass: type, implementation: type) -> None:
+ """Set the class providing an implementation of some base-class.
+
+ The provided implementation class must be a subclass of baseclass.
+ """
+ # Currently limiting this to logic-thread use; can revisit if needed
+ # (would need to guard access to our implementations dict).
+ assert _ba.in_logic_thread()
+
+ if not issubclass(implementation, baseclass):
+ raise TypeError(
+ f'Implementation {implementation}'
+ f' is not a subclass of baseclass {baseclass}.'
+ )
+
+ self._implementations[baseclass] = implementation
+
+ # If we're the first thing getting dirtied, set up a callback to
+ # clean everything. And add ourself to the dirty list regardless.
+ if not self._dirty_base_classes:
+ _ba.pushcall(self._run_change_callbacks)
+ self._dirty_base_classes.add(baseclass)
+
+ def getclass(self, baseclass: T) -> T:
+ """Given a base-class, return the currently set implementation class.
+
+ If no custom implementation has been set, the provided base-class
+ is returned.
+ """
+ assert _ba.in_logic_thread()
+
+ del baseclass # Unused.
+ return cast(T, None)
+
+ def register_change_callback(
+ self, baseclass: T, callback: Callable[[T], None]
+ ) -> None:
+ """Register a callback to fire when a class implementation changes.
+
+ The callback will be scheduled to run in the logic thread event
+ loop. Note that any further setclass calls before the callback
+ runs will not result in additional callbacks.
+ """
+ assert _ba.in_logic_thread()
+ self._change_callbacks.setdefault(baseclass, []).append(callback)
+
+ def _run_change_callbacks(self) -> None:
+ pass
diff --git a/assets/src/ba_data/python/ba/_bootstrap.py b/assets/src/ba_data/python/ba/_bootstrap.py
index d29cdb24..0b41cbbb 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 = 20999
+ expected_build = 21005
running_build: int = env['build_number']
if running_build != expected_build:
print(
diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
index 562edbcf..46426ce9 100644
--- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
@@ -1240,6 +1240,7 @@
setactivity
setattro
setattrofunc
+ setclass
setdata
setname
setnode
diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc
index 9355c990..dcfc21b3 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 = 20999;
+const int kAppBuildNumber = 21005;
const char* kAppVersion = "1.7.20";
// Our standalone globals.
diff --git a/tools/efro/cloudshell.py b/tools/efro/cloudshell.py
new file mode 100644
index 00000000..b1e031e6
--- /dev/null
+++ b/tools/efro/cloudshell.py
@@ -0,0 +1,49 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""My nifty ssh/mosh/rsync mishmash."""
+
+from __future__ import annotations
+
+from enum import Enum
+from dataclasses import dataclass
+
+from efro.dataclassio import ioprepped
+
+
+class LockType(Enum):
+ """Types of locks that can be acquired on a host."""
+
+ HOST = 'host'
+ WORKSPACE = 'workspace'
+ PYCHARM = 'pycharm'
+ CLION = 'clion'
+
+
+@ioprepped
+@dataclass
+class HostConfig:
+ """Config for a cloud machine to run commands on.
+
+ precommand, if set, will be run before the passed commands.
+ Note that it is not run in interactive mode (when no command is given).
+ """
+
+ address: str | None = None
+ user: str = 'ubuntu'
+ port: int = 22
+ mosh_port: int | None = None
+ mosh_server_path: str | None = None
+ mosh_shell: str = 'sh'
+ workspaces_root: str = '/home/${USER}/cloudshell_workspaces'
+ sync_perms: bool = True
+ precommand: str | None = None
+ managed: bool = False
+ idle_minutes: int = 5
+ can_sudo_reboot: bool = False
+ max_sessions: int = 3
+ reboot_wait_seconds: int = 20
+ reboot_attempts: int = 1
+
+ def resolved_workspaces_root(self) -> str:
+ """Returns workspaces_root with standard substitutions."""
+ return self.workspaces_root.replace('${USER}', self.user)
diff --git a/tools/efro/log.py b/tools/efro/log.py
index 71bbd8ea..fa0dc1ce 100644
--- a/tools/efro/log.py
+++ b/tools/efro/log.py
@@ -171,6 +171,12 @@ class LogHandler(logging.Handler):
def _log_thread_main(self) -> None:
self._event_loop = asyncio.new_event_loop()
+
+ # In our background thread event loop we do a fair amount of
+ # slow synchronous stuff such as mucking with the log cache.
+ # Let's avoid getting tons of warnings about this in debug mode.
+ self._event_loop.slow_callback_duration = 2.0 # Default is 0.1
+
# NOTE: if we ever use default threadpool at all we should allow
# setting it for our loop.
asyncio.set_event_loop(self._event_loop)