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()