From d16698e35efaff0864909bcc3ee3892a5c819c11 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Sat, 21 Dec 2024 08:40:17 -0800 Subject: [PATCH] more work on chests --- .efrocachemap | 72 ++-- CHANGELOG.md | 2 +- config/requirements.txt | 2 +- src/assets/.asset_manifest_public.json | 2 + src/assets/Makefile | 2 + src/assets/ba_data/python/babase/__init__.py | 2 + src/assets/ba_data/python/babase/_app.py | 6 + src/assets/ba_data/python/babase/_apputils.py | 11 + src/assets/ba_data/python/babase/_general.py | 11 +- .../ba_data/python/baclassic/_appmode.py | 44 ++- src/assets/ba_data/python/baenv.py | 2 +- src/assets/ba_data/python/baplus/_cloud.py | 18 + src/assets/ba_data/python/bauiv1/__init__.py | 2 + src/assets/ba_data/python/bauiv1lib/chest.py | 348 ++++++++++++++++++ .../ba_data/python/bauiv1lib/inventory.py | 4 +- src/ballistica/shared/ballistica.cc | 2 +- .../python/methods/python_methods_ui_v1.cc | 4 + src/ballistica/ui_v1/widget/root_widget.cc | 10 +- tools/bacommon/cloud.py | 81 +++- 19 files changed, 576 insertions(+), 49 deletions(-) create mode 100644 src/assets/ba_data/python/bauiv1lib/chest.py diff --git a/.efrocachemap b/.efrocachemap index b26183bb..638e7a80 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -4103,42 +4103,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": "50d25937dea753465f4c37093252a506", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "9379bbb7a0e05265618072ce54b5150b", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "31d6adcaabf9c6626bba3840cad8e887", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "b9eac18a217bc7688b2d9144b2d712f6", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "e89c0ffde5d3e39aeb585544de2b5020", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "a265586bc3c0a3190ca43cd66bb786d1", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "bd5776295818b26d4933e0c95de5ccf5", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "202c243dea911c748673edebd01a6d27", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "80b9857bec34269d850fc0a196628ff1", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "1bf73ae24f6a33a1d8bffb901e655072", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "f81be586ef989a7a70d7d6db66e6d333", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "989eaa1a362d5584bca1c703f1fb297a", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "fc585782cef6148c70914988a0a6c4d6", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "7c51de6bbecaf69f9c447819891537ad", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "7eaae5174e50c2007af260ae6ca1db4c", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "daa74a12e1655dc73603dfcc7c14029f", - "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "144b0407bc4aa4fd889adeb9938f7c83", - "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "b546945362b7e71974acaf41ddd52c6e", - "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "144b0407bc4aa4fd889adeb9938f7c83", - "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "b546945362b7e71974acaf41ddd52c6e", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "f7a4426a032132569d5d61addd25932f", - "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "a8d2a7566260465abe9dd3cb75dcffc5", - "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "f7a4426a032132569d5d61addd25932f", - "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "a8d2a7566260465abe9dd3cb75dcffc5", - "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "f052e79cad083c718834345ef01015ca", - "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "5484ab93d479f69111fb2a83cafbf269", - "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "f052e79cad083c718834345ef01015ca", - "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "5484ab93d479f69111fb2a83cafbf269", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "cec1716fa0fd08cfb444b12fc3a1d90b", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "3a4f575fe91ac1944e6d5a2935b2e18d", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "48ef32642a8de1aabb0533ce692f5344", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "06af8dc996eaa17fc01b3ddbaef704ad", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "45437fa0d848b76b77aeae9bf3792389", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "9b32bec0aff1d5b2a56b2548d33ae9f8", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "9bb774ea2805698e5fa47cf0e8367f8a", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "57c81da4ae274d0694e470c61da13967", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "56e112f2611f640ef7bfea763fc0ea16", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "5e0cffe1c4d0df70f18d49ba9681cc72", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "4b8e5afb0166ad3d3116d04d494f9ec1", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "0285611c819eca94e4c2940a7a16c2b8", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "e33e669704eb9545a5297fca0f7e28f2", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "8485f8eff6706edb86e59ae6a5384f04", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "478942a23a7c1aca322ec1a3f891f7c7", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "af131198a42bceb8267bfbcbf27dd6a7", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "322b35f9163d234b268514b5c0d5b1c9", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "fb16e35043f457789acffad702d44a6a", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "de8ce2028178cf500e5e566d585ab7f5", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "2a8cb6b7dae73d61f6099d632bf65ea3", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "b1de7efc71e14c2dc90b7e174bb3c87a", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "2d41f6d86c37743f9816ca7cffe1d9d9", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "ae53c7ca3472750729b21e645a54b087", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "116735872e943dd1fa2c28784fdbdecc", + "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "c8c1f174ed35992eb71bf1ff22787f4e", + "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "67e1e5286f3fe64c9935e0e22d3fe443", + "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "c8c1f174ed35992eb71bf1ff22787f4e", + "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "67e1e5286f3fe64c9935e0e22d3fe443", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "c6f45e134e680f696df6d5f605fb8308", + "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "cfb87dbb40bfd0cdf4be9da5086265f8", + "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "c6f45e134e680f696df6d5f605fb8308", + "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "cfb87dbb40bfd0cdf4be9da5086265f8", + "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "b39713b9f3a9102fefff1f6d62a9fd82", + "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "1c2d42d75e8f975514b2e30f5602516d", + "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "b39713b9f3a9102fefff1f6d62a9fd82", + "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "1c2d42d75e8f975514b2e30f5602516d", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "7d1035b51bf53dc9021d4d4123433b53", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "a299482d1e4b0991326809fee71f0705", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "cf2b630383875f27554c0260578f09c6", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "a19bfc04eff7a19f1719488c441c9408", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "58ee8e1778866d7024ebe47de325ccb9", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "9d5b9c3c508803b95f9a8e747bfc5921", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "8a0d29889da01c8aa1613d481dbfa151", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "568ff8fd799e4b2f51b939716d75e23c", "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 650e3ba0..7e06064c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.37 (build 22139, api 9, 2024-12-19) +### 1.7.37 (build 22140, api 9, 2024-12-21) - 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 cea08571..e8820be2 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -2,7 +2,7 @@ cpplint==2.0.0 dmgbuild==1.6.2 filelock==3.16.1 furo==2024.8.6 -mypy==1.13.0 +mypy==1.14.0 pbxproj==4.2.1 pdoc==15.0.1 pur==7.3.3 diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json index 896bf162..ba8a9e43 100644 --- a/src/assets/.asset_manifest_public.json +++ b/src/assets/.asset_manifest_public.json @@ -378,6 +378,7 @@ "ba_data/python/bauiv1lib/__pycache__/achievements.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/appinvite.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/characterpicker.cpython-312.opt-1.pyc", + "ba_data/python/bauiv1lib/__pycache__/chest.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/colorpicker.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/config.cpython-312.opt-1.pyc", "ba_data/python/bauiv1lib/__pycache__/confirm.cpython-312.opt-1.pyc", @@ -430,6 +431,7 @@ "ba_data/python/bauiv1lib/achievements.py", "ba_data/python/bauiv1lib/appinvite.py", "ba_data/python/bauiv1lib/characterpicker.py", + "ba_data/python/bauiv1lib/chest.py", "ba_data/python/bauiv1lib/colorpicker.py", "ba_data/python/bauiv1lib/config.py", "ba_data/python/bauiv1lib/confirm.py", diff --git a/src/assets/Makefile b/src/assets/Makefile index 5bed78cc..a2fd0782 100644 --- a/src/assets/Makefile +++ b/src/assets/Makefile @@ -347,6 +347,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \ $(BUILD_DIR)/ba_data/python/bauiv1lib/achievements.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/appinvite.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/characterpicker.py \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/chest.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/colorpicker.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/config.py \ $(BUILD_DIR)/ba_data/python/bauiv1lib/confirm.py \ @@ -627,6 +628,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/achievements.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/appinvite.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/characterpicker.cpython-312.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/chest.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/colorpicker.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/config.cpython-312.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/bauiv1lib/__pycache__/confirm.cpython-312.opt-1.pyc \ diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py index cb659fab..9ab5baf5 100644 --- a/src/assets/ba_data/python/babase/__init__.py +++ b/src/assets/ba_data/python/babase/__init__.py @@ -134,6 +134,7 @@ from babase._apputils import ( garbage_collect, get_remote_app_name, AppHealthMonitor, + utc_now_cloud, ) from babase._cloud import CloudSubscription from babase._devconsole import ( @@ -360,6 +361,7 @@ __all__ = [ 'update_internal_logger_levels', 'user_agent_string', 'user_ran_commands', + 'utc_now_cloud', 'utf8_all', 'Vec3', 'vec3validate', diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py index c58bc9d1..5fd5d048 100644 --- a/src/assets/ba_data/python/babase/_app.py +++ b/src/assets/ba_data/python/babase/_app.py @@ -258,6 +258,12 @@ class App: """ return _babase.app_is_active() + @property + def mode(self) -> AppMode | None: + """The app's current mode.""" + assert _babase.in_logic_thread() + return self._mode + @property def asyncio_loop(self) -> asyncio.AbstractEventLoop: """The logic thread's asyncio event loop. diff --git a/src/assets/ba_data/python/babase/_apputils.py b/src/assets/ba_data/python/babase/_apputils.py index 1bd43ae2..29545f40 100644 --- a/src/assets/ba_data/python/babase/_apputils.py +++ b/src/assets/ba_data/python/babase/_apputils.py @@ -11,6 +11,7 @@ from functools import partial from dataclasses import dataclass from typing import TYPE_CHECKING, override +from efro.util import utc_now from efro.logging import LogLevel from efro.dataclassio import ioprepped, dataclass_to_json, dataclass_from_json @@ -18,11 +19,21 @@ import _babase from babase._appsubsystem import AppSubsystem if TYPE_CHECKING: + import datetime from typing import Any, TextIO, Callable import babase +def utc_now_cloud() -> datetime.datetime: + """Returns estimated utc time regardless of local clock settings. + + Applies offsets pulled from server communication/etc. + """ + # FIXME - do something smart here. + return utc_now() + + def is_browser_likely_available() -> bool: """Return whether a browser likely exists on the current device. diff --git a/src/assets/ba_data/python/babase/_general.py b/src/assets/ba_data/python/babase/_general.py index 8611615f..92f9707d 100644 --- a/src/assets/ba_data/python/babase/_general.py +++ b/src/assets/ba_data/python/babase/_general.py @@ -156,6 +156,9 @@ class _WeakCall: to wrap them in weakrefs manually if desired. """ + # Optimize performance a bit; we shouldn't need to be super dynamic. + __slots__ = ['_call', '_args', '_keywds'] + _did_invalid_call_warning = False def __init__(self, *args: Any, **keywds: Any) -> None: @@ -175,7 +178,7 @@ class _WeakCall: ' to avoid this warning.', stack_info=True, ) - self._did_invalid_call_warning = True + type(self)._did_invalid_call_warning = True self._call = args[0] self._args = args[1:] self._keywds = keywds @@ -214,6 +217,9 @@ class _Call: without keeping its object alive. """ + # Optimize performance a bit; we shouldn't need to be super dynamic. + __slots__ = ['_call', '_args', '_keywds'] + def __init__(self, *args: Any, **keywds: Any): """Instantiate a Call. @@ -280,6 +286,9 @@ class WeakMethod: free to die. If called with a dead target, is simply a no-op. """ + # Optimize performance a bit; we shouldn't need to be super dynamic. + __slots__ = ['_func', '_obj'] + def __init__(self, call: types.MethodType): assert isinstance(call, types.MethodType) self._func = call.__func__ diff --git a/src/assets/ba_data/python/baclassic/_appmode.py b/src/assets/ba_data/python/baclassic/_appmode.py index e9ff509d..2e5ad16b 100644 --- a/src/assets/ba_data/python/baclassic/_appmode.py +++ b/src/assets/ba_data/python/baclassic/_appmode.py @@ -15,10 +15,11 @@ import bauiv1 import _baclassic if TYPE_CHECKING: - from typing import Callable, Any + from typing import Callable, Any, Literal from efro.call import CallbackRegistration import bacommon.cloud + from bauiv1lib.chest import ChestWindow # ba_meta export babase.AppMode @@ -481,5 +482,42 @@ class ClassicAppMode(babase.AppMode): ) def _root_ui_chest_slot_pressed(self, index: int) -> None: - print(f'CHEST {index} PRESSED') - babase.screenmessage('UNDER CONSTRUCTION.') + from bauiv1lib.chest import ( + ChestWindow0, + ChestWindow1, + ChestWindow2, + ChestWindow3, + ) + from bauiv1lib.connectivity import wait_for_connectivity + + widgetid: Literal[ + 'chest_0_button', + 'chest_1_button', + 'chest_2_button', + 'chest_3_button', + ] + winclass: type[ChestWindow] + if index == 0: + widgetid = 'chest_0_button' + winclass = ChestWindow0 + elif index == 1: + widgetid = 'chest_1_button' + winclass = ChestWindow1 + elif index == 2: + widgetid = 'chest_2_button' + winclass = ChestWindow2 + elif index == 3: + widgetid = 'chest_3_button' + winclass = ChestWindow3 + else: + raise RuntimeError(f'Invalid index {index}') + + wait_for_connectivity( + on_connected=lambda: self._auxiliary_window_nav( + win_type=winclass, + win_create_call=lambda: winclass( + index=index, + origin_widget=bauiv1.get_special_widget(widgetid), + ), + ) + ) diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index 27a7bd5e..f3b6fcab 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 = 22139 +TARGET_BALLISTICA_BUILD = 22140 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 21ea7f6f..d2ab5c20 100644 --- a/src/assets/ba_data/python/baplus/_cloud.py +++ b/src/assets/ba_data/python/baplus/_cloud.py @@ -134,6 +134,24 @@ class CloudSubsystem(babase.AppSubsystem): ], ) -> None: ... + @overload + def send_message_cb( + self, + msg: bacommon.cloud.BSChestInfoMessage, + on_response: Callable[ + [bacommon.cloud.BSChestInfoResponse | Exception], None + ], + ) -> None: ... + + @overload + def send_message_cb( + self, + msg: bacommon.cloud.BSChestActionMessage, + on_response: Callable[ + [bacommon.cloud.BSChestActionResponse | Exception], None + ], + ) -> None: ... + def send_message_cb( self, msg: Message, diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py index 4feaa848..2195ad5c 100644 --- a/src/assets/ba_data/python/bauiv1/__init__.py +++ b/src/assets/ba_data/python/bauiv1/__init__.py @@ -96,6 +96,7 @@ from babase import ( timestring, UIScale, unlock_all_input, + utc_now_cloud, WeakCall, workspaces_in_use, ) @@ -238,6 +239,7 @@ __all__ = [ 'UIScale', 'UIV1AppSubsystem', 'unlock_all_input', + 'utc_now_cloud', 'WeakCall', 'widget', 'Widget', diff --git a/src/assets/ba_data/python/bauiv1lib/chest.py b/src/assets/ba_data/python/bauiv1lib/chest.py new file mode 100644 index 00000000..d8b7cf96 --- /dev/null +++ b/src/assets/ba_data/python/bauiv1lib/chest.py @@ -0,0 +1,348 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides chest related ui.""" + +from __future__ import annotations + +from typing import override, TYPE_CHECKING + +import bacommon.cloud +import bauiv1 as bui + +if TYPE_CHECKING: + pass + + +class ChestWindow(bui.MainWindow): + """Allows operations on a chest.""" + + def __del__(self) -> None: + print('~ChestWindow()') + + def __init__( + self, + index: int, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): + print('ChestWindow()') + + self._index = index + + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + self._width = 1050 if uiscale is bui.UIScale.SMALL else 850 + self._height = ( + 500 + if uiscale is bui.UIScale.SMALL + else 500 if uiscale is bui.UIScale.MEDIUM else 500 + ) + self._xoffs = 70 if uiscale is bui.UIScale.SMALL else 0 + self._yoffs = -42 if uiscale is bui.UIScale.SMALL else -25 + self._action_in_flight = False + self._open_now_button: bui.Widget | None = None + self._watch_ad_button: bui.Widget | None = None + + # The set of widgets we keep when doing a clear. + self._core_widgets: list[bui.Widget] = [] + + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height), + toolbar_visibility='menu_full', + scale=( + 1.55 + if uiscale is bui.UIScale.SMALL + else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.9 + ), + stack_offset=( + (0, 0) + if uiscale is bui.UIScale.SMALL + else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0) + ), + ), + transition=transition, + origin_widget=origin_widget, + ) + + self._core_widgets.append( + bui.textwidget( + parent=self._root_widget, + position=(0, self._height - 45 + self._yoffs), + size=(self._width, 25), + text=f'Chest #{self._index + 1}', + color=bui.app.ui_v1.title_color, + maxwidth=150.0, + h_align='center', + v_align='center', + ) + ) + + if uiscale is bui.UIScale.SMALL: + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + else: + btn = bui.buttonwidget( + parent=self._root_widget, + position=(self._xoffs + 50, self._height - 55 + self._yoffs), + size=(60, 55), + scale=0.8, + label=bui.charstr(bui.SpecialChar.BACK), + button_type='backSmall', + extra_touch_border_scale=2.0, + autoselect=True, + on_activate_call=self.main_window_back, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + self._core_widgets.append(btn) + + self._infotext = bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 200 + self._yoffs), + size=(0, 0), + text=bui.Lstr(resource='loadingText'), + maxwidth=700, + scale=0.7, + color=(0.6, 0.5, 0.6), + h_align='center', + v_align='center', + ) + self._core_widgets.append(self._infotext) + + plus = bui.app.plus + if plus is None: + self._error('Plus feature-set is not present.') + return + + if plus.accounts.primary is None: + self._error(bui.Lstr(resource='notSignedInText')) + return + + # Start by showing info/options for our target chest. Note that + # we always ask the server for these values even though we may + # have them through our appmode subscription which updates the + # chest UI. This is because the wait_for_connectivity() + # mechanism will often bring our window up a split second before + # the chest subscription receives its first values which would + # lead us to incorrectly think there is no chest there. If we + # want to optimize this in the future we could perhaps use local + # values only if there is a chest present in them. + assert not self._action_in_flight + self._action_in_flight = True + with plus.accounts.primary: + plus.cloud.send_message_cb( + bacommon.cloud.BSChestInfoMessage(chest_id=str(self._index)), + on_response=bui.WeakCall(self._on_chest_info_response), + ) + + def _on_chest_info_response( + self, response: bacommon.cloud.BSChestInfoResponse | Exception + ) -> None: + assert self._action_in_flight # Should be us. + self._action_in_flight = False + + if isinstance(response, Exception): + self._error( + bui.Lstr(resource='internal.unavailableNoConnectionText') + ) + return + + if response.chest is None: + self._error('Would show general info about chests.') + return + + self.show_chest_actions(response.chest) + + def _on_chest_action_response( + self, response: bacommon.cloud.BSChestActionResponse | Exception + ) -> None: + assert self._action_in_flight # Should be us. + self._action_in_flight = False + + # Communication/local error: + if isinstance(response, Exception): + self._error( + bui.Lstr(resource='internal.unavailableNoConnectionText') + ) + return + + # Server-side error: + if response.error is not None: + self._error(bui.Lstr(translate=('serverResponses', response.error))) + return + + # If there's contents listed in the response, show them. + if response.contents is not None: + print('WOULD SHOW CONTENTS:', response.contents) + else: + # Otherwise we're done here; just close out our UI. + self.main_window_back() + + def show_chest_actions( + self, chest: bacommon.cloud.BSChestInfoResponse.Chest + ) -> None: + """Show state for our chest.""" + # pylint: disable=cyclic-import + from baclassic import ClassicAppMode + + # We expect to be run under classic. + mode = bui.app.mode + if not isinstance(mode, ClassicAppMode): + self._error('Classic app mode not active.') + return + + now = bui.utc_now_cloud() + secs_till_open = max(0.0, (chest.unlock_time - now).total_seconds()) + tstr = bui.timestring(secs_till_open, centi=False) + + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 120 + self._yoffs), + size=(0, 0), + text=tstr, + maxwidth=700, + scale=0.7, + color=(0.6, 0.5, 0.6), + h_align='center', + v_align='center', + ) + self._open_now_button = bui.buttonwidget( + parent=self._root_widget, + position=( + self._width * 0.5 - 200, + self._height - 250 + self._yoffs, + ), + size=(150, 100), + label=f'OPEN NOW FOR {chest.unlock_tokens} TOKENS', + button_type='square', + autoselect=True, + on_activate_call=bui.WeakCall( + self._open_now_press, chest.unlock_tokens + ), + ) + + self._watch_ad_button = bui.buttonwidget( + parent=self._root_widget, + position=( + self._width * 0.5 + 50, + self._height - 250 + self._yoffs, + ), + size=(150, 100), + label='WATCH AN AD TO REDUCE WAIT', + button_type='square', + autoselect=True, + on_activate_call=bui.WeakCall(self._watch_ad_press), + ) + bui.textwidget(edit=self._infotext, text='') + + def _open_now_press(self, token_payment: int) -> None: + + # Allow only one in-flight action at once. + if self._action_in_flight: + bui.screenmessage( + bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0) + ) + bui.getsound('error').play() + return + + plus = bui.app.plus + assert plus is not None + + if plus.accounts.primary is None: + self._error(bui.Lstr(resource='notSignedInText')) + return + + self._action_in_flight = True + with plus.accounts.primary: + plus.cloud.send_message_cb( + bacommon.cloud.BSChestActionMessage( + chest_id=str(self._index), + action=bacommon.cloud.BSChestActionMessage.Action.UNLOCK, + token_payment=token_payment, + ), + on_response=bui.WeakCall(self._on_chest_action_response), + ) + + # Convey that something is in progress. + if self._open_now_button: + bui.buttonwidget(edit=self._open_now_button, label='...') + + def _watch_ad_press(self) -> None: + + # Allow only one in-flight action at once. + if self._action_in_flight: + bui.screenmessage('ERR', color=(1, 0, 0)) + bui.getsound('error').play() + return + + plus = bui.app.plus + assert plus is not None + + if plus.accounts.primary is None: + self._error(bui.Lstr(resource='notSignedInText')) + return + + self._action_in_flight = True + with plus.accounts.primary: + plus.cloud.send_message_cb( + bacommon.cloud.BSChestActionMessage( + chest_id=str(self._index), + action=bacommon.cloud.BSChestActionMessage.Action.AD, + token_payment=0, + ), + on_response=bui.WeakCall(self._on_chest_action_response), + ) + + # Convey that something is in progress. + if self._watch_ad_button: + bui.buttonwidget(edit=self._watch_ad_button, label='...') + + def _reset(self) -> None: + """Clear all non-permanent widgets.""" + for widget in self._root_widget.get_children(): + if widget not in self._core_widgets: + widget.delete() + + def _error(self, msg: str | bui.Lstr) -> None: + """Put ourself in an error state with a visible error message.""" + self._reset() + bui.textwidget(edit=self._infotext, text=msg, color=(1, 0, 0)) + + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # Pull anything we need from self out here; if we do it in the + # lambda we keep self alive which is bad. + index = self._index + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + index=index, transition=transition, origin_widget=origin_widget + ) + ) + + +# Slight hack: we define different classes for our different chest slots +# so that the default UI behavior is to replace each other when +# different ones are pressed. If they are all the same class then the +# default behavior for such presses is to toggle the existing one back +# off. + + +class ChestWindow0(ChestWindow): + """Child class of ChestWindow for slighty hackish reasons.""" + + +class ChestWindow1(ChestWindow): + """Child class of ChestWindow for slighty hackish reasons.""" + + +class ChestWindow2(ChestWindow): + """Child class of ChestWindow for slighty hackish reasons.""" + + +class ChestWindow3(ChestWindow): + """Child class of ChestWindow for slighty hackish reasons.""" diff --git a/src/assets/ba_data/python/bauiv1lib/inventory.py b/src/assets/ba_data/python/bauiv1lib/inventory.py index 508ccfba..8fbafa27 100644 --- a/src/assets/ba_data/python/bauiv1lib/inventory.py +++ b/src/assets/ba_data/python/bauiv1lib/inventory.py @@ -28,7 +28,7 @@ class InventoryWindow(bui.MainWindow): if uiscale is bui.UIScale.SMALL else 530 if uiscale is bui.UIScale.MEDIUM else 600 ) - x_offs = 70 if uiscale is bui.UIScale.SMALL else 0 + xoffs = 70 if uiscale is bui.UIScale.SMALL else 0 yoffs = -45 if uiscale is bui.UIScale.SMALL else 0 super().__init__( @@ -71,7 +71,7 @@ class InventoryWindow(bui.MainWindow): else: btn = bui.buttonwidget( parent=self._root_widget, - position=(x_offs + 50, height - 55 + yoffs), + position=(xoffs + 50, height - 55 + yoffs), size=(60, 55), scale=0.8, label=bui.charstr(bui.SpecialChar.BACK), diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index c2ce720b..55d541d5 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 = 22139; +const int kEngineBuildNumber = 22140; 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 bdc6eee5..a1767105 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 @@ -2607,6 +2607,10 @@ static PyMethodDef PyGetSpecialWidgetDef = { " 'trophy_meter'," " 'level_meter'," " 'overlay_stack'," + " 'chest_0_button'," + " 'chest_1_button'," + " 'chest_2_button'," + " 'chest_3_button'," " ]) -> bauiv1.Widget\n" "\n" "(internal)", diff --git a/src/ballistica/ui_v1/widget/root_widget.cc b/src/ballistica/ui_v1/widget/root_widget.cc index 5ab188d8..95e66c56 100644 --- a/src/ballistica/ui_v1/widget/root_widget.cc +++ b/src/ballistica/ui_v1/widget/root_widget.cc @@ -1335,6 +1335,14 @@ auto RootWidget::GetSpecialWidget(const std::string& s) const -> Widget* { return level_meter_button_ ? level_meter_button_->widget.get() : nullptr; } else if (s == "overlay_stack") { return overlay_stack_widget_; + } else if (s == "chest_0_button") { + return chest_0_button_->widget.get(); + } else if (s == "chest_1_button") { + return chest_1_button_->widget.get(); + } else if (s == "chest_2_button") { + return chest_2_button_->widget.get(); + } else if (s == "chest_3_button") { + return chest_3_button_->widget.get(); } return nullptr; } @@ -1462,7 +1470,7 @@ void RootWidget::SetChests(const std::string& chest_0_appearance, assert(b); if (appearance == "") { b->widget->set_color(0.473f, 0.44f, 0.583f); - b->widget->set_opacity(0.5f); + b->widget->set_opacity(have_chests ? 1.0f : 0.5f); b->width = b->height = 80.0f; b->y = have_chests ? 44.0f : 0.0f; { diff --git a/tools/bacommon/cloud.py b/tools/bacommon/cloud.py index 6a426dd0..fbd28701 100644 --- a/tools/bacommon/cloud.py +++ b/tools/bacommon/cloud.py @@ -347,8 +347,6 @@ class BSClassicAccountLiveData: BSClassicChestAppearance, IOAttrs('a', enum_fallback=BSClassicChestAppearance.UNKNOWN), ] - create_time: Annotated[datetime.datetime, IOAttrs('c')] - unlock_tokens: Annotated[int, IOAttrs('tk')] unlock_time: Annotated[datetime.datetime, IOAttrs('t')] ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')] @@ -433,6 +431,85 @@ class BSInboxRequestResponse(Response): error: Annotated[str | None, IOAttrs('e')] = None +@ioprepped +@dataclass +class BSChestInfoMessage(Message): + """Request info about a chest.""" + + chest_id: Annotated[str, IOAttrs('i')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [BSChestInfoResponse] + + +@ioprepped +@dataclass +class BSChestInfoResponse(Response): + """Here's that inbox contents you asked for, boss.""" + + @dataclass + class Chest: + """A lovely chest.""" + + appearance: Annotated[ + BSClassicChestAppearance, + IOAttrs('a', enum_fallback=BSClassicChestAppearance.UNKNOWN), + ] + + # How much to unlock *now*. + unlock_tokens: Annotated[int, IOAttrs('tk')] + + # When unlocks on its own. + unlock_time: Annotated[datetime.datetime, IOAttrs('t')] + + # Are ads allowed now? + ad_allow: Annotated[bool, IOAttrs('aa')] + + chest: Annotated[Chest | None, IOAttrs('c')] + + +@ioprepped +@dataclass +class BSChestActionMessage(Message): + """Request action about a chest.""" + + class Action(Enum): + """Types of actions we can request.""" + + # Unlocking (for free or with tokens). + UNLOCK = 'u' + + # Watched an ad to reduce wait. + AD = 'ad' + + action: Annotated[Action, IOAttrs('a')] + + # Tokens we are paying (only applies to unlock). + token_payment: Annotated[int, IOAttrs('t')] + + chest_id: Annotated[str, IOAttrs('i')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [BSChestActionResponse] + + +@ioprepped +@dataclass +class BSChestActionResponse(Response): + """Here's the results of that action you asked for, boss.""" + + # If present, signifies the chest has been opened and we should show + # the user this stuff that was in it. + contents: Annotated[list[str] | None, IOAttrs('c')] = None + + # Printable error if something goes wrong. + error: Annotated[str | None, IOAttrs('e')] = None + + class BSInboxEntryProcessType(Enum): """Types of processing we can ask for."""