diff --git a/.efrocachemap b/.efrocachemap
index 300321f8..9f55a5f5 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -421,42 +421,42 @@
"build/assets/ba_data/audio/zoeOw.ogg": "74befe45a8417e95b6a2233c51992a26",
"build/assets/ba_data/audio/zoePickup01.ogg": "48ab8cddfcde36a750856f3f81dd20c8",
"build/assets/ba_data/audio/zoeScream01.ogg": "2b468aedfa8741090247f04eb9e6df55",
- "build/assets/ba_data/data/langdata.json": "bb60812044af0a8d1bdefee759ff2522",
+ "build/assets/ba_data/data/langdata.json": "831b83240126d0a851104f4148712ed1",
"build/assets/ba_data/data/languages/arabic.json": "0db32e21b6d5337ccca478381744aa88",
"build/assets/ba_data/data/languages/belarussian.json": "a112dfca3e188387516788bd8229c5b0",
- "build/assets/ba_data/data/languages/chinese.json": "93f3ca9f90d86dc7c8d0923f5f11ef46",
+ "build/assets/ba_data/data/languages/chinese.json": "1360ffde06828b63ce4fe956c3c3cd1d",
"build/assets/ba_data/data/languages/chinesetraditional.json": "319565f8a15667488f48dbce59278e39",
"build/assets/ba_data/data/languages/croatian.json": "766532c67af5bd0144c2d63cab0516fa",
- "build/assets/ba_data/data/languages/czech.json": "c9d518a324870066b987b8f412881dd3",
+ "build/assets/ba_data/data/languages/czech.json": "7171420af6d662e3a47b64576850a384",
"build/assets/ba_data/data/languages/danish.json": "3fd69080783d5c9dcc0af737f02b6f1e",
- "build/assets/ba_data/data/languages/dutch.json": "5cbf1a68a9d93dee00dbc27f834d878a",
+ "build/assets/ba_data/data/languages/dutch.json": "b0900d572c9141897d53d6574c471343",
"build/assets/ba_data/data/languages/english.json": "1c4037fea1066d39d6eced419f314f35",
"build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880",
- "build/assets/ba_data/data/languages/filipino.json": "0031cbb8eb6a638a94fb43c5d892346c",
- "build/assets/ba_data/data/languages/french.json": "8bc35eb4b20a0b30c3348bcc9a3844a6",
+ "build/assets/ba_data/data/languages/filipino.json": "43e838754fe013b8bac75f75aef78cb3",
+ "build/assets/ba_data/data/languages/french.json": "cc8ac601f5443dd539893728db983f5c",
"build/assets/ba_data/data/languages/german.json": "450fa41ae264f29a5d1af22143d0d0ad",
"build/assets/ba_data/data/languages/gibberish.json": "b461539243e8efe3137137b886256ba7",
"build/assets/ba_data/data/languages/greek.json": "287c0ec437b38772284ef9d3e4fb2fc3",
- "build/assets/ba_data/data/languages/hindi.json": "8848f6b0caec0fcf9d85bc6e683809ec",
+ "build/assets/ba_data/data/languages/hindi.json": "5b6c8e988ffa84a7e26d120b6cd8e1a4",
"build/assets/ba_data/data/languages/hungarian.json": "796a290a8c44a1e7635208c2ff5fdc6e",
- "build/assets/ba_data/data/languages/indonesian.json": "408fb026e84c24a8dd7a43cb2b794541",
+ "build/assets/ba_data/data/languages/indonesian.json": "9103845242b572aa8ba48e24f81ddb68",
"build/assets/ba_data/data/languages/italian.json": "f550810b6866ea9bcf1985b7228f8cff",
- "build/assets/ba_data/data/languages/korean.json": "03fd99d5e1155e81053fc028f69df982",
+ "build/assets/ba_data/data/languages/korean.json": "4e3524327a0174250aff5e1ef4c0c597",
"build/assets/ba_data/data/languages/malay.json": "832562ce997fc70704b9234c95fb2e38",
"build/assets/ba_data/data/languages/persian.json": "9728d631cf7d9ad3b209ae1244bb59c0",
"build/assets/ba_data/data/languages/polish.json": "3a90b2d9e2c59305580c96f8098fc839",
- "build/assets/ba_data/data/languages/portuguese.json": "0274cb9a4b7d2bd49c8eb8120144a1bf",
+ "build/assets/ba_data/data/languages/portuguese.json": "b52164747c6308fc9d054eb6c0ff3c54",
"build/assets/ba_data/data/languages/romanian.json": "aeebdd54f65939c2facc6ac50c117826",
"build/assets/ba_data/data/languages/russian.json": "30d5f3d2415088e1fb6558fcd6ccfa98",
"build/assets/ba_data/data/languages/serbian.json": "d7452dd72ac0e51680cb39b5ebaa1c69",
"build/assets/ba_data/data/languages/slovak.json": "27962d53dc3f7dd4e877cd40faafeeef",
- "build/assets/ba_data/data/languages/spanish.json": "08d9b39a519743da9a715c2524c3b6ca",
+ "build/assets/ba_data/data/languages/spanish.json": "e3e9ac8f96f52302a480c7e955aed71f",
"build/assets/ba_data/data/languages/swedish.json": "5142a96597d17d8344be96a603da64ac",
"build/assets/ba_data/data/languages/tamil.json": "b4de1a2851afe4869c82e9acd94cd89c",
- "build/assets/ba_data/data/languages/thai.json": "77755219bbf5fb7eea0d6b226684f403",
+ "build/assets/ba_data/data/languages/thai.json": "9c425b420f0488a7f883da98947657ad",
"build/assets/ba_data/data/languages/turkish.json": "2be25c89ca754341f27750e0d595f31e",
"build/assets/ba_data/data/languages/ukrainian.json": "b54a38e93deebafa5706ba2d1f626892",
- "build/assets/ba_data/data/languages/venetian.json": "8e9714d98a85e428ce3543fc49188a46",
+ "build/assets/ba_data/data/languages/venetian.json": "f896fc3df13a42f1bef8813ca80b1a09",
"build/assets/ba_data/data/languages/vietnamese.json": "921cd1e50f60fe3e101f246e172750ba",
"build/assets/ba_data/data/maps/big_g.json": "1dd301d490643088a435ce75df971054",
"build/assets/ba_data/data/maps/bridgit.json": "6aea74805f4880cc11237c5734a24422",
@@ -950,7 +950,7 @@
"build/assets/ba_data/python-site-packages/certifi/__main__.py": "ef02e73f8581609df189a9f61aca365b",
"build/assets/ba_data/python-site-packages/certifi/cacert.pem": "4422aed09ab445f7290df7d72a301a47",
"build/assets/ba_data/python-site-packages/certifi/core.py": "1b505388f1475fabd1b60031f985271c",
- "build/assets/ba_data/python-site-packages/typing_extensions.py": "2d974cad17a71505d86513d1322976a5",
+ "build/assets/ba_data/python-site-packages/typing_extensions.py": "188320d92e530be7ea345d3ce3be38de",
"build/assets/ba_data/python-site-packages/yaml/__init__.py": "2b747e5772c203377222afc888ac6b71",
"build/assets/ba_data/python-site-packages/yaml/composer.py": "cef871e1f5f99ba2a7c44941b70afb06",
"build/assets/ba_data/python-site-packages/yaml/constructor.py": "8a15e361e34b79491c81553bb3534062",
@@ -4060,50 +4060,50 @@
"build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1",
"build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
- "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "cd19dfdf480de6e73949db674e1b02d2",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "8c08cdda59e731a3830624000de5ca7f",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "14685eca62b8540cc2a268883d0ebc5d",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "f81876d7827a10be412306c52b03fa08",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "b8370845743ebba86ed6eaa6ee1d79d5",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "cdf04825dedae8fb2c26502ec2a505db",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "4036493f98646b58de8bf425bee227cb",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "1cf36c63f68ecaa954fb9c48a132725e",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "b757a940c5157197a0138e12e308f859",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "92a07f83fceeddf3b29cfe2ead57f7e3",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "90002c085c66be4af378d4b3fc8e0260",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "eeb363d0d48e68f5f2ac2e536a26aeeb",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "e119b480fc7e542f33ceb16e8c04585f",
- "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "6e657aa09d052765ed891789ec60dfb2",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "2d4eab8ea8399defd1afdbe548216e9c",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "b03bb9d5d1eb695a11843f64f24906ef",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "163fbc40b479ef1db1c753f7beb73c0f",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "37291abd76871f4556348f77e12dd363",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "acca6904f2f2f952ecae99922c602b9d",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "3c2dfd9cf26e77a0b803ed43c85df113",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "3af5cf00e5eb30d55030e8705b83353a",
- "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "be337c05f72235b5b486277bb1a9c259",
- "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "3af5cf00e5eb30d55030e8705b83353a",
- "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "be337c05f72235b5b486277bb1a9c259",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "17c7d0041bc7c84077bf6692b16e3988",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "4affbfea91e8a33ab62da763ffc07ddd",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "17c7d0041bc7c84077bf6692b16e3988",
- "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "4affbfea91e8a33ab62da763ffc07ddd",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "58656b49d34e6c650983fbf79b5c41ae",
- "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "3ce5652e0ff5d277e256f517dec4eb61",
- "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "58656b49d34e6c650983fbf79b5c41ae",
- "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "3ce5652e0ff5d277e256f517dec4eb61",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "b94fff3a719003c1d8f5dd16dffdb3fc",
- "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "5d68e1957febe6053815bbee3f068e76",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "85bbca447ca8a1d0fad984afc6f0700a",
- "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "5d68e1957febe6053815bbee3f068e76",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "7f2ed141a475e051d3350d571ef6cb0c",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "93fb764531ae16a30d4886eb183c3681",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "750cd7ef428f4faf65ccbeff50c21f8e",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "104cf85df9f1f7ffbf4de5997f7c6879",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "af85fb387d755b152c42f8dfb0891ad7",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "01ce8e0619b342e4cc2cc5f18f81a727",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "f8aae2e04f95f4cfb863791da36ff931",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "66252d58a1be97db8523bd0bf8098a16",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "d5a8312cd9cf65f32ca2a7c4a2063c03",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "aecb00e9044fa677583e1036fa7875d8",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "eca7f9ab892edfa7423a9d4a6f89e571",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "99647f48362f84112d23a9bc89eaa983",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "31e21a64d77fc0834832b633a26d986b",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "7c12b4078c3af6e627a4051b1c1d8370",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "f7a66c48321efa4462e8eae6b72db2b2",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "08cdbeb2ca4fa8c996f3369680c4e5cd",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "f92679bab5a0d057427962869e19f057",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "d5bcd695f84dab1ab32655989d399c9e",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "c766f437ece15dae0ee971e4c2e10a2d",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "cbecc4c11b9aa4621abfdc996fecfd74",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "7af782c9d9bcf1396a15dea6f2493d70",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "2c04f3f68db3e73e4aad4c656d956c00",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "132c83ee8811828739601ac3d0599fe9",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "8de942a2e1ff96c147a9500a56ca4f64",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "6bf51ccbd01937bf1b28cfffe029d857",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "c5f0d834a47852f1c240e17a6c933e0a",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "4f74b71dabd207bee732dc91c9a28dc4",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "f48ab8e4c4d05f4b2231bebf33c965f1",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "ee36a39fd0f524989cb68930c89c8868",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "dbed9145e5db116d92aa47cb9e98da39",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "ee36a39fd0f524989cb68930c89c8868",
+ "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "dbed9145e5db116d92aa47cb9e98da39",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "dc078f11a4e93062adc7d210fd4f08fb",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "a74bea3380d0fb39f78ac7b7598c1a72",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "dc078f11a4e93062adc7d210fd4f08fb",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "a74bea3380d0fb39f78ac7b7598c1a72",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "b397e020f33132c4dd2280cb1222cd14",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "ff0cb4db976707d25bd401bce80a4882",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "b397e020f33132c4dd2280cb1222cd14",
+ "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "ff0cb4db976707d25bd401bce80a4882",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "c464accef921df1325459bdd10c59b84",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "0896e849885cef50bcf33ce863efa7d2",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "e53c808357cc0a2f0da7b870be147083",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "0896e849885cef50bcf33ce863efa7d2",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "e34cc55fd284e31d9ed1151c5a51bf34",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "36cb65be158a0103d81c82d8a51dc8b6",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "21f8a61745c2fec88749299f5aeeeaf9",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "d61272f101f87b140b84895e482b07f4",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "36c30bcd93d38569b9515ed17896d8de",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "841c7cd3cc96c91ecd11335a91c0c465",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "305aab4423bf510f6bf95fe0c996128f",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "f1066b8591d7859df76c8e976ceee2d5",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "b611c090513a21e2fe90e56582724e9d",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "72bfed2cce8ff19741989dec28302f3f",
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 2df5214d..32b08077 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -9,7 +9,7 @@ assignees: ''
### Description
Describe the bug. Do not forget to fill the title.
-Make sure you're running game without any modifications (unless you want to report an api bug).
+Make sure you're running game without any modifications.
### Steps to reproduce
1. Launch BombSquad
@@ -18,16 +18,17 @@ Make sure you're running game without any modifications (unless you want to repo
4. Bug!
### Expected behavior
-Describe what you think should happen.
+Describe what you think should happen if it's not obvious.
### Machine
-**Platform**: Windows 10 / Ubuntu 20.04 LTS / AOSP 8.1 / etc.
-**BombSquad version**: [1.5.27](https://github.com/efroemling/ballistica/releases/tag/v1.5.27)
-**Commit**: [2642488](https://github.com/efroemling/ballistica/commit/2642488a51b250752169738f5aeeccaafa2bc8de)
-Select what do you want to use: release version or commit. Please use a hyperlink.
+**Platform**: Windows 11 / Ubuntu 22.04 LTS / Android 12 / MyToasterOS 7.3 / ... \
+**BombSquad version**: [1.7.32](https://github.com/efroemling/ballistica/tree/v1.7.32) \
+**Commit**: https://github.com/efroemling/ballistica/tree/978f32f9f098bd0ff1dc64b496ec31cf493ded09
+
+You may specify BombSquad version you're running or refer to the latest commit.
### Screenshots
Put some screenshots here if needed.
### Extra
-Put some extra information here. For example, describe your assumptions about the cause of the bug.
+You may put some extra information here. For example, describe your assumptions about the cause of the bug.
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index ebeea7ef..f88e4998 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -14,6 +14,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install pip requirements
run: tools/pcommand install_pip_reqs
- name: Make the build
@@ -31,6 +34,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install pip requirements
run: tools/pcommand install_pip_reqs
- name: Make the build
@@ -48,6 +54,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install pip requirements
run: tools/pcommand install_pip_reqs
- name: Make the build
@@ -65,6 +74,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install pip requirements
run: tools/pcommand install_pip_reqs
- name: Make the build
@@ -82,6 +94,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install pip requirements
run: tools/pcommand install_pip_reqs
- name: Make the build
@@ -99,6 +114,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install pip requirements
run: tools/pcommand install_pip_reqs
- name: Make the build
@@ -116,6 +134,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install pip requirements
run: tools/pcommand install_pip_reqs
- name: Make the build
@@ -133,6 +154,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install pip requirements
run: tools/pcommand install_pip_reqs
- name: Make the build
@@ -150,6 +174,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install pip requirements
run: tools/pcommand install_pip_reqs
- name: Make the build
@@ -167,6 +194,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install pip requirements
run: tools/pcommand install_pip_reqs
- name: Make the build
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0878ab4e..70bb196e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,6 +21,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install dependencies
run: tools/pcommand install_pip_reqs
- name: Run checks
@@ -35,6 +38,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install dependencies
run: tools/pcommand install_pip_reqs
- name: Assemble monolithic server build
@@ -53,6 +59,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install dependencies
run: tools/pcommand install_pip_reqs
- name: Build spinoff project with only core featureset
@@ -71,6 +80,9 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.11'
+ # Remove this once we upgrade to 3.12.
+ - name: Install typing_extensions (temp)
+ run: python3.11 -m pip install typing_extensions
- name: Install dependencies
run: tools/pcommand install_pip_reqs
- name: Create poo feature-set
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78e6c96a..d8f5910a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,4 @@
-### 1.7.33 (build 21757, api 8, 2024-01-06)
-- Exposed an override for `bascenev1.Session`'s max players on servers (by EraOSBeta)
-- Added UI for customizing teams and FFA series length (by EraOSBeta, idea by 3alTemp)
+### 1.7.33 (build 21766, api 8, 2024-02-01)
- Stress test input-devices are now a bit smarter; they won't press any buttons
while UIs are up (this could cause lots of chaos if it happened).
- Added a 'Show Demos When Idle' option in advanced settings. If enabled, the
@@ -13,7 +11,17 @@
- Players now get points for killing bots with their own bombs by catching it
and throwing it back at them. This is actually old logic but was disabled due
to a logic flaw, but should be fixed now. (Thanks VinniTR!)
-
+- Updated the 'Settings->Advanced->Enter Code' functionality to talk to the V2
+ master server (V1 is still used as a fallback).
+- Adopted the `@override` decorator in all Python code and set up Mypy to
+ enforce its usage. Currently `override` comes from `typing_extensions` module
+ but when we upgrade to Python 3.12 soon it will come from the standard
+ `typing` module. This decorator should be familiar to users of other
+ languages; I feel it helps keep logic more understandable and should help us
+ catch problems where a base class changes or removes a method and child
+ classes forget to adapt to the change.
+- Implemented `efro.dataclassio.IOMultiType` which will make my life a lot
+ easier.
### 1.7.32 (build 21741, api 8, 2023-12-20)
- Fixed a screen message that no one will ever see (Thanks vishal332008?...)
- Plugins window now displays 'No Plugins Installed' when no plugins are present (Thanks vishal332008!)
diff --git a/Makefile b/Makefile
index d21ab3e5..ddd594fc 100644
--- a/Makefile
+++ b/Makefile
@@ -49,7 +49,7 @@ endif
# Prereq targets that should be safe to run anytime; even if project-files
# are out of date.
PREREQS_SAFE = .cache/checkenv $(PCOMMANDBATCHBIN) .dir-locals.el .mypy.ini \
- .pyrightconfig.json .pycheckers .pylintrc .style.yapf .clang-format \
+ .pyrightconfig.json .pylintrc .style.yapf .clang-format \
ballisticakit-cmake/.clang-format .editorconfig
# Prereq targets that may break if the project needs updating should go here.
@@ -1216,9 +1216,6 @@ ENV_SRC = $(PCOMMAND) tools/batools/build.py
.pyrightconfig.json: config/toolconfigsrc/pyrightconfig.yaml $(TOOL_CFG_SRC)
@$(TOOL_CFG_INST) $< $@
-.pycheckers: config/toolconfigsrc/pycheckers $(TOOL_CFG_SRC)
- @$(TOOL_CFG_INST) $< $@
-
# Set this to 1 to skip environment checks.
SKIP_ENV_CHECKS ?= 0
diff --git a/ballisticakit-cmake/.idea/misc.xml b/ballisticakit-cmake/.idea/misc.xml
index f01f08b8..fbb3740e 100644
--- a/ballisticakit-cmake/.idea/misc.xml
+++ b/ballisticakit-cmake/.idea/misc.xml
@@ -49,9 +49,6 @@
-
-
-
diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json
index 8a62973f..80f06984 100644
--- a/src/assets/.asset_manifest_public.json
+++ b/src/assets/.asset_manifest_public.json
@@ -13,7 +13,6 @@
"ba_data/python/babase/__pycache__/_apputils.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_assetmanager.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_asyncio.cpython-311.opt-1.pyc",
- "ba_data/python/babase/__pycache__/_cloud.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_devconsole.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_emptyappmode.cpython-311.opt-1.pyc",
"ba_data/python/babase/__pycache__/_env.cpython-311.opt-1.pyc",
@@ -42,7 +41,6 @@
"ba_data/python/babase/_apputils.py",
"ba_data/python/babase/_assetmanager.py",
"ba_data/python/babase/_asyncio.py",
- "ba_data/python/babase/_cloud.py",
"ba_data/python/babase/_devconsole.py",
"ba_data/python/babase/_emptyappmode.py",
"ba_data/python/babase/_env.py",
@@ -121,8 +119,10 @@
"ba_data/python/baenv.py",
"ba_data/python/baplus/__init__.py",
"ba_data/python/baplus/__pycache__/__init__.cpython-311.opt-1.pyc",
+ "ba_data/python/baplus/__pycache__/_cloud.cpython-311.opt-1.pyc",
"ba_data/python/baplus/__pycache__/_hooks.cpython-311.opt-1.pyc",
"ba_data/python/baplus/__pycache__/_subsystem.cpython-311.opt-1.pyc",
+ "ba_data/python/baplus/_cloud.py",
"ba_data/python/baplus/_hooks.py",
"ba_data/python/baplus/_subsystem.py",
"ba_data/python/bascenev1/__init__.py",
diff --git a/src/assets/Makefile b/src/assets/Makefile
index 1ee71109..a92c9ad7 100644
--- a/src/assets/Makefile
+++ b/src/assets/Makefile
@@ -171,7 +171,6 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/babase/_apputils.py \
$(BUILD_DIR)/ba_data/python/babase/_assetmanager.py \
$(BUILD_DIR)/ba_data/python/babase/_asyncio.py \
- $(BUILD_DIR)/ba_data/python/babase/_cloud.py \
$(BUILD_DIR)/ba_data/python/babase/_devconsole.py \
$(BUILD_DIR)/ba_data/python/babase/_emptyappmode.py \
$(BUILD_DIR)/ba_data/python/babase/_env.py \
@@ -210,6 +209,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
$(BUILD_DIR)/ba_data/python/baclassic/osmusic.py \
$(BUILD_DIR)/ba_data/python/baenv.py \
$(BUILD_DIR)/ba_data/python/baplus/__init__.py \
+ $(BUILD_DIR)/ba_data/python/baplus/_cloud.py \
$(BUILD_DIR)/ba_data/python/baplus/_hooks.py \
$(BUILD_DIR)/ba_data/python/baplus/_subsystem.py \
$(BUILD_DIR)/ba_data/python/bascenev1/__init__.py \
@@ -446,7 +446,6 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_apputils.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_assetmanager.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_asyncio.cpython-311.opt-1.pyc \
- $(BUILD_DIR)/ba_data/python/babase/__pycache__/_cloud.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_devconsole.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_emptyappmode.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/babase/__pycache__/_env.cpython-311.opt-1.pyc \
@@ -485,6 +484,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
$(BUILD_DIR)/ba_data/python/baclassic/__pycache__/osmusic.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/__pycache__/baenv.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/baplus/__pycache__/__init__.cpython-311.opt-1.pyc \
+ $(BUILD_DIR)/ba_data/python/baplus/__pycache__/_cloud.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/baplus/__pycache__/_hooks.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/baplus/__pycache__/_subsystem.cpython-311.opt-1.pyc \
$(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/__init__.cpython-311.opt-1.pyc \
diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py
index 28da01e6..e3f115a7 100644
--- a/src/assets/ba_data/python/babase/__init__.py
+++ b/src/assets/ba_data/python/babase/__init__.py
@@ -118,7 +118,6 @@ from babase._apputils import (
get_remote_app_name,
AppHealthMonitor,
)
-from babase._cloud import CloudSubsystem
from babase._devconsole import (
DevConsoleTab,
DevConsoleTabEntry,
@@ -213,7 +212,6 @@ __all__ = [
'clipboard_has_text',
'clipboard_is_supported',
'clipboard_set_text',
- 'CloudSubsystem',
'commit_app_config',
'ContextCall',
'ContextError',
diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py
index 4a021ab8..f7fb9071 100644
--- a/src/assets/ba_data/python/babase/_app.py
+++ b/src/assets/ba_data/python/babase/_app.py
@@ -1,15 +1,17 @@
# Released under the MIT License. See LICENSE for details.
#
+# pylint: disable=too-many-lines
"""Functionality related to the high level state of the app."""
from __future__ import annotations
import os
import logging
from enum import Enum
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, TypeVar
from concurrent.futures import ThreadPoolExecutor
from functools import cached_property
+from typing_extensions import override
from efro.call import tpartial
import _babase
@@ -26,7 +28,7 @@ from babase._devconsole import DevConsoleSubsystem
if TYPE_CHECKING:
import asyncio
- from typing import Any, Callable, Coroutine
+ from typing import Any, Callable, Coroutine, Generator, Awaitable
from concurrent.futures import Future
import babase
@@ -42,6 +44,8 @@ if TYPE_CHECKING:
# __FEATURESET_APP_SUBSYSTEM_IMPORTS_END__
+T = TypeVar('T')
+
class App:
"""A class for high level app functionality and state.
@@ -124,6 +128,7 @@ class App:
statically in a spinoff project.
"""
+ @override
def app_mode_for_intent(
self, intent: AppIntent
) -> type[AppMode] | None:
@@ -199,7 +204,8 @@ class App:
self._called_on_running = False
self._subsystem_registration_ended = False
self._pending_apply_app_config = False
- self._aioloop: asyncio.AbstractEventLoop | None = None
+ self._asyncio_loop: asyncio.AbstractEventLoop | None = None
+ self._asyncio_tasks: set[asyncio.Task] = set()
self._asyncio_timer: babase.AppTimer | None = None
self._config: babase.AppConfig | None = None
self._pending_intent: AppIntent | None = None
@@ -239,18 +245,68 @@ class App:
return _babase.app_is_active()
@property
- def aioloop(self) -> asyncio.AbstractEventLoop:
+ def asyncio_loop(self) -> asyncio.AbstractEventLoop:
"""The logic thread's asyncio event loop.
This allow async tasks to be run in the logic thread.
+
+ Generally you should call App.create_async_task() to schedule
+ async code to run instead of using this directly. That will
+ handle retaining the task and logging errors automatically.
+ Only schedule tasks onto asyncio_loop yourself when you intend
+ to hold on to the returned task and await its results. Releasing
+ the task reference can lead to subtle bugs such as unreported
+ errors and garbage-collected tasks disappearing before their
+ work is done.
+
Note that, at this time, the asyncio loop is encapsulated
and explicitly stepped by the engine's logic thread loop and
- thus things like asyncio.get_running_loop() will not return this
- loop from most places in the logic thread; only from within a
- task explicitly created in this loop.
+ thus things like asyncio.get_running_loop() will unintuitively
+ *not* return this loop from most places in the logic thread;
+ only from within a task explicitly created in this loop.
+ Hopefully this situation will be improved in the future with a
+ unified event loop.
"""
- assert self._aioloop is not None
- return self._aioloop
+ assert _babase.in_logic_thread()
+ assert self._asyncio_loop is not None
+ return self._asyncio_loop
+
+ def create_async_task(
+ self,
+ coro: Generator[Any, Any, T] | Coroutine[Any, Any, T],
+ *,
+ name: str | None = None,
+ ) -> None:
+ """Create a fully managed async task.
+
+ This will automatically retain and release a reference to the task
+ and log any exceptions that occur in it. If you need to await a task
+ or otherwise need more control, schedule a task directly using
+ App.asyncio_loop.
+ """
+ assert _babase.in_logic_thread()
+ # Hold a strong reference to the task until it is done.
+ # Otherwise it is possible for it to be garbage collected and
+ # disappear midway if the caller does not hold on to the
+ # returned task, which seems like a great way to introduce
+ # hard-to-track bugs.
+ task = self.asyncio_loop.create_task(coro, name=name)
+ self._asyncio_tasks.add(task)
+ task.add_done_callback(self._on_task_done)
+ # return task
+
+ def _on_task_done(self, task: asyncio.Task) -> None:
+ # Report any errors that occurred.
+ try:
+ exc = task.exception()
+ if exc is not None:
+ logging.error(
+ "Error in async task '%s'.", task.get_name(), exc_info=exc
+ )
+ except Exception:
+ logging.exception('Error reporting async task error.')
+
+ self._asyncio_tasks.remove(task)
@property
def config(self) -> babase.AppConfig:
@@ -594,7 +650,7 @@ class App:
_env.on_app_state_initing()
- self._aioloop = _asyncio.setup_asyncio()
+ self._asyncio_loop = _asyncio.setup_asyncio()
self.health_monitor = AppHealthMonitor()
# __FEATURESET_APP_SUBSYSTEM_CREATE_BEGIN__
@@ -874,8 +930,8 @@ class App:
)
# Now kick off any async shutdown task(s).
- assert self._aioloop is not None
- self._shutdown_task = self._aioloop.create_task(self._shutdown())
+ assert self._asyncio_loop is not None
+ self._shutdown_task = self._asyncio_loop.create_task(self._shutdown())
def _on_shutdown_complete(self) -> None:
"""(internal)"""
diff --git a/src/assets/ba_data/python/babase/_appsubsystem.py b/src/assets/ba_data/python/babase/_appsubsystem.py
index 812dc600..78ba01d5 100644
--- a/src/assets/ba_data/python/babase/_appsubsystem.py
+++ b/src/assets/ba_data/python/babase/_appsubsystem.py
@@ -40,16 +40,16 @@ class AppSubsystem:
"""Called when the app reaches the running state."""
def on_app_suspend(self) -> None:
- """Called when the app enters the paused state."""
+ """Called when the app enters the suspended state."""
def on_app_unsuspend(self) -> None:
- """Called when the app exits the paused state."""
+ """Called when the app exits the suspended state."""
def on_app_shutdown(self) -> None:
- """Called when the app is shutting down."""
+ """Called when the app begins shutting down."""
def on_app_shutdown_complete(self) -> None:
- """Called when the app is done shutting down."""
+ """Called when the app completes shutting down."""
def do_apply_app_config(self) -> None:
"""Called when the app config should be applied."""
diff --git a/src/assets/ba_data/python/babase/_apputils.py b/src/assets/ba_data/python/babase/_apputils.py
index 59d76481..ce911c90 100644
--- a/src/assets/ba_data/python/babase/_apputils.py
+++ b/src/assets/ba_data/python/babase/_apputils.py
@@ -10,9 +10,11 @@ from threading import Thread
from dataclasses import dataclass
from typing import TYPE_CHECKING
+from typing_extensions import override
from efro.call import tpartial
from efro.log import LogLevel
from efro.dataclassio import ioprepped, dataclass_to_json, dataclass_from_json
+
import _babase
from babase._appsubsystem import AppSubsystem
@@ -386,6 +388,7 @@ class AppHealthMonitor(AppSubsystem):
self._response = False
self._first_check = True
+ @override
def on_app_loading(self) -> None:
# If any traceback dumps happened last run, log and clear them.
log_dumped_app_state(from_previous_run=True)
@@ -449,10 +452,12 @@ class AppHealthMonitor(AppSubsystem):
self._first_check = False
+ @override
def on_app_suspend(self) -> None:
assert _babase.in_logic_thread()
self._running = False
+ @override
def on_app_unsuspend(self) -> None:
assert _babase.in_logic_thread()
self._running = True
diff --git a/src/assets/ba_data/python/babase/_devconsole.py b/src/assets/ba_data/python/babase/_devconsole.py
index fb63c9e7..47580b81 100644
--- a/src/assets/ba_data/python/babase/_devconsole.py
+++ b/src/assets/ba_data/python/babase/_devconsole.py
@@ -8,6 +8,8 @@ from typing import TYPE_CHECKING
from dataclasses import dataclass
import logging
+from typing_extensions import override
+
import _babase
if TYPE_CHECKING:
@@ -96,6 +98,7 @@ class DevConsoleTab:
class DevConsoleTabPython(DevConsoleTab):
"""The Python dev-console tab."""
+ @override
def refresh(self) -> None:
self.python_terminal()
@@ -103,6 +106,7 @@ class DevConsoleTabPython(DevConsoleTab):
class DevConsoleTabTest(DevConsoleTab):
"""Test dev-console tab."""
+ @override
def refresh(self) -> None:
import random
diff --git a/src/assets/ba_data/python/babase/_emptyappmode.py b/src/assets/ba_data/python/babase/_emptyappmode.py
index f3905b71..3573769a 100644
--- a/src/assets/ba_data/python/babase/_emptyappmode.py
+++ b/src/assets/ba_data/python/babase/_emptyappmode.py
@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
from bacommon.app import AppExperience
import _babase
@@ -18,15 +19,18 @@ if TYPE_CHECKING:
class EmptyAppMode(AppMode):
"""An empty app mode that can be used as a fallback/etc."""
+ @override
@classmethod
def get_app_experience(cls) -> AppExperience:
return AppExperience.EMPTY
+ @override
@classmethod
def _supports_intent(cls, intent: AppIntent) -> bool:
# We support default and exec intents currently.
return isinstance(intent, AppIntentExec | AppIntentDefault)
+ @override
def handle_intent(self, intent: AppIntent) -> None:
if isinstance(intent, AppIntentExec):
_babase.empty_app_mode_handle_intent_exec(intent.code)
@@ -34,10 +38,12 @@ class EmptyAppMode(AppMode):
assert isinstance(intent, AppIntentDefault)
_babase.empty_app_mode_handle_intent_default()
+ @override
def on_activate(self) -> None:
# Let the native layer do its thing.
_babase.on_empty_app_mode_activate()
+ @override
def on_deactivate(self) -> None:
# Let the native layer do its thing.
_babase.on_empty_app_mode_deactivate()
diff --git a/src/assets/ba_data/python/babase/_env.py b/src/assets/ba_data/python/babase/_env.py
index 2af28c23..124caa27 100644
--- a/src/assets/ba_data/python/babase/_env.py
+++ b/src/assets/ba_data/python/babase/_env.py
@@ -9,6 +9,7 @@ import logging
import warnings
from typing import TYPE_CHECKING
+from typing_extensions import override
from efro.log import LogLevel
if TYPE_CHECKING:
@@ -216,6 +217,7 @@ def _feed_logs_to_babase(log_handler: LogHandler) -> None:
class _CustomHelper:
"""Replacement 'help' that behaves better for our setup."""
+ @override
def __repr__(self) -> str:
return 'Type help(object) for help about object.'
diff --git a/src/assets/ba_data/python/babase/_general.py b/src/assets/ba_data/python/babase/_general.py
index 206ac036..2d2738da 100644
--- a/src/assets/ba_data/python/babase/_general.py
+++ b/src/assets/ba_data/python/babase/_general.py
@@ -10,6 +10,7 @@ import logging
import inspect
from typing import TYPE_CHECKING, TypeVar, Protocol, NewType
+from typing_extensions import override
from efro.terminal import Clr
import _babase
@@ -178,6 +179,7 @@ class _WeakCall:
def __call__(self, *args_extra: Any) -> Any:
return self._call(*self._args + args_extra, **self._keywds)
+ @override
def __str__(self) -> str:
return (
''
diff --git a/src/assets/ba_data/python/babase/_language.py b/src/assets/ba_data/python/babase/_language.py
index 059e60ab..3983a7a7 100644
--- a/src/assets/ba_data/python/babase/_language.py
+++ b/src/assets/ba_data/python/babase/_language.py
@@ -8,6 +8,8 @@ import json
import logging
from typing import TYPE_CHECKING, overload
+from typing_extensions import override
+
import _babase
from babase._appsubsystem import AppSubsystem
@@ -217,6 +219,7 @@ class LanguageSubsystem(AppSubsystem):
color=(0, 1, 0),
)
+ @override
def do_apply_app_config(self) -> None:
assert _babase.in_logic_thread()
assert isinstance(_babase.app.config, dict)
@@ -598,9 +601,11 @@ class Lstr:
_error.print_exception('_get_json failed for', self.args)
return 'JSON_ERR'
+ @override
def __str__(self) -> str:
return ''
+ @override
def __repr__(self) -> str:
return ''
@@ -648,5 +653,6 @@ class AttrDict(dict):
assert not isinstance(val, bytes)
return val
+ @override
def __setattr__(self, attr: str, value: Any) -> None:
raise AttributeError()
diff --git a/src/assets/ba_data/python/babase/_login.py b/src/assets/ba_data/python/babase/_login.py
index 39cfa0ed..67968440 100644
--- a/src/assets/ba_data/python/babase/_login.py
+++ b/src/assets/ba_data/python/babase/_login.py
@@ -9,6 +9,7 @@ import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, final
+from typing_extensions import override
from bacommon.login import LoginType
import _babase
@@ -353,6 +354,7 @@ class LoginAdapterNative(LoginAdapter):
self._sign_in_attempt_num = 123
self._sign_in_attempts: dict[int, Callable[[str | None], None]] = {}
+ @override
def get_sign_in_token(
self, completion_cb: Callable[[str | None], None]
) -> None:
@@ -363,6 +365,7 @@ class LoginAdapterNative(LoginAdapter):
self.login_type.value, attempt_id
)
+ @override
def on_back_end_active_change(self, active: bool) -> None:
_babase.login_adapter_back_end_active_change(
self.login_type.value, active
diff --git a/src/assets/ba_data/python/babase/_meta.py b/src/assets/ba_data/python/babase/_meta.py
index 76f88b76..6e1224d8 100644
--- a/src/assets/ba_data/python/babase/_meta.py
+++ b/src/assets/ba_data/python/babase/_meta.py
@@ -26,7 +26,7 @@ EXPORT_CLASS_NAME_SHORTCUTS: dict[str, str] = {
'plugin': 'babase.Plugin',
# DEPRECATED as of 12/2023. Currently am warning if finding these
# but should take this out eventually.
- 'keyboard': 'babase.Keyboard',
+ 'keyboard': 'bauiv1.Keyboard',
}
T = TypeVar('T')
@@ -288,14 +288,12 @@ class DirectoryScan:
) -> None:
"""Scan provided path and add module entries to provided list."""
try:
- # Special case: let's save some time and skip the whole 'babase'
- # package since we know it doesn't contain any meta tags.
fullpath = Path(path, subpath)
+ # Note: skipping hidden dirs (starting with '.').
entries = [
(path, Path(subpath, name))
for name in os.listdir(fullpath)
- # Actually scratch that for now; trying to avoid special cases.
- # if name != 'babase'
+ if not name.startswith('.')
]
except PermissionError:
# Expected sometimes.
diff --git a/src/assets/ba_data/python/babase/_plugin.py b/src/assets/ba_data/python/babase/_plugin.py
index 692840b1..d83050b9 100644
--- a/src/assets/ba_data/python/babase/_plugin.py
+++ b/src/assets/ba_data/python/babase/_plugin.py
@@ -8,6 +8,8 @@ import logging
import importlib.util
from typing import TYPE_CHECKING
+from typing_extensions import override
+
import _babase
from babase._appsubsystem import AppSubsystem
@@ -158,6 +160,7 @@ class PluginSubsystem(AppSubsystem):
if config_changed:
_babase.app.config.commit()
+ @override
def on_app_running(self) -> None:
# Load up our plugins and go ahead and call their on_app_running
# calls.
@@ -170,6 +173,7 @@ class PluginSubsystem(AppSubsystem):
_error.print_exception('Error in plugin on_app_running()')
+ @override
def on_app_suspend(self) -> None:
for plugin in self.active_plugins:
try:
@@ -179,6 +183,7 @@ class PluginSubsystem(AppSubsystem):
_error.print_exception('Error in plugin on_app_suspend()')
+ @override
def on_app_unsuspend(self) -> None:
for plugin in self.active_plugins:
try:
@@ -188,6 +193,7 @@ class PluginSubsystem(AppSubsystem):
_error.print_exception('Error in plugin on_app_unsuspend()')
+ @override
def on_app_shutdown(self) -> None:
for plugin in self.active_plugins:
try:
@@ -197,6 +203,7 @@ class PluginSubsystem(AppSubsystem):
_error.print_exception('Error in plugin on_app_shutdown()')
+ @override
def on_app_shutdown_complete(self) -> None:
for plugin in self.active_plugins:
try:
diff --git a/src/assets/ba_data/python/babase/_ui.py b/src/assets/ba_data/python/babase/_ui.py
index 6f6b0595..6457eadf 100644
--- a/src/assets/ba_data/python/babase/_ui.py
+++ b/src/assets/ba_data/python/babase/_ui.py
@@ -5,6 +5,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
+
from babase._stringedit import StringEditAdapter
import _babase
@@ -24,9 +26,11 @@ class DevConsoleStringEditAdapter(StringEditAdapter):
description, initial_text, max_length, screen_space_center
)
+ @override
def _do_apply(self, new_text: str) -> None:
_babase.set_dev_console_input_text(new_text)
_babase.dev_console_input_adapter_finish()
+ @override
def _do_cancel(self) -> None:
_babase.dev_console_input_adapter_finish()
diff --git a/src/assets/ba_data/python/baclassic/_ads.py b/src/assets/ba_data/python/baclassic/_ads.py
index 2373df36..5cd454d0 100644
--- a/src/assets/ba_data/python/baclassic/_ads.py
+++ b/src/assets/ba_data/python/baclassic/_ads.py
@@ -229,9 +229,7 @@ class AdsSubsystem:
await asyncio.sleep(1.0)
payload.run(fallback=True)
- _fallback_task = babase.app.aioloop.create_task(
- add_fallback_task()
- )
+ babase.app.create_async_task(add_fallback_task())
self.show_ad('between_game', on_completion_call=payload.run)
else:
babase.pushcall(call) # Just run the callback without the ad.
diff --git a/src/assets/ba_data/python/baclassic/_benchmark.py b/src/assets/ba_data/python/baclassic/_benchmark.py
index 42e64636..23033eef 100644
--- a/src/assets/ba_data/python/baclassic/_benchmark.py
+++ b/src/assets/ba_data/python/baclassic/_benchmark.py
@@ -7,6 +7,7 @@ import random
from dataclasses import dataclass
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
import bascenev1
import _baclassic
@@ -43,6 +44,7 @@ def run_cpu_benchmark() -> None:
cfg['Graphics Quality'] = self._old_quality
cfg.apply()
+ @override
def on_player_request(self, player: bascenev1.SessionPlayer) -> bool:
return False
diff --git a/src/assets/ba_data/python/baclassic/_net.py b/src/assets/ba_data/python/baclassic/_net.py
index a76258df..04d3fb6c 100644
--- a/src/assets/ba_data/python/baclassic/_net.py
+++ b/src/assets/ba_data/python/baclassic/_net.py
@@ -9,6 +9,7 @@ import threading
from enum import Enum
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
import bascenev1
@@ -68,6 +69,7 @@ class MasterServerV1CallThread(threading.Thread):
with self._context:
self._callback(arg)
+ @override
def run(self) -> None:
# pylint: disable=consider-using-with
# pylint: disable=too-many-branches
diff --git a/src/assets/ba_data/python/baclassic/_subsystem.py b/src/assets/ba_data/python/baclassic/_subsystem.py
index 4639a608..51dd7679 100644
--- a/src/assets/ba_data/python/baclassic/_subsystem.py
+++ b/src/assets/ba_data/python/baclassic/_subsystem.py
@@ -8,6 +8,7 @@ import random
import logging
import weakref
+from typing_extensions import override
from efro.dataclassio import dataclass_from_dict
import babase
import bauiv1
@@ -149,6 +150,7 @@ class ClassicSubsystem(babase.AppSubsystem):
assert isinstance(self._env['legacy_user_agent_string'], str)
return self._env['legacy_user_agent_string']
+ @override
def on_app_loading(self) -> None:
from bascenev1lib.actor import spazappearance
from bascenev1lib import maps as stdmaps
@@ -230,13 +232,16 @@ class ClassicSubsystem(babase.AppSubsystem):
self.accounts.on_app_loading()
+ @override
def on_app_suspend(self) -> None:
self.accounts.on_app_suspend()
+ @override
def on_app_unsuspend(self) -> None:
self.accounts.on_app_unsuspend()
self.music.on_app_unsuspend()
+ @override
def on_app_shutdown(self) -> None:
self.music.on_app_shutdown()
diff --git a/src/assets/ba_data/python/baclassic/macmusicapp.py b/src/assets/ba_data/python/baclassic/macmusicapp.py
index 27b0e126..12fe6846 100644
--- a/src/assets/ba_data/python/baclassic/macmusicapp.py
+++ b/src/assets/ba_data/python/baclassic/macmusicapp.py
@@ -8,6 +8,7 @@ import threading
from collections import deque
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
from baclassic._music import MusicPlayer
@@ -27,6 +28,7 @@ class MacMusicAppMusicPlayer(MusicPlayer):
self._thread = _MacMusicAppThread()
self._thread.start()
+ @override
def on_select_entry(
self,
callback: Callable[[Any], None],
@@ -40,6 +42,7 @@ class MacMusicAppMusicPlayer(MusicPlayer):
callback, current_entry, selection_target_name
)
+ @override
def on_set_volume(self, volume: float) -> None:
self._thread.set_volume(volume)
@@ -47,6 +50,7 @@ class MacMusicAppMusicPlayer(MusicPlayer):
"""Asynchronously fetch the list of available iTunes playlists."""
self._thread.get_playlists(callback)
+ @override
def on_play(self, entry: Any) -> None:
assert babase.app.classic is not None
music = babase.app.classic.music
@@ -59,9 +63,11 @@ class MacMusicAppMusicPlayer(MusicPlayer):
entry_type,
)
+ @override
def on_stop(self) -> None:
self._thread.play_playlist(None)
+ @override
def on_app_shutdown(self) -> None:
self._thread.shutdown()
@@ -77,6 +83,7 @@ class _MacMusicAppThread(threading.Thread):
self._current_playlist: str | None = None
self._orig_volume: int | None = None
+ @override
def run(self) -> None:
"""Run the Music.app thread."""
babase.set_thread_name('BA_MacMusicAppThread')
diff --git a/src/assets/ba_data/python/baclassic/osmusic.py b/src/assets/ba_data/python/baclassic/osmusic.py
index d808000d..f2198ce5 100644
--- a/src/assets/ba_data/python/baclassic/osmusic.py
+++ b/src/assets/ba_data/python/baclassic/osmusic.py
@@ -9,6 +9,7 @@ import logging
import threading
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
from baclassic._music import MusicPlayer
@@ -33,6 +34,7 @@ class OSMusicPlayer(MusicPlayer):
# FIXME: should ask the C++ layer for these; just hard-coding for now.
return ['mp3', 'ogg', 'm4a', 'wav', 'flac', 'mid']
+ @override
def on_select_entry(
self,
callback: Callable[[Any], None],
@@ -48,9 +50,11 @@ class OSMusicPlayer(MusicPlayer):
callback, current_entry, selection_target_name
)
+ @override
def on_set_volume(self, volume: float) -> None:
babase.music_player_set_volume(volume)
+ @override
def on_play(self, entry: Any) -> None:
assert babase.app.classic is not None
music = babase.app.classic.music
@@ -99,11 +103,13 @@ class OSMusicPlayer(MusicPlayer):
self._actually_playing = True
babase.music_player_play(result)
+ @override
def on_stop(self) -> None:
self._want_to_play = False
self._actually_playing = False
babase.music_player_stop()
+ @override
def on_app_shutdown(self) -> None:
babase.music_player_shutdown()
@@ -120,6 +126,7 @@ class _PickFolderSongThread(threading.Thread):
self._callback = callback
self._path = path
+ @override
def run(self) -> None:
do_log_error = True
try:
diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py
index 664ef95d..cdcf043b 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 21757
+TARGET_BALLISTICA_BUILD = 21766
TARGET_BALLISTICA_VERSION = '1.7.33'
@@ -287,9 +287,9 @@ def _setup_certs(contains_python_dist: bool) -> None:
import certifi
# Let both OpenSSL and requests (if present) know to use this.
- os.environ['SSL_CERT_FILE'] = os.environ[
- 'REQUESTS_CA_BUNDLE'
- ] = certifi.where()
+ os.environ['SSL_CERT_FILE'] = os.environ['REQUESTS_CA_BUNDLE'] = (
+ certifi.where()
+ )
def _setup_paths(
diff --git a/src/assets/ba_data/python/baplus/__init__.py b/src/assets/ba_data/python/baplus/__init__.py
index dd2e4cb3..4040f7b4 100644
--- a/src/assets/ba_data/python/baplus/__init__.py
+++ b/src/assets/ba_data/python/baplus/__init__.py
@@ -16,9 +16,11 @@ from __future__ import annotations
import logging
+from baplus._cloud import CloudSubsystem
from baplus._subsystem import PlusSubsystem
__all__ = [
+ 'CloudSubsystem',
'PlusSubsystem',
]
diff --git a/src/assets/ba_data/python/babase/_cloud.py b/src/assets/ba_data/python/baplus/_cloud.py
similarity index 88%
rename from src/assets/ba_data/python/babase/_cloud.py
rename to src/assets/ba_data/python/baplus/_cloud.py
index 3f5643db..d2e51eb0 100644
--- a/src/assets/ba_data/python/babase/_cloud.py
+++ b/src/assets/ba_data/python/baplus/_cloud.py
@@ -7,8 +7,7 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, overload
-import _babase
-from babase._appsubsystem import AppSubsystem
+import babase
if TYPE_CHECKING:
from typing import Callable, Any
@@ -23,7 +22,7 @@ DEBUG_LOG = False
# internal protocols.
-class CloudSubsystem(AppSubsystem):
+class CloudSubsystem(babase.AppSubsystem):
"""Manages communication with cloud components."""
@property
@@ -44,7 +43,7 @@ class CloudSubsystem(AppSubsystem):
if DEBUG_LOG:
logging.debug('CloudSubsystem: Connectivity is now %s.', connected)
- plus = _babase.app.plus
+ plus = babase.app.plus
assert plus is not None
# Inform things that use this.
@@ -117,12 +116,11 @@ class CloudSubsystem(AppSubsystem):
The provided on_response call will be run in the logic thread
and passed either the response or the error that occurred.
"""
- from babase._general import Call
del msg # Unused.
- _babase.pushcall(
- Call(
+ babase.pushcall(
+ babase.Call(
on_response,
RuntimeError('Cloud functionality is not available.'),
)
@@ -153,6 +151,25 @@ class CloudSubsystem(AppSubsystem):
"""
raise RuntimeError('Cloud functionality is not available.')
+ @overload
+ async def send_message_async(
+ self, msg: bacommon.cloud.PromoCodeMessage
+ ) -> bacommon.cloud.PromoCodeResponse:
+ ...
+
+ @overload
+ async def send_message_async(
+ self, msg: bacommon.cloud.TestMessage
+ ) -> bacommon.cloud.TestResponse:
+ ...
+
+ async def send_message_async(self, msg: Message) -> Response | None:
+ """Synchronously send a message to the cloud.
+
+ Must be called from the logic thread.
+ """
+ raise RuntimeError('Cloud functionality is not available.')
+
def cloud_console_exec(code: str) -> None:
"""Called by the cloud console to run code in the logic thread."""
@@ -188,7 +205,7 @@ def cloud_console_exec(code: str) -> None:
except Exception:
import traceback
- apptime = _babase.apptime()
+ apptime = babase.apptime()
print(f'Exec error at time {apptime:.2f}.', file=sys.stderr)
traceback.print_exc()
diff --git a/src/assets/ba_data/python/baplus/_subsystem.py b/src/assets/ba_data/python/baplus/_subsystem.py
index d38ca31a..567f3cd1 100644
--- a/src/assets/ba_data/python/baplus/_subsystem.py
+++ b/src/assets/ba_data/python/baplus/_subsystem.py
@@ -5,13 +5,17 @@ from __future__ import annotations
from typing import TYPE_CHECKING
-import _baplus
+from typing_extensions import override
from babase import AppSubsystem
+import _baplus
+
if TYPE_CHECKING:
from typing import Callable, Any
- from babase import CloudSubsystem, AccountV2Subsystem
+ from babase import AccountV2Subsystem
+
+ from baplus._cloud import CloudSubsystem
class PlusSubsystem(AppSubsystem):
@@ -32,6 +36,7 @@ class PlusSubsystem(AppSubsystem):
accounts: AccountV2Subsystem
cloud: CloudSubsystem
+ @override
def on_app_loading(self) -> None:
_baplus.on_app_loading()
self.accounts.on_app_loading()
diff --git a/src/assets/ba_data/python/bascenev1/_activitytypes.py b/src/assets/ba_data/python/bascenev1/_activitytypes.py
index c0e410e1..9b46b3eb 100644
--- a/src/assets/ba_data/python/bascenev1/_activitytypes.py
+++ b/src/assets/ba_data/python/bascenev1/_activitytypes.py
@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
import _bascenev1
@@ -34,11 +35,13 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
self.inherits_vr_camera_offset = True
self.inherits_vr_overlay_center = True
+ @override
def on_transition_in(self) -> None:
super().on_transition_in()
babase.fade_screen(False)
babase.lock_all_input()
+ @override
def on_begin(self) -> None:
# pylint: disable=cyclic-import
@@ -77,6 +80,7 @@ class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
self._tips_text: bascenev1.Actor | None = None
self._join_info: JoinInfo | None = None
+ @override
def on_transition_in(self) -> None:
# pylint: disable=cyclic-import
from bascenev1lib.actor.tipstext import TipsText
@@ -110,6 +114,7 @@ class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]):
super().__init__(settings)
self._background: bascenev1.Actor | None = None
+ @override
def on_transition_in(self) -> None:
# pylint: disable=cyclic-import
from bascenev1lib.actor.background import Background
@@ -119,6 +124,7 @@ class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]):
fade_time=0.5, start_faded=False, show_logo=False
)
+ @override
def on_begin(self) -> None:
super().on_begin()
@@ -152,6 +158,7 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
self._custom_continue_message: babase.Lstr | None = None
self._server_transitioning: bool | None = None
+ @override
def on_player_join(self, player: EmptyPlayer) -> None:
super().on_player_join(player)
time_till_assign = max(
@@ -164,6 +171,7 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
time_till_assign, babase.WeakCall(self._safe_assign, player)
)
+ @override
def on_transition_in(self) -> None:
from bascenev1lib.actor.tipstext import TipsText
from bascenev1lib.actor.background import Background
@@ -176,6 +184,7 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
self._tips_text = TipsText()
setmusic(self.default_music)
+ @override
def on_begin(self) -> None:
# pylint: disable=cyclic-import
from bascenev1lib.actor.text import Text
diff --git a/src/assets/ba_data/python/bascenev1/_appmode.py b/src/assets/ba_data/python/bascenev1/_appmode.py
index 9408865d..481fb4e6 100644
--- a/src/assets/ba_data/python/bascenev1/_appmode.py
+++ b/src/assets/ba_data/python/bascenev1/_appmode.py
@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
from bacommon.app import AppExperience
from babase import (
app,
@@ -23,15 +24,18 @@ if TYPE_CHECKING:
class SceneV1AppMode(AppMode):
"""Our app-mode."""
+ @override
@classmethod
def get_app_experience(cls) -> AppExperience:
return AppExperience.MELEE
+ @override
@classmethod
def _supports_intent(cls, intent: AppIntent) -> bool:
# We support default and exec intents currently.
return isinstance(intent, AppIntentExec | AppIntentDefault)
+ @override
def handle_intent(self, intent: AppIntent) -> None:
if isinstance(intent, AppIntentExec):
_bascenev1.handle_app_intent_exec(intent.code)
@@ -39,14 +43,17 @@ class SceneV1AppMode(AppMode):
assert isinstance(intent, AppIntentDefault)
_bascenev1.handle_app_intent_default()
+ @override
def on_activate(self) -> None:
# Let the native layer do its thing.
_bascenev1.on_app_mode_activate()
+ @override
def on_deactivate(self) -> None:
# Let the native layer do its thing.
_bascenev1.on_app_mode_deactivate()
+ @override
def on_app_active_changed(self) -> None:
# If we've gone inactive, bring up the main menu, which has the
# side effect of pausing the action (when possible).
diff --git a/src/assets/ba_data/python/bascenev1/_coopgame.py b/src/assets/ba_data/python/bascenev1/_coopgame.py
index 2c684f85..e0af5158 100644
--- a/src/assets/ba_data/python/bascenev1/_coopgame.py
+++ b/src/assets/ba_data/python/bascenev1/_coopgame.py
@@ -6,6 +6,7 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, TypeVar
+from typing_extensions import override
import babase
import _bascenev1
@@ -31,6 +32,7 @@ class CoopGameActivity(GameActivity[PlayerT, TeamT]):
# We can assume our session is a CoopSession.
session: bascenev1.CoopSession
+ @override
@classmethod
def supports_session_type(
cls, sessiontype: type[bascenev1.Session]
@@ -49,6 +51,7 @@ class CoopGameActivity(GameActivity[PlayerT, TeamT]):
self._life_warning_beep_timer: bascenev1.Timer | None = None
self._warn_beeps_sound = _bascenev1.getsound('warnBeeps')
+ @override
def on_begin(self) -> None:
super().on_begin()
@@ -139,6 +142,7 @@ class CoopGameActivity(GameActivity[PlayerT, TeamT]):
)
vval -= 55
+ @override
def spawn_player_spaz(
self,
player: PlayerT,
diff --git a/src/assets/ba_data/python/bascenev1/_coopsession.py b/src/assets/ba_data/python/bascenev1/_coopsession.py
index 72a67919..f1d59ae4 100644
--- a/src/assets/ba_data/python/bascenev1/_coopsession.py
+++ b/src/assets/ba_data/python/bascenev1/_coopsession.py
@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
import _bascenev1
@@ -97,6 +98,7 @@ class CoopSession(Session):
"""Get the game instance currently being played."""
return self._current_game_instance
+ @override
def should_allow_mid_activity_joins(
self, activity: bascenev1.Activity
) -> bool:
@@ -174,9 +176,11 @@ class CoopSession(Session):
self._tutorial_activity = _bascenev1.newactivity(TutorialActivity)
+ @override
def get_custom_menu_entries(self) -> list[dict[str, Any]]:
return self._custom_menu_ui
+ @override
def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
super().on_player_leave(sessionplayer)
@@ -256,6 +260,7 @@ class CoopSession(Session):
activity.end(results={'outcome': 'restart'}, force=True)
# noinspection PyUnresolvedReferences
+ @override
def on_activity_end(
self, activity: bascenev1.Activity, results: Any
) -> None:
diff --git a/src/assets/ba_data/python/bascenev1/_dependency.py b/src/assets/ba_data/python/bascenev1/_dependency.py
index bbc956c0..0a2f49f2 100644
--- a/src/assets/ba_data/python/bascenev1/_dependency.py
+++ b/src/assets/ba_data/python/bascenev1/_dependency.py
@@ -7,6 +7,7 @@ from __future__ import annotations
import weakref
from typing import Generic, TypeVar, TYPE_CHECKING
+from typing_extensions import override
import babase
import _bascenev1
@@ -313,6 +314,7 @@ class AssetPackage(DependencyComponent):
self.package_id = entry.config
print(f'LOADING ASSET PACKAGE {self.package_id}')
+ @override
@classmethod
def dep_is_present(cls, config: Any = None) -> bool:
assert isinstance(config, str)
diff --git a/src/assets/ba_data/python/bascenev1/_dualteamsession.py b/src/assets/ba_data/python/bascenev1/_dualteamsession.py
index 4b4ab102..82807684 100644
--- a/src/assets/ba_data/python/bascenev1/_dualteamsession.py
+++ b/src/assets/ba_data/python/bascenev1/_dualteamsession.py
@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
import _bascenev1
@@ -32,6 +33,7 @@ class DualTeamSession(MultiTeamSession):
babase.increment_analytics_count('Teams session start')
super().__init__()
+ @override
def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None:
# pylint: disable=cyclic-import
from bascenev1lib.activity.multiteamvictory import (
diff --git a/src/assets/ba_data/python/bascenev1/_freeforallsession.py b/src/assets/ba_data/python/bascenev1/_freeforallsession.py
index 6c71d1b4..2718377c 100644
--- a/src/assets/ba_data/python/bascenev1/_freeforallsession.py
+++ b/src/assets/ba_data/python/bascenev1/_freeforallsession.py
@@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
import _bascenev1
@@ -53,6 +54,7 @@ class FreeForAllSession(MultiTeamSession):
babase.increment_analytics_count('Free-for-all session start')
super().__init__()
+ @override
def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None:
# pylint: disable=cyclic-import
from efro.util import asserttype
diff --git a/src/assets/ba_data/python/bascenev1/_gameactivity.py b/src/assets/ba_data/python/bascenev1/_gameactivity.py
index ad9bf0a5..1557ffc1 100644
--- a/src/assets/ba_data/python/bascenev1/_gameactivity.py
+++ b/src/assets/ba_data/python/bascenev1/_gameactivity.py
@@ -9,6 +9,7 @@ import random
import logging
from typing import TYPE_CHECKING, TypeVar
+from typing_extensions import override
import babase
import _bascenev1
@@ -377,6 +378,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
"""
return ''
+ @override
def on_transition_in(self) -> None:
super().on_transition_in()
@@ -488,6 +490,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
self.end_game()
+ @override
def on_begin(self) -> None:
super().on_begin()
@@ -536,12 +539,14 @@ class GameActivity(Activity[PlayerT, TeamT]):
max(5, data_t[0]['timeRemaining'])
)
+ @override
def on_player_join(self, player: PlayerT) -> None:
super().on_player_join(player)
# By default, just spawn a dude.
self.spawn_player(player)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, PlayerDiedMessage):
# pylint: disable=cyclic-import
@@ -835,6 +840,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0})
_bascenev1.timer(5.0, tnode.delete)
+ @override
def end(
self, results: Any = None, delay: float = 0.0, force: bool = False
) -> None:
diff --git a/src/assets/ba_data/python/bascenev1/_level.py b/src/assets/ba_data/python/bascenev1/_level.py
index 725c962e..2f1906e6 100644
--- a/src/assets/ba_data/python/bascenev1/_level.py
+++ b/src/assets/ba_data/python/bascenev1/_level.py
@@ -7,6 +7,7 @@ import copy
import weakref
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
if TYPE_CHECKING:
@@ -38,6 +39,7 @@ class Level:
self._index: int | None = None
self._score_version_string: str | None = None
+ @override
def __repr__(self) -> str:
cls = type(self)
return f"<{cls.__module__}.{cls.__name__} '{self._name}'>"
diff --git a/src/assets/ba_data/python/bascenev1/_map.py b/src/assets/ba_data/python/bascenev1/_map.py
index 818098fd..832632fc 100644
--- a/src/assets/ba_data/python/bascenev1/_map.py
+++ b/src/assets/ba_data/python/bascenev1/_map.py
@@ -6,6 +6,7 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
import _bascenev1
@@ -353,9 +354,11 @@ class Map(Actor):
return self.flag_points_default[:3]
return self.flag_points[team_index % len(self.flag_points)][:3]
+ @override
def exists(self) -> bool:
return bool(self.node)
+ @override
def handlemessage(self, msg: Any) -> Any:
from bascenev1 import _messages
diff --git a/src/assets/ba_data/python/bascenev1/_multiteamsession.py b/src/assets/ba_data/python/bascenev1/_multiteamsession.py
index 1fcb3d32..77459baa 100644
--- a/src/assets/ba_data/python/bascenev1/_multiteamsession.py
+++ b/src/assets/ba_data/python/bascenev1/_multiteamsession.py
@@ -8,7 +8,9 @@ import random
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
+
import _bascenev1
from bascenev1._session import Session
@@ -160,6 +162,7 @@ class MultiTeamSession(Session):
"""Returns which game in the series is currently being played."""
return self._game_number
+ @override
def on_team_join(self, team: bascenev1.SessionTeam) -> None:
team.customdata['previous_score'] = team.customdata['score'] = 0
@@ -178,6 +181,7 @@ class MultiTeamSession(Session):
self._next_game_spec['settings'],
)
+ @override
def on_activity_end(
self, activity: bascenev1.Activity, results: Any
) -> None:
diff --git a/src/assets/ba_data/python/bascenev1/_nodeactor.py b/src/assets/ba_data/python/bascenev1/_nodeactor.py
index 2e9a1483..7b3551e7 100644
--- a/src/assets/ba_data/python/bascenev1/_nodeactor.py
+++ b/src/assets/ba_data/python/bascenev1/_nodeactor.py
@@ -6,6 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
+
from bascenev1._messages import DieMessage
from bascenev1._actor import Actor
@@ -28,6 +30,7 @@ class NodeActor(Actor):
super().__init__()
self.node = node
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, DieMessage):
if self.node:
@@ -35,5 +38,6 @@ class NodeActor(Actor):
return None
return super().handlemessage(msg)
+ @override
def exists(self) -> bool:
return bool(self.node)
diff --git a/src/assets/ba_data/python/bascenev1/_teamgame.py b/src/assets/ba_data/python/bascenev1/_teamgame.py
index d438dbca..e835bd0e 100644
--- a/src/assets/ba_data/python/bascenev1/_teamgame.py
+++ b/src/assets/ba_data/python/bascenev1/_teamgame.py
@@ -7,6 +7,7 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, TypeVar
+from typing_extensions import override
import babase
import _bascenev1
@@ -35,6 +36,7 @@ class TeamGameActivity(GameActivity[PlayerT, TeamT]):
bascenev1.Player has their own bascenev1.Team)
"""
+ @override
@classmethod
def supports_session_type(
cls, sessiontype: type[bascenev1.Session]
@@ -57,6 +59,7 @@ class TeamGameActivity(GameActivity[PlayerT, TeamT]):
if isinstance(self.session, FreeForAllSession):
self.show_kill_points = False
+ @override
def on_transition_in(self) -> None:
# pylint: disable=cyclic-import
from bascenev1._coopsession import CoopSession
@@ -85,6 +88,7 @@ class TeamGameActivity(GameActivity[PlayerT, TeamT]):
).autoretain()
setattr(self.session, attrname, True)
+ @override
def on_begin(self) -> None:
super().on_begin()
try:
@@ -104,6 +108,7 @@ class TeamGameActivity(GameActivity[PlayerT, TeamT]):
except Exception:
logging.exception('Error in on_begin.')
+ @override
def spawn_player_spaz(
self,
player: PlayerT,
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py b/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py
index 0777e8f1..55f3fd8e 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/coopjoin.py
@@ -4,6 +4,7 @@
from __future__ import annotations
+from typing_extensions import override
import bascenev1 as bs
@@ -18,6 +19,7 @@ class CoopJoinActivity(bs.JoinActivity):
session = self.session
assert isinstance(session, bs.CoopSession)
+ @override
def on_transition_in(self) -> None:
from bascenev1lib.actor.controlsguide import ControlsGuide
from bascenev1lib.actor.text import Text
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
index 8b335a19..a88c418b 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/coopscore.py
@@ -9,6 +9,7 @@ import random
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
from bacommon.login import LoginType
import bascenev1 as bs
import bauiv1 as bui
@@ -186,6 +187,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
self._victory: bool = settings['outcome'] == 'victory'
+ @override
def __del__(self) -> None:
super().__del__()
@@ -194,6 +196,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
with bui.ContextRef.empty():
bui.containerwidget(edit=self._root_ui, transition='out_left')
+ @override
def on_transition_in(self) -> None:
from bascenev1lib.actor import background # FIXME NO BSSTD
@@ -574,6 +577,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
self._player_press,
)
+ @override
def on_player_join(self, player: bs.Player) -> None:
super().on_player_join(player)
@@ -585,6 +589,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
bs.timer(time_till_assign, bs.WeakCall(self._safe_assign, player))
+ @override
def on_begin(self) -> None:
# FIXME: Clean this up.
# pylint: disable=too-many-statements
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/drawscore.py b/src/assets/ba_data/python/bascenev1lib/activity/drawscore.py
index 2d260dfd..d6e1b289 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/drawscore.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/drawscore.py
@@ -4,7 +4,9 @@
from __future__ import annotations
+from typing_extensions import override
import bascenev1 as bs
+
from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity
from bascenev1lib.actor.zoomtext import ZoomText
@@ -14,6 +16,7 @@ class DrawScoreScreenActivity(MultiTeamScoreScreenActivity):
default_music = None # Awkward silence...
+ @override
def on_begin(self) -> None:
bs.set_analytics_screen('Draw Score Screen')
super().on_begin()
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/dualteamscore.py b/src/assets/ba_data/python/bascenev1lib/activity/dualteamscore.py
index c369e17b..2df370bd 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/dualteamscore.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/dualteamscore.py
@@ -4,7 +4,9 @@
from __future__ import annotations
+from typing_extensions import override
import bascenev1 as bs
+
from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity
from bascenev1lib.actor.zoomtext import ZoomText
@@ -17,6 +19,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
self._winner: bs.SessionTeam = settings['winner']
assert isinstance(self._winner, bs.SessionTeam)
+ @override
def on_begin(self) -> None:
bs.set_analytics_screen('Teams Score Screen')
super().on_begin()
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/freeforallvictory.py b/src/assets/ba_data/python/bascenev1lib/activity/freeforallvictory.py
index 45d116ac..cf82d4ad 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/freeforallvictory.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/freeforallvictory.py
@@ -6,9 +6,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING
-from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity
+from typing_extensions import override
import bascenev1 as bs
+from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity
+
if TYPE_CHECKING:
from typing import Any
@@ -23,6 +25,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
self.transition_time = 0.5
self._cymbal_sound = bs.getsound('cymbal')
+ @override
def on_begin(self) -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/multiteamjoin.py b/src/assets/ba_data/python/bascenev1lib/activity/multiteamjoin.py
index 2870c1fd..ff8f74be 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/multiteamjoin.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/multiteamjoin.py
@@ -4,7 +4,9 @@
from __future__ import annotations
+from typing_extensions import override
import bascenev1 as bs
+
from bascenev1lib.actor.text import Text
@@ -15,6 +17,7 @@ class MultiTeamJoinActivity(bs.JoinActivity):
super().__init__(settings)
self._next_up_text: Text | None = None
+ @override
def on_transition_in(self) -> None:
from bascenev1lib.actor.controlsguide import ControlsGuide
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/multiteamscore.py b/src/assets/ba_data/python/bascenev1lib/activity/multiteamscore.py
index ef2ba012..7b754014 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/multiteamscore.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/multiteamscore.py
@@ -3,7 +3,9 @@
"""Functionality related to teams mode score screen."""
from __future__ import annotations
+from typing_extensions import override
import bascenev1 as bs
+
from bascenev1lib.actor.text import Text
from bascenev1lib.actor.image import Image
@@ -18,6 +20,7 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
self._show_up_next: bool = True
+ @override
def on_begin(self) -> None:
super().on_begin()
session = self.session
diff --git a/src/assets/ba_data/python/bascenev1lib/activity/multiteamvictory.py b/src/assets/ba_data/python/bascenev1lib/activity/multiteamvictory.py
index 203f0516..afc2a26b 100644
--- a/src/assets/ba_data/python/bascenev1lib/activity/multiteamvictory.py
+++ b/src/assets/ba_data/python/bascenev1lib/activity/multiteamvictory.py
@@ -4,7 +4,9 @@
from __future__ import annotations
+from typing_extensions import override
import bascenev1 as bs
+
from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity
@@ -22,6 +24,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
self._tips_text = None
self._default_show_tips = False
+ @override
def on_begin(self) -> None:
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/background.py b/src/assets/ba_data/python/bascenev1lib/actor/background.py
index 2ad629a2..d5625a27 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/background.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/background.py
@@ -9,6 +9,7 @@ import weakref
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
if TYPE_CHECKING:
@@ -104,6 +105,7 @@ class Background(bs.Actor):
timeval += random.random() * 0.1
bs.animate(cmb, 'input1', keys, loop=True)
+ @override
def __del__(self) -> None:
# Normal actors don't get sent DieMessages when their
# activity is shutting down, but we still need to do so
@@ -138,6 +140,7 @@ class Background(bs.Actor):
)
bs.timer(self.fade_time + 0.1, self.node.delete)
+ @override
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, bs.DieMessage):
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/bomb.py b/src/assets/ba_data/python/bascenev1lib/actor/bomb.py
index 00cb4e76..058057d0 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/bomb.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/bomb.py
@@ -10,7 +10,9 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING, TypeVar
+from typing_extensions import override
import bascenev1 as bs
+
from bascenev1lib.gameutils import SharedObjects
if TYPE_CHECKING:
@@ -661,6 +663,7 @@ class Blast(bs.Actor):
bs.timer(0.4, _extra_debris_sound)
+ @override
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
@@ -935,6 +938,7 @@ class Bomb(bs.Actor):
else None
)
+ @override
def on_expire(self) -> None:
super().on_expire()
@@ -1140,6 +1144,7 @@ class Bomb(bs.Actor):
if msg.srcnode:
pass
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ExplodeMessage):
self.explode()
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py b/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py
index 23c4a977..f3068f0e 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/controlsguide.py
@@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
if TYPE_CHECKING:
@@ -547,9 +548,11 @@ class ControlsGuide(bs.Actor):
self._update_timer = None
self._dead = True
+ @override
def exists(self) -> bool:
return not self._dead
+ @override
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, bs.DieMessage):
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/flag.py b/src/assets/ba_data/python/bascenev1lib/actor/flag.py
index 7c8f5364..cd24c9ef 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/flag.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/flag.py
@@ -7,9 +7,11 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
-from bascenev1lib.gameutils import SharedObjects
+from typing_extensions import override
import bascenev1 as bs
+from bascenev1lib.gameutils import SharedObjects
+
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -328,6 +330,7 @@ class Flag(bs.Actor):
1.0, bs.WeakCall(self._hide_score_text)
)
+ @override
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, bs.DieMessage):
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/image.py b/src/assets/ba_data/python/bascenev1lib/actor/image.py
index 4fc61bfa..00e71d0c 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/image.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/image.py
@@ -7,6 +7,7 @@ from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
if TYPE_CHECKING:
@@ -165,6 +166,7 @@ class Image(bs.Actor):
bs.WeakCall(self.handlemessage, bs.DieMessage()),
)
+ @override
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, bs.DieMessage):
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/onscreencountdown.py b/src/assets/ba_data/python/bascenev1lib/actor/onscreencountdown.py
index 7c1a9042..68c477b6 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/onscreencountdown.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/onscreencountdown.py
@@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
if TYPE_CHECKING:
@@ -72,6 +73,7 @@ class OnScreenCountdown(bs.Actor):
)
self._timer = bs.Timer(1.0, self._update, repeat=True)
+ @override
def on_expire(self) -> None:
super().on_expire()
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/onscreentimer.py b/src/assets/ba_data/python/bascenev1lib/actor/onscreentimer.py
index 6f3b8dfa..192fecc2 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/onscreentimer.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/onscreentimer.py
@@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import logging
+from typing_extensions import override
import bascenev1 as bs
if TYPE_CHECKING:
@@ -93,6 +94,7 @@ class OnScreenTimer(bs.Actor):
"""Shortcut for start time in seconds."""
return self.getstarttime()
+ @override
def handlemessage(self, msg: Any) -> Any:
# if we're asked to die, just kill our node/timer
if isinstance(msg, bs.DieMessage):
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/playerspaz.py b/src/assets/ba_data/python/bascenev1lib/actor/playerspaz.py
index 4902e57f..2ea3ead7 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/playerspaz.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/playerspaz.py
@@ -6,7 +6,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING, TypeVar, overload
+from typing_extensions import override
import bascenev1 as bs
+
from bascenev1lib.actor.spaz import Spaz
if TYPE_CHECKING:
@@ -183,6 +185,7 @@ class PlayerSpaz(Spaz):
' non-connected player'
)
+ @override
def handlemessage(self, msg: Any) -> Any:
# FIXME: Tidy this up.
# pylint: disable=too-many-branches
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/popuptext.py b/src/assets/ba_data/python/bascenev1lib/actor/popuptext.py
index c5c1dccd..6fdd41d6 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/popuptext.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/popuptext.py
@@ -7,6 +7,7 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
if TYPE_CHECKING:
@@ -118,6 +119,7 @@ class PopupText(bs.Actor):
lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage())
)
+ @override
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, bs.DieMessage):
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/powerupbox.py b/src/assets/ba_data/python/bascenev1lib/actor/powerupbox.py
index 893cb2df..1b95e4df 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/powerupbox.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/powerupbox.py
@@ -7,7 +7,9 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
+
from bascenev1lib.gameutils import SharedObjects
if TYPE_CHECKING:
@@ -278,6 +280,7 @@ class PowerupBox(bs.Actor):
if self.node:
self.node.flashing = True
+ @override
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/spaz.py b/src/assets/ba_data/python/bascenev1lib/actor/spaz.py
index 71a31a4c..ad914b8c 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/spaz.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/spaz.py
@@ -9,11 +9,13 @@ import random
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.bomb import Bomb, Blast
from bascenev1lib.actor.powerupbox import PowerupBoxFactory
from bascenev1lib.actor.spazfactory import SpazFactory
from bascenev1lib.gameutils import SharedObjects
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence, Callable
@@ -228,9 +230,11 @@ class Spaz(bs.Actor):
self.punch_callback: Callable[[Spaz], Any] | None = None
self.pick_up_powerup_callback: Callable[[Spaz], Any] | None = None
+ @override
def exists(self) -> bool:
return bool(self.node)
+ @override
def on_expire(self) -> None:
super().on_expire()
@@ -249,6 +253,7 @@ class Spaz(bs.Actor):
assert not self.expired
self._dropped_bomb_callbacks.append(call)
+ @override
def is_alive(self) -> bool:
"""
Method override; returns whether ol' spaz is still kickin'.
@@ -694,6 +699,7 @@ class Spaz(bs.Actor):
else:
self.shield_decay_timer = None
+ @override
def handlemessage(self, msg: Any) -> Any:
# pylint: disable=too-many-return-statements
# pylint: disable=too-many-statements
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/spazbot.py b/src/assets/ba_data/python/bascenev1lib/actor/spazbot.py
index 75d0eeb3..bdd8c271 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/spazbot.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/spazbot.py
@@ -10,6 +10,7 @@ import weakref
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
from bascenev1lib.actor.spaz import Spaz
@@ -489,6 +490,7 @@ class SpazBot(Spaz):
self.on_punch_press()
self.on_punch_release()
+ @override
def on_punched(self, damage: int) -> None:
"""
Method override; sends bs.SpazBotPunchedMessage
@@ -496,6 +498,7 @@ class SpazBot(Spaz):
"""
bs.getactivity().handlemessage(SpazBotPunchedMessage(self, damage))
+ @override
def on_expire(self) -> None:
super().on_expire()
@@ -503,6 +506,7 @@ class SpazBot(Spaz):
# no chance of them keeping activities or other things alive.
self.update_callback = None
+ @override
def handlemessage(self, msg: Any) -> Any:
# pylint: disable=too-many-branches
assert not self.expired
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/text.py b/src/assets/ba_data/python/bascenev1lib/actor/text.py
index 1a274d64..15698510 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/text.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/text.py
@@ -7,6 +7,7 @@ from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
if TYPE_CHECKING:
@@ -221,6 +222,7 @@ class Text(bs.Actor):
bs.WeakCall(self.handlemessage, bs.DieMessage()),
)
+ @override
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, bs.DieMessage):
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/tipstext.py b/src/assets/ba_data/python/bascenev1lib/actor/tipstext.py
index ede00802..7a6335fc 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/tipstext.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/tipstext.py
@@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
if TYPE_CHECKING:
@@ -95,6 +96,7 @@ class TipsText(bs.Actor):
)
self.node.text = next_tip
+ @override
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, bs.DieMessage):
diff --git a/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py b/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py
index e50a16a9..12cdb757 100644
--- a/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py
+++ b/src/assets/ba_data/python/bascenev1lib/actor/zoomtext.py
@@ -8,6 +8,7 @@ import random
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
if TYPE_CHECKING:
@@ -158,6 +159,7 @@ class ZoomText(bs.Actor):
if lifespan is not None:
bs.timer(lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage()))
+ @override
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, bs.DieMessage):
diff --git a/src/assets/ba_data/python/bascenev1lib/game/assault.py b/src/assets/ba_data/python/bascenev1lib/game/assault.py
index 183a017f..12eb1229 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/assault.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/assault.py
@@ -10,11 +10,13 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.flag import Flag
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.gameutils import SharedObjects
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -71,10 +73,12 @@ class AssaultGame(bs.TeamGameActivity[Player, Team]):
bs.BoolSetting('Epic Mode', default=False),
]
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return issubclass(sessiontype, bs.DualTeamSession)
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -96,16 +100,19 @@ class AssaultGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH
)
+ @override
def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1:
return 'Touch the enemy flag.'
return 'Touch the enemy flag ${ARG1} times.', self._score_to_win
+ @override
def get_instance_description_short(self) -> str | Sequence:
if self._score_to_win == 1:
return 'touch 1 flag'
return 'touch ${ARG1} flags', self._score_to_win
+ @override
def create_team(self, sessionteam: bs.SessionTeam) -> Team:
shared = SharedObjects.get()
base_pos = self.map.get_flag_position(sessionteam.id)
@@ -151,16 +158,19 @@ class AssaultGame(bs.TeamGameActivity[Player, Team]):
return team
+ @override
def on_team_join(self, team: Team) -> None:
# Can't do this in create_team because the team's color/etc. have
# not been wired up yet at that point.
self._update_scoreboard()
+ @override
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
super().handlemessage(msg) # Augment standard.
@@ -249,6 +259,7 @@ class AssaultGame(bs.TeamGameActivity[Player, Team]):
if player_team.score >= self._score_to_win:
self.end_game()
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
diff --git a/src/assets/ba_data/python/bascenev1lib/game/capturetheflag.py b/src/assets/ba_data/python/bascenev1lib/game/capturetheflag.py
index 4e5da2d6..837c3be1 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/capturetheflag.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/capturetheflag.py
@@ -10,6 +10,9 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.flag import (
@@ -19,7 +22,6 @@ from bascenev1lib.actor.flag import (
FlagDroppedMessage,
FlagDiedMessage,
)
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -141,10 +143,12 @@ class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]):
bs.BoolSetting('Epic Mode', default=False),
]
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return issubclass(sessiontype, bs.DualTeamSession)
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -173,16 +177,19 @@ class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FLAG_CATCHER
)
+ @override
def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1:
return 'Steal the enemy flag.'
return 'Steal the enemy flag ${ARG1} times.', self._score_to_win
+ @override
def get_instance_description_short(self) -> str | Sequence:
if self._score_to_win == 1:
return 'return 1 flag'
return 'return ${ARG1} flags', self._score_to_win
+ @override
def create_team(self, sessionteam: bs.SessionTeam) -> Team:
# Create our team instance and its initial values.
@@ -272,12 +279,14 @@ class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]):
return team
+ @override
def on_team_join(self, team: Team) -> None:
# Can't do this in create_team because the team's color/etc. have
# not been wired up yet at that point.
self._spawn_flag_for_team(team)
self._update_scoreboard()
+ @override
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
@@ -406,6 +415,7 @@ class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]):
if team.score >= self._score_to_win:
self.end_game()
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
@@ -532,6 +542,7 @@ class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]):
bs.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
bs.timer(length, light.delete)
+ @override
def spawn_player_spaz(
self,
player: Player,
@@ -576,6 +587,7 @@ class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]):
team, team.score, self._score_to_win
)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
super().handlemessage(msg) # Augment standard behavior.
diff --git a/src/assets/ba_data/python/bascenev1lib/game/chosenone.py b/src/assets/ba_data/python/bascenev1lib/game/chosenone.py
index 81576190..aba373d7 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/chosenone.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/chosenone.py
@@ -10,11 +10,13 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.flag import Flag
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.gameutils import SharedObjects
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -83,6 +85,7 @@ class ChosenOneGame(bs.TeamGameActivity[Player, Team]):
]
scoreconfig = bs.ScoreConfig(label='Time Held')
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -121,20 +124,25 @@ class ChosenOneGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.CHOSEN_ONE
)
+ @override
def get_instance_description(self) -> str | Sequence:
return 'There can be only one.'
+ @override
def create_team(self, sessionteam: bs.SessionTeam) -> Team:
return Team(time_remaining=self._chosen_one_time)
+ @override
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
+ @override
def on_player_leave(self, player: Player) -> None:
super().on_player_leave(player)
if self._get_chosen_one_player() is player:
self._set_chosen_one_player(None)
+ @override
def on_begin(self) -> None:
super().on_begin()
shared = SharedObjects.get()
@@ -251,6 +259,7 @@ class ChosenOneGame(bs.TeamGameActivity[Player, Team]):
logging.error('got nonexistent player as chosen one in _tick')
self._set_chosen_one_player(None)
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
@@ -335,6 +344,7 @@ class ChosenOneGame(bs.TeamGameActivity[Player, Team]):
'position', light.node, 'position'
)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
# Augment standard behavior.
diff --git a/src/assets/ba_data/python/bascenev1lib/game/conquest.py b/src/assets/ba_data/python/bascenev1lib/game/conquest.py
index 8b1589c1..d50041f6 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/conquest.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/conquest.py
@@ -10,12 +10,14 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.flag import Flag
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.gameutils import SharedObjects
from bascenev1lib.actor.respawnicon import RespawnIcon
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -108,10 +110,12 @@ class ConquestGame(bs.TeamGameActivity[Player, Team]):
bs.BoolSetting('Epic Mode', default=False),
]
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return issubclass(sessiontype, bs.DualTeamSession)
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -143,16 +147,20 @@ class ConquestGame(bs.TeamGameActivity[Player, Team]):
),
)
+ @override
def get_instance_description(self) -> str | Sequence:
return 'Secure all ${ARG1} flags.', len(self.map.flag_points)
+ @override
def get_instance_description_short(self) -> str | Sequence:
return 'secure all ${ARG1} flags', len(self.map.flag_points)
+ @override
def on_team_join(self, team: Team) -> None:
if self.has_begun():
self._update_scores()
+ @override
def on_player_join(self, player: Player) -> None:
player.respawn_timer = None
@@ -160,6 +168,7 @@ class ConquestGame(bs.TeamGameActivity[Player, Team]):
if player.team.flags_held > 0:
self.spawn_player(player)
+ @override
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
@@ -221,6 +230,7 @@ class ConquestGame(bs.TeamGameActivity[Player, Team]):
team, team.flags_held, len(self._flags)
)
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
@@ -272,6 +282,7 @@ class ConquestGame(bs.TeamGameActivity[Player, Team]):
):
self.spawn_player(otherplayer)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
# Augment standard behavior.
@@ -287,6 +298,7 @@ class ConquestGame(bs.TeamGameActivity[Player, Team]):
else:
super().handlemessage(msg)
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
# We spawn players at different places based on what flags are held.
return self.spawn_player_spaz(
diff --git a/src/assets/ba_data/python/bascenev1lib/game/deathmatch.py b/src/assets/ba_data/python/bascenev1lib/game/deathmatch.py
index ccbadd8d..7721b636 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/deathmatch.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/deathmatch.py
@@ -9,9 +9,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.scoreboard import Scoreboard
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -38,6 +40,7 @@ class DeathMatchGame(bs.TeamGameActivity[Player, Team]):
# Print messages when players die since it matters here.
announce_player_deaths = True
+ @override
@classmethod
def get_available_settings(
cls, sessiontype: type[bs.Session]
@@ -87,12 +90,14 @@ class DeathMatchGame(bs.TeamGameActivity[Player, Team]):
return settings
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return issubclass(sessiontype, bs.DualTeamSession) or issubclass(
sessiontype, bs.FreeForAllSession
)
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -116,16 +121,20 @@ class DeathMatchGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.TO_THE_DEATH
)
+ @override
def get_instance_description(self) -> str | Sequence:
return 'Crush ${ARG1} of your enemies.', self._score_to_win
+ @override
def get_instance_description_short(self) -> str | Sequence:
return 'kill ${ARG1} enemies', self._score_to_win
+ @override
def on_team_join(self, team: Team) -> None:
if self.has_begun():
self._update_scoreboard()
+ @override
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
@@ -137,6 +146,7 @@ class DeathMatchGame(bs.TeamGameActivity[Player, Team]):
)
self._update_scoreboard()
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
# Augment standard behavior.
@@ -197,6 +207,7 @@ class DeathMatchGame(bs.TeamGameActivity[Player, Team]):
team, team.score, self._score_to_win
)
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
diff --git a/src/assets/ba_data/python/bascenev1lib/game/easteregghunt.py b/src/assets/ba_data/python/bascenev1lib/game/easteregghunt.py
index 4eb2d5b5..e7159f8d 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/easteregghunt.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/easteregghunt.py
@@ -10,6 +10,9 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.bomb import Bomb
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.spazbot import SpazBotSet, BouncyBot, SpazBotDiedMessage
@@ -17,7 +20,6 @@ from bascenev1lib.actor.onscreencountdown import OnScreenCountdown
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.respawnicon import RespawnIcon
from bascenev1lib.gameutils import SharedObjects
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any
@@ -51,11 +53,13 @@ class EasterEggHuntGame(bs.TeamGameActivity[Player, Team]):
scoreconfig = bs.ScoreConfig(label='Score', scoretype=bs.ScoreType.POINTS)
# We're currently hard-coded for one map.
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
return ['Tower D']
# We support teams, free-for-all, and co-op sessions.
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return (
@@ -93,11 +97,13 @@ class EasterEggHuntGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH
)
+ @override
def on_team_join(self, team: Team) -> None:
if self.has_begun():
self._update_scoreboard()
# Called when our game actually starts.
+ @override
def on_begin(self) -> None:
from bascenev1lib.maps import TowerD
@@ -118,6 +124,7 @@ class EasterEggHuntGame(bs.TeamGameActivity[Player, Team]):
self._spawn_evil_bunny()
# Overriding the default character spawning.
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
spaz = self.spawn_player_spaz(player)
spaz.connect_controls_to_player()
@@ -191,6 +198,7 @@ class EasterEggHuntGame(bs.TeamGameActivity[Player, Team]):
self._eggs.append(Egg(position=(xpos, ypos, zpos)))
# Various high-level game events come through this method.
+ @override
def handlemessage(self, msg: Any) -> Any:
# Respawn dead players.
if isinstance(msg, bs.PlayerDiedMessage):
@@ -231,6 +239,7 @@ class EasterEggHuntGame(bs.TeamGameActivity[Player, Team]):
for team in self.teams:
self._scoreboard.set_team_value(team, team.score)
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
@@ -271,9 +280,11 @@ class Egg(bs.Actor):
},
)
+ @override
def exists(self) -> bool:
return bool(self.node)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.DieMessage):
if self.node:
diff --git a/src/assets/ba_data/python/bascenev1lib/game/elimination.py b/src/assets/ba_data/python/bascenev1lib/game/elimination.py
index 51b09895..a7f9429f 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/elimination.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/elimination.py
@@ -10,9 +10,11 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.spazfactory import SpazFactory
from bascenev1lib.actor.scoreboard import Scoreboard
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -157,6 +159,7 @@ class Icon(bs.Actor):
if lives == 0:
bs.timer(0.6, self.update_for_lives)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.DieMessage):
self.node.delete()
@@ -194,6 +197,7 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]):
allow_mid_activity_joins = False
+ @override
@classmethod
def get_available_settings(
cls, sessiontype: type[bs.Session]
@@ -238,12 +242,14 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]):
)
return settings
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return issubclass(sessiontype, bs.DualTeamSession) or issubclass(
sessiontype, bs.FreeForAllSession
)
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -269,6 +275,7 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL
)
+ @override
def get_instance_description(self) -> str | Sequence:
return (
'Last team standing wins.'
@@ -276,6 +283,7 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]):
else 'Last one standing wins.'
)
+ @override
def get_instance_description_short(self) -> str | Sequence:
return (
'last team standing wins'
@@ -283,6 +291,7 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]):
else 'last one standing wins'
)
+ @override
def on_player_join(self, player: Player) -> None:
player.lives = self._lives_per_player
@@ -299,6 +308,7 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]):
if self.has_begun():
self._update_icons()
+ @override
def on_begin(self) -> None:
super().on_begin()
self._start_time = bs.time()
@@ -469,6 +479,7 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]):
return points[-1][1]
return None
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
actor = self.spawn_player_spaz(player, self._get_spawn_point(player))
if not self._solo_mode:
@@ -495,6 +506,7 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]):
position=player.node.position,
).autoretain()
+ @override
def on_player_leave(self, player: Player) -> None:
super().on_player_leave(player)
player.icons = []
@@ -518,6 +530,7 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]):
def _get_total_team_lives(self, team: Team) -> int:
return sum(player.lives for player in team.players)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
# Augment standard behavior.
@@ -588,6 +601,7 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]):
and any(player.lives > 0 for player in team.players)
]
+ @override
def end_game(self) -> None:
if self.has_ended():
return
diff --git a/src/assets/ba_data/python/bascenev1lib/game/football.py b/src/assets/ba_data/python/bascenev1lib/game/football.py
index 5a286a63..1c99555b 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/football.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/football.py
@@ -1,5 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
+# pylint: disable=too-many-lines
"""Implements football games (both co-op and teams varieties)."""
# ba_meta require api 8
@@ -12,6 +13,9 @@ import random
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.bomb import TNTSpawner
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.scoreboard import Scoreboard
@@ -39,7 +43,6 @@ from bascenev1lib.actor.spazbot import (
StickyBot,
ExplodeyBot,
)
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -128,11 +131,13 @@ class FootballTeamGame(bs.TeamGameActivity[Player, Team]):
bs.BoolSetting('Epic Mode', default=False),
]
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
# We only support two-team play.
return issubclass(sessiontype, bs.DualTeamSession)
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -170,6 +175,7 @@ class FootballTeamGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FOOTBALL
)
+ @override
def get_instance_description(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
@@ -181,6 +187,7 @@ class FootballTeamGame(bs.TeamGameActivity[Player, Team]):
return 'Score ${ARG1} touchdowns.', touchdowns
return 'Score a touchdown.'
+ @override
def get_instance_description_short(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
touchdowns = math.ceil(touchdowns)
@@ -188,6 +195,7 @@ class FootballTeamGame(bs.TeamGameActivity[Player, Team]):
return 'score ${ARG1} touchdowns', touchdowns
return 'score a touchdown'
+ @override
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
@@ -224,6 +232,7 @@ class FootballTeamGame(bs.TeamGameActivity[Player, Team]):
self._update_scoreboard()
self._chant_sound.play()
+ @override
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
@@ -285,6 +294,7 @@ class FootballTeamGame(bs.TeamGameActivity[Player, Team]):
bs.cameraflash(duration=10.0)
self._update_scoreboard()
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
@@ -298,6 +308,7 @@ class FootballTeamGame(bs.TeamGameActivity[Player, Team]):
team, team.score, self._score_to_win
)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, FlagPickedUpMessage):
assert isinstance(msg.flag, FootballFlag)
@@ -379,9 +390,11 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
default_music = bs.MusicType.FOOTBALL
# FIXME: Need to update co-op games to use getscoreconfig.
+ @override
def get_score_type(self) -> str:
return 'time'
+ @override
def get_instance_description(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
touchdowns = math.ceil(touchdowns)
@@ -389,6 +402,7 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
return 'Score ${ARG1} touchdowns.', touchdowns
return 'Score a touchdown.'
+ @override
def get_instance_description_short(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
touchdowns = math.ceil(touchdowns)
@@ -444,6 +458,7 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
self._flag_respawn_light: bs.Actor | None = None
self._flag: FootballFlag | None = None
+ @override
def on_transition_in(self) -> None:
super().on_transition_in()
self._scoreboard = Scoreboard()
@@ -480,6 +495,7 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
)
self._chant_sound.play()
+ @override
def on_begin(self) -> None:
# FIXME: Split this up a bit.
# pylint: disable=too-many-statements
@@ -795,11 +811,13 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
if i == 0:
bs.cameraflash(duration=10.0)
+ @override
def end_game(self) -> None:
bs.setmusic(None)
self._bots.final_celebrate()
bs.timer(0.001, bs.Call(self.do_end, 'defeat'))
+ @override
def on_continue(self) -> None:
# Subtract one touchdown from the bots and get them moving again.
assert self._bot_team is not None
@@ -897,6 +915,7 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
},
)
+ @override
def handlemessage(self, msg: Any) -> Any:
"""handle high-level game messages"""
if isinstance(msg, bs.PlayerDiedMessage):
@@ -959,6 +978,7 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
del player # Unused.
self._player_has_punched = True
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
spaz = self.spawn_player_spaz(
player, position=self.map.get_start_position(player.team.id)
diff --git a/src/assets/ba_data/python/bascenev1lib/game/hockey.py b/src/assets/ba_data/python/bascenev1lib/game/hockey.py
index b61b7dfd..64487e5a 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/hockey.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/hockey.py
@@ -9,11 +9,13 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.powerupbox import PowerupBoxFactory
from bascenev1lib.gameutils import SharedObjects
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -58,6 +60,7 @@ class Puck(bs.Actor):
)
bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1})
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.DieMessage):
if self.node:
@@ -152,10 +155,12 @@ class HockeyGame(bs.TeamGameActivity[Player, Team]):
bs.BoolSetting('Epic Mode', default=False),
]
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return issubclass(sessiontype, bs.DualTeamSession)
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -231,16 +236,19 @@ class HockeyGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.HOCKEY
)
+ @override
def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1:
return 'Score a goal.'
return 'Score ${ARG1} goals.', self._score_to_win
+ @override
def get_instance_description_short(self) -> str | Sequence:
if self._score_to_win == 1:
return 'score a goal'
return 'score ${ARG1} goals', self._score_to_win
+ @override
def on_begin(self) -> None:
super().on_begin()
@@ -281,6 +289,7 @@ class HockeyGame(bs.TeamGameActivity[Player, Team]):
self._update_scoreboard()
self._chant_sound.play()
+ @override
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
@@ -364,6 +373,7 @@ class HockeyGame(bs.TeamGameActivity[Player, Team]):
bs.cameraflash(duration=10.0)
self._update_scoreboard()
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
@@ -375,6 +385,7 @@ class HockeyGame(bs.TeamGameActivity[Player, Team]):
for team in self.teams:
self._scoreboard.set_team_value(team, team.score, winscore)
+ @override
def handlemessage(self, msg: Any) -> Any:
# Respawn dead players if they're still in the game.
if isinstance(msg, bs.PlayerDiedMessage):
diff --git a/src/assets/ba_data/python/bascenev1lib/game/keepaway.py b/src/assets/ba_data/python/bascenev1lib/game/keepaway.py
index bc80b77d..416723e1 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/keepaway.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/keepaway.py
@@ -11,6 +11,9 @@ import logging
from enum import Enum
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.flag import (
@@ -19,7 +22,6 @@ from bascenev1lib.actor.flag import (
FlagDiedMessage,
FlagPickedUpMessage,
)
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -86,12 +88,14 @@ class KeepAwayGame(bs.TeamGameActivity[Player, Team]):
]
scoreconfig = bs.ScoreConfig(label='Time Held')
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return issubclass(sessiontype, bs.DualTeamSession) or issubclass(
sessiontype, bs.FreeForAllSession
)
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -129,18 +133,23 @@ class KeepAwayGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.KEEP_AWAY
)
+ @override
def get_instance_description(self) -> str | Sequence:
return 'Carry the flag for ${ARG1} seconds.', self._hold_time
+ @override
def get_instance_description_short(self) -> str | Sequence:
return 'carry the flag for ${ARG1} seconds', self._hold_time
+ @override
def create_team(self, sessionteam: bs.SessionTeam) -> Team:
return Team(timeremaining=self._hold_time)
+ @override
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
+ @override
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
@@ -181,6 +190,7 @@ class KeepAwayGame(bs.TeamGameActivity[Player, Team]):
if scoreteam.timeremaining <= 0:
self.end_game()
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
@@ -268,6 +278,7 @@ class KeepAwayGame(bs.TeamGameActivity[Player, Team]):
team, team.timeremaining, self._hold_time, countdown=True
)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
# Augment standard behavior.
diff --git a/src/assets/ba_data/python/bascenev1lib/game/kingofthehill.py b/src/assets/ba_data/python/bascenev1lib/game/kingofthehill.py
index 6492d823..907b4341 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/kingofthehill.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/kingofthehill.py
@@ -11,11 +11,13 @@ import weakref
from enum import Enum
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.flag import Flag
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.gameutils import SharedObjects
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -84,10 +86,12 @@ class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]):
]
scoreconfig = bs.ScoreConfig(label='Time Held')
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return issubclass(sessiontype, bs.MultiTeamSession)
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -144,15 +148,19 @@ class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SCARY
)
+ @override
def get_instance_description(self) -> str | Sequence:
return 'Secure the flag for ${ARG1} seconds.', self._hold_time
+ @override
def get_instance_description_short(self) -> str | Sequence:
return 'secure the flag for ${ARG1} seconds', self._hold_time
+ @override
def create_team(self, sessionteam: bs.SessionTeam) -> Team:
return Team(time_remaining=self._hold_time)
+ @override
def on_begin(self) -> None:
super().on_begin()
shared = SharedObjects.get()
@@ -223,6 +231,7 @@ class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]):
if scoring_team.time_remaining <= 0:
self.end_game()
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
@@ -283,6 +292,7 @@ class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]):
team, team.time_remaining, self._hold_time, countdown=True
)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
super().handlemessage(msg) # Augment default.
diff --git a/src/assets/ba_data/python/bascenev1lib/game/meteorshower.py b/src/assets/ba_data/python/bascenev1lib/game/meteorshower.py
index 35e0566d..a9a70ae0 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/meteorshower.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/meteorshower.py
@@ -10,9 +10,11 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.bomb import Bomb
from bascenev1lib.actor.onscreentimer import OnScreenTimer
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -49,11 +51,13 @@ class MeteorShowerGame(bs.TeamGameActivity[Player, Team]):
allow_mid_activity_joins = False
# We're currently hard-coded for one map.
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
return ['Rampage']
# We support teams, free-for-all, and co-op sessions.
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return (
@@ -77,6 +81,7 @@ class MeteorShowerGame(bs.TeamGameActivity[Player, Team]):
if self._epic_mode:
self.slow_motion = True
+ @override
def on_begin(self) -> None:
super().on_begin()
@@ -100,6 +105,7 @@ class MeteorShowerGame(bs.TeamGameActivity[Player, Team]):
# Check for immediate end (if we've only got 1 player, etc).
bs.timer(5.0, self._check_end_game)
+ @override
def on_player_leave(self, player: Player) -> None:
# Augment default behavior.
super().on_player_leave(player)
@@ -108,6 +114,7 @@ class MeteorShowerGame(bs.TeamGameActivity[Player, Team]):
self._check_end_game()
# overriding the default character spawning..
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
spaz = self.spawn_player_spaz(player)
@@ -122,6 +129,7 @@ class MeteorShowerGame(bs.TeamGameActivity[Player, Team]):
return spaz
# Various high-level game events come through this method.
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
# Augment standard behavior.
@@ -213,6 +221,7 @@ class MeteorShowerGame(bs.TeamGameActivity[Player, Team]):
def _decrement_meteor_time(self) -> None:
self._meteor_time = max(0.01, self._meteor_time * 0.9)
+ @override
def end_game(self) -> None:
cur_time = bs.time()
assert self._timer is not None
diff --git a/src/assets/ba_data/python/bascenev1lib/game/ninjafight.py b/src/assets/ba_data/python/bascenev1lib/game/ninjafight.py
index 501e920b..98ca8d0b 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/ninjafight.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/ninjafight.py
@@ -10,13 +10,15 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.spazbot import (
SpazBotSet,
ChargerBot,
SpazBotDiedMessage,
)
from bascenev1lib.actor.onscreentimer import OnScreenTimer
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any
@@ -44,6 +46,7 @@ class NinjaFightGame(bs.TeamGameActivity[Player, Team]):
)
default_music = bs.MusicType.TO_THE_DEATH
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
# For now we're hard-coding spawn positions and whatnot
@@ -51,6 +54,7 @@ class NinjaFightGame(bs.TeamGameActivity[Player, Team]):
# a specific map.
return ['Courtyard']
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
# We currently support Co-Op only.
@@ -67,6 +71,7 @@ class NinjaFightGame(bs.TeamGameActivity[Player, Team]):
self._preset = str(settings['preset'])
# Called when our game actually begins.
+ @override
def on_begin(self) -> None:
super().on_begin()
is_pro = self._preset == 'pro'
@@ -123,6 +128,7 @@ class NinjaFightGame(bs.TeamGameActivity[Player, Team]):
)
# Called for each spawning player.
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
# Let's spawn close to the center.
spawn_center = (0, 3, -2)
@@ -144,6 +150,7 @@ class NinjaFightGame(bs.TeamGameActivity[Player, Team]):
self.end_game()
# Called for miscellaneous messages.
+ @override
def handlemessage(self, msg: Any) -> Any:
# A player has died.
if isinstance(msg, bs.PlayerDiedMessage):
@@ -166,6 +173,7 @@ class NinjaFightGame(bs.TeamGameActivity[Player, Team]):
# When this is called, we should fill out results and end the game
# *regardless* of whether is has been won. (this may be called due
# to a tournament ending or other external reason).
+ @override
def end_game(self) -> None:
# Stop our on-screen timer so players can see what they got.
assert self._timer is not None
diff --git a/src/assets/ba_data/python/bascenev1lib/game/onslaught.py b/src/assets/ba_data/python/bascenev1lib/game/onslaught.py
index 2e8f98e4..d4644130 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/onslaught.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/onslaught.py
@@ -17,6 +17,9 @@ from enum import Enum, unique
from dataclasses import dataclass
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.popuptext import PopupText
from bascenev1lib.actor.bomb import TNTSpawner
from bascenev1lib.actor.playerspaz import PlayerSpazHurtMessage
@@ -45,7 +48,6 @@ from bascenev1lib.actor.spazbot import (
BrawlerBotPro,
BomberBotProShielded,
)
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -222,6 +224,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]):
self._land_mine_kills = 0
self._tnt_kills = 0
+ @override
def on_transition_in(self) -> None:
super().on_transition_in()
customdata = bs.getsession().customdata
@@ -286,6 +289,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]):
label=bs.Lstr(resource='scoreText'), score_split=0.5
)
+ @override
def on_begin(self) -> None:
super().on_begin()
player_count = len(self.players)
@@ -825,6 +829,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]):
break
entry_count += 1
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
# We keep track of who got hurt each wave for score purposes.
player.has_been_hurt = False
@@ -1414,6 +1419,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]):
assert self._scoreboard is not None
self._scoreboard.set_team_value(self.teams[0], score, max_score=None)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, PlayerSpazHurtMessage):
msg.spaz.getplayer(Player, True).has_been_hurt = True
@@ -1526,6 +1532,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]):
def _set_can_end_wave(self) -> None:
self._can_end_wave = True
+ @override
def end_game(self) -> None:
# Tell our bots to celebrate just to rub it in.
assert self._bots is not None
@@ -1534,6 +1541,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]):
self.do_end('defeat', delay=2.0)
bs.setmusic(None)
+ @override
def on_continue(self) -> None:
for player in self.players:
if not player.is_alive():
diff --git a/src/assets/ba_data/python/bascenev1lib/game/race.py b/src/assets/ba_data/python/bascenev1lib/game/race.py
index 2dd20f73..4a35fa4c 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/race.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/race.py
@@ -12,11 +12,13 @@ import logging
from typing import TYPE_CHECKING
from dataclasses import dataclass
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.bomb import Bomb
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.gameutils import SharedObjects
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -84,6 +86,7 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
label='Time', lower_is_better=True, scoretype=bs.ScoreType.MILLISECONDS
)
+ @override
@classmethod
def get_available_settings(
cls, sessiontype: type[bs.Session]
@@ -133,10 +136,12 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
)
return settings
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return issubclass(sessiontype, bs.MultiTeamSession)
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
assert bs.app.classic is not None
@@ -179,6 +184,7 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
bs.MusicType.EPIC_RACE if self._epic_mode else bs.MusicType.RACE
)
+ @override
def get_instance_description(self) -> str | Sequence:
if (
isinstance(self.session, bs.DualTeamSession)
@@ -192,11 +198,13 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
return 'Run ${ARG1} laps.' + t_str, self._laps
return 'Run 1 lap.' + t_str
+ @override
def get_instance_description_short(self) -> str | Sequence:
if self._laps > 1:
return 'run ${ARG1} laps', self._laps
return 'run 1 lap'
+ @override
def on_transition_in(self) -> None:
super().on_transition_in()
shared = SharedObjects.get()
@@ -379,9 +387,11 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
except Exception:
logging.exception('Error printing lap.')
+ @override
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
+ @override
def on_player_leave(self, player: Player) -> None:
super().on_player_leave(player)
@@ -442,6 +452,7 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
show_value=False,
)
+ @override
def on_begin(self) -> None:
from bascenev1lib.actor.onscreentimer import OnScreenTimer
@@ -670,6 +681,7 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
self._flash_mine(m_index)
bs.timer(0.95, bs.Call(self._make_mine, m_index))
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
if player.team.finished:
# FIXME: This is not type-safe!
@@ -758,6 +770,7 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
self.end_game()
return
+ @override
def end_game(self) -> None:
# Stop updating our time text, and set it to show the exact last
# finish time if we have one. (so users don't get upset if their
@@ -787,6 +800,7 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
announce_winning_team=isinstance(self.session, bs.DualTeamSession),
)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
# Augment default behavior.
diff --git a/src/assets/ba_data/python/bascenev1lib/game/runaround.py b/src/assets/ba_data/python/bascenev1lib/game/runaround.py
index 6640601f..b970d354 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/runaround.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/runaround.py
@@ -16,6 +16,9 @@ from enum import Enum
from dataclasses import dataclass
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.popuptext import PopupText
from bascenev1lib.actor.bomb import TNTSpawner
from bascenev1lib.actor.scoreboard import Scoreboard
@@ -40,7 +43,6 @@ from bascenev1lib.actor.spazbot import (
BomberBotPro,
BrawlerBotPro,
)
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -194,6 +196,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
self._flawless_bonus: int | None = None
self._wave_update_timer: bs.Timer | None = None
+ @override
def on_transition_in(self) -> None:
super().on_transition_in()
self._scoreboard = Scoreboard(
@@ -211,6 +214,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
)
)
+ @override
def on_begin(self) -> None:
super().on_begin()
player_count = len(self.players)
@@ -571,6 +575,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
),
)
+ @override
def on_continue(self) -> None:
self._lives = 3
assert self._lives_text is not None
@@ -578,6 +583,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
self._lives_text.node.text = str(self._lives)
self._bots.start_moving()
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
pos = (
self._spawn_center[0] + random.uniform(-1.5, 1.5),
@@ -654,6 +660,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
),
).autoretain()
+ @override
def end_game(self) -> None:
bs.pushcall(bs.Call(self.do_end, 'defeat'))
bs.setmusic(None)
@@ -1286,6 +1293,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
# Revert to normal bot behavior otherwise..
return False
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerScoredMessage):
self._score += msg.score
diff --git a/src/assets/ba_data/python/bascenev1lib/game/targetpractice.py b/src/assets/ba_data/python/bascenev1lib/game/targetpractice.py
index c39f9706..fdff48f2 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/targetpractice.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/targetpractice.py
@@ -10,11 +10,13 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.onscreencountdown import OnScreenCountdown
from bascenev1lib.actor.bomb import Bomb
from bascenev1lib.actor.popuptext import PopupText
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -49,10 +51,12 @@ class TargetPracticeGame(bs.TeamGameActivity[Player, Team]):
]
default_music = bs.MusicType.FORWARD_MARCH
+ @override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
return ['Doom Shroom']
+ @override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
# We support any teams or versus sessions.
@@ -70,10 +74,12 @@ class TargetPracticeGame(bs.TeamGameActivity[Player, Team]):
self._enable_impact_bombs = bool(settings['Enable Impact Bombs'])
self._enable_triple_bombs = bool(settings['Enable Triple Bombs'])
+ @override
def on_team_join(self, team: Team) -> None:
if self.has_begun():
self.update_scoreboard()
+ @override
def on_begin(self) -> None:
super().on_begin()
self.update_scoreboard()
@@ -86,6 +92,7 @@ class TargetPracticeGame(bs.TeamGameActivity[Player, Team]):
self._countdown = OnScreenCountdown(60, endcall=self.end_game)
bs.timer(4.0, self._countdown.start)
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
spawn_center = (0, 3, -5)
pos = (
@@ -169,6 +176,7 @@ class TargetPracticeGame(bs.TeamGameActivity[Player, Team]):
# Clear out targets that have died.
self._targets = [t for t in self._targets if t]
+ @override
def handlemessage(self, msg: Any) -> Any:
# When players die, respawn them.
if isinstance(msg, bs.PlayerDiedMessage):
@@ -188,6 +196,7 @@ class TargetPracticeGame(bs.TeamGameActivity[Player, Team]):
for team in self.teams:
self._scoreboard.set_team_value(team, team.score)
+ @override
def end_game(self) -> None:
results = bs.GameResults()
for team in self.teams:
@@ -252,9 +261,11 @@ class Target(bs.Actor):
bs.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]})
bs.getsound('laserReverse').play()
+ @override
def exists(self) -> bool:
return bool(self._nodes)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.DieMessage):
for node in self._nodes:
diff --git a/src/assets/ba_data/python/bascenev1lib/game/thelaststand.py b/src/assets/ba_data/python/bascenev1lib/game/thelaststand.py
index 5eb32fac..7752c3fa 100644
--- a/src/assets/ba_data/python/bascenev1lib/game/thelaststand.py
+++ b/src/assets/ba_data/python/bascenev1lib/game/thelaststand.py
@@ -9,6 +9,9 @@ import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING
+from typing_extensions import override
+import bascenev1 as bs
+
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.bomb import TNTSpawner
from bascenev1lib.actor.scoreboard import Scoreboard
@@ -29,7 +32,6 @@ from bascenev1lib.actor.spazbot import (
StickyBot,
ExplodeyBot,
)
-import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
@@ -109,6 +111,7 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]):
ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002),
}
+ @override
def on_transition_in(self) -> None:
super().on_transition_in()
bs.timer(1.3, self._new_wave_sound.play)
@@ -116,6 +119,7 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]):
label=bs.Lstr(resource='scoreText'), score_split=0.5
)
+ @override
def on_begin(self) -> None:
super().on_begin()
@@ -129,6 +133,7 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]):
position=self._tntspawnpos, respawn_time=10.0
)
+ @override
def spawn_player(self, player: Player) -> bs.Actor:
pos = (
self._spawn_center[0] + random.uniform(-1.5, 1.5),
@@ -290,6 +295,7 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]):
assert self._scoreboard is not None
self._scoreboard.set_team_value(self.teams[0], score, max_score=None)
+ @override
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
player = msg.getplayer(Player)
@@ -327,6 +333,7 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]):
else:
super().handlemessage(msg)
+ @override
def end_game(self) -> None:
# Tell our bots to celebrate just to rub it in.
self._bots.final_celebrate()
diff --git a/src/assets/ba_data/python/bascenev1lib/mainmenu.py b/src/assets/ba_data/python/bascenev1lib/mainmenu.py
index 7b7acfbf..8041176e 100644
--- a/src/assets/ba_data/python/bascenev1lib/mainmenu.py
+++ b/src/assets/ba_data/python/bascenev1lib/mainmenu.py
@@ -10,6 +10,7 @@ import random
import weakref
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
import bauiv1 as bui
@@ -44,6 +45,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
self._news: NewsDisplay | None = None
self._attract_mode_timer: bs.Timer | None = None
+ @override
def on_transition_in(self) -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
@@ -1139,6 +1141,7 @@ class MainMenuSession(bs.Session):
self._locked = False
self.setactivity(bs.newactivity(MainMenuActivity))
+ @override
def on_activity_end(self, activity: bs.Activity, results: Any) -> None:
if self._locked:
bui.unlock_all_input()
@@ -1146,6 +1149,7 @@ class MainMenuSession(bs.Session):
# Any ending activity leads us into the main menu one.
self.setactivity(bs.newactivity(MainMenuActivity))
+ @override
def on_player_request(self, player: bs.SessionPlayer) -> bool:
# Reject all player requests.
return False
diff --git a/src/assets/ba_data/python/bascenev1lib/maps.py b/src/assets/ba_data/python/bascenev1lib/maps.py
index 26349f56..d4731145 100644
--- a/src/assets/ba_data/python/bascenev1lib/maps.py
+++ b/src/assets/ba_data/python/bascenev1lib/maps.py
@@ -7,7 +7,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
+
from bascenev1lib.gameutils import SharedObjects
if TYPE_CHECKING:
@@ -22,15 +24,18 @@ class HockeyStadium(bs.Map):
name = 'Hockey Stadium'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'hockey', 'team_flag', 'keep_away']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'hockeyStadiumPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -114,15 +119,18 @@ class FootballStadium(bs.Map):
name = 'Football Stadium'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'football', 'team_flag', 'keep_away']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'footballStadiumPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -164,6 +172,7 @@ class FootballStadium(bs.Map):
gnode.vr_camera_offset = (0, -0.8, -1.1)
gnode.vr_near_clip = 0.5
+ @override
def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool:
box_position = self.defs.boxes['edge_box'][0:3]
box_scale = self.defs.boxes['edge_box'][6:9]
@@ -181,16 +190,19 @@ class Bridgit(bs.Map):
name = 'Bridgit'
dataname = 'bridgit'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
# print('getting playtypes', cls._getdata()['play_types'])
return ['melee', 'team_flag', 'keep_away']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'bridgitPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -286,6 +298,7 @@ class BigG(bs.Map):
name = 'Big G'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
@@ -298,10 +311,12 @@ class BigG(bs.Map):
'conquest',
]
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'bigGPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -397,15 +412,18 @@ class Roundabout(bs.Map):
name = 'Roundabout'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'keep_away', 'team_flag']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'roundaboutPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -502,15 +520,18 @@ class MonkeyFace(bs.Map):
name = 'Monkey Face'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'keep_away', 'team_flag']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'monkeyFacePreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -607,6 +628,7 @@ class ZigZag(bs.Map):
name = 'Zigzag'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
@@ -618,10 +640,12 @@ class ZigZag(bs.Map):
'king_of_the_hill',
]
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'zigzagPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -715,15 +739,18 @@ class ThePad(bs.Map):
name = 'The Pad'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'keep_away', 'team_flag', 'king_of_the_hill']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'thePadPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -804,15 +831,18 @@ class DoomShroom(bs.Map):
name = 'Doom Shroom'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'keep_away', 'team_flag']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'doomShroomPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -881,6 +911,7 @@ class DoomShroom(bs.Map):
gnode.vignette_outer = (0.76, 0.76, 0.76)
gnode.vignette_inner = (0.95, 0.95, 0.99)
+ @override
def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool:
xpos = point.x
zpos = point.z
@@ -900,15 +931,18 @@ class LakeFrigid(bs.Map):
name = 'Lake Frigid'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'keep_away', 'team_flag', 'race']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'lakeFrigidPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -987,15 +1021,18 @@ class TipTop(bs.Map):
name = 'Tip Top'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'keep_away', 'team_flag', 'king_of_the_hill']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'tipTopPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -1065,15 +1102,18 @@ class CragCastle(bs.Map):
name = 'Crag Castle'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'keep_away', 'team_flag', 'conquest']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'cragCastlePreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -1158,15 +1198,18 @@ class TowerD(bs.Map):
name = 'Tower D'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return []
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'towerDPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -1256,6 +1299,7 @@ class TowerD(bs.Map):
gnode.vignette_outer = (0.7, 0.73, 0.7)
gnode.vignette_inner = (0.95, 0.95, 0.95)
+ @override
def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool:
# see if we're within edge_box
boxes = self.defs.boxes
@@ -1281,6 +1325,7 @@ class HappyThoughts(bs.Map):
name = 'Happy Thoughts'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
@@ -1292,10 +1337,12 @@ class HappyThoughts(bs.Map):
'king_of_the_hill',
]
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'alwaysLandPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -1310,6 +1357,7 @@ class HappyThoughts(bs.Map):
}
return data
+ @override
@classmethod
def get_music_type(cls) -> bs.MusicType:
return bs.MusicType.FLYING
@@ -1397,15 +1445,18 @@ class StepRightUp(bs.Map):
name = 'Step Right Up'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'keep_away', 'team_flag', 'conquest']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'stepRightUpPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -1477,15 +1528,18 @@ class Courtyard(bs.Map):
name = 'Courtyard'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'keep_away', 'team_flag']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'courtyardPreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -1576,6 +1630,7 @@ class Courtyard(bs.Map):
gnode.vignette_outer = (0.6, 0.6, 0.64)
gnode.vignette_inner = (0.95, 0.95, 0.93)
+ @override
def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool:
# count anything off our ground level as safe (for our platforms)
# see if we're within edge_box
@@ -1593,15 +1648,18 @@ class Rampage(bs.Map):
name = 'Rampage'
+ @override
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['melee', 'keep_away', 'team_flag']
+ @override
@classmethod
def get_preview_texture_name(cls) -> str:
return 'rampagePreview'
+ @override
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
@@ -1681,6 +1739,7 @@ class Rampage(bs.Map):
gnode.vignette_outer = (0.62, 0.64, 0.69)
gnode.vignette_inner = (0.97, 0.95, 0.93)
+ @override
def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool:
box_position = self.defs.boxes['edge_box'][0:3]
box_scale = self.defs.boxes['edge_box'][6:9]
diff --git a/src/assets/ba_data/python/bascenev1lib/tutorial.py b/src/assets/ba_data/python/bascenev1lib/tutorial.py
index e51c4a8d..86aa6411 100644
--- a/src/assets/ba_data/python/bascenev1lib/tutorial.py
+++ b/src/assets/ba_data/python/bascenev1lib/tutorial.py
@@ -19,6 +19,7 @@ import logging
from collections import deque
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
from bascenev1lib.actor.spaz import Spaz
@@ -234,11 +235,13 @@ class TutorialActivity(bs.Activity[Player, Team]):
self._read_entries_timer: bs.Timer | None = None
self._entry_timer: bs.Timer | None = None
+ @override
def on_transition_in(self) -> None:
super().on_transition_in()
bs.setmusic(bs.MusicType.CHAR_SELECT, continuous=True)
self.map = self._map_type()
+ @override
def on_begin(self) -> None:
super().on_begin()
@@ -2513,6 +2516,7 @@ class TutorialActivity(bs.Activity[Player, Team]):
self._skip_text.color = (1, 1, 1)
self._issued_warning = False
+ @override
def on_player_join(self, player: Player) -> None:
super().on_player_join(player)
@@ -2527,6 +2531,7 @@ class TutorialActivity(bs.Activity[Player, Team]):
bs.Call(self._player_pressed_button, player),
)
+ @override
def on_player_leave(self, player: Player) -> None:
if not all(self.players):
logging.error(
diff --git a/src/assets/ba_data/python/bauiv1/_subsystem.py b/src/assets/ba_data/python/bauiv1/_subsystem.py
index 3c6b1e77..34d7f5af 100644
--- a/src/assets/ba_data/python/bauiv1/_subsystem.py
+++ b/src/assets/ba_data/python/bauiv1/_subsystem.py
@@ -8,7 +8,9 @@ import logging
import inspect
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
+
import _bauiv1
if TYPE_CHECKING:
@@ -82,6 +84,7 @@ class UIV1Subsystem(babase.AppSubsystem):
"""Current ui scale for the app."""
return self._uiscale
+ @override
def on_app_loading(self) -> None:
from bauiv1._uitypes import UIController, ui_upkeep
diff --git a/src/assets/ba_data/python/bauiv1/_uitypes.py b/src/assets/ba_data/python/bauiv1/_uitypes.py
index 5cbd14e2..2f93f227 100644
--- a/src/assets/ba_data/python/bauiv1/_uitypes.py
+++ b/src/assets/ba_data/python/bauiv1/_uitypes.py
@@ -9,6 +9,7 @@ import weakref
from dataclasses import dataclass
from typing import TYPE_CHECKING
+from typing_extensions import override
import babase
import _bauiv1
@@ -264,12 +265,14 @@ class TextWidgetStringEditAdapter(babase.StringEditAdapter):
description, initial_text, max_length, screen_space_center
)
+ @override
def _do_apply(self, new_text: str) -> None:
if self.widget:
_bauiv1.textwidget(
edit=self.widget, text=new_text, adapter_finished=True
)
+ @override
def _do_cancel(self) -> None:
if self.widget:
_bauiv1.textwidget(edit=self.widget, adapter_finished=True)
diff --git a/src/assets/ba_data/python/bauiv1lib/account/viewer.py b/src/assets/ba_data/python/bauiv1lib/account/viewer.py
index 914bd930..9046a805 100644
--- a/src/assets/ba_data/python/bauiv1lib/account/viewer.py
+++ b/src/assets/ba_data/python/bauiv1lib/account/viewer.py
@@ -7,9 +7,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import logging
-from bauiv1lib.popup import PopupWindow, PopupMenuWindow
+from typing_extensions import override
import bauiv1 as bui
+from bauiv1lib.popup import PopupWindow, PopupMenuWindow
+
if TYPE_CHECKING:
from typing import Any
@@ -596,6 +598,7 @@ class AccountViewerWindow(PopupWindow):
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/ba_data/python/bauiv1lib/achievements.py b/src/assets/ba_data/python/bauiv1lib/achievements.py
index 1c8cc673..df2345d5 100644
--- a/src/assets/ba_data/python/bauiv1lib/achievements.py
+++ b/src/assets/ba_data/python/bauiv1lib/achievements.py
@@ -4,6 +4,8 @@
from __future__ import annotations
+from typing_extensions import override
+
from bauiv1lib.popup import PopupWindow
import bauiv1 as bui
@@ -229,6 +231,7 @@ class AchievementsWindow(PopupWindow):
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/ba_data/python/bauiv1lib/characterpicker.py b/src/assets/ba_data/python/bauiv1lib/characterpicker.py
index b4bdd635..92194e2b 100644
--- a/src/assets/ba_data/python/bauiv1lib/characterpicker.py
+++ b/src/assets/ba_data/python/bauiv1lib/characterpicker.py
@@ -7,6 +7,8 @@ from __future__ import annotations
import math
from typing import TYPE_CHECKING
+from typing_extensions import override
+
from bauiv1lib.popup import PopupWindow
import bauiv1 as bui
@@ -208,6 +210,7 @@ class CharacterPicker(PopupWindow):
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/ba_data/python/bauiv1lib/colorpicker.py b/src/assets/ba_data/python/bauiv1lib/colorpicker.py
index 904c9e33..30887432 100644
--- a/src/assets/ba_data/python/bauiv1lib/colorpicker.py
+++ b/src/assets/ba_data/python/bauiv1lib/colorpicker.py
@@ -6,6 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
+
from bauiv1lib.popup import PopupWindow
import bauiv1 as bui
@@ -170,6 +172,7 @@ class ColorPicker(PopupWindow):
self._delegate.color_picker_closing(self)
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
if not self._transitioning_out:
bui.getsound('swish').play()
@@ -338,6 +341,7 @@ class ColorPickerExact(PopupWindow):
self._delegate.color_picker_closing(self)
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
if not self._transitioning_out:
bui.getsound('swish').play()
diff --git a/src/assets/ba_data/python/bauiv1lib/fileselector.py b/src/assets/ba_data/python/bauiv1lib/fileselector.py
index 17263119..e9cf0ca6 100644
--- a/src/assets/ba_data/python/bauiv1lib/fileselector.py
+++ b/src/assets/ba_data/python/bauiv1lib/fileselector.py
@@ -10,6 +10,8 @@ import logging
from threading import Thread
from typing import TYPE_CHECKING
+from typing_extensions import override
+
import bauiv1 as bui
if TYPE_CHECKING:
@@ -204,6 +206,7 @@ class FileSelectorWindow(bui.Window):
self._callback = callback
self._path = path
+ @override
def run(self) -> None:
try:
starttime = time.time()
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py b/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py
index b61ed339..1257c4bd 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/abouttab.py
@@ -6,6 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
+
from bauiv1lib.gather import GatherTab
import bauiv1 as bui
@@ -16,6 +18,7 @@ if TYPE_CHECKING:
class AboutGatherTab(GatherTab):
"""The about tab in the gather UI"""
+ @override
def on_activate(
self,
parent_widget: bui.Widget,
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py b/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py
index e09968b1..d7f3205e 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/manualtab.py
@@ -6,13 +6,13 @@
from __future__ import annotations
import logging
-from threading import Thread
-from typing import TYPE_CHECKING, cast
-
from enum import Enum
+from threading import Thread
from dataclasses import dataclass
+from typing import TYPE_CHECKING, cast
from bauiv1lib.gather import GatherTab
+from typing_extensions import override
import bauiv1 as bui
import bascenev1 as bs
@@ -42,6 +42,7 @@ class _HostLookupThread(Thread):
self._port = port
self._call = call
+ @override
def run(self) -> None:
result: str | None
try:
@@ -101,6 +102,7 @@ class ManualGatherTab(GatherTab):
self._party_edit_port_text: bui.Widget | None = None
self._no_parties_added_text: bui.Widget | None = None
+ @override
def on_activate(
self,
parent_widget: bui.Widget,
@@ -180,10 +182,12 @@ class ManualGatherTab(GatherTab):
return self._container
+ @override
def save_state(self) -> None:
assert bui.app.classic is not None
bui.app.ui_v1.window_states[type(self)] = State(sub_tab=self._sub_tab)
+ @override
def restore_state(self) -> None:
assert bui.app.classic is not None
state = bui.app.ui_v1.window_states.get(type(self))
@@ -771,6 +775,7 @@ class ManualGatherTab(GatherTab):
text=bui.Lstr(resource='gatherWindow.noPartiesAddedText'),
)
+ @override
def on_deactivate(self) -> None:
self._access_check_timer = None
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/nearbytab.py b/src/assets/ba_data/python/bauiv1lib/gather/nearbytab.py
index 146fcc7a..7393f6bd 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/nearbytab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/nearbytab.py
@@ -7,10 +7,12 @@ from __future__ import annotations
import weakref
from typing import TYPE_CHECKING
-from bauiv1lib.gather import GatherTab
+from typing_extensions import override
import bauiv1 as bui
import bascenev1 as bs
+from bauiv1lib.gather import GatherTab
+
if TYPE_CHECKING:
from typing import Any
@@ -104,6 +106,7 @@ class NearbyGatherTab(GatherTab):
self._net_scanner: NetScanner | None = None
self._container: bui.Widget | None = None
+ @override
def on_activate(
self,
parent_widget: bui.Widget,
@@ -156,5 +159,6 @@ class NearbyGatherTab(GatherTab):
bui.widget(edit=scrollw, autoselect=True, up_widget=tab_button)
return self._container
+ @override
def on_deactivate(self) -> None:
self._net_scanner = None
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py
index e66cd6c1..8b6cf7fb 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/privatetab.py
@@ -13,6 +13,7 @@ from enum import Enum
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
+from typing_extensions import override
from efro.dataclassio import dataclass_from_dict, dataclass_to_dict
from bacommon.net import (
PrivateHostingState,
@@ -81,6 +82,7 @@ class PrivateGatherTab(GatherTab):
logging.exception('Error building hosting config.')
self._hostingconfig = PrivateHostingConfig()
+ @override
def on_activate(
self,
parent_widget: bui.Widget,
@@ -253,6 +255,7 @@ class PrivateGatherTab(GatherTab):
return hcfg
+ @override
def on_deactivate(self) -> None:
self._update_timer = None
@@ -995,10 +998,12 @@ class PrivateGatherTab(GatherTab):
self._debug_server_comm('got connect response error')
bui.getsound('error').play()
+ @override
def save_state(self) -> None:
assert bui.app.classic is not None
bui.app.ui_v1.window_states[type(self)] = copy.deepcopy(self._state)
+ @override
def restore_state(self) -> None:
assert bui.app.classic is not None
state = bui.app.ui_v1.window_states.get(type(self))
diff --git a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py
index e1619436..83b3b534 100644
--- a/src/assets/ba_data/python/bauiv1lib/gather/publictab.py
+++ b/src/assets/ba_data/python/bauiv1lib/gather/publictab.py
@@ -13,6 +13,7 @@ from enum import Enum
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
+from typing_extensions import override
from bauiv1lib.gather import GatherTab
import bauiv1 as bui
import bascenev1 as bs
@@ -247,6 +248,7 @@ class AddrFetchThread(Thread):
super().__init__()
self._call = call
+ @override
def run(self) -> None:
sock: socket.socket | None = None
try:
@@ -284,6 +286,7 @@ class PingThread(Thread):
self._port = port
self._call = call
+ @override
def run(self) -> None:
assert bui.app.classic is not None
bui.app.classic.ping_thread_count += 1
@@ -392,6 +395,7 @@ class PublicGatherTab(GatherTab):
self._pending_party_infos: list[dict[str, Any]] = []
self._last_sub_scroll_height = 0.0
+ @override
def on_activate(
self,
parent_widget: bui.Widget,
@@ -478,9 +482,11 @@ class PublicGatherTab(GatherTab):
)
return self._container
+ @override
def on_deactivate(self) -> None:
self._update_timer = None
+ @override
def save_state(self) -> None:
# Save off a small number of parties with the lowest ping; we'll
# display these immediately when our UI comes back up which should
@@ -496,6 +502,7 @@ class PublicGatherTab(GatherTab):
have_valid_server_list=self._have_valid_server_list,
)
+ @override
def restore_state(self) -> None:
assert bui.app.classic is not None
state = bui.app.ui_v1.window_states.get(type(self))
diff --git a/src/assets/ba_data/python/bauiv1lib/getremote.py b/src/assets/ba_data/python/bauiv1lib/getremote.py
index 194725f7..0cea25ae 100644
--- a/src/assets/ba_data/python/bauiv1lib/getremote.py
+++ b/src/assets/ba_data/python/bauiv1lib/getremote.py
@@ -4,6 +4,8 @@
from __future__ import annotations
+from typing_extensions import override
+
from bauiv1lib.popup import PopupWindow
import bauiv1 as bui
@@ -77,6 +79,7 @@ class GetBSRemoteWindow(PopupWindow):
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/ba_data/python/bauiv1lib/iconpicker.py b/src/assets/ba_data/python/bauiv1lib/iconpicker.py
index e3cace22..260bbdec 100644
--- a/src/assets/ba_data/python/bauiv1lib/iconpicker.py
+++ b/src/assets/ba_data/python/bauiv1lib/iconpicker.py
@@ -7,6 +7,8 @@ from __future__ import annotations
import math
from typing import TYPE_CHECKING
+from typing_extensions import override
+
from bauiv1lib.popup import PopupWindow
import bauiv1 as bui
@@ -186,6 +188,7 @@ class IconPicker(PopupWindow):
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/share.py b/src/assets/ba_data/python/bauiv1lib/playlist/share.py
index d52d1e7f..480aba52 100644
--- a/src/assets/ba_data/python/bauiv1lib/playlist/share.py
+++ b/src/assets/ba_data/python/bauiv1lib/playlist/share.py
@@ -7,6 +7,8 @@ from __future__ import annotations
import time
from typing import TYPE_CHECKING
+from typing_extensions import override
+
from bauiv1lib.promocode import PromoCodeWindow
import bauiv1 as bui
@@ -55,6 +57,7 @@ class SharePlaylistImportWindow(PromoCodeWindow):
edit=self._root_widget, transition=self._transition_out
)
+ @override
def _do_enter(self) -> None:
plus = bui.app.plus
assert plus is not None
diff --git a/src/assets/ba_data/python/bauiv1lib/playoptions.py b/src/assets/ba_data/python/bauiv1lib/playoptions.py
index 88935bf3..637b50c1 100644
--- a/src/assets/ba_data/python/bauiv1lib/playoptions.py
+++ b/src/assets/ba_data/python/bauiv1lib/playoptions.py
@@ -7,6 +7,7 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
import bascenev1 as bs
import bauiv1 as bui
@@ -465,6 +466,7 @@ class PlayOptionsWindow(PopupWindow):
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition=transition)
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/ba_data/python/bauiv1lib/popup.py b/src/assets/ba_data/python/bauiv1lib/popup.py
index 34c591ca..b53cfc93 100644
--- a/src/assets/ba_data/python/bauiv1lib/popup.py
+++ b/src/assets/ba_data/python/bauiv1lib/popup.py
@@ -7,6 +7,8 @@ from __future__ import annotations
import weakref
from typing import TYPE_CHECKING
+from typing_extensions import override
+
import bauiv1 as bui
if TYPE_CHECKING:
@@ -275,6 +277,7 @@ class PopupMenuWindow(PopupWindow):
delegate.popup_menu_closing(self)
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
if not self._transitioning_out:
bui.getsound('swish').play()
diff --git a/src/assets/ba_data/python/bauiv1lib/promocode.py b/src/assets/ba_data/python/bauiv1lib/promocode.py
index 3cf745b5..09bcf86a 100644
--- a/src/assets/ba_data/python/bauiv1lib/promocode.py
+++ b/src/assets/ba_data/python/bauiv1lib/promocode.py
@@ -5,9 +5,14 @@
from __future__ import annotations
import time
+import logging
+from typing import TYPE_CHECKING
import bauiv1 as bui
+if TYPE_CHECKING:
+ from typing import Any
+
class PromoCodeWindow(bui.Window):
"""Window for entering promo codes."""
@@ -167,9 +172,6 @@ class PromoCodeWindow(bui.Window):
if not self._root_widget or self._root_widget.transitioning_out:
return
- plus = bui.app.plus
- assert plus is not None
-
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
@@ -179,11 +181,43 @@ class PromoCodeWindow(bui.Window):
AdvancedSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
+
+ code: Any = bui.textwidget(query=self._text_field)
+ assert isinstance(code, str)
+
+ bui.app.create_async_task(_run_code(code))
+
+
+async def _run_code(code: str) -> None:
+ from bacommon.cloud import PromoCodeMessage
+
+ plus = bui.app.plus
+ assert plus is not None
+
+ try:
+ # If we're signed in with a V2 account, ship this to V2 server.
+ if plus.accounts.primary is not None:
+ with plus.accounts.primary:
+ response = await plus.cloud.send_message_async(
+ PromoCodeMessage(code)
+ )
+ # If V2 handled it, we're done.
+ if response.valid:
+ # Support simple message printing from v2 server.
+ if response.message is not None:
+ bui.screenmessage(response.message, color=(0, 1, 0))
+ return
+
+ # If V2 didn't accept it (or isn't signed in) kick it over to V1.
plus.add_v1_account_transaction(
{
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
- 'code': bui.textwidget(query=self._text_field),
+ 'code': code,
}
)
plus.run_v1_account_transactions()
+ except Exception:
+ logging.exception('Error sending promo code.')
+ bui.screenmessage('Error sending code (see log).', color=(1, 0, 0))
+ bui.getsound('error').play()
diff --git a/src/assets/ba_data/python/bauiv1lib/qrcode.py b/src/assets/ba_data/python/bauiv1lib/qrcode.py
index 3d92eaf8..467bab7f 100644
--- a/src/assets/ba_data/python/bauiv1lib/qrcode.py
+++ b/src/assets/ba_data/python/bauiv1lib/qrcode.py
@@ -3,9 +3,11 @@
"""Provides functionality for displaying QR codes."""
from __future__ import annotations
-from bauiv1lib.popup import PopupWindow
+from typing_extensions import override
import bauiv1 as bui
+from bauiv1lib.popup import PopupWindow
+
class QRCodeWindow(PopupWindow):
"""Popup window that shows a QR code."""
@@ -58,6 +60,7 @@ class QRCodeWindow(PopupWindow):
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py b/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py
index c40b708d..6c3fc587 100644
--- a/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py
+++ b/src/assets/ba_data/python/bauiv1lib/resourcetypeinfo.py
@@ -4,6 +4,8 @@
from __future__ import annotations
+from typing_extensions import override
+
from bauiv1lib.popup import PopupWindow
import bauiv1 as bui
@@ -53,6 +55,7 @@ class ResourceTypeInfoWindow(PopupWindow):
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/ba_data/python/bauiv1lib/teamnamescolors.py b/src/assets/ba_data/python/bauiv1lib/teamnamescolors.py
index 2334e09b..3fe95459 100644
--- a/src/assets/ba_data/python/bauiv1lib/teamnamescolors.py
+++ b/src/assets/ba_data/python/bauiv1lib/teamnamescolors.py
@@ -6,6 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING, cast
+from typing_extensions import override
+
from bauiv1lib.popup import PopupWindow
from bauiv1lib.colorpicker import ColorPicker
import bauiv1 as bui
@@ -217,6 +219,7 @@ class TeamNamesColorsWindow(PopupWindow):
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition=transition)
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/ba_data/python/bauiv1lib/tournamententry.py b/src/assets/ba_data/python/bauiv1lib/tournamententry.py
index d00c37dd..90996bc0 100644
--- a/src/assets/ba_data/python/bauiv1lib/tournamententry.py
+++ b/src/assets/ba_data/python/bauiv1lib/tournamententry.py
@@ -7,6 +7,8 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING
+from typing_extensions import override
+
from bauiv1lib.popup import PopupWindow
import bauiv1 as bui
@@ -749,6 +751,7 @@ class TournamentEntryWindow(PopupWindow):
if self._on_close_call is not None:
self._on_close_call()
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._on_cancel()
diff --git a/src/assets/ba_data/python/bauiv1lib/tournamentscores.py b/src/assets/ba_data/python/bauiv1lib/tournamentscores.py
index 9d756318..ebd44de7 100644
--- a/src/assets/ba_data/python/bauiv1lib/tournamentscores.py
+++ b/src/assets/ba_data/python/bauiv1lib/tournamentscores.py
@@ -6,6 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
+
from bauiv1lib.popup import PopupWindow
import bauiv1 as bui
@@ -244,6 +246,7 @@ class TournamentScoresWindow(PopupWindow):
if self._on_close_call is not None:
self._on_close_call()
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/ba_data/python/bauiv1lib/trophies.py b/src/assets/ba_data/python/bauiv1lib/trophies.py
index b746d05c..e605ad7c 100644
--- a/src/assets/ba_data/python/bauiv1lib/trophies.py
+++ b/src/assets/ba_data/python/bauiv1lib/trophies.py
@@ -6,6 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
+from typing_extensions import override
+
from bauiv1lib import popup
import bauiv1 as bui
@@ -213,6 +215,7 @@ class TrophiesWindow(popup.PopupWindow):
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition='out_scale')
+ @override
def on_popup_cancel(self) -> None:
bui.getsound('swish').play()
self._transition_out()
diff --git a/src/assets/server_package/README.txt b/src/assets/server_package/README.txt
index 407c003c..fe0c8d35 100644
--- a/src/assets/server_package/README.txt
+++ b/src/assets/server_package/README.txt
@@ -14,7 +14,7 @@ Mac:
(brew install python3).
Linux (x86_64):
-- Server binaries are currently compiled against Ubuntu 20 LTS.
+- Server binaries are currently compiled against Ubuntu 22 LTS.
Raspberry Pi:
- The server binary was compiled on a Raspberry Pi 4 running Raspbian Buster.
diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc
index 4badfe1f..4e3a9e8d 100644
--- a/src/ballistica/shared/ballistica.cc
+++ b/src/ballistica/shared/ballistica.cc
@@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
namespace ballistica {
// These are set automatically via script; don't modify them here.
-const int kEngineBuildNumber = 21757;
+const int kEngineBuildNumber = 21766;
const char* kEngineVersion = "1.7.33";
const int kEngineApiVersion = 8;
diff --git a/tests/test_efro/test_dataclassio.py b/tests/test_efro/test_dataclassio.py
index 28e53eb8..125613e3 100644
--- a/tests/test_efro/test_dataclassio.py
+++ b/tests/test_efro/test_dataclassio.py
@@ -5,11 +5,20 @@
from __future__ import annotations
-from enum import Enum
+import copy
import datetime
+from enum import Enum
from dataclasses import field, dataclass
-from typing import TYPE_CHECKING, Any, Sequence, Annotated
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Sequence,
+ Annotated,
+ assert_type,
+ assert_never,
+)
+from typing_extensions import override
import pytest
from efro.util import utc_now
@@ -23,10 +32,11 @@ from efro.dataclassio import (
Codec,
DataclassFieldLookup,
IOExtendedData,
+ IOMultiType,
)
if TYPE_CHECKING:
- pass
+ from typing import Self
class _EnumTest(Enum):
@@ -855,10 +865,12 @@ def test_extended_data() -> None:
class _TestClass2(IOExtendedData):
vals: tuple[int, int]
+ @override
@classmethod
def will_input(cls, data: dict) -> None:
data['vals'] = data['vals'][:2]
+ @override
def will_output(self) -> None:
self.vals = (0, 0)
@@ -1066,3 +1078,221 @@ def test_soft_default() -> None:
todict = dataclass_to_dict(orig)
assert todict == {'ival': 2}
assert dataclass_from_dict(_TestClassE8, todict) == orig
+
+
+class MTTestTypeID(Enum):
+ """IDs for our multi-type class."""
+
+ CLASS_1 = 'm1'
+ CLASS_2 = 'm2'
+
+
+class MTTestBase(IOMultiType[MTTestTypeID]):
+ """Our multi-type class.
+
+ These top level multi-type classes are special parent classes
+ that know about all of their child classes and how to serialize
+ & deserialize them using explicit type ids. We can then use the
+ parent class in annotations and dataclassio will do the right thing.
+ Useful for stuff like Message classes where we may want to store a
+ bunch of different types of them into one place.
+ """
+
+ @override
+ @classmethod
+ def get_type(cls, type_id: MTTestTypeID) -> type[MTTestBase]:
+ """Return the subclass for each of our type-ids."""
+
+ # This uses assert_never() to ensure we cover all cases in the
+ # enum. Though this is less efficient than looking up by dict
+ # would be. If we had lots of values we could also support lazy
+ # loading by importing classes only when their value is being
+ # requested.
+ val: type[MTTestBase]
+ if type_id is MTTestTypeID.CLASS_1:
+ val = MTTestClass1
+ elif type_id is MTTestTypeID.CLASS_2:
+ val = MTTestClass2
+ else:
+ assert_never(type_id)
+ return val
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> MTTestTypeID:
+ """Provide the type-id for this subclass."""
+ # If we wanted, we could just maintain a static mapping
+ # of types-to-ids here, but there are benefits to letting
+ # each child class speak for itself. Namely that we can
+ # do lazy-loading and don't need to have all types present
+ # here.
+
+ # So we'll let all our child classes override this.
+ raise NotImplementedError()
+
+
+@ioprepped
+@dataclass(frozen=True) # Frozen so we can test in set()
+class MTTestClass1(MTTestBase):
+ """A test child-class for use with our multi-type class."""
+
+ ival: int
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> MTTestTypeID:
+ return MTTestTypeID.CLASS_1
+
+
+@ioprepped
+@dataclass(frozen=True) # Frozen so we can test in set()
+class MTTestClass2(MTTestBase):
+ """Another test child-class for use with our multi-type class."""
+
+ sval: str
+
+ @override
+ @classmethod
+ def get_type_id(cls) -> MTTestTypeID:
+ return MTTestTypeID.CLASS_2
+
+
+def test_multi_type() -> None:
+ """Test IOMultiType stuff."""
+ # pylint: disable=too-many-locals
+ # pylint: disable=too-many-statements
+
+ # Test converting single instances back and forth.
+ val1: MTTestBase = MTTestClass1(ival=123)
+ tpname = MTTestBase.ID_STORAGE_NAME
+ outdict = dataclass_to_dict(val1)
+ assert outdict == {'ival': 123, tpname: 'm1'}
+ val2: MTTestBase = MTTestClass2(sval='whee')
+ outdict2 = dataclass_to_dict(val2)
+ assert outdict2 == {'sval': 'whee', tpname: 'm2'}
+
+ # Make sure types and values work for both concrete types and the
+ # multi-type.
+ assert_type(dataclass_from_dict(MTTestClass1, outdict), MTTestClass1)
+ assert_type(dataclass_from_dict(MTTestBase, outdict), MTTestBase)
+
+ assert dataclass_from_dict(MTTestClass1, outdict) == val1
+ assert dataclass_from_dict(MTTestClass2, outdict2) == val2
+ assert dataclass_from_dict(MTTestBase, outdict) == val1
+ assert dataclass_from_dict(MTTestBase, outdict2) == val2
+
+ # Trying to load as a multi-type should fail if there is no type
+ # value present.
+ outdictmod = copy.deepcopy(outdict)
+ del outdictmod[tpname]
+ with pytest.raises(ValueError):
+ dataclass_from_dict(MTTestBase, outdictmod)
+
+ # However it should work when loading an exact type. This can be
+ # necessary to gracefully upgrade old data to multi-type form.
+ dataclass_from_dict(MTTestClass1, outdictmod)
+
+ # Now test our multi-type embedded in other classes. We should be
+ # able to throw a mix of things in there and have them deserialize
+ # back the types we started with.
+
+ # Individual values:
+
+ @ioprepped
+ @dataclass
+ class _TestContainerClass1:
+ obj_a: MTTestBase
+ obj_b: MTTestBase
+
+ container1 = _TestContainerClass1(
+ obj_a=MTTestClass1(234), obj_b=MTTestClass2('987')
+ )
+ outdict = dataclass_to_dict(container1)
+ container1b = dataclass_from_dict(_TestContainerClass1, outdict)
+ assert container1 == container1b
+
+ # Lists:
+
+ @ioprepped
+ @dataclass
+ class _TestContainerClass2:
+ objs: list[MTTestBase]
+
+ container2 = _TestContainerClass2(
+ objs=[MTTestClass1(111), MTTestClass2('bbb')]
+ )
+ outdict = dataclass_to_dict(container2)
+ container2b = dataclass_from_dict(_TestContainerClass2, outdict)
+ assert container2 == container2b
+
+ # Dict values:
+
+ @ioprepped
+ @dataclass
+ class _TestContainerClass3:
+ objs: dict[int, MTTestBase]
+
+ container3 = _TestContainerClass3(
+ objs={1: MTTestClass1(456), 2: MTTestClass2('gronk')}
+ )
+ outdict = dataclass_to_dict(container3)
+ container3b = dataclass_from_dict(_TestContainerClass3, outdict)
+ assert container3 == container3b
+
+ # Tuples:
+
+ @ioprepped
+ @dataclass
+ class _TestContainerClass4:
+ objs: tuple[MTTestBase, MTTestBase]
+
+ container4 = _TestContainerClass4(
+ objs=(MTTestClass1(932), MTTestClass2('potato'))
+ )
+ outdict = dataclass_to_dict(container4)
+ container4b = dataclass_from_dict(_TestContainerClass4, outdict)
+ assert container4 == container4b
+
+ # Sets (note: dataclasses must be frozen for this to work):
+
+ @ioprepped
+ @dataclass
+ class _TestContainerClass5:
+ objs: set[MTTestBase]
+
+ container5 = _TestContainerClass5(
+ objs={MTTestClass1(424), MTTestClass2('goo')}
+ )
+ outdict = dataclass_to_dict(container5)
+ container5b = dataclass_from_dict(_TestContainerClass5, outdict)
+ assert container5 == container5b
+
+ # Optionals.
+
+ @ioprepped
+ @dataclass
+ class _TestContainerClass6:
+ obj: MTTestBase | None
+
+ container6 = _TestContainerClass6(obj=None)
+ outdict = dataclass_to_dict(container6)
+ container6b = dataclass_from_dict(_TestContainerClass6, outdict)
+ assert container6 == container6b
+
+ container6 = _TestContainerClass6(obj=MTTestClass2('fwr'))
+ outdict = dataclass_to_dict(container6)
+ container6b = dataclass_from_dict(_TestContainerClass6, outdict)
+ assert container6 == container6b
+
+ @ioprepped
+ @dataclass
+ class _TestContainerClass7:
+ obj: Annotated[
+ MTTestBase | None,
+ IOAttrs('o', soft_default=None),
+ ]
+
+ container7 = _TestContainerClass7(obj=None)
+ outdict = dataclass_to_dict(container7)
+ container7b = dataclass_from_dict(_TestContainerClass7, {})
+ assert container7 == container7b
diff --git a/tests/test_efro/test_message.py b/tests/test_efro/test_message.py
index f29c5fd8..be5cf589 100644
--- a/tests/test_efro/test_message.py
+++ b/tests/test_efro/test_message.py
@@ -11,6 +11,7 @@ import asyncio
from typing import TYPE_CHECKING, overload, assert_type
from dataclasses import dataclass
+from typing_extensions import override
import pytest
from efro.error import CleanError, RemoteError, CommunicationError
from efro.dataclassio import ioprepped
@@ -39,6 +40,7 @@ class _TMsg1(Message):
ival: int
+ @override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [_TResp1]
@@ -51,6 +53,7 @@ class _TMsg2(Message):
sval: str
+ @override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [_TResp1, _TResp2]
@@ -146,16 +149,13 @@ class _BoundTestMessageSenderSync(BoundMessageSender):
"""Protocol-specific bound sender."""
@overload
- def send(self, message: _TMsg1) -> _TResp1:
- ...
+ def send(self, message: _TMsg1) -> _TResp1: ...
@overload
- def send(self, message: _TMsg2) -> _TResp1 | _TResp2:
- ...
+ def send(self, message: _TMsg2) -> _TResp1 | _TResp2: ...
@overload
- def send(self, message: _TMsg3) -> None:
- ...
+ def send(self, message: _TMsg3) -> None: ...
def send(self, message: Message) -> Response | None:
"""Send a message synchronously."""
@@ -185,16 +185,13 @@ class _BoundTestMessageSenderAsync(BoundMessageSender):
"""Protocol-specific bound sender."""
@overload
- async def send_async(self, message: _TMsg1) -> _TResp1:
- ...
+ async def send_async(self, message: _TMsg1) -> _TResp1: ...
@overload
- async def send_async(self, message: _TMsg2) -> _TResp1 | _TResp2:
- ...
+ async def send_async(self, message: _TMsg2) -> _TResp1 | _TResp2: ...
@overload
- async def send_async(self, message: _TMsg3) -> None:
- ...
+ async def send_async(self, message: _TMsg3) -> None: ...
def send_async(self, message: Message) -> Awaitable[Response | None]:
"""Send a message asynchronously."""
@@ -224,40 +221,32 @@ class _BoundTestMessageSenderBBoth(BoundMessageSender):
"""Protocol-specific bound sender."""
@overload
- def send(self, message: _TMsg1) -> _TResp1:
- ...
+ def send(self, message: _TMsg1) -> _TResp1: ...
@overload
- def send(self, message: _TMsg2) -> _TResp1 | _TResp2:
- ...
+ def send(self, message: _TMsg2) -> _TResp1 | _TResp2: ...
@overload
- def send(self, message: _TMsg3) -> None:
- ...
+ def send(self, message: _TMsg3) -> None: ...
@overload
- def send(self, message: _TMsg4) -> None:
- ...
+ def send(self, message: _TMsg4) -> None: ...
def send(self, message: Message) -> Response | None:
"""Send a message synchronously."""
return self._sender.send(self._obj, message)
@overload
- async def send_async(self, message: _TMsg1) -> _TResp1:
- ...
+ async def send_async(self, message: _TMsg1) -> _TResp1: ...
@overload
- async def send_async(self, message: _TMsg2) -> _TResp1 | _TResp2:
- ...
+ async def send_async(self, message: _TMsg2) -> _TResp1 | _TResp2: ...
@overload
- async def send_async(self, message: _TMsg3) -> None:
- ...
+ async def send_async(self, message: _TMsg3) -> None: ...
@overload
- async def send_async(self, message: _TMsg4) -> None:
- ...
+ async def send_async(self, message: _TMsg4) -> None: ...
def send_async(self, message: Message) -> Awaitable[Response | None]:
"""Send a message asynchronously."""
@@ -335,22 +324,19 @@ class _TestSyncMessageReceiver(MessageReceiver):
def handler(
self,
call: Callable[[Any, _TMsg1], _TResp1],
- ) -> Callable[[Any, _TMsg1], _TResp1]:
- ...
+ ) -> Callable[[Any, _TMsg1], _TResp1]: ...
@overload
def handler(
self,
call: Callable[[Any, _TMsg2], _TResp1 | _TResp2],
- ) -> Callable[[Any, _TMsg2], _TResp1 | _TResp2]:
- ...
+ ) -> Callable[[Any, _TMsg2], _TResp1 | _TResp2]: ...
@overload
def handler(
self,
call: Callable[[Any, _TMsg3], None],
- ) -> Callable[[Any, _TMsg3], None]:
- ...
+ ) -> Callable[[Any, _TMsg3], None]: ...
def handler(self, call: Callable) -> Callable:
"""Decorator to register message handlers."""
@@ -396,22 +382,19 @@ class _TestAsyncMessageReceiver(MessageReceiver):
def handler(
self,
call: Callable[[Any, _TMsg1], Awaitable[_TResp1]],
- ) -> Callable[[Any, _TMsg1], Awaitable[_TResp1]]:
- ...
+ ) -> Callable[[Any, _TMsg1], Awaitable[_TResp1]]: ...
@overload
def handler(
self,
call: Callable[[Any, _TMsg2], Awaitable[_TResp1 | _TResp2]],
- ) -> Callable[[Any, _TMsg2], Awaitable[_TResp1 | _TResp2]]:
- ...
+ ) -> Callable[[Any, _TMsg2], Awaitable[_TResp1 | _TResp2]]: ...
@overload
def handler(
self,
call: Callable[[Any, _TMsg3], Awaitable[None]],
- ) -> Callable[[Any, _TMsg3], Awaitable[None]]:
- ...
+ ) -> Callable[[Any, _TMsg3], Awaitable[None]]: ...
def handler(self, call: Callable) -> Callable:
"""Decorator to register message handlers."""
diff --git a/tools/bacommon/bacloud.py b/tools/bacommon/bacloud.py
index 2f41f622..15f85ee5 100644
--- a/tools/bacommon/bacloud.py
+++ b/tools/bacommon/bacloud.py
@@ -75,9 +75,9 @@ class ResponseData:
delay_seconds: Annotated[float, IOAttrs('d', store_default=False)] = 0.0
login: Annotated[str | None, IOAttrs('l', store_default=False)] = None
logout: Annotated[bool, IOAttrs('lo', store_default=False)] = False
- dir_manifest: Annotated[
- str | None, IOAttrs('man', store_default=False)
- ] = None
+ dir_manifest: Annotated[str | None, IOAttrs('man', store_default=False)] = (
+ None
+ )
uploads: Annotated[
tuple[list[str], str, dict] | None, IOAttrs('u', store_default=False)
] = None
@@ -97,9 +97,9 @@ class ResponseData:
input_prompt: Annotated[
tuple[str, bool] | None, IOAttrs('inp', store_default=False)
] = None
- end_message: Annotated[
- str | None, IOAttrs('em', store_default=False)
- ] = None
+ end_message: Annotated[str | None, IOAttrs('em', store_default=False)] = (
+ None
+ )
end_message_end: Annotated[str, IOAttrs('eme', store_default=False)] = '\n'
end_command: Annotated[
tuple[str, dict] | None, IOAttrs('ec', store_default=False)
diff --git a/tools/bacommon/build.py b/tools/bacommon/build.py
index 0ffcd3fb..7e26ed55 100644
--- a/tools/bacommon/build.py
+++ b/tools/bacommon/build.py
@@ -21,7 +21,7 @@ class BuildInfoSet:
@dataclass
class Entry:
- """Info about a particular build."""
+ """Info about a particular app build."""
filename: Annotated[str, IOAttrs('fname')]
size: Annotated[int, IOAttrs('size')]
diff --git a/tools/bacommon/cloud.py b/tools/bacommon/cloud.py
index df621b09..dd14bb5b 100644
--- a/tools/bacommon/cloud.py
+++ b/tools/bacommon/cloud.py
@@ -7,6 +7,7 @@ from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Annotated
from enum import Enum
+from typing_extensions import override
from efro.message import Message, Response
from efro.dataclassio import ioprepped, IOAttrs
from bacommon.transfer import DirectoryManifest
@@ -21,6 +22,7 @@ if TYPE_CHECKING:
class LoginProxyRequestMessage(Message):
"""Request send to the cloud to ask for a login-proxy."""
+ @override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [LoginProxyRequestResponse]
@@ -49,6 +51,7 @@ class LoginProxyStateQueryMessage(Message):
proxyid: Annotated[str, IOAttrs('p')]
proxykey: Annotated[str, IOAttrs('k')]
+ @override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [LoginProxyStateQueryResponse]
@@ -85,6 +88,7 @@ class LoginProxyCompleteMessage(Message):
class PingMessage(Message):
"""Standard ping."""
+ @override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [PingResponse]
@@ -103,6 +107,7 @@ class TestMessage(Message):
testfoo: Annotated[int, IOAttrs('f')]
+ @override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [TestResponse]
@@ -116,6 +121,28 @@ class TestResponse(Response):
testfoo: Annotated[int, IOAttrs('f')]
+@ioprepped
+@dataclass
+class PromoCodeMessage(Message):
+ """User is entering a promo code"""
+
+ code: Annotated[str, IOAttrs('c')]
+
+ @override
+ @classmethod
+ def get_response_types(cls) -> list[type[Response] | None]:
+ return [PromoCodeResponse]
+
+
+@ioprepped
+@dataclass
+class PromoCodeResponse(Response):
+ """Applied that promo code for ya, boss."""
+
+ valid: Annotated[bool, IOAttrs('v')]
+ message: Annotated[str | None, IOAttrs('m', store_default=False)] = None
+
+
@ioprepped
@dataclass
class WorkspaceFetchState:
@@ -136,6 +163,7 @@ class WorkspaceFetchMessage(Message):
workspaceid: Annotated[str, IOAttrs('w')]
state: Annotated[WorkspaceFetchState, IOAttrs('s')]
+ @override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [WorkspaceFetchResponse]
@@ -162,6 +190,7 @@ class WorkspaceFetchResponse(Response):
class MerchAvailabilityMessage(Message):
"""Can we show merch link?"""
+ @override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [MerchAvailabilityResponse]
@@ -187,6 +216,7 @@ class SignInMessage(Message):
description: Annotated[str, IOAttrs('d', soft_default='-')]
apptime: Annotated[float, IOAttrs('at', soft_default=-1.0)]
+ @override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [SignInResponse]
@@ -205,6 +235,7 @@ class SignInResponse(Response):
class ManageAccountMessage(Message):
"""Message asking for a manage-account url."""
+ @override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [ManageAccountResponse]
diff --git a/tools/bacommon/net.py b/tools/bacommon/net.py
index 2cc97df1..d07f774a 100644
--- a/tools/bacommon/net.py
+++ b/tools/bacommon/net.py
@@ -63,9 +63,9 @@ class PrivateHostingConfig:
randomize: bool = False
tutorial: bool = False
custom_team_names: tuple[str, str] | None = None
- custom_team_colors: tuple[
- tuple[float, float, float], tuple[float, float, float]
- ] | None = None
+ custom_team_colors: (
+ tuple[tuple[float, float, float], tuple[float, float, float]] | None
+ ) = None
playlist: list[dict[str, Any]] | None = None
exit_minutes: float = 120.0
exit_minutes_unclean: float = 180.0
diff --git a/tools/bacommon/servermanager.py b/tools/bacommon/servermanager.py
index 295a8be8..6cc5cf4b 100644
--- a/tools/bacommon/servermanager.py
+++ b/tools/bacommon/servermanager.py
@@ -138,9 +138,9 @@ class ServerConfig:
team_names: tuple[str, str] | None = None
# Team colors (teams mode only).
- team_colors: tuple[
- tuple[float, float, float], tuple[float, float, float]
- ] | None = None
+ team_colors: (
+ tuple[tuple[float, float, float], tuple[float, float, float]] | None
+ ) = None
# Whether to enable the queue where players can line up before entering
# your server. Disabling this can be used as a workaround to deal with
diff --git a/tools/bacommon/transfer.py b/tools/bacommon/transfer.py
index f6704b26..65221cbd 100644
--- a/tools/bacommon/transfer.py
+++ b/tools/bacommon/transfer.py
@@ -18,10 +18,10 @@ if TYPE_CHECKING:
@ioprepped
@dataclass
class DirectoryManifestFile:
- """Describes metadata and hashes for a file in a manifest."""
+ """Describes a file in a manifest."""
- filehash: Annotated[str, IOAttrs('h')]
- filesize: Annotated[int, IOAttrs('s')]
+ hash_sha256: Annotated[str, IOAttrs('h')]
+ size: Annotated[int, IOAttrs('s')]
@ioprepped
@@ -31,7 +31,7 @@ class DirectoryManifest:
files: Annotated[dict[str, DirectoryManifestFile], IOAttrs('f')]
- _empty_hash: str | None = None
+ # _empty_hash: str | None = None
@classmethod
def create_from_disk(cls, path: Path) -> DirectoryManifest:
@@ -67,7 +67,7 @@ class DirectoryManifest:
return (
filepath,
DirectoryManifestFile(
- filehash=sha.hexdigest(), filesize=filesize
+ hash_sha256=sha.hexdigest(), size=filesize
),
)
@@ -92,12 +92,12 @@ class DirectoryManifest:
)
break # 1 error is enough for now.
- @classmethod
- def get_empty_hash(cls) -> str:
- """Return the hash for an empty file."""
- if cls._empty_hash is None:
- import hashlib
+ # @classmethod
+ # def get_empty_hash(cls) -> str:
+ # """Return the hash for an empty file."""
+ # if cls._empty_hash is None:
+ # import hashlib
- sha = hashlib.sha256()
- cls._empty_hash = sha.hexdigest()
- return cls._empty_hash
+ # sha = hashlib.sha256()
+ # cls._empty_hash = sha.hexdigest()
+ # return cls._empty_hash
diff --git a/tools/batools/build.py b/tools/batools/build.py
index e9bd8b2d..72caf9e6 100644
--- a/tools/batools/build.py
+++ b/tools/batools/build.py
@@ -43,25 +43,25 @@ class PyRequirement:
# remove our custom module based stuff soon if nobody complains, which
# would free us to theoretically move to a requirements.txt based setup.
PY_REQUIREMENTS = [
- PyRequirement(pipname='mypy', minversion=[1, 7, 0]),
- PyRequirement(pipname='pylint', minversion=[3, 0, 2]),
+ PyRequirement(pipname='mypy', minversion=[1, 8, 0]),
+ PyRequirement(pipname='pylint', minversion=[3, 0, 3]),
PyRequirement(pipname='cpplint', minversion=[1, 6, 1]),
- PyRequirement(pipname='pytest', minversion=[7, 4, 2]),
+ PyRequirement(pipname='pytest', minversion=[7, 4, 4]),
PyRequirement(pipname='pytz', minversion=[2023, 3]),
PyRequirement(pipname='ansiwrap', minversion=[0, 8, 4]),
PyRequirement(pipname='requests', minversion=[2, 31, 0]),
- PyRequirement(pipname='pdoc', minversion=[14, 1, 0]),
+ PyRequirement(pipname='pdoc', minversion=[14, 4, 0]),
PyRequirement(pipname='PyYAML', minversion=[6, 0, 1]),
- PyRequirement(pipname='black', minversion=[23, 9, 1]),
- PyRequirement(pipname='typing_extensions', minversion=[4, 8, 0]),
+ PyRequirement(pipname='black', minversion=[24, 1, 1]),
+ PyRequirement(pipname='typing_extensions', minversion=[4, 9, 0]),
PyRequirement(pipname='types-filelock', minversion=[3, 2, 7]),
- PyRequirement(pipname='types-requests', minversion=[2, 31, 0, 6]),
+ PyRequirement(pipname='types-requests', minversion=[2, 31, 0, 20240106]),
PyRequirement(pipname='types-pytz', minversion=[2023, 3, 1, 1]),
PyRequirement(pipname='types-PyYAML', minversion=[6, 0, 12, 12]),
- PyRequirement(pipname='certifi', minversion=[2023, 7, 22]),
+ PyRequirement(pipname='certifi', minversion=[2023, 11, 17]),
PyRequirement(pipname='types-certifi', minversion=[2021, 10, 8, 3]),
- PyRequirement(pipname='pbxproj', minversion=[3, 5, 0]),
- PyRequirement(pipname='filelock', minversion=[3, 12, 4]),
+ PyRequirement(pipname='pbxproj', minversion=[4, 0, 0]),
+ PyRequirement(pipname='filelock', minversion=[3, 13, 1]),
PyRequirement(pipname='python-daemon', minversion=[3, 0, 1]),
]
@@ -614,7 +614,7 @@ def _get_server_config_template_yaml(projroot: str) -> str:
# Ignore indented lines (our few multi-line special cases).
continue
- if line.startswith(']'):
+ if line.startswith(']') or line.startswith(')'):
# Ignore closing lines (our few multi-line special cases).
continue
@@ -643,7 +643,7 @@ def _get_server_config_template_yaml(projroot: str) -> str:
before_equal_sign = before_equal_sign.strip()
vval_raw = vval_raw.strip()
vname = before_equal_sign.split()[0]
- assert vname.endswith(':')
+ assert vname.endswith(':'), f"'{vname}' does not end with ':'"
vname = vname[:-1]
vval: Any
if vval_raw == 'field(default_factory=list)':
diff --git a/tools/batools/dummymodule.py b/tools/batools/dummymodule.py
index 7fbf618e..5a29b870 100755
--- a/tools/batools/dummymodule.py
+++ b/tools/batools/dummymodule.py
@@ -408,13 +408,16 @@ def _special_class_cases(classname: str) -> str:
' return self\n'
'\n'
' # (for index access)\n'
+ ' @override\n'
' def __getitem__(self, typeargs: Any) -> Any:\n'
' return 0.0\n'
'\n'
+ ' @override\n'
' def __len__(self) -> int:\n'
' return 3\n'
'\n'
' # (for iterator access)\n'
+ ' @override\n'
' def __iter__(self) -> Any:\n'
' return self\n'
'\n'
@@ -886,6 +889,8 @@ class Generator:
'\n'
f'from typing import {typing_imports}\n'
'\n'
+ f'from typing_extensions import override\n'
+ '\n'
f'{enum_import_lines}'
'if TYPE_CHECKING:\n'
f' from typing import {typing_imports_tc}\n'
diff --git a/tools/efro/call.py b/tools/efro/call.py
index 8e811e50..933f3f0b 100644
--- a/tools/efro/call.py
+++ b/tools/efro/call.py
@@ -83,57 +83,46 @@ if TYPE_CHECKING:
class _CallNoArgs(Generic[OutT]):
"""Single argument variant of call wrapper."""
- def __init__(self, _call: Callable[[], OutT]):
- ...
+ def __init__(self, _call: Callable[[], OutT]): ...
- def __call__(self) -> OutT:
- ...
+ def __call__(self) -> OutT: ...
class _Call1Arg(Generic[In1T, OutT]):
"""Single argument variant of call wrapper."""
- def __init__(self, _call: Callable[[In1T], OutT]):
- ...
+ def __init__(self, _call: Callable[[In1T], OutT]): ...
- def __call__(self, _arg1: In1T) -> OutT:
- ...
+ def __call__(self, _arg1: In1T) -> OutT: ...
class _Call2Args(Generic[In1T, In2T, OutT]):
"""Two argument variant of call wrapper"""
- def __init__(self, _call: Callable[[In1T, In2T], OutT]):
- ...
+ def __init__(self, _call: Callable[[In1T, In2T], OutT]): ...
- def __call__(self, _arg1: In1T, _arg2: In2T) -> OutT:
- ...
+ def __call__(self, _arg1: In1T, _arg2: In2T) -> OutT: ...
class _Call3Args(Generic[In1T, In2T, In3T, OutT]):
"""Three argument variant of call wrapper"""
- def __init__(self, _call: Callable[[In1T, In2T, In3T], OutT]):
- ...
+ def __init__(self, _call: Callable[[In1T, In2T, In3T], OutT]): ...
- def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T) -> OutT:
- ...
+ def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T) -> OutT: ...
class _Call4Args(Generic[In1T, In2T, In3T, In4T, OutT]):
"""Four argument variant of call wrapper"""
- def __init__(self, _call: Callable[[In1T, In2T, In3T, In4T], OutT]):
- ...
+ def __init__(self, _call: Callable[[In1T, In2T, In3T, In4T], OutT]): ...
def __call__(
self, _arg1: In1T, _arg2: In2T, _arg3: In3T, _arg4: In4T
- ) -> OutT:
- ...
+ ) -> OutT: ...
class _Call5Args(Generic[In1T, In2T, In3T, In4T, In5T, OutT]):
"""Five argument variant of call wrapper"""
def __init__(
self, _call: Callable[[In1T, In2T, In3T, In4T, In5T], OutT]
- ):
- ...
+ ): ...
def __call__(
self,
@@ -142,16 +131,14 @@ if TYPE_CHECKING:
_arg3: In3T,
_arg4: In4T,
_arg5: In5T,
- ) -> OutT:
- ...
+ ) -> OutT: ...
class _Call6Args(Generic[In1T, In2T, In3T, In4T, In5T, In6T, OutT]):
"""Six argument variant of call wrapper"""
def __init__(
self, _call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T], OutT]
- ):
- ...
+ ): ...
def __call__(
self,
@@ -161,8 +148,7 @@ if TYPE_CHECKING:
_arg4: In4T,
_arg5: In5T,
_arg6: In6T,
- ) -> OutT:
- ...
+ ) -> OutT: ...
class _Call7Args(Generic[In1T, In2T, In3T, In4T, In5T, In6T, In7T, OutT]):
"""Seven argument variant of call wrapper"""
@@ -170,8 +156,7 @@ if TYPE_CHECKING:
def __init__(
self,
_call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T, In7T], OutT],
- ):
- ...
+ ): ...
def __call__(
self,
@@ -182,50 +167,43 @@ if TYPE_CHECKING:
_arg5: In5T,
_arg6: In6T,
_arg7: In7T,
- ) -> OutT:
- ...
+ ) -> OutT: ...
# No arg call; no args bundled.
# noinspection PyPep8Naming
@overload
- def Call(call: Callable[[], OutT]) -> _CallNoArgs[OutT]:
- ...
+ def Call(call: Callable[[], OutT]) -> _CallNoArgs[OutT]: ...
# 1 arg call; 1 arg bundled.
# noinspection PyPep8Naming
@overload
- def Call(call: Callable[[In1T], OutT], arg1: In1T) -> _CallNoArgs[OutT]:
- ...
+ def Call(call: Callable[[In1T], OutT], arg1: In1T) -> _CallNoArgs[OutT]: ...
# 1 arg call; no args bundled.
# noinspection PyPep8Naming
@overload
- def Call(call: Callable[[In1T], OutT]) -> _Call1Arg[In1T, OutT]:
- ...
+ def Call(call: Callable[[In1T], OutT]) -> _Call1Arg[In1T, OutT]: ...
# 2 arg call; 2 args bundled.
# noinspection PyPep8Naming
@overload
def Call(
call: Callable[[In1T, In2T], OutT], arg1: In1T, arg2: In2T
- ) -> _CallNoArgs[OutT]:
- ...
+ ) -> _CallNoArgs[OutT]: ...
# 2 arg call; 1 arg bundled.
# noinspection PyPep8Naming
@overload
def Call(
call: Callable[[In1T, In2T], OutT], arg1: In1T
- ) -> _Call1Arg[In2T, OutT]:
- ...
+ ) -> _Call1Arg[In2T, OutT]: ...
# 2 arg call; no args bundled.
# noinspection PyPep8Naming
@overload
def Call(
call: Callable[[In1T, In2T], OutT]
- ) -> _Call2Args[In1T, In2T, OutT]:
- ...
+ ) -> _Call2Args[In1T, In2T, OutT]: ...
# 3 arg call; 3 args bundled.
# noinspection PyPep8Naming
@@ -235,32 +213,28 @@ if TYPE_CHECKING:
arg1: In1T,
arg2: In2T,
arg3: In3T,
- ) -> _CallNoArgs[OutT]:
- ...
+ ) -> _CallNoArgs[OutT]: ...
# 3 arg call; 2 args bundled.
# noinspection PyPep8Naming
@overload
def Call(
call: Callable[[In1T, In2T, In3T], OutT], arg1: In1T, arg2: In2T
- ) -> _Call1Arg[In3T, OutT]:
- ...
+ ) -> _Call1Arg[In3T, OutT]: ...
# 3 arg call; 1 arg bundled.
# noinspection PyPep8Naming
@overload
def Call(
call: Callable[[In1T, In2T, In3T], OutT], arg1: In1T
- ) -> _Call2Args[In2T, In3T, OutT]:
- ...
+ ) -> _Call2Args[In2T, In3T, OutT]: ...
# 3 arg call; no args bundled.
# noinspection PyPep8Naming
@overload
def Call(
call: Callable[[In1T, In2T, In3T], OutT]
- ) -> _Call3Args[In1T, In2T, In3T, OutT]:
- ...
+ ) -> _Call3Args[In1T, In2T, In3T, OutT]: ...
# 4 arg call; 4 args bundled.
# noinspection PyPep8Naming
@@ -271,8 +245,7 @@ if TYPE_CHECKING:
arg2: In2T,
arg3: In3T,
arg4: In4T,
- ) -> _CallNoArgs[OutT]:
- ...
+ ) -> _CallNoArgs[OutT]: ...
# 4 arg call; 3 args bundled.
# noinspection PyPep8Naming
@@ -282,8 +255,7 @@ if TYPE_CHECKING:
arg1: In1T,
arg2: In2T,
arg3: In3T,
- ) -> _Call1Arg[In4T, OutT]:
- ...
+ ) -> _Call1Arg[In4T, OutT]: ...
# 4 arg call; 2 args bundled.
# noinspection PyPep8Naming
@@ -292,8 +264,7 @@ if TYPE_CHECKING:
call: Callable[[In1T, In2T, In3T, In4T], OutT],
arg1: In1T,
arg2: In2T,
- ) -> _Call2Args[In3T, In4T, OutT]:
- ...
+ ) -> _Call2Args[In3T, In4T, OutT]: ...
# 4 arg call; 1 arg bundled.
# noinspection PyPep8Naming
@@ -301,16 +272,14 @@ if TYPE_CHECKING:
def Call(
call: Callable[[In1T, In2T, In3T, In4T], OutT],
arg1: In1T,
- ) -> _Call3Args[In2T, In3T, In4T, OutT]:
- ...
+ ) -> _Call3Args[In2T, In3T, In4T, OutT]: ...
# 4 arg call; no args bundled.
# noinspection PyPep8Naming
@overload
def Call(
call: Callable[[In1T, In2T, In3T, In4T], OutT],
- ) -> _Call4Args[In1T, In2T, In3T, In4T, OutT]:
- ...
+ ) -> _Call4Args[In1T, In2T, In3T, In4T, OutT]: ...
# 5 arg call; 5 args bundled.
# noinspection PyPep8Naming
@@ -322,8 +291,7 @@ if TYPE_CHECKING:
arg3: In3T,
arg4: In4T,
arg5: In5T,
- ) -> _CallNoArgs[OutT]:
- ...
+ ) -> _CallNoArgs[OutT]: ...
# 6 arg call; 6 args bundled.
# noinspection PyPep8Naming
@@ -336,8 +304,7 @@ if TYPE_CHECKING:
arg4: In4T,
arg5: In5T,
arg6: In6T,
- ) -> _CallNoArgs[OutT]:
- ...
+ ) -> _CallNoArgs[OutT]: ...
# 7 arg call; 7 args bundled.
# noinspection PyPep8Naming
@@ -351,12 +318,10 @@ if TYPE_CHECKING:
arg5: In5T,
arg6: In6T,
arg7: In7T,
- ) -> _CallNoArgs[OutT]:
- ...
+ ) -> _CallNoArgs[OutT]: ...
# noinspection PyPep8Naming
- def Call(*_args: Any, **_keywds: Any) -> Any:
- ...
+ def Call(*_args: Any, **_keywds: Any) -> Any: ...
# (Type-safe Partial)
# A convenient wrapper around functools.partial which adds type-safety
diff --git a/tools/efro/dataclassio/__init__.py b/tools/efro/dataclassio/__init__.py
index 56c87b10..eae9c820 100644
--- a/tools/efro/dataclassio/__init__.py
+++ b/tools/efro/dataclassio/__init__.py
@@ -11,7 +11,13 @@ data formats in a nondestructive manner.
from __future__ import annotations
from efro.util import set_canonical_module_names
-from efro.dataclassio._base import Codec, IOAttrs, IOExtendedData
+from efro.dataclassio._base import (
+ Codec,
+ IOAttrs,
+ IOExtendedData,
+ IOMultiType,
+ EXTRA_ATTRS_ATTR,
+)
from efro.dataclassio._prep import (
ioprep,
ioprepped,
@@ -29,20 +35,22 @@ from efro.dataclassio._api import (
)
__all__ = [
- 'JsonStyle',
'Codec',
+ 'DataclassFieldLookup',
+ 'EXTRA_ATTRS_ATTR',
'IOAttrs',
'IOExtendedData',
- 'ioprep',
- 'ioprepped',
- 'will_ioprep',
- 'is_ioprepped_dataclass',
- 'DataclassFieldLookup',
- 'dataclass_to_dict',
- 'dataclass_to_json',
+ 'IOMultiType',
+ 'JsonStyle',
'dataclass_from_dict',
'dataclass_from_json',
+ 'dataclass_to_dict',
+ 'dataclass_to_json',
'dataclass_validate',
+ 'ioprep',
+ 'ioprepped',
+ 'is_ioprepped_dataclass',
+ 'will_ioprep',
]
# Have these things present themselves cleanly as 'thismodule.SomeClass'
diff --git a/tools/efro/dataclassio/_api.py b/tools/efro/dataclassio/_api.py
index ddadd4d8..0bd5f895 100644
--- a/tools/efro/dataclassio/_api.py
+++ b/tools/efro/dataclassio/_api.py
@@ -27,7 +27,7 @@ class JsonStyle(Enum):
"""Different style types for json."""
# Single line, no spaces, no sorting. Not deterministic.
- # Use this for most storage purposes.
+ # Use this where speed is more important than determinism.
FAST = 'fast'
# Single line, no spaces, sorted keys. Deterministic.
@@ -40,7 +40,9 @@ class JsonStyle(Enum):
def dataclass_to_dict(
- obj: Any, codec: Codec = Codec.JSON, coerce_to_float: bool = True
+ obj: Any,
+ codec: Codec = Codec.JSON,
+ coerce_to_float: bool = True,
) -> dict:
"""Given a dataclass object, return a json-friendly dict.
@@ -101,32 +103,36 @@ def dataclass_from_dict(
The dict must be formatted to match the specified codec (generally
json-friendly object types). This means that sequence values such as
- tuples or sets should be passed as lists, enums should be passed as their
- associated values, nested dataclasses should be passed as dicts, etc.
+ tuples or sets should be passed as lists, enums should be passed as
+ their associated values, nested dataclasses should be passed as dicts,
+ etc.
All values are checked to ensure their types/values are valid.
Data for attributes of type Any will be checked to ensure they match
types supported directly by json. This does not include types such
as tuples which are implicitly translated by Python's json module
- (as this would break the ability to do a lossless round-trip with data).
+ (as this would break the ability to do a lossless round-trip with
+ data).
If coerce_to_float is True, int values passed for float typed fields
will be converted to float values. Otherwise, a TypeError is raised.
- If allow_unknown_attrs is False, AttributeErrors will be raised for
- attributes present in the dict but not on the data class. Otherwise, they
- will be preserved as part of the instance and included if it is
- exported back to a dict, unless discard_unknown_attrs is True, in which
- case they will simply be discarded.
+ If `allow_unknown_attrs` is False, AttributeErrors will be raised for
+ attributes present in the dict but not on the data class. Otherwise,
+ they will be preserved as part of the instance and included if it is
+ exported back to a dict, unless `discard_unknown_attrs` is True, in
+ which case they will simply be discarded.
"""
- return _Inputter(
+ val = _Inputter(
cls,
codec=codec,
coerce_to_float=coerce_to_float,
allow_unknown_attrs=allow_unknown_attrs,
discard_unknown_attrs=discard_unknown_attrs,
).run(values)
+ assert isinstance(val, cls)
+ return val
def dataclass_from_json(
diff --git a/tools/efro/dataclassio/_base.py b/tools/efro/dataclassio/_base.py
index 6ef37105..d2edc2f8 100644
--- a/tools/efro/dataclassio/_base.py
+++ b/tools/efro/dataclassio/_base.py
@@ -8,39 +8,23 @@ import dataclasses
import typing
import datetime
from enum import Enum
-from typing import TYPE_CHECKING, get_args
+from typing import TYPE_CHECKING, get_args, TypeVar, Generic
# noinspection PyProtectedMember
from typing import _AnnotatedAlias # type: ignore
if TYPE_CHECKING:
- from typing import Any, Callable
+ from typing import Any, Callable, Literal, ClassVar, Self
# Types which we can pass through as-is.
SIMPLE_TYPES = {int, bool, str, float, type(None)}
-# Attr name for dict of extra attributes included on dataclass instances.
-# Note that this is only added if extra attributes are present.
+# Attr name for dict of extra attributes included on dataclass
+# instances. Note that this is only added if extra attributes are
+# present.
EXTRA_ATTRS_ATTR = '_DCIOEXATTRS'
-def _raise_type_error(
- fieldpath: str, valuetype: type, expected: tuple[type, ...]
-) -> None:
- """Raise an error when a field value's type does not match expected."""
- assert isinstance(expected, tuple)
- assert all(isinstance(e, type) for e in expected)
- if len(expected) == 1:
- expected_str = expected[0].__name__
- else:
- expected_str = ' | '.join(t.__name__ for t in expected)
- raise TypeError(
- f'Invalid value type for "{fieldpath}";'
- f' expected "{expected_str}", got'
- f' "{valuetype.__name__}".'
- )
-
-
class Codec(Enum):
"""Specifies expected data format exported to or imported from."""
@@ -70,33 +54,54 @@ class IOExtendedData:
Can be overridden to migrate old data formats to new, etc.
"""
+ def did_input(self) -> None:
+ """Called on a class instance after created from data.
-def _is_valid_for_codec(obj: Any, codec: Codec) -> bool:
- """Return whether a value consists solely of json-supported types.
+ Can be useful to correct values from the db, etc. in the
+ type-safe form.
+ """
- Note that this does not include things like tuples which are
- implicitly translated to lists by python's json module.
+
+EnumT = TypeVar('EnumT', bound=Enum)
+
+
+class IOMultiType(Generic[EnumT]):
+ """A base class for types that can map to multiple dataclass types.
+
+ This enables usage of high level base classes (for example
+ a 'Message' type) in annotations, with dataclassio automatically
+ serializing & deserializing dataclass subclasses based on their
+ type ('MessagePing', 'MessageChat', etc.)
+
+ Standard usage involves creating a class which inherits from this
+ one which acts as a 'registry', and then creating dataclass classes
+ inheriting from that registry class. Dataclassio will then do the
+ right thing when that registry class is used in type annotations.
+
+ See tests/test_efro/test_dataclassio.py for examples.
"""
- if obj is None:
- return True
- objtype = type(obj)
- if objtype in (int, float, str, bool):
- return True
- if objtype is dict:
- # JSON 'objects' supports only string dict keys, but all value types.
- return all(
- isinstance(k, str) and _is_valid_for_codec(v, codec)
- for k, v in obj.items()
- )
- if objtype is list:
- return all(_is_valid_for_codec(elem, codec) for elem in obj)
+ # Dataclasses inheriting from an IOMultiType will store a type-id
+ # with this key in their serialized data. This value can be
+ # overridden in IOMultiType subclasses as desired.
+ ID_STORAGE_NAME = '_dciotype'
- # A few things are valid in firestore but not json.
- if issubclass(objtype, datetime.datetime) or objtype is bytes:
- return codec is Codec.FIRESTORE
+ @classmethod
+ def get_type(cls, type_id: EnumT) -> type[Self]:
+ """Return a specific subclass given a type-id."""
+ raise NotImplementedError()
- return False
+ @classmethod
+ def get_type_id(cls) -> EnumT:
+ """Return the type-id for this subclass."""
+ raise NotImplementedError()
+
+ @classmethod
+ def get_type_id_type(cls) -> type[EnumT]:
+ """Return the Enum type this class uses as its type-id."""
+ out: type[EnumT] = cls.__orig_bases__[0].__args__[0] # type: ignore
+ assert issubclass(out, Enum)
+ return out
class IOAttrs:
@@ -185,7 +190,7 @@ class IOAttrs:
"""Ensure the IOAttrs instance is ok to use with the provided field."""
# Turning off store_default requires the field to have either
- # a default or a a default_factory or for us to have soft equivalents.
+ # a default or a default_factory or for us to have soft equivalents.
if not self.store_default:
field_default_factory: Any = field.default_factory
@@ -234,6 +239,52 @@ class IOAttrs:
)
+def _raise_type_error(
+ fieldpath: str, valuetype: type, expected: tuple[type, ...]
+) -> None:
+ """Raise an error when a field value's type does not match expected."""
+ assert isinstance(expected, tuple)
+ assert all(isinstance(e, type) for e in expected)
+ if len(expected) == 1:
+ expected_str = expected[0].__name__
+ else:
+ expected_str = ' | '.join(t.__name__ for t in expected)
+ raise TypeError(
+ f'Invalid value type for "{fieldpath}";'
+ f' expected "{expected_str}", got'
+ f' "{valuetype.__name__}".'
+ )
+
+
+def _is_valid_for_codec(obj: Any, codec: Codec) -> bool:
+ """Return whether a value consists solely of json-supported types.
+
+ Note that this does not include things like tuples which are
+ implicitly translated to lists by python's json module.
+ """
+ if obj is None:
+ return True
+
+ objtype = type(obj)
+ if objtype in (int, float, str, bool):
+ return True
+ if objtype is dict:
+ # JSON 'objects' supports only string dict keys, but all value
+ # types.
+ return all(
+ isinstance(k, str) and _is_valid_for_codec(v, codec)
+ for k, v in obj.items()
+ )
+ if objtype is list:
+ return all(_is_valid_for_codec(elem, codec) for elem in obj)
+
+ # A few things are valid in firestore but not json.
+ if issubclass(objtype, datetime.datetime) or objtype is bytes:
+ return codec is Codec.FIRESTORE
+
+ return False
+
+
def _get_origin(anntype: Any) -> Any:
"""Given a type annotation, return its origin or itself if there is none.
@@ -248,9 +299,9 @@ def _get_origin(anntype: Any) -> Any:
def _parse_annotated(anntype: Any) -> tuple[Any, IOAttrs | None]:
"""Parse Annotated() constructs, returning annotated type & IOAttrs."""
- # If we get an Annotated[foo, bar, eep] we take
- # foo as the actual type, and we look for IOAttrs instances in
- # bar/eep to affect our behavior.
+ # If we get an Annotated[foo, bar, eep] we take foo as the actual
+ # type, and we look for IOAttrs instances in bar/eep to affect our
+ # behavior.
ioattrs: IOAttrs | None = None
if isinstance(anntype, _AnnotatedAlias):
annargs = get_args(anntype)
@@ -263,8 +314,8 @@ def _parse_annotated(anntype: Any) -> tuple[Any, IOAttrs | None]:
)
ioattrs = annarg
- # I occasionally just throw a 'x' down when I mean IOAttrs('x');
- # catch these mistakes.
+ # I occasionally just throw a 'x' down when I mean
+ # IOAttrs('x'); catch these mistakes.
elif isinstance(annarg, (str, int, float, bool)):
raise RuntimeError(
f'Raw {type(annarg)} found in Annotated[] entry:'
@@ -272,3 +323,21 @@ def _parse_annotated(anntype: Any) -> tuple[Any, IOAttrs | None]:
)
anntype = annargs[0]
return anntype, ioattrs
+
+
+def _get_multitype_type(
+ cls: type[IOMultiType], fieldpath: str, val: Any
+) -> type[Any]:
+ if not isinstance(val, dict):
+ raise ValueError(
+ f"Found a {type(val)} at '{fieldpath}'; expected a dict."
+ )
+ storename = cls.ID_STORAGE_NAME
+ id_val = val.get(storename)
+ if id_val is None:
+ raise ValueError(
+ f"Expected a '{storename}'" f" value for object at '{fieldpath}'."
+ )
+ id_enum_type = cls.get_type_id_type()
+ id_enum = id_enum_type(id_val)
+ return cls.get_type(id_enum)
diff --git a/tools/efro/dataclassio/_inputter.py b/tools/efro/dataclassio/_inputter.py
index d6650a65..0e19cba3 100644
--- a/tools/efro/dataclassio/_inputter.py
+++ b/tools/efro/dataclassio/_inputter.py
@@ -13,7 +13,7 @@ import dataclasses
import typing
import types
import datetime
-from typing import TYPE_CHECKING, Generic, TypeVar
+from typing import TYPE_CHECKING
from efro.util import enum_by_value, check_utc
from efro.dataclassio._base import (
@@ -25,6 +25,8 @@ from efro.dataclassio._base import (
SIMPLE_TYPES,
_raise_type_error,
IOExtendedData,
+ _get_multitype_type,
+ IOMultiType,
)
from efro.dataclassio._prep import PrepSession
@@ -34,13 +36,11 @@ if TYPE_CHECKING:
from efro.dataclassio._base import IOAttrs
from efro.dataclassio._outputter import _Outputter
-T = TypeVar('T')
-
-class _Inputter(Generic[T]):
+class _Inputter:
def __init__(
self,
- cls: type[T],
+ cls: type[Any],
codec: Codec,
coerce_to_float: bool,
allow_unknown_attrs: bool = True,
@@ -59,16 +59,46 @@ class _Inputter(Generic[T]):
' when allow_unknown_attrs is False.'
)
- def run(self, values: dict) -> T:
+ def run(self, values: dict) -> Any:
"""Do the thing."""
- # For special extended data types, call their 'will_output' callback.
- tcls = self._cls
- if issubclass(tcls, IOExtendedData):
- tcls.will_input(values)
+ outcls: type[Any]
+
+ # If we're dealing with a multi-type subclass which is NOT a
+ # dataclass, we must rely on its stored type to figure out
+ # what type of dataclass we're going to. If we are a dataclass
+ # then we already know what type we're going to so we can
+ # survive without this, which is often necessary when reading
+ # old data that doesn't have a type id attr yet.
+ if issubclass(self._cls, IOMultiType) and not dataclasses.is_dataclass(
+ self._cls
+ ):
+ type_id_val = values.get(self._cls.ID_STORAGE_NAME)
+ if type_id_val is None:
+ raise ValueError(
+ f'No type id value present for multi-type object:'
+ f' {values}.'
+ )
+ type_id_enum = self._cls.get_type_id_type()
+ enum_val = type_id_enum(type_id_val)
+ outcls = self._cls.get_type(enum_val)
+ else:
+ outcls = self._cls
+
+ # FIXME - should probably move this into _dataclass_from_input
+ # so it can work on nested values.
+ if issubclass(outcls, IOExtendedData):
+ is_ext = True
+ outcls.will_input(values)
+ else:
+ is_ext = False
+
+ out = self._dataclass_from_input(outcls, '', values)
+ assert isinstance(out, outcls)
+
+ if is_ext:
+ out.did_input()
- out = self._dataclass_from_input(self._cls, '', values)
- assert isinstance(out, self._cls)
return out
def _value_from_input(
@@ -99,8 +129,8 @@ class _Inputter(Generic[T]):
# noinspection PyPep8
if origin is typing.Union or origin is types.UnionType:
# Currently, the only unions we support are None/Value
- # (translated from Optional), which we verified on prep.
- # So let's treat this as a simple optional case.
+ # (translated from Optional), which we verified on prep. So
+ # let's treat this as a simple optional case.
if value is None:
return None
childanntypes_l = [
@@ -111,13 +141,15 @@ class _Inputter(Generic[T]):
cls, fieldpath, childanntypes_l[0], value, ioattrs
)
- # Everything below this point assumes the annotation type resolves
- # to a concrete type. (This should have been verified at prep time).
+ # Everything below this point assumes the annotation type
+ # resolves to a concrete type. (This should have been verified
+ # at prep time).
assert isinstance(origin, type)
if origin in SIMPLE_TYPES:
if type(value) is not origin:
- # Special case: if they want to coerce ints to floats, do so.
+ # Special case: if they want to coerce ints to floats,
+ # do so.
if (
self._coerce_to_float
and origin is float
@@ -145,6 +177,16 @@ class _Inputter(Generic[T]):
if dataclasses.is_dataclass(origin):
return self._dataclass_from_input(origin, fieldpath, value)
+ # ONLY consider something as a multi-type when it's not a
+ # dataclass (all dataclasses inheriting from the multi-type
+ # should just be processed as dataclasses).
+ if issubclass(origin, IOMultiType):
+ return self._dataclass_from_input(
+ _get_multitype_type(anntype, fieldpath, value),
+ fieldpath,
+ value,
+ )
+
if issubclass(origin, Enum):
return enum_by_value(origin, value)
@@ -216,10 +258,23 @@ class _Inputter(Generic[T]):
f.name: _parse_annotated(prep.annotations[f.name]) for f in fields
}
+ # Special case: if this is a multi-type class it probably has a
+ # type attr. Ignore that while parsing since we already have a
+ # definite type and it will just pollute extra-attrs otherwise.
+ if issubclass(cls, IOMultiType):
+ type_id_store_name = cls.ID_STORAGE_NAME
+ else:
+ type_id_store_name = None
+
# Go through all data in the input, converting it to either dataclass
# args or extra data.
args: dict[str, Any] = {}
for rawkey, value in values.items():
+
+ # Ignore _dciotype or whatnot.
+ if type_id_store_name is not None and rawkey == type_id_store_name:
+ continue
+
key = prep.storage_names_to_attr_names.get(rawkey, rawkey)
field = fields_by_name.get(key)
@@ -461,6 +516,19 @@ class _Inputter(Generic[T]):
# We contain elements of some specified type.
assert len(childanntypes) == 1
childanntype = childanntypes[0]
+
+ # If our annotation type inherits from IOMultiType, use type-id
+ # values to determine which type to load for each element.
+ if issubclass(childanntype, IOMultiType):
+ return seqtype(
+ self._dataclass_from_input(
+ _get_multitype_type(childanntype, fieldpath, i),
+ fieldpath,
+ i,
+ )
+ for i in value
+ )
+
return seqtype(
self._value_from_input(cls, fieldpath, childanntype, i, ioattrs)
for i in value
diff --git a/tools/efro/dataclassio/_outputter.py b/tools/efro/dataclassio/_outputter.py
index 03e1d20f..216b11d9 100644
--- a/tools/efro/dataclassio/_outputter.py
+++ b/tools/efro/dataclassio/_outputter.py
@@ -25,6 +25,7 @@ from efro.dataclassio._base import (
SIMPLE_TYPES,
_raise_type_error,
IOExtendedData,
+ IOMultiType,
)
from efro.dataclassio._prep import PrepSession
@@ -49,6 +50,8 @@ class _Outputter:
assert dataclasses.is_dataclass(self._obj)
# For special extended data types, call their 'will_output' callback.
+ # FIXME - should probably move this into _process_dataclass so it
+ # can work on nested values.
if isinstance(self._obj, IOExtendedData):
self._obj.will_output()
@@ -69,6 +72,7 @@ class _Outputter:
def _process_dataclass(self, cls: type, obj: Any, fieldpath: str) -> Any:
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
+ # pylint: disable=too-many-statements
prep = PrepSession(explicit=False).prep_dataclass(
type(obj), recursion_level=0
)
@@ -139,6 +143,25 @@ class _Outputter:
if self._create:
assert out is not None
out.update(extra_attrs)
+
+ # If this obj inherits from multi-type, store its type id.
+ if isinstance(obj, IOMultiType):
+ type_id = obj.get_type_id()
+
+ # Sanity checks; make sure looking up this id gets us this
+ # type.
+ assert isinstance(type_id.value, str)
+ if obj.get_type(type_id) is not type(obj):
+ raise RuntimeError(
+ f'dataclassio: object of type {type(obj)}'
+ f' gives type-id {type_id} but that id gives type'
+ f' {obj.get_type(type_id)}. Something is out of sync.'
+ )
+ assert obj.get_type(type_id) is type(obj)
+ if self._create:
+ assert out is not None
+ out[obj.ID_STORAGE_NAME] = type_id.value
+
return out
def _process_value(
@@ -231,6 +254,7 @@ class _Outputter:
f'Expected a list for {fieldpath};'
f' found a {type(value)}'
)
+
childanntypes = typing.get_args(anntype)
# 'Any' type children; make sure they are valid values for
@@ -246,8 +270,37 @@ class _Outputter:
# Hmm; should we do a copy here?
return value if self._create else None
- # We contain elements of some specified type.
+ # We contain elements of some single specified type.
assert len(childanntypes) == 1
+ childanntype = childanntypes[0]
+
+ # If that type is a multi-type, we determine our type per-object.
+ if issubclass(childanntype, IOMultiType):
+ # In the multi-type case, we use each object's own type
+ # to do its conversion, but lets at least make sure each
+ # of those types inherits from the annotated multi-type
+ # class.
+ for x in value:
+ if not isinstance(x, childanntype):
+ raise ValueError(
+ f"Found a {type(x)} value under '{fieldpath}'."
+ f' Everything must inherit from'
+ f' {childanntype}.'
+ )
+
+ if self._create:
+ out: list[Any] = []
+ for x in value:
+ # We know these are dataclasses so no need to do
+ # the generic _process_value.
+ out.append(self._process_dataclass(cls, x, fieldpath))
+ return out
+ for x in value:
+ # We know these are dataclasses so no need to do
+ # the generic _process_value.
+ self._process_dataclass(cls, x, fieldpath)
+
+ # Normal non-multitype case; everything's got the same type.
if self._create:
return [
self._process_value(
@@ -307,6 +360,21 @@ class _Outputter:
)
return self._process_dataclass(cls, value, fieldpath)
+ # ONLY consider something as a multi-type when it's not a
+ # dataclass (all dataclasses inheriting from the multi-type should
+ # just be processed as dataclasses).
+ if issubclass(origin, IOMultiType):
+ # In the multi-type case, we use each object's own type to
+ # do its conversion, but lets at least make sure each of
+ # those types inherits from the annotated multi-type class.
+ if not isinstance(value, origin):
+ raise ValueError(
+ f"Found a {type(value)} value at '{fieldpath}'."
+ f' It is expected to inherit from {origin}.'
+ )
+
+ return self._process_dataclass(cls, value, fieldpath)
+
if issubclass(origin, Enum):
if not isinstance(value, origin):
raise TypeError(
diff --git a/tools/efro/dataclassio/_prep.py b/tools/efro/dataclassio/_prep.py
index 0800b559..d7f8e828 100644
--- a/tools/efro/dataclassio/_prep.py
+++ b/tools/efro/dataclassio/_prep.py
@@ -17,7 +17,12 @@ import datetime
from typing import TYPE_CHECKING, TypeVar, get_type_hints
# noinspection PyProtectedMember
-from efro.dataclassio._base import _parse_annotated, _get_origin, SIMPLE_TYPES
+from efro.dataclassio._base import (
+ _parse_annotated,
+ _get_origin,
+ SIMPLE_TYPES,
+ IOMultiType,
+)
if TYPE_CHECKING:
from typing import Any
@@ -260,6 +265,13 @@ class PrepSession:
origin = _get_origin(anntype)
+ # If we inherit from IOMultiType, we use its type map to
+ # determine which type we're going to instead of the annotation.
+ # And we can't really check those types because they are
+ # lazy-loaded. So I guess we're done here.
+ if issubclass(origin, IOMultiType):
+ return
+
# noinspection PyPep8
if origin is typing.Union or origin is types.UnionType:
self.prep_union(
diff --git a/tools/efro/dataclassio/extras.py b/tools/efro/dataclassio/extras.py
index 327f829e..c54b0c0d 100644
--- a/tools/efro/dataclassio/extras.py
+++ b/tools/efro/dataclassio/extras.py
@@ -7,6 +7,8 @@ from __future__ import annotations
import dataclasses
from typing import TYPE_CHECKING
+from typing_extensions import override
+
if TYPE_CHECKING:
from typing import Any
@@ -32,6 +34,7 @@ class DataclassDiff:
self._obj1 = obj1
self._obj2 = obj2
+ @override
def __repr__(self) -> str:
return dataclass_diff(self._obj1, self._obj2)
diff --git a/tools/efro/debug.py b/tools/efro/debug.py
index 438a84e6..1f9b8b90 100644
--- a/tools/efro/debug.py
+++ b/tools/efro/debug.py
@@ -278,9 +278,7 @@ def _desc(obj: Any) -> str:
tpss = (
f', contains [{tpsj}, ...]'
if len(obj) > 3
- else f', contains [{tpsj}]'
- if tps
- else ''
+ else f', contains [{tpsj}]' if tps else ''
)
extra = f' (len {len(obj)}{tpss})'
elif isinstance(obj, dict):
@@ -299,9 +297,7 @@ def _desc(obj: Any) -> str:
pairss = (
f', contains {{{pairsj}, ...}}'
if len(obj) > 3
- else f', contains {{{pairsj}}}'
- if pairs
- else ''
+ else f', contains {{{pairsj}}}' if pairs else ''
)
extra = f' (len {len(obj)}{pairss})'
if extra is None:
diff --git a/tools/efro/error.py b/tools/efro/error.py
index 7f902162..f0e561b7 100644
--- a/tools/efro/error.py
+++ b/tools/efro/error.py
@@ -6,6 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import errno
+from typing_extensions import override
+
if TYPE_CHECKING:
from typing import Any
@@ -82,6 +84,7 @@ class RemoteError(Exception):
super().__init__(msg)
self._peer_desc = peer_desc
+ @override
def __str__(self) -> str:
s = ''.join(str(arg) for arg in self.args)
# Indent so we can more easily tell what is the remote part when
diff --git a/tools/efro/log.py b/tools/efro/log.py
index 77b89996..e33e10a1 100644
--- a/tools/efro/log.py
+++ b/tools/efro/log.py
@@ -15,6 +15,7 @@ from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Annotated
from threading import Thread, current_thread, Lock
+from typing_extensions import override
from efro.util import utc_now
from efro.call import tpartial
from efro.terminal import Clr
@@ -91,9 +92,9 @@ class LogEntry:
# incorporated into custom log processing. To populate this, our
# LogHandler class looks for a 'labels' dict passed in the optional
# 'extra' dict arg to standard Python log calls.
- labels: Annotated[
- dict[str, str], IOAttrs('la', store_default=False)
- ] = field(default_factory=dict)
+ labels: Annotated[dict[str, str], IOAttrs('la', store_default=False)] = (
+ field(default_factory=dict)
+ )
@ioprepped
@@ -306,6 +307,7 @@ class LogHandler(logging.Handler):
"""Submit a call to be run in the logging background thread."""
self._event_loop.call_soon_threadsafe(call)
+ @override
def emit(self, record: logging.LogRecord) -> None:
# pylint: disable=too-many-branches
if __debug__:
@@ -481,11 +483,11 @@ class LogHandler(logging.Handler):
# after a short bit if we never get a newline.
ship_task = self._file_chunk_ship_task[name]
if ship_task is None:
- self._file_chunk_ship_task[
- name
- ] = self._event_loop.create_task(
- self._ship_chunks_task(name),
- name='log ship file chunks',
+ self._file_chunk_ship_task[name] = (
+ self._event_loop.create_task(
+ self._ship_chunks_task(name),
+ name='log ship file chunks',
+ )
)
except Exception:
diff --git a/tools/efro/message/_protocol.py b/tools/efro/message/_protocol.py
index 04c1dea5..57dd80a2 100644
--- a/tools/efro/message/_protocol.py
+++ b/tools/efro/message/_protocol.py
@@ -386,6 +386,7 @@ class MessageProtocol:
f'\n'
f'from typing import TYPE_CHECKING{ovld}{ovld2}\n'
f'\n'
+ # f'from typing_extensions import override\n'
f'{import_lines}'
f'\n'
f'if TYPE_CHECKING:\n'
@@ -498,8 +499,7 @@ class MessageProtocol:
f' @overload\n'
f' {pfx}def send{sfx}(self,'
f' message: {msgtypevar})'
- f' -> {rtypevar}:\n'
- f' ...\n'
+ f' -> {rtypevar}: ...\n'
)
rtypevar = 'Response | None'
if async_pass:
@@ -606,8 +606,7 @@ class MessageProtocol:
f' call: Callable[[Any, {msgtypevar}], '
f'{rtypevar}],\n'
f' )'
- f' -> Callable[[Any, {msgtypevar}], {rtypevar}]:\n'
- f' ...\n'
+ f' -> Callable[[Any, {msgtypevar}], {rtypevar}]: ...\n'
)
out += (
'\n'
diff --git a/tools/efro/message/_receiver.py b/tools/efro/message/_receiver.py
index f8c38783..8ef6e8b0 100644
--- a/tools/efro/message/_receiver.py
+++ b/tools/efro/message/_receiver.py
@@ -55,12 +55,13 @@ class MessageReceiver:
def __init__(self, protocol: MessageProtocol) -> None:
self.protocol = protocol
self._handlers: dict[type[Message], Callable] = {}
- self._decode_filter_call: Callable[
- [Any, dict, Message], None
- ] | None = None
- self._encode_filter_call: Callable[
- [Any, Message | None, Response | SysResponse, dict], None
- ] | None = None
+ self._decode_filter_call: (
+ Callable[[Any, dict, Message], None] | None
+ ) = None
+ self._encode_filter_call: (
+ Callable[[Any, Message | None, Response | SysResponse, dict], None]
+ | None
+ ) = None
# noinspection PyProtectedMember
def register_handler(
diff --git a/tools/efro/message/_sender.py b/tools/efro/message/_sender.py
index c1f9d00c..b7e18755 100644
--- a/tools/efro/message/_sender.py
+++ b/tools/efro/message/_sender.py
@@ -41,18 +41,18 @@ class MessageSender:
def __init__(self, protocol: MessageProtocol) -> None:
self.protocol = protocol
self._send_raw_message_call: Callable[[Any, str], str] | None = None
- self._send_async_raw_message_call: Callable[
- [Any, str], Awaitable[str]
- ] | None = None
- self._send_async_raw_message_ex_call: Callable[
- [Any, str, Message], Awaitable[str]
- ] | None = None
- self._encode_filter_call: Callable[
- [Any, Message, dict], None
- ] | None = None
- self._decode_filter_call: Callable[
- [Any, Message, dict, Response | SysResponse], None
- ] | None = None
+ self._send_async_raw_message_call: (
+ Callable[[Any, str], Awaitable[str]] | None
+ ) = None
+ self._send_async_raw_message_ex_call: (
+ Callable[[Any, str, Message], Awaitable[str]] | None
+ ) = None
+ self._encode_filter_call: (
+ Callable[[Any, Message, dict], None] | None
+ ) = None
+ self._decode_filter_call: (
+ Callable[[Any, Message, dict, Response | SysResponse], None] | None
+ ) = None
self._peer_desc_call: Callable[[Any], str] | None = None
def send_method(
diff --git a/tools/efro/terminal.py b/tools/efro/terminal.py
index 7b84c658..6d8cd745 100644
--- a/tools/efro/terminal.py
+++ b/tools/efro/terminal.py
@@ -317,8 +317,6 @@ _envval = os.environ.get('EFRO_TERMCOLORS')
color_enabled: bool = (
True
if _envval == '1'
- else False
- if _envval == '0'
- else _default_color_enabled()
+ else False if _envval == '0' else _default_color_enabled()
)
Clr: type[ClrBase] = ClrAlways if color_enabled else ClrNever
diff --git a/tools/efro/util.py b/tools/efro/util.py
index 8bc4542a..5fc5b3ec 100644
--- a/tools/efro/util.py
+++ b/tools/efro/util.py
@@ -174,17 +174,26 @@ def empty_weakref(objtype: type[T]) -> weakref.ref[T]:
# Just create an object and let it die. Is there a cleaner way to do this?
# return weakref.ref(_EmptyObj()) # type: ignore
+ # Sharing a single ones seems at least a bit better.
return _g_empty_weak_ref # type: ignore
-def data_size_str(bytecount: int) -> str:
+def data_size_str(bytecount: int, compact: bool = False) -> str:
"""Given a size in bytes, returns a short human readable string.
- This should be 6 or fewer chars for most all sane file sizes.
+ In compact mode this should be 6 or fewer chars for most all
+ sane file sizes.
"""
# pylint: disable=too-many-return-statements
+
+ # Special case: handle negatives.
+ if bytecount < 0:
+ val = data_size_str(-bytecount, compact=compact)
+ return f'-{val}'
+
if bytecount <= 999:
- return f'{bytecount} B'
+ suffix = 'B' if compact else 'bytes'
+ return f'{bytecount} {suffix}'
kbytecount = bytecount / 1024
if round(kbytecount, 1) < 10.0:
return f'{kbytecount:.1f} KB'
@@ -197,7 +206,7 @@ def data_size_str(bytecount: int) -> str:
return f'{mbytecount:.0f} MB'
gbytecount = bytecount / (1024 * 1024 * 1024)
if round(gbytecount, 1) < 10.0:
- return f'{mbytecount:.1f} GB'
+ return f'{gbytecount:.1f} GB'
return f'{gbytecount:.0f} GB'
@@ -450,8 +459,7 @@ if TYPE_CHECKING:
class ValueDispatcherMethod(Generic[ValT, RetT]):
"""Used by the valuedispatchmethod decorator."""
- def __call__(self, value: ValT) -> RetT:
- ...
+ def __call__(self, value: ValT) -> RetT: ...
def register(
self, value: ValT
@@ -623,7 +631,7 @@ def check_non_optional(obj: T | None) -> T:
Use assert_non_optional for a more efficient (but less safe) equivalent.
"""
if obj is None:
- raise TypeError('Got None value in check_non_optional.')
+ raise ValueError('Got None value in check_non_optional.')
return obj
diff --git a/tools/efrotools/__init__.py b/tools/efrotools/__init__.py
index 1c3bbd45..33cc1aa5 100644
--- a/tools/efrotools/__init__.py
+++ b/tools/efrotools/__init__.py
@@ -105,13 +105,11 @@ def extract_flag(args: list[str], name: str) -> bool:
@overload
def extract_arg(
args: list[str], name: str, required: Literal[False] = False
-) -> str | None:
- ...
+) -> str | None: ...
@overload
-def extract_arg(args: list[str], name: str, required: Literal[True]) -> str:
- ...
+def extract_arg(args: list[str], name: str, required: Literal[True]) -> str: ...
def extract_arg(
diff --git a/tools/efrotools/jsontools.py b/tools/efrotools/jsontools.py
index 86cd1033..708bf382 100644
--- a/tools/efrotools/jsontools.py
+++ b/tools/efrotools/jsontools.py
@@ -7,6 +7,8 @@ from __future__ import annotations
import json
from typing import TYPE_CHECKING
+from typing_extensions import override
+
if TYPE_CHECKING:
from typing import Any
@@ -30,6 +32,7 @@ class NoIndentEncoder(json.JSONEncoder):
del self.kwargs['indent']
self._replacement_map: dict = {}
+ @override
def default(self, o: Any) -> Any:
import uuid
@@ -40,6 +43,7 @@ class NoIndentEncoder(json.JSONEncoder):
return '@@%s@@' % (key,)
return super().default(o)
+ @override
def encode(self, o: Any) -> Any:
result = super().encode(o)
for k, v in self._replacement_map.items():
diff --git a/tools/efrotools/pcommand.py b/tools/efrotools/pcommand.py
index 43fb1be8..ace1aab8 100644
--- a/tools/efrotools/pcommand.py
+++ b/tools/efrotools/pcommand.py
@@ -120,9 +120,11 @@ def clientprint(
assert _g_thread_local_storage is not None
print(
*args,
- file=_g_thread_local_storage.stderr
- if stderr
- else _g_thread_local_storage.stdout,
+ file=(
+ _g_thread_local_storage.stderr
+ if stderr
+ else _g_thread_local_storage.stdout
+ ),
end=end,
)
else:
diff --git a/tools/efrotools/pybuild.py b/tools/efrotools/pybuild.py
index 50d5ac8c..1177ec79 100644
--- a/tools/efrotools/pybuild.py
+++ b/tools/efrotools/pybuild.py
@@ -46,7 +46,7 @@ XZ_VER_APPLE = '5.4.4'
# Android repo doesn't seem to be getting updated much so manually
# bumping various versions to keep things up to date.
-ZLIB_VER_ANDROID = '1.3'
+ZLIB_VER_ANDROID = '1.3.1'
XZ_VER_ANDROID = '5.4.5'
BZIP2_VER_ANDROID = '1.0.8'
GDBM_VER_ANDROID = '1.23'
diff --git a/tools/efrotools/sync.py b/tools/efrotools/sync.py
index 28e2ecc6..71cf7ddd 100644
--- a/tools/efrotools/sync.py
+++ b/tools/efrotools/sync.py
@@ -39,7 +39,6 @@ def _valid_filename(fname: str) -> bool:
'requirements.txt',
'pylintrc',
'clang-format',
- 'pycheckers',
'style.yapf',
'test_task_bin',
'.editorconfig',
diff --git a/tools/efrotools/toolconfig.py b/tools/efrotools/toolconfig.py
index deb43613..bb993a5c 100644
--- a/tools/efrotools/toolconfig.py
+++ b/tools/efrotools/toolconfig.py
@@ -47,7 +47,6 @@ def install_tool_config(projroot: Path, src: Path, dst: Path) -> None:
comment = ';;'
elif dst.name in [
'.mypy.ini',
- '.pycheckers',
'.pylintrc',
'.style.yapf',
'.clang-format',
@@ -183,7 +182,7 @@ def _filter_tool_config(projroot: Path, cfg: str) -> str:
no_implicit_reexport = True
enable_error_code = redundant-expr, truthy-bool, \
-truthy-function, unused-awaitable
+truthy-function, unused-awaitable, explicit-override
"""
).strip()