diff --git a/.efrocachemap b/.efrocachemap index 474b5987..ea435bbb 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -421,44 +421,44 @@ "build/assets/ba_data/audio/zoeOw.ogg": "b2d705c31c9dcc1efdc71394764c3beb", "build/assets/ba_data/audio/zoePickup01.ogg": "e9366dc2d2b8ab8b0c4e2c14c02d0789", "build/assets/ba_data/audio/zoeScream01.ogg": "903e0e45ee9b3373e9d9ce20c814374e", - "build/assets/ba_data/data/langdata.json": "54a0a77dc0adb7ed5dd76cd175741fc7", - "build/assets/ba_data/data/languages/arabic.json": "4a6fc46285d6289ee14a7ccd9e801ac4", + "build/assets/ba_data/data/langdata.json": "03810e5cca79c5fa092f47648060ca0f", + "build/assets/ba_data/data/languages/arabic.json": "8f89f09ad168c251765efebde4c9069c", "build/assets/ba_data/data/languages/belarussian.json": "1004e5ea10b8deaef517fd37e9309521", - "build/assets/ba_data/data/languages/chinese.json": "3c5e0a568008780f2e7258bf74b54efd", + "build/assets/ba_data/data/languages/chinese.json": "3a8ad6b99e13152872962019b3eef49d", "build/assets/ba_data/data/languages/chinesetraditional.json": "904b35b656c53f9830e406565edd5120", "build/assets/ba_data/data/languages/croatian.json": "1e541070309ff6be95b0c39940aa7e99", "build/assets/ba_data/data/languages/czech.json": "d18b7d1c6bf51fc81af4084ef0e69e3e", "build/assets/ba_data/data/languages/danish.json": "8e57db30c5250df2abff14a822f83ea7", - "build/assets/ba_data/data/languages/dutch.json": "734357560f53b4820221f6d60a0b79e8", - "build/assets/ba_data/data/languages/english.json": "dffc4a03b94c74f11da188a7c4187eda", + "build/assets/ba_data/data/languages/dutch.json": "f4e1e8e9231cda9d1bcc7e87a7f8821e", + "build/assets/ba_data/data/languages/english.json": "b5917c3b975155e35fedb655dbd7568c", "build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880", - "build/assets/ba_data/data/languages/filipino.json": "b5c8fb4f820bb3b521516321d51f4523", + "build/assets/ba_data/data/languages/filipino.json": "08b626ee9d8b66c55e79a4829c6cb9f2", "build/assets/ba_data/data/languages/french.json": "6d20655730b1017ef187fd828b91d43c", - "build/assets/ba_data/data/languages/german.json": "c979cb1397d53a1e5b6c9a7becf83072", - "build/assets/ba_data/data/languages/gibberish.json": "2efafa7c1d479ce1fa46e897739508e5", + "build/assets/ba_data/data/languages/german.json": "a150dbb5c0f43984757f7db295d96203", + "build/assets/ba_data/data/languages/gibberish.json": "df76e851aee59657b69e34efd54fee06", "build/assets/ba_data/data/languages/greek.json": "d28d1092fbb00ed857cbd53124c0dc78", - "build/assets/ba_data/data/languages/hindi.json": "5f60453c7dd3853c95c6f822e92d5300", + "build/assets/ba_data/data/languages/hindi.json": "567e6976b3c72f891431ad7fcc62ab16", "build/assets/ba_data/data/languages/hungarian.json": "9d88004a98f0fbe2ea72edd5e0b3002e", "build/assets/ba_data/data/languages/indonesian.json": "2ccb3fe081ead7706dbebb1008a8bc4e", - "build/assets/ba_data/data/languages/italian.json": "3557cd4697da8c59ed33bda066e8cd93", + "build/assets/ba_data/data/languages/italian.json": "43735ea42d14c121bc14eace16f904a2", "build/assets/ba_data/data/languages/korean.json": "4e3524327a0174250aff5e1ef4c0c597", "build/assets/ba_data/data/languages/malay.json": "f6ce0426d03a62612e3e436ed5d1be1f", "build/assets/ba_data/data/languages/persian.json": "2584895475fe62b3fe49a5ea5e69b4b1", "build/assets/ba_data/data/languages/piratespeak.json": "b9fe871e6331b7178cbacbf7eb3033aa", - "build/assets/ba_data/data/languages/polish.json": "89333fb207f9eb2f22fff5a95b022c35", - "build/assets/ba_data/data/languages/portuguese.json": "e1c4414fced051d2c1967417fd47650a", + "build/assets/ba_data/data/languages/polish.json": "d0822d5d3bdd72ddb04dc3c43a0b1395", + "build/assets/ba_data/data/languages/portuguese.json": "46649f4a8f3c5f69758e8b75ffacf439", "build/assets/ba_data/data/languages/romanian.json": "5ae206fe0b71c4015b02b86da8931c8f", - "build/assets/ba_data/data/languages/russian.json": "33c3943f1096aa37f9815d93c6ac1273", + "build/assets/ba_data/data/languages/russian.json": "72bdbb27ede61bbfeafbf81fa4a19e45", "build/assets/ba_data/data/languages/serbian.json": "623fa4129a1154c2f32ed7867e56ff6a", "build/assets/ba_data/data/languages/slovak.json": "3c08c748c96c71bd9e1d7291fb8817b6", - "build/assets/ba_data/data/languages/spanish.json": "27f564597977b8764583a10d750900be", + "build/assets/ba_data/data/languages/spanish.json": "13f587058931acbb68a48981063ee5ff", "build/assets/ba_data/data/languages/swedish.json": "3b179e7333183c70adb0811246b09959", "build/assets/ba_data/data/languages/tamil.json": "ead39b864228696a9b0d19344bc4b5ec", "build/assets/ba_data/data/languages/thai.json": "383540a1e9c7c131ac579f51afc87471", - "build/assets/ba_data/data/languages/turkish.json": "9e8268786667aa3531593edb6ee66112", - "build/assets/ba_data/data/languages/ukrainian.json": "3a5b8132690fcd583d280879876c85b7", + "build/assets/ba_data/data/languages/turkish.json": "440cb59e69ed689018c17d4be0fb4696", + "build/assets/ba_data/data/languages/ukrainian.json": "6063d27c9d6ed013b2b64ff452433621", "build/assets/ba_data/data/languages/venetian.json": "abebcc38ca2655578e65428cc0dd3c45", - "build/assets/ba_data/data/languages/vietnamese.json": "34a8b75acba2c0234e0b00fb4ef7d011", + "build/assets/ba_data/data/languages/vietnamese.json": "59f6686890ceac2b0ac92597751a18ca", "build/assets/ba_data/data/maps/big_g.json": "1dd301d490643088a435ce75df971054", "build/assets/ba_data/data/maps/bridgit.json": "6aea74805f4880cc11237c5734a24422", "build/assets/ba_data/data/maps/courtyard.json": "4b836554c8949bcd2ae382f5e3c1a9cc", @@ -4099,42 +4099,42 @@ "build/assets/windows/Win32/ucrtbased.dll": "bfd1180c269d3950b76f35a63655e9e1", "build/assets/windows/Win32/vc_redist.x86.exe": "15a5f1f876503885adbdf5b3989b3718", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", - "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "c9c741a8c1908f709d9e69f95078e54d", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "48f6568ba24b78e376fcb0109d8811fa", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "74f4a7a42a51aab41baa0b06933ba90a", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "7c3305d87a435b8a2308faf9798af681", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "f10715043bcd264941d66e63b00bb171", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "701b28fdf672a88bdc85aac449a52933", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "a9b00364c83414d86b81e99e5d9e1b10", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "76e1077a37e919260846cfd96985bb3e", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "fd6c8276e03e6af24c897d974838dc34", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "d5c311bb092286d8ef7f48f51e7da95f", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "be9ae9fa1f04a87c4dfd319c02158b92", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "bd15c256a1dd4e086564b23055904a68", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "380d586848a9351eaff8da027b1d3aeb", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "352089e2917fef437e176e148f5fcd91", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "7667304055cd226341597c45534c812e", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "b323fe4cdd3b7d2f31f6e05db7bb04f3", - "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "e57cd21ff8c3566b1eb5fc9e79474d69", - "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "206f0407a6d912b292c6530cd969030b", - "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "e57cd21ff8c3566b1eb5fc9e79474d69", - "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "206f0407a6d912b292c6530cd969030b", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "6a891ea7e3609d5c30a3b321db59dc78", - "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "7f37a6249393fc422a536b3e1622b96f", - "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "6a891ea7e3609d5c30a3b321db59dc78", - "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "7f37a6249393fc422a536b3e1622b96f", - "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "209def6990e220110e5433581dad95e7", - "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "910f66cc0ddf5d17e751dc295f84e63e", - "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "209def6990e220110e5433581dad95e7", - "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "910f66cc0ddf5d17e751dc295f84e63e", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "8cba3a66655dc40054d11acf13131226", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "a23295d8e71df2f8c308cfcd4b628827", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "72eaa6f4466fad2260c121711ec9ed09", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "c9748838494847186721a17dde8fd069", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "e41041e152263ae5552eb0e25c6ae8b8", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "a566b94b52cc4c281ada8a32a7fe078b", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "604c937543f27648e66217818cfc9714", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "d57aa1b54409ef038b2e86b1d59f9d26", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "4fef5a35efc6a2e74b6238ec87838be4", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "9ff03f147a4b5effc838bc2539c17227", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "455c1b9e6bba6db619756c25f6203b80", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "27ab3c363db144716d818003bd7a18f9", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "d9c645eb18380486349dd04f7fee4725", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "15606ae17b5d02daa4fbc6549e8f1290", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "fc798bac8c9e780695eabc930b605c2d", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "43a57614eb5df0c52ee8e64c3061d47d", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "7eec8b4ba4bb300f5f7389ca5a6abec3", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "5924800f1d7870bda557716722b2373b", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "0ab737966bf957046ca15121b1beafbb", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "f835b62e09a9933f5d4c98ba3c3195b1", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "7e2c3b108f358e5dcee152f7acbba90f", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "b2a7b275c79f8f8ea23b7ea1ec1bc3e6", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "1d17fe8c07b5ec076f799e4e758354a1", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "cb7eac5051dda9024db0076f7c7a0a64", + "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "678f8cbc0a31cf59f86211804f3d18f3", + "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "658a1267cf8454e74dd7a43d4921a862", + "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "678f8cbc0a31cf59f86211804f3d18f3", + "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "658a1267cf8454e74dd7a43d4921a862", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "5dfc9ffb40df78765dee25eed63b4d30", + "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "9c65f68f604202158a6c7486eeb985a9", + "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "5dfc9ffb40df78765dee25eed63b4d30", + "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "9c65f68f604202158a6c7486eeb985a9", + "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "4e5e0cac71feae718dfcccdeaa03eba1", + "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "c54e70ec873ecab76c655c20e13f595e", + "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "4e5e0cac71feae718dfcccdeaa03eba1", + "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "c54e70ec873ecab76c655c20e13f595e", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "131825eb39eb85e5231d1507cb03eee5", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "90f0217980b38b9068477208eff91a64", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "c7604ca233b96f41ee3d48e4d9841774", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "a8f0f4570ec807c71aa0aff1e9d02d40", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "9fb98011f0afe0231bbee6357be07d11", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "85cba33219b47db164d78d50b8b2edf5", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "7e72e3c500358123aae35a73a4060171", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "2476d7c95ae08ec0fb1662241fe27655", "src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c", "src/assets/ba_data/python/babase/_mgen/enums.py": "794d258d59fd17a61752843a9a0551ad", "src/ballistica/base/mgen/pyembed/binding_base.inc": "06042d31df0ff9af96b99477162e2a91", diff --git a/CHANGELOG.md b/CHANGELOG.md index c660f178..4962cd5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.37 (build 22123, api 9, 2024-12-03) +### 1.7.37 (build 22125, api 9, 2024-12-06) - Bumping api version to 9. As you'll see below, there's some UI changes that will require a bit of work for any UI mods to adapt to. If your mods don't touch UI stuff at all you can simply bump your api version and call it a day. diff --git a/config/requirements.txt b/config/requirements.txt index 82811250..ed4214ed 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -9,7 +9,7 @@ pur==7.3.2 pylint==3.3.2 pylsp-mypy==0.6.9 pytest==8.3.4 -python-daemon==3.1.0 +python-daemon==3.1.2 python-lsp-black==2.0.0 python-lsp-server==1.12.0 requests==2.32.3 diff --git a/src/assets/ba_data/python/babase/_general.py b/src/assets/ba_data/python/babase/_general.py index d9c21859..8611615f 100644 --- a/src/assets/ba_data/python/babase/_general.py +++ b/src/assets/ba_data/python/babase/_general.py @@ -252,6 +252,14 @@ if TYPE_CHECKING: # type checking on both positional and keyword arguments (as of mypy # 1.11). + # FIXME: Actually, currently (as of Dec 2024) mypy doesn't fully + # type check partial. The partial() call itself is checked, but the + # resulting callable seems to be essentially untyped. We should + # probably revise this stuff so that Call and WeakCall are for 100% + # complete calls so we can fully type check them using ParamSpecs or + # whatnot. We could then write a weak_partial() call if we actually + # need that particular combination of functionality. + # Note: Something here is wonky with pylint, possibly related to our # custom pylint plugin. Disabling all checks seems to fix it. # pylint: disable=all diff --git a/src/assets/ba_data/python/baclassic/_appmode.py b/src/assets/ba_data/python/baclassic/_appmode.py index 2db6bfc1..5d94a6c4 100644 --- a/src/assets/ba_data/python/baclassic/_appmode.py +++ b/src/assets/ba_data/python/baclassic/_appmode.py @@ -205,6 +205,9 @@ class ClassicAppMode(babase.AppMode): self, val: bacommon.cloud.ClassicAccountLiveData ) -> None: achp = round(val.achievements / max(val.achievements_total, 1) * 100.0) + ibc = str(val.inbox_count) + if val.inbox_count_is_max: + ibc += '+' _baclassic.set_root_ui_values( tickets_text=str(val.tickets), tokens_text=str(val.tokens), @@ -217,7 +220,7 @@ class ClassicAppMode(babase.AppMode): achievements_percent_text=f'{achp}%', level_text=str(val.level), xp_text=f'{val.xp}/{val.xpmax}', - inbox_count_text=str(val.inbox_count), + inbox_count_text=ibc, ) def _root_ui_menu_press(self) -> None: @@ -372,13 +375,16 @@ class ClassicAppMode(babase.AppMode): ) def _root_ui_inbox_press(self) -> None: + from bauiv1lib.connectivity import wait_for_connectivity from bauiv1lib.inbox import InboxWindow - self._auxiliary_window_nav( - win_type=InboxWindow, - win_create_call=lambda: InboxWindow( - origin_widget=bauiv1.get_special_widget('inbox_button') - ), + wait_for_connectivity( + on_connected=lambda: self._auxiliary_window_nav( + win_type=InboxWindow, + win_create_call=lambda: InboxWindow( + origin_widget=bauiv1.get_special_widget('inbox_button') + ), + ) ) def _root_ui_store_press(self) -> None: diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index 08b3c57c..75bef8a5 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -53,7 +53,7 @@ if TYPE_CHECKING: # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 22123 +TARGET_BALLISTICA_BUILD = 22125 TARGET_BALLISTICA_VERSION = '1.7.37' diff --git a/src/assets/ba_data/python/baplus/_cloud.py b/src/assets/ba_data/python/baplus/_cloud.py index 47728319..22d85823 100644 --- a/src/assets/ba_data/python/baplus/_cloud.py +++ b/src/assets/ba_data/python/baplus/_cloud.py @@ -117,6 +117,24 @@ class CloudSubsystem(babase.AppSubsystem): ], ) -> None: ... + @overload + def send_message_cb( + self, + msg: bacommon.cloud.BSInboxRequestMessage, + on_response: Callable[ + [bacommon.cloud.BSInboxRequestResponse | Exception], None + ], + ) -> None: ... + + @overload + def send_message_cb( + self, + msg: bacommon.cloud.BSInboxEntryProcessMessage, + on_response: Callable[ + [bacommon.cloud.BSInboxEntryProcessResponse | Exception], None + ], + ) -> None: ... + def send_message_cb( self, msg: Message, diff --git a/src/assets/ba_data/python/bauiv1lib/inbox.py b/src/assets/ba_data/python/bauiv1lib/inbox.py index d5c73000..4a0ae6fe 100644 --- a/src/assets/ba_data/python/bauiv1lib/inbox.py +++ b/src/assets/ba_data/python/bauiv1lib/inbox.py @@ -4,22 +4,53 @@ from __future__ import annotations +import weakref +from dataclasses import dataclass from typing import override -# from bauiv1lib.popup import PopupWindow +from efro.error import CommunicationError +import bacommon.cloud import bauiv1 as bui +# Messages with format versions higher than this will show up as +# 'app needs to be updated to view this' +SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION = 1 + + +@dataclass +class _MessageEntry: + type: bacommon.cloud.BSInboxEntryType + id: str + height: float + text_height: float + scale: float + text: str + color: tuple[float, float, float] + backing: bui.Widget | None = None + button_positive: bui.Widget | None = None + button_negative: bui.Widget | None = None + message_text: bui.Widget | None = None + processing_complete: bool = False + class InboxWindow(bui.MainWindow): """Popup window to show account messages.""" + def __del__(self) -> None: + print('~InboxWindow()') + def __init__( self, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): + + print('InboxWindow()') assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale + + self._message_entries: list[_MessageEntry] = [] + self._width = 600 if uiscale is bui.UIScale.SMALL else 450 self._height = ( 380 @@ -83,7 +114,7 @@ class InboxWindow(bui.MainWindow): h_align='center', v_align='center', scale=0.6, - text='INBOX (UNDER CONSTRUCTION)', + text=bui.Lstr(resource='inboxText'), maxwidth=200, color=bui.app.ui_v1.title_color, ) @@ -99,7 +130,9 @@ class InboxWindow(bui.MainWindow): (110 if uiscale is bui.UIScale.SMALL else 30) + yoffs, ), capture_arrows=True, - simple_culling_v=10, + simple_culling_v=200, + claims_left_right=True, + claims_up_down=True, ) bui.widget(edit=self._scrollwidget, autoselect=True) if uiscale is bui.UIScale.SMALL: @@ -109,32 +142,21 @@ class InboxWindow(bui.MainWindow): ) bui.containerwidget( - edit=self._root_widget, cancel_button=self._back_button + edit=self._root_widget, + cancel_button=self._back_button, + single_depth=True, ) - entries: list[str] = [] - incr = 20 - sub_width = self._width - 90 - sub_height = 40 + len(entries) * incr + # Kick off request. + plus = bui.app.plus + if plus is None or plus.accounts.primary is None: + self._error(bui.Lstr(resource='notSignedInText')) + return - self._subcontainer = bui.containerwidget( - parent=self._scrollwidget, - size=(sub_width, sub_height), - background=False, - ) - - for i, entry in enumerate(entries): - bui.textwidget( - parent=self._subcontainer, - position=(sub_width * 0.08 - 5, sub_height - 20 - incr * i), - maxwidth=20, - scale=0.5, - flatness=1.0, - shadow=0.0, - text=entry, - size=(0, 0), - h_align='right', - v_align='center', + with plus.accounts.primary: + plus.cloud.send_message_cb( + bacommon.cloud.BSInboxRequestMessage(), + on_response=bui.WeakCall(self._on_inbox_request_response), ) @override @@ -147,16 +169,385 @@ class InboxWindow(bui.MainWindow): ) ) - # def _on_cancel_press(self) -> None: - # self._transition_out() + def _error(self, errmsg: bui.Lstr | str) -> None: + """Put ourself in a permanent error state.""" + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + maxwidth=self._width * 0.7, + scale=1.0, + flatness=1.0, + color=(1, 0, 0), + shadow=0.0, + text=errmsg, + size=(0, 0), + h_align='center', + v_align='center', + ) - # def _transition_out(self) -> None: - # if not self._transitioning_out: - # self._transitioning_out = True - # bui.containerwidget( - # edit=self._root_widget, transition='out_scale') + def _on_message_entry_press( + self, + entry_weak: weakref.ReferenceType[_MessageEntry], + process_type: bacommon.cloud.BSInboxEntryProcessType, + ) -> None: + entry = entry_weak() + if entry is None: + return - # @override - # def on_popup_cancel(self) -> None: - # bui.getsound('swish').play() - # self._transition_out() + self._neuter_message_entry(entry) + + # We don't do anything for invalid messages. + if entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN: + entry.processing_complete = True + self._close_soon_if_all_processed() + return + + # Error if we're somehow signed out now. + plus = bui.app.plus + if plus is None or plus.accounts.primary is None: + bui.screenmessage( + bui.Lstr(resource='notSignedInText'), color=(1, 0, 0) + ) + bui.getsound('error').play() + return + + # Message the master-server to process the entry. + with plus.accounts.primary: + plus.cloud.send_message_cb( + bacommon.cloud.BSInboxEntryProcessMessage( + entry.id, process_type + ), + on_response=bui.WeakCall( + self._on_inbox_entry_process_response, + entry_weak, + process_type, + ), + ) + + # Tweak the button to show this is in progress. + button = ( + entry.button_positive + if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE + else entry.button_negative + ) + if button is not None: + bui.buttonwidget(edit=button, label='...') + + def _close_soon_if_all_processed(self) -> None: + bui.apptimer(0.25, bui.WeakCall(self._close_if_all_processed)) + + def _close_if_all_processed(self) -> None: + if not all(m.processing_complete for m in self._message_entries): + return + + self.main_window_back() + + def _neuter_message_entry(self, entry: _MessageEntry) -> None: + errsound = bui.getsound('error') + if entry.button_positive is not None: + bui.buttonwidget( + edit=entry.button_positive, + color=(0.5, 0.5, 0.5), + textcolor=(0.4, 0.4, 0.4), + on_activate_call=errsound.play, + ) + if entry.button_negative is not None: + bui.buttonwidget( + edit=entry.button_negative, + color=(0.5, 0.5, 0.5), + textcolor=(0.4, 0.4, 0.4), + on_activate_call=errsound.play, + ) + if entry.backing is not None: + bui.imagewidget(edit=entry.backing, color=(0.4, 0.4, 0.4)) + if entry.message_text is not None: + bui.textwidget(edit=entry.message_text, color=(0.5, 0.5, 0.5, 0.5)) + + def _on_inbox_entry_process_response( + self, + entry_weak: weakref.ReferenceType[_MessageEntry], + process_type: bacommon.cloud.BSInboxEntryProcessType, + response: bacommon.cloud.BSInboxEntryProcessResponse | Exception, + ) -> None: + entry = entry_weak() + if entry is None: + return + + assert not entry.processing_complete + entry.processing_complete = True + self._close_soon_if_all_processed() + + # No-op if our UI is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + + # Tweak the button to show results. + button = ( + entry.button_positive + if process_type is bacommon.cloud.BSInboxEntryProcessType.POSITIVE + else entry.button_negative + ) + + # See if we should show an error message. + if isinstance(response, Exception): + if isinstance(response, CommunicationError): + error_message = bui.Lstr( + resource='internal.unavailableNoConnectionText' + ) + else: + error_message = bui.Lstr(resource='errorText') + elif response.error is not None: + error_message = bui.Lstr( + translate=('serverResponses', response.error) + ) + else: + error_message = None + + # Show error message if so. + if error_message is not None: + bui.screenmessage(error_message, color=(1, 0, 0)) + bui.getsound('error').play() + if button is not None: + bui.buttonwidget( + edit=button, label=bui.Lstr(resource='errorText') + ) + return + + # Whee; no error. Mark as done. + if button is not None: + bui.buttonwidget(edit=button, label=bui.Lstr(resource='doneText')) + + def _on_inbox_request_response( + self, response: bacommon.cloud.BSInboxRequestResponse | Exception + ) -> None: + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + + # No-op if our UI is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + + errmsg: str | bui.Lstr + if isinstance(response, Exception): + errmsg = bui.Lstr(resource='internal.unavailableNoConnectionText') + is_error = True + else: + is_error = response.error is not None + errmsg = ( + '' + if response.error is None + else bui.Lstr(translate=('serverResponses', response.error)) + ) + + if is_error: + self._error(errmsg) + return + + assert isinstance(response, bacommon.cloud.BSInboxRequestResponse) + + # If we got no messages, don't touch anything. This keeps + # keyboard control working in the empty case. + if not response.entries: + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + maxwidth=self._width * 0.7, + scale=1.0, + flatness=1.0, + color=(0.4, 0.4, 0.4), + shadow=0.0, + text=bui.Lstr(resource='noMessagesText'), + size=(0, 0), + h_align='center', + v_align='center', + ) + return + + sub_width = self._width - 90 + sub_height = 0.0 + + # Run the math on row heights/etc. + for i, entry in enumerate(response.entries): + # We need to flatten text here so we can measure it. + textfin: str + color: tuple[float, float, float] + + # Messages with either newer formatting or unrecognized + # types show up as 'upgrade your app to see this'. + if ( + entry.format_version > SUPPORTED_INBOX_MESSAGE_FORMAT_VERSION + or entry.type is bacommon.cloud.BSInboxEntryType.UNKNOWN + ): + textfin = bui.Lstr( + translate=( + 'serverResponses', + 'You must update the app to view this.', + ) + ).evaluate() + color = (0.6, 0.6, 0.6) + else: + # Translate raw response and apply any replacements. + textfin = bui.Lstr( + translate=('serverResponses', entry.message) + ).evaluate() + assert len(entry.subs) % 2 == 0 # Should always be even. + for j in range(0, len(entry.subs) - 1, 2): + textfin = textfin.replace(entry.subs[j], entry.subs[j + 1]) + color = (0.55, 0.5, 0.7) + + # Calc scale to fit width and then see what height we need + # at that scale. + t_width = max( + 10.0, bui.get_string_width(textfin, suppress_warning=True) + ) + scale = min(0.6, (sub_width * 0.9) / t_width) + t_height = ( + max(10.0, bui.get_string_height(textfin, suppress_warning=True)) + * scale + ) + entry_height = 90.0 + t_height + self._message_entries.append( + _MessageEntry( + type=entry.type, + id=entry.id, + height=entry_height, + text_height=t_height, + scale=scale, + text=textfin, + color=color, + ) + ) + sub_height += entry_height + + subcontainer = bui.containerwidget( + id='inboxsub', + parent=self._scrollwidget, + size=(sub_width, sub_height), + background=False, + single_depth=True, + claims_left_right=True, + claims_up_down=True, + ) + + backing_tex = bui.gettexture('buttonSquareWide') + + buttonrows: list[list[bui.Widget]] = [] + y = sub_height + for i, _entry in enumerate(response.entries): + message_entry = self._message_entries[i] + message_entry_weak = weakref.ref(message_entry) + bwidth = 140 + bheight = 40 + + # Backing. + message_entry.backing = img = bui.imagewidget( + parent=subcontainer, + position=(-0.022 * sub_width, y - message_entry.height * 1.09), + texture=backing_tex, + size=(sub_width * 1.07, message_entry.height * 1.15), + color=message_entry.color, + opacity=0.9, + ) + bui.widget(edit=img, depth_range=(0, 0.1)) + + buttonrow: list[bui.Widget] = [] + have_negative_button = ( + message_entry.type + is bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD + ) + + message_entry.button_positive = btn = bui.buttonwidget( + parent=subcontainer, + position=( + ( + (sub_width - bwidth - 25) + if have_negative_button + else ((sub_width - bwidth) * 0.5) + ), + y - message_entry.height + 15.0, + ), + size=(bwidth, bheight), + label=bui.Lstr( + resource=( + 'claimText' + if message_entry.type + in { + bacommon.cloud.BSInboxEntryType.CLAIM, + bacommon.cloud.BSInboxEntryType.CLAIM_DISCARD, + } + else 'okText' + ) + ), + color=message_entry.color, + textcolor=(0, 1, 0), + on_activate_call=bui.WeakCall( + self._on_message_entry_press, + message_entry_weak, + bacommon.cloud.BSInboxEntryProcessType.POSITIVE, + ), + ) + bui.widget(edit=btn, depth_range=(0.1, 1.0)) + buttonrow.append(btn) + + if have_negative_button: + message_entry.button_negative = btn2 = bui.buttonwidget( + parent=subcontainer, + position=(25, y - message_entry.height + 15.0), + size=(bwidth, bheight), + label=bui.Lstr(resource='discardText'), + color=(0.85, 0.5, 0.7), + textcolor=(1, 0.4, 0.4), + on_activate_call=bui.WeakCall( + self._on_message_entry_press, + message_entry_weak, + bacommon.cloud.BSInboxEntryProcessType.NEGATIVE, + ), + ) + bui.widget(edit=btn2, depth_range=(0.1, 1.0)) + buttonrow.append(btn2) + + buttonrows.append(buttonrow) + + message_entry.message_text = bui.textwidget( + parent=subcontainer, + position=( + sub_width * 0.5, + y - message_entry.text_height * 0.5 - 23.0, + ), + scale=message_entry.scale, + flatness=1.0, + shadow=0.0, + text=message_entry.text, + size=(0, 0), + h_align='center', + v_align='center', + ) + y -= message_entry.height + + uiscale = bui.app.ui_v1.uiscale + above_widget = ( + bui.get_special_widget('back_button') + if uiscale is bui.UIScale.SMALL + else self._back_button + ) + assert above_widget is not None + for i, buttons in enumerate(buttonrows): + if i < len(buttonrows) - 1: + below_widget = buttonrows[i + 1][0] + else: + below_widget = None + + assert buttons # We should never have an empty row. + for j, button in enumerate(buttons): + bui.widget( + edit=button, + up_widget=above_widget, + down_widget=( + button if below_widget is None else below_widget + ), + right_widget=buttons[max(j - 1, 0)], + left_widget=buttons[min(j + 1, len(buttons) - 1)], + ) + + above_widget = buttons[0] diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index d266ee16..35399ae6 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 = 22123; +const int kEngineBuildNumber = 22125; const char* kEngineVersion = "1.7.37"; const int kEngineApiVersion = 9; diff --git a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc index 3f8650b1..bdc6eee5 100644 --- a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc +++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc @@ -1115,6 +1115,7 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) PyObject* click_activate_obj{Py_None}; PyObject* always_highlight_obj{Py_None}; PyObject* parent_obj{Py_None}; + PyObject* id_obj{Py_None}; ContainerWidget* parent_widget; PyObject* edit_obj{Py_None}; PyObject* selectable_obj{Py_None}; @@ -1124,6 +1125,7 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) static const char* kwlist[] = {"edit", "parent", + "id", "size", "position", "background", @@ -1156,16 +1158,16 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) nullptr}; if (!PyArg_ParseTupleAndKeywords( - args, keywds, "|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", - const_cast(kwlist), &edit_obj, &parent_obj, &size_obj, - &pos_obj, &background_obj, &selected_child_obj, &transition_obj, - &cancel_button_obj, &start_button_obj, &root_selectable_obj, - &on_activate_call_obj, &claims_left_right_obj, &claims_tab_obj, - &selection_loops_obj, &selection_loops_to_parent_obj, &scale_obj, - &on_outside_click_call_obj, &single_depth_obj, &visible_child_obj, - &stack_offset_obj, &color_obj, &on_cancel_call_obj, - &print_list_exit_instructions_obj, &click_activate_obj, - &always_highlight_obj, &selectable_obj, + args, keywds, "|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", + const_cast(kwlist), &edit_obj, &parent_obj, &id_obj, + &size_obj, &pos_obj, &background_obj, &selected_child_obj, + &transition_obj, &cancel_button_obj, &start_button_obj, + &root_selectable_obj, &on_activate_call_obj, &claims_left_right_obj, + &claims_tab_obj, &selection_loops_obj, &selection_loops_to_parent_obj, + &scale_obj, &on_outside_click_call_obj, &single_depth_obj, + &visible_child_obj, &stack_offset_obj, &color_obj, + &on_cancel_call_obj, &print_list_exit_instructions_obj, + &click_activate_obj, &always_highlight_obj, &selectable_obj, &scale_origin_stack_offset_obj, &toolbar_visibility_obj, &on_select_call_obj, &claim_outside_clicks_obj, &claims_up_down_obj)) { @@ -1187,6 +1189,9 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) throw Exception("Invalid or nonexistent widget.", PyExcType::kWidgetNotFound); } + if (id_obj != Py_None) { + throw Exception("ID can only be set when creating."); + } } else { if (parent_obj == Py_None) { BA_PRECONDITION(g_ui_v1 && g_ui_v1->screen_root_widget() != nullptr); @@ -1200,6 +1205,12 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) PyExcType::kWidgetNotFound); } widget = Object::New(); + + // Id needs to be set before adding to parent. + if (id_obj != Py_None) { + widget->set_id(Python::GetPyString(id_obj)); + } + g_ui_v1->AddWidget(widget.get(), parent_widget); } @@ -1386,6 +1397,7 @@ static PyMethodDef PyContainerWidgetDef = { "containerwidget(*,\n" " edit: bauiv1.Widget | None = None,\n" " parent: bauiv1.Widget | None = None,\n" + " id: str | None = None,\n" " size: Sequence[float] | None = None,\n" " position: Sequence[float] | None = None,\n" " background: bool | None = None,\n" @@ -2347,6 +2359,7 @@ static auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds) PyObject* show_buffer_bottom_obj{Py_None}; PyObject* show_buffer_left_obj{Py_None}; PyObject* show_buffer_right_obj{Py_None}; + PyObject* depth_range_obj{Py_None}; PyObject* autoselect_obj{Py_None}; static const char* kwlist[] = {"edit", @@ -2358,13 +2371,14 @@ static auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds) "show_buffer_bottom", "show_buffer_left", "show_buffer_right", + "depth_range", "autoselect", nullptr}; if (!PyArg_ParseTupleAndKeywords( - args, keywds, "O|OOOOOOOOO", const_cast(kwlist), &edit_obj, + args, keywds, "O|OOOOOOOOOO", const_cast(kwlist), &edit_obj, &up_widget_obj, &down_widget_obj, &left_widget_obj, &right_widget_obj, &show_buffer_top_obj, &show_buffer_bottom_obj, &show_buffer_left_obj, - &show_buffer_right_obj, &autoselect_obj)) + &show_buffer_right_obj, &depth_range_obj, &autoselect_obj)) return nullptr; if (!g_base->CurrentContext().IsEmpty()) { @@ -2424,6 +2438,21 @@ static auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds) if (show_buffer_right_obj != Py_None) { widget->set_show_buffer_right(Python::GetPyFloat(show_buffer_right_obj)); } + if (depth_range_obj != Py_None) { + auto depth_range = Python::GetPyFloats(depth_range_obj); + if (depth_range.size() != 2) { + throw Exception("Expected 2 float values.", PyExcType::kValue); + } + if (depth_range[0] < 0.0 || depth_range[1] > 1.0 + || depth_range[1] <= depth_range[0]) { + throw Exception( + "Invalid depth range values;" + " values must be between 0 and 1 and second value must be larger " + "than first.", + PyExcType::kValue); + } + widget->set_depth_range(depth_range[0], depth_range[1]); + } if (autoselect_obj != Py_None) { widget->set_auto_select(Python::GetPyBool(autoselect_obj)); } @@ -2450,6 +2479,7 @@ static PyMethodDef PyWidgetDef = { " show_buffer_bottom: float | None = None,\n" " show_buffer_left: float | None = None,\n" " show_buffer_right: float | None = None,\n" + " depth_range: tuple[float, float] | None = None,\n" " autoselect: bool | None = None) -> None\n" "\n" "Edit common attributes of any widget.\n" diff --git a/src/ballistica/ui_v1/widget/container_widget.cc b/src/ballistica/ui_v1/widget/container_widget.cc index e227d4f8..d6e6d32e 100644 --- a/src/ballistica/ui_v1/widget/container_widget.cc +++ b/src/ballistica/ui_v1/widget/container_widget.cc @@ -61,18 +61,18 @@ void ContainerWidget::DrawChildren(base::RenderPass* pass, // We're expected to fill z space 0..1 when we draw... so we need to divide // that space between our child widgets plus our bg layer. - float layer_thickness = 0.0f; - float layer_spacing = 0.0f; - float base_offset = 0.0f; - float layer_thickness1 = 0.0f; - float layer_thickness2 = 0.0f; - float layer_thickness3 = 0.0f; - float layer_spacing1 = 0.0f; - float layer_spacing2 = 0.0f; - float layer_spacing3 = 0.0f; - float base_offset1 = 0.0f; - float base_offset2 = 0.0f; - float base_offset3 = 0.0f; + float layer_thickness{}; + float layer_spacing{}; + float base_offset{}; + float layer_thickness1{}; + float layer_thickness2{}; + float layer_thickness3{}; + float layer_spacing1{}; + float layer_spacing2{}; + float layer_spacing3{}; + float base_offset1{}; + float base_offset2{}; + float base_offset3{}; // In single-depth mode we draw all widgets at the same depth so they each get // our full depth resolution. however they may overlap incorrectly. @@ -203,12 +203,17 @@ void ContainerWidget::DrawChildren(base::RenderPass* pass, // Widgets can opt to use a subset of their allotted depth slice. float d_min = w.depth_range_min(); float d_max = w.depth_range_max(); + float this_z_offs; + float this_layer_thickness; if (d_min != 0.0f || d_max != 1.0f) { - z_offs += layer_thickness * d_min; - layer_thickness *= (d_max - d_min); + this_z_offs = z_offs + layer_thickness * d_min; + this_layer_thickness = layer_thickness * (d_max - d_min); + } else { + this_z_offs = z_offs; + this_layer_thickness = layer_thickness; } - c.Translate(x_offset + tx, y_offset + ty, z_offs); - c.Scale(s, s, layer_thickness); + c.Translate(x_offset + tx, y_offset + ty, this_z_offs); + c.Scale(s, s, this_layer_thickness); c.Submit(); w.Draw(pass, draw_transparent); } @@ -276,12 +281,17 @@ void ContainerWidget::DrawChildren(base::RenderPass* pass, // Widgets can opt to use a subset of their allotted depth slice. float d_min = w.depth_range_min(); float d_max = w.depth_range_max(); + float this_z_offs; + float this_layer_thickness; if (d_min != 0.0f || d_max != 1.0f) { - z_offs += layer_thickness * d_min; - layer_thickness *= (d_max - d_min); + this_z_offs = z_offs + layer_thickness * d_min; + this_layer_thickness = layer_thickness * (d_max - d_min); + } else { + this_z_offs = z_offs; + this_layer_thickness = layer_thickness; } - c.Translate(x_offset + tx, y_offset + ty, z_offs); - c.Scale(s, s, layer_thickness); + c.Translate(x_offset + tx, y_offset + ty, this_z_offs); + c.Scale(s, s, this_layer_thickness); c.Submit(); w.Draw(pass, draw_transparent); } diff --git a/tools/bacommon/cloud.py b/tools/bacommon/cloud.py index 5e96a18b..79bcfcec 100644 --- a/tools/bacommon/cloud.py +++ b/tools/bacommon/cloud.py @@ -3,9 +3,11 @@ """Functionality related to cloud functionality.""" from __future__ import annotations + +import datetime +from enum import Enum from dataclasses import dataclass, field from typing import TYPE_CHECKING, Annotated, override -from enum import Enum from efro.message import Message, Response from efro.dataclassio import ioprepped, IOAttrs @@ -355,3 +357,85 @@ class ClassicAccountLiveData: xpmax: Annotated[int, IOAttrs('xpm')] inbox_count: Annotated[int, IOAttrs('ibc')] + inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')] + + +class BSInboxEntryType(Enum): + """Types of entries that can be in an inbox.""" + + UNKNOWN = 'u' # Entry types we don't support will be this. + SIMPLE = 's' + CLAIM = 'c' + CLAIM_DISCARD = 'cd' + + +@ioprepped +@dataclass +class BSInboxEntry: + """Single message in an inbox.""" + + type: Annotated[ + BSInboxEntryType, IOAttrs('t', enum_fallback=BSInboxEntryType.UNKNOWN) + ] + id: Annotated[str, IOAttrs('i')] + createtime: Annotated[datetime.datetime, IOAttrs('c')] + + # If clients don't support format_version of a message they will + # display 'app needs to be updated to show this'. + format_version: Annotated[int, IOAttrs('f', soft_default=1)] + + # These have soft defaults so can be removed in the future if desired. + message: Annotated[str, IOAttrs('m', soft_default='(invalid message)')] + subs: Annotated[list[str], IOAttrs('s', soft_default_factory=list)] + + +@ioprepped +@dataclass +class BSInboxRequestMessage(Message): + """Message requesting our inbox.""" + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [BSInboxRequestResponse] + + +@ioprepped +@dataclass +class BSInboxRequestResponse(Response): + """Here's that inbox contents you asked for, boss.""" + + entries: Annotated[list[BSInboxEntry], IOAttrs('m')] + + # Printable error if something goes wrong. + error: Annotated[str | None, IOAttrs('e')] = None + + +class BSInboxEntryProcessType(Enum): + """Types of processing we can ask for.""" + + POSITIVE = 'p' + NEGATIVE = 'n' + + +@ioprepped +@dataclass +class BSInboxEntryProcessMessage(Message): + """Do something to an inbox entry.""" + + id: Annotated[str, IOAttrs('i')] + process_type: Annotated[BSInboxEntryProcessType, IOAttrs('t')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [BSInboxEntryProcessResponse] + + +@ioprepped +@dataclass +class BSInboxEntryProcessResponse(Response): + """Did something to that inbox entry, boss.""" + + # Printable error if something goes wrong. + error: Annotated[str | None, IOAttrs('e')] = None