diff --git a/bombsquad_server b/bombsquad_server index 2431dfc..c82f94d 100644 --- a/bombsquad_server +++ b/bombsquad_server @@ -1,9 +1,8 @@ -#!/usr/bin/env -S python3.13 -OB +#!/usr/bin/env python3.13 # Released under the MIT License. See LICENSE for details. # # pylint: disable=too-many-lines """BallisticaKit server manager.""" - from __future__ import annotations import os @@ -12,7 +11,6 @@ import time import json import signal import tomllib -import logging import subprocess import platform from pathlib import Path @@ -27,39 +25,19 @@ sys.path += [ str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')), ] -from bacommon.servermanager import ServerConfig, StartServerModeCommand -from efro.dataclassio import dataclass_from_dict, dataclass_validate -from efro.error import CleanError from efro.terminal import Clr +from efro.error import CleanError +from efro.dataclassio import dataclass_from_dict, dataclass_validate +from bacommon.servermanager import ServerConfig, StartServerModeCommand if TYPE_CHECKING: from types import FrameType from bacommon.servermanager import ServerCommand -VERSION_STR = '1.3.5' +VERSION_STR = '1.3.2' # Version history: # -# 1.3.5 -# -# - Minor updates accounting for the fact that the game binary no longer -# bundles .pyc files but rather generates them itself in a dedicated -# directory. So we now run this wrapper with bytecode disabled (-B) -# to keep the source tree tidy; the wrapper isn't performance sensitive -# so this should have no impact on performance. -# -# 1.3.4 -# -# - Updated to use Python 3.13. -# -# 1.3.3 -# -# - Added log_levels dict in server config for setting levels on -# individual loggers within the server binary. Can be useful for -# debugging issues or just keeping better track of what the server is -# up to. Check the logging tab in the dev console in the graphical -# client to learn which loggers are available. -# # 1.3.2 # # - Updated to use Python 3.12. @@ -435,7 +413,8 @@ class ServerManagerApp: raise CleanError('Expected a config path as next arg.') path = sys.argv[i + 1] if not os.path.exists(path): - raise CleanError(f"Supplied path does not exist: '{path}'.") + raise CleanError( + f"Supplied path does not exist: '{path}'.") # We need an abs path because we may be in a different # cwd currently than we will be during the run. self._user_provided_config_path = os.path.abspath(path) @@ -723,14 +702,6 @@ class ServerManagerApp: # instead? os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1' - # Set particular things that can *only* be passed as args and - # not config vals (because they need to be handled by the binary - # before spinning up Python or whatnot). - extra_args: list[str] = [] - - if self._config.dont_write_bytecode: - extra_args += ['--dont-write-bytecode'] - # Set an environment var to change the device name. Device name # is used while making connection with master server, # cloud-console recognize us with this name. @@ -750,7 +721,7 @@ class ServerManagerApp: # Launch! try: self._subprocess = subprocess.Popen( - [binary_name, '--config-dir', self._ba_root_path] + extra_args, + [binary_name, '--config-dir', self._ba_root_path], stdin=subprocess.PIPE, cwd='dist', ) @@ -828,43 +799,26 @@ class ServerManagerApp: bincfg = {} # Some of our config values translate directly into the + # ballisticakit config file; the rest we pass at runtime. bincfg['Port'] = int(os.environ.get('PORT', self._config.port)) bincfg['Auto Balance Teams'] = self._config.auto_balance_teams bincfg['Show Tutorial'] = self._config.show_tutorial - binkey = 'SceneV1 Host Protocol' if self._config.protocol_version is not None: - bincfg[binkey] = self._config.protocol_version - elif binkey in bincfg: - del bincfg[binkey] - - binkey = 'Custom Team Names' + bincfg['SceneV1 Host Protocol'] = self._config.protocol_version if self._config.team_names is not None: - bincfg[binkey] = self._config.team_names - elif binkey in bincfg: - del bincfg[binkey] + bincfg['Custom Team Names'] = self._config.team_names + elif 'Custom Team Names' in bincfg: + del bincfg['Custom Team Names'] - binkey = 'Custom Team Colors' if self._config.team_colors is not None: - bincfg[binkey] = self._config.team_colors - elif binkey in bincfg: - del bincfg[binkey] + bincfg['Custom Team Colors'] = self._config.team_colors + elif 'Custom Team Colors' in bincfg: + del bincfg['Custom Team Colors'] bincfg['Idle Exit Minutes'] = self._config.idle_exit_minutes - - binkey = 'Log Levels' - if self._config.log_levels is not None: - # Users supply us log level names like NOTSET; convert those - # to numeric vals which the engine expects. - bincfg[binkey] = { - key: logging.getLevelName(val) - for key, val in self._config.log_levels.items() - } - elif binkey in bincfg: - del bincfg[binkey] - with open(cfgpath, 'w', encoding='utf-8') as outfile: outfile.write(json.dumps(bincfg)) diff --git a/dist/ba_data/python/babase/__init__.py b/dist/ba_data/python/babase/__init__.py index 43a4f55..b1e689d 100644 --- a/dist/ba_data/python/babase/__init__.py +++ b/dist/ba_data/python/babase/__init__.py @@ -7,7 +7,6 @@ directly. Instead one should use purpose-built packages such as :mod:`bascenev1` or :mod:`bauiv1` which themselves import various functionality from here and reexpose it in a more focused way. """ - # pylint: disable=redefined-builtin # ba_meta require api 9 @@ -16,14 +15,14 @@ functionality from here and reexpose it in a more focused way. # from other modules/packages. Code *within* this package should import # things from this package's submodules directly to reduce the chance of # dependency loops. The exception is TYPE_CHECKING blocks and -# annotations - since those aren't evaluated at runtime, it is cleaner -# looking to use top level names directly. +# annotations since those aren't evaluated at runtime. import _babase from _babase import ( add_clean_frame_callback, allows_ticket_sales, android_get_external_files_dir, + app_instance_uuid, appname, appnameupper, apptime, @@ -126,7 +125,6 @@ from _babase import ( ) from babase._accountv2 import AccountV2Handle, AccountV2Subsystem -from babase._analytics import AnalyticsSubsystem from babase._app import App, AppState from babase._appcomponent import AppComponentSubsystem from babase._appconfig import commit_app_config @@ -136,71 +134,68 @@ from babase._appsubsystem import AppSubsystem from babase._appmodeselector import AppModeSelector from babase._appconfig import AppConfig from babase._apputils import ( - AppHealthSubsystem, - get_remote_app_name, handle_leftover_v1_cloud_log_file, is_browser_likely_available, + get_remote_app_name, + AppHealthSubsystem, utc_now_cloud, ) from babase._cloud import CloudSubscription from babase._devconsole import ( DevConsoleButtonDef, - DevConsoleSubsystem, DevConsoleTab, DevConsoleTabEntry, + DevConsoleSubsystem, ) from babase._discord import DiscordSubsystem from babase._emptyappmode import EmptyAppMode from babase._error import ( - ActivityNotFoundError, - ActorNotFoundError, ContextError, - DelegateNotFoundError, - InputDeviceNotFoundError, - MapNotFoundError, - NodeNotFoundError, NotFoundError, PlayerNotFoundError, - SessionNotFoundError, SessionPlayerNotFoundError, - SessionTeamNotFoundError, - TeamNotFoundError, + NodeNotFoundError, + ActorNotFoundError, + InputDeviceNotFoundError, WidgetNotFoundError, + ActivityNotFoundError, + TeamNotFoundError, + MapNotFoundError, + SessionTeamNotFoundError, + SessionNotFoundError, + DelegateNotFoundError, ) from babase._gc import GarbageCollectionSubsystem from babase._general import ( - AppTime, - Call, - CallPartial, - CallStrict, DisplayTime, - Existable, + AppTime, WeakCall, - WeakCallPartial, - WeakCallStrict, + Call, existing, - get_type_name, - getclass, - storagename, + Existable, verify_object_death, + storagename, + getclass, + get_type_name, ) -from babase._language import LanguageSubsystem, Lstr +from babase._language import Lstr, LanguageSubsystem from babase._locale import LocaleSubsystem from babase._logging import ( + balog, accountlog, applog, - balog, lifecyclelog, netlog, uilog, ) from babase._login import LoginAdapter, LoginInfo + from babase._mgen.enums import ( - InputType, Permission, - QuitType, SpecialChar, + InputType, UIScale, + QuitType, ) from babase._math import normalized_color, is_point_in_box, vec3validate from babase._meta import MetadataSubsystem @@ -221,7 +216,6 @@ __all__ = [ 'ActorNotFoundError', 'allows_ticket_sales', 'add_clean_frame_callback', - 'AnalyticsSubsystem', 'android_get_external_files_dir', 'app', 'App', @@ -233,6 +227,7 @@ __all__ = [ 'AppIntentExec', 'AppMode', 'AppState', + 'app_instance_uuid', 'applog', 'appname', 'appnameupper', @@ -247,8 +242,6 @@ __all__ = [ 'atexit', 'balog', 'Call', - 'CallPartial', - 'CallStrict', 'fullscreen_control_available', 'fullscreen_control_get', 'fullscreen_control_key_shortcut', @@ -398,8 +391,6 @@ __all__ = [ 'vec3validate', 'verify_object_death', 'WeakCall', - 'WeakCallPartial', - 'WeakCallStrict', 'WidgetNotFoundError', 'workspaces_in_use', 'WorkspaceSubsystem', diff --git a/dist/ba_data/python/babase/_accountv2.py b/dist/ba_data/python/babase/_accountv2.py index 657432a..0a24d40 100644 --- a/dist/ba_data/python/babase/_accountv2.py +++ b/dist/ba_data/python/babase/_accountv2.py @@ -4,11 +4,9 @@ from __future__ import annotations -import time import hashlib import logging from functools import partial -from dataclasses import dataclass from typing import TYPE_CHECKING, assert_never from efro.error import CommunicationError @@ -21,8 +19,6 @@ import _babase if TYPE_CHECKING: from typing import Any, Callable - import bacommon.cloud - from babase._login import LoginAdapter, LoginInfo @@ -66,9 +62,6 @@ class AccountV2Subsystem: Callable[[AccountV2Handle | None], None] ] = CallbackSet() - # Request state per global-app-instance-id - self._auth_requests: dict[str, _AuthRequest] = {} - adapter: LoginAdapter if _babase.using_google_play_game_services(): adapter = LoginAdapterGPGS() @@ -111,9 +104,6 @@ class AccountV2Subsystem: """ assert _babase.in_logic_thread() - # Blow away any outstanding auth-requests. - self._auth_requests = {} - # Inform the base layer of new names/etc. if account is not None: _babase.set_account_sign_in_state(True, account.tag) @@ -211,78 +201,6 @@ class AccountV2Subsystem: self._initial_sign_in_completed = True _babase.app.on_initial_sign_in_complete() - def auth_request( - self, global_app_instance_id: str - ) -> None | tuple[bool, str]: - """Start/process an auth request.""" - import bacommon.cloud - - assert _babase.in_logic_thread() - plus = _babase.app.plus - assert plus is not None - - now = time.monotonic() - - # If there are any expired ones, do a prune pass. - if any(r.expire_time <= now for r in self._auth_requests.values()): - self._auth_requests = { - rid: r - for rid, r in self._auth_requests.items() - if r.expire_time > now - } - - auth_request = self._auth_requests.get(global_app_instance_id) - - # If we find no attempt in progress, kick one off. - if ( - auth_request is None - and plus.cloud.connected - and self.primary is not None - ): - # print('SENDING AUTH REQUEST') - auth_request = self._auth_requests[global_app_instance_id] = ( - _AuthRequest(expire_time=now + 10.0, error=None, token=None) - ) - with self.primary: - plus.cloud.send_message_cb( - bacommon.cloud.AuthRequestMessage(global_app_instance_id), - on_response=partial( - self._on_auth_request_response, auth_request - ), - ) - - # If we found results, return them. - if auth_request is None: - return None - if auth_request.error is not None: - assert auth_request.token is None - return (False, auth_request.error) - if auth_request.token is not None: - assert auth_request.error is None - return (True, auth_request.token) - # No error or token; its still in flight. - return None - - def _on_auth_request_response( - self, - auth_request: _AuthRequest, - response: bacommon.cloud.AuthRequestResponse | Exception, - ) -> None: - assert _babase.in_logic_thread() - - assert auth_request.error is None - assert auth_request.token is None - - if isinstance(response, Exception): - auth_request.error = 'An error has occurred.' - else: - # print('SETTING AUTH RESPONSE') - auth_request.error = response.error - auth_request.token = response.token - # Make sure this sticks around for long enough to complete - # the connection. - auth_request.expire_time = time.monotonic() + 10.0 - @staticmethod def _hashstr(val: str) -> str: md5 = hashlib.md5() @@ -583,10 +501,3 @@ class AccountV2Handle: This allows cloud messages to be sent on our behalf. """ - - -@dataclass -class _AuthRequest: - expire_time: float - error: str | None - token: str | None diff --git a/dist/ba_data/python/babase/_analytics.py b/dist/ba_data/python/babase/_analytics.py deleted file mode 100644 index 93aadac..0000000 --- a/dist/ba_data/python/babase/_analytics.py +++ /dev/null @@ -1,73 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Analytics functionality.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import bacommon.cloud -import _babase - -from babase._logging import balog - -if TYPE_CHECKING: - from bacommon.analytics import AnalyticsEvent - - -class AnalyticsSubsystem: - """Subsystem for wrangling analytics. - - Access the single shared instance of this class via the - :attr:`~babase.App.analytics` attr on the :class:`~babase.App` - class. - """ - - def __init__(self) -> None: - self.enabled: bool = True - - def submit_event(self, event: AnalyticsEvent) -> None: - """Submit an event. - - Should only be called from the logic thread. - """ - - if not _babase.in_logic_thread(): - balog.error( - 'submit_event() called outside logic thread.', stack_info=True - ) - return - - # No-op if analytics are disabled or we don't have plus. - if not self.enabled: - return - - plus = _babase.app.plus - if plus is None: - return - - # Currently just no-op if it seems we're not connected. Perhaps - # in the future we'd want to save these and submit later when we - # are. - if not plus.cloud.is_connected(): - return - - # Just kick off an immediate send in the bg with or without - # account info. - account = plus.accounts.primary - if account is None: - plus.cloud.send_message_cb( - bacommon.cloud.AnalyticsEventMessage(event), - on_response=self._on_analytics_message_response, - ) - else: - with account: - plus.cloud.send_message_cb( - bacommon.cloud.AnalyticsEventMessage(event), - on_response=self._on_analytics_message_response, - ) - - def _on_analytics_message_response( - self, response: Exception | None - ) -> None: - pass diff --git a/dist/ba_data/python/babase/_app.py b/dist/ba_data/python/babase/_app.py index 3807b88..8a2673f 100644 --- a/dist/ba_data/python/babase/_app.py +++ b/dist/ba_data/python/babase/_app.py @@ -2,12 +2,10 @@ # # pylint: disable=too-many-lines """Functionality related to the high level state of the app.""" - from __future__ import annotations import os import time -import asyncio import logging from enum import Enum from functools import partial @@ -30,12 +28,12 @@ from babase._appmodeselector import AppModeSelector from babase._appintent import AppIntentDefault, AppIntentExec from babase._stringedit import StringEditSubsystem from babase._devconsole import DevConsoleSubsystem -from babase._analytics import AnalyticsSubsystem from babase._appconfig import AppConfig from babase._logging import lifecyclelog, applog from babase._gc import GarbageCollectionSubsystem if TYPE_CHECKING: + import asyncio from typing import Any, Callable, Coroutine, Generator, Awaitable from concurrent.futures import Future @@ -154,9 +152,6 @@ class App: #: Subsystem for wrangling the dev-console UI. self.devconsole: DevConsoleSubsystem = DevConsoleSubsystem() - #: Subsystem for wrangling analytics. - self.analytics: AnalyticsSubsystem = AnalyticsSubsystem() - #: Incremented each time the app leaves the #: :attr:`~babase.AppState.SUSPENDED` state. This can be a simple #: way to determine if network data should be refreshed/etc. @@ -242,6 +237,7 @@ class App: loop. Hopefully this situation will be improved in the future with a unified event loop. """ + assert _babase.in_logic_thread() assert self._asyncio_loop is not None return self._asyncio_loop @@ -285,9 +281,8 @@ class App: def mode_selector(self, selector: babase.AppModeSelector) -> None: self._mode_selector = selector - def _on_task_done(self, task: Any) -> None: + def _on_task_done(self, task: asyncio.Task) -> None: # Report any errors that occurred. - assert isinstance(task, asyncio.Task) try: exc = task.exception() if exc is not None: @@ -1033,6 +1028,7 @@ class App: ) async def _shutdown(self) -> None: + import asyncio _babase.lock_all_input() try: @@ -1058,16 +1054,13 @@ class App: self, coro: Coroutine[None, None, None] ) -> None: """Run a shutdown task; report errors and abort if taking too long.""" + import asyncio task = asyncio.create_task(coro) try: await asyncio.wait_for(task, self.SHUTDOWN_TASK_TIMEOUT_SECONDS) - except TimeoutError: - # Log simple error message if it times out. - logging.error('Timed out waiting for shutdown task %s.', coro) except Exception: - # Go with full ugly stack trace for anything unexpected. - logging.exception('Error in shutdown task %s.', coro) + logging.exception('Error in shutdown task (%s).', coro) def _on_suspend(self) -> None: """Called when the app goes to a suspended state.""" @@ -1143,6 +1136,7 @@ class App: ) async def _wait_for_shutdown_suppressions(self) -> None: + import asyncio # Spin and wait for anything blocking shutdown to complete. starttime = _babase.apptime() @@ -1159,6 +1153,7 @@ class App: ) async def _fade_and_shutdown_graphics(self) -> None: + import asyncio # Kick off a short fade and give it time to complete. lifecyclelog.info('fade-and-shutdown-graphics begin') @@ -1194,6 +1189,7 @@ class App: lifecyclelog.info('fade-and-shutdown-graphics end') async def _fade_and_shutdown_audio(self) -> None: + import asyncio # Tell the audio system to go down and give it a bit of # time to do so gracefully. diff --git a/dist/ba_data/python/babase/_appcomponent.py b/dist/ba_data/python/babase/_appcomponent.py index 1d92ba0..e02ea02 100644 --- a/dist/ba_data/python/babase/_appcomponent.py +++ b/dist/ba_data/python/babase/_appcomponent.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides the AppComponent class.""" - from __future__ import annotations from typing import TYPE_CHECKING, cast diff --git a/dist/ba_data/python/babase/_appconfig.py b/dist/ba_data/python/babase/_appconfig.py index 89716b4..e331071 100644 --- a/dist/ba_data/python/babase/_appconfig.py +++ b/dist/ba_data/python/babase/_appconfig.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides the AppConfig class.""" - from __future__ import annotations from typing import TYPE_CHECKING @@ -11,7 +10,7 @@ import _babase if TYPE_CHECKING: from typing import Any -_g_pending_apply = False +_g_pending_apply = False # pylint: disable=invalid-name class AppConfig(dict): diff --git a/dist/ba_data/python/babase/_appintent.py b/dist/ba_data/python/babase/_appintent.py index 3b09079..d6d80e9 100644 --- a/dist/ba_data/python/babase/_appintent.py +++ b/dist/ba_data/python/babase/_appintent.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides AppIntent functionality.""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/babase/_appmode.py b/dist/ba_data/python/babase/_appmode.py index ffb27ae..48f19f9 100644 --- a/dist/ba_data/python/babase/_appmode.py +++ b/dist/ba_data/python/babase/_appmode.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides AppMode functionality.""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/babase/_appmodeselector.py b/dist/ba_data/python/babase/_appmodeselector.py index 298fc9b..bd4da08 100644 --- a/dist/ba_data/python/babase/_appmodeselector.py +++ b/dist/ba_data/python/babase/_appmodeselector.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Contains AppModeSelector base class.""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/babase/_appsubsystem.py b/dist/ba_data/python/babase/_appsubsystem.py index 6af6e86..11efcf4 100644 --- a/dist/ba_data/python/babase/_appsubsystem.py +++ b/dist/ba_data/python/babase/_appsubsystem.py @@ -1,11 +1,11 @@ # Released under the MIT License. See LICENSE for details. # """Provides the AppSubsystem base class.""" - from __future__ import annotations from typing import TYPE_CHECKING + if TYPE_CHECKING: from babase import UIScale diff --git a/dist/ba_data/python/babase/_apputils.py b/dist/ba_data/python/babase/_apputils.py index 9bf7326..7915970 100644 --- a/dist/ba_data/python/babase/_apputils.py +++ b/dist/ba_data/python/babase/_apputils.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Utility functionality related to the overall operation of the app.""" - from __future__ import annotations import os diff --git a/dist/ba_data/python/babase/_asyncio.py b/dist/ba_data/python/babase/_asyncio.py index 89f9ae7..3d49012 100644 --- a/dist/ba_data/python/babase/_asyncio.py +++ b/dist/ba_data/python/babase/_asyncio.py @@ -23,8 +23,8 @@ if TYPE_CHECKING: import babase # Our timer and event loop for the ballistica logic thread. -_g_asyncio_timer: babase.AppTimer | None = None -_g_asyncio_event_loop: asyncio.AbstractEventLoop | None = None +_asyncio_timer: babase.AppTimer | None = None +_asyncio_event_loop: asyncio.AbstractEventLoop | None = None DEBUG_TIMING = os.environ.get('BA_DEBUG_TIMING') == '1' @@ -46,12 +46,12 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: except RuntimeError: pass - global _g_asyncio_event_loop - _g_asyncio_event_loop = asyncio.new_event_loop() - _g_asyncio_event_loop.set_default_executor(babase.app.threadpool) + global _asyncio_event_loop + _asyncio_event_loop = asyncio.new_event_loop() + _asyncio_event_loop.set_default_executor(babase.app.threadpool) # Try to avoid reference loops from exceptions. - _g_asyncio_event_loop.set_exception_handler(_exception_handler) + _asyncio_event_loop.set_exception_handler(_exception_handler) # Ideally we should integrate asyncio into our C++ Thread class's # low level event loop so that asyncio timers/sockets/etc. could @@ -62,10 +62,10 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: # See https://stackoverflow.com/questions/29782377/ # is-it-possible-to-run-only-a-single-step-of-the-asyncio-event-loop def run_cycle() -> None: - assert _g_asyncio_event_loop is not None - _g_asyncio_event_loop.call_soon(_g_asyncio_event_loop.stop) + assert _asyncio_event_loop is not None + _asyncio_event_loop.call_soon(_asyncio_event_loop.stop) starttime = time.monotonic() if DEBUG_TIMING else 0 - _g_asyncio_event_loop.run_forever() + _asyncio_event_loop.run_forever() endtime = time.monotonic() if DEBUG_TIMING else 0 # Let's aim to have nothing take longer than 1/120 of a second. @@ -79,21 +79,21 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: warn_time, ) - global _g_asyncio_timer - _g_asyncio_timer = _babase.AppTimer(1.0 / 30.0, run_cycle, repeat=True) + global _asyncio_timer + _asyncio_timer = _babase.AppTimer(1.0 / 30.0, run_cycle, repeat=True) if bool(False): async def aio_test() -> None: print('TEST AIO TASK STARTING') - assert _g_asyncio_event_loop is not None - assert asyncio.get_running_loop() is _g_asyncio_event_loop + assert _asyncio_event_loop is not None + assert asyncio.get_running_loop() is _asyncio_event_loop await asyncio.sleep(2.0) print('TEST AIO TASK ENDING') - _testtask = _g_asyncio_event_loop.create_task(aio_test()) + _testtask = _asyncio_event_loop.create_task(aio_test()) - return _g_asyncio_event_loop + return _asyncio_event_loop def _exception_handler( diff --git a/dist/ba_data/python/babase/_cloud.py b/dist/ba_data/python/babase/_cloud.py index 43cbd2d..9ea0662 100644 --- a/dist/ba_data/python/babase/_cloud.py +++ b/dist/ba_data/python/babase/_cloud.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Cloud related functionality.""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/babase/_devconsole.py b/dist/ba_data/python/babase/_devconsole.py index ba90111..24c4d2f 100644 --- a/dist/ba_data/python/babase/_devconsole.py +++ b/dist/ba_data/python/babase/_devconsole.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Dev-Console functionality.""" - from __future__ import annotations import os diff --git a/dist/ba_data/python/babase/_devconsoletabs.py b/dist/ba_data/python/babase/_devconsoletabs.py index e535888..18221f1 100644 --- a/dist/ba_data/python/babase/_devconsoletabs.py +++ b/dist/ba_data/python/babase/_devconsoletabs.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Predefined tabs for the dev console.""" - from __future__ import annotations import math diff --git a/dist/ba_data/python/babase/_discord.py b/dist/ba_data/python/babase/_discord.py index 3c5aa39..27015ac 100644 --- a/dist/ba_data/python/babase/_discord.py +++ b/dist/ba_data/python/babase/_discord.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. """Functionality related to discord sdk integration""" - from __future__ import annotations from typing import TYPE_CHECKING, override diff --git a/dist/ba_data/python/babase/_emptyappmode.py b/dist/ba_data/python/babase/_emptyappmode.py index 9add278..2c83a14 100644 --- a/dist/ba_data/python/babase/_emptyappmode.py +++ b/dist/ba_data/python/babase/_emptyappmode.py @@ -1,11 +1,12 @@ # Released under the MIT License. See LICENSE for details. # """Provides AppMode functionality.""" - from __future__ import annotations from typing import TYPE_CHECKING, override +# from bacommon.app import AppExperience + import _babase from babase._appmode import AppMode from babase._appintent import AppIntentExec, AppIntentDefault diff --git a/dist/ba_data/python/babase/_env.py b/dist/ba_data/python/babase/_env.py index 2d1afe4..4eb1660 100644 --- a/dist/ba_data/python/babase/_env.py +++ b/dist/ba_data/python/babase/_env.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Environment related functionality.""" - from __future__ import annotations import os @@ -159,6 +158,26 @@ def on_main_thread_start_app() -> None: # situations. __main__.__builtins__.help = _CustomHelper() + # UPDATE: As of May 2025 I'm no longer seeing the below issue, so + # disabling this workaround for now and will remove it soon if no + # issues arise. + + # On Windows I'm seeing the following error creating asyncio loops + # in background threads with the default proactor setup: + + # ValueError: set_wakeup_fd only works in main thread of the main + # interpreter. + + # So let's explicitly request selector loops. Interestingly this + # error only started showing up once I moved Python init to the main + # thread; previously the various asyncio bg thread loops were + # working fine (maybe something caused them to default to selector + # in that case?.. + if sys.platform == 'win32' and bool(False): + import asyncio + + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + # Kick off networking bootstrapping. We do this here instead of in # our app net-subsystem so that it can proceed in parallel with the # rest of our bootstrapping (as networking stuff is often an overall @@ -628,5 +647,5 @@ class _CustomHelper: 'Interactive help is not available in this environment.\n' 'Type help(object) for help about object.' ) - return - pydoc.help(*args, **kwds) + return None + return pydoc.help(*args, **kwds) diff --git a/dist/ba_data/python/babase/_gc.py b/dist/ba_data/python/babase/_gc.py index 4d5672d..0729add 100644 --- a/dist/ba_data/python/babase/_gc.py +++ b/dist/ba_data/python/babase/_gc.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Utility functionality related to the overall operation of the app.""" - from __future__ import annotations import gc diff --git a/dist/ba_data/python/babase/_general.py b/dist/ba_data/python/babase/_general.py index 18ebd2e..d8982e2 100644 --- a/dist/ba_data/python/babase/_general.py +++ b/dist/ba_data/python/babase/_general.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Utility snippets applying to generic Python code.""" - from __future__ import annotations import sys @@ -10,7 +9,6 @@ import weakref import random import logging import inspect -import warnings from typing import TYPE_CHECKING, TypeVar, Protocol, NewType, override from efro.terminal import Clr @@ -19,7 +17,7 @@ import _babase if TYPE_CHECKING: import functools - from typing import Any, Callable + from typing import Any # Declare distinct types for different time measurements we use so the @@ -95,351 +93,160 @@ def get_type_name(cls: type) -> str: return f'{cls.__module__}.{cls.__qualname__}' -# Note: Something here is wonky with pylint, possibly related to our -# custom pylint plugin. Disabling all checks seems to fix it. -# pylint: disable=all -if TYPE_CHECKING: - # For type-checking, we point WeakCall and Call at - # functools.partial. This gives decent type-checking considering the - # open-ended nature of these calls (args being supplied at create - # time and/or at call time). Just remember that we're slightly lying - # to the type-checker here. - WeakCallPartial = functools.partial - CallPartial = functools.partial - WeakCall = functools.partial - Call = functools.partial -else: +class _WeakCall: + """Wrap a callable and arguments into a single callable object. - class WeakCallPartial: - """Wrap a callable and arguments into a single callable object. + When passed a bound method as the callable, the instance portion of + it is weak-referenced, meaning the underlying instance is free to + die if all other references to it go away. Should this occur, + calling the weak-call is simply a no-op. - When passed a bound method as the callable, the instance portion of - it is weak-referenced, meaning the underlying instance is free to - die if all other references to it go away. Should this occur, - calling the weak-call is simply a no-op. + Think of this as a handy way to tell an object to do something at + some point in the future if it happens to still exist. - Think of this as a handy way to tell an object to do something at - some point in the future if it happens to still exist. + **EXAMPLE A:** This code will create a ``FooClass`` instance and + call its ``bar()`` method 5 seconds later; it will be kept alive + even though we overwrite its variable with None because the bound + method we pass as a timer callback (``foo.bar``) strong-references + it:: - **EXAMPLE A:** This code will create a ``FooClass`` instance and - call its ``bar()`` method 5 seconds later; it will be kept alive - even though we overwrite its variable with None because the bound - method we pass as a timer callback (``foo.bar``) strong-references - it:: + foo = FooClass() + babase.apptimer(5.0, foo.bar) + foo = None - foo = FooClass() - babase.apptimer(5.0, foo.bar) - foo = None + **EXAMPLE B:** This code will *not* keep our object alive; it will + die when we overwrite it with ``None`` and the timer will be a no-op + when it fires:: - **EXAMPLE B:** This code will *not* keep our object alive; it will - die when we overwrite it with ``None`` and the timer will be a no-op - when it fires:: + foo = FooClass() + babase.apptimer(5.0, ba.WeakCall(foo.bar)) + foo = None - foo = FooClass() - babase.apptimer(5.0, ba.WeakCall(foo.bar)) - foo = None + **EXAMPLE C:** Wrap a method call with some positional and keyword + args:: - **EXAMPLE C:** Wrap a method call with some positional and keyword - args:: + myweakcall = babase.WeakCall(self.dostuff, argval1, + namedarg=argval2) - myweakcall = babase.WeakCall(self.dostuff, argval1, - namedarg=argval2) + # Now we have a single callable to run that whole mess. + # The same as calling myobj.dostuff(argval1, namedarg=argval2) + # (provided my_obj still exists; this will do nothing otherwise). + myweakcall() - # Now we have a single callable to run that whole mess. - # The same as calling myobj.dostuff(argval1, namedarg=argval2) - # (provided my_obj still exists; this will do nothing otherwise). - myweakcall() - - Note: additional args and keywords you provide to the weak-call - constructor are stored as regular strong-references; you'll need 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, call: Any, /, *args: Any, **keywds: Any) -> None: - # Note: keeping _call, _args, _keywds private in this case - # since we sub functools.partial for ourself in - # type-checking so they will be unrecognized anyway. Use - # non-partial versions if you want to access those. - if hasattr(call, '__func__'): - self._call = WeakMethod(call) - else: - app = _babase.app - if not self._did_invalid_call_warning: - logging.warning( - 'Warning: callable passed to WeakCall() is not' - ' weak-referencable (%s); use regular Call() instead' - ' to avoid this warning.', - args[0], - stack_info=True, - ) - type(self)._did_invalid_call_warning = True - self._call = call - self._args = args - self._keywds = keywds - - def __call__(self, *args_extra: Any, **keywds_extra: Any) -> Any: - # Fast path: no extra args or kwargs. - if not args_extra and not keywds_extra: - return self._call(*self._args, **self._keywds) - - # Slightly slower path: handle extra args. - if not keywds_extra: - # Only extra positional args; skip dict merge. - return self._call(*(self._args + args_extra), **self._keywds) - - # Handle kw overrides (call-time kwargs overriding stored). - merged = {**self._keywds, **keywds_extra} - return self._call(*(self._args + args_extra), **merged) - - @override - def __repr__(self) -> str: - return ( - f'' - ) - - class CallPartial: - """Wraps a callable and args into a single callable object. - - The callable is strong-referenced so it won't die until this - object does. - - Note that a bound method (ex: ``myobj.dosomething``) contains a - reference to ``self`` (``myobj`` in that case), so you will be - keeping that object alive too. Use babase.WeakCall if you want - to pass a method to a callback without keeping its object alive. - - Example: Wrap a method call with 1 positional and 1 keyword arg:: - - mycall = babase.Call(myobj.dostuff, argval, namedarg=argval2) - - # Now we have a single callable to run that whole mess. - # ..the same as calling myobj.dostuff(argval, namedarg=argval2) - mycall() - """ - - # Optimize performance a bit; we shouldn't need to be super dynamic. - __slots__ = ['_call', '_args', '_keywds'] - - def __init__(self, call: Any, /, *args: Any, **keywds: Any): - # Note: keeping _call, _args, _keywds private in this case - # since we sub functools.partial for ourself in - # type-checking so they will be unrecognized anyway. Use - # non-partial versions if you want to access those. - self._call = call - self._args = args - self._keywds = keywds - - def __call__(self, *args_extra: Any, **keywds_extra: Any) -> Any: - # Fast path: no extra args or kwargs. - if not args_extra and not keywds_extra: - return self._call(*self._args, **self._keywds) - - # Slightly slower path: handle extra args. - if not keywds_extra: - # Only extra positional args; skip dict merge. - return self._call(*(self._args + args_extra), **self._keywds) - - # Handle kw overrides (call-time kwargs overriding stored). - merged = {**self._keywds, **keywds_extra} - return self._call(*(self._args + args_extra), **merged) - - @override - def __repr__(self) -> str: - return ( - f'' - ) - - class WeakCall: - """Currently alias of :meth:`WeakCallPartial`.""" - - # Optimize performance a bit; we shouldn't need to be super dynamic. - __slots__ = ['_call', '_args', '_keywds'] - - _did_invalid_call_warning = False - - def __init__(self, call: Any, /, *args: Any, **keywds: Any) -> None: - warnings.warn( - 'WeakCall should be replaced with either WeakCallPartial' - ' (if passing extra args at call time) or WeakCallStrict' - ' (it not). Once API 9 support ends, WeakCall can again be' - ' used, but it will behave like WeakCallStrict instead of' - ' WeakCallPartial.', - DeprecationWarning, - stacklevel=2, - ) - # Note: keeping _call, _args, _keywds private in this case - # since we sub functools.partial for ourself in - # type-checking so they will be unrecognized anyway. Use - # non-partial versions if you want to access those. - if hasattr(call, '__func__'): - self._call = WeakMethod(call) - else: - app = _babase.app - if not self._did_invalid_call_warning: - logging.warning( - 'Warning: callable passed to WeakCall() is not' - ' weak-referencable (%r); use regular Call() instead' - ' to avoid this warning.', - args[0], - stack_info=True, - ) - type(self)._did_invalid_call_warning = True - self._call = call - self._args = args - self._keywds = keywds - - def __call__(self, *args_extra: Any, **keywds_extra: Any) -> Any: - # Fast path: no extra args or kwargs. - if not args_extra and not keywds_extra: - return self._call(*self._args, **self._keywds) - - # Slightly slower path: handle extra args. - if not keywds_extra: - # Only extra positional args; skip dict merge. - return self._call(*(self._args + args_extra), **self._keywds) - - # Handle kw overrides (call-time kwargs overriding stored). - merged = {**self._keywds, **keywds_extra} - return self._call(*(self._args + args_extra), **merged) - - @override - def __repr__(self) -> str: - return ( - f'' - ) - - class Call: - """Currently alias of :meth:`CallPartial`.""" - - # Optimize performance a bit; we shouldn't need to be super dynamic. - __slots__ = ['_call', '_args', '_keywds'] - - def __init__(self, call: Any, /, *args: Any, **keywds: Any): - warnings.warn( - 'Call should be replaced with either CallPartial' - ' (if passing extra args at call time) or CallStrict' - ' (it not). Once API 9 support ends, Call can again be' - ' used, but it will behave like CallStrict instead' - ' of CallPartial.', - DeprecationWarning, - stacklevel=2, - ) - # Note: keeping _call, _args, _keywds private in this case - # since we sub functools.partial for ourself in - # type-checking so they will be unrecognized anyway. Use - # non-partial versions if you want to access those. - self._call = call - self._args = args - self._keywds = keywds - - def __call__(self, *args_extra: Any, **keywds_extra: Any) -> Any: - # Fast path: no extra args or kwargs. - if not args_extra and not keywds_extra: - return self._call(*self._args, **self._keywds) - - # Slightly slower path: handle extra args. - if not keywds_extra: - # Only extra positional args; skip dict merge. - return self._call(*(self._args + args_extra), **self._keywds) - - # Handle kw overrides (call-time kwargs overriding stored). - merged = {**self._keywds, **keywds_extra} - return self._call(*(self._args + args_extra), **merged) - - @override - def __repr__(self) -> str: - return ( - f'' - ) - - -# pylint: enable=all - - -class CallStrict[**P, T]: - """Like :meth:`CallPartial()` but disallows extra args at call time. - - This allows more complete type checking to occur, so this is - recommended if you do not need extra args at call time. + Note: additional args and keywords you provide to the weak-call + constructor are stored as regular strong-references; you'll need to + wrap them in weakrefs manually if desired. """ - __slots__ = ('call', 'args', 'kwargs') - - def __init__( - self, call: Callable[P, T], /, *args: P.args, **kwargs: P.kwargs - ) -> None: - # Note: we allow access to these here since we don't use any - # tricks like pointing at functools.partial for type checking or - # whatnot that would break this. - self.call = call - self.args = args - self.kwargs = kwargs - - def __call__(self) -> T: - return self.call(*self.args, **self.kwargs) - - @override - def __repr__(self) -> str: - return ( - f'' - ) - - -class WeakCallStrict[**P, T]: - """Like :meth:`WeakCallPartial()` but disallows extra args at call time. - - This allows more complete type checking to occur, so this is - recommended if you do not need extra args at call time. - """ - - __slots__ = ('call', 'args', 'kwargs') + # Optimize performance a bit; we shouldn't need to be super dynamic. + __slots__ = ['_call', '_args', '_keywds'] _did_invalid_call_warning = False - def __init__( - self, call: Callable[P, T], /, *args: P.args, **kwargs: P.kwargs - ) -> None: - # Note: we allow access to these here since we don't use any - # tricks like pointing at functools.partial for type checking or - # whatnot that would break this. - if hasattr(call, '__func__'): - self.call: Any = WeakMethod(call) # type: ignore + def __init__(self, *args: Any, **keywds: Any) -> None: + if hasattr(args[0], '__func__'): + self._call = WeakMethod(args[0]) else: app = _babase.app if not self._did_invalid_call_warning: logging.warning( - 'Warning: callable passed to WeakCallStrict() is not' - ' weak-referencable (%r); use regular CallStrict() instead' + 'Warning: callable passed to babase.WeakCall() is not' + ' weak-referencable (%s); use functools.partial instead' ' to avoid this warning.', args[0], stack_info=True, ) type(self)._did_invalid_call_warning = True - self.call = call - self.args = args - self.kwargs = kwargs + self._call = args[0] + self._args = args[1:] + self._keywds = keywds - def __call__(self) -> T: - return self.call(*self.args, **self.kwargs) # type: ignore + def __call__(self, *args_extra: Any) -> Any: + return self._call(*self._args + args_extra, **self._keywds) @override - def __repr__(self) -> str: + def __str__(self) -> str: return ( - f'' + '' ) +class _Call: + """Wraps a callable and arguments into a single callable object. + + The callable is strong-referenced so it won't die until this + object does. + + Note that a bound method (ex: ``myobj.dosomething``) contains a reference + to ``self`` (``myobj`` in that case), so you will be keeping that object + alive too. Use babase.WeakCall if you want to pass a method to a callback + without keeping its object alive. + + Example: Wrap a method call with 1 positional and 1 keyword arg:: + + mycall = babase.Call(myobj.dostuff, argval, namedarg=argval2) + + # Now we have a single callable to run that whole mess. + # ..the same as calling myobj.dostuff(argval, namedarg=argval2) + mycall() + """ + + # Optimize performance a bit; we shouldn't need to be super dynamic. + __slots__ = ['_call', '_args', '_keywds'] + + def __init__(self, *args: Any, **keywds: Any): + self._call = args[0] + self._args = args[1:] + self._keywds = keywds + + def __call__(self, *args_extra: Any) -> Any: + return self._call(*self._args + args_extra, **self._keywds) + + @override + def __str__(self) -> str: + return ( + '' + ) + + +if TYPE_CHECKING: + # For type-checking, point at functools.partial which gives us full + # 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 + + WeakCall = functools.partial + Call = functools.partial +else: + WeakCall = _WeakCall + WeakCall.__name__ = 'WeakCall' + Call = _Call + Call.__name__ = 'Call' + + class WeakMethod: """A weak-referenced bound method. @@ -448,22 +255,22 @@ class WeakMethod: """ # Optimize performance a bit; we shouldn't need to be super dynamic. - __slots__ = ['func', 'obj'] + __slots__ = ['_func', '_obj'] def __init__(self, call: types.MethodType): assert isinstance(call, types.MethodType) - self.func = call.__func__ - self.obj = weakref.ref(call.__self__) + self._func = call.__func__ + self._obj = weakref.ref(call.__self__) def __call__(self, *args: Any, **keywds: Any) -> Any: - obj: Any = self.obj() + obj = self._obj() if obj is None: return None - return self.func(*((obj,) + args), **keywds) + return self._func(*((obj,) + args), **keywds) @override - def __repr__(self) -> str: - return f'' + def __str__(self) -> str: + return '' def verify_object_death(obj: object) -> None: @@ -485,7 +292,7 @@ def verify_object_death(obj: object) -> None: # Make this timer in an empty context; don't want it dying with the # scene/etc. with _babase.ContextRef.empty(): - _babase.apptimer(delay, CallStrict(_verify_object_death, ref)) + _babase.apptimer(delay, Call(_verify_object_death, ref)) def _verify_object_death(wref: weakref.ref) -> None: diff --git a/dist/ba_data/python/babase/_hooks.py b/dist/ba_data/python/babase/_hooks.py index e5df00d..4c9b771 100644 --- a/dist/ba_data/python/babase/_hooks.py +++ b/dist/ba_data/python/babase/_hooks.py @@ -9,7 +9,6 @@ until it broke at runtime. By instead defining such snippets here and then capturing references to them all at launch it is possible to allow linting and type-checking magic to happen and most issues will be caught immediately. """ - # (most of these are self-explanatory) # pylint: disable=missing-function-docstring from __future__ import annotations @@ -43,7 +42,7 @@ def get_v2_account_id() -> str | None: if account is not None: accountid = account.accountid # (Avoids mypy complaints when plus is not present) - assert isinstance(accountid, str | None) + assert isinstance(accountid, (str, type(None))) return accountid return None except Exception: @@ -462,36 +461,3 @@ def copy_dev_console_history() -> None: _babase.clipboard_set_text('\n'.join(lines)) _babase.screenmessage(Lstr(resource='copyConfirmText'), color=(0, 1, 0)) _babase.getsimplesound('gunCocking').play() - - -def v2_auth_request(global_app_instance_id: str) -> None | tuple[bool, str]: - """Kick off or process v2 auth requests. - - Return None if no results or (success, error/token) - """ - assert _babase.app.plus is not None - out: None | tuple[bool, str] = _babase.app.plus.accounts.auth_request( - global_app_instance_id - ) - return out - - -def v2_auth_data(token: str) -> None | tuple[str, str, dict]: - """Look up autheneticated v2 account data via a token.""" - assert _babase.in_logic_thread() - - classic = _babase.app.classic - if classic is None: - return None - - now = time.monotonic() - authdata = classic.v2_auth_datas.get(token) - if authdata is None or authdata.expire_time <= now: - return None - - # Success! - return ( - authdata.account_id, - authdata.account_tag, - authdata.player_profiles, - ) diff --git a/dist/ba_data/python/babase/_language.py b/dist/ba_data/python/babase/_language.py index 7f381e4..b8f3c6b 100644 --- a/dist/ba_data/python/babase/_language.py +++ b/dist/ba_data/python/babase/_language.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Language related functionality.""" - from __future__ import annotations import os @@ -562,7 +561,7 @@ class Lstr: You should avoid doing this as much as possible and instead pass and store ``Lstr`` values. """ - return _babase.evaluate_lstr(self.as_json()) + return _babase.evaluate_lstr(self._get_json()) def is_flat_value(self) -> bool: """Return whether this instance represents a 'flat' value. @@ -574,13 +573,22 @@ class Lstr: """ return bool('v' in self.args and not self.args.get('s', [])) - def as_json(self) -> str: - """Return the json dict representation of the Lstr.""" - return json.dumps(self.args, separators=(',', ':')) + def _get_json(self) -> str: + try: + return json.dumps(self.args, separators=(',', ':')) + except Exception: + from babase import _error + + applog.exception('_get_json failed for %s.', self.args) + return 'JSON_ERR' + + @override + def __str__(self) -> str: + return f'' @override def __repr__(self) -> str: - return f'' + return f'' @staticmethod def from_json(json_string: str) -> babase.Lstr: @@ -608,7 +616,7 @@ def _add_to_attr_dict(dst: AttrDict, src: dict) -> None: ) _add_to_attr_dict(dst_dict, value) else: - if not isinstance(value, float | int | bool | str | None): + if not isinstance(value, (float, int, bool, str, str, type(None))): raise TypeError( "invalid value type for res '" + key diff --git a/dist/ba_data/python/babase/_locale.py b/dist/ba_data/python/babase/_locale.py index 5371d13..c8de05e 100644 --- a/dist/ba_data/python/babase/_locale.py +++ b/dist/ba_data/python/babase/_locale.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Locale related functionality.""" - from __future__ import annotations from typing import TYPE_CHECKING, override, assert_never @@ -145,7 +144,6 @@ class LocaleSubsystem(AppSubsystem): or rlocale is cls.TAMIL or rlocale is cls.THAI or rlocale is cls.VIETNAMESE - or rlocale is cls.JAPANESE ): # Return True only if we can display full unicode. return _babase.supports_unicode_display() diff --git a/dist/ba_data/python/babase/_mgen/enums.py b/dist/ba_data/python/babase/_mgen/enums.py index ceb45dd..697fe4d 100644 --- a/dist/ba_data/python/babase/_mgen/enums.py +++ b/dist/ba_data/python/babase/_mgen/enums.py @@ -85,9 +85,7 @@ class Permission(Enum): class SpecialChar(Enum): - """Special characters the engine can diplay. Note that this currently - needs to be manually kept in sync with bacommon.text.SpecialChar. - """ + """Special characters the game can print.""" DOWN_ARROW = 0 UP_ARROW = 1 @@ -187,7 +185,3 @@ class SpecialChar(Enum): MIKIROG = 95 V2_LOGO = 96 CLOSE = 97 - SANTA_HAT = 98 - POTATO = 99 - PALM_TREE = 100 - BOXING_GLOVE = 101 diff --git a/dist/ba_data/python/babase/_net.py b/dist/ba_data/python/babase/_net.py index 6cbaaa0..16c1e7c 100644 --- a/dist/ba_data/python/babase/_net.py +++ b/dist/ba_data/python/babase/_net.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Networking related functionality.""" - from __future__ import annotations import socket diff --git a/dist/ba_data/python/babase/_ui.py b/dist/ba_data/python/babase/_ui.py index 9956580..55c895b 100644 --- a/dist/ba_data/python/babase/_ui.py +++ b/dist/ba_data/python/babase/_ui.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """UI related bits of babase.""" - from __future__ import annotations from typing import TYPE_CHECKING, override diff --git a/dist/ba_data/python/babase/modutils.py b/dist/ba_data/python/babase/modutils.py index b822734..831e9dd 100644 --- a/dist/ba_data/python/babase/modutils.py +++ b/dist/ba_data/python/babase/modutils.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to modding.""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/baclassic/_achievement.py b/dist/ba_data/python/baclassic/_achievement.py index a6acf62..6641829 100644 --- a/dist/ba_data/python/baclassic/_achievement.py +++ b/dist/ba_data/python/baclassic/_achievement.py @@ -1,13 +1,12 @@ # Released under the MIT License. See LICENSE for details. # """Various functionality related to achievements.""" - from __future__ import annotations import logging from typing import TYPE_CHECKING -from bacommon.classic import ClassicChestAppearance +from bacommon.bs import ClassicChestAppearance from baclassic._chest import ( CHEST_APPEARANCE_DISPLAY_INFOS, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, @@ -728,10 +727,7 @@ class Achievement: ) def get_award_chest_type(self) -> ClassicChestAppearance: - """Return the type of chest given for this achievement. - - :meta private: - """ + """Return the type of chest given for this achievement.""" # For now just map our old ticket values to chest types. # Can add distinct values if need be later. @@ -1524,7 +1520,5 @@ class Achievement: for actor in objs: bascenev1.timer( out_time + 1.000, - babase.WeakCallStrict( - actor.handlemessage, bascenev1.DieMessage() - ), + babase.WeakCall(actor.handlemessage, bascenev1.DieMessage()), ) diff --git a/dist/ba_data/python/baclassic/_ads.py b/dist/ba_data/python/baclassic/_ads.py new file mode 100644 index 0000000..25dabb2 --- /dev/null +++ b/dist/ba_data/python/baclassic/_ads.py @@ -0,0 +1,227 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to ads.""" +from __future__ import annotations + +import time +import asyncio +import logging +from typing import TYPE_CHECKING + +import babase +import bascenev1 + +if TYPE_CHECKING: + from typing import Callable, Any + + +class AdsSubsystem: + """Subsystem for ads functionality in the app. + + Access the single shared instance of this class at 'ba.app.ads'. + """ + + def __init__(self) -> None: + self.last_ad_network = 'unknown' + self.last_ad_network_set_time = time.time() + self.ad_amt: float | None = None + self.last_ad_purpose = 'invalid' + self.attempted_first_ad = False + self.last_in_game_ad_remove_message_show_time: float | None = None + self.last_ad_completion_time: float | None = None + self.last_ad_was_short = False + self._fallback_task: asyncio.Task | None = None + + def do_remove_in_game_ads_message(self) -> None: + """(internal)""" + + # Print this message once every 10 minutes at most. + tval = babase.apptime() + if self.last_in_game_ad_remove_message_show_time is None or ( + tval - self.last_in_game_ad_remove_message_show_time > 60 * 10 + ): + self.last_in_game_ad_remove_message_show_time = tval + with babase.ContextRef.empty(): + babase.apptimer( + 1.0, + lambda: babase.screenmessage( + babase.Lstr( + resource='removeInGameAdsTokenPurchaseText' + ), + color=(1, 1, 0), + ), + ) + + def show_ad( + self, purpose: str, on_completion_call: Callable[[], Any] | None = None + ) -> None: + """(internal)""" + self.last_ad_purpose = purpose + assert babase.app.plus is not None + babase.app.plus.show_ad(purpose, on_completion_call) + + def show_ad_2( + self, + purpose: str, + on_completion_call: Callable[[bool], Any] | None = None, + ) -> None: + """(internal)""" + self.last_ad_purpose = purpose + assert babase.app.plus is not None + babase.app.plus.show_ad_2(purpose, on_completion_call) + + def call_after_ad(self, call: Callable[[], Any]) -> None: + """Run a call after potentially showing an ad.""" + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + + app = babase.app + plus = app.plus + classic = app.classic + assert plus is not None + assert classic is not None + show = True + + # No ads without net-connections, etc. + if not plus.can_show_ad(): + show = False + + # Pro or other upgrades disable interstitials. + if ( + classic.accounts.have_pro() + or classic.gold_pass + or classic.remove_ads + ): + show = False + try: + session = bascenev1.get_foreground_host_session() + assert session is not None + is_tournament = session.tournament_id is not None + except Exception: + is_tournament = False + if is_tournament: + show = False # Never show ads during tournaments. + + if show: + interval: float | None + launch_count = app.config.get('launchCount', 0) + + # If we're seeing short ads we may want to space them differently. + interval_mult = ( + plus.get_v1_account_misc_read_val('ads.shortIntervalMult', 1.0) + if self.last_ad_was_short + else 1.0 + ) + if self.ad_amt is None: + if launch_count <= 1: + self.ad_amt = plus.get_v1_account_misc_read_val( + 'ads.startVal1', 0.99 + ) + else: + self.ad_amt = plus.get_v1_account_misc_read_val( + 'ads.startVal2', 1.0 + ) + interval = None + else: + # So far we're cleared to show; now calc our + # ad-show-threshold and see if we should *actually* show + # (we reach our threshold faster the longer we've been + # playing). + base = 'ads' if plus.has_video_ads() else 'ads2' + min_lc = plus.get_v1_account_misc_read_val(base + '.minLC', 0.0) + max_lc = plus.get_v1_account_misc_read_val(base + '.maxLC', 5.0) + min_lc_scale = plus.get_v1_account_misc_read_val( + base + '.minLCScale', 0.25 + ) + max_lc_scale = plus.get_v1_account_misc_read_val( + base + '.maxLCScale', 0.34 + ) + min_lc_interval = plus.get_v1_account_misc_read_val( + base + '.minLCInterval', 360 + ) + max_lc_interval = plus.get_v1_account_misc_read_val( + base + '.maxLCInterval', 300 + ) + if launch_count < min_lc: + lc_amt = 0.0 + elif launch_count > max_lc: + lc_amt = 1.0 + else: + lc_amt = (float(launch_count) - min_lc) / (max_lc - min_lc) + incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale + interval = ( + 1.0 - lc_amt + ) * min_lc_interval + lc_amt * max_lc_interval + self.ad_amt += incr + assert self.ad_amt is not None + if self.ad_amt >= 1.0: + self.ad_amt = self.ad_amt % 1.0 + self.attempted_first_ad = True + + # After we've reached the traditional show-threshold once, + # try again whenever its been INTERVAL since our last successful + # show. + elif self.attempted_first_ad and ( + self.last_ad_completion_time is None + or ( + interval is not None + and babase.apptime() - self.last_ad_completion_time + > (interval * interval_mult) + ) + ): + # Reset our other counter too in this case. + self.ad_amt = 0.0 + else: + show = False + + # If we're *still* cleared to show, actually tell the system to show. + if show: + # As a safety-check, we set up an object that will run the + # completion callback if we've returned and sat for several + # seconds (in case some random ad network doesn't properly + # deliver its completion callback). + class _Payload: + def __init__(self, pcall: Callable[[], Any]): + self._call = pcall + self._ran = False + + def run(self, fallback: bool = False) -> None: + """Run the payload.""" + assert app.classic is not None + if not self._ran: + if fallback: + lanst = app.classic.ads.last_ad_network_set_time + logging.error( + 'Relying on fallback ad-callback! ' + 'last network: %s (set %s seconds ago);' + ' purpose=%s.', + app.classic.ads.last_ad_network, + time.time() - lanst, + app.classic.ads.last_ad_purpose, + ) + babase.pushcall(self._call) + self._ran = True + + payload = _Payload(call) + + # Set up our backup. + with babase.ContextRef.empty(): + # Note to self: Previously this was a simple 5 second + # timer because the app got totally suspended while ads + # were showing (which delayed the timer), but these days + # the app may continue to run, so we need to be more + # careful and only fire the fallback after we see that + # the app has been front-and-center for several seconds. + async def add_fallback_task() -> None: + activesecs = 5 + while activesecs > 0: + if babase.app.active: + activesecs -= 1 + await asyncio.sleep(1.0) + payload.run(fallback=True) + + 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/dist/ba_data/python/baclassic/_analytics.py b/dist/ba_data/python/baclassic/_analytics.py index ad5fe8a..41a68a8 100644 --- a/dist/ba_data/python/baclassic/_analytics.py +++ b/dist/ba_data/python/baclassic/_analytics.py @@ -1,6 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Functionality related to classic analytics.""" +"""Functionality related to analytics.""" from __future__ import annotations diff --git a/dist/ba_data/python/baclassic/_appmode.py b/dist/ba_data/python/baclassic/_appmode.py index 20246ba..ce065b1 100644 --- a/dist/ba_data/python/baclassic/_appmode.py +++ b/dist/ba_data/python/baclassic/_appmode.py @@ -1,6 +1,5 @@ # Released under the MIT License. See LICENSE for details. # -# pylint: disable=too-many-lines """Contains ClassicAppMode.""" from __future__ import annotations @@ -12,11 +11,11 @@ from functools import partial from typing import TYPE_CHECKING, override from efro.error import CommunicationError -import bacommon.clienteffect as clfx -import bacommon.classic +import bacommon.bs from babase import AppMode import bauiv1 as bui from bauiv1lib.connectivity import wait_for_connectivity +from bauiv1lib.account.signin import show_sign_in_prompt import _baclassic @@ -234,19 +233,19 @@ class ClassicAppMode(AppMode): if item_id.startswith('tokens'): if item_id == 'tokens1': - tokens = bacommon.classic.TOKENS1_COUNT + tokens = bacommon.bs.TOKENS1_COUNT tokens_str = str(tokens) anim_time = 2.0 elif item_id == 'tokens2': - tokens = bacommon.classic.TOKENS2_COUNT + tokens = bacommon.bs.TOKENS2_COUNT tokens_str = str(tokens) anim_time = 2.5 elif item_id == 'tokens3': - tokens = bacommon.classic.TOKENS3_COUNT + tokens = bacommon.bs.TOKENS3_COUNT tokens_str = str(tokens) anim_time = 3.0 elif item_id == 'tokens4': - tokens = bacommon.classic.TOKENS4_COUNT + tokens = bacommon.bs.TOKENS4_COUNT tokens_str = str(tokens) anim_time = 3.5 else: @@ -258,19 +257,21 @@ class ClassicAppMode(AppMode): ) assert bui.app.classic is not None - effects: list[clfx.Effect] = [ - clfx.TokensAnimation( + effects: list[bacommon.bs.ClientEffect] = [ + bacommon.bs.ClientEffectTokensAnimation( duration=anim_time, startvalue=self._last_tokens_value, endvalue=self._last_tokens_value + tokens, ), - clfx.Delay(anim_time), - clfx.LegacyScreenMessage( + bacommon.bs.ClientEffectDelay(anim_time), + bacommon.bs.ClientEffectScreenMessage( message='You got ${COUNT} tokens!', subs=['${COUNT}', tokens_str], color=(0, 1, 0), ), - clfx.PlaySound(clfx.Sound.CASH_REGISTER), + bacommon.bs.ClientEffectSound( + sound=bacommon.bs.ClientEffectSound.Sound.CASH_REGISTER + ), ] bui.app.classic.run_bs_client_effects(effects) @@ -344,14 +345,14 @@ class ClassicAppMode(AppMode): with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.classic.GetClassicPurchasesMessage(), - on_response=bui.WeakCallPartial( + bacommon.bs.GetClassicPurchasesMessage(), + on_response=bui.WeakCall( self._on_get_classic_purchases_response ), ) def _on_get_classic_purchases_response( - self, response: bacommon.classic.GetClassicPurchasesResponse | Exception + self, response: bacommon.bs.GetClassicPurchasesResponse | Exception ) -> None: assert self._purchase_request_in_flight self._purchase_request_in_flight = False @@ -470,7 +471,6 @@ class ClassicAppMode(AppMode): chest_1_ad_allow_time=-1.0, chest_2_ad_allow_time=-1.0, chest_3_ad_allow_time=-1.0, - store_style='', ) self._have_account_values = False self._update_ui_live_state() @@ -505,7 +505,7 @@ class ClassicAppMode(AppMode): print(f'GOT SUB TEST UPDATE: {val}') def _on_classic_account_data_change( - self, val: bacommon.classic.ClassicLiveAccountClientData + self, val: bacommon.bs.ClassicAccountLiveData ) -> None: achp = round(val.achievements / max(val.achievements_total, 1) * 100.0) @@ -666,7 +666,6 @@ class ClassicAppMode(AppMode): if chest3 is None or chest3.ad_allow_time is None else chest3.ad_allow_time.timestamp() ), - store_style=val.store_style.value, ) # Note that we have values and updated faded state accordingly. @@ -724,56 +723,44 @@ class ClassicAppMode(AppMode): def _root_ui_achievements_press(self) -> None: from bauiv1lib.achievements import AchievementsWindow - btn = bui.get_special_widget('achievements_button') - - if not self._ensure_signed_in(origin_widget=btn): + if not self._ensure_signed_in_v1(): return wait_for_connectivity( on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate( win_type=AchievementsWindow, - win_create_call=lambda: AchievementsWindow(origin_widget=btn), + win_create_call=lambda: AchievementsWindow( + origin_widget=bui.get_special_widget('achievements_button') + ), ) ) def _root_ui_inbox_press(self) -> None: from bauiv1lib.inbox import InboxWindow - btn = bui.get_special_widget('inbox_button') - - if not self._ensure_signed_in(origin_widget=btn): + if not self._ensure_signed_in(): return wait_for_connectivity( on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate( win_type=InboxWindow, - win_create_call=lambda: InboxWindow(origin_widget=btn), + win_create_call=lambda: InboxWindow( + origin_widget=bui.get_special_widget('inbox_button') + ), ) ) def _root_ui_store_press(self) -> None: - import bacommon.docui.v1 as dui1 + from bauiv1lib.store.browser import StoreBrowserWindow - from bauiv1lib.docui import DocUIWindow - from bauiv1lib.store import StoreUIController - - btn = bui.get_special_widget('store_button') - - if not self._ensure_signed_in(origin_widget=btn): + if not self._ensure_signed_in_v1(): return - # Pop up an auxiliary window wherever we are in the nav stack. wait_for_connectivity( on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate( - win_type=DocUIWindow, - win_create_call=bui.CallStrict( - StoreUIController().create_window, - dui1.Request('/'), - origin_widget=btn, - uiopenstateid='classicstore', - ), - win_extra_type_id=( - StoreUIController.get_window_extra_type_id() + win_type=StoreBrowserWindow, + win_create_call=lambda: StoreBrowserWindow( + origin_widget=bui.get_special_widget('store_button') ), ) ) @@ -795,76 +782,80 @@ class ClassicAppMode(AppMode): def _root_ui_trophy_meter_press(self) -> None: from bauiv1lib.league.rankwindow import LeagueRankWindow - btn = bui.get_special_widget('trophy_meter') - - if not self._ensure_signed_in(origin_widget=btn): + if not self._ensure_signed_in_v1(): return bui.app.ui_v1.auxiliary_window_activate( win_type=LeagueRankWindow, - win_create_call=lambda: LeagueRankWindow(origin_widget=btn), + win_create_call=lambda: LeagueRankWindow( + origin_widget=bui.get_special_widget('trophy_meter') + ), ) def _root_ui_level_meter_press(self) -> None: from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow - btn = bui.get_special_widget('level_meter') - - if not self._ensure_signed_in(origin_widget=btn): - return - - ResourceTypeInfoWindow('xp', origin_widget=btn) - - def _root_ui_inventory_press(self) -> None: - import bacommon.docui.v1 as dui1 - - from bauiv1lib.docui import DocUIWindow - from bauiv1lib.inventory import InventoryUIController - - # Pop up an auxiliary window wherever we are in the nav stack. - bui.app.ui_v1.auxiliary_window_activate( - win_type=DocUIWindow, - win_create_call=bui.CallStrict( - InventoryUIController().create_window, - dui1.Request('/'), - origin_widget=bui.get_special_widget('inventory_button'), - uiopenstateid='classicinventory', - ), - win_extra_type_id=InventoryUIController.get_window_extra_type_id(), + ResourceTypeInfoWindow( + 'xp', origin_widget=bui.get_special_widget('level_meter') ) - def _ensure_signed_in(self, *, origin_widget: bui.Widget | None) -> bool: - """Make sure we're signed in (requiring modern v2 accounts).""" - from bauiv1lib.account.signin import show_sign_in_prompt + def _root_ui_inventory_press(self) -> None: + from bauiv1lib.inventory import InventoryWindow + if not self._ensure_signed_in_v1(): + return + + bui.app.ui_v1.auxiliary_window_activate( + win_type=InventoryWindow, + win_create_call=lambda: InventoryWindow( + origin_widget=bui.get_special_widget('inventory_button') + ), + ) + + def _ensure_signed_in(self) -> bool: + """Make sure we're signed in (requiring modern v2 accounts).""" plus = bui.app.plus if plus is None: bui.screenmessage('This requires plus.', color=(1, 0, 0)) bui.getsound('error').play() return False if plus.accounts.primary is None: - show_sign_in_prompt(origin_widget=origin_widget) + show_sign_in_prompt() + return False + return True + + def _ensure_signed_in_v1(self) -> bool: + """Make sure we're signed in (allowing legacy v1-only accounts).""" + plus = bui.app.plus + if plus is None: + bui.screenmessage('This requires plus.', color=(1, 0, 0)) + bui.getsound('error').play() + return False + if plus.get_v1_account_state() != 'signed_in': + show_sign_in_prompt() return False return True def _root_ui_get_tokens_press(self) -> None: - from bauiv1lib.gettokens import GetTokensWindow, show_get_tokens_window + from bauiv1lib.gettokens import GetTokensWindow - btn = bui.get_special_widget('get_tokens_button') - - if not self._ensure_signed_in(origin_widget=btn): + if not self._ensure_signed_in(): return - if bool(True): - show_get_tokens_window(origin_widget=btn, toggle=True) - else: - bui.app.ui_v1.auxiliary_window_activate( - win_type=GetTokensWindow, - win_create_call=lambda: GetTokensWindow(origin_widget=btn), - ) + bui.app.ui_v1.auxiliary_window_activate( + win_type=GetTokensWindow, + win_create_call=lambda: GetTokensWindow( + origin_widget=bui.get_special_widget('get_tokens_button') + ), + ) def _root_ui_chest_slot_pressed(self, index: int) -> None: - from bauiv1lib.chest import ChestWindow + from bauiv1lib.chest import ( + ChestWindow0, + ChestWindow1, + ChestWindow2, + ChestWindow3, + ) widgetid: Literal[ 'chest_0_button', @@ -875,20 +866,16 @@ class ClassicAppMode(AppMode): winclass: type[ChestWindow] if index == 0: widgetid = 'chest_0_button' - winclass = ChestWindow - extratypeid = '0' + winclass = ChestWindow0 elif index == 1: widgetid = 'chest_1_button' - winclass = ChestWindow - extratypeid = '1' + winclass = ChestWindow1 elif index == 2: widgetid = 'chest_2_button' - winclass = ChestWindow - extratypeid = '2' + winclass = ChestWindow2 elif index == 3: widgetid = 'chest_3_button' - winclass = ChestWindow - extratypeid = '3' + winclass = ChestWindow3 else: raise RuntimeError(f'Invalid index {index}') @@ -899,7 +886,6 @@ class ClassicAppMode(AppMode): index=index, origin_widget=bui.get_special_widget(widgetid), ), - win_extra_type_id=extratypeid, ) ) @@ -967,26 +953,16 @@ class ClassicAppMode(AppMode): return [ bui.DevConsoleButtonDef( 'MainWindow Template', - bui.WeakCallStrict(self._main_win_template_press), + bui.WeakCall(self._main_win_template_press), ), bui.DevConsoleButtonDef( - 'DocUI Test', bui.WeakCallStrict(self._doc_ui_test_press) + 'CloudUI Test', bui.WeakCall(self._cloud_ui_test_press) ), ] def _main_win_template_press(self) -> None: from bauiv1lib.template import show_template_main_window - # This only works if a main ui is up. - if bui.app.ui_v1.get_main_window() is None: - bui.screenmessage( - 'This requires a main-window to be present.' - ' Open a menu or whatnot first.', - color=(1, 0, 0), - ) - bui.getsound('error').play() - return - # Unintuitively, swish sounds come from buttons, not windows. # And dev-console buttons don't make sounds. So we need to # explicitly do so here. @@ -994,22 +970,12 @@ class ClassicAppMode(AppMode): show_template_main_window() - def _doc_ui_test_press(self) -> None: - from bauiv1lib.docuitest import show_test_doc_ui_window - - # This only works if a main ui is up. - if bui.app.ui_v1.get_main_window() is None: - bui.screenmessage( - 'This requires a main-window to be present.' - ' Open a menu or whatnot first.', - color=(1, 0, 0), - ) - bui.getsound('error').play() - return + def _cloud_ui_test_press(self) -> None: + from bauiv1 import show_cloud_ui_window # Unintuitively, swish sounds come from buttons, not windows. # And dev-console buttons don't make sounds. So we need to # explicitly do so here. bui.getsound('swish').play() - show_test_doc_ui_window() + show_cloud_ui_window() diff --git a/dist/ba_data/python/baclassic/_appsubsystem.py b/dist/ba_data/python/baclassic/_appsubsystem.py index 9f7ac83..9244069 100644 --- a/dist/ba_data/python/baclassic/_appsubsystem.py +++ b/dist/ba_data/python/baclassic/_appsubsystem.py @@ -3,15 +3,12 @@ # pylint: disable=too-many-lines """Provides classic app subsystem.""" - from __future__ import annotations -import time import random import logging import weakref -from dataclasses import dataclass -from typing import TYPE_CHECKING, override, assert_never, final +from typing import TYPE_CHECKING, override, assert_never from efro.dataclassio import dataclass_from_dict import babase @@ -28,12 +25,9 @@ from baclassic._store import StoreSubsystem from baclassic import _input if TYPE_CHECKING: - import datetime - from typing import Callable, Any, Sequence, Awaitable + from typing import Callable, Any, Sequence - import bacommon.classic - import bacommon.clienteffect as clfx - import bacommon.clouddialog.basic as bcdlg + import bacommon.bs from bascenev1lib.actor import spazappearance from bauiv1lib.party import PartyWindow @@ -42,72 +36,16 @@ if TYPE_CHECKING: class ClassicAppSubsystem(babase.AppSubsystem): - """Subsystem for classic bombsquad functionality in the app. + """Subsystem for classic functionality in the app. The single shared instance of this app can be accessed at - babase.app.classic. Note that it is possible for babase.app.classic - to be None if the classic package is not present, and futureproof - code should handle that case gracefully. + babase.app.classic. Note that it is possible for babase.app.classic to + be None if the classic package is not present, and code should handle + that case gracefully. """ # pylint: disable=too-many-public-methods - @dataclass - class V2AuthRequest: - """What is passed in to V2 auth handler.""" - - #: V2 account id of the connecting account (a-XXX). - account_id: str - - #: Globally unique tag of the connecting account. - account_tag: str - - #: When the connecting account was created. - account_create_time: datetime.datetime - - #: Total number of days the connecting account has been active. - account_total_active_days: int - - #: An abstract value generated from the connecting client's - #: app-instance-id. Can be used to identify repeat connection - #: attempts/etc. Note that this value changes each time the - #: client re-launches the app. - app_instance_signature: str - - #: An abstract value generated from the connecting client's ip - #: address. Can be used to identify repeat connection - #: attempts/etc. - address_signature: str - - #: An abstract value generated from the connecting client's - #: device. Can be used to identify repeat connection - #: attempts/etc. This value may change over time for a given - #: device but should be mostly constant. - device_signature: str - - @dataclass - class V2AuthResponse: - """What a V2 auth handler returns.""" - - #: Whether to allow this client to enter the server. - allow: bool - - #: A message to be shown to the client if allow is False. A - #: defualt rejection message will be shown if none is present - #: here. This will be translated using the 'serverResponses' - #: translation category so it can be good to use one of the - #: entries there if your server has multilingual users. - error_message: str | None = None - - @dataclass - class V2AuthData: - """Authenticated data we store for accepted clients.""" - - account_id: str - account_tag: str - player_profiles: dict - expire_time: float - from baclassic._music import MusicPlayMode def __init__(self) -> None: @@ -154,21 +92,6 @@ class ClassicAppSubsystem(babase.AppSubsystem): # Server Mode. self.server: ServerController | None = None - #: V2 authentication handler. - #: - #: To customize who is allowed in your server, assign your own - #: custom handler function to this attribute. This will be called - #: for all connecting clients before they are allowed in the - #: game. Note that protocol must be set to 36 or newer and - #: authenticate_clients must be enabled. For servers you can set - #: those values in the server config. - self.v2_auth_handler: Callable[ - [ClassicAppSubsystem.V2AuthRequest], - Awaitable[ClassicAppSubsystem.V2AuthResponse], - ] = self.default_v2_auth_handler - self.v2_auth_datas: dict[str, ClassicAppSubsystem.V2AuthData] = {} - - # Logging/debugging. self.log_have_new = False self.log_upload_timer_started = False self.printed_live_object_warning = False @@ -203,48 +126,8 @@ class ClassicAppSubsystem(babase.AppSubsystem): self.pro_sale_start_time: int | None = None self.pro_sale_start_val: int | None = None - async def default_v2_auth_handler( - self, request: V2AuthRequest - ) -> V2AuthResponse: - """Default auth handler function. Just allows everyone.""" - - # A custom handler would look at request here to determine - # whether to let this client in. - del request # Unused. - - return self.V2AuthResponse(allow=True) - - @final - async def run_v2_auth_handler( - self, request: V2AuthRequest, player_profiles: Any, token: str - ) -> V2AuthResponse: - """:meta private:""" - assert babase.in_logic_thread() - result = await self.v2_auth_handler(request) - - # If it was accepted, keep their auth data around just long enough - # for them to connect. - if result.allow: - now = time.monotonic() - self.v2_auth_datas[token] = self.V2AuthData( - account_id=request.account_id, - account_tag=request.account_tag, - player_profiles=player_profiles, - expire_time=now + 30.0, - ) - - # Lazily prune expired auth-data. - if any(a.expire_time <= now for a in self.v2_auth_datas.values()): - self.v2_auth_datas = { - k: v - for k, v in self.v2_auth_datas.items() - if v.expire_time > now - } - - return result - def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: - """:meta private:""" + """(internal)""" # If there's no main window up, just call immediately. if not babase.app.ui_v1.has_main_window(): @@ -288,7 +171,7 @@ class ClassicAppSubsystem(babase.AppSubsystem): return self._env['platform'] def scene_v1_protocol_version(self) -> int: - """:meta private:""" + """(internal)""" return bascenev1.protocol_version() @property @@ -525,7 +408,7 @@ class ClassicAppSubsystem(babase.AppSubsystem): # Otherwise just force the issue. else: babase.pushcall( - babase.CallStrict(bascenev1.new_host_session, MainMenuSession) + babase.Call(bascenev1.new_host_session, MainMenuSession) ) def getmaps(self, playtype: str) -> list[str]: @@ -578,7 +461,7 @@ class ClassicAppSubsystem(babase.AppSubsystem): ) def game_begin_analytics(self) -> None: - """:meta private:""" + """(internal)""" from baclassic import _analytics _analytics.game_begin_analytics() @@ -770,11 +653,11 @@ class ClassicAppSubsystem(babase.AppSubsystem): return bascenev1.get_player_profile_colors(profilename, profiles) def get_foreground_host_session(self) -> bascenev1.Session | None: - """:meta private:""" + """(internal)""" return bascenev1.get_foreground_host_session() def get_foreground_host_activity(self) -> bascenev1.Activity | None: - """:meta private:""" + """(internal)""" return bascenev1.get_foreground_host_activity() def value_test( @@ -783,26 +666,26 @@ class ClassicAppSubsystem(babase.AppSubsystem): change: float | None = None, absolute: float | None = None, ) -> float: - """:meta private:""" + """(internal)""" return _baclassic.value_test(arg, change, absolute) def set_master_server_source(self, source: int) -> None: - """:meta private:""" + """(internal)""" bascenev1.set_master_server_source(source) def get_game_port(self) -> int: - """:meta private:""" + """(internal)""" return bascenev1.get_game_port() def v2_upgrade_window(self, login_name: str, code: str) -> None: - """:meta private:""" + """(internal)""" from bauiv1lib.v2upgrade import V2UpgradeWindow V2UpgradeWindow(login_name, code) def server_dialog(self, delay: float, data: dict[str, Any]) -> None: - """:meta private:""" + """(internal)""" from bauiv1lib.serverdialog import ( ServerDialogData, ServerDialogWindow, @@ -819,17 +702,17 @@ class ClassicAppSubsystem(babase.AppSubsystem): if sddata is not None: babase.apptimer( delay, - babase.CallStrict(ServerDialogWindow, sddata), + babase.Call(ServerDialogWindow, sddata), ) def show_url_window(self, address: str) -> None: - """:meta private:""" + """(internal)""" from bauiv1lib.url import ShowURLWindow ShowURLWindow(address) def quit_window(self, quit_type: babase.QuitType) -> None: - """:meta private:""" + """(internal)""" from bauiv1lib.confirm import QuitWindow QuitWindow(quit_type) @@ -845,7 +728,7 @@ class ClassicAppSubsystem(babase.AppSubsystem): offset: tuple[float, float] = (0.0, 0.0), on_close_call: Callable[[], Any] | None = None, ) -> None: - """:meta private:""" + """(internal)""" from bauiv1lib.tournamententry import TournamentEntryWindow TournamentEntryWindow( @@ -859,7 +742,7 @@ class ClassicAppSubsystem(babase.AppSubsystem): ) def get_main_menu_session(self) -> type[bascenev1.Session]: - """:meta private:""" + """(internal)""" from bascenev1lib.mainmenu import MainMenuSession return MainMenuSession @@ -868,13 +751,10 @@ class ClassicAppSubsystem(babase.AppSubsystem): self, transition: str = 'in_right', origin_widget: bauiv1.Widget | None = None, - # selected_profile: str | None = None, + selected_profile: str | None = None, ) -> None: """Pop up a browser window from within a game.""" - import bacommon.docui.v1 as dui1 - - # from bauiv1lib.profile.browser import ProfileBrowserWindow - from bauiv1lib.inventory import InventoryUIController + from bauiv1lib.profile.browser import ProfileBrowserWindow main_window = babase.app.ui_v1.get_main_window() if main_window is not None: @@ -885,16 +765,15 @@ class ClassicAppSubsystem(babase.AppSubsystem): return babase.app.ui_v1.set_main_window( - InventoryUIController(player_profiles_only=True).create_window( - dui1.Request('/'), - uiopenstateid='classicinventory', + ProfileBrowserWindow( transition=transition, + selected_profile=selected_profile, origin_widget=origin_widget, + minimal_toolbar=True, ), is_top_level=True, back_state=None, suppress_warning=True, - extra_type_id=InventoryUIController.get_window_extra_type_id(), ) def preload_map_preview_media(self) -> None: @@ -910,7 +789,7 @@ class ClassicAppSubsystem(babase.AppSubsystem): logging.exception('Error preloading map preview media.') def party_icon_activate(self, origin: Sequence[float]) -> None: - """:meta private:""" + """(internal)""" from bauiv1lib.party import PartyWindow from babase import app @@ -931,7 +810,7 @@ class ClassicAppSubsystem(babase.AppSubsystem): self.party_window = weakref.ref(PartyWindow(origin=origin)) def request_main_ui(self) -> None: - """:meta private:""" + """(internal)""" from bauiv1lib.ingamemenu import InGameMenuWindow assert babase.app is not None @@ -956,7 +835,6 @@ class ClassicAppSubsystem(babase.AppSubsystem): suppress_warning=True, # Reset selections to default for consistency. restore_shared_state=False, - extra_type_id='', ) def save_ui_state(self) -> None: @@ -998,17 +876,11 @@ class ClassicAppSubsystem(babase.AppSubsystem): is_top_level=True, back_state=None, suppress_warning=True, - extra_type_id='', ) else: # If there's a saved ui state, restore that. if self.saved_ui_state is not None: app.ui_v1.restore_main_window_state(self.saved_ui_state) - # Kill the state now that we're back; we'll - # generate a new one when we leave. This keeps - # UIOpenStates stored in the state doing the - # right thing. - self.saved_ui_state = None else: # Otherwise start fresh at the main menu. from bauiv1lib.mainmenu import MainMenuWindow @@ -1018,32 +890,25 @@ class ClassicAppSubsystem(babase.AppSubsystem): is_top_level=True, back_state=None, suppress_warning=True, - extra_type_id='', ) @staticmethod def run_bs_client_effects( - effects: list[clfx.Effect], delay: float = 0.0 + effects: list[bacommon.bs.ClientEffect], delay: float = 0.0 ) -> None: - """Run client effects sent from the master server. - - :meta private: - """ + """Run client effects sent from the master server.""" from baclassic._clienteffect import run_bs_client_effects run_bs_client_effects(effects, delay=delay) @staticmethod def basic_client_ui_button_label_str( - label: bcdlg.ButtonLabel, + label: bacommon.bs.BasicCloudDialog.ButtonLabel, ) -> babase.Lstr: - """Given a client-ui label, return an Lstr. + """Given a client-ui label, return an Lstr.""" + import bacommon.bs - :meta private: - """ - import bacommon.clouddialog.basic as bcdlg - - cls = bcdlg.ButtonLabel + cls = bacommon.bs.BasicCloudDialog.ButtonLabel if label is cls.UNKNOWN: # Server should not be sending us unknown stuff; make noise # if they do. diff --git a/dist/ba_data/python/baclassic/_benchmark.py b/dist/ba_data/python/baclassic/_benchmark.py index 91be0c6..b110e25 100644 --- a/dist/ba_data/python/baclassic/_benchmark.py +++ b/dist/ba_data/python/baclassic/_benchmark.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Benchmark/Stress-Test related functionality.""" - from __future__ import annotations import random @@ -131,9 +130,9 @@ def _start_stress_test(args: _StressTestArgs) -> None: appconfig['Team Tournament Playlist Randomize'] = 1 babase.apptimer( 1.0, - babase.CallStrict( + babase.Call( babase.pushcall, - babase.CallStrict(bascenev1.new_host_session, DualTeamSession), + babase.Call(bascenev1.new_host_session, DualTeamSession), ), ) else: @@ -141,22 +140,18 @@ def _start_stress_test(args: _StressTestArgs) -> None: appconfig['Free-for-All Playlist Randomize'] = 1 babase.apptimer( 1.0, - babase.CallStrict( + babase.Call( babase.pushcall, - babase.CallStrict( - bascenev1.new_host_session, FreeForAllSession - ), + babase.Call(bascenev1.new_host_session, FreeForAllSession), ), ) _baclassic.set_stress_testing(True, args.player_count, args.attract_mode) classic.stress_test_update_timer = babase.AppTimer( - args.round_duration, babase.CallStrict(_reset_stress_test, args) + args.round_duration, babase.Call(_reset_stress_test, args) ) if args.attract_mode: classic.stress_test_update_timer_2 = babase.AppTimer( - 0.48, - babase.CallStrict(_update_attract_mode_test, args), - repeat=True, + 0.48, babase.Call(_update_attract_mode_test, args), repeat=True ) @@ -181,7 +176,7 @@ def _reset_stress_test(args: _StressTestArgs) -> None: # we just end back at the main menu. If things are idle there then # we'll get sent back to a new stress test. if not args.attract_mode: - babase.apptimer(1.0, babase.CallStrict(_start_stress_test, args)) + babase.apptimer(1.0, babase.Call(_start_stress_test, args)) def run_media_reload_benchmark() -> None: @@ -205,8 +200,8 @@ def run_media_reload_benchmark() -> None: color=(1, 1, 0), ) - babase.add_clean_frame_callback(babase.CallStrict(doit, start_time)) + babase.add_clean_frame_callback(babase.Call(doit, start_time)) # The reload starts (should add a completion callback to the reload # func to fix this). - babase.apptimer(0.05, babase.CallStrict(delay_add, babase.apptime())) + babase.apptimer(0.05, babase.Call(delay_add, babase.apptime())) diff --git a/dist/ba_data/python/baclassic/_chest.py b/dist/ba_data/python/baclassic/_chest.py index 0fed2f6..b5e18ce 100644 --- a/dist/ba_data/python/baclassic/_chest.py +++ b/dist/ba_data/python/baclassic/_chest.py @@ -1,13 +1,12 @@ # Released under the MIT License. See LICENSE for details. # """Chest related functionality.""" - from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING -from bacommon.classic import ClassicChestAppearance +from bacommon.bs import ClassicChestAppearance if TYPE_CHECKING: pass diff --git a/dist/ba_data/python/baclassic/_clienteffect.py b/dist/ba_data/python/baclassic/_clienteffect.py index da91995..50e3be7 100644 --- a/dist/ba_data/python/baclassic/_clienteffect.py +++ b/dist/ba_data/python/baclassic/_clienteffect.py @@ -9,25 +9,26 @@ from typing import TYPE_CHECKING, assert_never from efro.util import strict_partial +import bacommon.bs import bauiv1 import _baclassic if TYPE_CHECKING: - import bacommon.clienteffect as clfx + pass def run_bs_client_effects( - effects: list[clfx.Effect], delay: float = 0.0 + effects: list[bacommon.bs.ClientEffect], delay: float = 0.0 ) -> None: """Run effects.""" # pylint: disable=too-many-branches - import bacommon.clienteffect as clfx + from bacommon.bs import ClientEffectTypeID for effect in effects: effecttype = effect.get_type_id() - if effecttype is clfx.EffectTypeID.LEGACY_SCREEN_MESSAGE: - assert isinstance(effect, clfx.LegacyScreenMessage) + if effecttype is ClientEffectTypeID.SCREEN_MESSAGE: + assert isinstance(effect, bacommon.bs.ClientEffectScreenMessage) textfin = bauiv1.Lstr( translate=('serverResponses', effect.message) ).evaluate() @@ -45,33 +46,22 @@ def run_bs_client_effects( bauiv1.screenmessage, textfin, color=effect.color ), ) - elif effecttype is clfx.EffectTypeID.SCREEN_MESSAGE: - assert isinstance(effect, clfx.ScreenMessage) - bauiv1.apptimer( - delay, - strict_partial( - bauiv1.screenmessage, - effect.message, - color=effect.color, - literal=not effect.is_lstr, - ), - ) - elif effecttype is clfx.EffectTypeID.SOUND: - assert isinstance(effect, clfx.PlaySound) - scls = clfx.Sound + elif effecttype is ClientEffectTypeID.SOUND: + assert isinstance(effect, bacommon.bs.ClientEffectSound) + smcls = bacommon.bs.ClientEffectSound.Sound soundfile: str | None = None - if effect.sound is scls.UNKNOWN: + if effect.sound is smcls.UNKNOWN: # Server should avoid sending us sounds we don't # support. Make some noise if it happens. - logging.error('Got unrecognized bacommon.classic.Sound.') - elif effect.sound is scls.CASH_REGISTER: + logging.error('Got unrecognized bacommon.bs.ClientEffectSound.') + elif effect.sound is smcls.CASH_REGISTER: soundfile = 'cashRegister' - elif effect.sound is scls.ERROR: + elif effect.sound is smcls.ERROR: soundfile = 'error' - elif effect.sound is scls.POWER_DOWN: + elif effect.sound is smcls.POWER_DOWN: soundfile = 'powerdown01' - elif effect.sound is scls.GUN_COCKING: + elif effect.sound is smcls.GUN_COCKING: soundfile = 'gunCocking' else: assert_never(effect.sound) @@ -83,12 +73,14 @@ def run_bs_client_effects( ), ) - elif effecttype is clfx.EffectTypeID.DELAY: - assert isinstance(effect, clfx.Delay) + elif effecttype is ClientEffectTypeID.DELAY: + assert isinstance(effect, bacommon.bs.ClientEffectDelay) delay += effect.seconds - elif effecttype is clfx.EffectTypeID.CHEST_WAIT_TIME_ANIMATION: - assert isinstance(effect, clfx.ChestWaitTimeAnimation) + elif effecttype is ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION: + assert isinstance( + effect, bacommon.bs.ClientEffectChestWaitTimeAnimation + ) bauiv1.apptimer( delay, strict_partial( @@ -100,8 +92,8 @@ def run_bs_client_effects( ), ) - elif effecttype is clfx.EffectTypeID.TICKETS_ANIMATION: - assert isinstance(effect, clfx.TicketsAnimation) + elif effecttype is ClientEffectTypeID.TICKETS_ANIMATION: + assert isinstance(effect, bacommon.bs.ClientEffectTicketsAnimation) bauiv1.apptimer( delay, strict_partial( @@ -112,8 +104,8 @@ def run_bs_client_effects( ), ) - elif effecttype is clfx.EffectTypeID.TOKENS_ANIMATION: - assert isinstance(effect, clfx.TokensAnimation) + elif effecttype is ClientEffectTypeID.TOKENS_ANIMATION: + assert isinstance(effect, bacommon.bs.ClientEffectTokensAnimation) bauiv1.apptimer( delay, strict_partial( @@ -124,11 +116,12 @@ def run_bs_client_effects( ), ) - elif effecttype is clfx.EffectTypeID.UNKNOWN: + elif effecttype is ClientEffectTypeID.UNKNOWN: # Server should not send us stuff we can't digest. Make # some noise if it happens. logging.error( - 'Got unrecognized bacommon.classic.Effect; should not happen.' + 'Got unrecognized bacommon.bs.ClientEffect;' + ' should not happen.' ) else: diff --git a/dist/ba_data/python/baclassic/_displayitem.py b/dist/ba_data/python/baclassic/_displayitem.py index 195ddbd..da8215c 100644 --- a/dist/ba_data/python/baclassic/_displayitem.py +++ b/dist/ba_data/python/baclassic/_displayitem.py @@ -1,33 +1,28 @@ # Released under the MIT License. See LICENSE for details. # """Display-item related functionality.""" - from __future__ import annotations -from typing import TYPE_CHECKING, assert_never +from typing import TYPE_CHECKING from efro.util import pairs_from_flat -import bacommon.displayitem as ditm -import bacommon.classic +import bacommon.bs import bauiv1 + if TYPE_CHECKING: pass -# FIXME - migrate to use the doc-ui rendering for these instead. def show_display_item( - itemwrapper: ditm.Wrapper, + itemwrapper: bacommon.bs.DisplayItemWrapper, parent: bauiv1.Widget, pos: tuple[float, float], width: float, - debug: bool = False, ) -> None: """Create ui to depict a display-item.""" - # pylint: disable=too-many-locals - # Let's go with 4:3 aspect ratio. - height = width * 0.75 + height = width * 0.666 # Silent no-op if our parent ui is dead. if not parent: @@ -38,24 +33,15 @@ def show_display_item( text_y_offs = 0.0 show_text = True - itemtype = itemwrapper.item.get_type_id() - - if itemtype is ditm.ItemTypeID.TICKETS: + if isinstance(itemwrapper.item, bacommon.bs.TicketsDisplayItem): img = 'tickets' img_y_offs = width * 0.11 text_y_offs = width * -0.15 - elif itemtype is ditm.ItemTypeID.TICKETS_PURPLE: - img = 'ticketsPurple' - img_y_offs = width * 0.11 - text_y_offs = width * -0.15 - elif itemtype is ditm.ItemTypeID.TOKENS: + elif isinstance(itemwrapper.item, bacommon.bs.TokensDisplayItem): img = 'coin' img_y_offs = width * 0.11 text_y_offs = width * -0.15 - elif itemtype is ditm.ItemTypeID.CHEST: - assert isinstance( - itemwrapper.item, bacommon.classic.ClassicChestDisplayItem - ) + elif isinstance(itemwrapper.item, bacommon.bs.ChestDisplayItem): from baclassic._chest import ( CHEST_APPEARANCE_DISPLAY_INFOS, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, @@ -77,14 +63,9 @@ def show_display_item( tint_color=c_info.tint, tint2_color=c_info.tint2, ) - elif ( - itemtype is ditm.ItemTypeID.TEST or itemtype is ditm.ItemTypeID.UNKNOWN - ): - pass - else: - assert_never(itemtype) - if debug: + # Enable this for testing spacing. + if bool(False): bauiv1.imagewidget( parent=parent, position=( diff --git a/dist/ba_data/python/baclassic/_hooks.py b/dist/ba_data/python/baclassic/_hooks.py index f5b002c..2b37f25 100644 --- a/dist/ba_data/python/baclassic/_hooks.py +++ b/dist/ba_data/python/baclassic/_hooks.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Hooks for C++ layer to use for ClassicAppMode.""" - from __future__ import annotations import logging diff --git a/dist/ba_data/python/baclassic/_input.py b/dist/ba_data/python/baclassic/_input.py index feaa6d5..0effcb5 100644 --- a/dist/ba_data/python/baclassic/_input.py +++ b/dist/ba_data/python/baclassic/_input.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Input related functionality""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/baclassic/_music.py b/dist/ba_data/python/baclassic/_music.py index 4a2ffce..5d63316 100644 --- a/dist/ba_data/python/baclassic/_music.py +++ b/dist/ba_data/python/baclassic/_music.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Music related functionality.""" - from __future__ import annotations import copy diff --git a/dist/ba_data/python/baclassic/_net.py b/dist/ba_data/python/baclassic/_net.py index 2ef3250..4c1efc6 100644 --- a/dist/ba_data/python/baclassic/_net.py +++ b/dist/ba_data/python/baclassic/_net.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Networking related functionality.""" - from __future__ import annotations import zlib @@ -15,7 +14,7 @@ from typing import TYPE_CHECKING, override from efro.error import CommunicationError from efro.util import strip_exception_tracebacks -import bacommon.classic +import bacommon.bs import babase import bascenev1 @@ -114,7 +113,7 @@ class MasterServerV1CallThread(threading.Thread): dataenc = urllib.parse.urlencode(self._data) mresponse = plus.cloud.send_message( - bacommon.classic.LegacyRequest( + bacommon.bs.LegacyRequest( self._request, self._request_type, classic.legacy_user_agent_string, @@ -169,7 +168,7 @@ class MasterServerV1CallThread(threading.Thread): if self._callback is not None: babase.pushcall( - babase.CallStrict(self._run_callback, response_data), + babase.Call(self._run_callback, response_data), from_other_thread=True, ) diff --git a/dist/ba_data/python/baclassic/_servermode.py b/dist/ba_data/python/baclassic/_servermode.py index a1eb898..e0c39a0 100644 --- a/dist/ba_data/python/baclassic/_servermode.py +++ b/dist/ba_data/python/baclassic/_servermode.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to running the game in server-mode.""" - from __future__ import annotations import sys @@ -108,12 +107,6 @@ class ServerController: self._playlist_fetch_got_response = False self._playlist_fetch_code = -1 - # We do most configuration *after* we've fetched playlists or - # whatever other prep stuff we need to do, but this we should - # set immediately; otherwise unauthenticated clients can sneak - # into auth-enabled servers while they're bootstrapping. - bascenev1.set_authenticate_clients(self._config.authenticate_clients) - # Now sit around doing any pre-launch prep such as waiting for # account sign-in or fetching playlists; this will kick off the # session once done. @@ -434,6 +427,8 @@ class ServerController: classic.teams_series_length = self._config.teams_series_length classic.ffa_series_length = self._config.ffa_series_length + bascenev1.set_authenticate_clients(self._config.authenticate_clients) + bascenev1.set_enable_default_kick_voting( self._config.enable_default_kick_voting ) @@ -456,6 +451,7 @@ class ServerController: bascenev1.set_player_rejoin_cooldown( self._config.player_rejoin_cooldown ) + bascenev1.set_max_players_override( self._config.session_max_players_override ) @@ -474,6 +470,6 @@ class ServerController: bascenev1.new_host_session(sessiontype) # Run an access check if we're trying to make a public party. - if self._config.party_is_public and not self._ran_access_check: + if not self._ran_access_check : self._run_access_check() self._ran_access_check = True diff --git a/dist/ba_data/python/baclassic/_store.py b/dist/ba_data/python/baclassic/_store.py index 6ed4dc5..5dda5b5 100644 --- a/dist/ba_data/python/baclassic/_store.py +++ b/dist/ba_data/python/baclassic/_store.py @@ -292,18 +292,6 @@ class StoreSubsystem: 'icons.explodinary': { 'icon': babase.charstr(babase.SpecialChar.EXPLODINARY_LOGO) }, - 'icons.santa_hat': { - 'icon': babase.charstr(babase.SpecialChar.SANTA_HAT) - }, - 'icons.potato': { - 'icon': babase.charstr(babase.SpecialChar.POTATO) - }, - 'icons.palm_tree': { - 'icon': babase.charstr(babase.SpecialChar.PALM_TREE) - }, - 'icons.boxing_glove': { - 'icon': babase.charstr(babase.SpecialChar.BOXING_GLOVE) - }, } return babase.app.classic.store_items @@ -581,7 +569,7 @@ class StoreSubsystem: def get_unowned_maps(self) -> list[str]: """Return the list of local maps not owned by the current account.""" classic = babase.app.classic - purchases = classic.purchases if classic is not None else frozenset() + purchases = classic.purchases if classic is not None else set() unowned_maps: set[str] = set() if babase.app.env.gui: for map_section in self.get_store_layout()['maps']: @@ -595,9 +583,7 @@ class StoreSubsystem: """Return present game types not owned by the current account.""" try: classic = babase.app.classic - purchases = ( - classic.purchases if classic is not None else frozenset() - ) + purchases = classic.purchases if classic is not None else set() unowned_games: set[type[bascenev1.GameActivity]] = set() if babase.app.env.gui: for section in self.get_store_layout()['minigames']: diff --git a/dist/ba_data/python/baclassic/_tips.py b/dist/ba_data/python/baclassic/_tips.py index ae18032..5936c9c 100644 --- a/dist/ba_data/python/baclassic/_tips.py +++ b/dist/ba_data/python/baclassic/_tips.py @@ -3,7 +3,6 @@ """Functionality related to classic game tips. These can be shown at opportune times such as between rounds.""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/baclassic/_tournament.py b/dist/ba_data/python/baclassic/_tournament.py index d1b7aea..e71d960 100644 --- a/dist/ba_data/python/baclassic/_tournament.py +++ b/dist/ba_data/python/baclassic/_tournament.py @@ -6,7 +6,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from bacommon.classic import ClassicChestAppearance +from bacommon.bs import ClassicChestAppearance import babase import bauiv1 import bascenev1 diff --git a/dist/ba_data/python/baclassic/macmusicapp.py b/dist/ba_data/python/baclassic/macmusicapp.py index bcde38d..48a41e5 100644 --- a/dist/ba_data/python/baclassic/macmusicapp.py +++ b/dist/ba_data/python/baclassic/macmusicapp.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Music playback functionality using the Mac Music (formerly iTunes) app.""" - from __future__ import annotations import logging @@ -96,7 +95,7 @@ class _MacMusicAppThread(threading.Thread): def do_print() -> None: babase.apptimer( 0.5, - babase.CallStrict( + babase.Call( babase.screenmessage, babase.Lstr(resource='usingItunesText'), (0, 1, 0), @@ -199,9 +198,7 @@ class _MacMusicAppThread(threading.Thread): except Exception as exc: print('Error getting iTunes playlists:', exc) playlists = [] - babase.pushcall( - babase.CallStrict(target, playlists), from_other_thread=True - ) + babase.pushcall(babase.Call(target, playlists), from_other_thread=True) def _handle_play_command(self, target: str | None) -> None: if target is None: @@ -249,7 +246,7 @@ class _MacMusicAppThread(threading.Thread): pass else: babase.pushcall( - babase.CallStrict( + babase.Call( babase.screenmessage, babase.app.lang.get_resource('playlistNotFoundText') + ': \'' diff --git a/dist/ba_data/python/baclassic/osmusic.py b/dist/ba_data/python/baclassic/osmusic.py index c48b2fc..8a79bf7 100644 --- a/dist/ba_data/python/baclassic/osmusic.py +++ b/dist/ba_data/python/baclassic/osmusic.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Music playback using OS functionality exposed through the C++ layer.""" - from __future__ import annotations import os @@ -152,7 +151,7 @@ class _PickFolderSongThread(threading.Thread): ).evaluate() ) babase.pushcall( - babase.CallStrict(self._callback, all_files, None), + babase.Call(self._callback, all_files, None), from_other_thread=True, ) except Exception as exc: @@ -163,6 +162,6 @@ class _PickFolderSongThread(threading.Thread): except Exception: err_str = '' babase.pushcall( - babase.CallStrict(self._callback, self._path, err_str), + babase.Call(self._callback, self._path, err_str), from_other_thread=True, ) diff --git a/dist/ba_data/python/bacommon/analytics.py b/dist/ba_data/python/bacommon/analytics.py deleted file mode 100644 index fef2a9d..0000000 --- a/dist/ba_data/python/bacommon/analytics.py +++ /dev/null @@ -1,75 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Analytics support.""" - -from __future__ import annotations - -from typing import assert_never, override, Annotated - -from enum import Enum, unique -from dataclasses import dataclass - -from efro.dataclassio import ioprepped, IOMultiType, IOAttrs - - -class AnalyticsEventTypeID(Enum): - """Type ID for each of our subclasses.""" - - CLASSIC = 'c' - - -class AnalyticsEvent(IOMultiType[AnalyticsEventTypeID]): - """Top level class for our multitype.""" - - @override - @classmethod - def get_type_id(cls) -> AnalyticsEventTypeID: - # Require child classes to supply this themselves. If we did a - # full type registry/lookup here it would require us to import - # everything and would prevent lazy loading. - raise NotImplementedError() - - @override - @classmethod - def get_type(cls, type_id: AnalyticsEventTypeID) -> type[AnalyticsEvent]: - """Return the subclass for each of our type-ids.""" - # pylint: disable=cyclic-import - - t = AnalyticsEventTypeID - if type_id is t.CLASSIC: - return ClassicAnalyticsEvent - - # Important to make sure we provide all types. - assert_never(type_id) - - @override - @classmethod - def get_type_id_storage_name(cls) -> str: - return '_t' - - -@ioprepped -@dataclass -class ClassicAnalyticsEvent(AnalyticsEvent): - """Analytics event related to classic.""" - - @unique - class EventType(Enum): - """Types of classic events.""" - - JOIN_PUBLIC_PARTY = 'jpb' - JOIN_PRIVATE_PARTY = 'jpr' - JOIN_PARTY_BY_ADDRESS = 'ja' - JOIN_NEARBY_PARTY = 'jn' - START_TEAMS_SESSION = 'st' - START_FFA_SESSION = 'sf' - START_COOP_SESSION = 'sc' - START_TOURNEY_COOP_SESSION = 'stc' - - eventtype: Annotated[EventType, IOAttrs('t')] - extra: Annotated[str | None, IOAttrs('e', store_default=False)] = None - - @override - @classmethod - def get_type_id(cls) -> AnalyticsEventTypeID: - return AnalyticsEventTypeID.CLASSIC diff --git a/dist/ba_data/python/bacommon/assets.py b/dist/ba_data/python/bacommon/assets.py index 160a9e5..159307d 100644 --- a/dist/ba_data/python/bacommon/assets.py +++ b/dist/ba_data/python/bacommon/assets.py @@ -1,12 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Functionality related to cloud based assets. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" +"""Functionality related to cloud based assets.""" from __future__ import annotations diff --git a/dist/ba_data/python/bacommon/bacloud.py b/dist/ba_data/python/bacommon/bacloud.py index 37f1450..0ea6e86 100644 --- a/dist/ba_data/python/bacommon/bacloud.py +++ b/dist/ba_data/python/bacommon/bacloud.py @@ -1,12 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Functionality related to the bacloud tool. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" +"""Functionality related to the bacloud tool.""" from __future__ import annotations @@ -20,7 +14,7 @@ if TYPE_CHECKING: # Version is sent to the master-server with all commands. Can be incremented # if we need to change behavior server-side to go along with client changes. -BACLOUD_VERSION = 14 +BACLOUD_VERSION = 13 def asset_file_cache_path(filehash: str) -> str: @@ -97,30 +91,12 @@ class ResponseData: #: response processing (including error handling) occurs. message: Annotated[str | None, IOAttrs('m', store_default=False)] = None - #: Value for the 'end' arg of the message print() call. + #: End arg for message print() call. message_end: Annotated[str, IOAttrs('m_end', store_default=False)] = '\n' - #: If present, client should print this message before any other - #: response processing (including error handling) occurs. - message_stderr: Annotated[ - str | None, IOAttrs('m2', store_default=False) - ] = None - - #: Value for the 'end' arg of the message print() call. - message_stderr_end: Annotated[ - str, IOAttrs('m2_end', store_default=False) - ] = '\n' - - #: If present, client should abort with this error message and - #: return-code 2. + #: If present, client should abort with this error message. error: Annotated[str | None, IOAttrs('e', store_default=False)] = None - #: If present for an interactive command, specifies the return code - #: for the process. Note that this only applies if error is not set. - #: Standard return codes are 0 for success, 1 for a successful run - #: but negative result, and 2 for errors. - return_code: Annotated[int | None, IOAttrs('r', store_default=False)] = None - #: How long to wait before proceeding with remaining response (can #: be useful when waiting for server progress in a loop). delay_seconds: Annotated[float, IOAttrs('d', store_default=False)] = 0.0 @@ -194,17 +170,6 @@ class ResponseData: #: End arg for end_message print() call. end_message_end: Annotated[str, IOAttrs('eme', store_default=False)] = '\n' - #: If present, a message that should be printed after all other - #: response processing is done. - end_message_stderr: Annotated[ - str | None, IOAttrs('em2', store_default=False) - ] = None - - #: End arg for end_message print() call. - end_message_stderr_end: Annotated[ - str, IOAttrs('em2e', store_default=False) - ] = '\n' - #: If present, this command is run with these args at the end of #: response processing. end_command: Annotated[ diff --git a/dist/ba_data/python/bacommon/bs.py b/dist/ba_data/python/bacommon/bs.py new file mode 100644 index 0000000..0bba7bf --- /dev/null +++ b/dist/ba_data/python/bacommon/bs.py @@ -0,0 +1,1062 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=too-many-lines +"""BombSquad specific bits.""" + +from __future__ import annotations + +import datetime +from enum import Enum +from dataclasses import dataclass, field +from typing import Annotated, override, assert_never + +from efro.util import pairs_to_flat +from efro.dataclassio import ioprepped, IOAttrs, IOMultiType +from efro.message import Message, Response + +# Token counts for our various packs. +TOKENS1_COUNT = 50 +TOKENS2_COUNT = 500 +TOKENS3_COUNT = 1200 +TOKENS4_COUNT = 2600 + + +@ioprepped +@dataclass +class LegacyRequest(Message): + """A generic request for the legacy master server.""" + + request: Annotated[str, IOAttrs('r')] + request_type: Annotated[str, IOAttrs('t')] + user_agent_string: Annotated[str, IOAttrs('u')] + data: Annotated[str, IOAttrs('d')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [LegacyResponse] + + +@ioprepped +@dataclass +class LegacyResponse(Response): + """Response for generic legacy request.""" + + data: Annotated[str | None, IOAttrs('d')] + zipped: Annotated[bool, IOAttrs('z')] + + +@ioprepped +@dataclass +class PrivatePartyMessage(Message): + """Message asking about info we need for private-party UI.""" + + need_datacode: Annotated[bool, IOAttrs('d')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [PrivatePartyResponse] + + +@ioprepped +@dataclass +class PrivatePartyResponse(Response): + """Here's that private party UI info you asked for, boss.""" + + success: Annotated[bool, IOAttrs('s')] + tokens: Annotated[int, IOAttrs('t')] + gold_pass: Annotated[bool, IOAttrs('g')] + datacode: Annotated[str | None, IOAttrs('d')] + + +@ioprepped +@dataclass +class GetClassicPurchasesMessage(Message): + """Asking for current account's classic purchases.""" + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [GetClassicPurchasesResponse] + + +@ioprepped +@dataclass +class GetClassicPurchasesResponse(Response): + """Here's those classic purchases ya asked for boss.""" + + purchases: Annotated[set[str], IOAttrs('p')] + + +class ClassicChestAppearance(Enum): + """Appearances bombsquad classic chests can have.""" + + UNKNOWN = 'u' + DEFAULT = 'd' + L1 = 'l1' + L2 = 'l2' + L3 = 'l3' + L4 = 'l4' + L5 = 'l5' + L6 = 'l6' + + @property + def pretty_name(self) -> str: + """Pretty name for the chest in English.""" + # pylint: disable=too-many-return-statements + cls = type(self) + + if self is cls.UNKNOWN: + return 'Unknown Chest' + if self is cls.DEFAULT: + return 'Chest' + if self is cls.L1: + return 'L1 Chest' + if self is cls.L2: + return 'L2 Chest' + if self is cls.L3: + return 'L3 Chest' + if self is cls.L4: + return 'L4 Chest' + if self is cls.L5: + return 'L5 Chest' + if self is cls.L6: + return 'L6 Chest' + + assert_never(self) + + +@ioprepped +@dataclass +class ClassicAccountLiveData: + """Live account data fed to the client in the bs classic app mode.""" + + @dataclass + class Chest: + """A lovely chest.""" + + appearance: Annotated[ + ClassicChestAppearance, + IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN), + ] + create_time: Annotated[datetime.datetime, IOAttrs('c')] + unlock_time: Annotated[datetime.datetime, IOAttrs('t')] + unlock_tokens: Annotated[int, IOAttrs('k')] + ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')] + + class LeagueType(Enum): + """Type of league we are in.""" + + BRONZE = 'b' + SILVER = 's' + GOLD = 'g' + DIAMOND = 'd' + + class Flag(Enum): + """Flags set for our account.""" + + ASK_FOR_REVIEW = 'r' + + tickets: Annotated[int, IOAttrs('ti')] + + tokens: Annotated[int, IOAttrs('to')] + gold_pass: Annotated[bool, IOAttrs('g')] + remove_ads: Annotated[bool, IOAttrs('r')] + + achievements: Annotated[int, IOAttrs('a')] + achievements_total: Annotated[int, IOAttrs('at')] + + league_type: Annotated[LeagueType | None, IOAttrs('lt')] + league_num: Annotated[int | None, IOAttrs('ln')] + league_rank: Annotated[int | None, IOAttrs('lr')] + + level: Annotated[int, IOAttrs('lv')] + xp: Annotated[int, IOAttrs('xp')] + xpmax: Annotated[int, IOAttrs('xpm')] + + inbox_count: Annotated[int, IOAttrs('ibc')] + inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')] + inbox_contains_prize: Annotated[bool, IOAttrs('icp')] + + chests: Annotated[dict[str, Chest], IOAttrs('c')] + + # State id of our purchases for builds 22459+. + purchases_state: Annotated[str | None, IOAttrs('p')] + + flags: Annotated[set[Flag], IOAttrs('f', soft_default_factory=set)] + + +class DisplayItemTypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + TICKETS = 't' + TOKENS = 'k' + TEST = 's' + CHEST = 'c' + + +class DisplayItem(IOMultiType[DisplayItemTypeID]): + """Some amount of something that can be shown or described. + + Used to depict chest contents or other rewards or prices. + """ + + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + + t = DisplayItemTypeID + if type_id is t.UNKNOWN: + return UnknownDisplayItem + if type_id is t.TICKETS: + return TicketsDisplayItem + if type_id is t.TOKENS: + return TokensDisplayItem + if type_id is t.TEST: + return TestDisplayItem + if type_id is t.CHEST: + return ChestDisplayItem + + # Important to make sure we provide all types. + assert_never(type_id) + + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + """Return a string description and subs for the item. + + These decriptions are baked into the DisplayItemWrapper and + should be accessed from there when available. This allows + clients to give descriptions even for newer display items they + don't recognize. + """ + raise NotImplementedError() + + # Implement fallbacks so client can digest item lists even if they + # contain unrecognized stuff. DisplayItemWrapper contains basic + # baked down info that they can still use in such cases. + @override + @classmethod + def get_unknown_type_fallback(cls) -> DisplayItem: + return UnknownDisplayItem() + + +@ioprepped +@dataclass +class UnknownDisplayItem(DisplayItem): + """Something we don't know how to display.""" + + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.UNKNOWN + + @override + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + import logging + + # Make noise but don't break. + logging.exception( + 'UnknownDisplayItem.get_description() should never be called.' + ' Always access descriptions on the DisplayItemWrapper.' + ) + return 'Unknown', [] + + +@ioprepped +@dataclass +class TicketsDisplayItem(DisplayItem): + """Some amount of tickets.""" + + count: Annotated[int, IOAttrs('c')] + + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.TICKETS + + @override + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + return '${C} Tickets', [('${C}', str(self.count))] + + +@ioprepped +@dataclass +class TokensDisplayItem(DisplayItem): + """Some amount of tokens.""" + + count: Annotated[int, IOAttrs('c')] + + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.TOKENS + + @override + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + return '${C} Tokens', [('${C}', str(self.count))] + + +@ioprepped +@dataclass +class TestDisplayItem(DisplayItem): + """Fills usable space for a display-item - good for calibration.""" + + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.TEST + + @override + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + return 'Test Display Item Here', [] + + +@ioprepped +@dataclass +class ChestDisplayItem(DisplayItem): + """Display a chest.""" + + appearance: Annotated[ClassicChestAppearance, IOAttrs('a')] + + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.CHEST + + @override + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + return self.appearance.pretty_name, [] + + +@ioprepped +@dataclass +class DisplayItemWrapper: + """Wraps a DisplayItem and common info.""" + + item: Annotated[DisplayItem, IOAttrs('i')] + description: Annotated[str, IOAttrs('d')] + description_subs: Annotated[list[str] | None, IOAttrs('s')] + + @classmethod + def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper: + """Convenience method to wrap a DisplayItem.""" + desc, subs = item.get_description() + return DisplayItemWrapper(item, desc, pairs_to_flat(subs)) + + +@ioprepped +@dataclass +class ChestInfoMessage(Message): + """Request info about a chest.""" + + chest_id: Annotated[str, IOAttrs('i')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [ChestInfoResponse] + + +@ioprepped +@dataclass +class ChestInfoResponse(Response): + """Here's that chest info you asked for, boss.""" + + @dataclass + class Chest: + """A lovely chest.""" + + @dataclass + class PrizeSet: + """A possible set of prizes for this chest.""" + + weight: Annotated[float, IOAttrs('w')] + contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')] + + appearance: Annotated[ + ClassicChestAppearance, + IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN), + ] + + # How much it costs to unlock *now*. + unlock_tokens: Annotated[int, IOAttrs('tk')] + + # When it unlocks on its own. + unlock_time: Annotated[datetime.datetime, IOAttrs('t')] + + # Possible prizes we contain. + prizesets: Annotated[list[PrizeSet], IOAttrs('p')] + + # Are ads allowed now? + ad_allow: Annotated[bool, IOAttrs('aa')] + + chest: Annotated[Chest | None, IOAttrs('c')] + user_tokens: Annotated[int | None, IOAttrs('t')] + + +class ClientUITypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + BASIC = 'b' + + +class ClientUI(IOMultiType[ClientUITypeID]): + """Defines some user interface on the client.""" + + @override + @classmethod + def get_type_id(cls) -> ClientUITypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + out: type[ClientUI] + + t = ClientUITypeID + if type_id is t.UNKNOWN: + out = UnknownClientUI + elif type_id is t.BASIC: + out = BasicClientUI + else: + # Important to make sure we provide all types. + assert_never(type_id) + return out + + @override + @classmethod + def get_unknown_type_fallback(cls) -> ClientUI: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return UnknownClientUI() + + +@ioprepped +@dataclass +class UnknownClientUI(ClientUI): + """Fallback type for unrecognized entries.""" + + @override + @classmethod + def get_type_id(cls) -> ClientUITypeID: + return ClientUITypeID.UNKNOWN + + +class BasicClientUIComponentTypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + TEXT = 't' + LINK = 'l' + BS_CLASSIC_TOURNEY_RESULT = 'ct' + DISPLAY_ITEMS = 'di' + EXPIRE_TIME = 'd' + + +class BasicClientUIComponent(IOMultiType[BasicClientUIComponentTypeID]): + """Top level class for our multitype.""" + + @override + @classmethod + def get_type_id(cls) -> BasicClientUIComponentTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type( + cls, type_id: BasicClientUIComponentTypeID + ) -> type[BasicClientUIComponent]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + + t = BasicClientUIComponentTypeID + if type_id is t.UNKNOWN: + return BasicClientUIComponentUnknown + if type_id is t.TEXT: + return BasicClientUIComponentText + if type_id is t.LINK: + return BasicClientUIComponentLink + if type_id is t.BS_CLASSIC_TOURNEY_RESULT: + return BasicClientUIBsClassicTourneyResult + if type_id is t.DISPLAY_ITEMS: + return BasicClientUIDisplayItems + if type_id is t.EXPIRE_TIME: + return BasicClientUIExpireTime + + # Important to make sure we provide all types. + assert_never(type_id) + + @override + @classmethod + def get_unknown_type_fallback(cls) -> BasicClientUIComponent: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return BasicClientUIComponentUnknown() + + +@ioprepped +@dataclass +class BasicClientUIComponentUnknown(BasicClientUIComponent): + """An unknown basic client component type. + + In practice these should never show up since the master-server + generates these on the fly for the client and so should not send + clients one they can't digest. + """ + + @override + @classmethod + def get_type_id(cls) -> BasicClientUIComponentTypeID: + return BasicClientUIComponentTypeID.UNKNOWN + + +@ioprepped +@dataclass +class BasicClientUIComponentText(BasicClientUIComponent): + """Show some text in the inbox message.""" + + text: Annotated[str, IOAttrs('t')] + subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field( + default_factory=list + ) + scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0 + color: Annotated[ + tuple[float, float, float, float], IOAttrs('c', store_default=False) + ] = (1.0, 1.0, 1.0, 1.0) + spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 + spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 + + @override + @classmethod + def get_type_id(cls) -> BasicClientUIComponentTypeID: + return BasicClientUIComponentTypeID.TEXT + + +@ioprepped +@dataclass +class BasicClientUIComponentLink(BasicClientUIComponent): + """Show a link in the inbox message.""" + + url: Annotated[str, IOAttrs('u')] + label: Annotated[str, IOAttrs('l')] + subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field( + default_factory=list + ) + spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 + spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 + + @override + @classmethod + def get_type_id(cls) -> BasicClientUIComponentTypeID: + return BasicClientUIComponentTypeID.LINK + + +@ioprepped +@dataclass +class BasicClientUIBsClassicTourneyResult(BasicClientUIComponent): + """Show info about a classic tourney.""" + + tournament_id: Annotated[str, IOAttrs('t')] + game: Annotated[str, IOAttrs('g')] + players: Annotated[int, IOAttrs('p')] + rank: Annotated[int, IOAttrs('r')] + trophy: Annotated[str | None, IOAttrs('tr')] + prizes: Annotated[list[DisplayItemWrapper], IOAttrs('pr')] + + @override + @classmethod + def get_type_id(cls) -> BasicClientUIComponentTypeID: + return BasicClientUIComponentTypeID.BS_CLASSIC_TOURNEY_RESULT + + +@ioprepped +@dataclass +class BasicClientUIDisplayItems(BasicClientUIComponent): + """Show some display-items.""" + + items: Annotated[list[DisplayItemWrapper], IOAttrs('d')] + width: Annotated[float, IOAttrs('w')] = 100.0 + spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 + spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 + + @override + @classmethod + def get_type_id(cls) -> BasicClientUIComponentTypeID: + return BasicClientUIComponentTypeID.DISPLAY_ITEMS + + +@ioprepped +@dataclass +class BasicClientUIExpireTime(BasicClientUIComponent): + """Show expire-time.""" + + time: Annotated[datetime.datetime, IOAttrs('d')] + spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 + spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 + + @override + @classmethod + def get_type_id(cls) -> BasicClientUIComponentTypeID: + return BasicClientUIComponentTypeID.EXPIRE_TIME + + +@ioprepped +@dataclass +class BasicClientUI(ClientUI): + """A basic UI for the client.""" + + class ButtonLabel(Enum): + """Distinct button labels we support.""" + + UNKNOWN = 'u' + OK = 'o' + APPLY = 'a' + CANCEL = 'c' + ACCEPT = 'ac' + DECLINE = 'dn' + IGNORE = 'ig' + CLAIM = 'cl' + DISCARD = 'd' + + class InteractionStyle(Enum): + """Overall interaction styles we support.""" + + UNKNOWN = 'u' + BUTTON_POSITIVE = 'p' + BUTTON_POSITIVE_NEGATIVE = 'pn' + + components: Annotated[list[BasicClientUIComponent], IOAttrs('s')] + + interaction_style: Annotated[ + InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN) + ] = InteractionStyle.BUTTON_POSITIVE + + button_label_positive: Annotated[ + ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN) + ] = ButtonLabel.OK + + button_label_negative: Annotated[ + ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN) + ] = ButtonLabel.CANCEL + + @override + @classmethod + def get_type_id(cls) -> ClientUITypeID: + return ClientUITypeID.BASIC + + def contains_unknown_elements(self) -> bool: + """Whether something within us is an unknown type or enum.""" + return ( + self.interaction_style is self.InteractionStyle.UNKNOWN + or self.button_label_positive is self.ButtonLabel.UNKNOWN + or self.button_label_negative is self.ButtonLabel.UNKNOWN + or any( + c.get_type_id() is BasicClientUIComponentTypeID.UNKNOWN + for c in self.components + ) + ) + + +@ioprepped +@dataclass +class ClientUIWrapper: + """Wrapper for a ClientUI and its common data.""" + + id: Annotated[str, IOAttrs('i')] + createtime: Annotated[datetime.datetime, IOAttrs('c')] + ui: Annotated[ClientUI, IOAttrs('e')] + + +@ioprepped +@dataclass +class InboxRequestMessage(Message): + """Message requesting our inbox.""" + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [InboxRequestResponse] + + +@ioprepped +@dataclass +class InboxRequestResponse(Response): + """Here's that inbox contents you asked for, boss.""" + + wrappers: Annotated[list[ClientUIWrapper], IOAttrs('w')] + + # Printable error if something goes wrong. + error: Annotated[str | None, IOAttrs('e')] = None + + +class ClientUIAction(Enum): + """Types of actions we can run.""" + + BUTTON_PRESS_POSITIVE = 'p' + BUTTON_PRESS_NEGATIVE = 'n' + + +class ClientEffectTypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + SCREEN_MESSAGE = 'm' + SOUND = 's' + DELAY = 'd' + CHEST_WAIT_TIME_ANIMATION = 't' + TICKETS_ANIMATION = 'ta' + TOKENS_ANIMATION = 'toa' + + +class ClientEffect(IOMultiType[ClientEffectTypeID]): + """Something that can happen on the client. + + This can include screen messages, sounds, visual effects, etc. + """ + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + # pylint: disable=too-many-return-statements + + t = ClientEffectTypeID + if type_id is t.UNKNOWN: + return ClientEffectUnknown + if type_id is t.SCREEN_MESSAGE: + return ClientEffectScreenMessage + if type_id is t.SOUND: + return ClientEffectSound + if type_id is t.DELAY: + return ClientEffectDelay + if type_id is t.CHEST_WAIT_TIME_ANIMATION: + return ClientEffectChestWaitTimeAnimation + if type_id is t.TICKETS_ANIMATION: + return ClientEffectTicketsAnimation + if type_id is t.TOKENS_ANIMATION: + return ClientEffectTokensAnimation + + # Important to make sure we provide all types. + assert_never(type_id) + + @override + @classmethod + def get_unknown_type_fallback(cls) -> ClientEffect: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return ClientEffectUnknown() + + +@ioprepped +@dataclass +class ClientEffectUnknown(ClientEffect): + """Fallback substitute for types we don't recognize.""" + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.UNKNOWN + + +@ioprepped +@dataclass +class ClientEffectScreenMessage(ClientEffect): + """Display a screen-message.""" + + message: Annotated[str, IOAttrs('m')] + subs: Annotated[list[str], IOAttrs('s')] = field(default_factory=list) + color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0) + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.SCREEN_MESSAGE + + +@ioprepped +@dataclass +class ClientEffectSound(ClientEffect): + """Play a sound.""" + + class Sound(Enum): + """Sounds that can be made alongside the message.""" + + UNKNOWN = 'u' + CASH_REGISTER = 'c' + ERROR = 'e' + POWER_DOWN = 'p' + GUN_COCKING = 'g' + + sound: Annotated[Sound, IOAttrs('s', enum_fallback=Sound.UNKNOWN)] + volume: Annotated[float, IOAttrs('v')] = 1.0 + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.SOUND + + +@ioprepped +@dataclass +class ClientEffectChestWaitTimeAnimation(ClientEffect): + """Animate chest wait time changing.""" + + chestid: Annotated[str, IOAttrs('c')] + duration: Annotated[float, IOAttrs('u')] + startvalue: Annotated[datetime.datetime, IOAttrs('o')] + endvalue: Annotated[datetime.datetime, IOAttrs('n')] + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION + + +@ioprepped +@dataclass +class ClientEffectTicketsAnimation(ClientEffect): + """Animate tickets count.""" + + duration: Annotated[float, IOAttrs('u')] + startvalue: Annotated[int, IOAttrs('s')] + endvalue: Annotated[int, IOAttrs('e')] + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.TICKETS_ANIMATION + + +@ioprepped +@dataclass +class ClientEffectTokensAnimation(ClientEffect): + """Animate tokens count.""" + + duration: Annotated[float, IOAttrs('u')] + startvalue: Annotated[int, IOAttrs('s')] + endvalue: Annotated[int, IOAttrs('e')] + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.TOKENS_ANIMATION + + +@ioprepped +@dataclass +class ClientEffectDelay(ClientEffect): + """Delay effect processing.""" + + seconds: Annotated[float, IOAttrs('s')] + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.DELAY + + +@ioprepped +@dataclass +class ClientUIActionMessage(Message): + """Do something to a client ui.""" + + id: Annotated[str, IOAttrs('i')] + action: Annotated[ClientUIAction, IOAttrs('a')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [ClientUIActionResponse] + + +@ioprepped +@dataclass +class ClientUIActionResponse(Response): + """Did something to that inbox entry, boss.""" + + class ErrorType(Enum): + """Types of errors that may have occurred.""" + + # Probably a future error type we don't recognize. + UNKNOWN = 'u' + + # Something went wrong on the server, but specifics are not + # relevant. + INTERNAL = 'i' + + # The entry expired on the server. In various cases such as 'ok' + # buttons this can generally be ignored. + EXPIRED = 'e' + + error_type: Annotated[ + ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN) + ] + + # User facing error message in the case of errors. + error_message: Annotated[str | None, IOAttrs('em')] + + effects: Annotated[list[ClientEffect], IOAttrs('fx')] + + +@ioprepped +@dataclass +class ScoreSubmitMessage(Message): + """Let the server know we got some score in something.""" + + score_token: Annotated[str, IOAttrs('t')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [ScoreSubmitResponse] + + +@ioprepped +@dataclass +class ScoreSubmitResponse(Response): + """Did something to that inbox entry, boss.""" + + # Things we should show on our end. + effects: Annotated[list[ClientEffect], IOAttrs('fx')] + + +@ioprepped +@dataclass +class ChestActionMessage(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 [ChestActionResponse] + + +@ioprepped +@dataclass +class ChestActionResponse(Response): + """Here's the results of that action you asked for, boss.""" + + # Tokens that were actually charged. + tokens_charged: Annotated[int, IOAttrs('t')] = 0 + + # If present, signifies the chest has been opened and we should show + # the user this stuff that was in it. + contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None + + # If contents are present, which of the chest's prize-sets they + # represent. + prizeindex: Annotated[int, IOAttrs('i')] = 0 + + # Printable error if something goes wrong. + error: Annotated[str | None, IOAttrs('e')] = None + + # Printable warning. Shown in orange with an error sound. Does not + # mean the action failed; only that there's something to tell the + # users such as 'It looks like you are faking ad views; stop it or + # you won't have ad options anymore.' + warning: Annotated[str | None, IOAttrs('w', store_default=False)] = None + + # Printable success message. Shown in green with a cash-register + # sound. Can be used for things like successful wait reductions via + # ad views. Used in builds earlier than 22311; can remove once + # 22311+ is ubiquitous. + success_msg: Annotated[str | None, IOAttrs('s', store_default=False)] = None + + # Effects to show on the client. Replaces warning and success_msg in + # build 22311 or newer. + effects: Annotated[ + list[ClientEffect], IOAttrs('fx', store_default=False) + ] = field(default_factory=list) + + +@ioprepped +@dataclass +class GlobalProfileCheckMessage(Message): + """Is this global profile name available?""" + + name: Annotated[str, IOAttrs('n')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [GlobalProfileCheckResponse] + + +@ioprepped +@dataclass +class GlobalProfileCheckResponse(Response): + """Here's that profile check ya asked for boss.""" + + available: Annotated[bool, IOAttrs('a')] + ticket_cost: Annotated[int, IOAttrs('tc')] + + +@ioprepped +@dataclass +class SendInfoMessage(Message): + """User is using the send-info function.""" + + description: Annotated[str, IOAttrs('c')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [SendInfoResponse] + + +@ioprepped +@dataclass +class SendInfoResponse(Response): + """Response to sending info to the server.""" + + handled: Annotated[bool, IOAttrs('v')] + message: Annotated[str | None, IOAttrs('m', store_default=False)] = None + effects: Annotated[ + list[ClientEffect], IOAttrs('e', store_default=False) + ] = field(default_factory=list) + legacy_code: Annotated[str | None, IOAttrs('l', store_default=False)] = None diff --git a/dist/ba_data/python/bacommon/bs/__init__.py b/dist/ba_data/python/bacommon/bs/__init__.py new file mode 100644 index 0000000..7f26e38 --- /dev/null +++ b/dist/ba_data/python/bacommon/bs/__init__.py @@ -0,0 +1,139 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to bombsquad classic.""" + +from bacommon.bs._account import ( + ClassicAccountLiveData, +) +from bacommon.bs._bs import ( + TOKENS1_COUNT, + TOKENS2_COUNT, + TOKENS3_COUNT, + TOKENS4_COUNT, +) +from bacommon.bs._chest import ( + ClassicChestAppearance, +) +from bacommon.bs._clienteffect import ( + ClientEffect, + ClientEffectChestWaitTimeAnimation, + ClientEffectDelay, + ClientEffectScreenMessage, + ClientEffectSound, + ClientEffectTicketsAnimation, + ClientEffectTokensAnimation, + ClientEffectTypeID, + ClientEffectUnknown, +) +from bacommon.bs._clouddialog import ( + BasicCloudDialog, + BasicCloudDialogComponent, + BasicCloudDialogBsClassicTourneyResult, + BasicCloudDialogComponentLink, + BasicCloudDialogComponentText, + BasicCloudDialogComponentTypeID, + BasicCloudDialogComponentUnknown, + BasicCloudDialogDisplayItems, + BasicCloudDialogExpireTime, + CloudDialog, + CloudDialogAction, + CloudDialogTypeID, + CloudDialogWrapper, + UnknownCloudDialog, +) +from bacommon.bs._cloudui import CloudUITypeID, CloudUI +from bacommon.bs._displayitem import ( + ChestDisplayItem, + DisplayItem, + DisplayItemTypeID, + DisplayItemWrapper, + TestDisplayItem, + TicketsDisplayItem, + TokensDisplayItem, + UnknownDisplayItem, +) +from bacommon.bs._msg import ( + ChestActionMessage, + ChestActionResponse, + ChestInfoMessage, + ChestInfoResponse, + CloudDialogActionMessage, + CloudDialogActionResponse, + GetClassicPurchasesMessage, + GetClassicPurchasesResponse, + GlobalProfileCheckMessage, + GlobalProfileCheckResponse, + InboxRequestMessage, + InboxRequestResponse, + LegacyRequest, + LegacyResponse, + PrivatePartyMessage, + PrivatePartyResponse, + ScoreSubmitMessage, + ScoreSubmitResponse, + SendInfoMessage, + SendInfoResponse, +) + + +__all__ = [ + 'BasicCloudDialog', + 'BasicCloudDialogComponent', + 'BasicCloudDialogBsClassicTourneyResult', + 'BasicCloudDialogComponentLink', + 'BasicCloudDialogComponentText', + 'BasicCloudDialogComponentTypeID', + 'BasicCloudDialogComponentUnknown', + 'BasicCloudDialogDisplayItems', + 'BasicCloudDialogExpireTime', + 'ChestActionMessage', + 'ChestActionResponse', + 'ChestDisplayItem', + 'ChestInfoMessage', + 'ChestInfoResponse', + 'ClassicAccountLiveData', + 'ClassicChestAppearance', + 'ClientEffect', + 'ClientEffectChestWaitTimeAnimation', + 'ClientEffectDelay', + 'ClientEffectScreenMessage', + 'ClientEffectSound', + 'ClientEffectTicketsAnimation', + 'ClientEffectTokensAnimation', + 'ClientEffectTypeID', + 'ClientEffectUnknown', + 'CloudDialog', + 'CloudDialogAction', + 'CloudDialogActionMessage', + 'CloudDialogActionResponse', + 'CloudDialogTypeID', + 'CloudDialogWrapper', + 'CloudUI', + 'CloudUITypeID', + 'DisplayItem', + 'DisplayItemTypeID', + 'DisplayItemWrapper', + 'GetClassicPurchasesMessage', + 'GetClassicPurchasesResponse', + 'GlobalProfileCheckMessage', + 'GlobalProfileCheckResponse', + 'InboxRequestMessage', + 'InboxRequestResponse', + 'LegacyRequest', + 'LegacyResponse', + 'PrivatePartyMessage', + 'PrivatePartyResponse', + 'ScoreSubmitMessage', + 'ScoreSubmitResponse', + 'SendInfoMessage', + 'SendInfoResponse', + 'TestDisplayItem', + 'TicketsDisplayItem', + 'TOKENS1_COUNT', + 'TOKENS2_COUNT', + 'TOKENS3_COUNT', + 'TOKENS4_COUNT', + 'TokensDisplayItem', + 'UnknownCloudDialog', + 'UnknownDisplayItem', +] diff --git a/dist/ba_data/python/bacommon/classic/_account.py b/dist/ba_data/python/bacommon/bs/_account.py similarity index 86% rename from dist/ba_data/python/bacommon/classic/_account.py rename to dist/ba_data/python/bacommon/bs/_account.py index 41460f5..da8b9fe 100644 --- a/dist/ba_data/python/bacommon/classic/_account.py +++ b/dist/ba_data/python/bacommon/bs/_account.py @@ -10,12 +10,12 @@ from dataclasses import dataclass from typing import Annotated from efro.dataclassio import ioprepped, IOAttrs -from bacommon.classic._chest import ClassicChestAppearance +from bacommon.bs._chest import ClassicChestAppearance @ioprepped @dataclass -class ClassicLiveAccountClientData: +class ClassicAccountLiveData: """Live account data fed to the client in the bs classic app mode.""" @dataclass @@ -44,12 +44,6 @@ class ClassicLiveAccountClientData: ASK_FOR_REVIEW = 'r' - class StoreStyle(Enum): - """Special looks for the store.""" - - NORMAL = 'n' - SANTA = 's' - tickets: Annotated[int, IOAttrs('ti')] tokens: Annotated[int, IOAttrs('to')] @@ -77,7 +71,3 @@ class ClassicLiveAccountClientData: purchases_state: Annotated[str | None, IOAttrs('p')] flags: Annotated[set[Flag], IOAttrs('f', soft_default_factory=set)] - - store_style: Annotated[ - StoreStyle, IOAttrs('s', enum_fallback=StoreStyle.NORMAL) - ] diff --git a/dist/ba_data/python/bacommon/classic/_classic.py b/dist/ba_data/python/bacommon/bs/_bs.py similarity index 100% rename from dist/ba_data/python/bacommon/classic/_classic.py rename to dist/ba_data/python/bacommon/bs/_bs.py diff --git a/dist/ba_data/python/bacommon/classic/_chest.py b/dist/ba_data/python/bacommon/bs/_chest.py similarity index 62% rename from dist/ba_data/python/bacommon/classic/_chest.py rename to dist/ba_data/python/bacommon/bs/_chest.py index 849e9af..ba87b94 100644 --- a/dist/ba_data/python/bacommon/classic/_chest.py +++ b/dist/ba_data/python/bacommon/bs/_chest.py @@ -5,11 +5,7 @@ from __future__ import annotations from enum import Enum -from typing import assert_never, Annotated, override -from dataclasses import dataclass - -from efro.dataclassio import ioprepped, IOAttrs -import bacommon.displayitem as ditm +from typing import assert_never class ClassicChestAppearance(Enum): @@ -28,7 +24,7 @@ class ClassicChestAppearance(Enum): def pretty_name(self) -> str: """Pretty name for the chest in English.""" # pylint: disable=too-many-return-statements - cls = ClassicChestAppearance + cls = type(self) if self is cls.UNKNOWN: return 'Unknown Chest' @@ -48,20 +44,3 @@ class ClassicChestAppearance(Enum): return 'L6 Chest' assert_never(self) - - -@ioprepped -@dataclass -class ClassicChestDisplayItem(ditm.Item): - """Display a chest.""" - - appearance: Annotated[ClassicChestAppearance, IOAttrs('a')] - - @override - @classmethod - def get_type_id(cls) -> ditm.ItemTypeID: - return ditm.ItemTypeID.CHEST - - @override - def get_description(self) -> tuple[str, list[tuple[str, str]]]: - return self.appearance.pretty_name, [] diff --git a/dist/ba_data/python/bacommon/bs/_clienteffect.py b/dist/ba_data/python/bacommon/bs/_clienteffect.py new file mode 100644 index 0000000..c7f7f70 --- /dev/null +++ b/dist/ba_data/python/bacommon/bs/_clienteffect.py @@ -0,0 +1,180 @@ +# Released under the MIT License. See LICENSE for details. +# +"""ClientEffect related functionality.""" + +from __future__ import annotations + +import datetime +from enum import Enum +from dataclasses import dataclass, field +from typing import Annotated, override, assert_never + +from efro.dataclassio import ioprepped, IOAttrs, IOMultiType + + +class ClientEffectTypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + SCREEN_MESSAGE = 'm' + SOUND = 's' + DELAY = 'd' + CHEST_WAIT_TIME_ANIMATION = 't' + TICKETS_ANIMATION = 'ta' + TOKENS_ANIMATION = 'toa' + + +class ClientEffect(IOMultiType[ClientEffectTypeID]): + """Something that can happen on the client. + + This can include screen messages, sounds, visual effects, etc. + """ + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + # pylint: disable=too-many-return-statements + + t = ClientEffectTypeID + if type_id is t.UNKNOWN: + return ClientEffectUnknown + if type_id is t.SCREEN_MESSAGE: + return ClientEffectScreenMessage + if type_id is t.SOUND: + return ClientEffectSound + if type_id is t.DELAY: + return ClientEffectDelay + if type_id is t.CHEST_WAIT_TIME_ANIMATION: + return ClientEffectChestWaitTimeAnimation + if type_id is t.TICKETS_ANIMATION: + return ClientEffectTicketsAnimation + if type_id is t.TOKENS_ANIMATION: + return ClientEffectTokensAnimation + + # Important to make sure we provide all types. + assert_never(type_id) + + @override + @classmethod + def get_unknown_type_fallback(cls) -> ClientEffect: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return ClientEffectUnknown() + + +@ioprepped +@dataclass +class ClientEffectUnknown(ClientEffect): + """Fallback substitute for types we don't recognize.""" + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.UNKNOWN + + +@ioprepped +@dataclass +class ClientEffectScreenMessage(ClientEffect): + """Display a screen-message.""" + + message: Annotated[str, IOAttrs('m')] + subs: Annotated[list[str], IOAttrs('s')] = field(default_factory=list) + color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0) + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.SCREEN_MESSAGE + + +@ioprepped +@dataclass +class ClientEffectSound(ClientEffect): + """Play a sound.""" + + class Sound(Enum): + """Sounds that can be made alongside the message.""" + + UNKNOWN = 'u' + CASH_REGISTER = 'c' + ERROR = 'e' + POWER_DOWN = 'p' + GUN_COCKING = 'g' + + sound: Annotated[Sound, IOAttrs('s', enum_fallback=Sound.UNKNOWN)] + volume: Annotated[float, IOAttrs('v')] = 1.0 + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.SOUND + + +@ioprepped +@dataclass +class ClientEffectChestWaitTimeAnimation(ClientEffect): + """Animate chest wait time changing.""" + + chestid: Annotated[str, IOAttrs('c')] + duration: Annotated[float, IOAttrs('u')] + startvalue: Annotated[datetime.datetime, IOAttrs('o')] + endvalue: Annotated[datetime.datetime, IOAttrs('n')] + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION + + +@ioprepped +@dataclass +class ClientEffectTicketsAnimation(ClientEffect): + """Animate tickets count.""" + + duration: Annotated[float, IOAttrs('u')] + startvalue: Annotated[int, IOAttrs('s')] + endvalue: Annotated[int, IOAttrs('e')] + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.TICKETS_ANIMATION + + +@ioprepped +@dataclass +class ClientEffectTokensAnimation(ClientEffect): + """Animate tokens count.""" + + duration: Annotated[float, IOAttrs('u')] + startvalue: Annotated[int, IOAttrs('s')] + endvalue: Annotated[int, IOAttrs('e')] + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.TOKENS_ANIMATION + + +@ioprepped +@dataclass +class ClientEffectDelay(ClientEffect): + """Delay effect processing.""" + + seconds: Annotated[float, IOAttrs('s')] + + @override + @classmethod + def get_type_id(cls) -> ClientEffectTypeID: + return ClientEffectTypeID.DELAY diff --git a/dist/ba_data/python/bacommon/bs/_clouddialog.py b/dist/ba_data/python/bacommon/bs/_clouddialog.py new file mode 100644 index 0000000..4b4288f --- /dev/null +++ b/dist/ba_data/python/bacommon/bs/_clouddialog.py @@ -0,0 +1,308 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Simple cloud-defined UIs for things like notifications.""" + +from __future__ import annotations + +import datetime +from enum import Enum +from dataclasses import dataclass, field +from typing import Annotated, override, assert_never + +from efro.dataclassio import ioprepped, IOAttrs, IOMultiType + +from bacommon.bs._displayitem import DisplayItemWrapper + + +class CloudDialogTypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + BASIC = 'b' + + +class CloudDialog(IOMultiType[CloudDialogTypeID]): + """Small self-contained ui bit provided by the cloud. + + These take care of updating and/or dismissing themselves based on + user input. Useful for things such as inbox messages. For more + complex UI construction, look at :class:`CloudUI`. + """ + + @override + @classmethod + def get_type_id(cls) -> CloudDialogTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type(cls, type_id: CloudDialogTypeID) -> type[CloudDialog]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + out: type[CloudDialog] + + t = CloudDialogTypeID + if type_id is t.UNKNOWN: + out = UnknownCloudDialog + elif type_id is t.BASIC: + out = BasicCloudDialog + else: + # Important to make sure we provide all types. + assert_never(type_id) + return out + + @override + @classmethod + def get_unknown_type_fallback(cls) -> CloudDialog: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return UnknownCloudDialog() + + +@ioprepped +@dataclass +class UnknownCloudDialog(CloudDialog): + """Fallback type for unrecognized entries.""" + + @override + @classmethod + def get_type_id(cls) -> CloudDialogTypeID: + return CloudDialogTypeID.UNKNOWN + + +class BasicCloudDialogComponentTypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + TEXT = 't' + LINK = 'l' + BS_CLASSIC_TOURNEY_RESULT = 'ct' + DISPLAY_ITEMS = 'di' + EXPIRE_TIME = 'd' + + +class BasicCloudDialogComponent(IOMultiType[BasicCloudDialogComponentTypeID]): + """Top level class for our multitype.""" + + @override + @classmethod + def get_type_id(cls) -> BasicCloudDialogComponentTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type( + cls, type_id: BasicCloudDialogComponentTypeID + ) -> type[BasicCloudDialogComponent]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + + t = BasicCloudDialogComponentTypeID + if type_id is t.UNKNOWN: + return BasicCloudDialogComponentUnknown + if type_id is t.TEXT: + return BasicCloudDialogComponentText + if type_id is t.LINK: + return BasicCloudDialogComponentLink + if type_id is t.BS_CLASSIC_TOURNEY_RESULT: + return BasicCloudDialogBsClassicTourneyResult + if type_id is t.DISPLAY_ITEMS: + return BasicCloudDialogDisplayItems + if type_id is t.EXPIRE_TIME: + return BasicCloudDialogExpireTime + + # Important to make sure we provide all types. + assert_never(type_id) + + @override + @classmethod + def get_unknown_type_fallback(cls) -> BasicCloudDialogComponent: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return BasicCloudDialogComponentUnknown() + + +@ioprepped +@dataclass +class BasicCloudDialogComponentUnknown(BasicCloudDialogComponent): + """An unknown basic client component type. + + In practice these should never show up since the master-server + generates these on the fly for the client and so should not send + clients one they can't digest. + """ + + @override + @classmethod + def get_type_id(cls) -> BasicCloudDialogComponentTypeID: + return BasicCloudDialogComponentTypeID.UNKNOWN + + +@ioprepped +@dataclass +class BasicCloudDialogComponentText(BasicCloudDialogComponent): + """Show some text in the inbox message.""" + + text: Annotated[str, IOAttrs('t')] + subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field( + default_factory=list + ) + scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0 + color: Annotated[ + tuple[float, float, float, float], IOAttrs('c', store_default=False) + ] = (1.0, 1.0, 1.0, 1.0) + spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 + spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 + + @override + @classmethod + def get_type_id(cls) -> BasicCloudDialogComponentTypeID: + return BasicCloudDialogComponentTypeID.TEXT + + +@ioprepped +@dataclass +class BasicCloudDialogComponentLink(BasicCloudDialogComponent): + """Show a link in the inbox message.""" + + url: Annotated[str, IOAttrs('u')] + label: Annotated[str, IOAttrs('l')] + subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field( + default_factory=list + ) + spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 + spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 + + @override + @classmethod + def get_type_id(cls) -> BasicCloudDialogComponentTypeID: + return BasicCloudDialogComponentTypeID.LINK + + +@ioprepped +@dataclass +class BasicCloudDialogBsClassicTourneyResult(BasicCloudDialogComponent): + """Show info about a classic tourney.""" + + tournament_id: Annotated[str, IOAttrs('t')] + game: Annotated[str, IOAttrs('g')] + players: Annotated[int, IOAttrs('p')] + rank: Annotated[int, IOAttrs('r')] + trophy: Annotated[str | None, IOAttrs('tr')] + prizes: Annotated[list[DisplayItemWrapper], IOAttrs('pr')] + + @override + @classmethod + def get_type_id(cls) -> BasicCloudDialogComponentTypeID: + return BasicCloudDialogComponentTypeID.BS_CLASSIC_TOURNEY_RESULT + + +@ioprepped +@dataclass +class BasicCloudDialogDisplayItems(BasicCloudDialogComponent): + """Show some display-items.""" + + items: Annotated[list[DisplayItemWrapper], IOAttrs('d')] + width: Annotated[float, IOAttrs('w')] = 100.0 + spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 + spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 + + @override + @classmethod + def get_type_id(cls) -> BasicCloudDialogComponentTypeID: + return BasicCloudDialogComponentTypeID.DISPLAY_ITEMS + + +@ioprepped +@dataclass +class BasicCloudDialogExpireTime(BasicCloudDialogComponent): + """Show expire-time.""" + + time: Annotated[datetime.datetime, IOAttrs('d')] + spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 + spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 + + @override + @classmethod + def get_type_id(cls) -> BasicCloudDialogComponentTypeID: + return BasicCloudDialogComponentTypeID.EXPIRE_TIME + + +@ioprepped +@dataclass +class BasicCloudDialog(CloudDialog): + """A basic UI for the client.""" + + class ButtonLabel(Enum): + """Distinct button labels we support.""" + + UNKNOWN = 'u' + OK = 'o' + APPLY = 'a' + CANCEL = 'c' + ACCEPT = 'ac' + DECLINE = 'dn' + IGNORE = 'ig' + CLAIM = 'cl' + DISCARD = 'd' + + class InteractionStyle(Enum): + """Overall interaction styles we support.""" + + UNKNOWN = 'u' + BUTTON_POSITIVE = 'p' + BUTTON_POSITIVE_NEGATIVE = 'pn' + + components: Annotated[list[BasicCloudDialogComponent], IOAttrs('s')] + + interaction_style: Annotated[ + InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN) + ] = InteractionStyle.BUTTON_POSITIVE + + button_label_positive: Annotated[ + ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN) + ] = ButtonLabel.OK + + button_label_negative: Annotated[ + ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN) + ] = ButtonLabel.CANCEL + + @override + @classmethod + def get_type_id(cls) -> CloudDialogTypeID: + return CloudDialogTypeID.BASIC + + def contains_unknown_elements(self) -> bool: + """Whether something within us is an unknown type or enum.""" + return ( + self.interaction_style is self.InteractionStyle.UNKNOWN + or self.button_label_positive is self.ButtonLabel.UNKNOWN + or self.button_label_negative is self.ButtonLabel.UNKNOWN + or any( + c.get_type_id() is BasicCloudDialogComponentTypeID.UNKNOWN + for c in self.components + ) + ) + + +@ioprepped +@dataclass +class CloudDialogWrapper: + """Wrapper for a CloudDialog and its common data.""" + + id: Annotated[str, IOAttrs('i')] + createtime: Annotated[datetime.datetime, IOAttrs('c')] + ui: Annotated[CloudDialog, IOAttrs('e')] + + +class CloudDialogAction(Enum): + """Types of actions we can run.""" + + BUTTON_PRESS_POSITIVE = 'p' + BUTTON_PRESS_NEGATIVE = 'n' diff --git a/dist/ba_data/python/bacommon/bs/_cloudui.py b/dist/ba_data/python/bacommon/bs/_cloudui.py new file mode 100644 index 0000000..1b4659c --- /dev/null +++ b/dist/ba_data/python/bacommon/bs/_cloudui.py @@ -0,0 +1,180 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Full UIs defined in the cloud - similar to a basic form of html""" + +from __future__ import annotations + +from enum import Enum +from dataclasses import dataclass +from typing import Annotated, override, assert_never + +from efro.dataclassio import ioprepped, IOAttrs, IOMultiType + + +class CloudUITypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + V1 = 'v1' + + +class CloudUI(IOMultiType[CloudUITypeID]): + """UI defined by the cloud. + + Conceptually similar to a basic html page, except using app UI. + """ + + @override + @classmethod + def get_type_id(cls) -> CloudUITypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type(cls, type_id: CloudUITypeID) -> type[CloudUI]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + out: type[CloudUI] + + t = CloudUITypeID + if type_id is t.UNKNOWN: + out = UnknownCloudUI + elif type_id is t.V1: + out = V1CloudUI + else: + # Important to make sure we provide all types. + assert_never(type_id) + return out + + @override + @classmethod + def get_unknown_type_fallback(cls) -> CloudUI: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return UnknownCloudUI() + + +@ioprepped +@dataclass +class UnknownCloudUI(CloudUI): + """Fallback type for unrecognized UI types. + + Will show the client a 'cannot display this UI' placeholder page. + """ + + @override + @classmethod + def get_type_id(cls) -> CloudUITypeID: + return CloudUITypeID.UNKNOWN + + +class V1CloudUIComponentTypeID(Enum): + """Type ID for each of our subclasses.""" + + UNKNOWN = 'u' + TEXT = 't' + + +class V1CloudUIComponent(IOMultiType[V1CloudUIComponentTypeID]): + """Top level class for our multitype.""" + + @override + @classmethod + def get_type_id(cls) -> V1CloudUIComponentTypeID: + # Require child classes to supply this themselves. If we did a + # full type registry/lookup here it would require us to import + # everything and would prevent lazy loading. + raise NotImplementedError() + + @override + @classmethod + def get_type( + cls, type_id: V1CloudUIComponentTypeID + ) -> type[V1CloudUIComponent]: + """Return the subclass for each of our type-ids.""" + # pylint: disable=cyclic-import + + t = V1CloudUIComponentTypeID + if type_id is t.UNKNOWN: + return V1CloudUIComponentUnknown + if type_id is t.TEXT: + return V1CloudUIComponentText + + # Important to make sure we provide all types. + assert_never(type_id) + + @override + @classmethod + def get_unknown_type_fallback(cls) -> V1CloudUIComponent: + # If we encounter some future message type we don't know + # anything about, drop in a placeholder. + return V1CloudUIComponentUnknown() + + +@ioprepped +@dataclass +class V1CloudUIComponentUnknown(V1CloudUIComponent): + """An unknown basic client component type. + + In practice these should never show up since the master-server + generates these on the fly for the client and so should not send + clients one they can't digest. + """ + + @override + @classmethod + def get_type_id(cls) -> V1CloudUIComponentTypeID: + return V1CloudUIComponentTypeID.UNKNOWN + + +@ioprepped +@dataclass +class V1CloudUIComponentText(V1CloudUIComponent): + """Show some text over a button.""" + + text: Annotated[str, IOAttrs('t')] + # position: Annotated[float, IOAttrs('p', store_default=False)] = 1.0 + # scale: Annotated[float, IOAttrs('s', store_default=False)] = 1.0 + # color: Annotated[ + # tuple[float, float, float, float], IOAttrs('c', store_default=False) + # ] = (1.0, 1.0, 1.0, 1.0) + + @override + @classmethod + def get_type_id(cls) -> V1CloudUIComponentTypeID: + return V1CloudUIComponentTypeID.TEXT + + +@ioprepped +@dataclass +class V1CloudUIButton: + """A button in our cloud ui.""" + + color: Annotated[tuple[float, float, float], IOAttrs('cl')] + size: Annotated[tuple[float, float], IOAttrs('sz')] + components: Annotated[list[V1CloudUIComponent], IOAttrs('c')] + scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0 + + +@ioprepped +@dataclass +class V1CloudUIRow: + """A row in our cloud ui.""" + + buttons: Annotated[list[V1CloudUIButton], IOAttrs('b')] + + +@ioprepped +@dataclass +class V1CloudUI(CloudUI): + """Version 1 of our cloud-defined UI type.""" + + rows: Annotated[list[V1CloudUIRow], IOAttrs('r')] + + @override + @classmethod + def get_type_id(cls) -> CloudUITypeID: + return CloudUITypeID.V1 diff --git a/dist/ba_data/python/bacommon/displayitem.py b/dist/ba_data/python/bacommon/bs/_displayitem.py similarity index 58% rename from dist/ba_data/python/bacommon/displayitem.py rename to dist/ba_data/python/bacommon/bs/_displayitem.py index 566695b..0593e31 100644 --- a/dist/ba_data/python/bacommon/displayitem.py +++ b/dist/ba_data/python/bacommon/bs/_displayitem.py @@ -1,12 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Functionality for displaying currencies, prizes, owned items, etc. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" +"""DisplayItem related functionality.""" from __future__ import annotations @@ -17,19 +11,20 @@ from typing import Annotated, override, assert_never from efro.util import pairs_to_flat from efro.dataclassio import ioprepped, IOAttrs, IOMultiType +from bacommon.bs._chest import ClassicChestAppearance -class ItemTypeID(Enum): + +class DisplayItemTypeID(Enum): """Type ID for each of our subclasses.""" UNKNOWN = 'u' TICKETS = 't' - TICKETS_PURPLE = 'tp' TOKENS = 'k' TEST = 's' CHEST = 'c' -class Item(IOMultiType[ItemTypeID]): +class DisplayItem(IOMultiType[DisplayItemTypeID]): """Some amount of something that can be shown or described. Used to depict chest contents, inventory, rewards, etc. @@ -37,7 +32,7 @@ class Item(IOMultiType[ItemTypeID]): @override @classmethod - def get_type_id(cls) -> ItemTypeID: + def get_type_id(cls) -> DisplayItemTypeID: # Require child classes to supply this themselves. If we did a # full type registry/lookup here it would require us to import # everything and would prevent lazy loading. @@ -45,25 +40,21 @@ class Item(IOMultiType[ItemTypeID]): @override @classmethod - def get_type(cls, type_id: ItemTypeID) -> type[Item]: + def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]: """Return the subclass for each of our type-ids.""" # pylint: disable=cyclic-import - t = ItemTypeID + t = DisplayItemTypeID if type_id is t.UNKNOWN: - return Unknown + return UnknownDisplayItem if type_id is t.TICKETS: - return Tickets - if type_id is t.TICKETS_PURPLE: - return PurpleTickets + return TicketsDisplayItem if type_id is t.TOKENS: - return Tokens + return TokensDisplayItem if type_id is t.TEST: - return Test + return TestDisplayItem if type_id is t.CHEST: - from bacommon.classic._chest import ClassicChestDisplayItem - - return ClassicChestDisplayItem + return ChestDisplayItem # Important to make sure we provide all types. assert_never(type_id) @@ -71,34 +62,31 @@ class Item(IOMultiType[ItemTypeID]): def get_description(self) -> tuple[str, list[tuple[str, str]]]: """Return a string description and subs for the item. - Will be translated on the client using the 'displayItemNames' - Lstr category. - - These decriptions are baked into the display-item wrapper and + These decriptions are baked into the DisplayItemWrapper and should be accessed from there when available. This allows - clients to give descriptions even for newer display item types - they don't recognize. + clients to give descriptions even for newer display items they + don't recognize. """ raise NotImplementedError() # Implement fallbacks so client can digest item lists even if they - # contain unrecognized stuff. The wrapper contains basic + # contain unrecognized stuff. DisplayItemWrapper contains basic # baked down info that they can still use in such cases. @override @classmethod - def get_unknown_type_fallback(cls) -> Item: - return Unknown() + def get_unknown_type_fallback(cls) -> DisplayItem: + return UnknownDisplayItem() @ioprepped @dataclass -class Unknown(Item): +class UnknownDisplayItem(DisplayItem): """Something we don't know how to display.""" @override @classmethod - def get_type_id(cls) -> ItemTypeID: - return ItemTypeID.UNKNOWN + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.UNKNOWN @override def get_description(self) -> tuple[str, list[tuple[str, str]]]: @@ -106,23 +94,23 @@ class Unknown(Item): # Make noise but don't break. logging.exception( - 'Unknown.get_description() should never be called.' - ' Always access descriptions on the display-item wrapper.' + 'UnknownDisplayItem.get_description() should never be called.' + ' Always access descriptions on the DisplayItemWrapper.' ) return 'Unknown', [] @ioprepped @dataclass -class Tickets(Item): +class TicketsDisplayItem(DisplayItem): """Some amount of tickets.""" count: Annotated[int, IOAttrs('c')] @override @classmethod - def get_type_id(cls) -> ItemTypeID: - return ItemTypeID.TICKETS + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.TICKETS @override def get_description(self) -> tuple[str, list[tuple[str, str]]]: @@ -131,32 +119,15 @@ class Tickets(Item): @ioprepped @dataclass -class PurpleTickets(Item): - """Some amount of purple tickets.""" - - count: Annotated[int, IOAttrs('c')] - - @override - @classmethod - def get_type_id(cls) -> ItemTypeID: - return ItemTypeID.TICKETS_PURPLE - - @override - def get_description(self) -> tuple[str, list[tuple[str, str]]]: - return '${C} Purple Tickets', [('${C}', str(self.count))] - - -@ioprepped -@dataclass -class Tokens(Item): +class TokensDisplayItem(DisplayItem): """Some amount of tokens.""" count: Annotated[int, IOAttrs('c')] @override @classmethod - def get_type_id(cls) -> ItemTypeID: - return ItemTypeID.TOKENS + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.TOKENS @override def get_description(self) -> tuple[str, list[tuple[str, str]]]: @@ -165,34 +136,47 @@ class Tokens(Item): @ioprepped @dataclass -class Test(Item): +class TestDisplayItem(DisplayItem): """Fills usable space for a display-item - good for calibration.""" @override @classmethod - def get_type_id(cls) -> ItemTypeID: - return ItemTypeID.TEST + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.TEST @override def get_description(self) -> tuple[str, list[tuple[str, str]]]: - return 'Test', [] + return 'Test Display Item Here', [] @ioprepped @dataclass -class Wrapper: - """Wraps a display-item and some baked out info. +class ChestDisplayItem(DisplayItem): + """Display a chest.""" - This allows clients to at least give descriptions of new - display-item types they may not have locally. - """ + appearance: Annotated[ClassicChestAppearance, IOAttrs('a')] - item: Annotated[Item, IOAttrs('i')] + @override + @classmethod + def get_type_id(cls) -> DisplayItemTypeID: + return DisplayItemTypeID.CHEST + + @override + def get_description(self) -> tuple[str, list[tuple[str, str]]]: + return self.appearance.pretty_name, [] + + +@ioprepped +@dataclass +class DisplayItemWrapper: + """Wraps a DisplayItem and common info.""" + + item: Annotated[DisplayItem, IOAttrs('i')] description: Annotated[str, IOAttrs('d')] description_subs: Annotated[list[str] | None, IOAttrs('s')] @classmethod - def for_item(cls, item: Item) -> Wrapper: - """Convenience method to wrap a display-item.""" + def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper: + """Convenience method to wrap a DisplayItem.""" desc, subs = item.get_description() - return Wrapper(item, desc, pairs_to_flat(subs)) + return DisplayItemWrapper(item, desc, pairs_to_flat(subs)) diff --git a/dist/ba_data/python/bacommon/classic/_msg.py b/dist/ba_data/python/bacommon/bs/_msg.py similarity index 59% rename from dist/ba_data/python/bacommon/classic/_msg.py rename to dist/ba_data/python/bacommon/bs/_msg.py index 198e8e4..77e9ff5 100644 --- a/dist/ba_data/python/bacommon/classic/_msg.py +++ b/dist/ba_data/python/bacommon/bs/_msg.py @@ -5,38 +5,125 @@ from __future__ import annotations import datetime +from enum import Enum from dataclasses import dataclass, field from typing import Annotated, override from efro.dataclassio import ioprepped, IOAttrs from efro.message import Message, Response -import bacommon.displayitem as ditm -import bacommon.clouddialog as cdlg -import bacommon.clienteffect as clfx -from bacommon.classic._chest import ClassicChestAppearance +from bacommon.bs._displayitem import DisplayItemWrapper +from bacommon.bs._clienteffect import ClientEffect +from bacommon.bs._clouddialog import CloudDialogAction, CloudDialogWrapper +from bacommon.bs._chest import ClassicChestAppearance @ioprepped @dataclass -class GetClassicLeaguePresidentButtonInfoMessage(Message): - """Curious who is president of my league?..""" +class ChestActionMessage(Message): + """Request action about a chest.""" - season: Annotated[str | None, IOAttrs('s')] + 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 [GetClassicLeaguePresidentButtonInfoResponse] + return [ChestActionResponse] @ioprepped @dataclass -class GetClassicLeaguePresidentButtonInfoResponse(Response): - """Here's that info about the president you asked for boss.""" +class ChestActionResponse(Response): + """Here's the results of that action you asked for, boss.""" - # Lstr for the name shown on the button. - name: Annotated[str | None, IOAttrs('n')] + # Tokens that were actually charged. + tokens_charged: Annotated[int, IOAttrs('t')] = 0 + + # If present, signifies the chest has been opened and we should show + # the user this stuff that was in it. + contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None + + # If contents are present, which of the chest's prize-sets they + # represent. + prizeindex: Annotated[int, IOAttrs('i')] = 0 + + # Printable error if something goes wrong. + error: Annotated[str | None, IOAttrs('e')] = None + + # Printable warning. Shown in orange with an error sound. Does not + # mean the action failed; only that there's something to tell the + # users such as 'It looks like you are faking ad views; stop it or + # you won't have ad options anymore.' + warning: Annotated[str | None, IOAttrs('w', store_default=False)] = None + + # Printable success message. Shown in green with a cash-register + # sound. Can be used for things like successful wait reductions via + # ad views. Used in builds earlier than 22311; can remove once + # 22311+ is ubiquitous. + success_msg: Annotated[str | None, IOAttrs('s', store_default=False)] = None + + # Effects to show on the client. Replaces warning and success_msg in + # build 22311 or newer. + effects: Annotated[ + list[ClientEffect], IOAttrs('fx', store_default=False) + ] = field(default_factory=list) + + +@ioprepped +@dataclass +class CloudDialogActionMessage(Message): + """Do something to a client ui.""" + + id: Annotated[str, IOAttrs('i')] + action: Annotated[CloudDialogAction, IOAttrs('a')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [CloudDialogActionResponse] + + +@ioprepped +@dataclass +class CloudDialogActionResponse(Response): + """Did something to that inbox entry, boss.""" + + class ErrorType(Enum): + """Types of errors that may have occurred.""" + + # Probably a future error type we don't recognize. + UNKNOWN = 'u' + + # Something went wrong on the server, but specifics are not + # relevant. + INTERNAL = 'i' + + # The entry expired on the server. In various cases such as 'ok' + # buttons this can generally be ignored. + EXPIRED = 'e' + + error_type: Annotated[ + ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN) + ] + + # User facing error message in the case of errors. + error_message: Annotated[str | None, IOAttrs('em')] + + effects: Annotated[list[ClientEffect], IOAttrs('fx')] @ioprepped @@ -96,7 +183,7 @@ class InboxRequestMessage(Message): class InboxRequestResponse(Response): """Here's that inbox contents you asked for, boss.""" - wrappers: Annotated[list[cdlg.Wrapper], IOAttrs('w')] + wrappers: Annotated[list[CloudDialogWrapper], IOAttrs('w')] # Printable error if something goes wrong. error: Annotated[str | None, IOAttrs('e')] = None @@ -154,7 +241,7 @@ class ChestInfoResponse(Response): """A possible set of prizes for this chest.""" weight: Annotated[float, IOAttrs('w')] - contents: Annotated[list[ditm.Wrapper], IOAttrs('c')] + contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')] appearance: Annotated[ ClassicChestAppearance, @@ -220,7 +307,7 @@ class ScoreSubmitResponse(Response): """Did something to that inbox entry, boss.""" # Things we should show on our end. - effects: Annotated[list[clfx.Effect], IOAttrs('fx')] + effects: Annotated[list[ClientEffect], IOAttrs('fx')] @ioprepped @@ -243,7 +330,7 @@ class SendInfoResponse(Response): handled: Annotated[bool, IOAttrs('v')] message: Annotated[str | None, IOAttrs('m', store_default=False)] = None - effects: Annotated[list[clfx.Effect], IOAttrs('e', store_default=False)] = ( - field(default_factory=list) - ) + effects: Annotated[ + list[ClientEffect], IOAttrs('e', store_default=False) + ] = field(default_factory=list) legacy_code: Annotated[str | None, IOAttrs('l', store_default=False)] = None diff --git a/dist/ba_data/python/bacommon/build.py b/dist/ba_data/python/bacommon/build.py index 6d440f7..7e26ed5 100644 --- a/dist/ba_data/python/bacommon/build.py +++ b/dist/ba_data/python/bacommon/build.py @@ -1,12 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Functionality related to game builds. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" +"""Functionality related to game builds.""" from __future__ import annotations diff --git a/dist/ba_data/python/bacommon/classic/__init__.py b/dist/ba_data/python/bacommon/classic/__init__.py deleted file mode 100644 index fbf3bcb..0000000 --- a/dist/ba_data/python/bacommon/classic/__init__.py +++ /dev/null @@ -1,71 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Functionality related to bombsquad classic. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" - -from bacommon.classic._account import ( - ClassicLiveAccountClientData, -) -from bacommon.classic._classic import ( - TOKENS1_COUNT, - TOKENS2_COUNT, - TOKENS3_COUNT, - TOKENS4_COUNT, -) -from bacommon.classic._chest import ( - ClassicChestAppearance, - ClassicChestDisplayItem, -) -from bacommon.classic._msg import ( - GetClassicLeaguePresidentButtonInfoMessage, - GetClassicLeaguePresidentButtonInfoResponse, - ChestInfoMessage, - ChestInfoResponse, - GetClassicPurchasesMessage, - GetClassicPurchasesResponse, - GlobalProfileCheckMessage, - GlobalProfileCheckResponse, - InboxRequestMessage, - InboxRequestResponse, - LegacyRequest, - LegacyResponse, - PrivatePartyMessage, - PrivatePartyResponse, - ScoreSubmitMessage, - ScoreSubmitResponse, - SendInfoMessage, - SendInfoResponse, -) - -__all__ = [ - 'ChestInfoMessage', - 'ChestInfoResponse', - 'ClassicLiveAccountClientData', - 'ClassicChestAppearance', - 'ClassicChestDisplayItem', - 'GetClassicLeaguePresidentButtonInfoMessage', - 'GetClassicLeaguePresidentButtonInfoResponse', - 'GetClassicPurchasesMessage', - 'GetClassicPurchasesResponse', - 'GlobalProfileCheckMessage', - 'GlobalProfileCheckResponse', - 'InboxRequestMessage', - 'InboxRequestResponse', - 'LegacyRequest', - 'LegacyResponse', - 'PrivatePartyMessage', - 'PrivatePartyResponse', - 'ScoreSubmitMessage', - 'ScoreSubmitResponse', - 'SendInfoMessage', - 'SendInfoResponse', - 'TOKENS1_COUNT', - 'TOKENS2_COUNT', - 'TOKENS3_COUNT', - 'TOKENS4_COUNT', -] diff --git a/dist/ba_data/python/bacommon/classic/_displayitem.py b/dist/ba_data/python/bacommon/classic/_displayitem.py deleted file mode 100644 index 917aa24..0000000 --- a/dist/ba_data/python/bacommon/classic/_displayitem.py +++ /dev/null @@ -1,28 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Display-item bits of classic.""" - -# from __future__ import annotations - -# from enum import Enum -# from typing import assert_never, Annotated, override -# from dataclasses import dataclass - -# from efro.dataclassio import ioprepped, IOAttrs -# import bacommon.displayitem as ditm - -# @ioprepped -# @dataclass -# class ClassicCharacterDisplayItem(ditm.Item): -# """Display a character.""" - -# : Annotated[ClassicChestAppearance, IOAttrs('a')] - -# @override -# @classmethod -# def get_type_id(cls) -> ditm.ItemTypeID: -# return ditm.ItemTypeID.CHEST - -# @override -# def get_description(self) -> tuple[str, list[tuple[str, str]]]: -# return self.appearance.pretty_name, [] diff --git a/dist/ba_data/python/bacommon/clienteffect.py b/dist/ba_data/python/bacommon/clienteffect.py deleted file mode 100644 index aa9ce5a..0000000 --- a/dist/ba_data/python/bacommon/clienteffect.py +++ /dev/null @@ -1,224 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""ClientEffect related functionality. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" - -from __future__ import annotations - -import datetime -from enum import Enum -from dataclasses import dataclass, field -from typing import Annotated, override, assert_never - -from efro.dataclassio import ioprepped, IOAttrs, IOMultiType - - -class EffectTypeID(Enum): - """Type ID for each of our subclasses.""" - - UNKNOWN = 'u' - LEGACY_SCREEN_MESSAGE = 'm' - SCREEN_MESSAGE = 'sm' - SOUND = 's' - DELAY = 'd' - CHEST_WAIT_TIME_ANIMATION = 't' - TICKETS_ANIMATION = 'ta' - TOKENS_ANIMATION = 'toa' - - -class Effect(IOMultiType[EffectTypeID]): - """Something that can happen on the client. - - This can include screen messages, sounds, visual effects, etc. - """ - - @override - @classmethod - def get_type_id(cls) -> EffectTypeID: - # Require child classes to supply this themselves. If we did a - # full type registry/lookup here it would require us to import - # everything and would prevent lazy loading. - raise NotImplementedError() - - @override - @classmethod - def get_type(cls, type_id: EffectTypeID) -> type[Effect]: - """Return the subclass for each of our type-ids.""" - # pylint: disable=cyclic-import - # pylint: disable=too-many-return-statements - - t = EffectTypeID - if type_id is t.UNKNOWN: - return Unknown - if type_id is t.LEGACY_SCREEN_MESSAGE: - return LegacyScreenMessage - if type_id is t.SCREEN_MESSAGE: - return ScreenMessage - if type_id is t.SOUND: - return PlaySound - if type_id is t.DELAY: - return Delay - if type_id is t.CHEST_WAIT_TIME_ANIMATION: - return ChestWaitTimeAnimation - if type_id is t.TICKETS_ANIMATION: - return TicketsAnimation - if type_id is t.TOKENS_ANIMATION: - return TokensAnimation - - # Important to make sure we provide all types. - assert_never(type_id) - - @override - @classmethod - def get_unknown_type_fallback(cls) -> Effect: - # If we encounter some future message type we don't know - # anything about, drop in a placeholder. - return Unknown() - - -@ioprepped -@dataclass -class Unknown(Effect): - """Fallback substitute for types we don't recognize.""" - - @override - @classmethod - def get_type_id(cls) -> EffectTypeID: - return EffectTypeID.UNKNOWN - - -@ioprepped -@dataclass -class LegacyScreenMessage(Effect): - """Display a screen-message (Legacy version). - - This will be processed as an Lstr with translation category - 'serverResponses'. - - When possible, migrate to using :class:`ScreenMessage`. - """ - - message: Annotated[str, IOAttrs('m')] - subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field( - default_factory=list - ) - color: Annotated[ - tuple[float, float, float], IOAttrs('c', store_default=False) - ] = (1.0, 1.0, 1.0) - - @override - @classmethod - def get_type_id(cls) -> EffectTypeID: - return EffectTypeID.LEGACY_SCREEN_MESSAGE - - -@ioprepped -@dataclass -class ScreenMessage(Effect): - """Display a screen-message. - - Supported on engine build 22606 or newer. - - This version does no translation by default (expecting translation - to happen server-side). Pass a Lstr json string and set is_lstr=True - for client-side translation. - """ - - message: Annotated[str, IOAttrs('m')] - color: Annotated[ - tuple[float, float, float], IOAttrs('c', store_default=False) - ] = (1.0, 1.0, 1.0) - is_lstr: Annotated[bool, IOAttrs('l', store_default=False)] = False - - @override - @classmethod - def get_type_id(cls) -> EffectTypeID: - return EffectTypeID.SCREEN_MESSAGE - - -class Sound(Enum): - """Sounds that can be played.""" - - UNKNOWN = 'u' - CASH_REGISTER = 'c' - ERROR = 'e' - POWER_DOWN = 'p' - GUN_COCKING = 'g' - - -@ioprepped -@dataclass -class PlaySound(Effect): - """Play a sound.""" - - sound: Annotated[Sound, IOAttrs('s', enum_fallback=Sound.UNKNOWN)] - volume: Annotated[float, IOAttrs('v', store_default=False)] = 1.0 - - @override - @classmethod - def get_type_id(cls) -> EffectTypeID: - return EffectTypeID.SOUND - - -@ioprepped -@dataclass -class ChestWaitTimeAnimation(Effect): - """Animate chest wait time changing.""" - - chestid: Annotated[str, IOAttrs('c')] - duration: Annotated[float, IOAttrs('u')] - startvalue: Annotated[datetime.datetime, IOAttrs('o')] - endvalue: Annotated[datetime.datetime, IOAttrs('n')] - - @override - @classmethod - def get_type_id(cls) -> EffectTypeID: - return EffectTypeID.CHEST_WAIT_TIME_ANIMATION - - -@ioprepped -@dataclass -class TicketsAnimation(Effect): - """Animate tickets count.""" - - duration: Annotated[float, IOAttrs('u')] - startvalue: Annotated[int, IOAttrs('s')] - endvalue: Annotated[int, IOAttrs('e')] - - @override - @classmethod - def get_type_id(cls) -> EffectTypeID: - return EffectTypeID.TICKETS_ANIMATION - - -@ioprepped -@dataclass -class TokensAnimation(Effect): - """Animate tokens count.""" - - duration: Annotated[float, IOAttrs('u')] - startvalue: Annotated[int, IOAttrs('s')] - endvalue: Annotated[int, IOAttrs('e')] - - @override - @classmethod - def get_type_id(cls) -> EffectTypeID: - return EffectTypeID.TOKENS_ANIMATION - - -@ioprepped -@dataclass -class Delay(Effect): - """Delay effect processing.""" - - seconds: Annotated[float, IOAttrs('s')] - - @override - @classmethod - def get_type_id(cls) -> EffectTypeID: - return EffectTypeID.DELAY diff --git a/dist/ba_data/python/bacommon/cloud.py b/dist/ba_data/python/bacommon/cloud.py index c12690b..6a58091 100644 --- a/dist/ba_data/python/bacommon/cloud.py +++ b/dist/ba_data/python/bacommon/cloud.py @@ -1,12 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Cloud related functionality. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" +"""Functionality related to cloud functionality.""" from __future__ import annotations @@ -16,13 +10,9 @@ from typing import TYPE_CHECKING, Annotated, override from efro.message import Message, Response from efro.dataclassio import ioprepped, IOAttrs -from bacommon.analytics import AnalyticsEvent from bacommon.securedata import SecureDataChecker from bacommon.transfer import DirectoryManifest from bacommon.login import LoginType -from bacommon.docui import DocUIRequest, DocUIResponse -import bacommon.displayitem as ditm -import bacommon.clienteffect as clfx if TYPE_CHECKING: pass @@ -367,120 +357,3 @@ class CloudValsResponse(Response): """Here's them cloud vals ya asked for, boss.""" vals: Annotated[CloudVals, IOAttrs('v')] - - -@ioprepped -@dataclass -class ChestActionMessage(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 [ChestActionResponse] - - -@ioprepped -@dataclass -class ChestActionResponse(Response): - """Here's the results of that action you asked for, boss.""" - - # Tokens that were actually charged. - tokens_charged: Annotated[int, IOAttrs('t')] = 0 - - # If present, signifies the chest has been opened and we should show - # the user this stuff that was in it. - contents: Annotated[list[ditm.Wrapper] | None, IOAttrs('c')] = None - - # If contents are present, which of the chest's prize-sets they - # represent. - prizeindex: Annotated[int, IOAttrs('i')] = 0 - - # Printable error if something goes wrong. - error: Annotated[str | None, IOAttrs('e')] = None - - # Printable warning. Shown in orange with an error sound. Does not - # mean the action failed; only that there's something to tell the - # users such as 'It looks like you are faking ad views; stop it or - # you won't have ad options anymore.' - warning: Annotated[str | None, IOAttrs('w', store_default=False)] = None - - # Printable success message. Shown in green with a cash-register - # sound. Can be used for things like successful wait reductions via - # ad views. Used in builds earlier than 22311; can remove once - # 22311+ is ubiquitous. - success_msg: Annotated[str | None, IOAttrs('s', store_default=False)] = None - - # Effects to show on the client. Replaces warning and success_msg in - # build 22311 or newer. - effects: Annotated[ - list[clfx.Effect], IOAttrs('fx', store_default=False) - ] = field(default_factory=list) - - -@ioprepped -@dataclass -class FulfillDocUIRequest(Message): - """Can a fella get a doc-ui round here?""" - - request: Annotated[DocUIRequest, IOAttrs('r')] - domain: Annotated[str, IOAttrs('d')] - - @override - @classmethod - def get_response_types(cls) -> list[type[Response] | None]: - return [FulfillDocUIResponse] - - -@ioprepped -@dataclass -class FulfillDocUIResponse(Response): - """Here's that doc-ui you asked for, boss.""" - - response: Annotated[DocUIResponse, IOAttrs('r')] - - -@ioprepped -@dataclass -class AnalyticsEventMessage(Message): - """Have a nice analytics event!""" - - event: Annotated[AnalyticsEvent, IOAttrs('e')] - - -@ioprepped -@dataclass -class AuthRequestMessage(Message): - """Request access to a server for a current account.""" - - global_app_instance_uuid: Annotated[str, IOAttrs('a')] - - @override - @classmethod - def get_response_types(cls) -> list[type[Response] | None]: - return [AuthRequestResponse] - - -@ioprepped -@dataclass -class AuthRequestResponse(Response): - """Here's that access ya asked for boss.""" - - error: Annotated[str | None, IOAttrs('e')] - token: Annotated[str | None, IOAttrs('t')] diff --git a/dist/ba_data/python/bacommon/clouddialog/__init__.py b/dist/ba_data/python/bacommon/clouddialog/__init__.py deleted file mode 100644 index c6e6a4c..0000000 --- a/dist/ba_data/python/bacommon/clouddialog/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Functionality related to cloud-dialogs. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" - -from bacommon.clouddialog._clouddialog import ( - CloudDialogTypeID, - CloudDialog, - Unknown, - Wrapper, - Action, - ActionMessage, - ActionResponse, -) - -__all__ = [ - 'CloudDialogTypeID', - 'CloudDialog', - 'Unknown', - 'Wrapper', - 'Action', - 'ActionMessage', - 'ActionResponse', -] diff --git a/dist/ba_data/python/bacommon/clouddialog/_clouddialog.py b/dist/ba_data/python/bacommon/clouddialog/_clouddialog.py deleted file mode 100644 index 4ec1015..0000000 --- a/dist/ba_data/python/bacommon/clouddialog/_clouddialog.py +++ /dev/null @@ -1,142 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Simple cloud-defined UIs for things like notifications. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" - -from __future__ import annotations - -import datetime -from enum import Enum -from dataclasses import dataclass -from typing import Annotated, override, assert_never - -from efro.dataclassio import ioprepped, IOAttrs, IOMultiType -from efro.message import Message, Response - -import bacommon.clienteffect as clfx - - -class CloudDialogTypeID(Enum): - """Type ID for each of our subclasses.""" - - UNKNOWN = 'u' - BASIC = 'b' - - -class CloudDialog(IOMultiType[CloudDialogTypeID]): - """Small self-contained ui bit provided by the cloud. - - These take care of updating and/or dismissing themselves based on - user input. Useful for things such as inbox messages. For more - complex UI construction, look at :mod:`bacommon.docui`. - """ - - @override - @classmethod - def get_type_id(cls) -> CloudDialogTypeID: - # Require child classes to supply this themselves. If we did a - # full type registry/lookup here it would require us to import - # everything and would prevent lazy loading. - raise NotImplementedError() - - @override - @classmethod - def get_type(cls, type_id: CloudDialogTypeID) -> type[CloudDialog]: - """Return the subclass for each of our type-ids.""" - # pylint: disable=cyclic-import - - t = CloudDialogTypeID - - if type_id is t.UNKNOWN: - return Unknown - - if type_id is t.BASIC: - from bacommon.clouddialog.basic import BasicCloudDialog - - return BasicCloudDialog - - # Make sure we provide all types. - assert_never(type_id) - - @override - @classmethod - def get_unknown_type_fallback(cls) -> CloudDialog: - # If we encounter some future message type we don't know - # anything about, drop in a placeholder. - return Unknown() - - -@ioprepped -@dataclass -class Unknown(CloudDialog): - """Fallback type for unrecognized entries.""" - - @override - @classmethod - def get_type_id(cls) -> CloudDialogTypeID: - return CloudDialogTypeID.UNKNOWN - - -@ioprepped -@dataclass -class Wrapper: - """Wrapper for a CloudDialog and its common data.""" - - id: Annotated[str, IOAttrs('i')] - createtime: Annotated[datetime.datetime, IOAttrs('c')] - ui: Annotated[CloudDialog, IOAttrs('e')] - - -class Action(Enum): - """Types of actions we can run.""" - - BUTTON_PRESS_POSITIVE = 'p' - BUTTON_PRESS_NEGATIVE = 'n' - - -@ioprepped -@dataclass -class ActionMessage(Message): - """Do something to a client ui.""" - - id: Annotated[str, IOAttrs('i')] - action: Annotated[Action, IOAttrs('a')] - - @override - @classmethod - def get_response_types(cls) -> list[type[Response] | None]: - return [ActionResponse] - - -@ioprepped -@dataclass -class ActionResponse(Response): - """Did something to that inbox entry, boss.""" - - class ErrorType(Enum): - """Types of errors that may have occurred.""" - - # Probably a future error type we don't recognize. - UNKNOWN = 'u' - - # Something went wrong on the server, but specifics are not - # relevant. - INTERNAL = 'i' - - # The entry expired on the server. In various cases such as 'ok' - # buttons this can generally be ignored. - EXPIRED = 'e' - - error_type: Annotated[ - ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN) - ] - - # User facing error message in the case of errors. - error_message: Annotated[str | None, IOAttrs('em')] - - effects: Annotated[list[clfx.Effect], IOAttrs('fx')] diff --git a/dist/ba_data/python/bacommon/clouddialog/basic.py b/dist/ba_data/python/bacommon/clouddialog/basic.py deleted file mode 100644 index 635b2fb..0000000 --- a/dist/ba_data/python/bacommon/clouddialog/basic.py +++ /dev/null @@ -1,233 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Basic cloud-dialog.""" - -from __future__ import annotations - -import datetime -from enum import Enum -from dataclasses import dataclass, field -from typing import Annotated, override, assert_never - -from efro.dataclassio import ioprepped, IOAttrs, IOMultiType - -import bacommon.displayitem as ditm -from bacommon.clouddialog._clouddialog import CloudDialog, CloudDialogTypeID - - -class ComponentTypeID(Enum): - """Type ID for each of our subclasses.""" - - UNKNOWN = 'u' - TEXT = 't' - LINK = 'l' - BS_CLASSIC_TOURNEY_RESULT = 'ct' - DISPLAY_ITEMS = 'di' - EXPIRE_TIME = 'd' - - -class Component(IOMultiType[ComponentTypeID]): - """Top level class for our multitype.""" - - @override - @classmethod - def get_type_id(cls) -> ComponentTypeID: - # Require child classes to supply this themselves. If we did a - # full type registry/lookup here it would require us to import - # everything and would prevent lazy loading. - raise NotImplementedError() - - @override - @classmethod - def get_type(cls, type_id: ComponentTypeID) -> type[Component]: - """Return the subclass for each of our type-ids.""" - # pylint: disable=cyclic-import - - t = ComponentTypeID - if type_id is t.UNKNOWN: - return Unknown - if type_id is t.TEXT: - return Text - if type_id is t.LINK: - return Link - if type_id is t.BS_CLASSIC_TOURNEY_RESULT: - return ClassicTourneyResult - if type_id is t.DISPLAY_ITEMS: - return DisplayItems - if type_id is t.EXPIRE_TIME: - return ExpireTime - - # Important to make sure we provide all types. - assert_never(type_id) - - @override - @classmethod - def get_unknown_type_fallback(cls) -> Component: - # If we encounter some future message type we don't know - # anything about, drop in a placeholder. - return Unknown() - - -@ioprepped -@dataclass -class Unknown(Component): - """An unknown basic client component type. - - In practice these should never show up since the master-server - generates these on the fly for the client and so should not send - clients one they can't digest. - """ - - @override - @classmethod - def get_type_id(cls) -> ComponentTypeID: - return ComponentTypeID.UNKNOWN - - -@ioprepped -@dataclass -class Text(Component): - """Show some text in the inbox message.""" - - text: Annotated[str, IOAttrs('t')] - subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field( - default_factory=list - ) - scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0 - color: Annotated[ - tuple[float, float, float, float], IOAttrs('c', store_default=False) - ] = (1.0, 1.0, 1.0, 1.0) - spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 - spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 - - @override - @classmethod - def get_type_id(cls) -> ComponentTypeID: - return ComponentTypeID.TEXT - - -@ioprepped -@dataclass -class Link(Component): - """Show a link in the inbox message.""" - - url: Annotated[str, IOAttrs('u')] - label: Annotated[str, IOAttrs('l')] - subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field( - default_factory=list - ) - spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 - spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 - - @override - @classmethod - def get_type_id(cls) -> ComponentTypeID: - return ComponentTypeID.LINK - - -@ioprepped -@dataclass -class ClassicTourneyResult(Component): - """Show info about a classic tourney.""" - - tournament_id: Annotated[str, IOAttrs('t')] - game: Annotated[str, IOAttrs('g')] - players: Annotated[int, IOAttrs('p')] - rank: Annotated[int, IOAttrs('r')] - trophy: Annotated[str | None, IOAttrs('tr')] - prizes: Annotated[list[ditm.Wrapper], IOAttrs('pr')] - - @override - @classmethod - def get_type_id(cls) -> ComponentTypeID: - return ComponentTypeID.BS_CLASSIC_TOURNEY_RESULT - - -@ioprepped -@dataclass -class DisplayItems(Component): - """Show some display-items.""" - - items: Annotated[list[ditm.Wrapper], IOAttrs('d')] - width: Annotated[float, IOAttrs('w')] = 100.0 - spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 - spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 - - @override - @classmethod - def get_type_id(cls) -> ComponentTypeID: - return ComponentTypeID.DISPLAY_ITEMS - - -@ioprepped -@dataclass -class ExpireTime(Component): - """Show expire-time.""" - - time: Annotated[datetime.datetime, IOAttrs('d')] - spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 - spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 - - @override - @classmethod - def get_type_id(cls) -> ComponentTypeID: - return ComponentTypeID.EXPIRE_TIME - - -class ButtonLabel(Enum): - """Distinct button labels we support.""" - - UNKNOWN = 'u' - OK = 'o' - APPLY = 'a' - CANCEL = 'c' - ACCEPT = 'ac' - DECLINE = 'dn' - IGNORE = 'ig' - CLAIM = 'cl' - DISCARD = 'd' - - -class InteractionStyle(Enum): - """Overall interaction styles we support.""" - - UNKNOWN = 'u' - BUTTON_POSITIVE = 'p' - BUTTON_POSITIVE_NEGATIVE = 'pn' - - -@ioprepped -@dataclass -class BasicCloudDialog(CloudDialog): - """A basic UI for the client.""" - - components: Annotated[list[Component], IOAttrs('s')] - - interaction_style: Annotated[ - InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN) - ] = InteractionStyle.BUTTON_POSITIVE - - button_label_positive: Annotated[ - ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN) - ] = ButtonLabel.OK - - button_label_negative: Annotated[ - ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN) - ] = ButtonLabel.CANCEL - - @override - @classmethod - def get_type_id(cls) -> CloudDialogTypeID: - return CloudDialogTypeID.BASIC - - def contains_unknown_elements(self) -> bool: - """Whether something within us is an unknown type or enum.""" - return ( - self.interaction_style is InteractionStyle.UNKNOWN - or self.button_label_positive is ButtonLabel.UNKNOWN - or self.button_label_negative is ButtonLabel.UNKNOWN - or any( - c.get_type_id() is ComponentTypeID.UNKNOWN - for c in self.components - ) - ) diff --git a/dist/ba_data/python/bacommon/docui/__init__.py b/dist/ba_data/python/bacommon/docui/__init__.py deleted file mode 100644 index 9d72511..0000000 --- a/dist/ba_data/python/bacommon/docui/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Declarative UI system. - -A high level way to build UIs that lives as a layer on top of engine -apis such as :mod:`bauiv1`. UIs can easily be serialized to json data -and be provided by webservers or other local or remote sources. -""" - -from bacommon.docui._docui import ( - DocUIRequest, - DocUIRequestTypeID, - UnknownDocUIRequest, - DocUIResponse, - DocUIResponseTypeID, - UnknownDocUIResponse, - DocUIWebRequest, - DocUIWebResponse, -) - -__all__ = [ - 'DocUIRequest', - 'DocUIRequestTypeID', - 'UnknownDocUIRequest', - 'DocUIResponse', - 'DocUIResponseTypeID', - 'UnknownDocUIResponse', - 'DocUIWebRequest', - 'DocUIWebResponse', -] diff --git a/dist/ba_data/python/bacommon/docui/_docui.py b/dist/ba_data/python/bacommon/docui/_docui.py deleted file mode 100644 index d7b8419..0000000 --- a/dist/ba_data/python/bacommon/docui/_docui.py +++ /dev/null @@ -1,172 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Version 1 of our doc-ui system.""" - -from __future__ import annotations - -from enum import Enum -from dataclasses import dataclass -from typing import override, assert_never, TYPE_CHECKING, Annotated - -from efro.dataclassio import ioprepped, IOAttrs, IOMultiType -from bacommon.locale import Locale - -if TYPE_CHECKING: - pass - - -class DocUIRequestTypeID(Enum): - """Type ID for each of our subclasses.""" - - UNKNOWN = 'u' - V1 = 'v1' - - -class DocUIRequest(IOMultiType[DocUIRequestTypeID]): - """A request for some UI.""" - - @override - @classmethod - def get_type_id(cls) -> DocUIRequestTypeID: - # Require child classes to supply this themselves. If we did a - # full type registry/lookup here it would require us to import - # everything and would prevent lazy loading. - raise NotImplementedError() - - @override - @classmethod - def get_type(cls, type_id: DocUIRequestTypeID) -> type[DocUIRequest]: - """Return the subclass for each of our type-ids.""" - # pylint: disable=cyclic-import - - t = DocUIRequestTypeID - if type_id is t.UNKNOWN: - return UnknownDocUIRequest - if type_id is t.V1: - from bacommon.docui.v1 import Request - - return Request - - # Make sure we cover all types. - assert_never(type_id) - - @override - @classmethod - def get_unknown_type_fallback(cls) -> DocUIRequest: - # If we encounter some future type we don't know anything about, - # drop in a placeholder. - return UnknownDocUIRequest() - - @override - @classmethod - def get_type_id_storage_name(cls) -> str: - return '_t' - - -@ioprepped -@dataclass -class UnknownDocUIRequest(DocUIRequest): - """Fallback type for unrecognized UI types. - - Will show the client a 'cannot display this UI' placeholder request. - """ - - @override - @classmethod - def get_type_id(cls) -> DocUIRequestTypeID: - return DocUIRequestTypeID.UNKNOWN - - -class DocUIResponseTypeID(Enum): - """Type ID for each of our subclasses.""" - - UNKNOWN = 'u' - V1 = 'v1' - - -class DocUIResponse(IOMultiType[DocUIResponseTypeID]): - """A UI provied in response to a :class:`DocUIRequest`.""" - - @override - @classmethod - def get_type_id(cls) -> DocUIResponseTypeID: - # Require child classes to supply this themselves. If we did a - # full type registry/lookup here it would require us to import - # everything and would prevent lazy loading. - raise NotImplementedError() - - @override - @classmethod - def get_type(cls, type_id: DocUIResponseTypeID) -> type[DocUIResponse]: - """Return the subclass for each of our type-ids.""" - # pylint: disable=cyclic-import - - t = DocUIResponseTypeID - if type_id is t.UNKNOWN: - return UnknownDocUIResponse - if type_id is t.V1: - from bacommon.docui.v1 import Response - - return Response - - # Make sure we cover all types. - assert_never(type_id) - - @override - @classmethod - def get_unknown_type_fallback(cls) -> DocUIResponse: - # If we encounter some future type we don't know anything about, - # drop in a placeholder. - return UnknownDocUIResponse() - - @override - @classmethod - def get_type_id_storage_name(cls) -> str: - return '_t' - - -@ioprepped -@dataclass -class UnknownDocUIResponse(DocUIResponse): - """Fallback type for unrecognized UI types. - - Will show the client a 'cannot display this UI' placeholder response. - """ - - @override - @classmethod - def get_type_id(cls) -> DocUIResponseTypeID: - return DocUIResponseTypeID.UNKNOWN - - -@ioprepped -@dataclass -class DocUIWebRequest: - """Complete data sent for doc-ui http requests.""" - - #: The wrapped doc-ui request. - doc_ui_request: Annotated[DocUIRequest, IOAttrs('r')] - - #: The current locale of the client. doc-ui generally deals in raw - #: strings and expects localization to happen on the server. - locale: Annotated[Locale, IOAttrs('l')] - - #: Engine build number. In some cases it may make sense to adjust - #: responses depending on available engine features. - engine_build_number: Annotated[int, IOAttrs('b')] - - -@ioprepped -@dataclass -class DocUIWebResponse: - """Complete data returned for doc-ui http requests.""" - - #: Human readable error string (if an error occurs). Either this or - #: doc_ui_response should be set; not both. - error: Annotated[str | None, IOAttrs('e', store_default=False)] = None - - #: doc-ui response. Either this or error should be set; not both. - doc_ui_response: Annotated[ - DocUIResponse | None, - IOAttrs('r', store_default=False), - ] = None diff --git a/dist/ba_data/python/bacommon/docui/v1.py b/dist/ba_data/python/bacommon/docui/v1.py deleted file mode 100644 index 91f8afc..0000000 --- a/dist/ba_data/python/bacommon/docui/v1.py +++ /dev/null @@ -1,783 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Version 1 doc-ui types.""" - -from __future__ import annotations - -from enum import Enum -from dataclasses import dataclass, field -from typing import Annotated, override, assert_never - -from efro.dataclassio import ioprepped, IOAttrs, IOMultiType - -import bacommon.displayitem as ditm -import bacommon.clienteffect as clfx -from bacommon.docui._docui import ( - DocUIRequest, - DocUIRequestTypeID, - DocUIResponse, - DocUIResponseTypeID, -) - - -class RequestMethod(Enum): - """Typeof of requests that can be made to doc-ui servers.""" - - #: An unknown request method. This can appear if a newer client is - #: requesting some method from an older server that is not known to - #: the server. - UNKNOWN = 'u' - - #: Fetch some resource. This can be retried and its results can - #: optionally be cached for some amount of time. - GET = 'g' - - #: Change some resource. This cannot be implicitly retried (at least - #: without deduplication), nor can it be cached. - POST = 'p' - - -@ioprepped -@dataclass -class Request(DocUIRequest): - """Full request to doc-ui.""" - - path: Annotated[str, IOAttrs('p')] - method: Annotated[ - RequestMethod, - IOAttrs('m', store_default=False, enum_fallback=RequestMethod.UNKNOWN), - ] = RequestMethod.GET - args: Annotated[dict, IOAttrs('r', store_default=False)] = field( - default_factory=dict - ) - - @override - @classmethod - def get_type_id(cls) -> DocUIRequestTypeID: - return DocUIRequestTypeID.V1 - - -class ActionTypeID(Enum): - """Type ID for each of our subclasses.""" - - BROWSE = 'b' - REPLACE = 'r' - LOCAL = 'l' - UNKNOWN = 'u' - - -class Action(IOMultiType[ActionTypeID]): - """Top level class for our multitype.""" - - @override - @classmethod - def get_type_id(cls) -> ActionTypeID: - # Require child classes to supply this themselves. If we did a - # full type registry/lookup here it would require us to import - # everything and would prevent lazy loading. - raise NotImplementedError() - - @override - @classmethod - def get_type(cls, type_id: ActionTypeID) -> type[Action]: - """Return the subclass for each of our type-ids.""" - # pylint: disable=cyclic-import - - t = ActionTypeID - if type_id is t.BROWSE: - return Browse - if type_id is t.REPLACE: - return Replace - if type_id is t.LOCAL: - return Local - if type_id is t.UNKNOWN: - return UnknownAction - - # Important to make sure we provide all types. - assert_never(type_id) - - @override - @classmethod - def get_type_id_storage_name(cls) -> str: - return '_t' - - @override - @classmethod - def get_unknown_type_fallback(cls) -> Action: - # If we encounter some future type we don't know anything about, - # drop in a placeholder. - return UnknownAction() - - -@ioprepped -@dataclass -class UnknownAction(Action): - """Action type we don't recognize.""" - - @override - @classmethod - def get_type_id(cls) -> ActionTypeID: - return ActionTypeID.UNKNOWN - - -@ioprepped -@dataclass -class Browse(Action): - """Browse to a new page in a new window.""" - - request: Annotated[Request, IOAttrs('r')] - - #: Plays a swish. - default_sound: Annotated[bool, IOAttrs('ds', store_default=False)] = True - - #: Client-effects to run immediately when the button is pressed. - #: - #: :meta private: - immediate_client_effects: Annotated[ - list[clfx.Effect], IOAttrs('fx', store_default=False) - ] = field(default_factory=list) - - #: Local action to run immediately when the button is pressed. Will - #: be handled by - #: :meth:`bauiv1lib.docui.DocUIController.local_action()`. - immediate_local_action: Annotated[ - str | None, IOAttrs('a', store_default=False) - ] = None - immediate_local_action_args: Annotated[ - dict | None, IOAttrs('aa', store_default=False) - ] = None - - @override - @classmethod - def get_type_id(cls) -> ActionTypeID: - return ActionTypeID.BROWSE - - -@ioprepped -@dataclass -class Replace(Action): - """Replace current page with a new one. - - Should be used to effectively 'modify' existing UIs by replacing - them with something slightly different. Things like scroll position - and selection will be carried across to the new layout when possible - to make for a seamless transition. - """ - - request: Annotated[Request, IOAttrs('r')] - - #: Plays a click if triggered by a button press. - default_sound: Annotated[bool, IOAttrs('ds', store_default=False)] = True - - #: Client-effects to run immediately when the button is pressed. - #: - #: :meta private: - immediate_client_effects: Annotated[ - list[clfx.Effect], IOAttrs('fx', store_default=False) - ] = field(default_factory=list) - - #: Local action to run immediately when the button is pressed. Will - #: be handled by - #: :meth:`bauiv1lib.docui.DocUIController.local_action()`. - immediate_local_action: Annotated[ - str | None, IOAttrs('a', store_default=False) - ] = None - immediate_local_action_args: Annotated[ - dict | None, IOAttrs('aa', store_default=False) - ] = None - - @override - @classmethod - def get_type_id(cls) -> ActionTypeID: - return ActionTypeID.REPLACE - - -@ioprepped -@dataclass -class Local(Action): - """Perform only local actions; no new requests or page changes.""" - - close_window: Annotated[bool, IOAttrs('c', store_default=False)] = False - - #: Plays a swish if closing the window or a click if triggered by a - #: button press. - default_sound: Annotated[bool, IOAttrs('ds', store_default=False)] = True - - #: Client-effects to run immediately when the button is pressed. - #: - #: :meta private: - immediate_client_effects: Annotated[ - list[clfx.Effect], IOAttrs('fx', store_default=False) - ] = field(default_factory=list) - - #: Local action to run immediately when the button is pressed. Will - #: be handled by - #: :meth:`bauiv1lib.docui.DocUIController.local_action()`. - immediate_local_action: Annotated[ - str | None, IOAttrs('a', store_default=False) - ] = None - immediate_local_action_args: Annotated[ - dict | None, IOAttrs('aa', store_default=False) - ] = None - - @override - @classmethod - def get_type_id(cls) -> ActionTypeID: - return ActionTypeID.LOCAL - - -class HAlign(Enum): - """Horizontal alignment.""" - - LEFT = 'l' - CENTER = 'c' - RIGHT = 'r' - - -class VAlign(Enum): - """Vertical alignment.""" - - TOP = 't' - CENTER = 'c' - BOTTOM = 'b' - - -class DecorationTypeID(Enum): - """Type ID for each of our subclasses.""" - - UNKNOWN = 'u' - TEXT = 't' - IMAGE = 'i' - DISPLAY_ITEM = 'd' - - -class Decoration(IOMultiType[DecorationTypeID]): - """Top level class for our multitype.""" - - @override - @classmethod - def get_type_id(cls) -> DecorationTypeID: - # Require child classes to supply this themselves. If we did a - # full type registry/lookup here it would require us to import - # everything and would prevent lazy loading. - raise NotImplementedError() - - @override - @classmethod - def get_type(cls, type_id: DecorationTypeID) -> type[Decoration]: - # pylint: disable=cyclic-import - - t = DecorationTypeID - if type_id is t.UNKNOWN: - return UnknownDecoration - if type_id is t.TEXT: - return Text - if type_id is t.IMAGE: - return Image - if type_id is t.DISPLAY_ITEM: - return DisplayItem - - # Important to make sure we provide all types. - assert_never(type_id) - - @override - @classmethod - def get_unknown_type_fallback(cls) -> Decoration: - # If we encounter some future type we don't know anything about, - # drop in a placeholder. - return UnknownDecoration() - - @override - @classmethod - def get_type_id_storage_name(cls) -> str: - return '_t' - - -@ioprepped -@dataclass -class UnknownDecoration(Decoration): - """An unknown decoration. - - In practice these should never show up since the master-server - generates these on the fly for the client and so should not send - clients one they can't digest. - """ - - @override - @classmethod - def get_type_id(cls) -> DecorationTypeID: - return DecorationTypeID.UNKNOWN - - -@ioprepped -@dataclass -class Text(Decoration): - """Text decoration.""" - - #: Note that doc-ui accepts only raw :class:`str` values for text; - #: use :meth:`babase.Lstr.evaluate()` or whatnot for multi-language - #: support. - text: Annotated[str, IOAttrs('t')] - position: Annotated[tuple[float, float], IOAttrs('p')] - - #: Note that this effectively is max-width and max-height. - size: Annotated[tuple[float, float], IOAttrs('i')] - scale: Annotated[float, IOAttrs('s', store_default=False)] = 1.0 - h_align: Annotated[HAlign, IOAttrs('ha', store_default=False)] = ( - HAlign.CENTER - ) - v_align: Annotated[VAlign, IOAttrs('va', store_default=False)] = ( - VAlign.CENTER - ) - color: Annotated[ - tuple[float, float, float, float] | None, - IOAttrs('c', store_default=False), - ] = None - flatness: Annotated[float | None, IOAttrs('f', store_default=False)] = None - shadow: Annotated[float | None, IOAttrs('sh', store_default=False)] = None - - is_lstr: Annotated[bool, IOAttrs('l', store_default=False)] = False - - highlight: Annotated[bool, IOAttrs('h', store_default=False)] = True - depth_range: Annotated[tuple[float, float] | None, IOAttrs('z')] = None - - #: Show max-width/height bounds; useful during development. - debug: Annotated[bool, IOAttrs('d', store_default=False)] = False - - @override - @classmethod - def get_type_id(cls) -> DecorationTypeID: - return DecorationTypeID.TEXT - - -@ioprepped -@dataclass -class Image(Decoration): - """Image decoration.""" - - texture: Annotated[str, IOAttrs('t')] - position: Annotated[tuple[float, float], IOAttrs('p')] - size: Annotated[tuple[float, float], IOAttrs('s')] - color: Annotated[ - tuple[float, float, float, float] | None, - IOAttrs('c', store_default=False), - ] = None - h_align: Annotated[HAlign, IOAttrs('ha', store_default=False)] = ( - HAlign.CENTER - ) - v_align: Annotated[VAlign, IOAttrs('va', store_default=False)] = ( - VAlign.CENTER - ) - tint_texture: Annotated[str | None, IOAttrs('tt', store_default=False)] = ( - None - ) - tint_color: Annotated[ - tuple[float, float, float] | None, IOAttrs('tc1', store_default=False) - ] = None - tint2_color: Annotated[ - tuple[float, float, float] | None, IOAttrs('tc2', store_default=False) - ] = None - mask_texture: Annotated[str | None, IOAttrs('mt', store_default=False)] = ( - None - ) - mesh_opaque: Annotated[str | None, IOAttrs('mo', store_default=False)] = ( - None - ) - mesh_transparent: Annotated[ - str | None, IOAttrs('mn', store_default=False) - ] = None - highlight: Annotated[bool, IOAttrs('h', store_default=False)] = True - depth_range: Annotated[tuple[float, float] | None, IOAttrs('z')] = None - - @override - @classmethod - def get_type_id(cls) -> DecorationTypeID: - return DecorationTypeID.IMAGE - - -class DisplayItemStyle(Enum): - """Styles a display-item can be drawn in.""" - - #: Shows graphics and/or text fully conveying what the item is. Fits - #: in to a 4:3 box and works best with large-ish displays. - FULL = 'f' - - #: Graphics and/or text fully conveying what the item is, but - #: condensed to fit in a 2:1 box displayed at small sizes. - COMPACT = 'c' - - #: A graphics-only representation of the item (though text may be - #: used in fallback cases). Does not fully convey what the item is, - #: but instead is intended to be used alongside the item's textual - #: description. For example, some number of coins may simply display - #: a coin graphic here without the number. Draws in a 1:1 box and - #: works for large or small display. - ICON = 'i' - - -@ioprepped -@dataclass -class DisplayItem(Decoration): - """DisplayItem decoration.""" - - wrapper: Annotated[ditm.Wrapper, IOAttrs('w')] - position: Annotated[tuple[float, float], IOAttrs('p')] - size: Annotated[tuple[float, float], IOAttrs('s')] - style: Annotated[DisplayItemStyle, IOAttrs('t', store_default=False)] = ( - DisplayItemStyle.FULL - ) - text_color: Annotated[ - tuple[float, float, float] | None, IOAttrs('c', store_default=False) - ] = None - highlight: Annotated[bool, IOAttrs('h', store_default=False)] = True - depth_range: Annotated[tuple[float, float] | None, IOAttrs('z')] = None - debug: Annotated[bool, IOAttrs('d', store_default=False)] = False - - @override - @classmethod - def get_type_id(cls) -> DecorationTypeID: - return DecorationTypeID.DISPLAY_ITEM - - -class ButtonStyle(Enum): - """Styles a button can be.""" - - SQUARE = 'q' - TAB = 't' - SMALL = 's' - MEDIUM = 'm' - LARGE = 'l' - LARGER = 'xl' - BACK = 'b' - BACK_SMALL = 'bs' - SQUARE_WIDE = 'w' - - -@ioprepped -@dataclass -class Button: - """A button in our doc-ui. - - Note that size, padding, and all decorations are scaled consistently - with 'scale'. - """ - - #: Note that doc-ui accepts only raw :class:`str` values for text; - #: use :meth:`babase.Lstr.evaluate()` or whatnot for multi-language - #: support. - label: Annotated[str | None, IOAttrs('l', store_default=False)] = None - - action: Annotated[Action | None, IOAttrs('a', store_default=False)] = None - - size: Annotated[ - tuple[float, float] | None, IOAttrs('sz', store_default=False) - ] = None - color: Annotated[ - tuple[float, float, float, float] | None, - IOAttrs('cl', store_default=False), - ] = None - label_color: Annotated[ - tuple[float, float, float, float] | None, - IOAttrs('lc', store_default=False), - ] = None - label_flatness: Annotated[ - float | None, IOAttrs('lf', store_default=False) - ] = None - label_scale: Annotated[float | None, IOAttrs('ls', store_default=False)] = ( - None - ) - label_is_lstr: Annotated[bool, IOAttrs('ll', store_default=False)] = False - texture: Annotated[str | None, IOAttrs('tex', store_default=False)] = None - scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0 - padding_left: Annotated[float, IOAttrs('pl', store_default=False)] = 0.0 - padding_top: Annotated[float, IOAttrs('pt', store_default=False)] = 0.0 - padding_right: Annotated[float, IOAttrs('pr', store_default=False)] = 0.0 - padding_bottom: Annotated[float, IOAttrs('pb', store_default=False)] = 0.0 - decorations: Annotated[ - list[Decoration] | None, IOAttrs('c', store_default=False) - ] = None - style: Annotated[ButtonStyle, IOAttrs('y', store_default=False)] = ( - ButtonStyle.SQUARE - ) - default: Annotated[bool, IOAttrs('df', store_default=False)] = False - selected: Annotated[bool, IOAttrs('sel', store_default=False)] = False - - icon: Annotated[str | None, IOAttrs('icn', store_default=False)] = None - icon_scale: Annotated[float | None, IOAttrs('is', store_default=False)] = ( - None - ) - icon_color: Annotated[ - tuple[float, float, float, float] | None, - IOAttrs('ic', store_default=False), - ] = None - depth_range: Annotated[ - tuple[float, float] | None, IOAttrs('z', store_default=None) - ] = None - - #: Custom widget id. Will be prefixed with window id, but must be - #: unique within the window. - widget_id: Annotated[str | None, IOAttrs('i', store_default=False)] = None - - #: Draw bounds of the button. - debug: Annotated[bool, IOAttrs('d', store_default=False)] = False - - -class RowTypeID(Enum): - """Type ID for each of our subclasses.""" - - BUTTON_ROW = 'b' - UNKNOWN = 'u' - - -class Row(IOMultiType[RowTypeID]): - """Top level class for our multitype.""" - - @override - @classmethod - def get_type_id(cls) -> RowTypeID: - # Require child classes to supply this themselves. If we did a - # full type registry/lookup here it would require us to import - # everything and would prevent lazy loading. - raise NotImplementedError() - - @override - @classmethod - def get_type(cls, type_id: RowTypeID) -> type[Row]: - """Return the subclass for each of our type-ids.""" - # pylint: disable=cyclic-import - - t = RowTypeID - if type_id is t.UNKNOWN: - return UnknownRow - if type_id is t.BUTTON_ROW: - return ButtonRow - - # Important to make sure we provide all types. - assert_never(type_id) - - @override - @classmethod - def get_unknown_type_fallback(cls) -> Row: - # If we encounter some future type we don't know anything about, - # drop in a placeholder. - return UnknownRow() - - @override - @classmethod - def get_type_id_storage_name(cls) -> str: - return '_t' - - -@ioprepped -@dataclass -class UnknownRow(Row): - """A row type we don't have.""" - - @override - @classmethod - def get_type_id(cls) -> RowTypeID: - return RowTypeID.UNKNOWN - - -@ioprepped -@dataclass -class ButtonRow(Row): - """A row consisting of buttons.""" - - buttons: Annotated[list[Button], IOAttrs('b')] - - header_height: Annotated[float, IOAttrs('h', store_default=False)] = 0.0 - header_scale: Annotated[float, IOAttrs('hs', store_default=False)] = 1.0 - header_decorations_left: Annotated[ - list[Decoration] | None, IOAttrs('hdl', store_default=False) - ] = None - header_decorations_center: Annotated[ - list[Decoration] | None, IOAttrs('hdc', store_default=False) - ] = None - header_decorations_right: Annotated[ - list[Decoration] | None, IOAttrs('hdr', store_default=False) - ] = None - - #: Note that doc-ui accepts only raw :class:`str` values for text; - #: use :meth:`babase.Lstr.evaluate()` or whatnot for multi-language - #: support. - title: Annotated[str | None, IOAttrs('t', store_default=False)] = None - title_color: Annotated[ - tuple[float, float, float, float] | None, - IOAttrs('tc', store_default=False), - ] = None - title_flatness: Annotated[ - float | None, IOAttrs('tf', store_default=False) - ] = None - title_shadow: Annotated[ - float | None, IOAttrs('ts', store_default=False) - ] = None - title_is_lstr: Annotated[bool, IOAttrs('tl', store_default=False)] = False - subtitle: Annotated[str | None, IOAttrs('s', store_default=False)] = None - subtitle_color: Annotated[ - tuple[float, float, float, float] | None, - IOAttrs('sc', store_default=False), - ] = None - subtitle_flatness: Annotated[ - float | None, IOAttrs('sf', store_default=False) - ] = None - subtitle_shadow: Annotated[ - float | None, IOAttrs('ss', store_default=False) - ] = None - subtitle_is_lstr: Annotated[bool, IOAttrs('sl', store_default=False)] = ( - False - ) - - #: Spacing between all buttons in the row. - button_spacing: Annotated[float, IOAttrs('bs', store_default=False)] = 15.0 - - #: Padding on the left of the row's horizonally-scrollable area. - padding_left: Annotated[float, IOAttrs('pl', store_default=False)] = 10.0 - #: Padding on the right of the row's horizonally-scrollable area. - padding_right: Annotated[float, IOAttrs('pr', store_default=False)] = 10.0 - #: Padding on the top of the row's horizonally-scrollable area. - padding_top: Annotated[float, IOAttrs('pt', store_default=False)] = 10.0 - #: Padding on the bottom of the row's horizonally-scrollable area. - padding_bottom: Annotated[float, IOAttrs('pb', store_default=False)] = 10.0 - - #: Extra space above the row's horizontally-scrollable area. - spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0 - - #: Extra space below the row's horizontally-scrollable area. - spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0 - - center_content: Annotated[bool, IOAttrs('c', store_default=False)] = False - center_title: Annotated[bool, IOAttrs('ct', store_default=False)] = False - - #: If things disappear when scrolling left/right, turn this up. - simple_culling_h: Annotated[float, IOAttrs('sch', store_default=False)] = ( - 100.0 - ) - - #: Draw bounds of the overall row and individual button columns - #: (including padding). The UI will scroll to keep these areas - #: visible in their entirety when changing selection via directional - #: controls, so try to make sure all decorations for a button are - #: within these bounds. - debug: Annotated[bool, IOAttrs('d', store_default=False)] = False - - @override - @classmethod - def get_type_id(cls) -> RowTypeID: - return RowTypeID.BUTTON_ROW - - -@ioprepped -@dataclass -class Page: - """Doc-UI page version 1.""" - - #: Note that doc-ui accepts only raw :class:`str` values for text; - #: use :meth:`babase.Lstr.evaluate()` or whatnot for multi-language - #: support. - title: Annotated[str, IOAttrs('t')] - rows: Annotated[list[Row], IOAttrs('r')] - - #: If True, content smaller than the available height will be - #: centered vertically. This can look natural for certain types of - #: content such as confirmation dialogs. - center_vertically: Annotated[bool, IOAttrs('cv', store_default=False)] = ( - False - ) - - row_spacing: Annotated[float, IOAttrs('s', store_default=False)] = 10.0 - - #: If things disappear when scrolling up and down, turn this up. - simple_culling_v: Annotated[float, IOAttrs('scv', store_default=False)] = ( - 100.0 - ) - - #: Whether the title is a json dict representing an Lstr. Generally - #: doc-ui translation should be handled server-side, but this can - #: allow client-side translation. - title_is_lstr: Annotated[bool, IOAttrs('tl', store_default=False)] = False - - padding_bottom: Annotated[float, IOAttrs('pb', store_default=False)] = 0.0 - padding_left: Annotated[float, IOAttrs('pl', store_default=False)] = 0.0 - padding_top: Annotated[float, IOAttrs('pt', store_default=False)] = 0.0 - padding_right: Annotated[float, IOAttrs('pr', store_default=False)] = 0.0 - - -class ResponseStatus(Enum): - """The overall result of a request.""" - - SUCCESS = 0 - - #: Something went wrong. That's all we know. - UNKNOWN_ERROR = 1 - - #: Something went wrong talking to the server. A 'Retry' button may - #: be appropriate to show here (for GET requests at least). - COMMUNICATION_ERROR = 2 - - #: This requires the user to be signed in, and they aint. - NOT_SIGNED_IN_ERROR = 3 - - -@ioprepped -@dataclass -class Response(DocUIResponse): - """Full docui response.""" - - page: Annotated[Page, IOAttrs('p')] - status: Annotated[ResponseStatus, IOAttrs('s', store_default=False)] = ( - ResponseStatus.SUCCESS - ) - - #: Effects to run on the client when this response is initially - #: received. Note that these effects will not re-run if the page is - #: automatically refreshed later (due to window resizing, back - #: navigation, etc). - #: - #: :meta private: - client_effects: Annotated[ - list[clfx.Effect], IOAttrs('fx', store_default=False) - ] = field(default_factory=list) - - #: Local action to run after this response is initially received. - #: Will be handled by - #: :meth:`bauiv1lib.docui.DocUIController.local_action()`. Note that - #: these actions will not re-run if the page is automatically - #: refreshed later (due to window resizing, back navigation, etc). - local_action: Annotated[str | None, IOAttrs('a', store_default=False)] = ( - None - ) - local_action_args: Annotated[ - dict | None, IOAttrs('aa', store_default=False) - ] = None - - #: New overall action to have the client schedule after this - #: response is received. Useful for redirecting to other pages or - #: closing the doc-ui window. - timed_action: Annotated[ - Action | None, IOAttrs('ta', store_default=False) - ] = None - timed_action_delay: Annotated[ - float, IOAttrs('tad', store_default=False) - ] = 0.0 - - #: If provided, error on builds older than this (can be used to gate - #: functionality without bumping entire docui version). - minimum_engine_build: Annotated[ - int | None, IOAttrs('b', store_default=False) - ] = None - - #: The client maintains some persistent state (such as widget - #: selection) for all pages viewed. The default index for these - #: states is the path of the request. If a server returns a - #: significant variety of responses for a single path, however, - #: (based on args, etc) then it may make sense for the server to - #: provide explicit state ids for those different variations. - shared_state_id: Annotated[ - str | None, IOAttrs('t', store_default=False) - ] = None - - @override - @classmethod - def get_type_id(cls) -> DocUIResponseTypeID: - return DocUIResponseTypeID.V1 diff --git a/dist/ba_data/python/bacommon/locale.py b/dist/ba_data/python/bacommon/locale.py index 5615f08..e6d9bb3 100644 --- a/dist/ba_data/python/bacommon/locale.py +++ b/dist/ba_data/python/bacommon/locale.py @@ -71,7 +71,6 @@ class Locale(Enum): VENETIAN = 'venetn' VIETNAMESE = 'viet' KAZAKH = 'kazk' - JAPANESE = 'jpn' # Note: We use if-statement chains here so we can use assert_never() # to ensure we cover all existing values. But we cache lookups so @@ -88,7 +87,7 @@ class Locale(Enum): # pylint: disable=too-many-branches # pylint: disable=too-many-return-statements - cls = Locale + cls = type(self) if self is cls.ENGLISH: return 'English' @@ -176,8 +175,6 @@ class Locale(Enum): return 'Vietnamese' if self is cls.KAZAKH: return 'Kazakh' - if self is cls.JAPANESE: - return 'Japanese' # Make sure we've covered all cases. assert_never(self) @@ -209,7 +206,7 @@ class Locale(Enum): # pylint: disable=too-many-branches # pylint: disable=too-many-return-statements - cls = Locale + cls = type(self) if self is cls.ENGLISH: return 'English' @@ -299,8 +296,6 @@ class Locale(Enum): return 'Vietnamese' if self is cls.KAZAKH: return 'Kazakh' - if self is cls.JAPANESE: - return 'Japanese' # Make sure we've covered all cases. assert_never(self) @@ -311,7 +306,7 @@ class Locale(Enum): # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches - cls = Locale + cls = type(self) R = LocaleResolved if self is cls.ENGLISH: @@ -394,8 +389,6 @@ class Locale(Enum): return R.VIETNAMESE if self is cls.KAZAKH: return R.KAZAKH - if self is cls.JAPANESE: - return R.JAPANESE # Make sure we're covering all cases. assert_never(self) @@ -451,7 +444,6 @@ class LocaleResolved(Enum): VENETIAN = 'venetn' VIETNAMESE = 'viet' KAZAKH = 'kazk' - JAPANESE = 'jpn' # Note: We use if-statement chains here so we can use assert_never() # to ensure we cover all existing values. But we cache lookups so @@ -472,7 +464,7 @@ class LocaleResolved(Enum): # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches - cls = LocaleResolved + cls = type(self) if self is cls.ENGLISH: return Locale.ENGLISH @@ -554,8 +546,6 @@ class LocaleResolved(Enum): return Locale.VIETNAMESE if self is cls.KAZAKH: return Locale.KAZAKH - if self is cls.JAPANESE: - return Locale.JAPANESE # Make sure we're covering all cases. assert_never(self) @@ -571,7 +561,7 @@ class LocaleResolved(Enum): """ # pylint: disable=too-many-branches # pylint: disable=too-many-statements - cls = LocaleResolved + cls = type(self) val: str | None = None @@ -657,8 +647,6 @@ class LocaleResolved(Enum): val = 'vi' elif self is cls.KAZAKH: val = 'kk' - elif self is cls.JAPANESE: - val = 'ja' else: # Make sure we cover all cases. assert_never(self) @@ -680,9 +668,9 @@ class LocaleResolved(Enum): return val - @staticmethod + @classmethod @lru_cache(maxsize=128) - def from_tag(tag: str) -> LocaleResolved: + def from_tag(cls, tag: str) -> LocaleResolved: """Return a locale for a given string tag. Tags can be provided in BCP 47 form ('en-US') or POSIX locale @@ -692,8 +680,6 @@ class LocaleResolved(Enum): # pylint: disable=too-many-statements # pylint: disable=too-many-return-statements - cls = LocaleResolved - # POSIX locale strings can contain a dot followed by an # encoding. Strip that off. tag2 = tag.split('.')[0] @@ -852,8 +838,6 @@ class LocaleResolved(Enum): return cls.VIETNAMESE if lang == 'kk': return cls.KAZAKH - if lang == 'ja': - return cls.JAPANESE # Make noise if we come across something unexpected so we can # add it. diff --git a/dist/ba_data/python/bacommon/loggercontrol.py b/dist/ba_data/python/bacommon/loggercontrol.py index 17c9b59..e07e811 100644 --- a/dist/ba_data/python/bacommon/loggercontrol.py +++ b/dist/ba_data/python/bacommon/loggercontrol.py @@ -108,11 +108,6 @@ class LoggerControlConfig: for logname in existinglognames: logger = logging.getLogger(logname) if logger.getEffectiveLevel() != self.get_effective_level(logname): - - # Exceptions for ones that I don't care to look into. - if logname in {'pyasn1'}: - continue - logging.error( 'loggercontrol effective-level sanity check failed;' ' expected logger %s to have effective level %s' diff --git a/dist/ba_data/python/bacommon/logging.py b/dist/ba_data/python/bacommon/logging.py index f115fdd..2b22fb0 100644 --- a/dist/ba_data/python/bacommon/logging.py +++ b/dist/ba_data/python/bacommon/logging.py @@ -48,7 +48,7 @@ class ClientLoggerName(Enum): """Return a short description for the logger.""" # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches - cls = ClientLoggerName + cls = type(self) if self is cls.BA: return 'top level Ballistica logger - use to adjust everything' if self is cls.ENV: diff --git a/dist/ba_data/python/bacommon/login.py b/dist/ba_data/python/bacommon/login.py index d7efb01..7fc3729 100644 --- a/dist/ba_data/python/bacommon/login.py +++ b/dist/ba_data/python/bacommon/login.py @@ -32,7 +32,7 @@ class LoginType(Enum): @property def displayname(self) -> str: """A human readable name for this value.""" - cls = LoginType + cls = type(self) match self: case cls.EMAIL: return 'Email/Password' @@ -44,7 +44,7 @@ class LoginType(Enum): @property def displaynameshort(self) -> str: """A short human readable name for this value.""" - cls = LoginType + cls = type(self) match self: case cls.EMAIL: return 'Email' diff --git a/dist/ba_data/python/bacommon/net.py b/dist/ba_data/python/bacommon/net.py index 8d60817..36596ed 100644 --- a/dist/ba_data/python/bacommon/net.py +++ b/dist/ba_data/python/bacommon/net.py @@ -1,12 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Network related data and functionality. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" +"""Network related data and functionality.""" from __future__ import annotations diff --git a/dist/ba_data/python/bacommon/securedata.py b/dist/ba_data/python/bacommon/securedata.py index 4e29856..2b4d740 100644 --- a/dist/ba_data/python/bacommon/securedata.py +++ b/dist/ba_data/python/bacommon/securedata.py @@ -1,12 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Functionality related to verifying server generated data. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" +"""Functionality related to verifying ballistica server generated data.""" import datetime from dataclasses import dataclass diff --git a/dist/ba_data/python/bacommon/servermanager.py b/dist/ba_data/python/bacommon/servermanager.py index e053098..9a0f8d9 100644 --- a/dist/ba_data/python/bacommon/servermanager.py +++ b/dist/ba_data/python/bacommon/servermanager.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to the server manager script.""" - from __future__ import annotations from enum import Enum @@ -30,19 +29,13 @@ class ServerConfig: # If True, all connecting clients will be authenticated through the # master server to screen for fake account info. Generally this # should always be enabled unless you are hosting on a LAN with no - # internet connection. Note that if you set protocol_version to 36 - # or newer, client authentication uses V2 account info. This is - # highly recommended as it does not have spoofing vulnerabilities - # like the earlier V1 authentication. + # internet connection. authenticate_clients: bool = True # IDs of server admins. Server admins are not kickable through the # default kick vote system and they are able to kick players without - # a vote. If protocol_version is set to 36 or newer this will use V2 - # account ids (a-XXX); otherwise it will use V1 ids (pb-XXX). To get - # your V2 account id, poke the 'manage account' button in the - # account window in-game. To get your V1 account id, enter - # 'getaccountid' in Settings->Advanced->Send Info + # a vote. To get your account id, enter 'getaccountid' in + # settings->advanced->enter-code. admins: list[str] = field(default_factory=list) # Whether the default kick-voting system is enabled. @@ -182,10 +175,7 @@ class ServerConfig: # Protocol version we host with. Currently the default is 33 which # still allows older 1.4 game clients to connect. Explicitly setting # to 35 no longer allows those clients but adds/fixes a few things - # such as making camera shake properly work in net games. Protocol - # 36 enables V2 account ids (a-XXX) for client authentication, which - # does not suffer from spoofing vulnerabilities that V1 account ids - # (pb-XXX) did. + # such as making camera shake properly work in net games. protocol_version: int | None = None # (internal) stress-testing mode. diff --git a/dist/ba_data/python/bacommon/text.py b/dist/ba_data/python/bacommon/text.py deleted file mode 100644 index 84e86ef..0000000 --- a/dist/ba_data/python/bacommon/text.py +++ /dev/null @@ -1,121 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Text related bits.""" - -from __future__ import annotations -from enum import Enum - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - pass - - -class SpecialChar(Enum): - """Custom unicode characters the engine can display. - - Keep this in sync with babase._mgen.enums.SpecialChar. - """ - - LEFT_ARROW = '\ue001' - RIGHT_ARROW = '\ue002' - UP_ARROW = '\ue003' - DOWN_ARROW = '\ue004' - LEFT_BUTTON = '\ue005' - TOP_BUTTON = '\ue006' - RIGHT_BUTTON = '\ue007' - BOTTOM_BUTTON = '\ue008' - DELETE = '\ue009' - SHIFT = '\ue00a' - BACK = '\ue00b' - LOGO_FLAT = '\ue00c' - REWIND_BUTTON = '\ue00d' - PLAY_PAUSE_BUTTON = '\ue00e' - FAST_FORWARD_BUTTON = '\ue00f' - DPAD_CENTER_BUTTON = '\ue010' - PLAY_STATION_CROSS_BUTTON = '\ue011' - PLAY_STATION_CIRCLE_BUTTON = '\ue012' - PLAY_STATION_TRIANGLE_BUTTON = '\ue013' - PLAY_STATION_SQUARE_BUTTON = '\ue014' - PLAY_BUTTON = '\ue015' - PAUSE_BUTTON = '\ue016' - CLOSE = '\ue017' - OUYA_BUTTON_O = '\ue019' - OUYA_BUTTON_U = '\ue01a' - OUYA_BUTTON_Y = '\ue01b' - OUYA_BUTTON_A = '\ue01c' - TOKEN = '\ue01d' - LOGO = '\ue01e' - TICKET = '\ue01f' - GOOGLE_PLAY_GAMES_LOGO = '\ue020' - GAME_CENTER_LOGO = '\ue021' - DICE_BUTTON1 = '\ue022' - DICE_BUTTON2 = '\ue023' - DICE_BUTTON3 = '\ue024' - DICE_BUTTON4 = '\ue025' - GAME_CIRCLE_LOGO = '\ue026' - PARTY_ICON = '\ue027' - TEST_ACCOUNT = '\ue028' - TICKET_BACKING = '\ue029' - TROPHY1 = '\ue02a' - TROPHY2 = '\ue02b' - TROPHY3 = '\ue02c' - TROPHY0A = '\ue02d' - TROPHY0B = '\ue02e' - TROPHY4 = '\ue02f' - LOCAL_ACCOUNT = '\ue030' - EXPLODINARY_LOGO = '\ue031' - FLAG_UNITED_STATES = '\ue032' - FLAG_MEXICO = '\ue033' - FLAG_GERMANY = '\ue034' - FLAG_BRAZIL = '\ue035' - FLAG_RUSSIA = '\ue036' - FLAG_CHINA = '\ue037' - FLAG_UNITED_KINGDOM = '\ue038' - FLAG_CANADA = '\ue039' - FLAG_INDIA = '\ue03a' - FLAG_JAPAN = '\ue03b' - FLAG_FRANCE = '\ue03c' - FLAG_INDONESIA = '\ue03d' - FLAG_ITALY = '\ue03e' - FLAG_SOUTH_KOREA = '\ue03f' - FLAG_NETHERLANDS = '\ue040' - FEDORA = '\ue041' - HAL = '\ue042' - CROWN = '\ue043' - YIN_YANG = '\ue044' - EYE_BALL = '\ue045' - SKULL = '\ue046' - HEART = '\ue047' - DRAGON = '\ue048' - HELMET = '\ue049' - MUSHROOM = '\ue04a' - NINJA_STAR = '\ue04b' - VIKING_HELMET = '\ue04c' - MOON = '\ue04d' - SPIDER = '\ue04e' - FIREBALL = '\ue04f' - FLAG_UNITED_ARAB_EMIRATES = '\ue050' - FLAG_QATAR = '\ue051' - FLAG_EGYPT = '\ue052' - FLAG_KUWAIT = '\ue053' - FLAG_ALGERIA = '\ue054' - FLAG_SAUDI_ARABIA = '\ue055' - FLAG_MALAYSIA = '\ue056' - FLAG_CZECH_REPUBLIC = '\ue057' - FLAG_AUSTRALIA = '\ue058' - FLAG_SINGAPORE = '\ue059' - OCULUS_LOGO = '\ue05a' - STEAM_LOGO = '\ue05b' - NVIDIA_LOGO = '\ue05c' - FLAG_IRAN = '\ue05d' - FLAG_POLAND = '\ue05e' - FLAG_ARGENTINA = '\ue05f' - FLAG_PHILIPPINES = '\ue060' - FLAG_CHILE = '\ue061' - MIKIROG = '\ue062' - V2_LOGO = '\ue063' - SANTA_HAT = '\ue064' - POTATO = '\ue065' - PALM_TREE = '\ue066' - BOXING_GLOVE = '\ue067' diff --git a/dist/ba_data/python/bacommon/transfer.py b/dist/ba_data/python/bacommon/transfer.py index 67f1ac7..aa8d38d 100644 --- a/dist/ba_data/python/bacommon/transfer.py +++ b/dist/ba_data/python/bacommon/transfer.py @@ -1,12 +1,6 @@ # Released under the MIT License. See LICENSE for details. # -"""Functionality related to transferring files/data. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" +"""Functionality related to transferring files/data.""" from __future__ import annotations diff --git a/dist/ba_data/python/bacommon/workspace/assetsv1.py b/dist/ba_data/python/bacommon/workspace/assetsv1.py index fa6f7ba..83a058a 100644 --- a/dist/ba_data/python/bacommon/workspace/assetsv1.py +++ b/dist/ba_data/python/bacommon/workspace/assetsv1.py @@ -2,10 +2,10 @@ # """Public types for assets-v1 workspaces. -While this module is currently only used server-side, its source code -can be useful as reference when setting workspace config data by hand or -for use in client-side workspace modification tools. There may be -advanced settings that are not accessible through the UI/etc. +These types may only be used server-side, but they are exposed here +for reference when setting workspace config data by hand or for use +in client-side workspace modification tools. There may be advanced +settings that are not accessible through the UI/etc. """ from __future__ import annotations @@ -18,6 +18,7 @@ from typing import TYPE_CHECKING, Annotated, override, assert_never from efro.dataclassio import ioprepped, IOAttrs, IOMultiType from bacommon.locale import Locale + if TYPE_CHECKING: pass @@ -84,8 +85,8 @@ class AssetsV1StringFileV1(AssetsV1StringFile): NONE = 'none' TITLE = 'title' - LOUD = 'loud' - SOFT = 'soft' + INTENSE = 'intense' + SUBTLE = 'subtle' @override @classmethod @@ -120,7 +121,7 @@ class AssetsV1PathValsTypeID(Enum): """Types of vals we can store for paths.""" TEX_V1 = 'tex_v1' - STR_V1 = 'str_v1' + # STR_V1 = 'str_v1' class AssetsV1PathVals(IOMultiType[AssetsV1PathValsTypeID]): @@ -150,9 +151,6 @@ class AssetsV1PathVals(IOMultiType[AssetsV1PathValsTypeID]): if type_id is t.TEX_V1: return AssetsV1PathValsTexV1 - if type_id is t.STR_V1: - return AssetsV1PathValsStrV1 - # Important to make sure we provide all types. assert_never(type_id) @@ -178,20 +176,3 @@ class AssetsV1PathValsTexV1(AssetsV1PathVals): @classmethod def get_type_id(cls) -> AssetsV1PathValsTypeID: return AssetsV1PathValsTypeID.TEX_V1 - - -@ioprepped -@dataclass -class AssetsV1PathValsStrV1(AssetsV1PathVals): - """Path-specific values for an assets_v1 workspace path.""" - - #: Hash generated when all translations for this entry are complete. - #: Used as a fast-out for checking whether updates are needed. - up_to_date_state: Annotated[ - str | None, IOAttrs('up_to_date_state', store_default=False) - ] = None - - @override - @classmethod - def get_type_id(cls) -> AssetsV1PathValsTypeID: - return AssetsV1PathValsTypeID.STR_V1 diff --git a/dist/ba_data/python/baenv.py b/dist/ba_data/python/baenv.py index a9995ce..84f0471 100644 --- a/dist/ba_data/python/baenv.py +++ b/dist/ba_data/python/baenv.py @@ -14,7 +14,6 @@ Ballistica can be used without explicitly configuring the environment in order to integrate it in arbitrary Python environments, but this may cause some features to be disabled or behave differently than expected. """ - from __future__ import annotations import os @@ -57,8 +56,8 @@ logger = logging.getLogger('ba.env') # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 22714 -TARGET_BALLISTICA_VERSION = '1.7.61' +TARGET_BALLISTICA_BUILD = 22584 +TARGET_BALLISTICA_VERSION = '1.7.53' @dataclass @@ -96,7 +95,7 @@ class EnvConfig: #: stderr into the engine so they show up on in-app consoles, etc. log_handler: LogHandler | None - #: Initial data from the ``config.json`` file in the config dir. + # Initial data from the ``config.json`` file in the config dir. initial_app_config: Any #: Timestamp when we first started doing stuff. diff --git a/dist/ba_data/python/baplus/_ads.py b/dist/ba_data/python/baplus/_ads.py index 4b77cbb..f6aec55 100644 --- a/dist/ba_data/python/baplus/_ads.py +++ b/dist/ba_data/python/baplus/_ads.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to ads.""" - from __future__ import annotations import time diff --git a/dist/ba_data/python/baplus/_appsubsystem.py b/dist/ba_data/python/baplus/_appsubsystem.py index 29862e2..507d172 100644 --- a/dist/ba_data/python/baplus/_appsubsystem.py +++ b/dist/ba_data/python/baplus/_appsubsystem.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides plus app subsystem.""" - from __future__ import annotations from typing import TYPE_CHECKING, override @@ -14,7 +13,7 @@ from baplus._ads import AdsSubsystem if TYPE_CHECKING: from typing import Callable, Any - import bacommon.classic + import bacommon.bs from babase import AccountV2Subsystem from baplus._cloud import CloudSubsystem @@ -143,6 +142,14 @@ class PlusAppSubsystem(AppSubsystem): """:meta private:""" return _baplus.get_v1_account_state_num() + # @staticmethod + # def get_v1_account_ticket_count() -> int: + # """Return the number of tickets for the current account. + + # :meta private: + # """ + # return _baplus.get_v1_account_ticket_count() + @staticmethod def get_v1_account_type() -> str: """:meta private:""" diff --git a/dist/ba_data/python/baplus/_cloud.py b/dist/ba_data/python/baplus/_cloud.py index bdf2e0a..f1f4097 100644 --- a/dist/ba_data/python/baplus/_cloud.py +++ b/dist/ba_data/python/baplus/_cloud.py @@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, overload from efro.error import CommunicationError from efro.call import CallbackSet from efro.dataclassio import dataclass_from_dict, dataclass_to_dict -import bacommon.classic +import bacommon.bs import bacommon.cloud import babase @@ -19,8 +19,7 @@ if TYPE_CHECKING: from typing import Callable, Any from efro.message import Message, Response, BoolResponse - import bacommon.classic - import bacommon.clouddialog as cdlg + import bacommon.bs # TODO: Should make it possible to define a protocol in bacommon.cloud and @@ -37,8 +36,6 @@ class CloudSubsystem(babase.AppSubsystem): """ #: General engine config values provided by the cloud. - #: - #: :meta private: vals: bacommon.cloud.CloudVals def __init__(self) -> None: @@ -217,9 +214,9 @@ class CloudSubsystem(babase.AppSubsystem): @overload def send_message_cb( self, - msg: bacommon.classic.GetClassicPurchasesMessage, + msg: bacommon.bs.GetClassicPurchasesMessage, on_response: Callable[ - [bacommon.classic.GetClassicPurchasesResponse | Exception], None + [bacommon.bs.GetClassicPurchasesResponse | Exception], None ], ) -> None: ... @@ -235,59 +232,61 @@ class CloudSubsystem(babase.AppSubsystem): @overload def send_message_cb( self, - msg: bacommon.classic.PrivatePartyMessage, + msg: bacommon.bs.PrivatePartyMessage, on_response: Callable[ - [bacommon.classic.PrivatePartyResponse | Exception], None + [bacommon.bs.PrivatePartyResponse | Exception], None ], ) -> None: ... @overload def send_message_cb( self, - msg: bacommon.classic.InboxRequestMessage, + msg: bacommon.bs.InboxRequestMessage, on_response: Callable[ - [bacommon.classic.InboxRequestResponse | Exception], None + [bacommon.bs.InboxRequestResponse | Exception], None ], ) -> None: ... @overload def send_message_cb( self, - msg: cdlg.ActionMessage, - on_response: Callable[[cdlg.ActionResponse | Exception], None], - ) -> None: ... - - @overload - def send_message_cb( - self, - msg: bacommon.classic.ChestInfoMessage, + msg: bacommon.bs.CloudDialogActionMessage, on_response: Callable[ - [bacommon.classic.ChestInfoResponse | Exception], None + [bacommon.bs.CloudDialogActionResponse | Exception], None ], ) -> None: ... @overload def send_message_cb( self, - msg: bacommon.cloud.ChestActionMessage, + msg: bacommon.bs.ChestInfoMessage, on_response: Callable[ - [bacommon.cloud.ChestActionResponse | Exception], None + [bacommon.bs.ChestInfoResponse | Exception], None ], ) -> None: ... @overload def send_message_cb( self, - msg: bacommon.classic.GlobalProfileCheckMessage, + msg: bacommon.bs.ChestActionMessage, + on_response: Callable[ + [bacommon.bs.ChestActionResponse | Exception], None + ], + ) -> None: ... + + @overload + def send_message_cb( + self, + msg: bacommon.bs.GlobalProfileCheckMessage, on_response: Callable[[BoolResponse | Exception], None], ) -> None: ... @overload def send_message_cb( self, - msg: bacommon.classic.ScoreSubmitMessage, + msg: bacommon.bs.ScoreSubmitMessage, on_response: Callable[ - [bacommon.classic.ScoreSubmitResponse | Exception], None + [bacommon.bs.ScoreSubmitResponse | Exception], None ], ) -> None: ... @@ -309,39 +308,6 @@ class CloudSubsystem(babase.AppSubsystem): ], ) -> None: ... - @overload - def send_message_cb( - self, - msg: bacommon.classic.GetClassicLeaguePresidentButtonInfoMessage, - on_response: Callable[ - [ - bacommon.classic.GetClassicLeaguePresidentButtonInfoResponse - | Exception - ], - None, - ], - ) -> None: ... - - @overload - def send_message_cb( - self, - msg: bacommon.cloud.AnalyticsEventMessage, - on_response: Callable[ - [None | Exception], - None, - ], - ) -> None: ... - - @overload - def send_message_cb( - self, - msg: bacommon.cloud.AuthRequestMessage, - on_response: Callable[ - [bacommon.cloud.AuthRequestResponse | Exception], - None, - ], - ) -> None: ... - def send_message_cb( self, msg: Message, @@ -373,13 +339,8 @@ class CloudSubsystem(babase.AppSubsystem): @overload def send_message( - self, msg: bacommon.classic.LegacyRequest - ) -> bacommon.classic.LegacyResponse: ... - - @overload - def send_message( - self, msg: bacommon.cloud.FulfillDocUIRequest - ) -> bacommon.cloud.FulfillDocUIResponse: ... + self, msg: bacommon.bs.LegacyRequest + ) -> bacommon.bs.LegacyResponse: ... def send_message(self, msg: Message) -> Response | None: """Synchronously send a message to the cloud. @@ -392,8 +353,8 @@ class CloudSubsystem(babase.AppSubsystem): @overload async def send_message_async( - self, msg: bacommon.classic.SendInfoMessage - ) -> bacommon.classic.SendInfoResponse: ... + self, msg: bacommon.bs.SendInfoMessage + ) -> bacommon.bs.SendInfoResponse: ... @overload async def send_message_async( @@ -422,14 +383,9 @@ class CloudSubsystem(babase.AppSubsystem): def subscribe_classic_account_data( self, - updatecall: Callable[ - [bacommon.classic.ClassicLiveAccountClientData], None - ], + updatecall: Callable[[bacommon.bs.ClassicAccountLiveData], None], ) -> babase.CloudSubscription: - """Subscribe to classic account data. - - :meta private: - """ + """Subscribe to classic account data.""" raise NotImplementedError( 'Cloud functionality is not present in this build.' ) diff --git a/dist/ba_data/python/baplus/_hooks.py b/dist/ba_data/python/baplus/_hooks.py index 95b43fe..0348914 100644 --- a/dist/ba_data/python/baplus/_hooks.py +++ b/dist/ba_data/python/baplus/_hooks.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Snippets of code for use by the c++ layer.""" - # (most of these are self-explanatory) # pylint: disable=missing-function-docstring from __future__ import annotations diff --git a/dist/ba_data/python/bascenev1/__init__.py b/dist/ba_data/python/bascenev1/__init__.py index aead594..f739b07 100644 --- a/dist/ba_data/python/bascenev1/__init__.py +++ b/dist/ba_data/python/bascenev1/__init__.py @@ -16,6 +16,7 @@ import logging # other modules; the goal is to let most simple mods rely solely on this # module to keep things simple. +# from efro.util import set_canonical_module_names from babase import ( ActivityNotFoundError, add_clean_frame_callback, @@ -31,8 +32,6 @@ from babase import ( apptimer, AppTimer, Call, - CallPartial, - CallStrict, ContextError, ContextRef, displaytime, @@ -64,8 +63,6 @@ from babase import ( unlock_all_input, Vec3, WeakCall, - WeakCallPartial, - WeakCallStrict, ) from _bascenev1 import ( @@ -278,8 +275,6 @@ __all__ = [ 'BaseTimer', 'BoolSetting', 'Call', - 'CallPartial', - 'CallStrict', 'cameraflash', 'camerashake', 'Campaign', @@ -480,11 +475,15 @@ __all__ = [ 'unlock_all_input', 'Vec3', 'WeakCall', - 'WeakCallPartial', - 'WeakCallStrict', 'WinnerGroup', ] +# We want stuff here to show up as bascenev1.Foo instead of +# bascenev1._submodule.Foo. +# UPDATE: Trying without this for now. Seems like this might cause more +# harm than good. Can flip it back on if it is missed. +# set_canonical_module_names(globals()) + # Sanity check: we want to keep ballistica's dependencies and # bootstrapping order clearly defined; let's check a few particular # modules to make sure they never directly or indirectly import us diff --git a/dist/ba_data/python/bascenev1/_activity.py b/dist/ba_data/python/bascenev1/_activity.py index 57aaa12..c78f1a6 100644 --- a/dist/ba_data/python/bascenev1/_activity.py +++ b/dist/ba_data/python/bascenev1/_activity.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Defines Activity class.""" - from __future__ import annotations import weakref @@ -13,8 +12,9 @@ import _bascenev1 from bascenev1._dependency import DependencyComponent from bascenev1._messages import UNHANDLED + if TYPE_CHECKING: - from typing import Any, Self + from typing import Any import bascenev1 @@ -192,7 +192,7 @@ class Activity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( session = self._session() if session is not None: babase.pushcall( - babase.CallStrict( + babase.Call( session.transitioning_out_activity_was_freed, self.can_show_ad_on_death, ) @@ -286,7 +286,7 @@ class Activity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( ref = weakref.ref(self) self._activity_death_check_timer = babase.AppTimer( 5.0, - babase.CallStrict(self._check_activity_death, ref, [0]), + babase.Call(self._check_activity_death, ref, [0]), repeat=True, ) @@ -722,7 +722,7 @@ class Activity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( @classmethod def _check_activity_death( - cls, activity_ref: weakref.ref[Self], counter: list[int] + cls, activity_ref: weakref.ref[Activity], counter: list[int] ) -> None: """Sanity check to make sure an Activity was destroyed properly. diff --git a/dist/ba_data/python/bascenev1/_activitytypes.py b/dist/ba_data/python/bascenev1/_activitytypes.py index fe05362..c80941a 100644 --- a/dist/ba_data/python/bascenev1/_activitytypes.py +++ b/dist/ba_data/python/bascenev1/_activitytypes.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Some handy base class and special purpose Activity types.""" - from __future__ import annotations from typing import TYPE_CHECKING, override @@ -15,6 +14,7 @@ from bascenev1._player import EmptyPlayer from bascenev1._team import EmptyTeam from bascenev1._music import MusicType, setmusic + if TYPE_CHECKING: import bascenev1 from bascenev1._lobby import JoinInfo @@ -54,7 +54,7 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]): babase.unlock_all_input() assert babase.app.plus is not None - call = babase.CallStrict(_bascenev1.new_host_session, main_menu_session) + call = babase.Call(_bascenev1.new_host_session, main_menu_session) if classic.can_show_interstitial(): plus.ads.call_after_ad(call) else: @@ -172,7 +172,7 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]): # If we're still kicking at the end of our assign-delay, assign this # guy's input to trigger us. _bascenev1.timer( - time_till_assign, babase.WeakCallStrict(self._safe_assign, player) + time_till_assign, babase.WeakCall(self._safe_assign, player) ) @override diff --git a/dist/ba_data/python/bascenev1/_campaign.py b/dist/ba_data/python/bascenev1/_campaign.py index 10d0de0..f45f8bc 100644 --- a/dist/ba_data/python/bascenev1/_campaign.py +++ b/dist/ba_data/python/bascenev1/_campaign.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to co-op campaigns.""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/bascenev1/_collision.py b/dist/ba_data/python/bascenev1/_collision.py index 5dc418f..11877f6 100644 --- a/dist/ba_data/python/bascenev1/_collision.py +++ b/dist/ba_data/python/bascenev1/_collision.py @@ -19,9 +19,7 @@ class Collision: @property def position(self) -> bascenev1.Vec3: """The position of the current collision.""" - out = babase.Vec3(_bascenev1.get_collision_info('position')) - assert isinstance(out, babase.Vec3) - return out + return babase.Vec3(_bascenev1.get_collision_info('position')) @property def sourcenode(self) -> bascenev1.Node: @@ -32,7 +30,7 @@ class Collision: start of the collision callback). """ node = _bascenev1.get_collision_info('sourcenode') - assert isinstance(node, _bascenev1.Node | None) + assert isinstance(node, (_bascenev1.Node, type(None))) if not node: raise babase.NodeNotFoundError() return node @@ -47,7 +45,7 @@ class Collision: currently-colliding node. """ node = _bascenev1.get_collision_info('opposingnode') - assert isinstance(node, _bascenev1.Node | None) + assert isinstance(node, (_bascenev1.Node, type(None))) if not node: raise babase.NodeNotFoundError() return node diff --git a/dist/ba_data/python/bascenev1/_coopgame.py b/dist/ba_data/python/bascenev1/_coopgame.py index 7861a29..56cfb12 100644 --- a/dist/ba_data/python/bascenev1/_coopgame.py +++ b/dist/ba_data/python/bascenev1/_coopgame.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to co-op games.""" - from __future__ import annotations import logging @@ -65,11 +64,11 @@ class CoopGameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( if not arcade_or_demo: _bascenev1.timer( - 3.8, babase.WeakCallStrict(self._show_remaining_achievements) + 3.8, babase.WeakCall(self._show_remaining_achievements) ) # Preload achievement images in case we get some. - _bascenev1.timer(2.0, babase.WeakCallStrict(self._preload_achievements)) + _bascenev1.timer(2.0, babase.WeakCall(self._preload_achievements)) # FIXME: this is now redundant with activityutils.getscoreconfig(); # need to kill this. @@ -233,7 +232,7 @@ class CoopGameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( """Set up a beeping noise to play when any players are near death.""" self._life_warning_beep = None self._life_warning_beep_timer = _bascenev1.Timer( - 1.0, babase.WeakCallStrict(self._update_life_warning), repeat=True + 1.0, babase.WeakCall(self._update_life_warning), repeat=True ) def _update_life_warning(self) -> None: diff --git a/dist/ba_data/python/bascenev1/_coopsession.py b/dist/ba_data/python/bascenev1/_coopsession.py index d066115..c8b176b 100644 --- a/dist/ba_data/python/bascenev1/_coopsession.py +++ b/dist/ba_data/python/bascenev1/_coopsession.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to coop-mode sessions.""" - from __future__ import annotations from typing import TYPE_CHECKING, override @@ -186,9 +185,7 @@ class CoopSession(Session): def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None: super().on_player_leave(sessionplayer) - _bascenev1.timer( - 2.0, babase.WeakCallStrict(self._handle_empty_activity) - ) + _bascenev1.timer(2.0, babase.WeakCall(self._handle_empty_activity)) def _handle_empty_activity(self) -> None: """Handle cases where all players have left the current activity.""" @@ -361,7 +358,7 @@ class CoopSession(Session): { 'label': babase.Lstr(resource='restartText'), 'resume_on_call': False, - 'call': babase.WeakCallPartial( + 'call': babase.WeakCall( self._on_tournament_restart_menu_press ), } @@ -370,7 +367,7 @@ class CoopSession(Session): self._custom_menu_ui = [ { 'label': babase.Lstr(resource='restartText'), - 'call': babase.WeakCallStrict(self.restart), + 'call': babase.WeakCall(self.restart), } ] diff --git a/dist/ba_data/python/bascenev1/_debug.py b/dist/ba_data/python/bascenev1/_debug.py index 9348635..768b42b 100644 --- a/dist/ba_data/python/bascenev1/_debug.py +++ b/dist/ba_data/python/bascenev1/_debug.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Debugging functionality.""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/bascenev1/_dualteamsession.py b/dist/ba_data/python/bascenev1/_dualteamsession.py index 8b013b4..7ea0a13 100644 --- a/dist/ba_data/python/bascenev1/_dualteamsession.py +++ b/dist/ba_data/python/bascenev1/_dualteamsession.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to teams sessions.""" - from __future__ import annotations from typing import TYPE_CHECKING, override diff --git a/dist/ba_data/python/bascenev1/_gameactivity.py b/dist/ba_data/python/bascenev1/_gameactivity.py index f5cd386..698fec2 100644 --- a/dist/ba_data/python/bascenev1/_gameactivity.py +++ b/dist/ba_data/python/bascenev1/_gameactivity.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides GameActivity class.""" - # pylint: disable=too-many-lines from __future__ import annotations @@ -406,9 +405,7 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( 'tournamentIDs': [tournament_id], 'source': 'in-game time remaining query', }, - callback=babase.WeakCallPartial( - self._on_tournament_query_response - ), + callback=babase.WeakCall(self._on_tournament_query_response), ) def _on_tournament_query_response( @@ -808,7 +805,7 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( player.customdata['respawn_timer'] = _bascenev1.Timer( respawn_time, - babase.WeakCallStrict(self.spawn_player_if_exists, player), + babase.WeakCall(self.spawn_player_if_exists, player), ) player.customdata['respawn_icon'] = RespawnIcon( player, respawn_time @@ -905,7 +902,7 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( self._powerup_drop_timer = _bascenev1.Timer( DEFAULT_POWERUP_INTERVAL, - babase.WeakCallStrict(self._standard_drop_powerups), + babase.WeakCall(self._standard_drop_powerups), repeat=True, ) self._standard_drop_powerups() @@ -930,7 +927,7 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( points = self.map.powerup_spawn_points for i in range(len(points)): _bascenev1.timer( - i * 0.4, babase.WeakCallStrict(self._standard_drop_powerup, i) + i * 0.4, babase.WeakCall(self._standard_drop_powerup, i) ) def _setup_standard_tnt_drops(self) -> None: @@ -956,9 +953,7 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( return self._standard_time_limit_time = int(duration) self._standard_time_limit_timer = _bascenev1.Timer( - 1.0, - babase.WeakCallStrict(self._standard_time_limit_tick), - repeat=True, + 1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True ) self._standard_time_limit_text = NodeActor( _bascenev1.newnode( @@ -1048,9 +1043,7 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team]( # then we have to mess with contexts and whatnot since its currently # not available in activity contexts. :-/ self._tournament_time_limit_timer = _bascenev1.BaseTimer( - 1.0, - babase.WeakCallStrict(self._tournament_time_limit_tick), - repeat=True, + 1.0, babase.WeakCall(self._tournament_time_limit_tick), repeat=True ) self._tournament_time_limit_title_text = NodeActor( _bascenev1.newnode( diff --git a/dist/ba_data/python/bascenev1/_gameresults.py b/dist/ba_data/python/bascenev1/_gameresults.py index 61e5333..795070c 100644 --- a/dist/ba_data/python/bascenev1/_gameresults.py +++ b/dist/ba_data/python/bascenev1/_gameresults.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to game results.""" - from __future__ import annotations import copy diff --git a/dist/ba_data/python/bascenev1/_hooks.py b/dist/ba_data/python/bascenev1/_hooks.py index cde61d8..b88bfb6 100644 --- a/dist/ba_data/python/bascenev1/_hooks.py +++ b/dist/ba_data/python/bascenev1/_hooks.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Snippets of code for use by the c++ layer.""" - # (most of these are self-explanatory) # pylint: disable=missing-function-docstring from __future__ import annotations diff --git a/dist/ba_data/python/bascenev1/_level.py b/dist/ba_data/python/bascenev1/_level.py index c26a0a1..0f777dc 100644 --- a/dist/ba_data/python/bascenev1/_level.py +++ b/dist/ba_data/python/bascenev1/_level.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to individual levels in a campaign.""" - from __future__ import annotations import copy @@ -119,11 +118,10 @@ class Level: def get_high_scores(self) -> dict: """Return the current high scores for this level.""" config = self._get_config_dict() - high_scores_key = f'High Scores{self.get_score_version_string()}' - val = config.get(high_scores_key) - if isinstance(val, dict): - return copy.deepcopy(val) - return {} + high_scores_key = 'High Scores' + self.get_score_version_string() + if high_scores_key not in config: + return {} + return copy.deepcopy(config[high_scores_key]) def set_high_scores(self, high_scores: dict) -> None: """Set high scores for this level.""" diff --git a/dist/ba_data/python/bascenev1/_lobby.py b/dist/ba_data/python/bascenev1/_lobby.py index 69814a5..99f36c0 100644 --- a/dist/ba_data/python/bascenev1/_lobby.py +++ b/dist/ba_data/python/bascenev1/_lobby.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Implements lobby system for gathering before games, char select, etc.""" - # pylint: disable=too-many-lines from __future__ import annotations @@ -119,7 +118,7 @@ class JoinInfo: ) self._timer = _bascenev1.Timer( - 4.0, babase.WeakCallStrict(self._update), repeat=True + 4.0, babase.WeakCall(self._update), repeat=True ) def _update_for_keyboard(self, keyboard: bascenev1.InputDevice) -> None: @@ -339,10 +338,10 @@ class Chooser: if inputdevice.is_controller_app and '_random' in profilenames: return profilenames.index('_random') - # If its a client connection, for now just force the account - # profile if possible. (need to provide a way for clients to - # specify/remember their default profile on remote servers that - # do not already know them). + # If its a client connection, for now just force + # the account profile if possible.. (need to provide a + # way for clients to specify/remember their default + # profile on remote servers that do not already know them). if inputdevice.is_remote_client and '__account__' in profilenames: return profilenames.index('__account__') @@ -610,29 +609,25 @@ class Chooser: if not ready: self._sessionplayer.assigninput( babase.InputType.LEFT_PRESS, - babase.CallStrict( - self.handlemessage, ChangeMessage('team', -1) - ), + babase.Call(self.handlemessage, ChangeMessage('team', -1)), ) self._sessionplayer.assigninput( babase.InputType.RIGHT_PRESS, - babase.CallStrict(self.handlemessage, ChangeMessage('team', 1)), + babase.Call(self.handlemessage, ChangeMessage('team', 1)), ) self._sessionplayer.assigninput( babase.InputType.BOMB_PRESS, - babase.CallStrict( - self.handlemessage, ChangeMessage('character', 1) - ), + babase.Call(self.handlemessage, ChangeMessage('character', 1)), ) self._sessionplayer.assigninput( babase.InputType.UP_PRESS, - babase.CallStrict( + babase.Call( self.handlemessage, ChangeMessage('profileindex', -1) ), ) self._sessionplayer.assigninput( babase.InputType.DOWN_PRESS, - babase.CallStrict( + babase.Call( self.handlemessage, ChangeMessage('profileindex', 1) ), ) @@ -642,9 +637,7 @@ class Chooser: babase.InputType.PICK_UP_PRESS, babase.InputType.PUNCH_PRESS, ), - babase.CallStrict( - self.handlemessage, ChangeMessage('ready', 1) - ), + babase.Call(self.handlemessage, ChangeMessage('ready', 1)), ) self._ready = False self._update_text() @@ -669,9 +662,7 @@ class Chooser: babase.InputType.PICK_UP_PRESS, babase.InputType.PUNCH_PRESS, ), - babase.CallStrict( - self.handlemessage, ChangeMessage('ready', 0) - ), + babase.Call(self.handlemessage, ChangeMessage('ready', 0)), ) # Store the last profile picked by this input for reuse. diff --git a/dist/ba_data/python/bascenev1/_map.py b/dist/ba_data/python/bascenev1/_map.py index b6ae04f..5718289 100644 --- a/dist/ba_data/python/bascenev1/_map.py +++ b/dist/ba_data/python/bascenev1/_map.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Map related functionality.""" - from __future__ import annotations import random diff --git a/dist/ba_data/python/bascenev1/_messages.py b/dist/ba_data/python/bascenev1/_messages.py index 9518241..2c8166c 100644 --- a/dist/ba_data/python/bascenev1/_messages.py +++ b/dist/ba_data/python/bascenev1/_messages.py @@ -99,7 +99,7 @@ class PlayerDiedMessage: Pass the Player type being used by the current game. """ - assert isinstance(self._killerplayer, playertype | None) + assert isinstance(self._killerplayer, (playertype, type(None))) return self._killerplayer def getplayer[PlayerT: bascenev1.Player]( diff --git a/dist/ba_data/python/bascenev1/_multiteamsession.py b/dist/ba_data/python/bascenev1/_multiteamsession.py index 3faa06c..85b6799 100644 --- a/dist/ba_data/python/bascenev1/_multiteamsession.py +++ b/dist/ba_data/python/bascenev1/_multiteamsession.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to teams sessions.""" - from __future__ import annotations import copy diff --git a/dist/ba_data/python/bascenev1/_net.py b/dist/ba_data/python/bascenev1/_net.py index 2a649c8..279c329 100644 --- a/dist/ba_data/python/bascenev1/_net.py +++ b/dist/ba_data/python/bascenev1/_net.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to net play.""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/bascenev1/_player.py b/dist/ba_data/python/bascenev1/_player.py index 4c937f7..53a056b 100644 --- a/dist/ba_data/python/bascenev1/_player.py +++ b/dist/ba_data/python/bascenev1/_player.py @@ -35,8 +35,7 @@ class StandLocation: angle: float | None = None -# class Player[TeamT: bascenev1.Team]: -class Player[TeamT]: +class Player[TeamT: bascenev1.Team]: """A player in a specific bascenev1.Activity. These correspond to bascenev1.SessionPlayer objects, but are associated @@ -316,5 +315,5 @@ def playercast_o[PlayerT: bascenev1.Player]( totype: type[PlayerT], player: bascenev1.Player | None ) -> PlayerT | None: """A variant of bascenev1.playercast() for optional Player values.""" - assert isinstance(player, totype | None) + assert isinstance(player, (totype, type(None))) return player diff --git a/dist/ba_data/python/bascenev1/_profile.py b/dist/ba_data/python/bascenev1/_profile.py index 6883cf1..a097456 100644 --- a/dist/ba_data/python/bascenev1/_profile.py +++ b/dist/ba_data/python/bascenev1/_profile.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to player profiles.""" - from __future__ import annotations import random diff --git a/dist/ba_data/python/bascenev1/_session.py b/dist/ba_data/python/bascenev1/_session.py index b021839..ae1594e 100644 --- a/dist/ba_data/python/bascenev1/_session.py +++ b/dist/ba_data/python/bascenev1/_session.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Defines base session class.""" - from __future__ import annotations import math @@ -25,7 +24,7 @@ if TYPE_CHECKING: _g_player_rejoin_cooldown: float = 0.0 # overrides the session's decision of max_players -_g_max_players_override: int | None = None +_max_players_override: int | None = None def set_player_rejoin_cooldown(cooldown: float) -> None: @@ -36,8 +35,8 @@ def set_player_rejoin_cooldown(cooldown: float) -> None: def set_max_players_override(max_players: int | None) -> None: """Set the override for how many players can join a session""" - global _g_max_players_override # pylint: disable=global-statement - _g_max_players_override = max_players + global _max_players_override # pylint: disable=global-statement + _max_players_override = max_players class Session: @@ -176,8 +175,8 @@ class Session: self.min_players = min_players self.max_players = ( max_players - if _g_max_players_override is None - else _g_max_players_override + if _max_players_override is None + else _max_players_override ) self.submit_score = submit_score @@ -339,9 +338,7 @@ class Session: with babase.ContextRef.empty(): self._waitlist_timers[identifier] = babase.AppTimer( _g_player_rejoin_cooldown, - babase.CallStrict( - self._remove_player_from_waitlist, identifier - ), + babase.Call(self._remove_player_from_waitlist, identifier), ) if not sessionplayer.in_game: @@ -375,7 +372,7 @@ class Session: # Grab their activity-specific player instance. player = sessionplayer.activityplayer - assert isinstance(player, Player | None) + assert isinstance(player, (Player, type(None))) # Remove them from any current Activity. if player is not None and activity is not None: @@ -501,9 +498,7 @@ class Session: # Set a timer to set in motion this activity's demise. self._activity_end_timer = _bascenev1.BaseTimer( delay, - babase.CallStrict( - self._complete_end_activity, activity, results - ), + babase.Call(self._complete_end_activity, activity, results), ) def handlemessage(self, msg: Any) -> Any: diff --git a/dist/ba_data/python/bascenev1/_stats.py b/dist/ba_data/python/bascenev1/_stats.py index 698a00c..d5a2877 100644 --- a/dist/ba_data/python/bascenev1/_stats.py +++ b/dist/ba_data/python/bascenev1/_stats.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to scores and statistics.""" - from __future__ import annotations import random @@ -14,6 +13,7 @@ import babase import _bascenev1 + if TYPE_CHECKING: from typing import Any, Sequence @@ -237,7 +237,7 @@ class PlayerRecord: if name is not None: _bascenev1.timer( 0.3 + delay, - babase.CallStrict( + babase.Call( _apply, name, score, showpoints, color, scale, sound ), ) diff --git a/dist/ba_data/python/bascenev1/_team.py b/dist/ba_data/python/bascenev1/_team.py index fc6001e..d9b99ad 100644 --- a/dist/ba_data/python/bascenev1/_team.py +++ b/dist/ba_data/python/bascenev1/_team.py @@ -65,8 +65,7 @@ class SessionTeam: self.customdata = {} -# class Team[PlayerT: bascenev1.Player]: -class Team[PlayerT]: +class Team[PlayerT: bascenev1.Player]: """A team in a specific :class:`~bascenev1.Activity`. These correspond to :class:`~bascenev1.SessionTeam` objects, but are diff --git a/dist/ba_data/python/bascenev1lib/activity/coopjoin.py b/dist/ba_data/python/bascenev1lib/activity/coopjoin.py index 0b8a28d..f8f7506 100644 --- a/dist/ba_data/python/bascenev1lib/activity/coopjoin.py +++ b/dist/ba_data/python/bascenev1lib/activity/coopjoin.py @@ -63,10 +63,10 @@ class CoopJoinActivity(bs.JoinActivity): vpos = -140.0 # Now list our remaining achievements for this level. - assert isinstance(self.session, bs.CoopSession) assert self.session.campaign is not None + assert isinstance(self.session, bs.CoopSession) levelname = ( - f'{self.session.campaign.name}:{self.session.campaign_level_name}' + self.session.campaign.name + ':' + self.session.campaign_level_name ) ts_h_offs = 60 diff --git a/dist/ba_data/python/bascenev1lib/activity/coopscore.py b/dist/ba_data/python/bascenev1lib/activity/coopscore.py index 85a5f93..bb832dd 100644 --- a/dist/ba_data/python/bascenev1lib/activity/coopscore.py +++ b/dist/ba_data/python/bascenev1lib/activity/coopscore.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides a score screen for coop games.""" - # pylint: disable=too-many-lines from __future__ import annotations @@ -11,7 +10,7 @@ import logging from typing import TYPE_CHECKING, override from efro.util import strict_partial -import bacommon.classic +import bacommon.bs from bacommon.login import LoginType import bascenev1 as bs import bauiv1 as bui @@ -134,10 +133,10 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): assert all(isinstance(i, bs.PlayerInfo) for i in self._playerinfos) self._score: int | None = settings['score'] - assert isinstance(self._score, int | None) + assert isinstance(self._score, (int, type(None))) self._fail_message: bs.Lstr | None = settings['fail_message'] - assert isinstance(self._fail_message, bs.Lstr | None) + assert isinstance(self._fail_message, (bs.Lstr, type(None))) self._begin_time: float | None = None @@ -206,7 +205,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): def _ui_menu(self) -> None: bui.containerwidget(edit=self._root_ui, transition='out_left') with self.context: - bs.timer(0.1, bs.CallStrict(bs.WeakCallStrict(self.session.end))) + bs.timer(0.1, bs.Call(bs.WeakCall(self.session.end))) def _ui_restart(self) -> None: from bauiv1lib.tournamententry import TournamentEntryWindow @@ -310,7 +309,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): bui.getsound('error').play() bs.timer( 2.0, - bs.WeakCallStrict( + bs.WeakCall( self._next_level_error.handlemessage, bs.DieMessage() ), ) @@ -333,7 +332,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): # main menu up, so instead we add a callback for when the menu # closes; if we're still alive, we'll come up then. # If there's no main menu this gets called immediately. - classic.add_main_menu_close_callback(bui.WeakCallStrict(self.show_ui)) + classic.add_main_menu_close_callback(bui.WeakCall(self.show_ui)) def show_ui(self) -> None: """Show the UI for restarting, playing the next Level, etc.""" @@ -346,7 +345,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): delay = 0.7 if (self._score is not None) else 0.0 # If there's no players left in the game, lets not show the UI - # (it would allow restarting the game with zero players, etc). + # (that would allow restarting the game with zero players, etc). if not self.players: return @@ -374,7 +373,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): position=(h_offs - 520, v_offs + 450 - 235 + 40), size=(300, 60), label=bui.Lstr(resource='achievementsText'), - on_activate_call=bui.WeakCallStrict(self._ui_show_achievements), + on_activate_call=bui.WeakCall(self._ui_show_achievements), transition_delay=delay + 1.5, icon=self._game_service_achievements_texture, icon_color=self._game_service_icon_color, @@ -398,7 +397,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): ) ), autoselect=True, - on_activate_call=bui.WeakCallStrict(self._ui_worlds_best), + on_activate_call=bui.WeakCall(self._ui_worlds_best), transition_delay=delay + 1.9, selectable=can_select_extra_buttons, ) @@ -428,7 +427,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): size=(100, 50), label='', button_type='square', - on_activate_call=bui.WeakCallStrict(self._ui_menu), + on_activate_call=bui.WeakCall(self._ui_menu), ) bui.imagewidget( parent=rootc, @@ -445,7 +444,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): position=(h_offs - 130 - 60, v_offs), size=(110, 85), label='', - on_activate_call=bui.WeakCallStrict(self._ui_menu), + on_activate_call=bui.WeakCall(self._ui_menu), ) bui.imagewidget( parent=rootc, @@ -464,7 +463,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): size=(100, 50), label='', button_type='square', - on_activate_call=bui.WeakCallStrict(self._ui_restart), + on_activate_call=bui.WeakCall(self._ui_restart), ) bui.imagewidget( parent=rootc, @@ -481,7 +480,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): position=(h_offs - 60, v_offs), size=(110, 85), label='', - on_activate_call=bui.WeakCallStrict(self._ui_restart), + on_activate_call=bui.WeakCall(self._ui_restart), ) bui.imagewidget( parent=rootc, @@ -498,12 +497,12 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): # level yet and invisible if there is none. if show_next_button: if self._is_complete: - call = bui.WeakCallStrict(self._ui_next) + call = bui.WeakCall(self._ui_next) button_sound = True image_opacity = 0.8 color = None else: - call = bui.WeakCallStrict(self._ui_error) + call = bui.WeakCall(self._ui_error) button_sound = False image_opacity = 0.2 color = (0.3, 0.3, 0.3) @@ -616,9 +615,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): 0, self._birth_time + self._min_view_time - bs.time() ) - bs.timer( - time_till_assign, bs.WeakCallStrict(self._safe_assign, player) - ) + bs.timer(time_till_assign, bs.WeakCall(self._safe_assign, player)) @override def on_begin(self) -> None: @@ -664,7 +661,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): cfg.commit() self._campaign.set_selected_level(self._next_level_name) - bs.timer(1.0, bs.WeakCallStrict(self.request_ui)) + bs.timer(1.0, bs.WeakCall(self.request_ui)) variant = bs.app.env.variant vart = type(variant) @@ -796,9 +793,9 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): self._show_info = {} if self._score is not None: - bs.timer(0.8, bs.WeakCallStrict(self._show_score_val, offs_x)) + bs.timer(0.8, bs.WeakCall(self._show_score_val, offs_x)) else: - bs.pushcall(bs.WeakCallStrict(self._show_fail)) + bs.pushcall(bs.WeakCall(self._show_fail)) self._name_str = name_str = ', '.join( [p.name for p in self._playerinfos] @@ -817,7 +814,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): ) if self._score is not None and self._submit_score: - bs.timer(0.4, bs.WeakCallStrict(self._play_drumroll)) + bs.timer(0.4, bs.WeakCall(self._play_drumroll)) # Add us to high scores, filter, and store. our_high_scores_all = self._campaign.getlevel( @@ -869,7 +866,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): # We expect this only in kiosk mode; complain otherwise. if not arcade_or_demo: logging.error('got not-signed-in at score-submit; unexpected') - bs.pushcall(bs.WeakCallStrict(self._got_score_results, None)) + bs.pushcall(bs.WeakCall(self._got_score_results, None)) else: assert self._game_name_str is not None assert self._game_config_str is not None @@ -878,7 +875,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): self._game_config_str, name_str, self._score, - bs.WeakCallPartial(self._got_score_results), + bs.WeakCall(self._got_score_results), order=self._score_order, tournament_id=self.session.tournament_id, score_type=self._score_type, @@ -1062,7 +1059,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): vval -= 55 tdelay += 0.250 - bs.timer(5.0, bs.WeakCallStrict(self._show_tips)) + bs.timer(5.0, bs.WeakCall(self._show_tips)) def _play_drumroll(self) -> None: bs.NodeActor( @@ -1212,14 +1209,14 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): ).autoretain() def _on_v2_score_results( - self, response: bacommon.classic.ScoreSubmitResponse | Exception + self, response: bacommon.bs.ScoreSubmitResponse | Exception ) -> None: if isinstance(response, Exception): logging.debug('Got error score-submit response: %s', response) return - assert isinstance(response, bacommon.classic.ScoreSubmitResponse) + assert isinstance(response, bacommon.bs.ScoreSubmitResponse) # Aim to have these effects run shortly after the final rating # hit happens. @@ -1278,10 +1275,8 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): ): with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.classic.ScoreSubmitMessage(score_token), - on_response=bui.WeakCallPartial( - self._on_v2_score_results - ), + bacommon.bs.ScoreSubmitMessage(score_token), + on_response=bui.WeakCall(self._on_v2_score_results), ) self._score_link = results['link'] @@ -1302,7 +1297,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): self._tournament_time_remaining = secs_remaining self._tournament_time_remaining_text_timer = bs.BaseTimer( 1.0, - bs.WeakCallStrict( + bs.WeakCall( self._update_tournament_time_remaining_text ), repeat=True, @@ -1320,7 +1315,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): if self._score is not None: bs.basetimer( (1.5 + base_delay), - bs.WeakCallStrict(self._show_world_rank, offs_x), + bs.WeakCall(self._show_world_rank, offs_x), ) ts_h_offs = 280 ts_height = 300 @@ -1579,7 +1574,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): ] # pylint: disable=useless-suppression # pylint: disable=unbalanced-tuple-unpacking - pr1, pv1, pr2, pv2, pr3, pv3 = ( + (pr1, pv1, pr2, pv2, pr3, pv3) = ( bs.app.classic.get_tournament_prize_strings( tourney_info, include_tickets=False ) diff --git a/dist/ba_data/python/bascenev1lib/activity/dualteamscore.py b/dist/ba_data/python/bascenev1lib/activity/dualteamscore.py index ef5d70a..6929a10 100644 --- a/dist/ba_data/python/bascenev1lib/activity/dualteamscore.py +++ b/dist/ba_data/python/bascenev1lib/activity/dualteamscore.py @@ -61,7 +61,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): for team in self.session.sessionteams: bs.timer( i * 0.15 + 0.15, - bs.WeakCallStrict( + bs.WeakCall( self._show_team_name, vval - i * height, team, @@ -76,7 +76,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): delay = 1.2 bs.timer( i * 0.150 + 0.2, - bs.WeakCallStrict( + bs.WeakCall( self._show_team_old_score, vval - i * height, team, @@ -87,7 +87,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): bs.timer( i * 0.150 + delay, - bs.WeakCallStrict( + bs.WeakCall( self._show_team_score, vval - i * height, team, diff --git a/dist/ba_data/python/bascenev1lib/activity/freeforallvictory.py b/dist/ba_data/python/bascenev1lib/activity/freeforallvictory.py index e05a643..3f88e3b 100644 --- a/dist/ba_data/python/bascenev1lib/activity/freeforallvictory.py +++ b/dist/ba_data/python/bascenev1lib/activity/freeforallvictory.py @@ -132,7 +132,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): bs.timer( tdelay + delay3, - bs.WeakCallStrict( + bs.WeakCall( self._safe_animate, title.position_combine, 'input0', @@ -160,7 +160,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ).autoretain() bs.timer( tdelay + delay2, - bs.WeakCallStrict( + bs.WeakCall( self._safe_animate, img.position_combine, 'input1', @@ -172,7 +172,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ) bs.timer( tdelay + delay3, - bs.WeakCallStrict( + bs.WeakCall( self._safe_animate, img.position_combine, 'input0', @@ -198,7 +198,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ).autoretain() bs.timer( tdelay + delay2, - bs.WeakCallStrict( + bs.WeakCall( self._safe_animate, txt.position_combine, 'input1', @@ -210,7 +210,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ) bs.timer( tdelay + delay3, - bs.WeakCallStrict( + bs.WeakCall( self._safe_animate, txt.position_combine, 'input0', @@ -235,7 +235,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ).autoretain() bs.timer( tdelay + delay3, - bs.WeakCallStrict( + bs.WeakCall( self._safe_animate, txt_num.position_combine, 'input0', @@ -256,7 +256,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ) bs.timer( tdelay + delay2, - bs.WeakCallStrict( + bs.WeakCall( self._safe_animate, s_txt.position_combine, 'input1', @@ -268,7 +268,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ) bs.timer( tdelay + delay3, - bs.WeakCallStrict( + bs.WeakCall( self._safe_animate, s_txt.position_combine, 'input0', @@ -297,7 +297,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ) bs.timer( tdelay + delay2, - bs.WeakCallStrict( + bs.WeakCall( self._safe_animate, s_txt_2.position_combine, 'input1', @@ -309,7 +309,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ) bs.timer( tdelay + delay3, - bs.WeakCallStrict( + bs.WeakCall( self._safe_animate, s_txt_2.position_combine, 'input0', @@ -328,14 +328,12 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): bs.timer( tdelay + delay1, - bs.CallStrict( - _safesetattr, s_txt.node, 'color', (1, 1, 1, 1) - ), + bs.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1)), ) for j in range(score_change): bs.timer( (tdelay + delay1 + 0.15 * j), - bs.CallStrict( + bs.Call( _safesetattr, s_txt.node, 'text', diff --git a/dist/ba_data/python/bascenev1lib/activity/multiteamscore.py b/dist/ba_data/python/bascenev1lib/activity/multiteamscore.py index b8f6089..0bf4bd3 100644 --- a/dist/ba_data/python/bascenev1lib/activity/multiteamscore.py +++ b/dist/ba_data/python/bascenev1lib/activity/multiteamscore.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to teams mode score screen.""" - from __future__ import annotations from typing import override diff --git a/dist/ba_data/python/bascenev1lib/activity/multiteamvictory.py b/dist/ba_data/python/bascenev1lib/activity/multiteamvictory.py index 8052893..058ffd4 100644 --- a/dist/ba_data/python/bascenev1lib/activity/multiteamvictory.py +++ b/dist/ba_data/python/bascenev1lib/activity/multiteamvictory.py @@ -4,14 +4,14 @@ from __future__ import annotations -from typing import override, TYPE_CHECKING, Any, cast +from typing import override, TYPE_CHECKING import bascenev1 as bs from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity if TYPE_CHECKING: - pass + from typing import Any class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): @@ -54,10 +54,9 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): winning_sessionteam = self.settings_raw['winner'] # Pause a moment before playing victory music. - bs.timer(0.6, bs.WeakCallStrict(self._play_victory_music)) + bs.timer(0.6, bs.WeakCall(self._play_victory_music)) bs.timer( - 4.4, - bs.WeakCallStrict(self._show_winner, self.settings_raw['winner']), + 4.4, bs.WeakCall(self._show_winner, self.settings_raw['winner']) ) bs.timer(4.6, self._score_display_sound.play) @@ -77,9 +76,9 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ) player_entries.sort(reverse=True, key=lambda x: x[0]) if len(player_entries) > 0: - # Store some info for the top ffa player so we can show - # winner info even if they leave. - self._ffa_top_player_info = cast(Any, list(player_entries[0])) + # Store some info for the top ffa player so we can + # show winner info even if they leave. + self._ffa_top_player_info = list(player_entries[0]) self._ffa_top_player_info[1] = self._ffa_top_player_info[ 2 ].getname() @@ -179,7 +178,6 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): transition_delay=t_incr * 4, ).autoretain() - assert isinstance(session, bs.MultiTeamSession) win_score = (session.get_series_length() - 1) // 2 + 1 lose_score = 0 for team in self.teams: @@ -423,7 +421,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): transition_delay=tdelay, ).autoretain() - bs.timer(15.0, bs.WeakCallStrict(self._show_tips)) + bs.timer(15.0, bs.WeakCall(self._show_tips)) def _show_tips(self) -> None: from bascenev1lib.actor.tipstext import TipsText diff --git a/dist/ba_data/python/bascenev1lib/actor/bomb.py b/dist/ba_data/python/bascenev1lib/actor/bomb.py index a4b92a3..04dc297 100644 --- a/dist/ba_data/python/bascenev1lib/actor/bomb.py +++ b/dist/ba_data/python/bascenev1lib/actor/bomb.py @@ -864,11 +864,10 @@ class Bomb(bs.Actor): }, ) self.arm_timer = bs.Timer( - 0.2, bs.WeakCallStrict(self.handlemessage, ArmMessage()) + 0.2, bs.WeakCall(self.handlemessage, ArmMessage()) ) self.warn_timer = bs.Timer( - fuse_time - 1.7, - bs.WeakCallStrict(self.handlemessage, WarnMessage()), + fuse_time - 1.7, bs.WeakCall(self.handlemessage, WarnMessage()) ) else: @@ -919,8 +918,7 @@ class Bomb(bs.Actor): if self.bomb_type not in ('land_mine', 'tnt'): assert fuse_time is not None bs.timer( - fuse_time, - bs.WeakCallStrict(self.handlemessage, ExplodeMessage()), + fuse_time, bs.WeakCall(self.handlemessage, ExplodeMessage()) ) bs.animate( @@ -978,7 +976,7 @@ class Bomb(bs.Actor): def _handle_dropped(self) -> None: if self.bomb_type == 'land_mine': self.arm_timer = bs.Timer( - 1.25, bs.WeakCallStrict(self.handlemessage, ArmMessage()) + 1.25, bs.WeakCall(self.handlemessage, ArmMessage()) ) # Once we've thrown a sticky bomb we can stick to it. @@ -1030,7 +1028,7 @@ class Bomb(bs.Actor): # We blew up so we need to go away. # NOTE TO SELF: do we actually need this delay? - bs.timer(0.001, bs.WeakCallStrict(self.handlemessage, bs.DieMessage())) + bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage())) def _handle_warn(self) -> None: if self.texture_sequence and self.node: @@ -1066,7 +1064,7 @@ class Bomb(bs.Actor): # We now make it explodable. bs.timer( 0.25, - bs.WeakCallStrict( + bs.WeakCall( self._add_material, factory.land_mine_blast_material ), ) @@ -1083,7 +1081,7 @@ class Bomb(bs.Actor): ) bs.timer( 0.25, - bs.WeakCallStrict( + bs.WeakCall( self._add_material, factory.land_mine_blast_material ), ) @@ -1123,7 +1121,7 @@ class Bomb(bs.Actor): bs.timer( 0.1 + random.random() * 0.1, - bs.WeakCallStrict(self.handlemessage, ExplodeMessage()), + bs.WeakCall(self.handlemessage, ExplodeMessage()), ) assert self.node self.node.handlemessage( @@ -1192,7 +1190,7 @@ class TNTSpawner: # Go with slightly more than 1 second to avoid timer stacking. self._update_timer = bs.Timer( - 1.1, bs.WeakCallStrict(self._update), repeat=True + 1.1, bs.WeakCall(self._update), repeat=True ) def _update(self) -> None: diff --git a/dist/ba_data/python/bascenev1lib/actor/controlsguide.py b/dist/ba_data/python/bascenev1lib/actor/controlsguide.py index 966d41e..40b3f18 100644 --- a/dist/ba_data/python/bascenev1lib/actor/controlsguide.py +++ b/dist/ba_data/python/bascenev1lib/actor/controlsguide.py @@ -263,7 +263,7 @@ class ControlsGuide(bs.Actor): node.opacity = 0.0 # Don't do anything until our delay has passed. - bs.timer(delay, bs.WeakCallStrict(self._start_updating)) + bs.timer(delay, bs.WeakCall(self._start_updating)) @staticmethod def _meaningful_button_name( @@ -290,12 +290,10 @@ class ControlsGuide(bs.Actor): if self._lifespan is not None: self._cancel_timer = bs.Timer( self._lifespan, - bs.WeakCallStrict( - self.handlemessage, bs.DieMessage(immediate=True) - ), + bs.WeakCall(self.handlemessage, bs.DieMessage(immediate=True)), ) self._fade_in_timer = bs.Timer( - 1.0, bs.WeakCallStrict(self._check_fade_in), repeat=True + 1.0, bs.WeakCall(self._check_fade_in), repeat=True ) self._check_fade_in() # Do one check immediately. @@ -351,12 +349,11 @@ class ControlsGuide(bs.Actor): # If we were given a lifespan, transition out after it. if self._lifespan is not None: bs.timer( - self._lifespan, - bs.WeakCallStrict(self.handlemessage, bs.DieMessage()), + self._lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage()) ) self._update() self._update_timer = bs.Timer( - 1.0, bs.WeakCallStrict(self._update), repeat=True + 1.0, bs.WeakCall(self._update), repeat=True ) def _update(self) -> None: @@ -568,6 +565,6 @@ class ControlsGuide(bs.Actor): # die later. for node in self._nodes: bs.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0}) - bs.timer(3.1, bs.WeakCallStrict(self._die)) + bs.timer(3.1, bs.WeakCall(self._die)) return None return super().handlemessage(msg) diff --git a/dist/ba_data/python/bascenev1lib/actor/flag.py b/dist/ba_data/python/bascenev1lib/actor/flag.py index 3d42d02..591c73b 100644 --- a/dist/ba_data/python/bascenev1lib/actor/flag.py +++ b/dist/ba_data/python/bascenev1lib/actor/flag.py @@ -211,7 +211,7 @@ class Flag(bs.Actor): if self._dropped_timeout is not None: self._count = self._dropped_timeout self._tick_timer = bs.Timer( - 1.0, call=bs.WeakCallStrict(self._tick), repeat=True + 1.0, call=bs.WeakCall(self._tick), repeat=True ) self._counter = bs.newnode( 'text', @@ -314,7 +314,7 @@ class Flag(bs.Actor): self._score_text.color = bs.safecolor(self.node.color) bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) self._score_text_hide_timer = bs.Timer( - 1.0, bs.WeakCallStrict(self._hide_score_text) + 1.0, bs.WeakCall(self._hide_score_text) ) @override diff --git a/dist/ba_data/python/bascenev1lib/actor/image.py b/dist/ba_data/python/bascenev1lib/actor/image.py index bf84859..4e38f6b 100644 --- a/dist/ba_data/python/bascenev1lib/actor/image.py +++ b/dist/ba_data/python/bascenev1lib/actor/image.py @@ -169,7 +169,7 @@ class Image(bs.Actor): if transition_out_delay is not None: bs.timer( transition_delay + transition_out_delay + 1.0, - bs.WeakCallStrict(self.handlemessage, bs.DieMessage()), + bs.WeakCall(self.handlemessage, bs.DieMessage()), ) @override diff --git a/dist/ba_data/python/bascenev1lib/actor/onscreentimer.py b/dist/ba_data/python/bascenev1lib/actor/onscreentimer.py index 8326aaf..196674e 100644 --- a/dist/ba_data/python/bascenev1lib/actor/onscreentimer.py +++ b/dist/ba_data/python/bascenev1lib/actor/onscreentimer.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Defines Actor(s).""" - from __future__ import annotations from typing import TYPE_CHECKING, override diff --git a/dist/ba_data/python/bascenev1lib/actor/popuptext.py b/dist/ba_data/python/bascenev1lib/actor/popuptext.py index e332954..9c0d09c 100644 --- a/dist/ba_data/python/bascenev1lib/actor/popuptext.py +++ b/dist/ba_data/python/bascenev1lib/actor/popuptext.py @@ -116,7 +116,7 @@ class PopupText(bs.Actor): # kill ourself self._die_timer = bs.Timer( - lifespan, bs.WeakCallStrict(self.handlemessage, bs.DieMessage()) + lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage()) ) @override diff --git a/dist/ba_data/python/bascenev1lib/actor/powerupbox.py b/dist/ba_data/python/bascenev1lib/actor/powerupbox.py index cf3f866..dd19b54 100644 --- a/dist/ba_data/python/bascenev1lib/actor/powerupbox.py +++ b/dist/ba_data/python/bascenev1lib/actor/powerupbox.py @@ -265,11 +265,11 @@ class PowerupBox(bs.Actor): if expire: bs.timer( DEFAULT_POWERUP_INTERVAL - 2.5, - bs.WeakCallStrict(self._start_flashing), + bs.WeakCall(self._start_flashing), ) bs.timer( DEFAULT_POWERUP_INTERVAL - 1.0, - bs.WeakCallStrict(self.handlemessage, bs.DieMessage()), + bs.WeakCall(self.handlemessage, bs.DieMessage()), ) def _start_flashing(self) -> None: diff --git a/dist/ba_data/python/bascenev1lib/actor/respawnicon.py b/dist/ba_data/python/bascenev1lib/actor/respawnicon.py index b5b5b58..3e31350 100644 --- a/dist/ba_data/python/bascenev1lib/actor/respawnicon.py +++ b/dist/ba_data/python/bascenev1lib/actor/respawnicon.py @@ -66,7 +66,6 @@ class RespawnIcon: ) ) - assert self._image assert self._image.node bs.animate(self._image.node, 'opacity', {0.0: 0, 0.2: 0.7}) @@ -90,7 +89,6 @@ class RespawnIcon: ) ) - assert self._name assert self._name.node bs.animate(self._name.node, 'scale', {0: 0, 0.1: 0.5}) @@ -135,7 +133,6 @@ class RespawnIcon: ) ) - assert self._text assert self._text.node bs.animate(self._text.node, 'scale', {0: 0, 0.1: 0.9}) if self._dec_text: @@ -145,7 +142,7 @@ class RespawnIcon: self._dec_timer: bs.Timer | None = None self._update() self._timer: bs.Timer | None = bs.Timer( - 1.0, bs.WeakCallStrict(self._update), repeat=True + 1.0, bs.WeakCall(self._update), repeat=True ) @property @@ -214,7 +211,7 @@ class RespawnIcon: # Start the timer to tick down. self._dec_timer = bs.Timer( 0.25, - bs.WeakCallStrict(self._dec_step, ['..', '.', '']), + bs.WeakCall(self._dec_step, ['..', '.', '']), repeat=True, ) else: diff --git a/dist/ba_data/python/bascenev1lib/actor/scoreboard.py b/dist/ba_data/python/bascenev1lib/actor/scoreboard.py index 3c0940a..18ba89d 100644 --- a/dist/ba_data/python/bascenev1lib/actor/scoreboard.py +++ b/dist/ba_data/python/bascenev1lib/actor/scoreboard.py @@ -189,7 +189,7 @@ class _Entry: def flash(self, countdown: bool, extra_flash: bool) -> None: """Flash momentarily.""" self._flash_timer = bs.Timer( - 0.1, bs.WeakCallStrict(self._do_flash), repeat=True + 0.1, bs.WeakCall(self._do_flash), repeat=True ) if countdown: self._flash_counter = 10 @@ -358,7 +358,7 @@ class _EntryProxy: return try: - bs.pushcall(bs.CallStrict(scoreboard.remove_team, self._team_id)) + bs.pushcall(bs.Call(scoreboard.remove_team, self._team_id)) except bs.ContextError: # This happens if we fire after the activity expires. # In that case we don't need to do anything. diff --git a/dist/ba_data/python/bascenev1lib/actor/spaz.py b/dist/ba_data/python/bascenev1lib/actor/spaz.py index dbeb5f9..f5bfab0 100644 --- a/dist/ba_data/python/bascenev1lib/actor/spaz.py +++ b/dist/ba_data/python/bascenev1lib/actor/spaz.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Defines the spaz actor.""" - # pylint: disable=too-many-lines from __future__ import annotations @@ -173,9 +172,7 @@ class Spaz(bs.Actor): if node: setattr(node, attr, val) - bs.timer( - 1.0, bs.CallStrict(_safesetattr, self.node, 'invincible', False) - ) + bs.timer(1.0, bs.Call(_safesetattr, self.node, 'invincible', False)) self.hitpoints = self.default_hitpoints self.hitpoints_max = self.default_hitpoints self.shield_hitpoints: int | None = None @@ -379,7 +376,7 @@ class Spaz(bs.Actor): bs.animate(self._score_text, 'scale', {0.0: start_scale, 0.2: 0.02}) self._score_text_hide_timer = bs.Timer( - 1.0, bs.WeakCallStrict(self._hide_score_text) + 1.0, bs.WeakCall(self._hide_score_text) ) def on_jump_press(self) -> None: @@ -465,7 +462,7 @@ class Spaz(bs.Actor): if not self.node.hold_node: bs.timer( 0.1, - bs.WeakCallStrict( + bs.WeakCall( self._safe_play_sound, SpazFactory.get().swish_sound, 0.8, @@ -632,9 +629,7 @@ class Spaz(bs.Actor): ) self._curse_timer = bs.Timer( self.curse_time, - bs.WeakCallStrict( - self.handlemessage, CurseExplodeMessage() - ), + bs.WeakCall(self.handlemessage, CurseExplodeMessage()), ) def equip_boxing_gloves(self) -> None: @@ -676,7 +671,7 @@ class Spaz(bs.Actor): if self.shield_decay_rate > 0: self.shield_decay_timer = bs.Timer( - 0.5, bs.WeakCallStrict(self.shield_decay), repeat=True + 0.5, bs.WeakCall(self.shield_decay), repeat=True ) # So user can see the decay. self.shield.always_show_health_bar = True @@ -723,12 +718,12 @@ class Spaz(bs.Actor): # Eww; seems we have to do this in a timer or it wont work right. # (since we're getting called from within update() perhaps?..) # NOTE: should test to see if that's still the case. - bs.timer(0.001, bs.WeakCallStrict(self.shatter)) + bs.timer(0.001, bs.WeakCall(self.shatter)) elif isinstance(msg, bs.ImpactDamageMessage): # Eww; seems we have to do this in a timer or it wont work right. # (since we're getting called from within update() perhaps?..) - bs.timer(0.001, bs.WeakCallStrict(self._hit_self, msg.intensity)) + bs.timer(0.001, bs.WeakCall(self._hit_self, msg.intensity)) elif isinstance(msg, bs.PowerupMessage): if self._dead or not self.node: @@ -749,11 +744,11 @@ class Spaz(bs.Actor): ) self._multi_bomb_wear_off_flash_timer = bs.Timer( (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, - bs.WeakCallStrict(self._multi_bomb_wear_off_flash), + bs.WeakCall(self._multi_bomb_wear_off_flash), ) self._multi_bomb_wear_off_timer = bs.Timer( POWERUP_WEAR_OFF_TIME / 1000.0, - bs.WeakCallStrict(self._multi_bomb_wear_off), + bs.WeakCall(self._multi_bomb_wear_off), ) elif msg.poweruptype == 'land_mines': self.set_land_mine_count(min(self.land_mine_count + 3, 3)) @@ -771,11 +766,11 @@ class Spaz(bs.Actor): ) self._bomb_wear_off_flash_timer = bs.Timer( (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, - bs.WeakCallStrict(self._bomb_wear_off_flash), + bs.WeakCall(self._bomb_wear_off_flash), ) self._bomb_wear_off_timer = bs.Timer( POWERUP_WEAR_OFF_TIME / 1000.0, - bs.WeakCallStrict(self._bomb_wear_off), + bs.WeakCall(self._bomb_wear_off), ) elif msg.poweruptype == 'sticky_bombs': self.bomb_type = 'sticky' @@ -791,11 +786,11 @@ class Spaz(bs.Actor): ) self._bomb_wear_off_flash_timer = bs.Timer( (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, - bs.WeakCallStrict(self._bomb_wear_off_flash), + bs.WeakCall(self._bomb_wear_off_flash), ) self._bomb_wear_off_timer = bs.Timer( POWERUP_WEAR_OFF_TIME / 1000.0, - bs.WeakCallStrict(self._bomb_wear_off), + bs.WeakCall(self._bomb_wear_off), ) elif msg.poweruptype == 'punch': tex = PowerupBoxFactory.get().tex_punch @@ -812,11 +807,11 @@ class Spaz(bs.Actor): ) self._boxing_gloves_wear_off_flash_timer = bs.Timer( (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, - bs.WeakCallStrict(self._gloves_wear_off_flash), + bs.WeakCall(self._gloves_wear_off_flash), ) self._boxing_gloves_wear_off_timer = bs.Timer( POWERUP_WEAR_OFF_TIME / 1000.0, - bs.WeakCallStrict(self._gloves_wear_off), + bs.WeakCall(self._gloves_wear_off), ) elif msg.poweruptype == 'shield': factory = SpazFactory.get() @@ -839,11 +834,11 @@ class Spaz(bs.Actor): ) self._bomb_wear_off_flash_timer = bs.Timer( (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, - bs.WeakCallStrict(self._bomb_wear_off_flash), + bs.WeakCall(self._bomb_wear_off_flash), ) self._bomb_wear_off_timer = bs.Timer( POWERUP_WEAR_OFF_TIME / 1000.0, - bs.WeakCallStrict(self._bomb_wear_off), + bs.WeakCall(self._bomb_wear_off), ) elif msg.poweruptype == 'health': if self._cursed: @@ -890,8 +885,7 @@ class Spaz(bs.Actor): self.frozen = True self.node.frozen = True bs.timer( - msg.time, - bs.WeakCallStrict(self.handlemessage, bs.ThawMessage()), + msg.time, bs.WeakCall(self.handlemessage, bs.ThawMessage()) ) # Instantly shatter if we're already dead. # (otherwise its hard to tell we're dead). @@ -1171,7 +1165,7 @@ class Spaz(bs.Actor): if self._cursed and damage > 0: bs.timer( 0.05, - bs.WeakCallStrict( + bs.WeakCall( self.curse_explode, msg.get_source_player(bs.Player) ), ) @@ -1390,7 +1384,7 @@ class Spaz(bs.Actor): if dropping_bomb: self.bomb_count -= 1 bomb.node.add_death_action( - bs.WeakCallStrict(self.handlemessage, BombDiedMessage()) + bs.WeakCall(self.handlemessage, BombDiedMessage()) ) self._pick_up(bomb.node) diff --git a/dist/ba_data/python/bascenev1lib/actor/spazappearance.py b/dist/ba_data/python/bascenev1lib/actor/spazappearance.py index 1432bfc..e7521c9 100644 --- a/dist/ba_data/python/bascenev1lib/actor/spazappearance.py +++ b/dist/ba_data/python/bascenev1lib/actor/spazappearance.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Appearance functionality for spazzes.""" - from __future__ import annotations import bascenev1 as bs @@ -819,9 +818,9 @@ def register_appearances() -> None: t.death_sounds = ['oldLadyDeath'] t.pickup_sounds = old_lady_sounds t.fall_sounds = ['oldLadyFall'] - t.style = 'bones' - t.default_color = (0.2, 1.0, 1.0) - t.default_highlight = (0.5, 0.25, 1.0) + t.style = 'spaz' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) # Gladiator ################################### t = Appearance('Gladiator') diff --git a/dist/ba_data/python/bascenev1lib/actor/spazbot.py b/dist/ba_data/python/bascenev1lib/actor/spazbot.py index 660fa66..7cbbcc8 100644 --- a/dist/ba_data/python/bascenev1lib/actor/spazbot.py +++ b/dist/ba_data/python/bascenev1lib/actor/spazbot.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Bot versions of Spaz.""" - # pylint: disable=too-many-lines from __future__ import annotations @@ -112,14 +111,12 @@ class SpazBot(Spaz): can_accept_powerups=False, ) - from bascenev1lib.mainmenu import MainMenuActivity - # If you need to add custom behavior to a bot, set this to a callable # which takes one arg (the bot) and returns False if the bot's normal # update should be run and True if not. self.update_callback: Callable[[SpazBot], Any] | None = None activity = self.activity - assert isinstance(activity, (bs.GameActivity, MainMenuActivity)) + assert isinstance(activity, bs.GameActivity) self._map = weakref.ref(activity.map) self.last_player_attacked_by: bs.Player | None = None self.last_attacked_time = 0.0 @@ -330,10 +327,10 @@ class SpazBot(Spaz): self.node.jump_pressed = False # Throws: - bs.timer(0.1, bs.CallStrict(_safe_pickup, self.node)) + bs.timer(0.1, bs.Call(_safe_pickup, self.node)) else: # Throws: - bs.timer(0.1, bs.CallStrict(_safe_pickup, self.node)) + bs.timer(0.1, bs.Call(_safe_pickup, self.node)) if self.static: if time_till_throw < 0.3: @@ -909,67 +906,6 @@ class ExplodeyBotShielded(ExplodeyBot): points_mult = 5 -class DemoBot(SpazBot): - """A bs.SpazBot who lacks many specific traits and is used for the "Bots - Free-for-All" easter egg. - - category: Bot Classes - """ - - run = True - - @classmethod - def randomize_traits(cls, appearance: str) -> None: - """Randomize the behavioral traits of the bot. Should be called - everytime before creating a new instance. - """ - - cls.color = (random.random(), random.random(), random.random()) - cls.highlight = (random.random(), random.random(), random.random()) - cls.character = appearance - cls.punchiness = random.uniform(0.5, 1.0) - cls.throwiness = random.uniform(0.5, 1.0) - cls.bouncy = appearance == 'Easter Bunny' - cls.throw_rate = random.uniform(0.5, 2.0) - cls.default_bomb_type = random.choice( - ('normal', 'sticky', 'ice', 'impact') - ) - cls.default_boxing_gloves = random.choice((True, False, False, False)) - - @override - def __init__(self) -> None: - super().__init__() - self._init_time = bs.time() - - @override - def handlemessage(self, msg: Any) -> Any: - if ( - isinstance(msg, bs.HitMessage) - and self.node - and bs.time() - self._init_time <= 1.0 - ): - assert msg.force_direction is not None - self.node.handlemessage( - 'impulse', - msg.pos[0], - msg.pos[1], - msg.pos[2], - msg.velocity[0], - msg.velocity[1], - msg.velocity[2], - msg.magnitude * self.impact_scale, - msg.velocity_magnitude * self.impact_scale, - msg.radius, - 0, - msg.force_direction[0], - msg.force_direction[1], - msg.force_direction[2], - ) - self.node.handlemessage('hurt_sound') - return None - return super().handlemessage(msg) - - class SpazBotSet: """A container/controller for one or more bs.SpazBots. @@ -1009,7 +945,7 @@ class SpazBotSet: pt=pos, spawn_time=spawn_time, send_spawn_message=False, - spawn_callback=bs.CallStrict( + spawn_callback=bs.Call( self._spawn_bot, bot_type, pos, on_spawn_call ), ) @@ -1104,7 +1040,7 @@ class SpazBotSet: def start_moving(self) -> None: """Start processing bot AI updates so they start doing their thing.""" self._bot_update_timer = bs.Timer( - 0.05, bs.WeakCallStrict(self._update), repeat=True + 0.05, bs.WeakCall(self._update), repeat=True ) def stop_moving(self) -> None: @@ -1147,7 +1083,7 @@ class SpazBotSet: bot.node.move_up_down = 0 bs.timer( 0.5 * random.random(), - bs.CallStrict(bot.handlemessage, bs.CelebrateMessage()), + bs.Call(bot.handlemessage, bs.CelebrateMessage()), ) jump_duration = random.randrange(400, 500) j = random.randrange(0, 200) @@ -1157,58 +1093,18 @@ class SpazBotSet: j += jump_duration bs.timer( random.uniform(0.0, 1.0), - bs.CallStrict(bot.node.handlemessage, 'attack_sound'), + bs.Call(bot.node.handlemessage, 'attack_sound'), ) bs.timer( random.uniform(1.0, 2.0), - bs.CallStrict(bot.node.handlemessage, 'attack_sound'), + bs.Call(bot.node.handlemessage, 'attack_sound'), ) bs.timer( random.uniform(2.0, 3.0), - bs.CallStrict(bot.node.handlemessage, 'attack_sound'), + bs.Call(bot.node.handlemessage, 'attack_sound'), ) def add_bot(self, bot: SpazBot) -> None: """Add a bs.SpazBot instance to the set.""" self._bot_lists[self._bot_add_list].append(bot) self._bot_add_list = (self._bot_add_list + 1) % self._bot_list_count - - -class DemoSpazBotSet(SpazBotSet): - """A bs.SpazBotSet that has its bs.SpazBots attack every other bs.Spaz - instead of only going after bs.Players. - - category: Bot Classes - """ - - @override - def _update(self) -> None: - # Update one of our bot lists each time through. - # First off, remove no-longer-existing bots from the list. - try: - bot_list = self._bot_lists[self._bot_update_list] = [ - b for b in self._bot_lists[self._bot_update_list] if b - ] - except Exception: - bot_list = [] - logging.exception( - 'Error updating bot list: %s', - self._bot_lists[self._bot_update_list], - ) - self._bot_update_list = ( - self._bot_update_list + 1 - ) % self._bot_list_count - - # Update our list of player points for the bots to use. - spaz_pts = [] - our_bots = self.get_living_bots() - for node in bs.getnodes(): - spaz = node.getdelegate(Spaz) - if spaz and spaz.is_alive() and spaz not in our_bots: - spaz_pts.append( - (bs.Vec3(node.position), bs.Vec3(node.velocity)) - ) - - for bot in bot_list: - bot.set_player_points(spaz_pts) - bot.update_ai() diff --git a/dist/ba_data/python/bascenev1lib/actor/text.py b/dist/ba_data/python/bascenev1lib/actor/text.py index 6fbed08..42611e8 100644 --- a/dist/ba_data/python/bascenev1lib/actor/text.py +++ b/dist/ba_data/python/bascenev1lib/actor/text.py @@ -219,7 +219,7 @@ class Text(bs.Actor): if transition_out_delay is not None: bs.timer( transition_delay + transition_out_delay + 1.0, - bs.WeakCallStrict(self.handlemessage, bs.DieMessage()), + bs.WeakCall(self.handlemessage, bs.DieMessage()), ) @override diff --git a/dist/ba_data/python/bascenev1lib/actor/tipstext.py b/dist/ba_data/python/bascenev1lib/actor/tipstext.py index 371188f..9c16168 100644 --- a/dist/ba_data/python/bascenev1lib/actor/tipstext.py +++ b/dist/ba_data/python/bascenev1lib/actor/tipstext.py @@ -53,7 +53,7 @@ class TipsText(bs.Actor): self._message_spacing = 3000 self._change_timer = bs.Timer( 0.001 * (self._message_duration + self._message_spacing), - bs.WeakCallStrict(self.change_phrase), + bs.WeakCall(self.change_phrase), repeat=True, ) self._combine = bs.newnode( diff --git a/dist/ba_data/python/bascenev1lib/actor/zoomtext.py b/dist/ba_data/python/bascenev1lib/actor/zoomtext.py index 6049db2..d65b728 100644 --- a/dist/ba_data/python/bascenev1lib/actor/zoomtext.py +++ b/dist/ba_data/python/bascenev1lib/actor/zoomtext.py @@ -81,14 +81,12 @@ class ZoomText(bs.Actor): positionadjusted2 = (shiftposition[0], shiftposition[1] - 100) bs.timer( shiftdelay, - bs.WeakCallStrict( - self._shift, positionadjusted, positionadjusted2 - ), + bs.WeakCall(self._shift, positionadjusted, positionadjusted2), ) if jitter > 0.0: bs.timer( shiftdelay + 0.25, - bs.WeakCallStrict( + bs.WeakCall( self._jitter, positionadjusted2, jitter * scale ), ) @@ -157,9 +155,7 @@ class ZoomText(bs.Actor): # if they give us a lifespan, kill ourself down the line if lifespan is not None: - bs.timer( - lifespan, bs.WeakCallStrict(self.handlemessage, bs.DieMessage()) - ) + bs.timer(lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage())) @override def handlemessage(self, msg: Any) -> Any: diff --git a/dist/ba_data/python/bascenev1lib/game/assault.py b/dist/ba_data/python/bascenev1lib/game/assault.py index e9bf756..1f0938c 100644 --- a/dist/ba_data/python/bascenev1lib/game/assault.py +++ b/dist/ba_data/python/bascenev1lib/game/assault.py @@ -149,7 +149,7 @@ class AssaultGame(bs.TeamGameActivity[Player, Team]): ( 'call', 'at_connect', - bs.CallStrict(self._handle_base_collide, team), + bs.Call(self._handle_base_collide, team), ), ), ) @@ -270,7 +270,7 @@ class AssaultGame(bs.TeamGameActivity[Player, Team]): self._teleport(player, new_pos, random_num) bs.timer( 0.01, - bs.CallStrict( + bs.Call( self._teleport, player, new_pos, random_num ), ) diff --git a/dist/ba_data/python/bascenev1lib/game/capturetheflag.py b/dist/ba_data/python/bascenev1lib/game/capturetheflag.py index cbe397b..e62820d 100644 --- a/dist/ba_data/python/bascenev1lib/game/capturetheflag.py +++ b/dist/ba_data/python/bascenev1lib/game/capturetheflag.py @@ -534,7 +534,7 @@ class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]): if team.flag_return_touches == 1: team.touch_return_timer = bs.Timer( 0.1, - call=bs.CallStrict(self._touch_return_update, team), + call=bs.Call(self._touch_return_update, team), repeat=True, ) team.touch_return_timer_ticking = None @@ -646,9 +646,7 @@ class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]): elif isinstance(msg, FlagDiedMessage): assert isinstance(msg.flag, CTFFlag) - bs.timer( - 0.1, bs.CallStrict(self._spawn_flag_for_team, msg.flag.team) - ) + bs.timer(0.1, bs.Call(self._spawn_flag_for_team, msg.flag.team)) elif isinstance(msg, FlagPickedUpMessage): # Store the last player to hold the flag for scoring purposes. diff --git a/dist/ba_data/python/bascenev1lib/game/chosenone.py b/dist/ba_data/python/bascenev1lib/game/chosenone.py index 73bd609..983521d 100644 --- a/dist/ba_data/python/bascenev1lib/game/chosenone.py +++ b/dist/ba_data/python/bascenev1lib/game/chosenone.py @@ -168,11 +168,7 @@ class ChosenOneGame(bs.TeamGameActivity[Player, Team]): actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), - ( - 'call', - 'at_connect', - bs.WeakCallStrict(self._handle_reset_collide), - ), + ('call', 'at_connect', bs.WeakCall(self._handle_reset_collide)), ), ) diff --git a/dist/ba_data/python/bascenev1lib/game/conquest.py b/dist/ba_data/python/bascenev1lib/game/conquest.py index da22e76..ee9c355 100644 --- a/dist/ba_data/python/bascenev1lib/game/conquest.py +++ b/dist/ba_data/python/bascenev1lib/game/conquest.py @@ -50,7 +50,7 @@ class Player(bs.Player['Team']): def respawn_timer(self) -> bs.Timer | None: """Type safe access to standard respawn timer.""" val = self.customdata.get('respawn_timer', None) - assert isinstance(val, bs.Timer | None) + assert isinstance(val, (bs.Timer, type(None))) return val @respawn_timer.setter @@ -61,7 +61,7 @@ class Player(bs.Player['Team']): def respawn_icon(self) -> RespawnIcon | None: """Type safe access to standard respawn icon.""" val = self.customdata.get('respawn_icon', None) - assert isinstance(val, RespawnIcon | None) + assert isinstance(val, (RespawnIcon, type(None))) return val @respawn_icon.setter diff --git a/dist/ba_data/python/bascenev1lib/game/easteregghunt.py b/dist/ba_data/python/bascenev1lib/game/easteregghunt.py index 3f98c76..443c77b 100644 --- a/dist/ba_data/python/bascenev1lib/game/easteregghunt.py +++ b/dist/ba_data/python/bascenev1lib/game/easteregghunt.py @@ -213,7 +213,7 @@ class EasterEggHuntGame(bs.TeamGameActivity[Player, Team]): assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 player.respawn_timer = bs.Timer( - respawn_time, bs.CallStrict(self.spawn_player_if_exists, player) + respawn_time, bs.Call(self.spawn_player_if_exists, player) ) player.respawn_icon = RespawnIcon(player, respawn_time) diff --git a/dist/ba_data/python/bascenev1lib/game/elimination.py b/dist/ba_data/python/bascenev1lib/game/elimination.py index cb274fd..34f1d4a 100644 --- a/dist/ba_data/python/bascenev1lib/game/elimination.py +++ b/dist/ba_data/python/bascenev1lib/game/elimination.py @@ -495,7 +495,7 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]): """Spawn a player (override).""" actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) if not self._solo_mode: - bs.timer(0.3, bs.CallStrict(self._print_lives, player)) + bs.timer(0.3, bs.Call(self._print_lives, player)) # If we have any icons, update their state. for icon in player.icons: @@ -582,9 +582,8 @@ class EliminationGame(bs.TeamGameActivity[Player, Team]): # In solo, put ourself at the back of the spawn order. if self._solo_mode: - if player in player.team.spawn_order: - player.team.spawn_order.remove(player) - player.team.spawn_order.append(player) + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) def _update(self) -> None: if self._solo_mode: diff --git a/dist/ba_data/python/bascenev1lib/game/football.py b/dist/ba_data/python/bascenev1lib/game/football.py index 563ce62..df0a9fc 100644 --- a/dist/ba_data/python/bascenev1lib/game/football.py +++ b/dist/ba_data/python/bascenev1lib/game/football.py @@ -518,9 +518,8 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]): @override def on_begin(self) -> None: - # pylint: disable=too-many-locals + # FIXME: Split this up a bit. # pylint: disable=too-many-statements - from bascenev1lib.actor import controlsguide super().on_begin() @@ -573,10 +572,11 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]): ) abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite typed_bot_list: list[type[SpazBot]] = [] - bomberbots: list[type[SpazBot]] = [BomberBot] * ( - 1 if len(self.initialplayerinfos) < 3 else 2 + self._bot_types_7 = ( + typed_bot_list + + [abot] + + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2) ) - self._bot_types_7 = typed_bot_list + [abot] + bomberbots bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot self._bot_types_14 = [bbot] * ( 1 if len(self.initialplayerinfos) < 3 else 2 @@ -587,9 +587,11 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]): abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot typed_bot_list_2: list[type[SpazBot]] = [] - stickybots: list[type[SpazBot]] = [StickyBot] - abots: list[type[SpazBot]] = [abot] * len(self.initialplayerinfos) - self._bot_types_initial = typed_bot_list_2 + stickybots + abots + self._bot_types_initial = ( + typed_bot_list_2 + + [StickyBot] + + [abot] * len(self.initialplayerinfos) + ) self._bot_types_7 = [bbot] * ( 1 if len(self.initialplayerinfos) < 3 else 2 ) @@ -731,8 +733,7 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]): spawnpoints = self.map.powerup_spawn_points for i, _point in enumerate(spawnpoints): bs.timer( - 1.0 + i * 0.5, - bs.CallStrict(self._drop_powerup, i, poweruptype), + 1.0 + i * 0.5, bs.Call(self._drop_powerup, i, poweruptype) ) else: point = ( @@ -836,7 +837,7 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]): bs.setmusic(None) self._bots.final_celebrate() - bs.timer(0.001, bs.CallStrict(self.do_end, 'defeat')) + bs.timer(0.001, bs.Call(self.do_end, 'defeat')) def update_scores(self) -> None: """update scoreboard and check for winners""" @@ -939,13 +940,13 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]): assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 player.respawn_timer = bs.Timer( - respawn_time, bs.CallStrict(self.spawn_player_if_exists, player) + respawn_time, bs.Call(self.spawn_player_if_exists, player) ) player.respawn_icon = RespawnIcon(player, respawn_time) elif isinstance(msg, SpazBotDiedMessage): # Every time a bad guy dies, spawn a new one. - bs.timer(3.0, bs.CallStrict(self._spawn_bot, (type(msg.spazbot)))) + bs.timer(3.0, bs.Call(self._spawn_bot, (type(msg.spazbot)))) elif isinstance(msg, SpazBotPunchedMessage): if self._preset in ['rookie', 'rookie_easy']: diff --git a/dist/ba_data/python/bascenev1lib/game/keepaway.py b/dist/ba_data/python/bascenev1lib/game/keepaway.py index d8a11da..6fe75ec 100644 --- a/dist/ba_data/python/bascenev1lib/game/keepaway.py +++ b/dist/ba_data/python/bascenev1lib/game/keepaway.py @@ -215,7 +215,7 @@ class KeepAwayGame(bs.TeamGameActivity[Player, Team]): for player in self.players: holdingflag = False try: - assert isinstance(player.actor, PlayerSpaz | None) + assert isinstance(player.actor, (PlayerSpaz, type(None))) if ( player.actor and player.actor.node diff --git a/dist/ba_data/python/bascenev1lib/game/kingofthehill.py b/dist/ba_data/python/bascenev1lib/game/kingofthehill.py index 7ee2c25..f4375af 100644 --- a/dist/ba_data/python/bascenev1lib/game/kingofthehill.py +++ b/dist/ba_data/python/bascenev1lib/game/kingofthehill.py @@ -133,16 +133,12 @@ class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]): ( 'call', 'at_connect', - bs.CallStrict( - self._handle_player_flag_region_collide, True - ), + bs.Call(self._handle_player_flag_region_collide, True), ), ( 'call', 'at_disconnect', - bs.CallStrict( - self._handle_player_flag_region_collide, False - ), + bs.Call(self._handle_player_flag_region_collide, False), ), ), ) @@ -196,33 +192,15 @@ class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]): ) # Flag region. flagmats = [self._flag_region_material, shared.region_material] - if self.map.getname() == 'Happy Thoughts': - # Exclusive region for happy thoughts, to avoid - # marking points from the bottom of the platform. - bs.newnode( - 'region', - attrs={ - 'position': ( - self._flag_pos[0], - self._flag_pos[1] * 1.06, - self._flag_pos[2], - ), - 'scale': (3.4, 1.75, 0.8), - 'type': 'square', - 'materials': flagmats, - }, - ) - - else: - bs.newnode( - 'region', - attrs={ - 'position': self._flag_pos, - 'scale': (1.8, 1.8, 1.8), - 'type': 'sphere', - 'materials': flagmats, - }, - ) + bs.newnode( + 'region', + attrs={ + 'position': self._flag_pos, + 'scale': (1.8, 1.8, 1.8), + 'type': 'sphere', + 'materials': flagmats, + }, + ) self._update_scoreboard() self._update_flag_state() diff --git a/dist/ba_data/python/bascenev1lib/game/meteorshower.py b/dist/ba_data/python/bascenev1lib/game/meteorshower.py index de400a6..187c558 100644 --- a/dist/ba_data/python/bascenev1lib/game/meteorshower.py +++ b/dist/ba_data/python/bascenev1lib/game/meteorshower.py @@ -218,7 +218,7 @@ class MeteorShowerGame(bs.TeamGameActivity[Player, Team]): random.uniform(-3.066, -4.12), 0, ) - bs.timer(delay, bs.CallStrict(self._drop_bomb, pos, vel)) + bs.timer(delay, bs.Call(self._drop_bomb, pos, vel)) delay += 0.1 self._set_meteor_timer() diff --git a/dist/ba_data/python/bascenev1lib/game/onslaught.py b/dist/ba_data/python/bascenev1lib/game/onslaught.py index 6221b2a..8cec75f 100644 --- a/dist/ba_data/python/bascenev1lib/game/onslaught.py +++ b/dist/ba_data/python/bascenev1lib/game/onslaught.py @@ -972,7 +972,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): def _start_powerup_drops(self) -> None: self._powerup_drop_timer = bs.Timer( - 3.0, bs.WeakCallStrict(self._drop_powerups), repeat=True + 3.0, bs.WeakCall(self._drop_powerups), repeat=True ) def _drop_powerups( @@ -984,7 +984,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): for i in range(len(points)): bs.timer( 1.0 + i * 0.5, - bs.WeakCallStrict( + bs.WeakCall( self._drop_powerup, i, poweruptype if i == 0 else None ), ) @@ -1070,7 +1070,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): bs.timer(0, self._cashregistersound.play) bs.timer( base_delay, - bs.WeakCallStrict(self._award_time_bonus, self._time_bonus), + bs.WeakCall(self._award_time_bonus, self._time_bonus), ) base_delay += 1.0 @@ -1082,9 +1082,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): have_flawless = True bs.timer( base_delay, - bs.WeakCallStrict( - self._award_flawless_bonus, player - ), + bs.WeakCall(self._award_flawless_bonus, player), ) player.has_been_hurt = False # reset if have_flawless: @@ -1096,9 +1094,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): ) self.celebrate(20.0) self._award_completion_achievements() - bs.timer( - base_delay, bs.WeakCallStrict(self._award_completion_bonus) - ) + bs.timer(base_delay, bs.WeakCall(self._award_completion_bonus)) base_delay += 0.85 self._winsound.play() bs.cameraflash() @@ -1108,7 +1104,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): # Can't just pass delay to do_end because our extra bonuses # haven't been added yet (once we call do_end the score # gets locked in). - bs.timer(base_delay, bs.WeakCallStrict(self.do_end, 'victory')) + bs.timer(base_delay, bs.WeakCall(self.do_end, 'victory')) return self._wavenum += 1 @@ -1116,7 +1112,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): # Short celebration after waves. if self._wavenum > 1: self.celebrate(0.5) - bs.timer(base_delay, bs.WeakCallStrict(self._start_next_wave)) + bs.timer(base_delay, bs.WeakCall(self._start_next_wave)) def _award_completion_bonus(self) -> None: self._cashregistersound.play() @@ -1170,7 +1166,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): def _start_time_bonus_timer(self) -> None: self._time_bonus_timer = bs.Timer( - 1.0, bs.WeakCallStrict(self._update_time_bonus), repeat=True + 1.0, bs.WeakCall(self._update_time_bonus), repeat=True ) def _update_player_spawn_info(self) -> None: @@ -1244,7 +1240,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): point = info.point if point is not None: assert bot_type_2 is not None - spcall = bs.WeakCallStrict( + spcall = bs.WeakCall( self.add_bot_at_point, point, bot_type_2, spawn_time ) bs.timer(tval, spcall) @@ -1253,7 +1249,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): spacing = info.spacing bot_angle += spacing * 0.5 if bot_type_2 is not None: - tcall = bs.WeakCallStrict( + tcall = bs.WeakCall( self.add_bot_at_angle, bot_angle, bot_type_2, spawn_time ) bs.timer(tval, tcall) @@ -1263,7 +1259,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): # We can end the wave after all the spawning happens. bs.timer( tval + spawn_time - dtime + 0.01, - bs.WeakCallStrict(self._set_can_end_wave), + bs.WeakCall(self._set_can_end_wave), ) def _start_next_wave(self) -> None: @@ -1322,7 +1318,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): ) ) - bs.timer(5.0, bs.WeakCallStrict(self._start_time_bonus_timer)) + bs.timer(5.0, bs.WeakCall(self._start_time_bonus_timer)) wtcolor = (1, 1, 1, 1) wttxt = bs.Lstr( value='${A} ${B}', @@ -1514,7 +1510,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): def _start_updating_waves(self) -> None: self._wave_update_timer = bs.Timer( - 2.0, bs.WeakCallStrict(self._update_waves), repeat=True + 2.0, bs.WeakCall(self._update_waves), repeat=True ) def _update_scores(self) -> None: @@ -1610,8 +1606,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): self._tnt_kills += 1 if self._tnt_kills >= 6: bs.timer( - 0.5, - bs.WeakCallStrict(self._award_achievement, 'TNT Terror'), + 0.5, bs.WeakCall(self._award_achievement, 'TNT Terror') ) def _handle_pro_kill_achievements(self, msg: SpazBotDiedMessage) -> None: @@ -1621,7 +1616,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]): if self._tnt_kills >= 3: bs.timer( 0.5, - bs.WeakCallStrict( + bs.WeakCall( self._award_achievement, 'Boom Goes the Dynamite' ), ) diff --git a/dist/ba_data/python/bascenev1lib/game/race.py b/dist/ba_data/python/bascenev1lib/game/race.py index aa48c29..41ff4f6 100644 --- a/dist/ba_data/python/bascenev1lib/game/race.py +++ b/dist/ba_data/python/bascenev1lib/game/race.py @@ -649,8 +649,7 @@ class RaceGame(bs.TeamGameActivity[Player, Team]): pos[2] + random.uniform(*z_range), ) bs.timer( - random.uniform(0.0, 2.0), - bs.WeakCallStrict(self._spawn_bomb_at_pos, pos), + random.uniform(0.0, 2.0), bs.WeakCall(self._spawn_bomb_at_pos, pos) ) def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None: @@ -691,7 +690,7 @@ class RaceGame(bs.TeamGameActivity[Player, Team]): assert rmine is not None if not rmine.mine: self._flash_mine(m_index) - bs.timer(0.95, bs.CallStrict(self._make_mine, m_index)) + bs.timer(0.95, bs.Call(self._make_mine, m_index)) @override def spawn_player(self, player: Player) -> bs.Actor: diff --git a/dist/ba_data/python/bascenev1lib/game/runaround.py b/dist/ba_data/python/bascenev1lib/game/runaround.py index 287f7c3..ddb14d6 100644 --- a/dist/ba_data/python/bascenev1lib/game/runaround.py +++ b/dist/ba_data/python/bascenev1lib/game/runaround.py @@ -576,7 +576,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): for _i in range(4): bs.timer( delay, - bs.CallStrict( + bs.Call( _safesetattr, self._lives_text.node, 'color', @@ -587,14 +587,12 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): assert self._lives_bg.node bs.timer( delay, - bs.CallStrict( - _safesetattr, self._lives_bg.node, 'opacity', 0.5 - ), + bs.Call(_safesetattr, self._lives_bg.node, 'opacity', 0.5), ) delay += 0.125 bs.timer( delay, - bs.CallStrict( + bs.Call( _safesetattr, self._lives_text.node, 'color', @@ -603,14 +601,12 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): ) bs.timer( delay, - bs.CallStrict( - _safesetattr, self._lives_bg.node, 'opacity', 1.0 - ), + bs.Call(_safesetattr, self._lives_bg.node, 'opacity', 1.0), ) delay += 0.125 bs.timer( delay, - bs.CallStrict( + bs.Call( _safesetattr, self._lives_text.node, 'color', @@ -670,7 +666,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): for i in range(len(points)): bs.timer( 1.0 + i * 0.5, - bs.CallStrict( + bs.Call( self._drop_powerup, i, force_first if i == 0 else None ), ) @@ -701,7 +697,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): def end_game(self) -> None: # (Pylint Bug?) pylint: disable=missing-function-docstring - bs.pushcall(bs.CallStrict(self.do_end, 'defeat')) + bs.pushcall(bs.Call(self.do_end, 'defeat')) bs.setmusic(None) self._player_death_sound.play() @@ -758,7 +754,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): bs.timer(0, self._cashregistersound.play) bs.timer( base_delay, - bs.CallStrict(self._award_time_bonus, self._time_bonus), + bs.Call(self._award_time_bonus, self._time_bonus), ) base_delay += 1.0 @@ -804,7 +800,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): bs.cameraflash() bs.setmusic(bs.MusicType.VICTORY) self._game_over = True - bs.timer(base_delay, bs.CallStrict(self.do_end, 'victory')) + bs.timer(base_delay, bs.Call(self.do_end, 'victory')) return self._wavenum += 1 @@ -1106,7 +1102,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): delay = base_delay delay /= self._get_bot_speed(bot_type) t_sec += delay * 0.5 - tcall = bs.CallStrict( + tcall = bs.Call( self.add_bot_at_point, point, bot_type, @@ -1223,7 +1219,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): spaztype, pos=pos, spawn_time=spawn_time, - on_spawn_call=bs.CallPartial(self._on_bot_spawn, path), + on_spawn_call=bs.Call(self._on_bot_spawn, path), ) def _update_time_bonus(self) -> None: @@ -1351,7 +1347,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]): assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 player.respawn_timer = bs.Timer( - respawn_time, bs.CallStrict(self.spawn_player_if_exists, player) + respawn_time, bs.Call(self.spawn_player_if_exists, player) ) player.respawn_icon = RespawnIcon(player, respawn_time) diff --git a/dist/ba_data/python/bascenev1lib/game/targetpractice.py b/dist/ba_data/python/bascenev1lib/game/targetpractice.py index 805f1f5..ae0b3c1 100644 --- a/dist/ba_data/python/bascenev1lib/game/targetpractice.py +++ b/dist/ba_data/python/bascenev1lib/game/targetpractice.py @@ -398,6 +398,6 @@ class Target(bs.Actor): 1, {0.9: self._nodes[2].size, 1.1: [0.0]}, ) - bs.timer(1.1, bs.CallStrict(self.handlemessage, bs.DieMessage())) + bs.timer(1.1, bs.Call(self.handlemessage, bs.DieMessage())) return bullseye diff --git a/dist/ba_data/python/bascenev1lib/game/thelaststand.py b/dist/ba_data/python/bascenev1lib/game/thelaststand.py index ce0d058..7cca417 100644 --- a/dist/ba_data/python/bascenev1lib/game/thelaststand.py +++ b/dist/ba_data/python/bascenev1lib/game/thelaststand.py @@ -127,8 +127,8 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]): # Spit out a few powerups and start dropping more shortly. self._drop_powerups(standard_points=True) - bs.timer(2.0, bs.WeakCallStrict(self._start_powerup_drops)) - bs.timer(0.001, bs.WeakCallStrict(self._start_bot_updates)) + bs.timer(2.0, bs.WeakCall(self._start_powerup_drops)) + bs.timer(0.001, bs.WeakCall(self._start_bot_updates)) self.setup_low_life_warning_sound() self._update_scores() self._tntspawner = TNTSpawner( @@ -155,7 +155,7 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]): if len(self.players) > 3: self._update_bots() self._bot_update_timer = bs.Timer( - self._bot_update_interval, bs.WeakCallStrict(self._update_bots) + self._bot_update_interval, bs.WeakCall(self._update_bots) ) def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: @@ -170,7 +170,7 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]): def _start_powerup_drops(self) -> None: self._powerup_drop_timer = bs.Timer( - 3.0, bs.WeakCallStrict(self._drop_powerups), repeat=True + 3.0, bs.WeakCall(self._drop_powerups), repeat=True ) def _drop_powerups( @@ -184,7 +184,7 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]): for i in range(len(pts)): bs.timer( 1.0 + i * 0.5, - bs.WeakCallStrict( + bs.WeakCall( self._drop_powerup, i, force_first if i == 0 else None ), ) @@ -227,7 +227,7 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]): assert self._bot_update_interval is not None self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98) self._bot_update_timer = bs.Timer( - self._bot_update_interval, bs.WeakCallStrict(self._update_bots) + self._bot_update_interval, bs.WeakCall(self._update_bots) ) botspawnpts: list[Sequence[float]] = [ [-5.0, 5.5, -4.14], @@ -346,7 +346,7 @@ class TheLastStandGame(bs.CoopGameActivity[Player, Team]): # Tell our bots to celebrate just to rub it in. self._bots.final_celebrate() bs.setmusic(None) - bs.pushcall(bs.WeakCallStrict(self.do_end, 'defeat')) + bs.pushcall(bs.WeakCall(self.do_end, 'defeat')) def _checkroundover(self) -> None: """End the round if conditions are met.""" diff --git a/dist/ba_data/python/bascenev1lib/mainmenu.py b/dist/ba_data/python/bascenev1lib/mainmenu.py index 35d25a5..760da9a 100644 --- a/dist/ba_data/python/bascenev1lib/mainmenu.py +++ b/dist/ba_data/python/bascenev1lib/mainmenu.py @@ -16,9 +16,7 @@ import bauiv1 as bui if TYPE_CHECKING: from typing import Any - import bacommon.classic - - from bascenev1lib.actor.spazbot import DemoSpazBotSet + import bacommon.bs class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): @@ -38,9 +36,12 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): self.version: bs.NodeActor | None = None self.beta_info: bs.NodeActor | None = None self.beta_info_2: bs.NodeActor | None = None - self.map: bs.Map | None = None - self.bot_sets: list[DemoSpazBotSet] = [] + self.bottom: bs.NodeActor | None = None + self.vr_bottom_fill: bs.NodeActor | None = None + self.vr_top_fill: bs.NodeActor | None = None + self.terrain: bs.NodeActor | None = None self.trees: bs.NodeActor | None = None + self.bgterrain: bs.NodeActor | None = None self._ts = 0.86 self._language: str | None = None self._update_timer: bs.Timer | None = None @@ -50,13 +51,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): @override def on_transition_in(self) -> None: + # pylint: disable=too-many-locals super().on_transition_in() - - from bascenev1lib.maps import ThePad - - ThePad.preload() - self.map = ThePad(main_menu_style=True) - random.seed(123) app = bs.app env = app.env @@ -112,8 +108,17 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): assert self.beta_info.node bs.animate(self.beta_info.node, 'opacity', {1.3: 0, 1.8: 1.0}) + mesh = bs.getmesh('thePadLevel') trees_mesh = bs.getmesh('trees') + bottom_mesh = bs.getmesh('thePadLevelBottom') + color_texture = bs.gettexture('thePadLevelColor') trees_texture = bs.gettexture('treesColor') + bgtex = bs.gettexture('menuBG') + bgmesh = bs.getmesh('thePadBG') + + # Load these last since most platforms don't use them. + vr_bottom_fill_mesh = bs.getmesh('thePadVRFillBottom') + vr_top_fill_mesh = bs.getmesh('thePadVRFillTop') gnode = self.globalsnode gnode.camera_mode = 'rotate' @@ -124,6 +129,51 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): gnode.vignette_outer = (0.45, 0.55, 0.54) gnode.vignette_inner = (0.99, 0.98, 0.98) + self.bottom = bs.NodeActor( + bs.newnode( + 'terrain', + attrs={ + 'mesh': bottom_mesh, + 'lighting': False, + 'reflection': 'soft', + 'reflection_scale': [0.45], + 'color_texture': color_texture, + }, + ) + ) + self.vr_bottom_fill = bs.NodeActor( + bs.newnode( + 'terrain', + attrs={ + 'mesh': vr_bottom_fill_mesh, + 'lighting': False, + 'vr_only': True, + 'color_texture': color_texture, + }, + ) + ) + self.vr_top_fill = bs.NodeActor( + bs.newnode( + 'terrain', + attrs={ + 'mesh': vr_top_fill_mesh, + 'vr_only': True, + 'lighting': False, + 'color_texture': bgtex, + }, + ) + ) + self.terrain = bs.NodeActor( + bs.newnode( + 'terrain', + attrs={ + 'mesh': mesh, + 'color_texture': color_texture, + 'reflection': 'soft', + 'reflection_scale': [0.3], + }, + ) + ) self.trees = bs.NodeActor( bs.newnode( 'terrain', @@ -136,12 +186,24 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): }, ) ) + self.bgterrain = bs.NodeActor( + bs.newnode( + 'terrain', + attrs={ + 'mesh': bgmesh, + 'color': (0.92, 0.91, 0.9), + 'lighting': False, + 'background': True, + 'color_texture': bgtex, + }, + ) + ) self._update_timer = bs.Timer(0.1, self._update, repeat=True) self._update() # Hopefully this won't hitch but lets space these out anyway. - bs.add_clean_frame_callback(bs.WeakCallStrict(self._start_preloads)) + bs.add_clean_frame_callback(bs.WeakCall(self._start_preloads)) random.seed() @@ -659,7 +721,7 @@ class NewsDisplay: # If we're signed in, fetch news immediately. Otherwise wait # until we are signed in. self._fetch_timer: bs.Timer | None = bs.Timer( - 1.0, bs.WeakCallStrict(self._try_fetching_news), repeat=True + 1.0, bs.WeakCall(self._try_fetching_news), repeat=True ) self._try_fetching_news() @@ -773,7 +835,7 @@ class NewsDisplay: ] self._phrase_change_timer = bs.Timer( (self._message_duration + self._message_spacing), - bs.WeakCallStrict(self._change_phrase), + bs.WeakCall(self._change_phrase), repeat=True, ) diff --git a/dist/ba_data/python/bascenev1lib/maps.py b/dist/ba_data/python/bascenev1lib/maps.py index 23fcc78..c6aa225 100644 --- a/dist/ba_data/python/bascenev1lib/maps.py +++ b/dist/ba_data/python/bascenev1lib/maps.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Standard maps.""" - # pylint: disable=too-many-lines from __future__ import annotations @@ -784,47 +783,26 @@ class ThePad(bs.Map): # fixme should chop this into vr/non-vr sections for efficiency return data - def __init__(self, main_menu_style: bool = False) -> None: + def __init__(self) -> None: super().__init__() shared = SharedObjects.get() self.node = bs.newnode( 'terrain', delegate=self, - attrs=( - { - 'collision_mesh': self.preloaddata['collision_mesh'], - 'mesh': self.preloaddata['mesh'], - 'color_texture': self.preloaddata['tex'], - 'materials': [shared.footing_material], - 'reflection': 'soft', - 'reflection_scale': [0.3], - } - if main_menu_style - else { - 'collision_mesh': self.preloaddata['collision_mesh'], - 'mesh': self.preloaddata['mesh'], - 'color_texture': self.preloaddata['tex'], - 'materials': [shared.footing_material], - } - ), + attrs={ + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material], + }, ) self.bottom = bs.newnode( 'terrain', - attrs=( - { - 'mesh': self.preloaddata['bottom_mesh'], - 'lighting': False, - 'color_texture': self.preloaddata['tex'], - 'reflection': 'soft', - 'reflection_scale': [0.45], - } - if main_menu_style - else { - 'mesh': self.preloaddata['bottom_mesh'], - 'lighting': False, - 'color_texture': self.preloaddata['tex'], - } - ), + attrs={ + 'mesh': self.preloaddata['bottom_mesh'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'], + }, ) self.background = bs.newnode( 'terrain', diff --git a/dist/ba_data/python/bascenev1lib/tutorial.py b/dist/ba_data/python/bascenev1lib/tutorial.py index 2d09a58..341ec59 100644 --- a/dist/ba_data/python/bascenev1lib/tutorial.py +++ b/dist/ba_data/python/bascenev1lib/tutorial.py @@ -71,8 +71,8 @@ class ButtonPress: img = a.pickup_image color = a.pickup_image_color elif self._button == 'run': - call = bs.CallStrict(s.on_run, 1.0) - release_call = bs.CallStrict(s.on_run, 0.0) + call = bs.Call(s.on_run, 1.0) + release_call = bs.Call(s.on_run, 0.0) img = None color = None else: @@ -97,11 +97,11 @@ class ButtonPress: if img is not None: bs.timer( self._delay / 1000.0, - bs.CallStrict(_safesetattr, img, 'color', c_bright), + bs.Call(_safesetattr, img, 'color', c_bright), ) bs.timer( self._delay / 1000.0, - bs.CallStrict(_safesetattr, img, 'vr_depth', -30), + bs.Call(_safesetattr, img, 'vr_depth', -30), ) if self._release: if self._delay == 0 and self._release_delay == 0: @@ -113,11 +113,11 @@ class ButtonPress: if img is not None: bs.timer( (self._delay + self._release_delay + 100) / 1000.0, - bs.CallStrict(_safesetattr, img, 'color', color), + bs.Call(_safesetattr, img, 'color', color), ) bs.timer( (self._delay + self._release_delay + 100) / 1000.0, - bs.CallStrict(_safesetattr, img, 'vr_depth', -20), + bs.Call(_safesetattr, img, 'vr_depth', -20), ) @@ -149,7 +149,7 @@ class ButtonRelease: img = a.pickup_image color = a.pickup_image_color elif self._button == 'run': - call = bs.CallStrict(s.on_run, 0.0) + call = bs.Call(s.on_run, 0.0) img = None color = None else: @@ -161,11 +161,11 @@ class ButtonRelease: if img is not None: bs.timer( (self._delay + 100) / 1000.0, - bs.CallStrict(_safesetattr, img, 'color', color), + bs.Call(_safesetattr, img, 'color', color), ) bs.timer( (self._delay + 100 / 1000.0), - bs.CallStrict(_safesetattr, img, 'vr_depth', -20), + bs.Call(_safesetattr, img, 'vr_depth', -20), ) @@ -2429,7 +2429,7 @@ class TutorialActivity(bs.Activity[Player, Team]): # Otherwise try again in a few seconds. else: self._read_entries_timer = bs.Timer( - 3.0, bs.WeakCallStrict(self._read_entries) + 3.0, bs.WeakCall(self._read_entries) ) def _run_next_entry(self) -> None: @@ -2445,13 +2445,13 @@ class TutorialActivity(bs.Activity[Player, Team]): # otherwise just keep going. if result is not None: self._entry_timer = bs.Timer( - result / 1000.0, bs.WeakCallStrict(self._run_next_entry) + result / 1000.0, bs.WeakCall(self._run_next_entry) ) return # Done with these entries.. start over soon. self._read_entries_timer = bs.Timer( - 1.0, bs.WeakCallStrict(self._read_entries) + 1.0, bs.WeakCall(self._read_entries) ) def _update_skip_votes(self) -> None: @@ -2502,17 +2502,15 @@ class TutorialActivity(bs.Activity[Player, Team]): for _i in range(6): bs.timer( t / 1000.0, - bs.CallStrict( - setattr, self._skip_text, 'color', (1, 0.5, 0.1) - ), + bs.Call(setattr, self._skip_text, 'color', (1, 0.5, 0.1)), ) t += incr bs.timer( t / 1000.0, - bs.CallStrict(setattr, self._skip_text, 'color', (1, 1, 0)), + bs.Call(setattr, self._skip_text, 'color', (1, 1, 0)), ) t += incr - bs.timer(6.0, bs.WeakCallStrict(self._revert_confirm)) + bs.timer(6.0, bs.WeakCall(self._revert_confirm)) return player.pressed = True @@ -2548,7 +2546,7 @@ class TutorialActivity(bs.Activity[Player, Team]): bs.InputType.BOMB_PRESS, bs.InputType.PICK_UP_PRESS, ), - bs.CallStrict(self._player_pressed_button, player), + bs.Call(self._player_pressed_button, player), ) @override diff --git a/dist/ba_data/python/batemplatefs/_appsubsystem.py b/dist/ba_data/python/batemplatefs/_appsubsystem.py index 8ba09bd..74a6ebd 100644 --- a/dist/ba_data/python/batemplatefs/_appsubsystem.py +++ b/dist/ba_data/python/batemplatefs/_appsubsystem.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides the TemplateFs App-Subsystem.""" - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/dist/ba_data/python/bauiv1/__init__.py b/dist/ba_data/python/bauiv1/__init__.py index 8a06a83..a75bd09 100644 --- a/dist/ba_data/python/bauiv1/__init__.py +++ b/dist/ba_data/python/bauiv1/__init__.py @@ -14,6 +14,8 @@ from __future__ import annotations # pylint: disable=redefined-builtin +import logging + from babase import ( accountlog, AccountV2Handle, @@ -36,8 +38,6 @@ from babase import ( asset_loads_allowed, balog, Call, - CallPartial, - CallStrict, DevConsoleButtonDef, DevConsoleTab, DevConsoleTabEntry, @@ -117,8 +117,6 @@ from babase import ( unlock_all_input, utc_now_cloud, WeakCall, - WeakCallPartial, - WeakCallStrict, workspaces_in_use, ) @@ -141,6 +139,7 @@ from _bauiv1 import ( root_ui_resume_updates, rowwidget, scrollwidget, + set_party_window_open, spinnerwidget, Sound, Texture, @@ -150,11 +149,11 @@ from _bauiv1 import ( widget, widget_by_id, ) +from bauiv1._cloudui import show_cloud_ui_window from bauiv1._keyboard import Keyboard from bauiv1._uitypes import ( uicleanupcheck, RootUIUpdatePause, - UIOpenState, ) from bauiv1._appsubsystem import UIV1AppSubsystem from bauiv1._window import ( @@ -190,8 +189,6 @@ __all__ = [ 'BasicMainWindowState', 'buttonwidget', 'Call', - 'CallPartial', - 'CallStrict', 'DevConsoleButtonDef', 'DevConsoleTab', 'DevConsoleTabEntry', @@ -280,7 +277,9 @@ __all__ = [ 'scrollwidget', 'set_analytics_screen', 'set_low_level_config_value', + 'set_party_window_open', 'set_main_ui_input_device', + 'show_cloud_ui_window', 'shutdown_suppress_begin', 'shutdown_suppress_end', 'Sound', @@ -295,14 +294,11 @@ __all__ = [ 'uibounds', 'uicleanupcheck', 'uilog', - 'UIOpenState', 'UIScale', 'UIV1AppSubsystem', 'unlock_all_input', 'utc_now_cloud', 'WeakCall', - 'WeakCallPartial', - 'WeakCallStrict', 'widget', 'widget_by_id', 'Widget', @@ -317,7 +313,7 @@ __all__ = [ if __debug__: for _mdl in 'babase', '_babase': if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): - balog.warning( + logging.warning( '%s was imported before %s finished importing;' ' should not happen.', __name__, diff --git a/dist/ba_data/python/bauiv1/_appsubsystem.py b/dist/ba_data/python/bauiv1/_appsubsystem.py index a34892e..72bc0dd 100644 --- a/dist/ba_data/python/bauiv1/_appsubsystem.py +++ b/dist/ba_data/python/bauiv1/_appsubsystem.py @@ -157,7 +157,6 @@ class UIV1AppSubsystem(babase.AppSubsystem): window: bauiv1.MainWindow, *, back_state: MainWindowState | None, - extra_type_id: str = '', from_window: bauiv1.MainWindow | None | bool = True, is_back: bool = False, is_top_level: bool = False, @@ -229,7 +228,6 @@ class UIV1AppSubsystem(babase.AppSubsystem): back_state.is_top_level is None or back_state.is_auxiliary is None or back_state.window_type is None - or back_state.extra_type_id is None ): raise RuntimeError( 'Provided back_state is incomplete.' @@ -294,18 +292,15 @@ class UIV1AppSubsystem(babase.AppSubsystem): assert back_state.is_top_level is not None assert back_state.is_auxiliary is not None assert back_state.window_type is type(window) - assert back_state.extra_type_id is not None window.main_window_back_state = back_state.parent window.main_window_is_top_level = back_state.is_top_level window.main_window_is_auxiliary = back_state.is_auxiliary - window.main_window_extra_type_id = back_state.extra_type_id else: # Store if the window is top-level so we won't complain # later if we go back from it and there's nowhere to go to. window.main_window_is_top_level = is_top_level window.main_window_is_auxiliary = is_auxiliary - window.main_window_extra_type_id = extra_type_id # When navigating forward, generate a back-window-state from # the outgoing window. @@ -359,19 +354,19 @@ class UIV1AppSubsystem(babase.AppSubsystem): winstate.is_top_level = window.main_window_is_top_level winstate.is_auxiliary = window.main_window_is_auxiliary winstate.window_type = type(window) - winstate.extra_type_id = window.main_window_extra_type_id return winstate def save_current_main_window_state(self) -> MainWindowState | None: """Save state for the current window, if any.""" - # Calc a state from the current window. + # Calc a back-state from the current window. current_main_win = self._main_window() if current_main_win is None: - # We currenty only hold weak refs to windows so that they - # are free to die on their own, but we expect the main menu - # window to keep itself alive as long as its the main one. - # Holler if that seems to not be happening. + # We currenty only hold weak refs to windows so that + # they are free to die on their own, but we expect + # the main menu window to keep itself alive as long + # as its the main one. Holler if that seems to not + # be happening. babase.uilog.warning( 'save_current_main_window_state: No old MainWindow found;' ' this should not happen.' @@ -389,7 +384,6 @@ class UIV1AppSubsystem(babase.AppSubsystem): assert state.is_top_level is not None assert state.is_auxiliary is not None assert state.window_type is not None - assert state.extra_type_id is not None win = state.create_window(transition=None) self.set_main_window( @@ -399,7 +393,6 @@ class UIV1AppSubsystem(babase.AppSubsystem): is_auxiliary=state.is_auxiliary, back_state=state.parent, suppress_warning=True, - extra_type_id=state.extra_type_id, ) def should_suppress_window_recreates(self) -> bool: @@ -473,7 +466,6 @@ class UIV1AppSubsystem(babase.AppSubsystem): self, win_type: type[bauiv1.MainWindow], win_create_call: Callable[[], bauiv1.MainWindow], - win_extra_type_id: str = '', ) -> None: """Navigate to or away from an Auxiliary window. @@ -482,12 +474,12 @@ class UIV1AppSubsystem(babase.AppSubsystem): ranking windows that the user might want to visit without losing their place in the regular hierarchy. - If an auxiliary window matching the provided type and - extra-type-id exists in the stack, this call will back out past - it (think of it as toggling the side-quest back off). + Calling this method with a MainWindow of the provided type + already in the stack will back out past it (effectively toggling + the 'side quest' back off). - If a non-matching auxiliary window exists in the stack, this - call will back out past that and replace it with this + Calling this method with a *different* auxiliary window in the + stack will back out past that and replace it with this (effectively ending the old side-quest and starting a new one). """ # pylint: disable=unidiomatic-typecheck @@ -507,12 +499,8 @@ class UIV1AppSubsystem(babase.AppSubsystem): state = current_main_window.main_window_back_state while state is not None: assert state.window_type is not None - assert state.extra_type_id is not None if state.is_auxiliary: - if ( - state.window_type is win_type - and state.extra_type_id == win_extra_type_id - ): + if state.window_type is win_type: aux_matching_state = state else: aux_state = state @@ -530,7 +518,7 @@ class UIV1AppSubsystem(babase.AppSubsystem): current_main_window.main_window_back() return - # If there's an ancestor auxiliary state *not* matching our + # If there's an ancestory auxiliary state *not* matching our # type, crop the state and swap in our new auxiliary UI # (example: poking settings, then poking account, then poking # back should end up where things were before the settings @@ -544,22 +532,19 @@ class UIV1AppSubsystem(babase.AppSubsystem): back_state=aux_state.parent, suppress_warning=True, is_auxiliary=True, - extra_type_id=win_extra_type_id, ) return # Ok, no auxiliary states found. Now if current window is - # auxiliary and the type/extra-id matches, simply do a back. + # auxiliary and the type matches, simply do a back. if ( current_main_window.main_window_is_auxiliary and type(current_main_window) is win_type - and current_main_window.main_window_extra_type_id - == win_extra_type_id ): current_main_window.main_window_back() return - # If current window is auxiliary but type/extra-id doesn't match, + # If current window is auxiliary but type doesn't match, # swap it out for our new auxiliary UI. if current_main_window.main_window_is_auxiliary: self.clear_main_window() @@ -569,25 +554,15 @@ class UIV1AppSubsystem(babase.AppSubsystem): back_state=current_main_window.main_window_back_state, suppress_warning=True, is_auxiliary=True, - extra_type_id=win_extra_type_id, ) return # Ok, no existing auxiliary stuff was found period. Just # navigate forward to this UI. - new_main_win = current_main_window.main_window_replace( - win_create_call, - is_auxiliary=True, - extra_type_id=win_extra_type_id, + current_main_window.main_window_replace( + win_create_call, is_auxiliary=True ) - # We should always be allowed to replace the main win in this - # case. - assert new_main_win is not None - - # Make sure what got made exactly matches the type we were passed. - assert type(new_main_win) is win_type - def _schedule_main_win_recreate(self) -> None: # If there is a timer set already, do nothing. diff --git a/dist/ba_data/python/bauiv1/_cloudui.py b/dist/ba_data/python/bauiv1/_cloudui.py new file mode 100644 index 0000000..642286c --- /dev/null +++ b/dist/ba_data/python/bauiv1/_cloudui.py @@ -0,0 +1,304 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UIs provided by the cloud (similar-ish to html in concept).""" + +from __future__ import annotations + +import random +from dataclasses import dataclass +from typing import TYPE_CHECKING, override, Annotated + +from efro.dataclassio import ioprepped, IOAttrs +import babase +from bauiv1._window import MainWindow, BasicMainWindowState +import _bauiv1 + + +if TYPE_CHECKING: + from bauiv1._window import MainWindowState + + +def show_cloud_ui_window() -> None: + """Bust out a cloud-ui window.""" + + # Pop up an auxiliary window wherever we are in the nav stack. + babase.app.ui_v1.auxiliary_window_activate( + win_type=CloudUIWindow, + win_create_call=lambda: CloudUIWindow(state=None), + ) + + +@ioprepped +@dataclass +class CloudUIButton: + """Represents a button in a cloud-ui.""" + + +@ioprepped +@dataclass +class CloudUIRow: + """Represents a row in a cloud-ui.""" + + buttons: Annotated[list[CloudUIButton], IOAttrs('b')] + + +@ioprepped +@dataclass +class CloudUIRoot: + """Represents an entire cloud-ui.""" + + title: Annotated[str, IOAttrs('t')] + rows: Annotated[list[CloudUIRow], IOAttrs('r')] + + +class CloudUIWindow(MainWindow): + """An example of a well-behaved main-window.""" + + @dataclass + class _State: + root: CloudUIRoot | None + + def __init__( + self, + state: _State | None, + *, + transition: str | None = 'in_right', + origin_widget: _bauiv1.Widget | None = None, + auxiliary_style: bool = True, + ): + ui = babase.app.ui_v1 + + self._state: CloudUIWindow._State | None = None + + # We want to display differently whether we're an auxiliary + # window or not, but unfortunately that value is not yet + # available until we're added to the main-window-stack so it + # must be explicitly passed in. + self._auxiliary_style = auxiliary_style + + # Calc scale and size for our backing window. For medium & large + # ui-scale we aim for a window small enough to always be fully + # visible on-screen and for small mode we aim for a window big + # enough that we never see the window edges; only the window + # texture covering the whole screen. + uiscale = ui.uiscale + self._width = 1400 if uiscale is babase.UIScale.SMALL else 750 + self._height = 1200 if uiscale is babase.UIScale.SMALL else 500 + scale = ( + 1.5 + if uiscale is babase.UIScale.SMALL + else 1.2 if uiscale is babase.UIScale.MEDIUM else 1.0 + ) + + # Do some fancy math to calculate our visible area; this will be + # limited by the screen size in small mode and our backing size + # otherwise. + screensize = babase.get_virtual_screen_size() + self._vis_width = min(self._width - 100, screensize[0] / scale) + self._vis_height = min(self._height - 100, screensize[1] / scale) + self._vis_top = 0.5 * self._height + 0.5 * self._vis_height + self._vis_left = 0.5 * self._width - 0.5 * self._vis_width + + # Nudge our vis area up a bit when we can see the full backing + # (visual fudge factor). + if uiscale is not babase.UIScale.SMALL: + self._vis_top += 12.0 + + super().__init__( + root_widget=_bauiv1.containerwidget( + size=(self._width, self._height), + toolbar_visibility='menu_full', + toolbar_cancel_button_style=( + 'close' if auxiliary_style else 'back' + ), + scale=scale, + ), + transition=transition, + origin_widget=origin_widget, + # We respond to screen size changes only at small ui-scale; + # in other cases we assume our window remains fully visible + # always (flip to windowed mode and resize the app window to + # confirm this). + refresh_on_screen_size_changes=uiscale is babase.UIScale.SMALL, + ) + # Avoid complaints if nothing is selected under us. + _bauiv1.widget(edit=self._root_widget, allow_preserve_selection=False) + + # Title. + self._title = _bauiv1.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._vis_top - 20), + size=(0, 0), + text='', + color=ui.title_color, + scale=0.9 if uiscale is babase.UIScale.SMALL else 1.0, + # Make sure we avoid overlapping meters in small mode. + maxwidth=(130 if uiscale is babase.UIScale.SMALL else 200), + h_align='center', + v_align='center', + ) + + # For small UI-scale we use the system back/close button; + # otherwise we make our own. + if uiscale is babase.UIScale.SMALL: + _bauiv1.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + else: + btn = _bauiv1.buttonwidget( + parent=self._root_widget, + id=f'{self.main_window_id_prefix}|close', + scale=0.8, + position=(self._vis_left - 15, self._vis_top - 30), + size=(50, 50) if auxiliary_style else (60, 55), + extra_touch_border_scale=2.0, + button_type=None if auxiliary_style else 'backSmall', + on_activate_call=self.main_window_back, + autoselect=True, + label=babase.charstr( + babase.SpecialChar.CLOSE + if auxiliary_style + else babase.SpecialChar.BACK + ), + ) + _bauiv1.containerwidget(edit=self._root_widget, cancel_button=btn) + + # Show our vis-area bounds (for debugging). + if bool(True): + # Skip top-left since its always overlapping back/close + # buttons. + if bool(False): + _bauiv1.textwidget( + parent=self._root_widget, + position=(self._vis_left, self._vis_top), + size=(0, 0), + color=(1, 1, 1, 0.5), + scale=0.5, + text='TL', + h_align='left', + v_align='top', + ) + _bauiv1.textwidget( + parent=self._root_widget, + position=(self._vis_left + self._vis_width, self._vis_top), + size=(0, 0), + color=(1, 1, 1, 0.5), + scale=0.5, + text='TR', + h_align='right', + v_align='top', + ) + _bauiv1.textwidget( + parent=self._root_widget, + position=(self._vis_left, self._vis_top - self._vis_height), + size=(0, 0), + color=(1, 1, 1, 0.5), + scale=0.5, + text='BL', + h_align='left', + v_align='bottom', + ) + _bauiv1.textwidget( + parent=self._root_widget, + position=( + self._vis_left + self._vis_width, + self._vis_top - self._vis_height, + ), + size=(0, 0), + scale=0.5, + color=(1, 1, 1, 0.5), + text='BR', + h_align='right', + v_align='bottom', + ) + + self._spinner: _bauiv1.Widget | None = _bauiv1.spinnerwidget( + parent=self._root_widget, + position=( + self._vis_left + self._vis_width * 0.5, + self._vis_top - self._vis_height * 0.5, + ), + size=48, + style='bomb', + ) + + if state is not None: + self._set_state(state) + else: + if random.random() < 0.3: + babase.apptimer(1.0, babase.WeakCall(self._on_error_response)) + else: + babase.apptimer(1.0, babase.WeakCall(self._on_response)) + + def _on_error_response(self) -> None: + self._set_state(self._State(None)) + + def _on_response(self) -> None: + self._set_state(self._State(CloudUIRoot(title='Testing', rows=[]))) + + def _set_state(self, state: _State) -> None: + """Set a final state (error or page contents). + + This state may be instantly restored if the window is recreated + (depending on cache lifespan/etc.) + """ + + assert self._state is None + self._state = state + + if self._spinner: + self._spinner.delete() + self._spinner = None + + if self._state.root is None: + _bauiv1.textwidget( + edit=self._title, + literal=False, # Allow Lstr. + text=babase.Lstr(resource='errorText'), + ) + _bauiv1.textwidget( + parent=self._root_widget, + position=( + self._vis_left + 0.5 * self._vis_width, + self._vis_top - 0.5 * self._vis_height, + ), + size=(0, 0), + scale=0.6, + text=babase.Lstr(resource='store.loadErrorText'), + h_align='center', + v_align='center', + ) + else: + _bauiv1.textwidget( + edit=self._title, + literal=True, # Never interpret as Lstr. + text=self._state.root.title, + ) + + @override + def get_main_window_state(self) -> MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + # IMPORTANT - Pull values from self HERE; if we do it in the + # lambda below it'll keep self alive which will lead to + # 'ui-not-getting-cleaned-up' warnings and memory leaks. + auxiliary_style = self._auxiliary_style + state = self._state + + return BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + state=state, + transition=transition, + origin_widget=origin_widget, + auxiliary_style=auxiliary_style, + ), + ) + + @override + def main_window_should_preserve_selection(self) -> bool: + return True + + @override + def get_main_window_shared_state_id(self) -> str | None: + return 'cloudui' diff --git a/dist/ba_data/python/bauiv1/_hooks.py b/dist/ba_data/python/bauiv1/_hooks.py index 642b5e7..872f872 100644 --- a/dist/ba_data/python/bauiv1/_hooks.py +++ b/dist/ba_data/python/bauiv1/_hooks.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Snippets of code for use by the c++ layer.""" - # (most of these are self-explanatory) # pylint: disable=missing-function-docstring from __future__ import annotations diff --git a/dist/ba_data/python/bauiv1/_uitypes.py b/dist/ba_data/python/bauiv1/_uitypes.py index ffb7698..3184da7 100644 --- a/dist/ba_data/python/bauiv1/_uitypes.py +++ b/dist/ba_data/python/bauiv1/_uitypes.py @@ -75,23 +75,3 @@ class RootUIUpdatePause: def __del__(self) -> None: _bauiv1.root_ui_resume_updates() - - -class UIOpenState: - """Keeps ui informed that something is open. - - Generally instances of this are assigned as a class member of some - UI class, which will then keep the UI informed upon its death. - - It is valid to have multiple states for one tag; the UI will keep a - tally. - """ - - __slots__ = ['stateid'] - - def __init__(self, stateid: str) -> None: - self.stateid = stateid - _bauiv1.ui_open_state_change(self.stateid, 1) - - def __del__(self) -> None: - _bauiv1.ui_open_state_change(self.stateid, -1) diff --git a/dist/ba_data/python/bauiv1/_window.py b/dist/ba_data/python/bauiv1/_window.py index 0827777..7c65a90 100644 --- a/dist/ba_data/python/bauiv1/_window.py +++ b/dist/ba_data/python/bauiv1/_window.py @@ -100,8 +100,6 @@ class MainWindow(Window): # old ones as back targets. self.main_window_is_auxiliary: bool = False - self.main_window_extra_type_id = '' - self._main_window_transition = transition self._main_window_origin_widget = origin_widget super().__init__( @@ -350,7 +348,6 @@ class MainWindow(Window): assert back_state.is_top_level is not None assert back_state.is_auxiliary is not None assert back_state.window_type is not None - assert back_state.extra_type_id is not None # When leaving an auxiliary window, scale the destination # window in instead of sliding to convey that its more of a @@ -367,7 +364,6 @@ class MainWindow(Window): is_back=True, back_state=back_state, suppress_warning=True, - extra_type_id=back_state.extra_type_id, ) def main_window_replace( @@ -375,7 +371,6 @@ class MainWindow(Window): new_window: MainWindow | Callable[[], MainWindow], back_state: MainWindowState | None = None, is_auxiliary: bool = False, - extra_type_id: str = '', ) -> MainWindow | None: """Replace ourself with a new MainWindow. @@ -454,7 +449,6 @@ class MainWindow(Window): from_window=self, back_state=back_state, is_auxiliary=is_auxiliary, - extra_type_id=extra_type_id, suppress_warning=True, ) return new_window @@ -548,7 +542,6 @@ class MainWindowState: self.is_top_level: bool | None = None self.is_auxiliary: bool | None = None self.window_type: type[MainWindow] | None = None - self.extra_type_id: str | None = None def create_window( self, @@ -579,15 +572,10 @@ class BasicMainWindowState(MainWindowState): ], bauiv1.MainWindow, ], - uiopenstate: bauiv1.UIOpenState | None = None, ) -> None: super().__init__() self.create_call = create_call - # We simply need to hold on to this to keep the ui-open-state - # alive. - self.uiopenstate = uiopenstate - @override def create_window( self, diff --git a/dist/ba_data/python/bauiv1/onscreenkeyboard.py b/dist/ba_data/python/bauiv1/onscreenkeyboard.py index bbb3018..c9f775c 100644 --- a/dist/ba_data/python/bauiv1/onscreenkeyboard.py +++ b/dist/ba_data/python/bauiv1/onscreenkeyboard.py @@ -245,9 +245,7 @@ class OnScreenKeyboardWindow(Window): textcolor=key_textcolor, color=key_color_dark, label=babase.Lstr(resource='spaceKeyText'), - on_activate_call=babase.CallStrict( - self._type_char, ' ' - ), + on_activate_call=babase.Call(self._type_char, ' '), ) # Show change instructions only if we have more than one @@ -361,7 +359,7 @@ class OnScreenKeyboardWindow(Window): _bauiv1.buttonwidget( edit=btn, label=chars[i] if have_char else ' ', - on_activate_call=babase.CallStrict( + on_activate_call=babase.Call( self._type_char, chars[i] if have_char else ' ' ), ) diff --git a/dist/ba_data/python/bauiv1lib/account/link.py b/dist/ba_data/python/bauiv1lib/account/link.py new file mode 100644 index 0000000..d19ed06 --- /dev/null +++ b/dist/ba_data/python/bauiv1lib/account/link.py @@ -0,0 +1,198 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for linking accounts.""" + +from __future__ import annotations + +import copy +import time +from typing import TYPE_CHECKING + +import bauiv1 as bui + +if TYPE_CHECKING: + from typing import Any + + +class AccountLinkWindow(bui.Window): + """Window for linking accounts.""" + + def __init__(self, origin_widget: bui.Widget | None = None): + plus = bui.app.plus + assert plus is not None + + scale_origin: tuple[float, float] | None + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + transition = 'in_right' + bg_color = (0.4, 0.4, 0.5) + self._width = 560 + self._height = 420 + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + base_scale = ( + 1.65 + if uiscale is bui.UIScale.SMALL + else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.1 + ) + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=base_scale, + scale_origin_stack_offset=scale_origin, + stack_offset=( + (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0) + ), + ) + ) + self._cancel_button = bui.buttonwidget( + parent=self._root_widget, + position=(40, self._height - 45), + size=(50, 50), + scale=0.7, + label='', + color=bg_color, + on_activate_call=self._cancel, + autoselect=True, + icon=bui.gettexture('crossOut'), + iconscale=1.2, + ) + maxlinks = plus.get_v1_account_misc_read_val('maxLinkAccounts', 5) + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.56), + size=(0, 0), + text=bui.Lstr( + resource=( + 'accountSettingsWindow.linkAccountsInstructionsNewText' + ), + subs=[('${COUNT}', str(maxlinks))], + ), + maxwidth=self._width * 0.9, + color=bui.app.ui_v1.infotextcolor, + max_height=self._height * 0.6, + h_align='center', + v_align='center', + ) + bui.containerwidget( + edit=self._root_widget, cancel_button=self._cancel_button + ) + bui.buttonwidget( + parent=self._root_widget, + position=(40, 30), + size=(200, 60), + label=bui.Lstr( + resource='accountSettingsWindow.linkAccountsGenerateCodeText' + ), + autoselect=True, + on_activate_call=self._generate_press, + ) + self._enter_code_button = bui.buttonwidget( + parent=self._root_widget, + position=(self._width - 240, 30), + size=(200, 60), + label=bui.Lstr( + resource='accountSettingsWindow.linkAccountsEnterCodeText' + ), + autoselect=True, + on_activate_call=self._enter_code_press, + ) + + def _generate_press(self) -> None: + from bauiv1lib.account.signin import show_sign_in_prompt + + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': + show_sign_in_prompt() + return + bui.screenmessage( + bui.Lstr(resource='gatherWindow.requestingAPromoCodeText'), + color=(0, 1, 0), + ) + plus.add_v1_account_transaction( + { + 'type': 'ACCOUNT_LINK_CODE_REQUEST', + 'expire_time': time.time() + 5, + } + ) + plus.run_v1_account_transactions() + + def _enter_code_press(self) -> None: + from bauiv1lib.sendinfo import SendInfoWindow + + SendInfoWindow( + modal=True, + legacy_code_mode=True, + origin_widget=self._enter_code_button, + ) + bui.containerwidget( + edit=self._root_widget, transition=self._transition_out + ) + + def _cancel(self) -> None: + bui.containerwidget( + edit=self._root_widget, transition=self._transition_out + ) + + +class AccountLinkCodeWindow(bui.Window): + """Window showing code for account-linking.""" + + def __init__(self, data: dict[str, Any]): + self._width = 350 + self._height = 200 + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height), + color=(0.45, 0.63, 0.15), + transition='in_scale', + scale=( + 1.8 + if uiscale is bui.UIScale.SMALL + else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 + ), + ) + ) + self._data = copy.deepcopy(data) + bui.getsound('cashRegister').play() + bui.getsound('swish').play() + self._cancel_button = bui.buttonwidget( + parent=self._root_widget, + scale=0.5, + position=(40, self._height - 40), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=bui.gettexture('crossOut'), + iconscale=1.2, + ) + bui.containerwidget( + edit=self._root_widget, cancel_button=self._cancel_button + ) + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=2.0, + h_align='center', + v_align='center', + text=data['code'], + maxwidth=self._width * 0.85, + ) + + def close(self) -> None: + """close the window""" + bui.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/dist/ba_data/python/bauiv1lib/account/settings.py b/dist/ba_data/python/bauiv1lib/account/settings.py index ba3dae7..900072c 100644 --- a/dist/ba_data/python/bauiv1lib/account/settings.py +++ b/dist/ba_data/python/bauiv1lib/account/settings.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides UI for account functionality.""" - # pylint: disable=too-many-lines from __future__ import annotations @@ -34,8 +33,6 @@ class AccountSettingsWindow(bui.MainWindow): plus = bui.app.plus assert plus is not None - self._uiopenstate = bui.UIOpenState('accountsettings') - self._sign_in_v2_proxy_button: bui.Widget | None = None self._sign_in_device_button: bui.Widget | None = None @@ -52,7 +49,7 @@ class AccountSettingsWindow(bui.MainWindow): self._v1_signed_in = plus.get_v1_account_state() == 'signed_in' self._v1_account_state_num = plus.get_v1_account_state_num() self._check_sign_in_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) self._can_reset_achievements = False @@ -866,9 +863,7 @@ class AccountSettingsWindow(bui.MainWindow): color=(0.55, 0.5, 0.6), icon=bui.gettexture('settingsIcon'), textcolor=(0.75, 0.7, 0.8), - on_activate_call=bui.WeakCallStrict( - self._on_manage_account_press - ), + on_activate_call=bui.WeakCall(self._on_manage_account_press), ) if first_selectable is None: first_selectable = btn @@ -889,9 +884,7 @@ class AccountSettingsWindow(bui.MainWindow): label=bui.Lstr(resource=f'{self._r}.createAnAccountText'), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), - on_activate_call=bui.WeakCallStrict( - self._on_create_account_press - ), + on_activate_call=bui.WeakCall(self._on_create_account_press), ) if first_selectable is None: first_selectable = btn @@ -1107,9 +1100,7 @@ class AccountSettingsWindow(bui.MainWindow): if bui.app.plus is not None: bui.apptimer( 0.15, - bui.CallStrict( - bui.app.plus.show_game_service_ui, 'achievements' - ), + bui.Call(bui.app.plus.show_game_service_ui, 'achievements'), ) else: logging.warning('show_game_service_ui requires plus feature-set.') @@ -1147,9 +1138,7 @@ class AccountSettingsWindow(bui.MainWindow): with plus.accounts.primary: plus.cloud.send_message_cb( bacommon.cloud.ManageAccountMessage(weblocation=weblocation), - on_response=bui.WeakCallPartial( - self._on_manage_account_response - ), + on_response=bui.WeakCall(self._on_manage_account_response), ) def _on_manage_account_response( @@ -1169,9 +1158,7 @@ class AccountSettingsWindow(bui.MainWindow): if bui.app.plus is not None: bui.apptimer( 0.15, - bui.CallStrict( - bui.app.plus.show_game_service_ui, 'leaderboards' - ), + bui.Call(bui.app.plus.show_game_service_ui, 'leaderboards'), ) else: logging.warning('show_game_service_ui requires classic') @@ -1258,7 +1245,7 @@ class AccountSettingsWindow(bui.MainWindow): self._needs_refresh = True # Speed UI updates along. - bui.apptimer(0.1, bui.WeakCallStrict(self._update)) + bui.apptimer(0.1, bui.WeakCall(self._update)) def _sign_out_press(self) -> None: plus = bui.app.plus @@ -1286,7 +1273,7 @@ class AccountSettingsWindow(bui.MainWindow): ) # Speed UI updates along. - bui.apptimer(0.1, bui.WeakCallStrict(self._update)) + bui.apptimer(0.1, bui.WeakCall(self._update)) def _sign_in_press(self, login_type: str | LoginType) -> None: @@ -1318,7 +1305,7 @@ class AccountSettingsWindow(bui.MainWindow): cfg['Auto Account State'] = login_type cfg.commit() self._needs_refresh = True - bui.apptimer(0.1, bui.WeakCallStrict(self._update)) + bui.apptimer(0.1, bui.WeakCall(self._update)) return # V2 login sign-in buttons generally go through adapters. @@ -1326,12 +1313,12 @@ class AccountSettingsWindow(bui.MainWindow): if adapter is not None: self._signing_in_adapter = adapter adapter.sign_in( - result_cb=bui.WeakCallPartial(self._on_adapter_sign_in_result), + result_cb=bui.WeakCall(self._on_adapter_sign_in_result), description='account settings button', ) # Will get 'Signing in...' to show. self._needs_refresh = True - bui.apptimer(0.1, bui.WeakCallStrict(self._update)) + bui.apptimer(0.1, bui.WeakCall(self._update)) else: bui.screenmessage(f'Unsupported login_type: {login_type.name}') @@ -1378,7 +1365,7 @@ class AccountSettingsWindow(bui.MainWindow): # credentials go through and the account name shows up. bui.apptimer( 1.5, - bui.CallStrict( + bui.Call( bui.screenmessage, bui.Lstr( resource=self._r @@ -1389,7 +1376,7 @@ class AccountSettingsWindow(bui.MainWindow): # Speed any UI updates along. self._needs_refresh = True - bui.apptimer(0.1, bui.WeakCallStrict(self._update)) + bui.apptimer(0.1, bui.WeakCall(self._update)) def _v2_proxy_sign_in_press(self) -> None: diff --git a/dist/ba_data/python/bauiv1lib/account/signin.py b/dist/ba_data/python/bauiv1lib/account/signin.py index eaddc8a..4cdf885 100644 --- a/dist/ba_data/python/bauiv1lib/account/signin.py +++ b/dist/ba_data/python/bauiv1lib/account/signin.py @@ -7,7 +7,7 @@ from __future__ import annotations import bauiv1 as bui -def show_sign_in_prompt(origin_widget: bui.Widget | None = None) -> None: +def show_sign_in_prompt() -> None: """Bring up a prompt telling the user they must sign in.""" from bauiv1lib.confirm import ConfirmWindow @@ -17,7 +17,6 @@ def show_sign_in_prompt(origin_widget: bui.Widget | None = None) -> None: ok_text=bui.Lstr(resource='accountSettingsWindow.signInText'), width=460, height=130, - origin_widget=origin_widget, ) @@ -48,7 +47,6 @@ def _show_account_settings() -> None: from_window=False, # Don't check where we're coming from. is_auxiliary=True, suppress_warning=True, - extra_type_id='', ) # Transition out any previous main window. diff --git a/dist/ba_data/python/bauiv1lib/account/unlink.py b/dist/ba_data/python/bauiv1lib/account/unlink.py new file mode 100644 index 0000000..ef80dad --- /dev/null +++ b/dist/ba_data/python/bauiv1lib/account/unlink.py @@ -0,0 +1,152 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for unlinking accounts.""" + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import bauiv1 as bui + +if TYPE_CHECKING: + from typing import Any + + +class AccountUnlinkWindow(bui.Window): + """A window to kick off account unlinks.""" + + def __init__(self, origin_widget: bui.Widget | None = None): + plus = bui.app.plus + assert plus is not None + + scale_origin: tuple[float, float] | None + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + transition = 'in_right' + bg_color = (0.4, 0.4, 0.5) + self._width = 540 + self._height = 350 + self._scroll_width = 400 + self._scroll_height = 200 + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + base_scale = ( + 2.0 + if uiscale is bui.UIScale.SMALL + else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.1 + ) + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=base_scale, + scale_origin_stack_offset=scale_origin, + stack_offset=( + (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0) + ), + ) + ) + self._cancel_button = bui.buttonwidget( + parent=self._root_widget, + position=(30, self._height - 50), + size=(50, 50), + scale=0.7, + label='', + color=bg_color, + on_activate_call=self._cancel, + autoselect=True, + icon=bui.gettexture('crossOut'), + iconscale=1.2, + ) + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.88), + size=(0, 0), + text=bui.Lstr( + resource='accountSettingsWindow.unlinkAccountsInstructionsText' + ), + maxwidth=self._width * 0.7, + color=bui.app.ui_v1.infotextcolor, + h_align='center', + v_align='center', + ) + bui.containerwidget( + edit=self._root_widget, cancel_button=self._cancel_button + ) + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + highlight=False, + position=( + (self._width - self._scroll_width) * 0.5, + self._height - 85 - self._scroll_height, + ), + size=(self._scroll_width, self._scroll_height), + ) + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) + self._columnwidget = bui.columnwidget( + parent=self._scrollwidget, border=2, margin=0, left_border=10 + ) + + our_login_id = plus.get_v1_account_public_login_id() + if our_login_id is None: + entries = [] + else: + account_infos = plus.get_v1_account_misc_read_val_2( + 'linkedAccounts2', [] + ) + entries = [ + {'name': ai['d'], 'id': ai['id']} + for ai in account_infos + if ai['id'] != our_login_id + ] + + # (avoid getting our selection stuck on an empty column widget) + if not entries: + bui.containerwidget(edit=self._scrollwidget, selectable=False) + for i, entry in enumerate(entries): + txt = bui.textwidget( + parent=self._columnwidget, + selectable=True, + text=entry['name'], + size=(self._scroll_width - 30, 30), + autoselect=True, + click_activate=True, + on_activate_call=bui.Call(self._on_entry_selected, entry), + ) + bui.widget(edit=txt, left_widget=self._cancel_button) + if i == 0: + bui.widget(edit=txt, up_widget=self._cancel_button) + + def _on_entry_selected(self, entry: dict[str, Any]) -> None: + plus = bui.app.plus + assert plus is not None + + bui.screenmessage( + bui.Lstr( + resource='pleaseWaitText', fallback_resource='requestingText' + ), + color=(0, 1, 0), + ) + plus.add_v1_account_transaction( + { + 'type': 'ACCOUNT_UNLINK_REQUEST', + 'accountID': entry['id'], + 'expire_time': time.time() + 5, + } + ) + plus.run_v1_account_transactions() + bui.containerwidget( + edit=self._root_widget, transition=self._transition_out + ) + + def _cancel(self) -> None: + bui.containerwidget( + edit=self._root_widget, transition=self._transition_out + ) diff --git a/dist/ba_data/python/bauiv1lib/account/v2proxy.py b/dist/ba_data/python/bauiv1lib/account/v2proxy.py index 81b25f5..2ddf981 100644 --- a/dist/ba_data/python/bauiv1lib/account/v2proxy.py +++ b/dist/ba_data/python/bauiv1lib/account/v2proxy.py @@ -103,9 +103,9 @@ class V2ProxySignInWindow(bui.Window): self._connection_wait_timeout_time = time.monotonic() + 10.0 self._update_timer = bui.AppTimer( - 0.371, bui.WeakCallStrict(self._update), repeat=True + 0.371, bui.WeakCall(self._update), repeat=True ) - bui.pushcall(bui.WeakCallStrict(self._update)) + bui.pushcall(bui.WeakCall(self._update)) def _update(self) -> None: @@ -135,7 +135,7 @@ class V2ProxySignInWindow(bui.Window): plus.cloud.send_message_cb( bacommon.cloud.LoginProxyRequestMessage(), - on_response=bui.WeakCallPartial(self._on_proxy_request_response), + on_response=bui.WeakCall(self._on_proxy_request_response), ) self._message_in_flight = True @@ -221,8 +221,7 @@ class V2ProxySignInWindow(bui.Window): self._proxyid = response.proxyid self._proxykey = response.proxykey bui.apptimer( - STATUS_CHECK_INTERVAL_SECONDS, - bui.WeakCallStrict(self._ask_for_status), + STATUS_CHECK_INTERVAL_SECONDS, bui.WeakCall(self._ask_for_status) ) def _show_overlay_sign_in_ui( @@ -283,9 +282,7 @@ class V2ProxySignInWindow(bui.Window): h_align='center', v_align='center', autoselect=True, - on_activate_call=bui.CallStrict( - self._copy_link, address_pretty - ), + on_activate_call=bui.Call(self._copy_link, address_pretty), selectable=True, ) qroffs = 20.0 @@ -309,7 +306,7 @@ class V2ProxySignInWindow(bui.Window): bacommon.cloud.LoginProxyStateQueryMessage( proxyid=self._proxyid, proxykey=self._proxykey ), - on_response=bui.WeakCallPartial(self._got_status), + on_response=bui.WeakCall(self._got_status), ) def _got_status( @@ -343,9 +340,7 @@ class V2ProxySignInWindow(bui.Window): bacommon.cloud.LoginProxyCompleteMessage( proxyid=self._proxyid ), - on_response=bui.WeakCallPartial( - self._proxy_complete_response - ), + on_response=bui.WeakCall(self._proxy_complete_response), ) except CommunicationError: pass @@ -365,7 +360,7 @@ class V2ProxySignInWindow(bui.Window): ): bui.apptimer( STATUS_CHECK_INTERVAL_SECONDS, - bui.WeakCallStrict(self._ask_for_status), + bui.WeakCall(self._ask_for_status), ) def _proxy_complete_response(self, response: None | Exception) -> None: diff --git a/dist/ba_data/python/bauiv1lib/account/viewer.py b/dist/ba_data/python/bauiv1lib/account/viewer.py index 3e3faec..2bf33d7 100644 --- a/dist/ba_data/python/bauiv1lib/account/viewer.py +++ b/dist/ba_data/python/bauiv1lib/account/viewer.py @@ -148,7 +148,7 @@ class AccountViewerWindow(PopupWindow): 'accountID': self._account_id, 'profileID': self._profile_id, }, - callback=bui.WeakCallPartial(self._on_query_response), + callback=bui.WeakCall(self._on_query_response), ) def popup_menu_selected_choice( diff --git a/dist/ba_data/python/bauiv1lib/achievements.py b/dist/ba_data/python/bauiv1lib/achievements.py index c949aff..3272a4f 100644 --- a/dist/ba_data/python/bauiv1lib/achievements.py +++ b/dist/ba_data/python/bauiv1lib/achievements.py @@ -30,8 +30,6 @@ class AchievementsWindow(bui.MainWindow): assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - self._uiopenstate = bui.UIOpenState('classicachievements') - self._width = 800 if uiscale is bui.UIScale.SMALL else 550 self._height = ( 450 diff --git a/dist/ba_data/python/bauiv1lib/appinvite.py b/dist/ba_data/python/bauiv1lib/appinvite.py index 7f3c050..a4f503d 100644 --- a/dist/ba_data/python/bauiv1lib/appinvite.py +++ b/dist/ba_data/python/bauiv1lib/appinvite.py @@ -150,7 +150,7 @@ class ShowFriendCodeWindow(bui.Window): position=(self._width * 0.5 - 100 + xoffs, 39), autoselect=True, label=bui.Lstr(resource='gatherWindow.emailItText'), - on_activate_call=bui.WeakCallStrict(self._email), + on_activate_call=bui.WeakCall(self._email), ) def _email(self) -> None: diff --git a/dist/ba_data/python/bauiv1lib/characterpicker.py b/dist/ba_data/python/bauiv1lib/characterpicker.py index f9ecc8c..835f3f0 100644 --- a/dist/ba_data/python/bauiv1lib/characterpicker.py +++ b/dist/ba_data/python/bauiv1lib/characterpicker.py @@ -147,7 +147,7 @@ class CharacterPicker(PopupWindow): color=(1, 1, 1), tint_color=tint_color, tint2_color=tint2_color, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._select_character, self._spazzes[index] ), position=pos, @@ -198,9 +198,8 @@ class CharacterPicker(PopupWindow): plus = bui.app.plus assert plus is not None - if plus.accounts.primary is None: + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() - self._transition_out() return if self._delegate is not None: diff --git a/dist/ba_data/python/bauiv1lib/chest.py b/dist/ba_data/python/bauiv1lib/chest.py index de42593..047207d 100644 --- a/dist/ba_data/python/bauiv1lib/chest.py +++ b/dist/ba_data/python/bauiv1lib/chest.py @@ -10,8 +10,7 @@ import random from typing import override, TYPE_CHECKING from efro.util import strict_partial -import bacommon.classic -import bacommon.displayitem as ditm +import bacommon.bs import bauiv1 as bui if TYPE_CHECKING: @@ -39,8 +38,6 @@ class ChestWindow(bui.MainWindow): # pylint: disable=too-many-statements self._index = index - self._uiopenstate = bui.UIOpenState(f'classicchest{index}') - # Get this loading before we need it. self._quote_bubble_tex = bui.gettexture('quoteBubble') @@ -59,9 +56,7 @@ class ChestWindow(bui.MainWindow): self._time_string_timer: bui.AppTimer | None = None self._time_string_text: bui.Widget | None = None self._open_me_flash_timer: bui.AppTimer | None = None - self._prizesets: list[ - bacommon.classic.ChestInfoResponse.Chest.PrizeSet - ] = [] + self._prizesets: list[bacommon.bs.ChestInfoResponse.Chest.PrizeSet] = [] self._prizeindex = -1 self._prizesettxts: dict[int, list[bui.Widget]] = {} self._prizesetimgs: dict[int, list[bui.Widget]] = {} @@ -204,8 +199,8 @@ class ChestWindow(bui.MainWindow): self._action_in_flight = True with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.classic.ChestInfoMessage(chest_id=str(self._index)), - on_response=bui.WeakCallPartial(self._on_chest_info_response), + bacommon.bs.ChestInfoMessage(chest_id=str(self._index)), + on_response=bui.WeakCall(self._on_chest_info_response), ) @override @@ -227,7 +222,7 @@ class ChestWindow(bui.MainWindow): def main_window_should_preserve_selection(self) -> bool: # This doesn't really benefit us since we do lots of widget # creates/destroys throughout our lifetime and also we're an - # auxiliary window so should never need to restore toolbar + # auxliary window so should never need to restore toolbar # selections. return False @@ -246,7 +241,7 @@ class ChestWindow(bui.MainWindow): bui.textwidget(edit=self._time_string_text, text=tstr) def _on_chest_info_response( - self, response: bacommon.classic.ChestInfoResponse | Exception + self, response: bacommon.bs.ChestInfoResponse | Exception ) -> None: assert self._action_in_flight # Should be us. self._action_in_flight = False @@ -266,7 +261,7 @@ class ChestWindow(bui.MainWindow): self._show_chest_actions(response.user_tokens, response.chest) def _on_chest_action_response( - self, response: bacommon.cloud.ChestActionResponse | Exception + self, response: bacommon.bs.ChestActionResponse | Exception ) -> None: assert self._action_in_flight # Should be us. self._action_in_flight = False @@ -301,7 +296,7 @@ class ChestWindow(bui.MainWindow): bui.app.classic.run_bs_client_effects(response.effects, delay=toffs) def _show_chest_actions( - self, user_tokens: int, chest: bacommon.classic.ChestInfoResponse.Chest + self, user_tokens: int, chest: bacommon.bs.ChestInfoResponse.Chest ) -> None: """Show state for our chest.""" # pylint: disable=too-many-statements @@ -347,7 +342,11 @@ class ChestWindow(bui.MainWindow): tint2_color=self._chestdisplayinfo.tint2, ) - # Store the prize-sets so we can display odds/etc. + # Store the prize-sets so we can display odds/etc. Sort them + # with smallest weights first (higher visually == better). + # self._prizesets = sorted( + # chest.prizesets, key=lambda s: s.weight, reverse=True + # ) self._prizesets = chest.prizesets if chest.unlock_tokens > 0: @@ -392,9 +391,7 @@ class ChestWindow(bui.MainWindow): self._time_string_timer = bui.AppTimer( 1.0, repeat=True, - call=bui.WeakCallStrict( - self._update_time_display, chest.unlock_time - ), + call=bui.WeakCall(self._update_time_display, chest.unlock_time), ) # Allow watching an ad IF the server tells us we can AND we have @@ -421,7 +418,7 @@ class ChestWindow(bui.MainWindow): label='', button_type='square', autoselect=True, - on_activate_call=bui.WeakCallStrict( + on_activate_call=bui.WeakCall( self._open_press, user_tokens, chest.unlock_tokens ), enable_sound=False, @@ -468,41 +465,38 @@ class ChestWindow(bui.MainWindow): v_align='center', ) ) - if bool(False): - pass - else: - self._open_now_images.append( - bui.imagewidget( - parent=self._root_widget, - size=(iconsize, iconsize), - position=( - self._width * 0.5 - iconsize * 0.5 + boffsx, - self._yoffs + bposy + bheight * 0.35, - ), - draw_controller=self._open_now_button, - texture=bui.gettexture('coin'), - ) + self._open_now_images.append( + bui.imagewidget( + parent=self._root_widget, + size=(iconsize, iconsize), + position=( + self._width * 0.5 - iconsize * 0.5 + boffsx, + self._yoffs + bposy + bheight * 0.35, + ), + draw_controller=self._open_now_button, + texture=bui.gettexture('coin'), ) - self._open_now_texts.append( - bui.textwidget( - parent=self._root_widget, - text=bui.Lstr( - resource='tokens.numTokensText', - subs=[('${COUNT}', str(chest.unlock_tokens))], - ), - position=( - self._width * 0.5 + boffsx, - self._yoffs + bposy + bheight * 0.25, - ), - scale=0.65, - color=(0, 1, 0), - draw_controller=self._open_now_button, - maxwidth=bwidth * 0.8, - size=(0, 0), - h_align='center', - v_align='center', - ) + ) + self._open_now_texts.append( + bui.textwidget( + parent=self._root_widget, + text=bui.Lstr( + resource='tokens.numTokensText', + subs=[('${COUNT}', str(chest.unlock_tokens))], + ), + position=( + self._width * 0.5 + boffsx, + self._yoffs + bposy + bheight * 0.25, + ), + scale=0.65, + color=(0, 1, 0), + draw_controller=self._open_now_button, + maxwidth=bwidth * 0.8, + size=(0, 0), + h_align='center', + v_align='center', ) + ) self._open_now_spinner = bui.spinnerwidget( parent=self._root_widget, position=( @@ -537,7 +531,7 @@ class ChestWindow(bui.MainWindow): label='', button_type='square', autoselect=True, - on_activate_call=bui.WeakCallStrict(self._watch_ad_press), + on_activate_call=bui.WeakCall(self._watch_ad_press), enable_sound=False, ) bui.imagewidget( @@ -601,7 +595,7 @@ class ChestWindow(bui.MainWindow): self._open_me_flash_timer = bui.AppTimer( 0.05, repeat=True, - call=bui.WeakCallStrict(self._open_me_backing_update), + call=bui.WeakCall(self._open_me_backing_update), ) self._open_me_widgets.clear() self._open_me_widgets.append(self._open_me_backing) @@ -614,6 +608,7 @@ class ChestWindow(bui.MainWindow): value='*${A}', subs=[('${A}', bui.Lstr(resource='openMeText'))], ), + # text=bui.Lstr(resource='openMeText'), maxwidth=175, scale=0.7, color=(0, 1.0, 0.7, 1), @@ -649,9 +644,7 @@ class ChestWindow(bui.MainWindow): color=(0.0, 0.8, 0.5), autoselect=True, text_flatness=1.0, - on_activate_call=bui.WeakCallStrict( - self._stop_showing_open_me_press - ), + on_activate_call=bui.WeakCall(self._stop_showing_open_me_press), ) # Avoid depth issues with the quote-bubble image. bui.widget(edit=btn, depth_range=(0.1, 1.0)) @@ -801,13 +794,10 @@ class ChestWindow(bui.MainWindow): for item in p.contents: x += 5.0 - if isinstance(item.item, ditm.Tickets): + if isinstance(item.item, bacommon.bs.TicketsDisplayItem): _mktxt(str(item.item.count)) _mkicon('tickets') - elif isinstance(item.item, ditm.PurpleTickets): - _mktxt(str(item.item.count)) - _mkicon('ticketsPurple') - elif isinstance(item.item, ditm.Tokens): + elif isinstance(item.item, bacommon.bs.TokensDisplayItem): _mktxt(str(item.item.count)) _mkicon('coin') else: @@ -817,7 +807,7 @@ class ChestWindow(bui.MainWindow): descfin = bui.Lstr( translate=('serverResponses', item.description) ).evaluate() - subs: list[str] = ( + subs = ( [] if item.description_subs is None else item.description_subs @@ -855,7 +845,7 @@ class ChestWindow(bui.MainWindow): # Hack: We disable normal swish for the open button and it # seems weird without a swish here, so explicitly do one. bui.getsound('swish').play() - show_get_tokens_prompt(origin_widget=self._open_now_button) + show_get_tokens_prompt() return self._action_in_flight = True @@ -867,12 +857,12 @@ class ChestWindow(bui.MainWindow): with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.cloud.ChestActionMessage( + bacommon.bs.ChestActionMessage( chest_id=str(self._index), - action=bacommon.cloud.ChestActionMessage.Action.UNLOCK, + action=bacommon.bs.ChestActionMessage.Action.UNLOCK, token_payment=token_payment, ), - on_response=bui.WeakCallPartial(self._on_chest_action_response), + on_response=bui.WeakCall(self._on_chest_action_response), ) # Convey that something is in progress. @@ -908,7 +898,7 @@ class ChestWindow(bui.MainWindow): self._action_in_flight = True bui.app.plus.ads.show_ad_2( 'reduce_chest_wait', - on_completion_call=bui.WeakCallPartial(self._watch_ad_complete), + on_completion_call=bui.WeakCall(self._watch_ad_complete), ) # Convey that something is in progress. @@ -947,12 +937,12 @@ class ChestWindow(bui.MainWindow): with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.cloud.ChestActionMessage( + bacommon.bs.ChestActionMessage( chest_id=str(self._index), - action=bacommon.cloud.ChestActionMessage.Action.AD, + action=bacommon.bs.ChestActionMessage.Action.AD, token_payment=0, ), - on_response=bui.WeakCallPartial(self._on_chest_action_response), + on_response=bui.WeakCall(self._on_chest_action_response), ) def _reset(self) -> None: @@ -982,14 +972,9 @@ class ChestWindow(bui.MainWindow): text=bui.Lstr(resource='chests.slotDescriptionText'), color=(1, 1, 1), ) - # This is somewhat redundant with the close button, but we need - # to have *something* selectable in our window for SMALL ui-mode - # (when we don't have our own close button) otherwise we can be - # left unable to select anything. - self._show_done_button(use_ok_label=True) def _show_chest_contents( - self, response: bacommon.cloud.ChestActionResponse + self, response: bacommon.bs.ChestActionResponse ) -> float: # pylint: disable=too-many-locals # pylint: disable=too-many-statements @@ -1004,7 +989,11 @@ class ChestWindow(bui.MainWindow): # Insert test items for testing. if bool(False): - response.contents += [ditm.Wrapper.for_item(ditm.Test())] + response.contents += [ + bacommon.bs.DisplayItemWrapper.for_display_item( + bacommon.bs.TestDisplayItem() + ) + ] tincr = 0.4 tendoffs = tincr * 4.0 @@ -1046,7 +1035,7 @@ class ChestWindow(bui.MainWindow): sign = -sign bui.apptimer( toffs, - bui.CallStrict( + bui.Call( _set_img, x=( 20.0 @@ -1093,7 +1082,7 @@ class ChestWindow(bui.MainWindow): ) toffsopen = toffs - bui.apptimer(toffs, bui.WeakCallStrict(self._show_chest_opening)) + bui.apptimer(toffs, bui.WeakCall(self._show_chest_opening)) toffs += tincr * 1.0 width = xspacing * 0.95 @@ -1113,12 +1102,11 @@ class ChestWindow(bui.MainWindow): self._yoffs - 250.0, ), width=width, - debug=False, ), ) xoffs += xspacing toffs += tincr - bui.apptimer(toffs, bui.WeakCallStrict(self._show_done_button)) + bui.apptimer(toffs, bui.WeakCall(self._show_done_button)) self._show_odds(initial_highlighted_row=-1) @@ -1138,7 +1126,7 @@ class ChestWindow(bui.MainWindow): while toffs2 > 0.0: bui.apptimer( toffs2, - bui.WeakCallStrict(self._highlight_odds_row, i), + bui.WeakCall(self._highlight_odds_row, i), ) toffs2 -= amt if ease_out: @@ -1196,7 +1184,7 @@ class ChestWindow(bui.MainWindow): # comes to rest before scale. bui.apptimer( toffs, - bui.CallStrict( + bui.Call( _set_img, x=( 1.0 @@ -1216,7 +1204,7 @@ class ChestWindow(bui.MainWindow): initial_highlighted_extra=True, ) - def _show_done_button(self, use_ok_label: bool = False) -> None: + def _show_done_button(self) -> None: # No-op if our ui is dead. if not self._root_widget: return @@ -1231,10 +1219,33 @@ class ChestWindow(bui.MainWindow): self._yoffs - 350, ), size=(bwidth, bheight), - label=bui.Lstr(resource='okText' if use_ok_label else 'doneText'), + label=bui.Lstr(resource='doneText'), autoselect=True, on_activate_call=self.main_window_back, ) bui.containerwidget( edit=self._root_widget, selected_child=btn, start_button=btn ) + + +# Slight hack: we define window 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 window 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/dist/ba_data/python/bauiv1lib/colorpicker.py b/dist/ba_data/python/bauiv1lib/colorpicker.py index b557def..476e059 100644 --- a/dist/ba_data/python/bauiv1lib/colorpicker.py +++ b/dist/ba_data/python/bauiv1lib/colorpicker.py @@ -12,6 +12,8 @@ import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Sequence +REQUIRE_PRO = False + class ColorPicker(PopupWindow): """A popup UI to select from a set of colors. @@ -85,7 +87,7 @@ class ColorPicker(PopupWindow): size=(35, 40), label='', button_type='square', - on_activate_call=bui.WeakCallStrict(self._select, x, y), + on_activate_call=bui.WeakCall(self._select, x, y), autoselect=True, color=color, extra_touch_border_scale=0.0, @@ -103,9 +105,19 @@ class ColorPicker(PopupWindow): fallback_resource='coopSelectWindow.customText', ), autoselect=True, - on_activate_call=bui.WeakCallStrict(self._select_other), + on_activate_call=bui.WeakCall(self._select_other), ) + assert bui.app.classic is not None + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro(): + bui.imagewidget( + parent=self.root_widget, + position=(50, 12), + size=(30, 30), + texture=bui.gettexture('lock'), + draw_controller=other_button, + ) + # If their color is close to one of our swatches, select it. # Otherwise select 'other'. if closest_dist < 0.03: @@ -123,6 +135,14 @@ class ColorPicker(PopupWindow): return self._tag def _select_other(self) -> None: + from bauiv1lib import purchase + + # Requires pro. + assert bui.app.classic is not None + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro(): + purchase.PurchaseWindow(items=['pro']) + self._transition_out() + return ColorPickerExact( parent=self._parent, position=self._position, @@ -262,7 +282,7 @@ class ColorPickerExact(PopupWindow): label=b_label, autoselect=True, enable_sound=False, - on_activate_call=bui.WeakCallStrict( + on_activate_call=bui.WeakCall( self._color_change_press, color_name, binc ), ) @@ -276,7 +296,7 @@ class ColorPickerExact(PopupWindow): color=(0.6, 0.6, 0.6), textcolor=(0.7, 0.7, 0.7), label=bui.Lstr(resource='doneText'), - on_activate_call=bui.WeakCallStrict(self._transition_out), + on_activate_call=bui.WeakCall(self._transition_out), autoselect=True, ) bui.containerwidget(edit=self.root_widget, start_button=btn) diff --git a/dist/ba_data/python/bauiv1lib/config.py b/dist/ba_data/python/bauiv1lib/config.py index 3dde744..cd28fba 100644 --- a/dist/ba_data/python/bauiv1lib/config.py +++ b/dist/ba_data/python/bauiv1lib/config.py @@ -152,7 +152,7 @@ class ConfigNumberEdit: size=(28, 28), label='-', autoselect=True, - on_activate_call=bui.CallStrict(self._down), + on_activate_call=bui.Call(self._down), repeat=True, enable_sound=changesound, ) @@ -163,7 +163,7 @@ class ConfigNumberEdit: size=(28, 28), label='+', autoselect=True, - on_activate_call=bui.CallStrict(self._up), + on_activate_call=bui.Call(self._up), repeat=True, enable_sound=changesound, ) diff --git a/dist/ba_data/python/bauiv1lib/connect.py b/dist/ba_data/python/bauiv1lib/connect.py index 695e7a1..099d698 100644 --- a/dist/ba_data/python/bauiv1lib/connect.py +++ b/dist/ba_data/python/bauiv1lib/connect.py @@ -74,7 +74,7 @@ class ConnectWindow(bui.Window): ) bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button) self._update_timer = bui.AppTimer( - 0.113, bui.WeakCallStrict(self._update), repeat=True + 0.113, bui.WeakCall(self._update), repeat=True ) def _update(self) -> None: diff --git a/dist/ba_data/python/bauiv1lib/connectivity.py b/dist/ba_data/python/bauiv1lib/connectivity.py index a48a702..4b801d5 100644 --- a/dist/ba_data/python/bauiv1lib/connectivity.py +++ b/dist/ba_data/python/bauiv1lib/connectivity.py @@ -100,7 +100,7 @@ class WaitForConnectivityWindow(bui.Window): ) bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button) self._update_timer = bui.AppTimer( - 0.113, bui.WeakCallStrict(self._update), repeat=True + 0.113, bui.WeakCall(self._update), repeat=True ) def _update(self) -> None: diff --git a/dist/ba_data/python/bauiv1lib/coop/browser.py b/dist/ba_data/python/bauiv1lib/coop/browser.py index 1acaac0..5ebfe91 100644 --- a/dist/ba_data/python/bauiv1lib/coop/browser.py +++ b/dist/ba_data/python/bauiv1lib/coop/browser.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """UI for browsing available co-op levels/games/etc.""" - # FIXME: Break this up. # pylint: disable=too-many-lines @@ -10,11 +9,9 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, override -from bacommon.analytics import ClassicAnalyticsEvent -import bauiv1 as bui +import bauiv1 as bui from bauiv1lib.utils import scroll_fade_top, scroll_fade_bottom -from bauiv1lib.connectivity import wait_for_connectivity if TYPE_CHECKING: from typing import Any @@ -62,8 +59,8 @@ class CoopBrowserWindow(bui.MainWindow): ), ) - # Try to recreate the same number of buttons we had last time so - # our re-selection code works. + # Try to recreate the same number of buttons we had last time so our + # re-selection code works. self._tournament_button_count = app.config.get('Tournament Rows', 0) assert isinstance(self._tournament_button_count, int) @@ -259,8 +256,7 @@ class CoopBrowserWindow(bui.MainWindow): self._subcontainer: bui.Widget | None = None - # Take note of our account state; we'll refresh later if this - # changes. + # Take note of our account state; we'll refresh later if this changes. self._account_state_num = plus.get_v1_account_state_num() # Same for fg/bg state. @@ -268,14 +264,14 @@ class CoopBrowserWindow(bui.MainWindow): self._refresh() - # Even though we might display cached tournament data - # immediately, we don't consider it valid until we've pinged. + # Even though we might display cached tournament data immediately, we + # don't consider it valid until we've pinged. # the server for an update self._tourney_data_up_to_date = False - # If we've got a cached tournament list for our account and info - # for each one of those tournaments, go ahead and display it as - # a starting point. + # If we've got a cached tournament list for our account and info for + # each one of those tournaments, go ahead and display it as a + # starting point. if ( classic.accounts.account_tournament_list is not None and classic.accounts.account_tournament_list[0] @@ -293,7 +289,7 @@ class CoopBrowserWindow(bui.MainWindow): # This will pull new data periodically, update timers, etc. self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() @@ -319,15 +315,17 @@ class CoopBrowserWindow(bui.MainWindow): def _preload_modules() -> None: """Preload modules we use; avoids hitches (called in bg thread).""" # pylint: disable=cyclic-import - import bauiv1lib.coop.gamebutton as _unused1 - import bauiv1lib.confirm as _unused2 - import bauiv1lib.account as _unused3 - import bauiv1lib.league.rankwindow as _unused4 - import bauiv1lib.account.viewer as _unused5 - import bauiv1lib.tournamentscores as _unused6 - import bauiv1lib.tournamententry as _unused7 - import bauiv1lib.play as _unused8 - import bauiv1lib.coop.tournamentbutton as _unused9 + import bauiv1lib.purchase as _unused1 + import bauiv1lib.coop.gamebutton as _unused2 + import bauiv1lib.confirm as _unused3 + import bauiv1lib.account as _unused4 + import bauiv1lib.league.rankwindow as _unused5 + import bauiv1lib.store.browser as _unused6 + import bauiv1lib.account.viewer as _unused7 + import bauiv1lib.tournamentscores as _unused8 + import bauiv1lib.tournamententry as _unused9 + import bauiv1lib.play as _unused10 + import bauiv1lib.coop.tournamentbutton as _unused11 def _update(self) -> None: plus = bui.app.plus @@ -358,8 +356,8 @@ class CoopBrowserWindow(bui.MainWindow): self._save_state() self._refresh() - # Also encourage a new tournament query since this will - # clear out our current results. + # Also encourage a new tournament query since this will clear out + # our current results. if not self._doing_tournament_query: self._last_tournament_query_time = None @@ -384,9 +382,7 @@ class CoopBrowserWindow(bui.MainWindow): self._doing_tournament_query = True plus.tournament_query( args={'source': 'coop window refresh', 'numScores': 1}, - callback=bui.WeakCallPartial( - self._on_tournament_query_response - ), + callback=bui.WeakCall(self._on_tournament_query_response), ) # Decrement time on our tournament buttons. @@ -461,8 +457,7 @@ class CoopBrowserWindow(bui.MainWindow): assert bui.app.classic is not None accounts = bui.app.classic.accounts if data is not None: - # This used to be the whole payload. - tournament_data = data['t'] + tournament_data = data['t'] # This used to be the whole payload. self._last_tournament_query_response_time = bui.apptime() else: tournament_data = None @@ -472,8 +467,7 @@ class CoopBrowserWindow(bui.MainWindow): self._tourney_data_up_to_date = True accounts.cache_tournament_info(tournament_data) - # Also cache the current tourney list/order for this - # account. + # Also cache the current tourney list/order for this account. accounts.account_tournament_list = ( plus.get_v1_account_state_num(), [e['tournamentID'] for e in tournament_data], @@ -483,11 +477,21 @@ class CoopBrowserWindow(bui.MainWindow): self._update_for_data(tournament_data) def _set_campaign_difficulty(self, difficulty: str) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.purchase import PurchaseWindow + plus = bui.app.plus assert plus is not None assert bui.app.classic is not None if difficulty != self._campaign_difficulty: + if ( + difficulty == 'hard' + and HARD_REQUIRES_PRO + and not bui.app.classic.accounts.have_pro_options() + ): + PurchaseWindow(items=['pro']) + return bui.getsound('gunCocking').play() if difficulty not in ('easy', 'hard'): print('ERROR: invalid campaign difficulty:', difficulty) @@ -535,12 +539,8 @@ class CoopBrowserWindow(bui.MainWindow): button_type='square', autoselect=True, enable_sound=False, - on_activate_call=bui.CallStrict( - self._set_campaign_difficulty, 'easy' - ), - on_select_call=bui.CallStrict( - self.sel_change, 'campaign', 'easyButton' - ), + on_activate_call=bui.Call(self._set_campaign_difficulty, 'easy'), + on_select_call=bui.Call(self.sel_change, 'campaign', 'easyButton'), color=( sel_color if self._campaign_difficulty == 'easy' @@ -570,12 +570,8 @@ class CoopBrowserWindow(bui.MainWindow): button_type='square', autoselect=True, enable_sound=False, - on_activate_call=bui.CallStrict( - self._set_campaign_difficulty, 'hard' - ), - on_select_call=bui.CallStrict( - self.sel_change, 'campaign', 'hardButton' - ), + on_activate_call=bui.Call(self._set_campaign_difficulty, 'hard'), + on_select_call=bui.Call(self.sel_change, 'campaign', 'hardButton'), color=( sel_color_hard if self._campaign_difficulty == 'hard' @@ -846,7 +842,7 @@ class CoopBrowserWindow(bui.MainWindow): highlight=False, border_opacity=0.0, color=(0.45, 0.4, 0.5), - on_select_call=bui.CallStrict( + on_select_call=bui.Call( self._on_row_selected, 'tournament' + str(i + 1) ), ) @@ -877,7 +873,7 @@ class CoopBrowserWindow(bui.MainWindow): h, v2, is_last_sel, - on_pressed=bui.WeakCallPartial(self.run_tournament), + on_pressed=bui.WeakCall(self.run_tournament), ) ) v -= 200 @@ -931,7 +927,7 @@ class CoopBrowserWindow(bui.MainWindow): highlight=False, border_opacity=0.0, color=(0.45, 0.4, 0.5), - on_select_call=bui.CallStrict(self._on_row_selected, 'custom'), + on_select_call=bui.Call(self._on_row_selected, 'custom'), ) bui.widget( edit=h_scroll, @@ -1037,12 +1033,6 @@ class CoopBrowserWindow(bui.MainWindow): from efro.util import strict_partial from bauiv1lib.confirm import ConfirmWindow - bui.app.analytics.submit_event( - ClassicAnalyticsEvent( - ClassicAnalyticsEvent.EventType.START_COOP_SESSION, extra=game - ) - ) - classic = bui.app.classic assert classic is not None @@ -1065,11 +1055,9 @@ class CoopBrowserWindow(bui.MainWindow): ) -> None: """Run the provided game.""" # pylint: disable=cyclic-import - import bacommon.docui.v1 as dui1 - from bauiv1lib.confirm import ConfirmWindow + from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.account.signin import show_sign_in_prompt - from bauiv1lib.store import StoreUIController plus = bui.app.plus assert plus is not None @@ -1087,44 +1075,21 @@ class CoopBrowserWindow(bui.MainWindow): cancel_button=False, width=460, height=130, - origin_widget=origin_widget, ) return required_purchases = bui.app.classic.required_purchases_for_game(game) - have_requirements = True - # Show pop-up to allow purchasing any required stuff we don't have. for purchase in required_purchases: if not purchase in bui.app.classic.purchases: - have_requirements = False - - if not have_requirements: - if plus.accounts.primary is None: - show_sign_in_prompt() - else: - # Push a custom store window onto our stack. We - # shouldn't use the standard auxiliary store window - # setup since this isn't a standard store window. - wait_for_connectivity( - on_connected=lambda: self.main_window_replace( - bui.CallStrict( - StoreUIController().create_window, - dui1.Request( - '/', - args={'unlockreqs': required_purchases}, - ), - origin_widget=origin_widget, - auxiliary_style=False, - ), - extra_type_id=( - StoreUIController.get_window_extra_type_id() - ), + if plus.accounts.primary is None: + show_sign_in_prompt() + else: + PurchaseWindow( + items=[purchase], origin_widget=origin_widget ) - ) - - return + return self._save_state() @@ -1135,11 +1100,9 @@ class CoopBrowserWindow(bui.MainWindow): """Run the provided tournament game.""" # pylint: disable=too-many-return-statements - import bacommon.docui.v1 as dui1 - + from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.account.signin import show_sign_in_prompt from bauiv1lib.tournamententry import TournamentEntryWindow - from bauiv1lib.store import StoreUIController plus = bui.app.plus assert plus is not None @@ -1205,42 +1168,16 @@ class CoopBrowserWindow(bui.MainWindow): # We gotta be missing *something* if its locked. assert required_purchases - have_requirements = True - for purchase in required_purchases: if purchase not in classic.purchases: - have_requirements = False - - if not have_requirements: - if plus.accounts.primary is None: - show_sign_in_prompt() - else: - # Push a custom store window onto our stack. We - # shouldn't use the standard auxiliary store - # window setup since this isn't a standard store - # window. - wait_for_connectivity( - on_connected=lambda: self.main_window_replace( - bui.CallStrict( - StoreUIController().create_window, - dui1.Request( - '/', - args={'unlockreqs': required_purchases}, - ), - origin_widget=tournament_button.button, - auxiliary_style=False, - ), - extra_type_id=( - StoreUIController.get_window_extra_type_id() - ), + if plus.accounts.primary is None: + show_sign_in_prompt() + else: + PurchaseWindow( + items=[purchase], + origin_widget=tournament_button.button, ) - ) - - # PurchaseWindow( - # items=[purchase], - # origin_widget=tournament_button.button, - # ) - return + return if tournament_button.time_remaining <= 0: bui.screenmessage( diff --git a/dist/ba_data/python/bauiv1lib/coop/gamebutton.py b/dist/ba_data/python/bauiv1lib/coop/gamebutton.py index 52b0254..dec0dc8 100644 --- a/dist/ba_data/python/bauiv1lib/coop/gamebutton.py +++ b/dist/ba_data/python/bauiv1lib/coop/gamebutton.py @@ -71,7 +71,7 @@ class GameButton: on_activate_call=self._on_press, button_type='square', autoselect=True, - on_select_call=bui.CallStrict(window.sel_change, row, game), + on_select_call=bui.Call(window.sel_change, row, game), ) bui.widget( edit=btn, @@ -188,7 +188,7 @@ class GameButton: # give a quasi-random update increment to spread the load.. self._update_timer = bui.AppTimer( 0.001 * (900 + random.randrange(200)), - bui.WeakCallStrict(self._update), + bui.WeakCall(self._update), repeat=True, ) self._update() diff --git a/dist/ba_data/python/bauiv1lib/coop/tournamentbutton.py b/dist/ba_data/python/bauiv1lib/coop/tournamentbutton.py index 6df1c80..e08c26d 100644 --- a/dist/ba_data/python/bauiv1lib/coop/tournamentbutton.py +++ b/dist/ba_data/python/bauiv1lib/coop/tournamentbutton.py @@ -52,7 +52,7 @@ class TournamentButton: label='', button_type='square', autoselect=True, - on_activate_call=bui.WeakCallStrict(self._pressed), + on_activate_call=bui.WeakCall(self._pressed), ) bui.widget( edit=btn, @@ -322,7 +322,7 @@ class TournamentButton: selectable=True, click_activate=True, autoselect=True, - on_activate_call=bui.WeakCallStrict(self._show_leader), + on_activate_call=bui.WeakCall(self._show_leader), size=(170 / 1.4, 40), h_align='center', text='-', @@ -362,7 +362,7 @@ class TournamentButton: autoselect=True, up_widget=self.current_leader_name_text, text_scale=0.6, - on_activate_call=bui.WeakCallStrict(self._show_scores), + on_activate_call=bui.WeakCall(self._show_scores), ) # We handle reselection manually for these so no ids. bui.widget(edit=self.more_scores_button, allow_preserve_selection=False) @@ -411,7 +411,7 @@ class TournamentButton: flatness=1.0, ) self._lock_update_timer = bui.AppTimer( - 1.03, bui.WeakCallStrict(self._update_lock_state), repeat=True + 1.03, bui.WeakCall(self._update_lock_state), repeat=True ) def _pressed(self) -> None: @@ -510,7 +510,7 @@ class TournamentButton: x_offs2c = x_offs2 + 50 # Fetch prize range and trophy strings. - pr1, pv1, pr2, pv2, pr3, pv3 = classic.get_tournament_prize_strings( + (pr1, pv1, pr2, pv2, pr3, pv3) = classic.get_tournament_prize_strings( entry, include_tickets=False ) diff --git a/dist/ba_data/python/bauiv1lib/discord.py b/dist/ba_data/python/bauiv1lib/discord.py index f432916..40aa34f 100644 --- a/dist/ba_data/python/bauiv1lib/discord.py +++ b/dist/ba_data/python/bauiv1lib/discord.py @@ -114,7 +114,7 @@ class DiscordWindow(bui.Window): autoselect=True, label=bui.Lstr(resource='discordJoinText'), text_scale=1.0, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( bui.open_url, 'https://ballistica.net/discord' ), ) diff --git a/dist/ba_data/python/bauiv1lib/docui/__init__.py b/dist/ba_data/python/bauiv1lib/docui/__init__.py deleted file mode 100644 index 189114b..0000000 --- a/dist/ba_data/python/bauiv1lib/docui/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -"""Functionality for using doc-ui on top of bauiv1.""" - -from bauiv1lib.docui._controller import DocUIController, DocUILocalAction -from bauiv1lib.docui._window import DocUIWindow - -__all__ = [ - 'DocUIController', - 'DocUIWindow', - 'DocUILocalAction', -] diff --git a/dist/ba_data/python/bauiv1lib/docui/_controller.py b/dist/ba_data/python/bauiv1lib/docui/_controller.py deleted file mode 100644 index a024c26..0000000 --- a/dist/ba_data/python/bauiv1lib/docui/_controller.py +++ /dev/null @@ -1,971 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Controller functionality for DocUI.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, assert_never -from dataclasses import dataclass -from enum import Enum -import weakref - -from efro.util import asserttype -from efro.error import CleanError, CommunicationError -from efro.dataclassio import dataclass_to_json, dataclass_from_json -from bacommon.docui import ( - DocUIRequestTypeID, - UnknownDocUIRequest, - DocUIResponseTypeID, - UnknownDocUIResponse, - DocUIWebRequest, - DocUIWebResponse, -) -import bauiv1 as bui - -from bauiv1lib.docui._window import DocUIWindow - -if TYPE_CHECKING: - from typing import Callable - - import bacommon.docui.v1 - from bacommon.docui import DocUIRequest, DocUIResponse - import bacommon.clienteffect as clfx - - from bauiv1lib.docui import v1prep - - -class _WinState(Enum): - """Per-window state.""" - - FETCHING_FRESH_REQUEST = 0 - REDISPLAYING_OLD_STATE = 1 - REFRESHING = 2 - ERRORED = 3 - IDLE = 4 - - -@dataclass -class _WinData: - state: _WinState - refresh_timer: bui.AppTimer | None = None - - -class DocUIController: - """Manages interactions between DocUI clients and servers. - - Can include logic to handle all requests locally or can submit them - to be handled by some server or can do some combination thereof. - """ - - class ErrorType(Enum): - """Types of errors that can occur in request processing.""" - - GENERIC = 'generic' - UNDER_CONSTRUCTION = 'under_construction' - COMMUNICATION_ERROR = 'communication' - NEED_UPDATE = 'need_update' - - def fulfill_request(self, request: DocUIRequest) -> DocUIResponse: - """Handle request fulfillment. - - Expected to be overridden by child classes. - - Be aware that this will always be called in a background thread. - - This method is expected to always return a response, even in the - case of errors. Use - :meth:`~bauiv1lib.docui.DocUIController.error_response()` to - translate error conditions to responses. - - The one exception to this rule (no pun intended) is the - :class:`efro.error.CleanError` exception. This can be raised as a - quick and dirty way to show custom error messages. The code - ``raise CleanError('Something broke.')`` will have the same - effect as ``return self.error_response(custom_message='Something - broke.')``. - """ - raise NotImplementedError() - - def local_action(self, action: DocUILocalAction) -> None: - """Do something locally on behalf of the doc-ui. - - Controller classes can override this to expose named actions - that can be triggered by doc-ui button presses, responses, - etc. - - Of course controllers can also perform arbitrary local actions - alongside their normal request fulfillment; this is simply a way - to do so without needing to provide actual ui pages alongside. - - Be *very* careful and focused with what you expose here, - especially if your doc-ui pages are coming from untrusted - sources. Generally things like launching or joining games are - good candidates for local actions. - """ - - def fulfill_request_web( - self, request: DocUIRequest, url: str - ) -> DocUIResponse: - """Fulfill a request by sending it to a webserver.""" - import bacommon.docui.v1 as dui1 - - import urllib3.util - - if not isinstance(request, dui1.Request): - raise RuntimeError(f'Unsupported docui request: {type(request)}') - - upool = bui.app.net.urllib3pool - - # Allow compressed results. - headers = urllib3.util.make_headers(accept_encoding=True) - - # Bundle our doc-ui request with some extra stuff that might - # be relevant to a remote server (language we're using, etc.). - webrequest = DocUIWebRequest( - doc_ui_request=request, - locale=bui.app.locale.current_locale, - engine_build_number=bui.app.env.engine_build_number, - ) - - try: - # Map docui GET requests to http GET and POST to POST. - if request.method is dui1.RequestMethod.GET: - # For GET we embed the request into a url param. - raw_response = upool.request( - 'GET', - url, - fields={ - 'doc_ui_web_request': dataclass_to_json(webrequest) - }, - headers=headers, - ) - - elif request.method is dui1.RequestMethod.POST: - # for POST we send the webrequest as json in body. - headers['Content-Type'] = 'application/json' - raw_response = upool.request( - 'POST', - url, - headers=headers, - body=dataclass_to_json(webrequest), - ) - elif request.method is dui1.RequestMethod.UNKNOWN: - raise RuntimeError('Unknown request method.') - else: - assert_never(request.method) - - try: - # We use 'lossy' here so response versions or elements - # that we don't know about will come through as - # 'Unknown' types instead of erroring completely. - webresponse = dataclass_from_json( - DocUIWebResponse, raw_response.data.decode(), lossy=True - ) - if ( - webresponse.error is None - and webresponse.doc_ui_response is None - ): - raise RuntimeError( - 'Invalid webresponse includes neither error' - ' nor doc-ui-response.' - ) - except Exception as exc: - bui.netlog.info( - 'Error reading docui web-response.', exc_info=True - ) - raise RuntimeError('Error reading docui web-response.') from exc - - # For now, consider all errors communication errors (should - # result in retry buttons in some cases). Can get more - # specific in the future for cases where retries would not - # help. - if raw_response.status != 200: - - # If the response bundled an error, log it. - if webresponse.error is not None: - bui.netlog.info( - 'doc-ui http request returned error: %s', - webresponse.error, - ) - return self.error_response( - request, self.ErrorType.COMMUNICATION_ERROR - ) - - except Exception: - # For now, consider all errors communication errors (should - # result in retry buttons in some cases). Can get more - # specific in the future for cases where retries would not - # help. - bui.netlog.info('Error in docui http request.', exc_info=True) - return self.error_response( - request, self.ErrorType.COMMUNICATION_ERROR - ) - - assert webresponse.doc_ui_response is not None - return webresponse.doc_ui_response - - def fulfill_request_cloud( - self, request: DocUIRequest, domain: str - ) -> DocUIResponse: - """Fulfill a request by sending it to ballistica's cloud. - - :meta private: - """ - import bacommon.cloud - - try: - plus = bui.app.plus - if plus is None: - raise RuntimeError('Plus not available.') - - account = plus.accounts.primary - if account is not None: - with account: - mresponse = plus.cloud.send_message( - bacommon.cloud.FulfillDocUIRequest( - request=request, domain=domain - ) - ) - else: - mresponse = plus.cloud.send_message( - bacommon.cloud.FulfillDocUIRequest( - request=request, domain=domain - ) - ) - assert isinstance(mresponse, bacommon.cloud.FulfillDocUIResponse) - - return mresponse.response - - except CommunicationError: - # Label comm-errors so we can possibly show retry buttons. - return self.error_response( - request, self.ErrorType.COMMUNICATION_ERROR - ) - except Exception: - return self.error_response(request) - - def error_response( - self, - request: DocUIRequest, - error_type: ErrorType = ErrorType.GENERIC, - custom_message: str | None = None, - ) -> DocUIResponse: - """Build a simple error message page. - - A message is included based on ``error_type``. Pass - ``custom_message`` to override this. - - Messages will be translated to the client language using the - 'serverResponses' Lstr translation category. - """ - import bacommon.docui.v1 as dui1 - - error_msg: bui.Lstr | None = None - error_msg_simple: str | None = None - - status_code = dui1.ResponseStatus.UNKNOWN_ERROR - - if custom_message is not None: - error_msg_simple = custom_message - else: - if error_type is self.ErrorType.GENERIC: - error_msg_simple = 'An error has occurred.' - elif error_type is self.ErrorType.NEED_UPDATE: - error_msg_simple = 'You must update the app to view this.' - elif error_type is self.ErrorType.UNDER_CONSTRUCTION: - error_msg_simple = 'Under construction - check back soon.' - elif error_type is self.ErrorType.COMMUNICATION_ERROR: - status_code = dui1.ResponseStatus.COMMUNICATION_ERROR - error_msg_simple = 'Error talking to server.' - else: - assert_never(error_type) - if error_msg_simple is not None: - error_msg = bui.Lstr( - translate=('serverResponses', error_msg_simple) - ) - assert error_msg is not None - - debug = False - - # Give a retry button for comm-errors on GET requests (POSTs may - # have unintentional side-effects so holding off on those for - # now). - do_retry = ( - isinstance(request, dui1.Request) - and request.method is dui1.RequestMethod.GET - and status_code is dui1.ResponseStatus.COMMUNICATION_ERROR - ) - - return dui1.Response( - status=status_code, - page=dui1.Page( - title=bui.Lstr(resource='errorText').as_json(), - title_is_lstr=True, - center_vertically=True, - rows=[ - dui1.ButtonRow( - buttons=[ - dui1.Button( - bui.Lstr( - resource=( - 'retryText' if do_retry else 'okText' - ) - ).as_json(), - ( - dui1.Replace( - asserttype(request, dui1.Request) - ) - if do_retry - else dui1.Local(close_window=True) - ), - label_is_lstr=True, - default=True, - style=dui1.ButtonStyle.MEDIUM, - size=(130, 50), - padding_left=200, - padding_right=200, - padding_top=100, - decorations=[ - dui1.Text( - error_msg.as_json(), - is_lstr=True, - position=(0, 80), - size=(480, 50), - highlight=False, - debug=debug, - ), - ], - debug=debug, - ), - ], - center_content=True, - debug=debug, - ), - ], - ), - ) - - def create_window( - self, - request: DocUIRequest, - *, - transition: str | None = 'in_right', - origin_widget: bui.Widget | None = None, - auxiliary_style: bool = True, - uiopenstateid: str | None = None, - suppress_win_extra_type_warning: bool = False, - ) -> DocUIWindow: - """Create a new window to handle a request.""" - assert bui.in_logic_thread() - - # Create a shiny new window. - win = DocUIWindow( - self, - request, - transition=transition, - origin_widget=origin_widget, - auxiliary_style=auxiliary_style, - uiopenstateid=uiopenstateid, - suppress_win_extra_type_warning=suppress_win_extra_type_warning, - ) - self._set_win_data(win, _WinData(_WinState.FETCHING_FRESH_REQUEST)) - - # Lock its ui and kick off a bg task to populate it. - win.lock_ui() - bui.app.threadpool.submit_no_wait( - bui.CallStrict( - self._process_request_in_bg, - request, - weakwin=weakref.ref(win), - uiscale=bui.app.ui_v1.uiscale, - scroll_width=win.scroll_width, - scroll_height=win.scroll_height, - idprefix=win.main_window_id_prefix, - immediate=False, - ) - ) - return win - - def save_window_shared_state( - self, window: DocUIWindow, state: dict - ) -> None: - """Called when a window shared state is being saved.""" - del window, state # Unused. - - def restore_window_shared_state( - self, window: DocUIWindow, state: dict - ) -> None: - """Called when a window shared state is being restored.""" - del window, state # Unused. - - @classmethod - def get_window_extra_type_id(cls) -> str: - """Return a string suitable for the ``window_extra_type_id`` arg to - :meth:`~bauiv1.UIV1AppSubsystem.auxiliary_window_activate()`. - - This ensures your doc-ui window is identified distinctly from - other doc-ui windows for navigation purposes. - """ - # Include the full path of our controller class. - return f'docui:{cls.__module__}.{cls.__qualname__}' - - def restore( - self, - win: DocUIWindow, - *, - last_response: DocUIResponse | None, - has_had_response: bool, - ) -> DocUIWindow: - """Restore a window from previous state. - - May immediately display old results or may kick off a new - request. - """ - import bacommon.docui.v1 as dui1 - - assert bui.in_logic_thread() - - explicit_response: DocUIResponse | None = None - explicit_error: DocUIController.ErrorType | None = None - - if last_response is not None: - # Re-prep our restored response so we have something to show - # immediately. We'll then fetch an updated version in the - # background to get the latest version. - explicit_response = last_response - else: - # We have no previous response to restore. Fetch a new one. - - # If the current request is a POST, never auto-refetch. Just - # build an error response. - assert isinstance(win.request, dui1.Request) - if win.request.method is dui1.RequestMethod.POST: - # Do we want a specific error for this? Though this case - # should be rare I think. - explicit_error = self.ErrorType.GENERIC - else: - explicit_error = None - - # We're either errored or redisplaying an old state. - if explicit_error is None: - self._set_win_data(win, _WinData(_WinState.REDISPLAYING_OLD_STATE)) - else: - self._set_win_data(win, _WinData(_WinState.ERRORED)) - - # Lock the ui and kick off this update. - win.lock_ui() - bui.app.threadpool.submit_no_wait( - bui.CallStrict( - self._process_request_in_bg, - win.request, - weakwin=weakref.ref(win), - uiscale=bui.app.ui_v1.uiscale, - scroll_width=win.scroll_width, - scroll_height=win.scroll_height, - idprefix=win.main_window_id_prefix, - # If this window has had a response already, snap things - # in immediately with no transitions. - immediate=has_had_response, - explicit_error=explicit_error, - explicit_response=explicit_response, - ) - ) - return win - - def replace( - self, - win: DocUIWindow, - request: DocUIRequest, - *, - origin_widget: bui.Widget | None = None, - is_refresh: bool = False, - ) -> None: - """Kick off a request to replace existing window contents.""" - import bacommon.docui.v1 as dui1 - - assert bui.in_logic_thread() - - win.request = request - - requesttype = request.get_type_id() - - if requesttype is DocUIRequestTypeID.V1: - assert isinstance(win.request, dui1.Request) - - self._set_win_data( - win, - ( - _WinData( - _WinState.REFRESHING - if is_refresh - else _WinState.FETCHING_FRESH_REQUEST - ) - ), - ) - - # Lock the ui and kick off this update. - win.lock_ui(origin_widget) - bui.app.threadpool.submit_no_wait( - bui.CallStrict( - self._process_request_in_bg, - win.request, - weakwin=weakref.ref(win), - uiscale=bui.app.ui_v1.uiscale, - scroll_width=win.scroll_width, - scroll_height=win.scroll_height, - idprefix=win.main_window_id_prefix, - immediate=True, - ) - ) - elif requesttype is DocUIRequestTypeID.UNKNOWN: - assert isinstance(win.request, UnknownDocUIRequest) - # Got a request type we don't know. Show a 'need a newer - # build' error. - - self._set_win_data(win, _WinData(_WinState.ERRORED)) - - # Lock the ui and kick off this update. - win.lock_ui(origin_widget) - bui.app.threadpool.submit_no_wait( - bui.CallStrict( - self._process_request_in_bg, - win.request, - weakwin=weakref.ref(win), - uiscale=bui.app.ui_v1.uiscale, - scroll_width=win.scroll_width, - scroll_height=win.scroll_height, - idprefix=win.main_window_id_prefix, - immediate=True, - explicit_error=self.ErrorType.NEED_UPDATE, - ) - ) - else: - assert_never(requesttype) - - def run_action( - self, - window: DocUIWindow, - widgetid: str | None, - action: bacommon.docui.v1.Action | None, - is_timed: bool = False, - ) -> None: - """Called when a button is pressed in a v1 ui.""" - # pylint: disable=too-many-branches - # pylint: disable=cyclic-import - - import bacommon.docui.v1 as dui - - assert bui.in_logic_thread() - - # If locked, been and tell them to try again. - if window.locked: - bui.getsound('error').play() - bui.screenmessage( - bui.Lstr(resource='pageRefreshingTryAgainText'), color=(1, 0, 0) - ) - return - - widget: bui.Widget | None - - if widgetid is not None: - # Find the associated button. - widget = bui.widget_by_id(widgetid) - if widget is None: - bui.uilog.warning( - 'DocUI button press widget not found: %s (not expected)', - widgetid, - ) - return - else: - widget = None - - # Play error beeps on buttons with no actions assigned to let - # the user know nothing is supposed to happen. - if action is None: - bui.getsound('error').play() - return - - action_type = action.get_type_id() - - if action_type is dui.ActionTypeID.BROWSE: - assert isinstance(action, dui.Browse) - if is_timed: - # Don't let timers pop up new windows. Untrusted servers - # would have a field-day with this. - bui.uilog.warning( - 'Ignoring BROWSE action (disallowed in timed actions).' - ) - else: - if action.default_sound: - bui.getsound('swish').play() - window.main_window_replace( - lambda: self.create_window( - action.request, - origin_widget=widget, - auxiliary_style=False, - suppress_win_extra_type_warning=True, - ) - ) - - self._run_immediate_effects_and_actions( - client_effects=action.immediate_client_effects, - local_action=action.immediate_local_action, - local_action_args=action.immediate_local_action_args, - widget=widget, - window=window, - is_timed=is_timed, - ) - - elif action_type is dui.ActionTypeID.REPLACE: - assert isinstance(action, dui.Replace) - - # Play default click sound only if this is coming from a - # button. - if widget is not None and action.default_sound: - bui.getsound('click01').play() - - # Force a state save so if our UI gets rebuilt with the same - # IDs we'll wind up with the same selection and whatnot. - window.main_window_save_shared_state() - self.replace(window, action.request, origin_widget=widget) - - self._run_immediate_effects_and_actions( - client_effects=action.immediate_client_effects, - local_action=action.immediate_local_action, - local_action_args=action.immediate_local_action_args, - widget=widget, - window=window, - is_timed=is_timed, - ) - - elif action_type is dui.ActionTypeID.LOCAL: - assert isinstance(action, dui.Local) - if action.default_sound: - if action.close_window: - # Always play close-window swish, even if we don't have - # a source button. - bui.getsound('swish').play() - else: - # Only play click sound if this is coming from a button. - if widget is not None: - bui.getsound('click01').play() - if action.close_window: - window.main_window_back() - - self._run_immediate_effects_and_actions( - client_effects=action.immediate_client_effects, - local_action=action.immediate_local_action, - local_action_args=action.immediate_local_action_args, - widget=widget, - window=window, - is_timed=is_timed, - ) - elif action_type is dui.ActionTypeID.UNKNOWN: - assert isinstance(action, dui.UnknownAction) - bui.screenmessage('Unknown action.', color=(1, 0, 0)) - bui.getsound('error').play() - else: - # Make sure we handle all options. - assert_never(action_type) - - def _run_immediate_effects_and_actions( - self, - *, - client_effects: list[clfx.Effect], - local_action: str | None, - local_action_args: dict | None, - widget: bui.Widget | None, - window: DocUIWindow, - is_timed: bool, - ) -> None: - # We don't allow timed actions to trigger immediate - # client-effects/local-actions. It would be too easy for such - # things to get unintentionally re-triggered when navigating - # back/etc. We only want those to happen due to direct button - # presses or initial (non-refresh) responses, which should keep - # things feeling mostly intentional. - if is_timed: - if client_effects: - bui.uilog.warning( - 'Ignoring client-effects (disallowed in timed actions).' - ) - if local_action is not None: - bui.uilog.warning( - 'Ignoring local-action (disallowed in timed actions).' - ) - return - - if bui.app.classic is not None and client_effects: - bui.app.classic.run_bs_client_effects(client_effects) - if local_action is not None: - try: - self.local_action( - DocUILocalAction( - name=local_action, - args=( - {} - if local_action_args is None - else local_action_args - ), - widget=widget, - window=window, - ) - ) - except Exception: - bui.uilog.exception( - 'Error running local-action %s.', - local_action, - ) - - def _get_win_data(self, window: DocUIWindow) -> _WinData: - val = getattr(window, '_wcdata') - assert isinstance(val, _WinData) - return val - - def _set_win_data(self, window: DocUIWindow, data: _WinData) -> None: - setattr(window, '_wcdata', data) - - def _process_request_in_bg( - self, - request: DocUIRequest, - *, - weakwin: weakref.ref[DocUIWindow], - uiscale: bui.UIScale, - scroll_width: float, - scroll_height: float, - idprefix: str, - immediate: bool, - explicit_error: ErrorType | None = None, - explicit_response: DocUIResponse | None = None, - ) -> None: - """Wrangle a request from within a background thread. - - This will always return a response, even on error conditions. - """ - # pylint: disable=too-many-locals - # pylint: disable=cyclic-import - import bacommon.docui.v1 as dui1 - from bauiv1lib.docui import v1prep - - assert not bui.in_logic_thread() - - response: DocUIResponse | None = None - error: DocUIController.ErrorType | None = None - - if explicit_error is not None: - error = explicit_error - elif explicit_response is not None: - response = explicit_response - else: - try: - response = self.fulfill_request(request) - except CleanError as exc: - # The one exception case we officially handle. Translate - # this to an error response with a custom message. - response = self.error_response(request, custom_message=str(exc)) - - except Exception: - # fulfill_request is expected to gracefully return even - # on errors. Make noise if it didn't. - bui.uilog.exception( - 'Error in fulfill_request().\n' - 'It should always return responses; not throw exceptions.\n' - 'Use error_response() when errors occur.', - exc_info=True, - ) - error = self.ErrorType.GENERIC - - # Validate any response we got. - if response is not None: - assert error is None - responsetype = response.get_type_id() - - if responsetype is DocUIResponseTypeID.V1: - - assert isinstance(response, dui1.Response) - - # If they require a build-number newer than us, say so. - minbuild = response.minimum_engine_build - if ( - minbuild is not None - and minbuild > bui.app.env.engine_build_number - ): - error = self.ErrorType.NEED_UPDATE - - elif responsetype is DocUIResponseTypeID.UNKNOWN: - assert isinstance(response, UnknownDocUIResponse) - bui.uilog.debug( - 'Got unsupported docui response.', exc_info=True - ) - error = self.ErrorType.NEED_UPDATE - response = None - else: - # Make sure we cover all types we're aware of. - assert_never(responsetype) - - if error is not None: - response = self.error_response(request, error) - - # Currently must be v1 if it made it to here. - assert isinstance(response, dui1.Response) - - pageprep = v1prep.prep_page( - response.page, - uiscale=uiscale, - scroll_width=scroll_width, - scroll_height=scroll_height, - immediate=immediate, - idprefix=idprefix, - ) - - # Go ahead and just push the response along with our weakref - # back to the logic thread for handling. We could quick-out here - # if the window is dead, but wrangling its refs here could - # theoretically lead to it being deallocated here which could be - # problematic. - bui.pushcall( - bui.CallStrict( - self._handle_response_in_ui_thread, - response, - weakwin, - pageprep, - ), - from_other_thread=True, - ) - - def _handle_response_in_ui_thread( - self, - response: DocUIResponse, - weakwin: weakref.ref[DocUIWindow], - pageprep: v1prep.PagePrep, - ) -> None: - import bacommon.docui.v1 as dui1 - - assert bui.in_logic_thread() - - # If our target window died since we made the request, no - # biggie. - win = weakwin() - if win is None: - return - - # Currently should only be sending ourself v1 responses here. - assert isinstance(response, dui1.Response) - - win.unlock_ui() - win.set_last_response( - response, - response.status == dui1.ResponseStatus.SUCCESS, - ) - - # Set the UI. - win.instantiate_ui(pageprep) - - state = self._get_win_data(win).state - - # Run client-effects and local-actions ONLY after fresh requests - # (don't want sounds and other actions firing when we navigate - # back or resize a window). - if state is _WinState.FETCHING_FRESH_REQUEST: - if response.client_effects and bui.app.classic is not None: - bui.app.classic.run_bs_client_effects(response.client_effects) - if response.local_action is not None: - try: - self.local_action( - DocUILocalAction( - name=response.local_action, - args=( - {} - if response.local_action_args is None - else response.local_action_args - ), - widget=None, - window=win, - ) - ) - except Exception: - bui.uilog.exception( - 'Error running local-action %s.', - response.local_action, - ) - - # Possibly take further action depending on state. - if state is _WinState.REDISPLAYING_OLD_STATE: - # Ok; we're done showing old state. For POST this is as far - # as we go (don't want to repeat POST effects), but for GET - # we can now kick off a refresh to swap in the latest - # version of the page. - assert isinstance(win.request, dui1.Request) - if win.request.method is dui1.RequestMethod.GET: - self.replace(win, win.request, is_refresh=True) - elif ( - win.request.method is dui1.RequestMethod.POST - or win.request.method is dui1.RequestMethod.UNKNOWN - ): - self._set_idle_and_schedule_timed_action(response, weakwin) - else: - assert_never(win.request.method) - - elif state is _WinState.ERRORED or state is _WinState.IDLE: - pass - elif ( - state is _WinState.FETCHING_FRESH_REQUEST - or state is _WinState.REFRESHING - ): - self._set_idle_and_schedule_timed_action(response, weakwin) - else: - assert_never(state) - - def _set_idle_and_schedule_timed_action( - self, response: DocUIResponse, weakwin: weakref.ref[DocUIWindow] - ) -> None: - import bacommon.docui.v1 as dui1 - - win = weakwin() - assert win is not None - assert self._get_win_data(win).state is not _WinState.IDLE - assert isinstance(response, dui1.Response) - - refresh_timer: bui.AppTimer | None = None - - if response.timed_action is not None: - - # Limit delay to .25 seconds or more to prevent excessive - # churn. Can revisit if there is a strong use case. - refresh_timer = bui.AppTimer( - max(0.250, response.timed_action_delay), - bui.WeakCallStrict( - self._run_timed_action, - weakwin, - response.timed_action, - ), - ) - self._set_win_data(win, _WinData(_WinState.IDLE, refresh_timer)) - - def _run_timed_action( - self, - weakwin: weakref.ref[DocUIWindow], - action: bacommon.docui.v1.Action, - ) -> None: - # If our target window died since we set this timer, no biggie. - win = weakwin() - if win is None: - return - - state = self._get_win_data(win).state - if state is not _WinState.IDLE: - bui.uilog.warning( - 'win has non-idle state in _run_timed_action; not expected' - ) - return - self.run_action(win, widgetid=None, action=action, is_timed=True) - - -@dataclass -class DocUILocalAction: - """Context for a local-action.""" - - name: str - args: dict - widget: bui.Widget | None - window: DocUIWindow diff --git a/dist/ba_data/python/bauiv1lib/docui/_window.py b/dist/ba_data/python/bauiv1lib/docui/_window.py deleted file mode 100644 index cd858fd..0000000 --- a/dist/ba_data/python/bauiv1lib/docui/_window.py +++ /dev/null @@ -1,546 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""UIs provided by the cloud (similar-ish to html in concept).""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, override, assert_never - -import bauiv1 as bui - -from bacommon.docui import DocUIRequestTypeID, DocUIResponseTypeID -from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top - -if TYPE_CHECKING: - from typing import Callable - - from bacommon.docui import DocUIRequest, DocUIResponse - import bacommon.docui.v1 - from bauiv1lib.docui._controller import DocUIController - from bauiv1lib.docui import v1prep - - -class DocUIWindow(bui.MainWindow): - """Window showing doc-ui content.""" - - def __init__( - self, - controller: DocUIController, - request: DocUIRequest, - *, - transition: str | None = 'in_right', - origin_widget: bui.Widget | None = None, - auxiliary_style: bool = True, - restored: bool = False, - uiopenstateid: str | None = None, - suppress_win_extra_type_warning: bool = False, - has_had_response: bool = False, - ): - # pylint: disable=too-many-statements - ui = bui.app.ui_v1 - - self._uiopenstate = ( - None if uiopenstateid is None else bui.UIOpenState(uiopenstateid) - ) - - self._locked = False - - self._restored = restored - - # Note: our windows and states both hold strong references to - # the controller, so we need to make sure the opposite is not - # true to avoid cycles. - self.controller = controller - - self._suppress_win_extra_type_warning = suppress_win_extra_type_warning - self._request = request - self._request_state_id = self._default_state_id(request) - - self._last_response: DocUIResponse | None = None - self._last_response_success: bool = False - self._last_response_shared_state_id: str | None = None - self._has_had_response: bool = has_had_response - - # We want to display differently whether we're an auxiliary - # window or not, but unfortunately that value is not yet - # available until we're added to the main-window-stack so it - # must be explicitly passed in. - self._auxiliary_style = auxiliary_style - - # Calc scale and size for our backing window. For medium & large - # ui-scale we aim for a window small enough to always be fully - # visible on-screen and for small mode we aim for a window big - # enough that we never see the window edges; only the window - # texture covering the whole screen. - uiscale = ui.uiscale - self._width = ( - 1400 - if uiscale is bui.UIScale.SMALL - else 1100 if uiscale is bui.UIScale.MEDIUM else 1200 - ) - self._height = ( - 1200 - if uiscale is bui.UIScale.SMALL - else 700 if uiscale is bui.UIScale.MEDIUM else 800 - ) - self._root_scale = ( - 1.45 - if uiscale is bui.UIScale.SMALL - else 0.9 if uiscale is bui.UIScale.MEDIUM else 0.8 - ) - - # Do some fancy math to calculate our visible area; this will be - # limited by the screen size in small mode and our backing size - # otherwise. - screensize = bui.get_virtual_screen_size() - self._vis_width = min( - self._width - 150, screensize[0] / self._root_scale - ) - self._vis_height = min( - self._height - 80, screensize[1] / self._root_scale - ) - self._vis_top = 0.5 * self._height + 0.5 * self._vis_height - self._vis_left = 0.5 * self._width - 0.5 * self._vis_width - - self._scroll_width = self._vis_width - self._scroll_left = self._vis_left + 0.5 * ( - self._vis_width - self._scroll_width - ) - # Go with full-screen scrollable aread in small ui. - self._scroll_height = self._vis_height - ( - -1 if uiscale is bui.UIScale.SMALL else 43 - ) - self._scroll_bottom = ( - self._vis_top - - (-1 if uiscale is bui.UIScale.SMALL else 32) - - self._scroll_height - ) - - # Nudge our vis area up a bit when we can see the full backing - # (visual fudge factor). - if uiscale is not bui.UIScale.SMALL: - self._vis_top += 12.0 - - super().__init__( - root_widget=bui.containerwidget( - size=(self._width, self._height), - toolbar_visibility='menu_full', - toolbar_cancel_button_style=( - 'close' if auxiliary_style else 'back' - ), - scale=self._root_scale, - ), - transition=transition, - origin_widget=origin_widget, - # We respond to screen size changes only at small ui-scale; - # in other cases we assume our window remains fully visible - # always (flip to windowed mode and resize the app window to - # confirm this). - refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, - ) - # Avoid complaints if nothing is selected under us. - bui.widget(edit=self._root_widget, allow_preserve_selection=False) - - self._subcontainer: bui.Widget | None = None - - self._scrollwidget = bui.scrollwidget( - parent=self._root_widget, - highlight=True, # Will turn off once we have UI. - size=(self._scroll_width, self._scroll_height), - position=(self._scroll_left, self._scroll_bottom), - border_opacity=0.4, - center_small_content_horizontally=True, - claims_left_right=True, - ) - bui.widget(edit=self._scrollwidget, autoselect=True) - - # With full-screen scrolling, fade content as it approaches - # toolbars. - if uiscale is bui.UIScale.SMALL and bool(True): - scroll_fade_top( - self._root_widget, - self._width * 0.5 - self._scroll_width * 0.5, - self._scroll_bottom, - self._scroll_width, - self._scroll_height, - ) - scroll_fade_bottom( - self._root_widget, - self._width * 0.5 - self._scroll_width * 0.5, - self._scroll_bottom, - self._scroll_width, - self._scroll_height, - ) - - # Title. - self._title = bui.textwidget( - parent=self._root_widget, - position=(self._width * 0.5, self._vis_top - 20), - size=(0, 0), - text='', - color=ui.title_color, - scale=0.9 if uiscale is bui.UIScale.SMALL else 1.0, - # Make sure we avoid overlapping meters in small mode. - maxwidth=(130 if uiscale is bui.UIScale.SMALL else 200), - h_align='center', - v_align='center', - ) - # Needed to display properly over scrolled content. - bui.widget(edit=self._title, depth_range=(0.9, 1.0)) - - # For small UI-scale we use the system back/close button; - # otherwise we make our own. - if uiscale is bui.UIScale.SMALL: - bui.containerwidget( - edit=self._root_widget, on_cancel_call=self.main_window_back - ) - self._back_button: bui.Widget | None = None - else: - self._back_button = bui.buttonwidget( - parent=self._root_widget, - id=f'{self.main_window_id_prefix}|close', - scale=0.8, - position=(self._vis_left + 2, self._vis_top - 35), - size=(50, 50) if auxiliary_style else (60, 55), - extra_touch_border_scale=2.0, - button_type=None if auxiliary_style else 'backSmall', - on_activate_call=self.main_window_back, - autoselect=True, - label=bui.charstr( - bui.SpecialChar.CLOSE - if auxiliary_style - else bui.SpecialChar.BACK - ), - ) - bui.containerwidget( - edit=self._root_widget, cancel_button=self._back_button - ) - - # Show our vis-area bounds (for debugging). - if bool(False): - # Skip top-left since its always overlapping back/close - # buttons. - if bool(False): - bui.textwidget( - parent=self._root_widget, - position=(self._vis_left, self._vis_top), - size=(0, 0), - color=(1, 1, 1, 0.5), - scale=0.5, - text='TL', - h_align='left', - v_align='top', - ) - bui.textwidget( - parent=self._root_widget, - position=(self._vis_left + self._vis_width, self._vis_top), - size=(0, 0), - color=(1, 1, 1, 0.5), - scale=0.5, - text='TR', - h_align='right', - v_align='top', - ) - bui.textwidget( - parent=self._root_widget, - position=(self._vis_left, self._vis_top - self._vis_height), - size=(0, 0), - color=(1, 1, 1, 0.5), - scale=0.5, - text='BL', - h_align='left', - v_align='bottom', - ) - bui.textwidget( - parent=self._root_widget, - position=( - self._vis_left + self._vis_width, - self._vis_top - self._vis_height, - ), - size=(0, 0), - scale=0.5, - color=(1, 1, 1, 0.5), - text='BR', - h_align='right', - v_align='bottom', - ) - - self._spinner: bui.Widget | None = None - - if not suppress_win_extra_type_warning: - bui.pushcall(bui.WeakCallStrict(self._sanity_check_win_extra_type)) - - def _sanity_check_win_extra_type(self) -> None: - # There will be lots of windows with this same type, so we really - # need the user to provide extra-type-ids so our logic can tell - # all of us apart for navigation purposes. - if not self.main_window_extra_type_id: - bui.uilog.warning( - '%s created by %s was not assigned an extra-type-id.' - ' Always pass a "win_extra_type_id" when calling' - ' `auxiliary_window_activate()` with a DocUIWindow.', - type(self).__name__, - type(self.controller), - ) - - @property - def request(self) -> DocUIRequest: - """The current request. - - Should only be accessed from the logic thread while the ui is - unlocked. - """ - assert bui.in_logic_thread() - if self._request is None: - raise RuntimeError('No request is set.') - return self._request - - @request.setter - def request(self, request: DocUIRequest) -> None: - assert bui.in_logic_thread() - self._request = request - self._request_state_id = self._default_state_id(request) - - # New requests immediately blow away existing responses. - self._last_response = None - self._last_response_success = False - self._last_response_shared_state_id = None - - @classmethod - def _default_state_id(cls, request: DocUIRequest) -> str: - """Calc a default state id for a request.""" - requesttypeid = request.get_type_id() - if requesttypeid is DocUIRequestTypeID.V1: - import bacommon.docui.v1 as dui1 - - # One state per path seems like a reasonable default. - assert isinstance(request, dui1.Request) - return request.path - if requesttypeid is DocUIRequestTypeID.UNKNOWN: - return 'unknown' - assert_never(requesttypeid) - - def lock_ui(self, origin_widget: bui.Widget | None = None) -> None: - """Stop UI interactions during some operation.""" - assert bui.in_logic_thread() - assert not self._locked - - # If a spinner-position is provided, make the spinner in our - # subcontainer at the provided spot and have it appear - # immediately instead of fading (Makes button presses feel more - # responsive). - parent = None if origin_widget is None else origin_widget.parent - if parent is not None: - assert origin_widget is not None - self._spinner = bui.spinnerwidget( - parent=parent, - position=origin_widget.center, - size=48, - fade=False, - ) - else: - # Otherwise do one at the center of our window (not in our - # subcontainer). - self._spinner = bui.spinnerwidget( - parent=self._root_widget, - position=( - self._vis_left + self._vis_width * 0.5, - self._vis_top - self._vis_height * 0.5, - ), - size=48, - # With restored windows we're likely to have stuff under - # the spinner. Bomb looks nicer but simple is more - # readable in those cases. - style='simple' if self._restored else 'bomb', - ) - self._locked = True - - def unlock_ui(self) -> None: - """Resume normal UI interactions.""" - assert bui.in_logic_thread() - assert self._locked - - if self._spinner: - self._spinner.delete() - self._spinner = None - self._locked = False - - @property - def locked(self) -> bool: - """Are we locked?""" - assert bui.in_logic_thread() - return self._locked - - @property - def scroll_width(self) -> float: - """Width of our scroll area.""" - return self._scroll_width - - @property - def scroll_height(self) -> float: - """Height of our scroll area.""" - return self._scroll_height - - def set_last_response(self, response: DocUIResponse, success: bool) -> None: - """Set a response to a request.""" - assert bui.in_logic_thread() - assert not self._locked - self._last_response = response - self._last_response_success = success - self._has_had_response = True - - # Grab any custom shared-state-id included in this response. - responsetypeid = response.get_type_id() - if responsetypeid is DocUIResponseTypeID.V1: - import bacommon.docui.v1 as dui1 - - assert isinstance(response, dui1.Response) - self._last_response_shared_state_id = response.shared_state_id - elif responsetypeid is DocUIResponseTypeID.UNKNOWN: - self._last_response_shared_state_id = None - else: - assert_never(responsetypeid) - - def instantiate_ui(self, pageprep: v1prep.PagePrep) -> None: - """Replace any current ui with provided prepped one. - - :meta private: - """ - from bauiv1lib.docui.v1prep._calls import ( - doc_ui_v1_instantiate_page_prep, - ) - - assert bui.in_logic_thread() - - # Set title. - bui.textwidget( - edit=self._title, - literal=not pageprep.title_is_lstr, - text=pageprep.title, - ) - - # Clear any existing children. - for child in self._scrollwidget.get_children(): - child.delete() - - if pageprep.rows: - # Stop showing scroll-widget highlights now that we've got - # child stuff to be highlighted. - bui.scrollwidget( - edit=self._scrollwidget, - highlight=False, - simple_culling_v=pageprep.simple_culling_v, - center_small_content=(pageprep.center_vertically), - ) - self._subcontainer = doc_ui_v1_instantiate_page_prep( - pageprep, - rootwidget=self._root_widget, - scrollwidget=self._scrollwidget, - backbutton=( - bui.get_special_widget('back_button') - if self._back_button is None - else self._back_button - ), - windowbackbutton=self._back_button, - window=self, - ) - else: - # No child stuff to show so let the scroll-widget highlight. - bui.scrollwidget( - edit=self._scrollwidget, - highlight=True, - simple_culling_v=0.0, - center_small_content=True, - ) - self._subcontainer = None - - bui.textwidget( - parent=self._scrollwidget, - h_align='center', - v_align='center', - text=bui.Lstr( - translate=('serverResponses', 'There is nothing here.') - ), - scale=0.75, - color=(1, 1, 1, 0.5), - size=(0, 0), - ) - - # Most of our UI won't exist until this point so we need to - # explicitly restore state for selection restore to work. - # - # Note to self: perhaps we should *not* do this if significant - # time has passed since the window was made or if input commands - # have happened. - self.main_window_restore_shared_state() - - @override - def get_main_window_state(self) -> bui.MainWindowState: - # Support recreating our window for back/refresh purposes. - cls = type(self) - - assert bui.in_logic_thread() - - # IMPORTANT - Pull values from self HERE; if we do it in the - # lambda below it'll keep self alive which will lead to - # 'ui-not-getting-cleaned-up' warnings and memory leaks. - auxiliary_style = self._auxiliary_style - controller = self.controller - request = self._request - last_response = self._last_response - has_had_response = self._has_had_response - uiopenstateid = ( - None if self._uiopenstate is None else self._uiopenstate.stateid - ) - suppress_win_extra_type_warning = self._suppress_win_extra_type_warning - - return bui.BasicMainWindowState( - create_call=( - lambda transition, origin_widget: controller.restore( - cls( - controller=controller, - request=request, - transition=transition, - origin_widget=origin_widget, - auxiliary_style=auxiliary_style, - uiopenstateid=uiopenstateid, - suppress_win_extra_type_warning=( - suppress_win_extra_type_warning - ), - restored=True, - has_had_response=has_had_response, - ), - last_response=last_response, - has_had_response=has_had_response, - ) - ), - uiopenstate=self._uiopenstate, - ) - - @override - def main_window_do_save_shared_state(self, state: dict) -> None: - # Give our controller a stab at this. - self.controller.save_window_shared_state(self, state) - - @override - def main_window_do_restore_shared_state(self, state: dict) -> None: - # Give our controller a stab at this. - self.controller.restore_window_shared_state(self, state) - - @override - def main_window_should_preserve_selection(self) -> bool: - return True - - @override - def get_main_window_shared_state_id(self) -> str | None: - base_id = ( - self._request_state_id - if self._last_response_shared_state_id is None - else self._last_response_shared_state_id - ) - - # Each controller has its own unique domain, so include that in - # the id. - ctp = type(self.controller) - out = f'{ctp.__module__}.{ctp.__qualname__}:{base_id}' - return out diff --git a/dist/ba_data/python/bauiv1lib/docui/v1prep/__init__.py b/dist/ba_data/python/bauiv1lib/docui/v1prep/__init__.py deleted file mode 100644 index 5cf353a..0000000 --- a/dist/ba_data/python/bauiv1lib/docui/v1prep/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -"""Functionality related to prepping a v1 doc-ui. - -.. warning:: - - This is an internal api and subject to change at any time. Do not use - it in mod code. -""" - -from bauiv1lib.docui.v1prep._types import ( - DecorationPrep, - ButtonPrep, - RowPrep, - PagePrep, -) -from bauiv1lib.docui.v1prep._calls import prep_page -from bauiv1lib.docui.v1prep._calls2 import ( - prep_text, - prep_decorations, - prep_image, - prep_row_debug, - prep_row_debug_button, - prep_button_debug, - prep_display_item, -) - -__all__ = [ - 'DecorationPrep', - 'ButtonPrep', - 'RowPrep', - 'PagePrep', - 'prep_page', - 'prep_text', - 'prep_decorations', - 'prep_image', - 'prep_row_debug', - 'prep_row_debug_button', - 'prep_button_debug', - 'prep_display_item', -] diff --git a/dist/ba_data/python/bauiv1lib/docui/v1prep/_calls.py b/dist/ba_data/python/bauiv1lib/docui/v1prep/_calls.py deleted file mode 100644 index 9cb30a8..0000000 --- a/dist/ba_data/python/bauiv1lib/docui/v1prep/_calls.py +++ /dev/null @@ -1,778 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Prep functionality for our UI. - -We do all layout math and bake out partial ui calls in a background -thread so there's as little work to do in the ui thread as possible. -""" - -from __future__ import annotations - -import copy -from functools import partial -from typing import TYPE_CHECKING, assert_never - -from efro.util import strict_partial -import bacommon.docui.v1 as dui1 -import bauiv1 as bui - -from bauiv1lib.docui.v1prep._types import PagePrep, RowPrep, ButtonPrep - -if TYPE_CHECKING: - from typing import Callable - - from bauiv1lib.docui import DocUIWindow - - -def prep_page( - page: dui1.Page, - *, - uiscale: bui.UIScale, - scroll_width: float, - scroll_height: float, - idprefix: str, - immediate: bool = False, -) -> PagePrep: - """Prep a page.""" - # pylint: disable=too-many-statements - # pylint: disable=too-many-branches - # pylint: disable=too-many-locals - # pylint: disable=cyclic-import - - import bauiv1lib.docui.v1prep._calls2 as prepcalls2 - - # Create a filtered list of rows we know how to display. - page_rows_filtered: list[dui1.ButtonRow] = [] - for pagerow in page.rows: - if isinstance(pagerow, dui1.ButtonRow): - if not pagerow.buttons: - pagerow = copy.deepcopy(pagerow) - pagerow.buttons.append( - dui1.Button( - label=bui.Lstr( - translate=( - 'serverResponses', - 'There is nothing here.', - ) - ).as_json(), - label_color=(1, 1, 1, 0.3), - label_is_lstr=True, - size=(220, 100), - label_scale=0.6, - texture='buttonSquareWide', - padding_top=-8, - padding_bottom=-10, - color=(0.2, 0.2, 0.2, 0.15), - action=dui1.Local(default_sound=False), - ) - ) - page_rows_filtered.append(pagerow) - if len(page_rows_filtered) != len(page.rows): - bui.uilog.error('Got unknown row type(s) in doc-ui; ignoring.') - - # Ok; we've got some buttons. Build our full UI. - row_title_height_with_subtitle = 30.0 - row_title_height_no_subtitle = 38.0 - row_subtitle_height = 30.0 - - # Buffers for *everything*. Set bases here that look decent and - # allow page to offset them. - top_buffer = 20.0 + page.padding_top - bot_buffer = 20.0 + page.padding_bottom - left_buffer = 10.0 + page.padding_left - # Nudge a bit due to scrollbar. - right_buffer = 20.0 + page.padding_right - - # Extra buffers for title/headers stuff (not in h-scroll). - header_inset_left = 45.0 - header_inset_right = 30.0 - - default_button_width = 150.0 - default_button_height = 100.0 - - if uiscale is bui.UIScale.SMALL: - top_bar_overlap = 70 - bot_bar_overlap = 70 - top_buffer += top_bar_overlap - bot_buffer += bot_bar_overlap - else: - top_bar_overlap = 0 - bot_bar_overlap = 0 - - # Should look into why this is necessary. - fudge = 15.0 - hscrollinset = 15.0 - - rootcall: Callable[..., bui.Widget] | None = None - rows: list[RowPrep] = [] - width: float = scroll_width + fudge - height: float = ( - top_buffer - + bot_buffer - + page.row_spacing * max(0, (len(page_rows_filtered) - 1)) - ) - simple_culling_v: float = page.simple_culling_v - center_vertically: bool = page.center_vertically - title: str = page.title - title_is_lstr: bool = page.title_is_lstr - - # Called with root container after construction completes. - root_post_calls: list[Callable[[bui.Widget], None]] = [] - - nextbuttonid = 0 - - have_start_button = False - have_selected_button = False - - # Precalc basic info like dimensions for all rows. - for row in page_rows_filtered: - - # assert row.buttons - this_row_width = ( - left_buffer - + right_buffer - + row.padding_left - + row.padding_right - + row.button_spacing * (len(row.buttons) - 1) - ) - button_row_height = 30.0 - for button in row.buttons: - if button.size is None: - bwidth = default_button_width - bheight = default_button_height - else: - bwidth = button.size[0] - bheight = button.size[1] - bscale = button.scale - bwidthfull = bwidth * bscale - bheightfull = bheight * bscale - # Include button padding when calcing full needed height. - button_row_height = max( - button_row_height, - bheightfull - + (button.padding_top + button.padding_bottom) * button.scale, - ) - this_row_width += ( - bwidthfull - + (button.padding_left + button.padding_right) * button.scale - ) - # Note: this includes everything in the *scrollable* part of - # the row. - this_row_height = ( - row.padding_top + row.padding_bottom + button_row_height - ) - rows.append( - RowPrep( - width=this_row_width, - height=this_row_height, - titlecalls=[], - hscrollcall=None, - hscrolleditcall=None, - hsubcall=None, - buttons=[], - simple_culling_h=row.simple_culling_h, - decorations=[], - ) - ) - assert this_row_height > 0.0 - assert this_row_width > 0.0 - - # Add height that is *not* part of the h-scrollable area. - height += row.header_height * row.header_scale - if row.title is not None: - height += ( - row_title_height_no_subtitle - if row.subtitle is None - else row_title_height_with_subtitle - ) - if row.subtitle is not None: - height += row_subtitle_height - height += this_row_height - height += row.spacing_top + row.spacing_bottom - - # Ok; we've got all row dimensions. Now prep calls to make the - # subcontainers to fit everything and fill out all rows. - rootcall = partial( - bui.containerwidget, - size=(width, height), - claims_left_right=True, - background=False, - ) - y = height - top_buffer - - for i, (row, rowprep) in enumerate( - zip(page_rows_filtered, rows, strict=True) - ): - tdelaybase = 0.06 * (i + 1) - - y -= row.spacing_top - - if i != 0: - y -= page.row_spacing - - # Header decorations. - header_height_full = row.header_height * row.header_scale - y -= header_height_full - hdecs_l = ( - [] - if row.header_decorations_left is None - else row.header_decorations_left - ) - prepcalls2.prep_decorations( - hdecs_l, - left_buffer + header_inset_left, - y + header_height_full * 0.5, - row.header_scale, - tdelay=None if immediate else (tdelaybase + 0.05), - highlight=False, - out_decoration_preps=rowprep.decorations, - ) - hdecs_c = ( - [] - if row.header_decorations_center is None - else row.header_decorations_center - ) - prepcalls2.prep_decorations( - hdecs_c, - width * 0.5, - y + header_height_full * 0.5, - row.header_scale, - tdelay=None if immediate else (tdelaybase + 0.05), - highlight=False, - out_decoration_preps=rowprep.decorations, - ) - hdecs_r = ( - [] - if row.header_decorations_right is None - else row.header_decorations_right - ) - prepcalls2.prep_decorations( - hdecs_r, - width - right_buffer - header_inset_right, - y + header_height_full * 0.5, - row.header_scale, - tdelay=None if immediate else (tdelaybase + 0.05), - highlight=False, - out_decoration_preps=rowprep.decorations, - ) - - if row.title is not None: - rowprep.titlecalls.append( - partial( - bui.textwidget, - position=( - ( - ((width - left_buffer - right_buffer) * 0.5) - + 7.0 # Fudge factor to match hscroll - if row.center_title - else (left_buffer + header_inset_left) - ), - y - row_subtitle_height * 0.5, - ), - size=(0, 0), - text=row.title, - color=( - (0.85, 0.95, 0.89, 1.0) - if row.title_color is None - else row.title_color - ), - flatness=row.title_flatness, - shadow=row.title_shadow, - scale=1.0, - maxwidth=( - (width - left_buffer - right_buffer) - if row.center_title - else ( - width - - left_buffer - - right_buffer - - header_inset_left - - header_inset_right - ) - ), - h_align='center' if row.center_title else 'left', - v_align='center', - literal=not row.title_is_lstr, - transition_delay=( - None if immediate else (tdelaybase + 0.1) - ), - ) - ) - y -= ( - row_title_height_no_subtitle - if row.subtitle is None - else row_title_height_with_subtitle - ) - if row.subtitle is not None: - rowprep.titlecalls.append( - partial( - bui.textwidget, - position=( - ( - ((width - left_buffer - right_buffer) * 0.5) - + 7.0 # Fudge factor to match hscroll - if row.center_title - else (left_buffer + header_inset_left) - ), - y - row_subtitle_height * 0.5, - ), - size=(0, 0), - text=row.subtitle, - color=( - (0.6, 0.74, 0.6) - if row.subtitle_color is None - else row.subtitle_color - ), - flatness=row.subtitle_flatness, - shadow=row.subtitle_shadow, - scale=0.7, - maxwidth=( - (width - left_buffer - right_buffer) - if row.center_title - else ( - width - - left_buffer - - right_buffer - - header_inset_left - - header_inset_right - ) - ), - h_align='center' if row.center_title else 'left', - v_align='center', - literal=not row.subtitle_is_lstr, - transition_delay=( - None if immediate else (tdelaybase + 0.2) - ), - ) - ) - y -= row_subtitle_height - - y -= rowprep.height # includes padding-top/bottom - - if row.debug: - rowheightfull = ( - rowprep.height + row.header_height * row.header_scale - ) - if row.title is not None: - rowheightfull += ( - row_title_height_no_subtitle - if row.subtitle is None - else row_title_height_with_subtitle - ) - if row.subtitle is not None: - rowheightfull += row_subtitle_height - prepcalls2.prep_row_debug( - ( - width - left_buffer - right_buffer, - rowheightfull, - ), - (left_buffer, y), - None if immediate else tdelaybase, - rowprep.decorations, - ) - - rowprep.hscrollcall = partial( - bui.hscrollwidget, - size=(width - hscrollinset, rowprep.height), - position=(hscrollinset, y), - claims_left_right=True, - highlight=False, - border_opacity=0.0, - center_small_content=row.center_content, - simple_culling_h=row.simple_culling_h, - ) - rowprep.hsubcall = partial( - bui.containerwidget, - size=( - # Ideally we could just always use row-width, but - # currently that gets us right-aligned stuff when - # center-small-content is off. - ( - rowprep.width - if row.center_content - else max(width - hscrollinset - fudge, rowprep.width) - ), - rowprep.height, - ), - background=False, - ) - x = left_buffer + row.padding_left - # Calc height of buttons themselves (includes button padding but - # not row padding). - button_row_height = ( - rowprep.height - row.padding_top - row.padding_bottom - ) - bcount = len(row.buttons) - - # Clamp or max delay if we've got lots of buttons. - bdelaymax = min(0.5, 0.03 * bcount) - for j, button in enumerate(row.buttons): - # Calc amt 1 -> 0 across the row. - tdelayamt = 1.0 - (j / max(1, bcount - 1)) - # Rightmost buttons slide in first. - tdelay = tdelaybase + tdelayamt * bdelaymax - - xorig = x - x += button.padding_left * button.scale - bscale = button.scale - if button.size is None: - bwidth = default_button_width - bheight = default_button_height - else: - bwidth = button.size[0] - bheight = button.size[1] - bwidthfull = bscale * bwidth - bheightfull = bscale * bheight - # Vertically center the button plus its padding. - to_button_plus_padding_bottom = ( - button_row_height - - ( - bheightfull - + (button.padding_top + button.padding_bottom) - * button.scale - ) - ) * 0.5 - # Move up past bottom padding to get button bottom. - to_button_bottom = ( - to_button_plus_padding_bottom - + button.padding_bottom * button.scale - ) - - center_x = x + bwidthfull * 0.5 - center_y = row.padding_bottom + to_button_bottom + bheightfull * 0.5 - - bstyle: str - if button.style is dui1.ButtonStyle.SQUARE: - bstyle = 'square' - elif button.style is dui1.ButtonStyle.TAB: - bstyle = 'tab' - elif button.style is dui1.ButtonStyle.SMALL: - bstyle = 'small' - elif button.style is dui1.ButtonStyle.MEDIUM: - bstyle = 'medium' - elif button.style is dui1.ButtonStyle.LARGE: - bstyle = 'large' - elif button.style is dui1.ButtonStyle.LARGER: - bstyle = 'larger' - elif button.style is dui1.ButtonStyle.BACK: - bstyle = 'back' - elif button.style is dui1.ButtonStyle.BACK_SMALL: - bstyle = 'backSmall' - elif button.style is dui1.ButtonStyle.SQUARE_WIDE: - bstyle = 'squareWide' - else: - assert_never(button.style) - - widgetid: str - if button.widget_id is None: - widgetid = f'{idprefix}|button{nextbuttonid}' - nextbuttonid += 1 - else: - widgetid = f'{idprefix}|{button.widget_id}' - - if button.default: - if have_start_button: - bui.uilog.warning( - 'Multiple buttons flagged as default.' - ' There can be only one per page.' - ) - else: - have_start_button = True - root_post_calls.append(partial(_set_start_button, widgetid)) - if button.selected: - if have_selected_button: - bui.uilog.warning( - 'Multiple buttons flagged as selected.' - ' There can be only one per page.' - ) - else: - have_selected_button = True - root_post_calls.append( - partial(_set_selected_button, widgetid) - ) - - show_buffer_left = button.padding_left * bscale - show_buffer_right = button.padding_right * bscale - - # Calc the total height of what we're trying to keep on - # screen, and then nudge that towards the total visible - # height of the scroll area. - total_show_width = ( - bwidth + button.padding_left + button.padding_right - ) * bscale - - # How much to push show-height towards full available space. - # 1.0 should lead to always perfect centering (but that - # might feel too aggressive). - amt = 0.6 - buffer_extra = max( - 0.0, (scroll_width - total_show_width) * 0.5 * amt - ) - show_buffer_left += buffer_extra - show_buffer_right += buffer_extra - - buttonprep = ButtonPrep( - buttoncall=partial( - bui.buttonwidget, - id=widgetid, - position=(x, row.padding_bottom + to_button_bottom), - size=(bwidth, bheight), - scale=bscale, - color=(None if button.color is None else button.color[:3]), - textcolor=button.label_color, - text_flatness=(button.label_flatness), - text_scale=button.label_scale, - button_type=bstyle, - opacity=(1.0 if button.color is None else button.color[3]), - label='' if button.label is None else button.label, - text_literal=not button.label_is_lstr, - autoselect=True, - enable_sound=False, - transition_delay=None if immediate else tdelay, - icon_color=button.icon_color, - iconscale=button.icon_scale, - better_bg_fit=True, - ), - buttoneditcall=partial( - bui.widget, - # TODO: Calc left/right vals properly based on - # our size and padding. - show_buffer_left=show_buffer_left, - show_buffer_right=show_buffer_right, - depth_range=button.depth_range, - # We explicitly assign all neighbor selection; - # anything left over should go to toolbars. - auto_select_toolbars_only=True, - ), - decorations=[], - textures={}, - widgetid=widgetid, - action=button.action, - ) - if button.texture is not None: - buttonprep.textures['texture'] = button.texture - - if button.icon is not None: - buttonprep.textures['icon'] = button.icon - - # With row-debug on, visualize the area we try to scroll to - # show when each button is selected. Note that we're clamped - # by the h-scroll here so we have to draw a separate box for - # the row title/subtitle. - if row.debug: - prepcalls2.prep_row_debug_button( - ( - bwidthfull - + (button.padding_left + button.padding_right) - * button.scale, - rowprep.height, - ), - (xorig, 0.0), - None if immediate else tdelay, - buttonprep.decorations, - ) - - if button.debug: - prepcalls2.prep_button_debug( - (bwidthfull, bheightfull), - (center_x, center_y), - None if immediate else tdelay, - buttonprep.decorations, - ) - decorations = ( - [] if button.decorations is None else button.decorations - ) - prepcalls2.prep_decorations( - decorations, - center_x, - center_y, - bscale, - None if immediate else tdelay, - highlight=True, - out_decoration_preps=buttonprep.decorations, - ) - - rowprep.buttons.append(buttonprep) - - x += ( - bwidthfull - + (button.padding_right * button.scale) - + row.button_spacing - ) - - # Add an edit call for our new hscroll to give it proper - # show-buffers. - - # Incorporate top buffer so we scroll all the way up - # when selecting the top row (and stay clear of - # toolbars). - show_buffer_top = top_buffer - show_buffer_bottom = bot_buffer - - # Scroll so title/subtitle is in view when selecting. - # Note that we don't need to account for - # padding-top/bottom since the h-scroll that we're - # applying to encompasses both. - show_buffer_top += row.header_height * row.header_scale - if row.title is not None: - show_buffer_top += ( - row_title_height_no_subtitle - if row.subtitle is None - else row_title_height_with_subtitle - ) - if row.subtitle is not None: - show_buffer_top += row_subtitle_height - - # Calc the total height of what we're trying to keep on - # screen, and then nudge that towards the total visible - # height of the scroll area. - total_show_height = ( - rowprep.height + show_buffer_top + show_buffer_bottom - ) - # How much to push show-height towards full available space. - # 1.0 should lead to always perfect centering (but that - # might feel too aggressive). - amt = 0.5 - buffer_extra = max(0.0, (scroll_height - total_show_height) * 0.5 * amt) - - show_buffer_top += buffer_extra - show_buffer_bottom += buffer_extra - - rowprep.hscrolleditcall = partial( - bui.widget, - show_buffer_top=show_buffer_top, - show_buffer_bottom=show_buffer_bottom, - ) - y -= row.spacing_bottom - - return PagePrep( - rootcall=rootcall, - rows=rows, - width=width, - height=height, - simple_culling_v=simple_culling_v, - center_vertically=center_vertically, - title=title, - title_is_lstr=title_is_lstr, - root_post_calls=root_post_calls, - ) - - -def doc_ui_v1_instantiate_page_prep( - pageprep: PagePrep, - *, - rootwidget: bui.Widget, - scrollwidget: bui.Widget, - backbutton: bui.Widget, - windowbackbutton: bui.Widget | None, - window: DocUIWindow, -) -> bui.Widget: - """Create a UI using prepped data.""" - # pylint: disable=too-many-locals - # pylint: disable=too-many-branches - outrows: list[tuple[bui.Widget, list[bui.Widget]]] = [] - - # Now go through and run our prepped ui calls to build our - # widgets, plugging in appropriate parent widgets args and - # whatnot as we go. - assert pageprep.rootcall is not None - subcontainer = pageprep.rootcall(parent=scrollwidget) - for rowprep in pageprep.rows: - for uicall in rowprep.titlecalls: - uicall(parent=subcontainer) - assert rowprep.hscrollcall is not None - hscroll = rowprep.hscrollcall(parent=subcontainer) - for decoration in rowprep.decorations: - kwds: dict = {'parent': subcontainer} - for texarg, texname in decoration.textures.items(): - kwds[texarg] = bui.gettexture(texname) - for mesharg, meshname in decoration.meshes.items(): - kwds[mesharg] = bui.getmesh(meshname) - decoration.call(**kwds) - outrow: tuple[bui.Widget, list[bui.Widget]] = (hscroll, []) - assert rowprep.hsubcall is not None - hsub = rowprep.hsubcall(parent=hscroll) - for i, buttonprep in enumerate(rowprep.buttons): - kwds = { - 'parent': hsub, - 'on_activate_call': strict_partial( - window.controller.run_action, - window, - buttonprep.widgetid, - buttonprep.action, - ), - } - for texarg, texname in buttonprep.textures.items(): - kwds[texarg] = bui.gettexture(texname) - btn = buttonprep.buttoncall(**kwds) - assert buttonprep.buttoneditcall is not None - buttonprep.buttoneditcall(edit=btn) - for decoration in buttonprep.decorations: - kwds = {'parent': hsub} - if decoration.highlight: - kwds['draw_controller'] = btn - for texarg, texname in decoration.textures.items(): - kwds[texarg] = bui.gettexture(texname) - for mesharg, meshname in decoration.meshes.items(): - kwds[mesharg] = bui.getmesh(meshname) - decoration.call(**kwds) - - # Make sure row is scrolled so leftmost button is - # visible (though it kinda seems like this should happen - # by default). - if i == 0: - bui.containerwidget(edit=hsub, visible_child=btn) - outrow[1].append(btn) - - outrows.append(outrow) - assert rowprep.hscrolleditcall is not None - rowprep.hscrolleditcall(edit=hscroll) - - for root_post_call in pageprep.root_post_calls: - root_post_call(rootwidget) - - # Ok; we've got all widgets. Now wire up directional nav between - # rows/buttons. - - # Up press on any top-row button should select window back button - # (if there is one). - if outrows and windowbackbutton is not None: - _scroll, buttons = outrows[0] - for button in buttons: - bui.widget(edit=button, up_widget=windowbackbutton) - for _scroll, buttons in outrows: - # Left press on first button in any row should select back - # button (either system one or window one). - if buttons: - bui.widget(edit=buttons[0], left_widget=backbutton) - # Left/right presses should select neighbor button in - # row (when there is one). - for i in range(0, len(buttons) - 1): - leftbutton = buttons[i] - rightbutton = buttons[i + 1] - bui.widget(edit=leftbutton, right_widget=rightbutton) - bui.widget(edit=rightbutton, left_widget=leftbutton) - # Down/up presses should select next/prev row (when there is - # one). - for i in range(0, len(outrows) - 1): - topscroll, topbuttons = outrows[i] - botscroll, botbuttons = outrows[i + 1] - for topbutton in topbuttons: - bui.widget(edit=topbutton, down_widget=botscroll) - for botbutton in botbuttons: - bui.widget(edit=botbutton, up_widget=topscroll) - - return subcontainer - - -def _set_start_button(buttonid: str, root: bui.Widget) -> None: - widget = bui.widget_by_id(buttonid) - if widget: - bui.containerwidget(edit=root, start_button=widget) - - -def _set_selected_button(buttonid: str, root: bui.Widget) -> None: - del root # Unused. - widget = bui.widget_by_id(buttonid) - if widget: - widget.global_select() diff --git a/dist/ba_data/python/bauiv1lib/docui/v1prep/_calls2.py b/dist/ba_data/python/bauiv1lib/docui/v1prep/_calls2.py deleted file mode 100644 index 192255c..0000000 --- a/dist/ba_data/python/bauiv1lib/docui/v1prep/_calls2.py +++ /dev/null @@ -1,604 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Prep functionality for our UI. - -We do all layout math and bake out partial ui calls in a background -thread so there's as little work to do in the ui thread as possible. -""" - -from __future__ import annotations - -from functools import partial -from typing import TYPE_CHECKING, assert_never - -from efro.util import pairs_from_flat -import bacommon.displayitem as ditm -import bacommon.docui.v1 as dui1 -import bauiv1 as bui - -from bauiv1lib.docui.v1prep._types import DecorationPrep - -if TYPE_CHECKING: - from typing import Callable - - from bauiv1lib.docui import DocUIWindow - - -def prep_decorations( - decorations: list[dui1.Decoration], - center_x: float, - center_y: float, - scale: float, - tdelay: float | None, - *, - highlight: bool, - out_decoration_preps: list[DecorationPrep], -) -> None: - """Prep appropriate decoration types for a list of decorations.""" - for decoration in decorations: - dectypeid = decoration.get_type_id() - if dectypeid is dui1.DecorationTypeID.UNKNOWN: - if bui.do_once(): - bui.uilog.exception( - 'DocUI receieved unknown decoration;' - ' this is likely a server error.' - ) - elif dectypeid is dui1.DecorationTypeID.TEXT: - assert isinstance(decoration, dui1.Text) - prep_text( - decoration, - (center_x, center_y), - scale, - tdelay, - out_decoration_preps, - highlight=highlight, - ) - - elif dectypeid is dui1.DecorationTypeID.IMAGE: - assert isinstance(decoration, dui1.Image) - prep_image( - decoration, - (center_x, center_y), - scale, - tdelay, - out_decoration_preps, - highlight=highlight, - ) - elif dectypeid is dui1.DecorationTypeID.DISPLAY_ITEM: - assert isinstance(decoration, dui1.DisplayItem) - prep_display_item( - decoration, - (center_x, center_y), - scale, - tdelay, - out_decoration_preps, - highlight=highlight, - ) - else: - assert_never(dectypeid) - - -def prep_text( - text: dui1.Text, - bcenter: tuple[float, float], - bscale: float, - tdelay: float | None, - out_decoration_preps: list[DecorationPrep], - *, - highlight: bool, -) -> None: - """Prep decorations for text.""" - # pylint: disable=too-many-branches - xoffs = bcenter[0] + text.position[0] * bscale - yoffs = bcenter[1] + text.position[1] * bscale - - if text.h_align is dui1.HAlign.LEFT: - h_align = 'left' - elif text.h_align is dui1.HAlign.CENTER: - h_align = 'center' - elif text.h_align is dui1.HAlign.RIGHT: - h_align = 'right' - else: - assert_never(text.h_align) - - if text.v_align is dui1.VAlign.TOP: - v_align = 'top' - elif text.v_align is dui1.VAlign.CENTER: - v_align = 'center' - elif text.v_align is dui1.VAlign.BOTTOM: - v_align = 'bottom' - else: - assert_never(text.v_align) - - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.textwidget, - position=(xoffs, yoffs), - scale=text.scale * bscale, - maxwidth=text.size[0] * bscale, - max_height=text.size[1] * bscale, - flatness=text.flatness, - shadow=text.shadow, - h_align=h_align, - v_align=v_align, - size=(0, 0), - color=text.color, - text=text.text, - literal=not text.is_lstr, - transition_delay=tdelay, - depth_range=text.depth_range, - ), - textures={}, - meshes={}, - highlight=highlight and text.highlight, - ) - ) - # Draw square around max width/height in debug mode. - if text.debug: - mwfull = bscale * text.size[0] - mhfull = bscale * text.size[1] - - if text.h_align is dui1.HAlign.LEFT: - mwxoffs = xoffs - elif text.h_align is dui1.HAlign.CENTER: - mwxoffs = xoffs - mwfull * 0.5 - elif text.h_align is dui1.HAlign.RIGHT: - mwxoffs = xoffs - mwfull - else: - assert_never(text.h_align) - - if text.v_align is dui1.VAlign.TOP: - mwyoffs = yoffs - mhfull - elif text.v_align is dui1.VAlign.CENTER: - mwyoffs = yoffs - mhfull * 0.5 - elif text.v_align is dui1.VAlign.BOTTOM: - mwyoffs = yoffs - else: - assert_never(text.v_align) - - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.imagewidget, - position=(mwxoffs, mwyoffs), - size=(mwfull, mhfull), - color=(1, 0, 0), - opacity=0.2, - transition_delay=tdelay, - ), - textures={'texture': 'white'}, - meshes={}, - highlight=True, - ) - ) - - -def prep_image( - image: dui1.Image, - bcenter: tuple[float, float], - bscale: float, - tdelay: float | None, - out_decoration_preps: list[DecorationPrep], - *, - highlight: bool, -) -> None: - """Prep decorations for an image.""" - xoffs = bcenter[0] + image.position[0] * bscale - yoffs = bcenter[1] + image.position[1] * bscale - - widthfull = bscale * image.size[0] - heightfull = bscale * image.size[1] - - if image.h_align is dui1.HAlign.LEFT: - xoffsfin = xoffs - elif image.h_align is dui1.HAlign.CENTER: - xoffsfin = xoffs - widthfull * 0.5 - elif image.h_align is dui1.HAlign.RIGHT: - xoffsfin = xoffs - widthfull - else: - assert_never(image.h_align) - - if image.v_align is dui1.VAlign.TOP: - yoffsfin = yoffs - heightfull - elif image.v_align is dui1.VAlign.CENTER: - yoffsfin = yoffs - heightfull * 0.5 - elif image.v_align is dui1.VAlign.BOTTOM: - yoffsfin = yoffs - else: - assert_never(image.v_align) - - textures: dict[str, str] = {'texture': image.texture} - if image.tint_texture is not None: - textures['tint_texture'] = image.tint_texture - if image.mask_texture is not None: - textures['mask_texture'] = image.mask_texture - - meshes: dict[str, str] = {} - if image.mesh_opaque is not None: - meshes['mesh_opaque'] = image.mesh_opaque - if image.mesh_transparent is not None: - meshes['mesh_transparent'] = image.mesh_transparent - - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.imagewidget, - position=(xoffsfin, yoffsfin), - size=(widthfull, heightfull), - color=None if image.color is None else image.color[:3], - opacity=1.0 if image.color is None else image.color[3], - tint_color=image.tint_color, - tint2_color=image.tint2_color, - transition_delay=tdelay, - depth_range=image.depth_range, - ), - textures=textures, - meshes=meshes, - highlight=highlight and image.highlight, - ) - ) - - -def prep_row_debug( - size: tuple[float, float], - pos: tuple[float, float], - tdelay: float | None, - out_decoration_preps: list[DecorationPrep], -) -> None: - """Prep debug decorations for a row.""" - - textures: dict[str, str] = {'texture': 'white'} - - # Shrink the square we draw a tiny bit so rows butted up to - # eachother can be seen. - border_shrink = 1.0 - - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.imagewidget, - position=(pos[0], pos[1] + border_shrink), - size=(size[0], size[1] - 2.0 * border_shrink), - color=(0, 0, 1.0), - opacity=0.1, - transition_delay=tdelay, - ), - textures=textures, - meshes={}, - highlight=True, - ) - ) - - -def prep_row_debug_button( - bsize: tuple[float, float], - bcorner: tuple[float, float], - tdelay: float | None, - out_decoration_preps: list[DecorationPrep], -) -> None: - """Prep debug decorations for a button.""" - xoffs = bcorner[0] - yoffs = bcorner[1] - - textures: dict[str, str] = {'texture': 'white'} - - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.imagewidget, - position=(xoffs, yoffs), - size=bsize, - color=(0.0, 0.0, 1), - opacity=0.15, - transition_delay=tdelay, - ), - textures=textures, - meshes={}, - highlight=True, - ) - ) - - -def prep_button_debug( - bsize: tuple[float, float], - bcenter: tuple[float, float], - tdelay: float | None, - out_decoration_preps: list[DecorationPrep], -) -> None: - """Prep debug decorations for a button.""" - textures: dict[str, str] = {'texture': 'white'} - - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.imagewidget, - position=( - bcenter[0] - bsize[0] * 0.5, - bcenter[1] - bsize[1] * 0.5, - ), - size=bsize, - color=(0, 1, 0), - opacity=0.1, - transition_delay=tdelay, - ), - textures=textures, - meshes={}, - highlight=True, - ) - ) - - -def prep_display_item( - display_item: dui1.DisplayItem, - parent_center: tuple[float, float], - parent_scale: float, - tdelay: float | None, - out_decoration_preps: list[DecorationPrep], - *, - highlight: bool, -) -> None: - """Prep decorations for a display-item.""" - # pylint: disable=too-many-branches - # pylint: disable=too-many-statements - # pylint: disable=too-many-locals - - # Calc center and size of our bounds based on parent. - our_center = ( - parent_center[0] + display_item.position[0] * parent_scale, - parent_center[1] + display_item.position[1] * parent_scale, - ) - bounds_size = ( - parent_scale * display_item.size[0], - parent_scale * display_item.size[1], - ) - - wrapper = display_item.wrapper - item = wrapper.item - itemtype = item.get_type_id() - - # Draw our bounds if debug mode is enabled (or we're a test-item). - if display_item.debug or itemtype is ditm.ItemTypeID.TEST: - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.imagewidget, - color=(1, 1, 0), - opacity=0.1, - position=( - our_center[0] - bounds_size[0] * 0.5, - our_center[1] - bounds_size[1] * 0.5, - ), - size=bounds_size, - transition_delay=tdelay, - ), - textures={'texture': 'white'}, - meshes={}, - highlight=highlight and display_item.highlight, - ) - ) - - # Calc our width and height based on our aspect ratio so we fit in - # the provided bounds. - if display_item.style is dui1.DisplayItemStyle.FULL: - aspect_ratio = 0.75 # Bit less tall than wide (graphic centric). - compact = False - icon = False - elif display_item.style is dui1.DisplayItemStyle.COMPACT: - aspect_ratio = 0.5 # Significantly wider (text centric) - compact = True - icon = False - elif display_item.style is dui1.DisplayItemStyle.ICON: - aspect_ratio = 1.0 # Square - compact = False - icon = True - else: - # Make sure we cover all possibilities. - assert_never(display_item.style) - - if bounds_size[0] * aspect_ratio > bounds_size[1]: - height = bounds_size[1] - width = height / aspect_ratio - else: - width = bounds_size[0] - height = width * aspect_ratio - - # Show our constrained bounds in debug mode. - if display_item.debug or itemtype is ditm.ItemTypeID.TEST: - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.imagewidget, - color=(1, 0.5, 0), - opacity=0.2, - position=( - our_center[0] - width * 0.5, - our_center[1] - height * 0.5, - ), - size=(width, height), - transition_delay=tdelay, - ), - textures={'texture': 'white'}, - meshes={}, - highlight=highlight and display_item.highlight, - ) - ) - - img: str | None = None - img_x_offs = 0.0 - img_y_offs = 0.0 - imgsize = width * (0.5 if compact else 1.0 if icon else 0.33) - - show_text = True - text_mult = 0.006 - text: str | None = None # Uses default if None - text_x_offs = 0.0 - text_y_offs = 0.0 - text_align = 'center' - text_max_width: float | None = width * 0.9 - - if itemtype is ditm.ItemTypeID.CHEST: - from baclassic import ( - CHEST_APPEARANCE_DISPLAY_INFOS, - CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, - ) - import bacommon.classic - - assert isinstance(item, bacommon.classic.ClassicChestDisplayItem) - - img = None - show_text = False - c_info = CHEST_APPEARANCE_DISPLAY_INFOS.get( - item.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT - ) - c_size = width * (0.66 if compact else 1.05 if icon else 0.83) - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.imagewidget, - position=( - our_center[0] - c_size * 0.5, - our_center[1] - c_size * 0.5, - ), - size=(c_size, c_size), - transition_delay=tdelay, - tint_color=c_info.tint, - tint2_color=c_info.tint2, - depth_range=display_item.depth_range, - ), - textures={ - 'texture': c_info.texclosed, - 'tint_texture': c_info.texclosedtint, - }, - meshes={}, - highlight=highlight and display_item.highlight, - ) - ) - elif itemtype is ditm.ItemTypeID.TEST: - assert isinstance(item, ditm.Test) - # Nothing to do here. This is just another way to enable debug - # drawing. - if icon or compact: - text_mult = 0.02 # Very large text. - - elif ( - itemtype is ditm.ItemTypeID.TOKENS - or itemtype is ditm.ItemTypeID.TICKETS - or itemtype is ditm.ItemTypeID.TICKETS_PURPLE - ): - if itemtype is ditm.ItemTypeID.TOKENS: - assert isinstance(item, ditm.Tokens) - img = 'coin' - if compact: - text = str(item.count) - elif itemtype is ditm.ItemTypeID.TICKETS: - assert isinstance(item, ditm.Tickets) - img = 'tickets' - if compact: - text = str(item.count) - elif itemtype is ditm.ItemTypeID.TICKETS_PURPLE: - assert isinstance(item, ditm.PurpleTickets) - img = 'ticketsPurple' - if compact: - text = str(item.count) - else: - assert_never(itemtype) - - if compact: - imgamt = 0.85 # How much of img dimensions we measure. - - assert text is not None - text_mult = 0.01 - strwidth = ( - width - * bui.get_string_width(text, suppress_warning=True) - * text_mult - ) - totwidth = strwidth + imgsize * imgamt - - maxwidth = width * 0.95 - if totwidth > maxwidth: - mult = maxwidth / totwidth - text_mult *= mult - strwidth *= mult - totwidth *= mult - imgsize *= mult - - text_max_width = None # We calc this fully ourself. - # Move to right and then left by half img width. - img_x_offs = totwidth * 0.5 - imgsize * imgamt * 0.5 - # Move to left and then right by half text width. - text_x_offs = totwidth * -0.5 + strwidth * 0.5 - elif icon: - img_y_offs = 0.0 - show_text = False - else: - img_y_offs = width * 0.11 - text_y_offs = width * -0.15 - elif itemtype is ditm.ItemTypeID.UNKNOWN: - assert isinstance(item, ditm.Unknown) - # Just do default text here. - if icon: - text_mult = 0.02 # Very large text. - else: - # Make sure we cover all possibilities. - assert_never(itemtype) - - if img is not None: - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.imagewidget, - position=( - our_center[0] - imgsize * 0.5 + img_x_offs, - our_center[1] - imgsize * 0.5 + img_y_offs, - ), - size=(imgsize, imgsize), - transition_delay=tdelay, - depth_range=display_item.depth_range, - ), - textures={'texture': img}, - meshes={}, - highlight=highlight and display_item.highlight, - ) - ) - if show_text: - if text is None: - subs = wrapper.description_subs - if subs is None: - subs = [] - text = bui.Lstr( - translate=('displayItemNames', wrapper.description), - subs=pairs_from_flat(subs), - ).as_json() - - out_decoration_preps.append( - DecorationPrep( - call=partial( - bui.textwidget, - position=( - our_center[0] + text_x_offs, - our_center[1] + text_y_offs, - ), - scale=width * text_mult, - maxwidth=text_max_width, - h_align=text_align, - v_align='center', - size=(0, 0), - color=( - (1, 1, 1) - if display_item.text_color is None - else display_item.text_color - ), - text=text, - flatness=1.0, - shadow=1.0, - literal=False, - transition_delay=tdelay, - depth_range=display_item.depth_range, - ), - textures={}, - meshes={}, - highlight=highlight and display_item.highlight, - ) - ) diff --git a/dist/ba_data/python/bauiv1lib/docui/v1prep/_types.py b/dist/ba_data/python/bauiv1lib/docui/v1prep/_types.py deleted file mode 100644 index 62d8f71..0000000 --- a/dist/ba_data/python/bauiv1lib/docui/v1prep/_types.py +++ /dev/null @@ -1,73 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Types used in prepping v1 doc-ui. - -Prepping involves doing as much math and layout work as possible in a -pre-pass (generally run in a background thread) so that the actual calls -made to instantiate the ui are as fast and minimal as possible. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Callable - - import bacommon.docui.v1 - import bauiv1 - - from bauiv1lib.docui._window import DocUIWindow - - -@dataclass -class DecorationPrep: - """Prep for a decoration in a v1 doc-ui""" - - call: Callable[..., bauiv1.Widget] - textures: dict[str, str] - meshes: dict[str, str] - highlight: bool - - -@dataclass -class ButtonPrep: - """Prep for a button in a v1 doc-ui""" - - buttoncall: Callable[..., bauiv1.Widget] - buttoneditcall: Callable | None - decorations: list[DecorationPrep] - textures: dict[str, str] - widgetid: str - action: bacommon.docui.v1.Action | None - - -@dataclass -class RowPrep: - """Prep for a row in a v1 doc-ui""" - - width: float - height: float - titlecalls: list[Callable[..., bauiv1.Widget]] - hscrollcall: Callable[..., bauiv1.Widget] | None - hscrolleditcall: Callable | None - hsubcall: Callable[..., bauiv1.Widget] | None - buttons: list[ButtonPrep] - simple_culling_h: float - decorations: list[DecorationPrep] - - -@dataclass -class PagePrep: - """Prep for a page in a v1 doc-ui""" - - rootcall: Callable[..., bauiv1.Widget] | None - rows: list[RowPrep] - width: float - height: float - simple_culling_v: float - center_vertically: bool - title: str - title_is_lstr: bool - root_post_calls: list[Callable[[bauiv1.Widget], None]] diff --git a/dist/ba_data/python/bauiv1lib/docuitest.py b/dist/ba_data/python/bauiv1lib/docuitest.py deleted file mode 100644 index 6e3430c..0000000 --- a/dist/ba_data/python/bauiv1lib/docuitest.py +++ /dev/null @@ -1,1092 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Examples/tests for using DocUI to build UIs.""" - -# pylint: disable=too-many-lines -from __future__ import annotations - -import time -import copy -from typing import TYPE_CHECKING, override - -from efro.error import CleanError -import bauiv1 as bui - -from bauiv1lib.docui import DocUIWindow, DocUIController - -if TYPE_CHECKING: - from bacommon.docui import DocUIRequest, DocUIResponse - import bacommon.docui.v1 - - from bauiv1lib.docui import DocUILocalAction - - -def show_test_doc_ui_window() -> None: - """Bust out a doc-ui window.""" - import bacommon.docui.v1 as dui1 - - # Pop up an auxiliary window wherever we are in the nav stack. - bui.app.ui_v1.auxiliary_window_activate( - win_type=DocUIWindow, - win_create_call=bui.CallStrict( - TestDocUIController().create_window, dui1.Request('/') - ), - win_extra_type_id=TestDocUIController.get_window_extra_type_id(), - ) - - -class TestDocUIController(DocUIController): - """Provides various tests/demonstrations of docui functionality.""" - - @override - def fulfill_request(self, request: DocUIRequest) -> DocUIResponse: - """Fulfill a request. - - Will be called in a background thread. - """ - # pylint: disable=too-many-return-statements - - import bacommon.docui.v1 as dui1 - - # We currently support v1 requests only. - if not isinstance(request, dui1.Request): - raise CleanError('Invalid request version.') - - # Handle some pages purely locally. - if request.path == '/': - return _test_page_root(request) - if request.path == '/test2': - return _test_page_2(request) - if request.path == '/slow': - return _test_page_long(request) - if request.path == '/timedactions': - return _test_page_timed_actions(request) - if request.path == '/displayitems': - return _test_page_display_items(request) - if request.path == '/emptypage': - return _test_page_empty(request) - if request.path == '/boundstests': - return _test_bounds(request) - - # Ship '/webtest/*' off to some webserver to handle. - if request.path.startswith('/webtest/'): - return self.fulfill_request_web( - request, 'https://www.ballistica.net/docuitest' - ) - - # Ship '/cloudmsgtest/*' through our cloud connection to handle. - if request.path.startswith('/cloudmsgtest/'): - return self.fulfill_request_cloud(request, 'test') - - raise CleanError('Invalid request path.') - - @override - def local_action(self, action: DocUILocalAction) -> None: - bui.screenmessage( - f'Would do {action.name!r} with args {action.args!r}.' - ) - - -def _test_page_long( - request: bacommon.docui.v1.Request, -) -> bacommon.docui.v1.Response: - """Testing a page that takes a bit of time to load.""" - import bacommon.docui.v1 as dui1 - - del request # Unused. - - # Simulate a slow connection or whatnot. - time.sleep(3.0) - - return dui1.Response( - page=dui1.Page( - title='Test', - center_vertically=True, - rows=[ - dui1.ButtonRow( - title='That took a while', - center_title=True, - center_content=True, - buttons=[ - dui1.Button( - 'Sure Did', - size=(120, 80), - action=dui1.Browse(dui1.Request('/')), - ), - ], - ), - ], - ) - ) - - -def _test_page_timed_actions( - request: bacommon.docui.v1.Request, -) -> bacommon.docui.v1.Response: - """Testing a page that takes a bit of time to load.""" - import bacommon.docui.v1 as dui1 - - val = request.args.get('val') - if not isinstance(val, int): - val = 5 - - return dui1.Response( - page=dui1.Page( - title='Test', - center_vertically=True, - rows=[ - dui1.ButtonRow( - title=f'Hello there {val}', - subtitle='Each change here is a new request/response.', - center_title=True, - center_content=True, - buttons=[ - dui1.Button( - 'Done', - size=(120, 80), - action=dui1.Local(close_window=True), - default=True, - ), - ], - ), - ], - ), - # Refresh this page with a countdown until we hit zero and then - # close the window. - timed_action=( - dui1.Replace(dui1.Request('/timedactions', args={'val': val - 1})) - if (val - 1) > 0 - else dui1.Local(close_window=True) - ), - timed_action_delay=1.0, - ) - - -def _test_page_effects() -> bacommon.docui.v1.Page: - """Testing effects after a page load.""" - import bacommon.docui.v1 as dui1 - - return dui1.Page( - title='Effects', - center_vertically=True, - rows=[ - dui1.ButtonRow( - title='Have some lovely effects', - center_title=True, - center_content=True, - buttons=[ - dui1.Button( - 'Nice!', - size=(120, 80), - action=dui1.Local(close_window=True), - ), - ], - ), - ], - ) - - -def _test_page_2( - request: bacommon.docui.v1.Request, -) -> bacommon.docui.v1.Response: - """More testing.""" - import bacommon.docui.v1 as dui1 - - del request # Unused. - - return dui1.Response( - page=dui1.Page( - title='Test 2', - rows=[ - dui1.ButtonRow( - title='More Tests', - buttons=[ - dui1.Button( - 'Browse', - size=(120, 80), - action=dui1.Browse(dui1.Request('/')), - ), - dui1.Button( - 'Replace', - size=(120, 80), - action=dui1.Replace(dui1.Request('/')), - ), - dui1.Button( - 'Close', - size=(120, 80), - action=dui1.Local(close_window=True), - selected=True, # Testing this - ), - ], - ), - ], - ) - ) - - -def _test_page_root( - request: bacommon.docui.v1.Request, -) -> bacommon.docui.v1.Response: - """Return test page.""" - - import bacommon.clienteffect as clfx - import bacommon.docui.v1 as dui1 - - # Show some specific debug bits if they ask us to. - debug = bool(request.args.get('debug', False)) - - response = dui1.Response( - page=dui1.Page( - title='Test Root', - rows=[ - dui1.ButtonRow( - debug=debug, - header_height=100, - header_decorations_left=[ - dui1.Text( - 'HeaderLeft', - position=(0, 10 + 20), - color=(1, 1, 1, 0.3), - size=(150, 30), - h_align=dui1.HAlign.LEFT, - debug=debug, - ), - ], - header_decorations_center=[ - dui1.Text( - 'Hello From DocUI!', - position=(0, 10 + 20), - size=(300, 30), - debug=debug, - ), - dui1.Text( - ( - 'Use this as reference for building' - ' UIs with DocUI.' - ' Its code lives at bauiv1lib.docuitest' - ), - scale=0.5, - position=(0, -18 + 20), - size=(600, 23), - debug=debug, - ), - dui1.Image( - 'nub', position=(0, -58 + 20), size=(60, 60) - ), - ], - header_decorations_right=[ - dui1.Text( - 'HeaderRight', - position=(0, 10 + 20), - color=(1, 1, 1, 0.3), - size=(150, 30), - h_align=dui1.HAlign.RIGHT, - debug=debug, - ), - ], - title='Some Tests', - buttons=[ - dui1.Button( - 'Browse', - size=(120, 80), - action=dui1.Browse(dui1.Request('/test2')), - ), - dui1.Button( - 'Replace', - size=(120, 80), - action=dui1.Replace(dui1.Request('/test2')), - ), - dui1.Button( - 'Close', - size=(120, 80), - action=dui1.Local(close_window=True), - ), - dui1.Button( - 'Invalid\nRequest', - size=(120, 80), - action=dui1.Browse(dui1.Request('/invalidrequest')), - ), - dui1.Button( - 'Immediate\nClientEffects', - size=(120, 80), - action=dui1.Local( - immediate_client_effects=[ - clfx.ScreenMessage( - 'Hello From Immediate Client Effects', - color=(0, 1, 0), - ), - clfx.PlaySound(clfx.Sound.CASH_REGISTER), - clfx.Delay(1.0), - clfx.ScreenMessage( - '{"r":"successText"}', - is_lstr=True, - color=(0, 1, 0), - ), - clfx.PlaySound(clfx.Sound.CASH_REGISTER), - ] - ), - ), - dui1.Button( - 'Response\nClientEffects', - size=(120, 80), - action=dui1.Browse( - dui1.Request('/', args={'test_effects': True}) - ), - ), - dui1.Button( - 'Immediate\nLocalAction', - size=(120, 80), - action=dui1.Local( - immediate_local_action='testaction', - immediate_local_action_args={'testparam': 123}, - ), - ), - dui1.Button( - 'Response\nLocalAction', - size=(120, 80), - action=dui1.Browse( - dui1.Request('/', args={'test_action': True}) - ), - ), - ], - ), - dui1.ButtonRow( - title='A Few More Tests', - buttons=[ - dui1.Button( - 'Hide\nDebug' if debug else 'Show\nDebug', - size=(120, 80), - action=dui1.Replace( - dui1.Request('/', args={'debug': not debug}) - ), - ), - dui1.Button( - 'Slow\nBrowse', - size=(120, 80), - action=dui1.Browse(dui1.Request('/slow')), - ), - dui1.Button( - 'Slow\nReplace', - size=(120, 80), - action=dui1.Replace(dui1.Request('/slow')), - ), - dui1.Button( - 'Timed\nActions', - size=(120, 80), - action=dui1.Browse(dui1.Request('/timedactions')), - ), - dui1.Button( - 'Web\nGET', - size=(120, 80), - action=dui1.Browse(dui1.Request('/webtest/get')), - ), - dui1.Button( - 'Web\nPOST', - size=(120, 80), - action=dui1.Browse( - dui1.Request( - '/webtest/post', - method=dui1.RequestMethod.POST, - ) - ), - ), - dui1.Button( - 'DisplayItems', - size=(120, 80), - action=dui1.Browse(dui1.Request('/displayitems')), - ), - dui1.Button( - 'Empty\nPage', - size=(120, 80), - action=dui1.Browse(dui1.Request('/emptypage')), - ), - ], - ), - dui1.ButtonRow( - title='Even More Tests', - buttons=[ - dui1.Button( - 'Cloud-Msg\nGET', - size=(120, 80), - action=dui1.Browse( - dui1.Request('/cloudmsgtest/get') - ), - ), - dui1.Button( - 'Cloud-Msg\nPOST', - size=(120, 80), - action=dui1.Browse( - dui1.Request( - '/cloudmsgtest/post', - method=dui1.RequestMethod.POST, - ) - ), - ), - dui1.Button( - 'Bounds\nTests', - size=(120, 80), - action=dui1.Browse(dui1.Request('/boundstests')), - ), - ], - ), - dui1.ButtonRow(title='Empty Row', buttons=[]), - dui1.ButtonRow( - title='Layout Tests', - debug=debug, - padding_left=5.0, - buttons=[ - dui1.Button( - label='Test', - size=(180, 200), - decorations=[ - dui1.Image( - 'powerupPunch', - position=(-70, 0), - size=(40, 40), - h_align=dui1.HAlign.LEFT, - ), - dui1.Image( - 'powerupSpeed', - position=(0, 75), - size=(35, 35), - v_align=dui1.VAlign.TOP, - ), - dui1.Text( - 'TL', - position=(-70, 75), - size=(50, 50), - h_align=dui1.HAlign.LEFT, - v_align=dui1.VAlign.TOP, - debug=debug, - ), - dui1.Text( - 'TR', - position=(70, 75), - size=(50, 50), - h_align=dui1.HAlign.RIGHT, - v_align=dui1.VAlign.TOP, - debug=debug, - ), - dui1.Text( - 'BL', - position=(-70, -75), - size=(50, 50), - h_align=dui1.HAlign.LEFT, - v_align=dui1.VAlign.BOTTOM, - debug=debug, - ), - dui1.Text( - 'BR', - position=(70, -75), - size=(50, 50), - h_align=dui1.HAlign.RIGHT, - v_align=dui1.VAlign.BOTTOM, - debug=debug, - ), - ], - ), - dui1.Button( - label='Test2', - size=(100, 100), - color=(1, 0, 0, 1), - label_color=(1, 1, 1, 1), - padding_right=4, - ), - # Should look like the first button but - # scaled down. - dui1.Button( - label='Test', - size=(180, 200), - scale=0.6, - padding_bottom=30, # Should nudge us up. - debug=debug, # Show bounds. - decorations=[ - dui1.Image( - 'powerupPunch', - position=(-70, 0), - size=(40, 40), - h_align=dui1.HAlign.LEFT, - ), - dui1.Image( - 'powerupSpeed', - position=(0, 75), - size=(35, 35), - v_align=dui1.VAlign.TOP, - ), - dui1.Text( - 'TL', - position=(-70, 75), - size=(50, 50), - h_align=dui1.HAlign.LEFT, - v_align=dui1.VAlign.TOP, - debug=debug, - ), - dui1.Text( - 'TR', - position=(70, 75), - size=(50, 50), - h_align=dui1.HAlign.RIGHT, - v_align=dui1.VAlign.TOP, - debug=debug, - ), - dui1.Text( - 'BL', - position=(-70, -75), - size=(50, 50), - h_align=dui1.HAlign.LEFT, - v_align=dui1.VAlign.BOTTOM, - debug=debug, - ), - dui1.Text( - 'BR', - position=(70, -75), - size=(50, 50), - h_align=dui1.HAlign.RIGHT, - v_align=dui1.VAlign.BOTTOM, - debug=debug, - ), - ], - ), - # Testing custom button images and opacity. - dui1.Button( - label='Test3', - texture='buttonSquareWide', - padding_left=10.0, - padding_right=10.0, - color=(1, 1, 1, 0.3), - size=(200, 100), - ), - # Testing image drawing vs bounds - dui1.Button( - label='BoundsTest', - texture='white', - color=(1, 1, 1, 0.3), - size=(150, 100), - debug=debug, - ), - ], - ), - dui1.ButtonRow( - title='Long Row Test', - subtitle='Look - a subtitle!', - buttons=[ - dui1.Button( - size=(150, 100), - decorations=[ - dui1.Text( - 'MaxWidthTest', - position=(0, 25), - size=(150 * 0.8, 32.0), - flatness=1.0, - shadow=0.0, - debug=debug, - ), - dui1.Text( - 'MaxHeightTest\nSecondLine', - position=(0, -20), - size=(150 * 0.8, 40), - flatness=1.0, - shadow=0.0, - debug=debug, - ), - ], - ), - dui1.Button( - size=(150, 100), - decorations=[ - dui1.Image( - 'zoeIcon', - position=(0, 0), - size=(70, 70), - tint_texture='zoeIconColorMask', - tint_color=(1, 0, 0), - tint2_color=(0, 1, 0), - mask_texture='characterIconMask', - ), - ], - ), - dui1.Button( - size=(150, 100), - decorations=[ - dui1.Image( - 'bridgitPreview', - position=(0, 10), - size=(120, 60), - mask_texture='mapPreviewMask', - mesh_opaque='level_select_button_opaque', - mesh_transparent=( - 'level_select_button_transparent' - ), - ), - ], - ), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button( - 'foo', - size=(150, 100), - scale=0.4, - padding_left=100, - padding_right=200, - ), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - dui1.Button(size=(150, 100)), - ], - ), - dui1.ButtonRow( - spacing_top=-15, - subtitle='Subtitle only!', - buttons=[ - dui1.Button(size=(200, 120)), - ], - ), - dui1.ButtonRow( - buttons=[ - dui1.Button( - 'Row-With-No-Title Test', - size=(300, 80), - style=dui1.ButtonStyle.MEDIUM, - color=(0.8, 0.8, 0.8, 1), - icon='buttonPunch', - icon_color=(0.5, 0.3, 1.0, 1.0), - icon_scale=1.2, - ), - ], - ), - dui1.ButtonRow( - title='Centered Content / Faded Title', - title_color=(0.6, 0.6, 1.0, 0.3), - title_flatness=1.0, - title_shadow=1.0, - subtitle='Testing Centered Title/Content', - subtitle_color=(1.0, 0.5, 1.0, 0.5), - subtitle_flatness=1.0, - subtitle_shadow=0.0, - center_content=True, - center_title=True, - buttons=[ - dui1.Button( - 'Hello There!', - size=(200, 120), - color=(0.7, 0.7, 0.9, 1), - ), - ], - ), - ], - ), - ) - - # Include some client effects if they ask. - if request.args.get('test_effects', False): - response.client_effects = [ - clfx.ScreenMessage( - 'Hello From Response Client Effects', color=(0, 1, 0) - ), - clfx.PlaySound(clfx.Sound.CASH_REGISTER), - clfx.Delay(1.0), - clfx.ScreenMessage( - '{"r":"successText"}', is_lstr=True, color=(0, 1, 0) - ), - clfx.PlaySound(clfx.Sound.CASH_REGISTER), - ] - - # Include a local-action if they ask. - if request.args.get('test_action', False): - response.local_action = 'testaction' - response.local_action_args = {'testparam': 234} - - return response - - -def _test_page_empty( - request: bacommon.docui.v1.Request, -) -> bacommon.docui.v1.Response: - import bacommon.docui.v1 as dui1 - - del request # Unused. - - return dui1.Response(page=dui1.Page(title='EmptyPage', rows=[])) - - -def _test_page_display_items( - request: bacommon.docui.v1.Request, -) -> bacommon.docui.v1.Response: - """Testing display-items.""" - from bacommon.classic import ClassicChestAppearance, ClassicChestDisplayItem - import bacommon.docui.v1 as dui1 - import bacommon.displayitem as ditm - - # Show some specific debug bits if they ask us to. - debug = bool(request.args.get('debug', False)) - - def _make_test_button( - scale: float, - wrapper: ditm.Wrapper, - ) -> dui1.Button: - - # See how this looks when unrecognized (relying on wrapper info - # only). - uwrapper = copy.deepcopy(wrapper) - uwrapper.item = ditm.Unknown() - - return dui1.Button( - size=(300, 400), - scale=scale, - decorations=[ - dui1.DisplayItem( - wrapper=wrapper, - style=dui1.DisplayItemStyle.FULL, - position=(-62, 100), - size=(120, 120), - debug=debug, - ), - dui1.DisplayItem( - wrapper=uwrapper, - style=dui1.DisplayItemStyle.FULL, - position=(62, 100), - size=(120, 120), - debug=debug, - ), - dui1.DisplayItem( - wrapper=wrapper, - style=dui1.DisplayItemStyle.COMPACT, - position=(-55, -20), - size=(80, 80), - debug=debug, - ), - dui1.DisplayItem( - wrapper=uwrapper, - style=dui1.DisplayItemStyle.COMPACT, - position=(55, -20), - size=(80, 80), - debug=debug, - ), - dui1.DisplayItem( - wrapper=wrapper, - style=dui1.DisplayItemStyle.ICON, - position=(-55, -120), - size=(100, 80), - debug=debug, - ), - dui1.DisplayItem( - wrapper=uwrapper, - style=dui1.DisplayItemStyle.ICON, - position=(55, -120), - size=(100, 80), - debug=debug, - ), - ], - ) - - return dui1.Response( - page=dui1.Page( - padding_left=20, - padding_right=20, - title='DisplayItems', - rows=[ - dui1.ButtonRow( - debug=debug, - padding_left=-10, - title='Display Item Tests', - subtitle=( - 'top=FULL, center=COMPACT, bottom=ICON;' - ' left=regular, right=unknown' - ), - buttons=[ - _make_test_button( - 1.0, - ditm.Wrapper.for_item(ditm.Tickets(count=213)), - ), - _make_test_button( - 0.47, - ditm.Wrapper.for_item(ditm.Tickets(count=213)), - ), - _make_test_button( - 1.0, - ditm.Wrapper.for_item( - ClassicChestDisplayItem( - appearance=ClassicChestAppearance.L3 - ) - ), - ), - _make_test_button( - 1.0, - ditm.Wrapper.for_item(ditm.Tokens(count=3)), - ), - _make_test_button( - 1.0, - ditm.Wrapper.for_item(ditm.Tokens(count=1414287)), - ), - _make_test_button( - 1.0, - ditm.Wrapper.for_item(ditm.Test()), - ), - ], - ), - dui1.ButtonRow( - buttons=[ - dui1.Button( - 'Hide Debug' if debug else 'Show Debug', - style=dui1.ButtonStyle.MEDIUM, - size=(240, 60), - color=(0.6, 0.4, 0.8, 1.0), - action=dui1.Replace( - dui1.Request( - request.path, args={'debug': not debug} - ) - ), - ) - ], - ), - ], - ) - ) - - -def _test_bounds( - request: bacommon.docui.v1.Request, -) -> bacommon.docui.v1.Response: - import bacommon.docui.v1 as dui1 - - del request # Unused. - - def _nm(style: dui1.ButtonStyle) -> str: - return f'{type(style).__name__}.{style.name}' - - return dui1.Response( - page=dui1.Page( - title='BoundsTests', - rows=[ - dui1.ButtonRow( - title=_nm(dui1.ButtonStyle.SQUARE), - buttons=[ - dui1.Button('Hello', size=(300, 300), debug=True), - dui1.Button('Hello', size=(200, 200), debug=True), - dui1.Button('Hello', size=(100, 100), debug=True), - ], - ), - dui1.ButtonRow( - title=_nm(dui1.ButtonStyle.SQUARE_WIDE), - buttons=[ - dui1.Button( - 'Hello', - size=(400, 200), - style=dui1.ButtonStyle.SQUARE_WIDE, - debug=True, - ), - dui1.Button( - 'Hello', - size=(200, 250), - style=dui1.ButtonStyle.SQUARE_WIDE, - debug=True, - ), - dui1.Button( - 'Hello', - size=(60, 100), - style=dui1.ButtonStyle.SQUARE_WIDE, - debug=True, - ), - ], - ), - dui1.ButtonRow( - title='(background texture)', - buttons=[ - dui1.Button( - 'Hello', - size=(300, 300), - texture='white', - color=(1, 0, 0, 0.3), - debug=True, - ), - dui1.Button( - 'Hello', - size=(200, 200), - texture='white', - color=(1, 0, 0, 0.3), - debug=True, - ), - dui1.Button( - 'Hello', - size=(100, 100), - texture='white', - color=(1, 0, 0, 0.3), - debug=True, - ), - ], - ), - dui1.ButtonRow( - title=_nm(dui1.ButtonStyle.TAB), - buttons=[ - dui1.Button( - 'Hello', - size=(400, 100), - style=dui1.ButtonStyle.TAB, - debug=True, - ), - dui1.Button( - 'Hello', - size=(200, 50), - style=dui1.ButtonStyle.TAB, - debug=True, - ), - dui1.Button( - 'Hello', - size=(100, 60), - style=dui1.ButtonStyle.TAB, - debug=True, - ), - ], - ), - dui1.ButtonRow( - title=_nm(dui1.ButtonStyle.LARGER), - buttons=[ - dui1.Button( - 'Hello', - size=(500, 100), - style=dui1.ButtonStyle.LARGER, - debug=True, - ), - dui1.Button( - 'Hello', - size=(200, 50), - style=dui1.ButtonStyle.LARGER, - debug=True, - ), - dui1.Button( - 'Hello', - size=(100, 60), - style=dui1.ButtonStyle.LARGER, - debug=True, - ), - ], - ), - dui1.ButtonRow( - title=_nm(dui1.ButtonStyle.LARGE), - buttons=[ - dui1.Button( - 'Hello', - size=(400, 100), - style=dui1.ButtonStyle.LARGE, - debug=True, - ), - dui1.Button( - 'Hello', - size=(200, 50), - style=dui1.ButtonStyle.LARGE, - debug=True, - ), - dui1.Button( - 'Hello', - size=(100, 60), - style=dui1.ButtonStyle.LARGE, - debug=True, - ), - ], - ), - dui1.ButtonRow( - title=_nm(dui1.ButtonStyle.MEDIUM), - buttons=[ - dui1.Button( - 'Hello', - size=(300, 100), - style=dui1.ButtonStyle.MEDIUM, - debug=True, - ), - dui1.Button( - 'Hello', - size=(200, 50), - style=dui1.ButtonStyle.MEDIUM, - debug=True, - ), - dui1.Button( - 'Hello', - size=(100, 60), - style=dui1.ButtonStyle.MEDIUM, - debug=True, - ), - ], - ), - dui1.ButtonRow( - title=_nm(dui1.ButtonStyle.SMALL), - buttons=[ - dui1.Button( - 'Hello', - size=(200, 100), - style=dui1.ButtonStyle.SMALL, - debug=True, - ), - dui1.Button( - 'Hello', - size=(200, 50), - style=dui1.ButtonStyle.SMALL, - debug=True, - ), - dui1.Button( - 'Hello', - size=(100, 60), - style=dui1.ButtonStyle.SMALL, - debug=True, - ), - ], - ), - dui1.ButtonRow( - title=_nm(dui1.ButtonStyle.BACK), - buttons=[ - dui1.Button( - 'Hello', - size=(200, 100), - style=dui1.ButtonStyle.BACK, - debug=True, - ), - dui1.Button( - 'Hello', - size=(200, 50), - style=dui1.ButtonStyle.BACK, - debug=True, - ), - dui1.Button( - 'Hello', - size=(100, 60), - style=dui1.ButtonStyle.BACK, - debug=True, - ), - ], - ), - dui1.ButtonRow( - title=_nm(dui1.ButtonStyle.BACK_SMALL), - buttons=[ - dui1.Button( - 'Hello', - size=(200, 100), - style=dui1.ButtonStyle.BACK_SMALL, - debug=True, - ), - dui1.Button( - 'Hello', - size=(200, 50), - style=dui1.ButtonStyle.BACK_SMALL, - debug=True, - ), - dui1.Button( - 'Hello', - size=(100, 60), - style=dui1.ButtonStyle.BACK_SMALL, - debug=True, - ), - ], - ), - ], - ) - ) diff --git a/dist/ba_data/python/bauiv1lib/feedback.py b/dist/ba_data/python/bauiv1lib/feedback.py new file mode 100644 index 0000000..cbab894 --- /dev/null +++ b/dist/ba_data/python/bauiv1lib/feedback.py @@ -0,0 +1,91 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to users rating the game.""" + +from __future__ import annotations + +import bauiv1 as bui + + +def ask_for_rating() -> bui.Widget | None: + """(internal)""" + app = bui.app + assert app.classic is not None + platform = app.classic.platform + subplatform = app.classic.subplatform + + # FIXME: should whitelist platforms we *do* want this for. + if bui.app.env.test: + return None + if not ( + platform == 'mac' + or (platform == 'android' and subplatform in ['google', 'cardboard']) + ): + return None + width = 700 + height = 400 + spacing = 40 + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + dlg = bui.containerwidget( + size=(width, height), + transition='in_right', + scale=( + 1.6 + if uiscale is bui.UIScale.SMALL + else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 + ), + ) + v = height - 50 + v -= spacing + v -= 140 + bui.imagewidget( + parent=dlg, + position=(width / 2 - 100, v + 10), + size=(200, 200), + texture=bui.gettexture('cuteSpaz'), + ) + bui.textwidget( + parent=dlg, + position=(15, v - 55), + size=(width - 30, 30), + color=bui.app.ui_v1.infotextcolor, + text=bui.Lstr( + resource='pleaseRateText', + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], + ), + maxwidth=width * 0.95, + max_height=130, + scale=0.85, + h_align='center', + v_align='center', + ) + + def do_rating() -> None: + # This is not currently in use anywhere. + bui.screenmessage(bui.Lstr(resource='error')) + # bui.open_url(url) + bui.containerwidget(edit=dlg, transition='out_left') + + bui.buttonwidget( + parent=dlg, + position=(60, 20), + size=(200, 60), + label=bui.Lstr(resource='wellSureText'), + autoselect=True, + on_activate_call=do_rating, + ) + + def close() -> None: + bui.containerwidget(edit=dlg, transition='out_left') + + btn = bui.buttonwidget( + parent=dlg, + position=(width - 270, 20), + size=(200, 60), + label=bui.Lstr(resource='noThanksText'), + autoselect=True, + on_activate_call=close, + ) + bui.containerwidget(edit=dlg, cancel_button=btn, selected_child=btn) + return dlg diff --git a/dist/ba_data/python/bauiv1lib/fileselector.py b/dist/ba_data/python/bauiv1lib/fileselector.py index 161140c..e5f1588 100644 --- a/dist/ba_data/python/bauiv1lib/fileselector.py +++ b/dist/ba_data/python/bauiv1lib/fileselector.py @@ -249,7 +249,7 @@ class FileSelectorWindow(bui.MainWindow): if duration < min_time: time.sleep(min_time - duration) bui.pushcall( - bui.CallStrict(self._callback, files, None), + bui.Call(self._callback, files, None), from_other_thread=True, ) except Exception as exc: @@ -258,7 +258,7 @@ class FileSelectorWindow(bui.MainWindow): logging.exception('Error in fileselector refresh thread.') nofiles: list[str] = [] bui.pushcall( - bui.CallStrict(self._callback, nofiles, str(exc)), + bui.Call(self._callback, nofiles, str(exc)), from_other_thread=True, ) @@ -431,9 +431,7 @@ class FileSelectorWindow(bui.MainWindow): root_selectable=True, background=False, click_activate=True, - on_activate_call=bui.CallStrict( - self._on_entry_activated, entry - ), + on_activate_call=bui.Call(self._on_entry_activated, entry), ) if num == 0: bui.widget(edit=cnt, up_widget=self._back_button) diff --git a/dist/ba_data/python/bauiv1lib/gather/__init__.py b/dist/ba_data/python/bauiv1lib/gather/__init__.py index 3b0ed5e..fe25edb 100644 --- a/dist/ba_data/python/bauiv1lib/gather/__init__.py +++ b/dist/ba_data/python/bauiv1lib/gather/__init__.py @@ -2,6 +2,7 @@ # """Provides UI for inviting/joining friends.""" + from bauiv1lib.gather._gather import GatherTab, GatherWindow __all__ = ['GatherTab', 'GatherWindow'] diff --git a/dist/ba_data/python/bauiv1lib/gather/_gather.py b/dist/ba_data/python/bauiv1lib/gather/_gather.py index 5d07195..43843de 100644 --- a/dist/ba_data/python/bauiv1lib/gather/_gather.py +++ b/dist/ba_data/python/bauiv1lib/gather/_gather.py @@ -212,7 +212,7 @@ class GatherWindow(bui.MainWindow): self._scroll_left + tab_inset, self._scroll_bottom + self._scroll_height - 4.0, ), - on_select_call=bui.WeakCallPartial(self._set_tab), + on_select_call=bui.WeakCall(self._set_tab), ) # Now instantiate handlers for these tabs. diff --git a/dist/ba_data/python/bauiv1lib/gather/abouttab.py b/dist/ba_data/python/bauiv1lib/gather/abouttab.py index 55ab7fc..bd5442d 100644 --- a/dist/ba_data/python/bauiv1lib/gather/abouttab.py +++ b/dist/ba_data/python/bauiv1lib/gather/abouttab.py @@ -160,7 +160,7 @@ class AboutGatherTab(GatherTab): fallback_resource='gatherWindow.getFriendInviteCodeText', ), autoselect=True, - on_activate_call=bui.WeakCallStrict(self._invite_to_try_press), + on_activate_call=bui.WeakCall(self._invite_to_try_press), up_widget=tab_button, show_buffer_top=500, ) @@ -190,9 +190,7 @@ class AboutGatherTab(GatherTab): textcolor=(0.6, 0.6, 1), label=bui.Lstr(resource='discordJoinText'), autoselect=True, - on_activate_call=bui.WeakCallStrict( - self._join_the_discord_press - ), + on_activate_call=bui.WeakCall(self._join_the_discord_press), up_widget=( invite_button if invite_button is not None else tab_button ), diff --git a/dist/ba_data/python/bauiv1lib/gather/manualtab.py b/dist/ba_data/python/bauiv1lib/gather/manualtab.py index cb24238..5b38a89 100644 --- a/dist/ba_data/python/bauiv1lib/gather/manualtab.py +++ b/dist/ba_data/python/bauiv1lib/gather/manualtab.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Defines the manual tab in the gather UI.""" - # pylint: disable=too-many-lines from __future__ import annotations @@ -11,13 +10,11 @@ from enum import Enum from threading import Thread from dataclasses import dataclass from typing import TYPE_CHECKING, cast, override +from bauiv1lib.gather import GatherTab -from bacommon.analytics import ClassicAnalyticsEvent import bauiv1 as bui import bascenev1 as bs -from bauiv1lib.gather import GatherTab - if TYPE_CHECKING: from typing import Any, Callable @@ -326,7 +323,7 @@ class ManualGatherTab(GatherTab): label=bui.Lstr(resource='gatherWindow.' 'manualConnectText'), position=(c_width * 0.5 - 300, v), autoselect=True, - on_activate_call=bui.CallStrict(self._connect, txt, txt2), + on_activate_call=bui.Call(self._connect, txt, txt2), ) savebutton = bui.buttonwidget( parent=self._container, @@ -335,7 +332,7 @@ class ManualGatherTab(GatherTab): label=bui.Lstr(resource='gatherWindow.favoritesSaveText'), position=(c_width * 0.5 - 240 + 490 - 200, v), autoselect=True, - on_activate_call=bui.CallStrict(self._save_server, txt, txt2), + on_activate_call=bui.Call(self._save_server, txt, txt2), ) bui.widget(edit=btn, right_widget=savebutton) bui.widget(edit=savebutton, left_widget=btn, up_widget=txt2) @@ -356,7 +353,7 @@ class ManualGatherTab(GatherTab): color=(0.5, 0.9, 0.5), scale=0.8, selectable=True, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._on_show_my_address_button_press, v, self._container, @@ -519,7 +516,7 @@ class ManualGatherTab(GatherTab): _HostLookupThread( name=config['addr'], port=config['port'], - call=bui.WeakCallPartial(self._host_lookup_result), + call=bui.WeakCall(self._host_lookup_result), ).start() def _on_favorites_edit_press(self) -> None: @@ -640,7 +637,7 @@ class ManualGatherTab(GatherTab): cbtn = bui.buttonwidget( parent=cnt, label=bui.Lstr(resource='cancelText'), - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( lambda c: bui.containerwidget(edit=c, transition='out_scale'), cnt, ), @@ -653,7 +650,7 @@ class ManualGatherTab(GatherTab): label=bui.Lstr(resource='saveText'), size=(180, 60), position=(c_width - 230, 30), - on_activate_call=bui.CallStrict(self._edit_saved_party), + on_activate_call=bui.Call(self._edit_saved_party), autoselect=True, ) bui.widget(edit=cbtn, right_widget=okb) @@ -757,7 +754,7 @@ class ManualGatherTab(GatherTab): selectable=True, color=(1.0, 1, 0.4), always_highlight=True, - on_select_call=bui.CallStrict(self._on_favorite_select, server), + on_select_call=bui.Call(self._on_favorite_select, server), on_activate_call=self._favorites_connect_button.activate, text=( config['Saved Servers'][server]['name'] @@ -806,13 +803,6 @@ class ManualGatherTab(GatherTab): def _connect( self, textwidget: bui.Widget, port_textwidget: bui.Widget ) -> None: - - bui.app.analytics.submit_event( - ClassicAnalyticsEvent( - ClassicAnalyticsEvent.EventType.JOIN_PARTY_BY_ADDRESS - ) - ) - addr = cast(str, bui.textwidget(query=textwidget)) if addr == '': bui.screenmessage( @@ -834,9 +824,7 @@ class ManualGatherTab(GatherTab): return _HostLookupThread( - name=addr, - port=port, - call=bui.WeakCallPartial(self._host_lookup_result), + name=addr, port=port, call=bui.WeakCall(self._host_lookup_result) ).start() def _save_server( @@ -918,7 +906,7 @@ class ManualGatherTab(GatherTab): val = sock.getsockname()[0] sock.close() bui.pushcall( - bui.CallStrict( + bui.Call( _safe_set_text, self._checking_state_text, val, @@ -930,7 +918,7 @@ class ManualGatherTab(GatherTab): if is_udp_communication_error(exc): bui.pushcall( - bui.CallStrict( + bui.Call( _safe_set_text, self._checking_state_text, bui.Lstr(resource='gatherWindow.' 'noConnectionText'), @@ -940,7 +928,7 @@ class ManualGatherTab(GatherTab): ) else: bui.pushcall( - bui.CallStrict( + bui.Call( _safe_set_text, self._checking_state_text, bui.Lstr( @@ -1065,7 +1053,7 @@ class ManualGatherTab(GatherTab): self._access_check_count = 0 # Cap our refreshes eventually. self._access_check_timer = bui.AppTimer( 10.0, - bui.WeakCallStrict( + bui.WeakCall( self._access_check_update, t_addr, t_accessible, @@ -1098,7 +1086,7 @@ class ManualGatherTab(GatherTab): bui.app.classic.master_server_v1_get( 'bsAccessCheck', {'b': bui.app.env.engine_build_number}, - callback=bui.WeakCallPartial(self._on_accessible_response), + callback=bui.WeakCall(self._on_accessible_response), ) def _on_accessible_response(self, data: dict[str, Any] | None) -> None: diff --git a/dist/ba_data/python/bauiv1lib/gather/nearbytab.py b/dist/ba_data/python/bauiv1lib/gather/nearbytab.py index 621bda8..013e2a2 100644 --- a/dist/ba_data/python/bauiv1lib/gather/nearbytab.py +++ b/dist/ba_data/python/bauiv1lib/gather/nearbytab.py @@ -7,7 +7,6 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING, override -from bacommon.analytics import ClassicAnalyticsEvent import bauiv1 as bui import bascenev1 as bs @@ -45,17 +44,14 @@ class NetScanner: bui.widget(edit=self._columnwidget, up_widget=tab_button) self._width = width self._last_selected_host: dict[str, Any] | None = None - self._last_scan: list[dict[str, str]] | None = None self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self.update), repeat=True ) - # Run two cycles pretty immediately - this should send out a - # "who's there" and update the list with any immediate-ish - # results so we may not have to wait a second to see things - # appear. - self._update() - bui.apptimer(0.25, bui.WeakCallStrict(self._update)) + # Go ahead and run a few *almost* immediately so we don't + # have to wait a second. + self.update() + bui.apptimer(0.25, bui.WeakCall(self.update)) def __del__(self) -> None: bs.end_host_scanning() @@ -65,53 +61,37 @@ class NetScanner: def _on_activate(self, host: dict[str, Any]) -> None: - bui.app.analytics.submit_event( - ClassicAnalyticsEvent( - ClassicAnalyticsEvent.EventType.JOIN_NEARBY_PARTY - ) - ) - # Store UI location to return to when done. if bs.app.classic is not None: bs.app.classic.save_ui_state() bs.connect_to_party(host['address']) - def _update(self) -> None: + def update(self) -> None: """(internal)""" # In case our UI was killed from under us. if not self._columnwidget: - bui.uilog.error( - 'nearbytab NetScanner running without UI at time %s.', - bui.apptime(), + print( + f'ERROR: NetScanner running without UI at time {bui.apptime()}.' ) return - hosts = bs.host_scan_cycle() - - # If nothing has changed since our last run, do nothing. If we - # do redundant rebuilds then we are likely to lose some clicks - # due to rebuilding after a click starts but before it ends. - if hosts == self._last_scan: - return - - self._last_scan = hosts - t_scale = 1.6 for child in self._columnwidget.get_children(): child.delete() # Grab this now this since adding widgets will change it. last_selected_host = self._last_selected_host + hosts = bs.host_scan_cycle() for i, host in enumerate(hosts): txt3 = bui.textwidget( parent=self._columnwidget, size=(self._width / t_scale, 30), selectable=True, color=(1, 1, 1), - on_select_call=bui.CallStrict(self._on_select, host), - on_activate_call=bui.CallStrict(self._on_activate, host), + on_select_call=bui.Call(self._on_select, host), + on_activate_call=bui.Call(self._on_activate, host), click_activate=True, text=host['display_string'], h_align='left', diff --git a/dist/ba_data/python/bauiv1lib/gather/privatetab.py b/dist/ba_data/python/bauiv1lib/gather/privatetab.py index 8ec6a23..6b4bf9f 100644 --- a/dist/ba_data/python/bauiv1lib/gather/privatetab.py +++ b/dist/ba_data/python/bauiv1lib/gather/privatetab.py @@ -15,7 +15,6 @@ from typing import TYPE_CHECKING, cast, override from efro.error import CommunicationError from efro.dataclassio import dataclass_from_dict, dataclass_to_dict -from bacommon.analytics import ClassicAnalyticsEvent import bacommon.cloud from bacommon.net import ( PrivateHostingState, @@ -64,7 +63,7 @@ class PrivateGatherTab(GatherTab): self._state: State = State() self._last_datacode_refresh_time: float | None = None self._hostingstate = PrivateHostingState() - self._v2state: bacommon.classic.PrivatePartyResponse | None = None + self._v2state: bacommon.bs.PrivatePartyResponse | None = None self._join_sub_tab_text: bui.Widget | None = None self._host_sub_tab_text: bui.Widget | None = None self._update_timer: bui.AppTimer | None = None @@ -166,7 +165,7 @@ class PrivateGatherTab(GatherTab): ) self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) # Prevent taking any action until we've updated our state. @@ -323,7 +322,7 @@ class PrivateGatherTab(GatherTab): 'type': 'PRIVATE_PARTY_QUERY', 'expire_time': time.time() + 20, }, - callback=bui.WeakCallPartial( + callback=bui.WeakCall( self._idle_hosting_state_response ), ) @@ -342,7 +341,7 @@ class PrivateGatherTab(GatherTab): if plus.accounts.primary is not None: with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.classic.PrivatePartyMessage( + bacommon.bs.PrivatePartyMessage( need_datacode=( self._last_datacode_refresh_time is None or time.monotonic() @@ -350,7 +349,7 @@ class PrivateGatherTab(GatherTab): > 30.0 ) ), - on_response=bui.WeakCallPartial( + on_response=bui.WeakCall( self._on_private_party_query_response ), ) @@ -358,7 +357,7 @@ class PrivateGatherTab(GatherTab): self._last_v2_state_query_time = now def _on_private_party_query_response( - self, response: bacommon.classic.PrivatePartyResponse | Exception + self, response: bacommon.bs.PrivatePartyResponse | Exception ) -> None: if isinstance(response, Exception): self._debug_server_comm('got pp v2 state response (err)') @@ -949,13 +948,6 @@ class PrivateGatherTab(GatherTab): ) def _connect_to_party_code(self, code: str) -> None: - - bui.app.analytics.submit_event( - ClassicAnalyticsEvent( - ClassicAnalyticsEvent.EventType.JOIN_PRIVATE_PARTY - ) - ) - # Ignore attempted followup sends for a few seconds (this will # reset if we get a response). plus = bui.app.plus @@ -979,7 +971,7 @@ class PrivateGatherTab(GatherTab): 'expire_time': time.time() + 20, 'code': code, }, - callback=bui.WeakCallPartial(self._connect_response), + callback=bui.WeakCall(self._connect_response), ) plus.run_v1_account_transactions() @@ -1037,7 +1029,7 @@ class PrivateGatherTab(GatherTab): 'expire_time': time.time() + 20, 'datacode': self._v2state.datacode, }, - callback=bui.WeakCallPartial(self._hosting_state_response), + callback=bui.WeakCall(self._hosting_state_response), ) plus.run_v1_account_transactions() @@ -1048,7 +1040,7 @@ class PrivateGatherTab(GatherTab): 'type': 'PRIVATE_PARTY_STOP', 'expire_time': time.time() + 20, }, - callback=bui.WeakCallPartial(self._hosting_state_response), + callback=bui.WeakCall(self._hosting_state_response), ) plus.run_v1_account_transactions() bui.getsound('click01').play() diff --git a/dist/ba_data/python/bauiv1lib/gather/publictab.py b/dist/ba_data/python/bauiv1lib/gather/publictab.py index ee43692..8aa82bf 100644 --- a/dist/ba_data/python/bauiv1lib/gather/publictab.py +++ b/dist/ba_data/python/bauiv1lib/gather/publictab.py @@ -12,7 +12,6 @@ from enum import Enum from dataclasses import dataclass from typing import TYPE_CHECKING, cast, override -from bacommon.analytics import ClassicAnalyticsEvent from bauiv1lib.gather import GatherTab import bauiv1 as bui import bascenev1 as bs @@ -118,13 +117,11 @@ class UIRow: size=(sub_scroll_width * 0.46, 20), position=(0 + hpos, 4 + vpos), selectable=True, - on_select_call=bui.WeakCallStrict( + on_select_call=bui.WeakCall( tab.set_public_party_selection, Selection(party.get_key(), SelectionComponent.NAME), ), - on_activate_call=bui.WeakCallStrict( - tab.on_public_party_activate, party - ), + on_activate_call=bui.WeakCall(tab.on_public_party_activate, party), click_activate=True, maxwidth=sub_scroll_width * 0.45, corner_scale=1.4, @@ -163,8 +160,8 @@ class UIRow: label=bui.Lstr(resource='statsText'), parent=columnwidget, autoselect=True, - on_activate_call=bui.CallStrict(bui.open_url, url), - on_select_call=bui.WeakCallStrict( + on_activate_call=bui.Call(bui.open_url, url), + on_select_call=bui.WeakCall( tab.set_public_party_selection, Selection(party.get_key(), SelectionComponent.STATS_BUTTON), ), @@ -271,9 +268,7 @@ class AddrFetchThread(Thread): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect(('8.8.8.8', 80)) val = sock.getsockname()[0] - bui.pushcall( - bui.CallStrict(self._call, val), from_other_thread=True - ) + bui.pushcall(bui.Call(self._call, val), from_other_thread=True) except Exception as exc: from efro.error import is_udp_communication_error @@ -339,7 +334,7 @@ class PingThread(Thread): time.sleep(1) ping = (time.time() - starttime) * 1000.0 bui.pushcall( - bui.CallStrict( + bui.Call( self._call, self._address, self._port, @@ -502,13 +497,11 @@ class PublicGatherTab(GatherTab): # Attempt to fetch our local address so we have it for error # messages. if self._local_address is None: - AddrFetchThread( - bui.WeakCallPartial(self._fetch_local_addr_cb) - ).start() + AddrFetchThread(bui.WeakCall(self._fetch_local_addr_cb)).start() self._set_sub_tab(self._sub_tab, region_width, region_height) self._update_timer = bui.AppTimer( - 0.1, bui.WeakCallStrict(self._update), repeat=True + 0.1, bui.WeakCall(self._update), repeat=True ) return self._container @@ -821,7 +814,7 @@ class PublicGatherTab(GatherTab): parent=self._container, id=f'{self._idprefix}|maxsizeminus', size=(40, 40), - on_activate_call=bui.WeakCallStrict( + on_activate_call=bui.WeakCall( self._on_max_public_party_size_minus_press ), position=(280 + xoffs, v - 26), @@ -832,7 +825,7 @@ class PublicGatherTab(GatherTab): parent=self._container, id=f'{self._idprefix}|maxsizeplus', size=(40, 40), - on_activate_call=bui.WeakCallStrict( + on_activate_call=bui.WeakCall( self._on_max_public_party_size_plus_press ), position=(350 + xoffs, v - 26), @@ -1161,7 +1154,7 @@ class PublicGatherTab(GatherTab): # Now, new or not, update its values. party.queue = party_in.get('q') - assert isinstance(party.queue, str | None) + assert isinstance(party.queue, (str, type(None))) party.port = port party.name = party_in['n'] assert isinstance(party.name, str) @@ -1174,7 +1167,7 @@ class PublicGatherTab(GatherTab): party.ping_interval = 0.001 * party_in['pi'] assert isinstance(party.ping_interval, float) party.stats_addr = party_in['sa'] - assert isinstance(party.stats_addr, str | None) + assert isinstance(party.stats_addr, (str, type(None))) # Make sure the party's UI gets updated. party.clean_display_index = None @@ -1262,9 +1255,7 @@ class PublicGatherTab(GatherTab): 'proto': bs.protocol_version(), 'lang': bui.app.lang.language, }, - callback=bui.WeakCallPartial( - self._on_public_party_query_result - ), + callback=bui.WeakCall(self._on_public_party_query_result), ) plus.run_v1_account_transactions() else: @@ -1306,9 +1297,7 @@ class PublicGatherTab(GatherTab): party.ping_attempts += 1 PingThread( - party.address, - party.port, - bui.WeakCallPartial(self._ping_callback), + party.address, party.port, bui.WeakCall(self._ping_callback) ).start() def _ping_callback( @@ -1424,9 +1413,7 @@ class PublicGatherTab(GatherTab): bui.app.classic.master_server_v1_get( 'bsAccessCheck', {'b': bui.app.env.engine_build_number}, - callback=bui.WeakCallPartial( - self._on_public_party_accessible_response - ), + callback=bui.WeakCall(self._on_public_party_accessible_response), ) def _on_start_advertizing_press(self) -> None: @@ -1496,13 +1483,6 @@ class PublicGatherTab(GatherTab): def on_public_party_activate(self, party: PartyEntry) -> None: """Called when a party is clicked or otherwise activated.""" self.save_state() - - bui.app.analytics.submit_event( - ClassicAnalyticsEvent( - ClassicAnalyticsEvent.EventType.JOIN_PUBLIC_PARTY - ) - ) - if party.queue is not None: from bauiv1lib.partyqueue import PartyQueueWindow diff --git a/dist/ba_data/python/bauiv1lib/gettokens.py b/dist/ba_data/python/bauiv1lib/gettokens.py index 1493004..7602d03 100644 --- a/dist/ba_data/python/bauiv1lib/gettokens.py +++ b/dist/ba_data/python/bauiv1lib/gettokens.py @@ -6,13 +6,15 @@ from __future__ import annotations import time from enum import Enum +from functools import partial from dataclasses import dataclass from typing import TYPE_CHECKING, assert_never, override import bacommon.cloud -import bacommon.classic +import bacommon.bs import bauiv1 as bui + if TYPE_CHECKING: from typing import Any, Callable @@ -71,9 +73,6 @@ class GetTokensWindow(bui.MainWindow): auxiliary_style: bool = True, ): # pylint: disable=too-many-locals - - self._auxiliary_style = auxiliary_style - self._uiopenstate = bui.UIOpenState('gettokens') bwidthstd = 170 bwidthwide = 300 ycolor = (0, 0, 0.3) @@ -108,12 +107,7 @@ class GetTokensWindow(bui.MainWindow): _TxtDef( bui.Lstr( resource='tokens.numTokensText', - subs=[ - ( - '${COUNT}', - str(bacommon.classic.TOKENS1_COUNT), - ) - ], + subs=[('${COUNT}', str(bacommon.bs.TOKENS1_COUNT))], ), pos=(bwidthstd * 0.5, pos1), color=(1.1, 1.05, 1.0), @@ -153,12 +147,7 @@ class GetTokensWindow(bui.MainWindow): _TxtDef( bui.Lstr( resource='tokens.numTokensText', - subs=[ - ( - '${COUNT}', - str(bacommon.classic.TOKENS2_COUNT), - ) - ], + subs=[('${COUNT}', str(bacommon.bs.TOKENS2_COUNT))], ), pos=(bwidthstd * 0.5, pos1), color=(1.1, 1.05, 1.0), @@ -198,12 +187,7 @@ class GetTokensWindow(bui.MainWindow): _TxtDef( bui.Lstr( resource='tokens.numTokensText', - subs=[ - ( - '${COUNT}', - str(bacommon.classic.TOKENS3_COUNT), - ) - ], + subs=[('${COUNT}', str(bacommon.bs.TOKENS3_COUNT))], ), pos=(bwidthstd * 0.5, pos1), color=(1.1, 1.05, 1.0), @@ -243,12 +227,7 @@ class GetTokensWindow(bui.MainWindow): _TxtDef( bui.Lstr( resource='tokens.numTokensText', - subs=[ - ( - '${COUNT}', - str(bacommon.classic.TOKENS4_COUNT), - ) - ], + subs=[('${COUNT}', str(bacommon.bs.TOKENS4_COUNT))], ), pos=(bwidthstd * 0.5, pos1), color=(1.1, 1.05, 1.0), @@ -365,7 +344,7 @@ class GetTokensWindow(bui.MainWindow): color=(0.3, 0.23, 0.36), scale=scale, toolbar_visibility=( - 'menu_tokens' + 'get_tokens' if uiscale is bui.UIScale.SMALL else 'menu_full' ), @@ -454,7 +433,7 @@ class GetTokensWindow(bui.MainWindow): self._state = self.State.LOADING self._update_timer = bui.AppTimer( - 0.789, bui.WeakCallStrict(self._update), repeat=True + 0.789, bui.WeakCall(self._update), repeat=True ) self._update() @@ -462,16 +441,9 @@ class GetTokensWindow(bui.MainWindow): def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. cls = type(self) - - # Pull everything out of self here. If we do it below in the lambda, - # we'll keep self alive which is bad. - auxiliary_style = self._auxiliary_style - return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( - transition=transition, - origin_widget=origin_widget, - auxiliary_style=auxiliary_style, + transition=transition, origin_widget=origin_widget ) ) @@ -498,9 +470,7 @@ class GetTokensWindow(bui.MainWindow): with plus.accounts.primary: plus.cloud.send_message_cb( bacommon.cloud.StoreQueryMessage(), - on_response=bui.WeakCallPartial( - self._on_store_query_response - ), + on_response=bui.WeakCall(self._on_store_query_response), ) # Can't do much until we get a store state. @@ -645,7 +615,7 @@ class GetTokensWindow(bui.MainWindow): scale=0.8, color=(0.4, 0.25, 0.5), textcolor=self._textcolor, - on_activate_call=bui.WeakCallStrict( + on_activate_call=partial( self._on_learn_more_press, response.token_info_url ), ) @@ -680,7 +650,7 @@ class GetTokensWindow(bui.MainWindow): size=(buttondef.width, 275), position=(x, -10 + yoffs), button_type='square', - on_activate_call=bui.WeakCallStrict( + on_activate_call=partial( self._purchase_press, buttondef.itemid ), ) @@ -816,7 +786,7 @@ class GetTokensWindow(bui.MainWindow): bui.open_url(url) -def show_get_tokens_prompt(origin_widget: bui.Widget | None = None) -> None: +def show_get_tokens_prompt() -> None: """Show a 'not enough tokens' prompt with an option to purchase more. Note that the purchase option may not always be available @@ -826,19 +796,14 @@ def show_get_tokens_prompt(origin_widget: bui.Widget | None = None) -> None: assert bui.app.classic is not None - get_tokens_button = bui.get_special_widget('get_tokens_button') - # Currently always allowing token purchases. if bool(True): ConfirmWindow( bui.Lstr(resource='tokens.notEnoughTokensText'), - bui.CallStrict( - show_get_tokens_window, origin_widget=get_tokens_button - ), + show_get_tokens_window, ok_text=bui.Lstr(resource='tokens.getTokensText'), width=460, height=130, - origin_widget=origin_widget, ) else: ConfirmWindow( @@ -846,45 +811,39 @@ def show_get_tokens_prompt(origin_widget: bui.Widget | None = None) -> None: cancel_button=False, width=460, height=130, - origin_widget=origin_widget, ) -def show_get_tokens_window( - origin_widget: bui.Widget | None = None, toggle: bool = False -) -> None: +def show_get_tokens_window(origin_widget: bui.Widget | None = None) -> None: """Transition to the get-tokens main-window from anywhere.""" - # NOTE TO USERS: The code below is not the standard way to do things; + # NOTE TO USERS: The code below is not the proper way to do things; # whenever possible one should use a MainWindow's # main_window_replace() or main_window_back() methods or # bauiv1.auxiliary_window_activate(). We just need to do things a # bit more manually in this particular case. - # Basically we want to push our window on to the stack from - # anywhere so we can go back to where we were once done even if it - # was an auxiliary window. + # Basically we want to pop up our auxiliary window but we don't want + # to replace any existing auxiliary windows; we want our close + # button to go back to whatever was there already, no matter whether + # it was an auxiliary window or not. prev_main_window = bui.app.ui_v1.get_main_window() # Special-case: If it seems we're already in the window, do nothing. if isinstance(prev_main_window, GetTokensWindow): - if toggle: - prev_main_window.main_window_back() return ui = bui.app.ui_v1 - # Set our new main window. Note that we pass auxiliary_style=False - # so that we get a back button instead of a close button. + # Set our new main window. ui.set_main_window( - GetTokensWindow(origin_widget=origin_widget, auxiliary_style=False), + GetTokensWindow(origin_widget=origin_widget), from_window=False, # Don't check where we're coming from. back_state=ui.save_current_main_window_state(), - is_auxiliary=False, + is_auxiliary=True, suppress_warning=True, - extra_type_id='', ) # Transition out any previous main window. if prev_main_window is not None: - prev_main_window.main_window_close(transition='out_left') + prev_main_window.main_window_close() diff --git a/dist/ba_data/python/bauiv1lib/help.py b/dist/ba_data/python/bauiv1lib/help.py index cda1b6b..73f7ee0 100644 --- a/dist/ba_data/python/bauiv1lib/help.py +++ b/dist/ba_data/python/bauiv1lib/help.py @@ -447,9 +447,7 @@ class HelpWindow(bui.MainWindow): color=(1, 0.7, 0.3), selectable=False, enable_sound=False, - on_activate_call=bui.WeakCallStrict( - self._play_sound, 'spazAttack0', 4 - ), + on_activate_call=bui.WeakCall(self._play_sound, 'spazAttack0', 4), ) txt_scale = getres(f'{self._r}.punchInfoTextScale') @@ -477,9 +475,7 @@ class HelpWindow(bui.MainWindow): color=(1, 0.3, 0.3), selectable=False, enable_sound=False, - on_activate_call=bui.WeakCallStrict( - self._play_sound, 'explosion0', 5 - ), + on_activate_call=bui.WeakCall(self._play_sound, 'explosion0', 5), ) txt = bui.Lstr(resource=f'{self._r}.bombInfoText').evaluate() @@ -508,9 +504,7 @@ class HelpWindow(bui.MainWindow): color=(0.5, 0.5, 1), selectable=False, enable_sound=False, - on_activate_call=bui.WeakCallStrict( - self._play_sound, 'spazPickup0', 1 - ), + on_activate_call=bui.WeakCall(self._play_sound, 'spazPickup0', 1), ) txtl = bui.Lstr(resource=f'{self._r}.pickUpInfoText') @@ -538,9 +532,7 @@ class HelpWindow(bui.MainWindow): color=(0.4, 1, 0.4), selectable=False, enable_sound=False, - on_activate_call=bui.WeakCallStrict( - self._play_sound, 'spazJump0', 4 - ), + on_activate_call=bui.WeakCall(self._play_sound, 'spazJump0', 4), ) txt = bui.Lstr(resource=f'{self._r}.jumpInfoText').evaluate() diff --git a/dist/ba_data/python/bauiv1lib/iconpicker.py b/dist/ba_data/python/bauiv1lib/iconpicker.py index 9c3301f..0a9bd4b 100644 --- a/dist/ba_data/python/bauiv1lib/iconpicker.py +++ b/dist/ba_data/python/bauiv1lib/iconpicker.py @@ -130,7 +130,7 @@ class IconPicker(PopupWindow): text_scale=1.2, label='', color=(0.65, 0.65, 0.65), - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._select_icon, self._icons[index] ), position=pos, @@ -176,7 +176,7 @@ class IconPicker(PopupWindow): plus = bui.app.plus assert plus is not None - if plus.accounts.primary is None: + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return diff --git a/dist/ba_data/python/bauiv1lib/inbox.py b/dist/ba_data/python/bauiv1lib/inbox.py index fc5c589..1ec082d 100644 --- a/dist/ba_data/python/bauiv1lib/inbox.py +++ b/dist/ba_data/python/bauiv1lib/inbox.py @@ -12,9 +12,7 @@ from typing import override, assert_never, TYPE_CHECKING from efro.util import strict_partial, pairs_from_flat from efro.error import CommunicationError -import bacommon.clouddialog as cdlg -import bacommon.clouddialog.basic as bcdlg -import bacommon.classic +import bacommon.bs from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top import bauiv1 as bui @@ -22,8 +20,6 @@ if TYPE_CHECKING: import datetime from typing import Callable - import bacommon.displayitem as ditm - class _Section: def get_height(self) -> float: @@ -168,7 +164,7 @@ class _DisplayItemsSection(_Section): self, *, sub_width: float, - items: list[ditm.Wrapper], + items: list[bacommon.bs.DisplayItemWrapper], width: float = 100.0, spacing_top: float = 0.0, spacing_bottom: float = 0.0, @@ -290,17 +286,15 @@ class _ExpireTimeSection(_Section): h_align='center', v_align='center', ) - self._timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True - ) + self._timer = bui.AppTimer(1.0, bui.WeakCall(self._update), repeat=True) self._update() @dataclass class _EntryDisplay: - interaction_style: bcdlg.InteractionStyle - button_label_positive: bcdlg.ButtonLabel - button_label_negative: bcdlg.ButtonLabel + interaction_style: bacommon.bs.BasicCloudDialog.InteractionStyle + button_label_positive: bacommon.bs.BasicCloudDialog.ButtonLabel + button_label_negative: bacommon.bs.BasicCloudDialog.ButtonLabel sections: list[_Section] id: str total_height: float @@ -326,8 +320,6 @@ class InboxWindow(bui.MainWindow): assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale - self._uiopenstate = bui.UIOpenState('classicinbox') - self._action_ui_pause: bui.RootUIUpdatePause | None = None self._entry_displays: list[_EntryDisplay] = [] @@ -495,10 +487,8 @@ class InboxWindow(bui.MainWindow): with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.classic.InboxRequestMessage(), - on_response=bui.WeakCallPartial( - self._on_inbox_request_response - ), + bacommon.bs.InboxRequestMessage(), + on_response=bui.WeakCall(self._on_inbox_request_response), ) @override @@ -527,7 +517,7 @@ class InboxWindow(bui.MainWindow): def _on_entry_display_press( self, display_weak: weakref.ReferenceType[_EntryDisplay], - action: cdlg.Action, + action: bacommon.bs.CloudDialogAction, ) -> None: display = display_weak() if display is None: @@ -539,7 +529,10 @@ class InboxWindow(bui.MainWindow): # We currently only recognize basic entries and their possible # interaction types. - if display.interaction_style is bcdlg.InteractionStyle.UNKNOWN: + if ( + display.interaction_style + is bacommon.bs.BasicCloudDialog.InteractionStyle.UNKNOWN + ): display.processing_complete = True self._close_soon_if_all_processed() return @@ -561,8 +554,8 @@ class InboxWindow(bui.MainWindow): # Ask the master-server to run our action. with plus.accounts.primary: plus.cloud.send_message_cb( - cdlg.ActionMessage(display.id, action), - on_response=bui.WeakCallPartial( + bacommon.bs.CloudDialogActionMessage(display.id, action), + on_response=bui.WeakCall( self._on_client_ui_action_response, display_weak, action, @@ -572,12 +565,12 @@ class InboxWindow(bui.MainWindow): # Tweak the UI to show that things are in motion. button = ( display.button_positive - if action is cdlg.Action.BUTTON_PRESS_POSITIVE + if action is bacommon.bs.CloudDialogAction.BUTTON_PRESS_POSITIVE else display.button_negative ) button_spinner = ( display.button_spinner_positive - if action is cdlg.Action.BUTTON_PRESS_POSITIVE + if action is bacommon.bs.CloudDialogAction.BUTTON_PRESS_POSITIVE else display.button_spinner_negative ) if button is not None: @@ -586,7 +579,7 @@ class InboxWindow(bui.MainWindow): bui.spinnerwidget(edit=button_spinner, visible=True) def _close_soon_if_all_processed(self) -> None: - bui.apptimer(0.25, bui.WeakCallStrict(self._close_if_all_processed)) + 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._entry_displays): @@ -616,8 +609,8 @@ class InboxWindow(bui.MainWindow): def _on_client_ui_action_response( self, display_weak: weakref.ReferenceType[_EntryDisplay], - action: cdlg.Action, - response: cdlg.ActionResponse | Exception, + action: bacommon.bs.CloudDialogAction, + response: bacommon.bs.CloudDialogActionResponse | Exception, ) -> None: # pylint: disable=too-many-branches @@ -640,12 +633,12 @@ class InboxWindow(bui.MainWindow): # Tweak the button to show results. button = ( display.button_positive - if action is cdlg.Action.BUTTON_PRESS_POSITIVE + if action is bacommon.bs.CloudDialogAction.BUTTON_PRESS_POSITIVE else display.button_negative ) button_spinner = ( display.button_spinner_positive - if action is cdlg.Action.BUTTON_PRESS_POSITIVE + if action is bacommon.bs.CloudDialogAction.BUTTON_PRESS_POSITIVE else display.button_spinner_negative ) # Always hide spinner at this point. @@ -697,7 +690,7 @@ class InboxWindow(bui.MainWindow): bui.buttonwidget(edit=button, label=label) def _on_inbox_request_response( - self, response: bacommon.classic.InboxRequestResponse | Exception + self, response: bacommon.bs.InboxRequestResponse | Exception ) -> None: # pylint: disable=too-many-locals # pylint: disable=too-many-statements @@ -723,7 +716,7 @@ class InboxWindow(bui.MainWindow): self._error(errmsg) return - assert isinstance(response, bacommon.classic.InboxRequestResponse) + assert isinstance(response, bacommon.bs.InboxRequestResponse) # If we got no messages, don't touch anything. This keeps # keyboard control working in the empty case. @@ -766,9 +759,9 @@ class InboxWindow(bui.MainWindow): # textfin: str color: tuple[float, float, float] - interaction_style: bcdlg.InteractionStyle - button_label_positive: bcdlg.ButtonLabel - button_label_negative: bcdlg.ButtonLabel + interaction_style: bacommon.bs.BasicCloudDialog.InteractionStyle + button_label_positive: bacommon.bs.BasicCloudDialog.ButtonLabel + button_label_negative: bacommon.bs.BasicCloudDialog.ButtonLabel sections: list[_Section] = [] total_height = 80.0 @@ -776,7 +769,7 @@ class InboxWindow(bui.MainWindow): # Display only entries where we recognize all style/label # values and ui component types. if ( - isinstance(wrapper.ui, bcdlg.BasicCloudDialog) + isinstance(wrapper.ui, bacommon.bs.BasicCloudDialog) and not wrapper.ui.contains_unknown_elements() ): color = (0.55, 0.5, 0.7) @@ -784,13 +777,15 @@ class InboxWindow(bui.MainWindow): button_label_positive = wrapper.ui.button_label_positive button_label_negative = wrapper.ui.button_label_negative - idcls = bcdlg.ComponentTypeID + idcls = bacommon.bs.BasicCloudDialogComponentTypeID for component in wrapper.ui.components: ctypeid = component.get_type_id() section: _Section if ctypeid is idcls.TEXT: - assert isinstance(component, bcdlg.Text) + assert isinstance( + component, bacommon.bs.BasicCloudDialogComponentText + ) section = _TextSection( sub_width=sub_width, text=bui.Lstr( @@ -806,7 +801,9 @@ class InboxWindow(bui.MainWindow): sections.append(section) elif ctypeid is idcls.LINK: - assert isinstance(component, bcdlg.Link) + assert isinstance( + component, bacommon.bs.BasicCloudDialogComponentLink + ) def _do_open_url(url: str, sec: _ButtonSection) -> None: del sec # Unused. @@ -830,7 +827,7 @@ class InboxWindow(bui.MainWindow): elif ctypeid is idcls.DISPLAY_ITEMS: assert isinstance( component, - bcdlg.DisplayItems, + bacommon.bs.BasicCloudDialogDisplayItems, ) section = _DisplayItemsSection( sub_width=sub_width, @@ -847,7 +844,7 @@ class InboxWindow(bui.MainWindow): assert isinstance( component, - bcdlg.ClassicTourneyResult, + bacommon.bs.BasicCloudDialogBsClassicTourneyResult, ) campaignname, levelname = component.game.split(':') assert bui.app.classic is not None @@ -971,7 +968,9 @@ class InboxWindow(bui.MainWindow): sections.append(section) elif ctypeid is idcls.EXPIRE_TIME: - assert isinstance(component, bcdlg.ExpireTime) + assert isinstance( + component, bacommon.bs.BasicCloudDialogExpireTime + ) section = _ExpireTimeSection( sub_width=sub_width, time=component.time, @@ -992,9 +991,15 @@ class InboxWindow(bui.MainWindow): # Display anything with unknown components as an # 'upgrade your app to see this' message. color = (0.6, 0.6, 0.6) - interaction_style = bcdlg.InteractionStyle.UNKNOWN - button_label_positive = bcdlg.ButtonLabel.OK - button_label_negative = bcdlg.ButtonLabel.CANCEL + interaction_style = ( + bacommon.bs.BasicCloudDialog.InteractionStyle.UNKNOWN + ) + button_label_positive = ( + bacommon.bs.BasicCloudDialog.ButtonLabel.OK + ) + button_label_negative = ( + bacommon.bs.BasicCloudDialog.ButtonLabel.CANCEL + ) section = _TextSection( sub_width=sub_width, @@ -1042,7 +1047,9 @@ class InboxWindow(bui.MainWindow): if uiscale is bui.UIScale.SMALL: y -= 36 + # for i, _wrapper in enumerate(response.wrappers): for entry_display in self._entry_displays: + # entry_display = self._entry_displays[i] entry_display_weak = weakref.ref(entry_display) bwidth = 140 bheight = 40 @@ -1082,7 +1089,9 @@ class InboxWindow(bui.MainWindow): buttonrow: list[bui.Widget] = [] have_negative_button = ( entry_display.interaction_style - is bcdlg.InteractionStyle.BUTTON_POSITIVE_NEGATIVE + is ( + bacommon.bs.BasicCloudDialog + ).InteractionStyle.BUTTON_POSITIVE_NEGATIVE ) bpos = ( @@ -1107,10 +1116,10 @@ class InboxWindow(bui.MainWindow): ), color=entry_display.color, textcolor=(0, 1, 0), - on_activate_call=bui.WeakCallStrict( + on_activate_call=bui.WeakCall( self._on_entry_display_press, entry_display_weak, - cdlg.Action.BUTTON_PRESS_POSITIVE, + bacommon.bs.CloudDialogAction.BUTTON_PRESS_POSITIVE, ), enable_sound=False, ) @@ -1142,10 +1151,10 @@ class InboxWindow(bui.MainWindow): ), color=(0.85, 0.5, 0.7), textcolor=(1, 0.4, 0.4), - on_activate_call=bui.WeakCallStrict( + on_activate_call=bui.WeakCall( self._on_entry_display_press, entry_display_weak, - (cdlg.Action).BUTTON_PRESS_NEGATIVE, + (bacommon.bs.CloudDialogAction).BUTTON_PRESS_NEGATIVE, ), enable_sound=False, ) diff --git a/dist/ba_data/python/bauiv1lib/ingamemenu.py b/dist/ba_data/python/bauiv1lib/ingamemenu.py index c425ec0..f9fe988 100644 --- a/dist/ba_data/python/bauiv1lib/ingamemenu.py +++ b/dist/ba_data/python/bauiv1lib/ingamemenu.py @@ -177,9 +177,7 @@ class InGameMenuWindow(bui.MainWindow): # Keep updating in a timer in case it gets changed elsewhere. self._change_replay_speed_timer = bui.AppTimer( - 0.25, - bui.WeakCallStrict(self._change_replay_speed, 0), - repeat=True, + 0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True ) btn = bui.buttonwidget( parent=self._root_widget, @@ -192,7 +190,7 @@ class InGameMenuWindow(bui.MainWindow): size=(b_size, b_size), label='', autoselect=True, - on_activate_call=bui.CallStrict(self._change_replay_speed, -1), + on_activate_call=bui.Call(self._change_replay_speed, -1), ) bui.textwidget( parent=self._root_widget, @@ -215,7 +213,7 @@ class InGameMenuWindow(bui.MainWindow): size=(b_size, b_size), label='', autoselect=True, - on_activate_call=bui.CallStrict(self._change_replay_speed, 1), + on_activate_call=bui.Call(self._change_replay_speed, 1), ) bui.textwidget( parent=self._root_widget, @@ -242,7 +240,7 @@ class InGameMenuWindow(bui.MainWindow): else bui.SpecialChar.PAUSE_BUTTON ), autoselect=True, - on_activate_call=bui.CallStrict(self._pause_or_resume_replay), + on_activate_call=bui.Call(self._pause_or_resume_replay), ) btn = bui.buttonwidget( parent=self._root_widget, @@ -255,7 +253,7 @@ class InGameMenuWindow(bui.MainWindow): size=(b_size, b_size), label='', autoselect=True, - on_activate_call=bui.WeakCallStrict(self._rewind_replay), + on_activate_call=bui.WeakCall(self._rewind_replay), ) bui.textwidget( parent=self._root_widget, @@ -282,7 +280,7 @@ class InGameMenuWindow(bui.MainWindow): size=(b_size, b_size), label='', autoselect=True, - on_activate_call=bui.WeakCallStrict(self._forward_replay), + on_activate_call=bui.WeakCall(self._forward_replay), ) bui.textwidget( parent=self._root_widget, @@ -410,11 +408,9 @@ class InGameMenuWindow(bui.MainWindow): resume = bool(entry.get('resume_on_call', True)) if resume: - call = bui.CallStrict(self._resume_and_call, entry['call']) + call = bui.Call(self._resume_and_call, entry['call']) else: - call = bui.CallStrict( - entry['call'], bui.WeakCallStrict(self._resume) - ) + call = bui.Call(entry['call'], bui.WeakCall(self._resume)) bui.buttonwidget( parent=self._root_widget, diff --git a/dist/ba_data/python/bauiv1lib/inventory.py b/dist/ba_data/python/bauiv1lib/inventory.py index 5a9bcac..256f5ae 100644 --- a/dist/ba_data/python/bauiv1lib/inventory.py +++ b/dist/ba_data/python/bauiv1lib/inventory.py @@ -4,405 +4,151 @@ from __future__ import annotations -import random +from typing import override -from typing import override, TYPE_CHECKING - -from efro.util import asserttype -import bacommon.docui.v1 as dui1 import bauiv1 as bui -from bauiv1lib.docui import DocUIController -if TYPE_CHECKING: - from typing import Any +class InventoryWindow(bui.MainWindow): + """Shows what you got.""" - from bacommon.docui import DocUIRequest, DocUIResponse + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + auxiliary_style: bool = True, + ): - from bauiv1lib.docui import DocUILocalAction, DocUIWindow + uiscale = bui.app.ui_v1.uiscale + self._width = 1400 if uiscale is bui.UIScale.SMALL else 750 + self._height = ( + 1200 + if uiscale is bui.UIScale.SMALL + else 530 if uiscale is bui.UIScale.MEDIUM else 600 + ) + # Do some fancy math to fill all available screen area up to the + # size of our backing container. This lets us fit to the exact + # screen shape at small ui scale. + screensize = bui.get_virtual_screen_size() + scale = ( + 1.55 + if uiscale is bui.UIScale.SMALL + else 1.15 if uiscale is bui.UIScale.MEDIUM else 1.0 + ) -class InventoryUIController(DocUIController): - """DocUI setup for inventory.""" + # Calc screen size in our local container space and clamp to a + # bit smaller than our container size. + target_height = min(self._height - 100, screensize[1] / scale) - def __init__(self, player_profiles_only: bool = False) -> None: - self._next_selected_profile: str | None = None - self._player_profiles_only = player_profiles_only + # To get top/left coords, go to the center of our window and + # offset by half the width/height of our target area. + yoffs = 0.5 * self._height + 0.5 * target_height + 30.0 - @override - def fulfill_request(self, request: DocUIRequest) -> DocUIResponse: - - response: DocUIResponse - - # If we only want player profiles, we can skip the whole cloud - # request bit. - if self._player_profiles_only: - response = dui1.Response( - page=dui1.Page( - title='{"r":"inventoryText"}', - title_is_lstr=True, - rows=[], - ) - ) - else: - # *Most* of our inventory comes from the cloud - we just supply - # profiles ourself so it works offline. - response = self.fulfill_request_cloud(request, 'classicinventory') - - assert isinstance(request, dui1.Request) - assert isinstance(response, dui1.Response) - - if request.path != '/': - return response - - signed_in = ( - bui.app.plus is not None - and bui.app.plus.accounts.primary is not None - ) - - # If anything went wrong, replace the error page they sent us with - # a minimal 'most stuff is only available online' page. - inv_only_signin_t = '{"r":"inventoryOnlyAvailableSignedInText"}' - inv_only_online_t = '{"r":"inventoryOnlyAvailableOnlineText"}' - if response.status is not dui1.ResponseStatus.SUCCESS: - response = dui1.Response( - page=dui1.Page( - title='{"r":"inventoryText"}', - title_is_lstr=True, - rows=[ - dui1.ButtonRow( - center_content=True, - buttons=[ - dui1.Button( - ( - inv_only_signin_t - if not signed_in - else inv_only_online_t - ), - label_is_lstr=True, - texture='white', - size=(600, 100), - color=(1, 1, 1, 0.0), - label_scale=0.7, - label_color=(1, 0.4, 0.4, 0.8), - ) - ], - ), - ], - ), - ) - - for row in response.page.rows: - if ( - isinstance(row, dui1.ButtonRow) - and row.title - and '"r":"store.yourCharactersText"' in row.title - ): - for button in row.buttons: - if not button.decorations: - continue - for decoration in button.decorations: - if isinstance(decoration, dui1.Text): - button.action = dui1.Local( - immediate_local_action='spawn_bot', - immediate_local_action_args={ - 'name': decoration.text - }, - ) - break - - # Now add in our profiles, which we handle locally so it is - # available offline. - response.page.rows = [ - dui1.ButtonRow( - title='{"r":"playerProfilesWindow.titleText"}', - title_is_lstr=True, - subtitle='{"r":"playerProfilesWindow.explanationText"}', - subtitle_is_lstr=True, - button_spacing=15, - buttons=self._get_profile_buttons(), - ), - dui1.ButtonRow( - spacing_top=-15, - spacing_bottom=15, - padding_left=13, - buttons=[ - dui1.Button( - '{"r":"editProfileWindow.titleNewText"}', - dui1.Local( - default_sound=False, - immediate_local_action='new_profile', - ), - icon='plusButton', - icon_scale=1.3, - icon_color=(0.7, 0.6, 0.9, 1), - label_is_lstr=True, - style=dui1.ButtonStyle.MEDIUM, - size=(210, 60), - scale=0.8, - color=(0.6, 0.5, 0.8, 1.0), - label_color=(1, 1, 1, 1), - ), - ], - ), - ] + response.page.rows - - return response - - @override - def local_action(self, action: DocUILocalAction) -> None: - if action.name == 'new_profile': - self._new_profile(action) - elif action.name == 'edit_profile': - self._edit_profile(action) - elif action.name == 'spawn_bot': - self._spawn_bot(action) - else: - bui.screenmessage( - f'Invalid local-action "{action.name}".', color=(1, 0, 0) - ) - bui.getsound('error').play() - - @override - def restore_window_shared_state( - self, window: DocUIWindow, state: dict - ) -> None: - """Called when a window shared state is being restored.""" - - if not isinstance(window.request, dui1.Request): - return - - # If desired, set the profile button that will be selected in - # the new window. We do this when coming back from creating a - # new profile/etc. - if ( - window.request.path == '/' - and self._next_selected_profile is not None - ): - state['selection'] = f'$(WIN)|profile.{self._next_selected_profile}' - - # Only do this once (return to normal selection save/restore - # after). - self._next_selected_profile = None - - def _on_profile_save(self, name: str) -> None: - # An editor we launched tells us it saved a profile. - - # Have this one selected when we go back to the listing. - self._next_selected_profile = name - bui.pushcall(self._notify_profiles_changed) - - def _on_profile_delete(self, name: str) -> None: - # An editor we launched tells us it deleted a profile. - - # Ask the inventory list to select/show the profile right before - # the one we're deleting. - profiles = bui.app.config.get('Player Profiles', {}) - items = list(profiles.items()) - items.sort(key=lambda x: asserttype(x[0], str).lower()) - - namelower = name.lower() - - prevname = items[0][0] if items else None - for item in items: - if item[0].lower() < namelower: - prevname = item[0] - else: - break - - if prevname is not None: - self._next_selected_profile = prevname - - self._notify_profiles_changed() - - def _notify_profiles_changed(self) -> None: - import bascenev1 as bs - - # If there's a team-chooser in existence, tell it the profile-list - # has probably changed. - session = bs.get_foreground_host_session() - if session is not None: - session.handlemessage(bs.PlayerProfilesChangedMessage()) - - def _get_profile_buttons(self) -> list[dui1.Button]: - # pylint: disable=too-many-locals - - plus = bui.app.plus - assert plus is not None - classic = bui.app.classic - assert classic is not None - - buttons: list[dui1.Button] = [] - - profiles = bui.app.config.get('Player Profiles', {}) - items = list(profiles.items()) - items.sort(key=lambda x: asserttype(x[0], str).lower()) - - account_name: str | None - if plus.get_v1_account_state() == 'signed_in': - account_name = plus.get_v1_account_display_string() - else: - account_name = None - - spaz_appearances = classic.spaz_appearances - spaz_appearance_default = spaz_appearances['Spaz'] - - for p_name, p_info in items: - if p_name == '__account__' and account_name is None: - continue - color, highlight = classic.get_player_profile_colors(p_name) - tval = ( - account_name - if p_name == '__account__' - else classic.get_player_profile_icon(p_name) + p_name - ) - assert tval is not None - - tcolor: Any = bui.safecolor(color, 0.4) + (1.0,) - assert len(tcolor) == 4 - - appearance = spaz_appearances.get(p_info['character']) - if appearance is None: - appearance = spaz_appearance_default - - buttons.append( - dui1.Button( - texture='white', - size=(145, 175), - action=dui1.Local( - default_sound=False, - immediate_local_action='edit_profile', - immediate_local_action_args={'profile': p_name}, - ), - # color=(0.6, 0.5, 0.7, 1.0), - color=(1, 1, 1, 0.0), - widget_id=f'profile.{p_name}', - decorations=[ - dui1.Image( - appearance.icon_texture, - position=(0, 15), - size=(140, 140), - mask_texture='characterIconMask', - tint_texture=appearance.icon_mask_texture, - tint_color=color, - tint2_color=highlight, - ), - dui1.Text( - tval, - position=(0, -75), - size=(130, 40), - flatness=1.0, - shadow=1.0, - color=tcolor, - ), - ], - ) - ) - - return buttons - - def _new_profile(self, action: DocUILocalAction) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.profile.edit import EditProfileWindow - - bui.getsound('swish').play() - - plus = bui.app.plus - assert plus is not None - - # Clamp at 100 profiles (otherwise the server will and that's less - # elegant looking). - profiles = bui.app.config.get('Player Profiles', {}) - if len(profiles) > 100: - bui.screenmessage( - bui.Lstr( - translate=( - 'serverResponses', - 'Max number of profiles reached.', - ) + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height), + toolbar_visibility=( + 'menu_full' if uiscale is bui.UIScale.SMALL else 'menu_full' ), - color=(1, 0, 0), - ) - bui.getsound('error').play() - return - - action.window.main_window_replace( - lambda: EditProfileWindow( - existing_profile=None, - on_profile_save=bui.WeakCallPartial(self._on_profile_save), - on_profile_delete=bui.WeakCallPartial(self._on_profile_delete), - ) + toolbar_cancel_button_style=( + 'close' if auxiliary_style else 'back' + ), + scale=scale, + ), + transition=transition, + origin_widget=origin_widget, + # We're affected by screen size only at small ui-scale. + refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, ) - def _edit_profile(self, action: DocUILocalAction) -> None: + bui.textwidget( + parent=self._root_widget, + position=( + self._width * 0.5, + yoffs - (50 if uiscale is bui.UIScale.SMALL else 30), + ), + size=(0, 0), + text=bui.Lstr(resource='inventoryText'), + color=bui.app.ui_v1.title_color, + scale=0.9 if uiscale is bui.UIScale.SMALL else 1.0, + maxwidth=(130 if uiscale is bui.UIScale.SMALL else 200), + 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, + id=f'{self.main_window_id_prefix}|back', + scale=0.8, + position=(50, yoffs - 50), + size=(50, 50) if auxiliary_style else (60, 55), + extra_touch_border_scale=2.0, + button_type=None if auxiliary_style else 'backSmall', + on_activate_call=self.main_window_back, + autoselect=True, + label=bui.charstr( + bui.SpecialChar.CLOSE + if auxiliary_style + else bui.SpecialChar.BACK + ), + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + + if bool(False): + print('WOULD TEST NEW STUFF') + else: + button_width = 300 + self._player_profiles_button = btn = bui.buttonwidget( + parent=self._root_widget, + id=f'{self.main_window_id_prefix}|playerprofiles', + position=(self._width * 0.5 - button_width * 0.5, yoffs - 200), + autoselect=True, + size=(button_width, 60), + label=bui.Lstr(resource='playerProfilesWindow.titleText'), + color=(0.55, 0.5, 0.6), + icon=bui.gettexture('cuteSpaz'), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._player_profiles_press, + ) + # Select this by default. + bui.containerwidget(edit=self._root_widget, selected_child=btn) + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, yoffs - 250), + size=(0, 0), + text=bui.Lstr(resource='moreSoonText'), + scale=0.7, + maxwidth=self._width * 0.9, + h_align='center', + v_align='center', + ) + + def _player_profiles_press(self) -> None: # pylint: disable=cyclic-import - from bauiv1lib.profile.edit import EditProfileWindow + from bauiv1lib.profile.browser import ProfileBrowserWindow - bui.getsound('swish').play() - - profile = action.args.get('profile') - assert isinstance(profile, str) - - # Play a random sound from the character. - classic = bui.app.classic - if classic is not None: - profiles = bui.app.config.get('Player Profiles', {}) - p_info = profiles.get(profile) - if p_info: - char = p_info.get('character', 'Spaz') - appearance = classic.spaz_appearances.get(char) - if appearance: - sounds = ( - appearance.jump_sounds - + appearance.attack_sounds - + appearance.pickup_sounds - ) - if sounds: - bui.getsound(random.choice(sounds)).play() - - action.window.main_window_replace( - lambda: EditProfileWindow( - profile, - origin_widget=action.widget, - on_profile_save=bui.WeakCallPartial(self._on_profile_save), - on_profile_delete=bui.WeakCallPartial(self._on_profile_delete), + self.main_window_replace( + lambda: ProfileBrowserWindow( + origin_widget=self._player_profiles_button ) ) - def _spawn_bot(self, action: DocUILocalAction) -> None: - import bascenev1 as bs - from bascenev1lib.mainmenu import MainMenuActivity - from bascenev1lib.actor.spazbot import DemoSpazBotSet, DemoBot - from bascenev1lib.actor.spazappearance import get_appearances + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) - name = action.args.get('name') - assert isinstance(name, str) - - activity = bs.get_foreground_host_activity() - if not isinstance(activity, MainMenuActivity) or activity.map is None: - return - bounds = activity.map.get_def_bound_box('map_bounds') - if bounds is None: - return - i = 0 - while i < len(activity.bot_sets): - if activity.bot_sets[i].have_living_bots(): - i += 1 - else: - activity.bot_sets.pop(i) - for appearance in get_appearances(True): - if f'"{appearance}"' in name: - with activity.context: - bot_set = DemoSpazBotSet() - DemoBot.randomize_traits(appearance) - bot_set.spawn_bot( - DemoBot, - ( - (bounds[0] + bounds[3]) / 2 + random.uniform(-7, 7), - bounds[4] - 2, - (bounds[2] + bounds[5]) / 2 + random.uniform(-7, 7), - ), - 0, - ) - activity.bot_sets.append(bot_set) - break + @override + def main_window_should_preserve_selection(self) -> bool: + return True diff --git a/dist/ba_data/python/bauiv1lib/kiosk.py b/dist/ba_data/python/bauiv1lib/kiosk.py index bf3453f..64da3c5 100644 --- a/dist/ba_data/python/bauiv1lib/kiosk.py +++ b/dist/ba_data/python/bauiv1lib/kiosk.py @@ -114,7 +114,7 @@ class KioskWindow(bui.MainWindow): parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=bui.CallStrict(self._do_game, 'easy'), + on_activate_call=bui.Call(self._do_game, 'easy'), transition_delay=tdelay, position=(h - b_width * 0.5, b_v), label='', @@ -149,7 +149,7 @@ class KioskWindow(bui.MainWindow): parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=bui.CallStrict(self._do_game, 'medium'), + on_activate_call=bui.Call(self._do_game, 'medium'), position=(h - b_width * 0.5, b_v), label='', button_type='square', @@ -184,7 +184,7 @@ class KioskWindow(bui.MainWindow): parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=bui.CallStrict(self._do_game, 'hard'), + on_activate_call=bui.Call(self._do_game, 'hard'), transition_delay=tdelay, position=(h - b_width * 0.5, b_v), label='', @@ -239,7 +239,7 @@ class KioskWindow(bui.MainWindow): parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=bui.CallStrict(self._do_game, 'ctf'), + on_activate_call=bui.Call(self._do_game, 'ctf'), transition_delay=tdelay, position=(h - b_width * 0.5, b_v), label='', @@ -275,7 +275,7 @@ class KioskWindow(bui.MainWindow): parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=bui.CallStrict(self._do_game, 'hockey'), + on_activate_call=bui.Call(self._do_game, 'hockey'), position=(h - b_width * 0.5, b_v), label='', button_type='square', @@ -310,7 +310,7 @@ class KioskWindow(bui.MainWindow): parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=bui.CallStrict(self._do_game, 'epic'), + on_activate_call=bui.Call(self._do_game, 'epic'), transition_delay=tdelay, position=(h - b_width * 0.5, b_v), label='', @@ -361,7 +361,7 @@ class KioskWindow(bui.MainWindow): self._restore_state() self._update() self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) @override @@ -468,11 +468,9 @@ class KioskWindow(bui.MainWindow): appconfig['Free-for-All Playlist Selection'] = 'Just Epic Elim' bui.fade_screen( False, - endcall=bui.CallStrict( + endcall=bui.Call( bui.pushcall, - bui.CallStrict( - bs.new_host_session, bs.FreeForAllSession - ), + bui.Call(bs.new_host_session, bs.FreeForAllSession), ), ) else: @@ -509,9 +507,9 @@ class KioskWindow(bui.MainWindow): ) bui.fade_screen( False, - endcall=bui.CallStrict( + endcall=bui.Call( bui.pushcall, - bui.CallStrict(bs.new_host_session, bs.DualTeamSession), + bui.Call(bs.new_host_session, bs.DualTeamSession), ), ) bui.containerwidget(edit=self._root_widget, transition='out_left') diff --git a/dist/ba_data/python/bauiv1lib/league/presidency.py b/dist/ba_data/python/bauiv1lib/league/presidency.py deleted file mode 100644 index 3d35b76..0000000 --- a/dist/ba_data/python/bauiv1lib/league/presidency.py +++ /dev/null @@ -1,40 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Shiny new doc-ui based store.""" - -from __future__ import annotations - -from typing import override, TYPE_CHECKING - -from bauiv1lib.docui import DocUIController - -import bauiv1 as bui - -if TYPE_CHECKING: - from bacommon.docui import DocUIRequest, DocUIResponse - - from bauiv1lib.docui import DocUILocalAction - - -class LeaguePresidencyUIController(DocUIController): - """DocUI setup for store.""" - - @override - def fulfill_request(self, request: DocUIRequest) -> DocUIResponse: - return self.fulfill_request_cloud(request, 'classicleaguepresidency') - - @override - def local_action(self, action: DocUILocalAction) -> None: - if action.name == 'get_tokens': - self._get_tokens(action) - else: - bui.screenmessage( - f'Invalid local-action "{action.name}".', color=(1, 0, 0) - ) - bui.getsound('error').play() - - def _get_tokens(self, action: DocUILocalAction) -> None: - from bauiv1lib.gettokens import show_get_tokens_window - - bui.getsound('swish').play() - show_get_tokens_window(origin_widget=bui.existing(action.widget)) diff --git a/dist/ba_data/python/bauiv1lib/league/rankwindow.py b/dist/ba_data/python/bauiv1lib/league/rankwindow.py index f27bd4f..f145abb 100644 --- a/dist/ba_data/python/bauiv1lib/league/rankwindow.py +++ b/dist/ba_data/python/bauiv1lib/league/rankwindow.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """UI related to league rank.""" - # pylint: disable=too-many-lines from __future__ import annotations @@ -10,10 +9,9 @@ import copy import logging from typing import TYPE_CHECKING, override -import bacommon.classic -import bauiv1 as bui from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top from bauiv1lib.popup import PopupMenu +import bauiv1 as bui if TYPE_CHECKING: from typing import Any @@ -32,8 +30,6 @@ class LeagueRankWindow(bui.MainWindow): plus = bui.app.plus assert plus is not None - self._uiopenstate = bui.UIOpenState('classicleaguerank') - bui.set_analytics_screen('League Rank Window') self._league_rank_data: dict[str, Any] | None = None @@ -220,34 +216,10 @@ class LeagueRankWindow(bui.MainWindow): self._update_for_league_rank_data(info) self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) self._update(show=info is None) - def _on_p_button_info_response( - self, - response: ( - bacommon.classic.GetClassicLeaguePresidentButtonInfoResponse - | Exception - ), - ) -> None: - - # If our window has died, no-op. - if not self._root_widget: - return - - if isinstance(response, Exception) or response.name is None: - bui.textwidget( - edit=self._president_name, color=(0.6, 0.6, 1, 0.2), text='-' - ) - return - - bui.textwidget( - edit=self._president_name, - color=(0.6, 0.6, 1, 0.9), - text=response.name, - ) - @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. @@ -409,24 +381,8 @@ class LeagueRankWindow(bui.MainWindow): self._doing_power_ranking_query = True plus.power_ranking_query( season=self._requested_season, - callback=bui.WeakCallPartial( - self._on_power_ranking_query_response - ), + callback=bui.WeakCall(self._on_power_ranking_query_response), ) - # Also kick off a query to v2 for latest league-president - # info. - if plus.accounts.primary is not None: - with plus.accounts.primary: - plus.cloud.send_message_cb( - ( - bacommon.classic - ).GetClassicLeaguePresidentButtonInfoMessage( - season=self._requested_season - ), - on_response=bui.WeakCallPartial( - self._on_p_button_info_response - ), - ) def _refresh(self) -> None: # pylint: disable=too-many-statements @@ -457,7 +413,7 @@ class LeagueRankWindow(bui.MainWindow): v2 = v - 60 worth_color = (0.6, 0.6, 0.65) tally_color = (0.5, 0.6, 0.8) - spc = 23 + spc = 43 h_offs_tally = 150 tally_maxwidth = 120 @@ -484,7 +440,7 @@ class LeagueRankWindow(bui.MainWindow): size=(200, 80), icon=bui.gettexture('achievementsIcon'), autoselect=True, - on_activate_call=bui.WeakCallStrict(self._on_achievements_press), + on_activate_call=bui.WeakCall(self._on_achievements_press), up_widget=self._back_button, left_widget=self._back_button, color=(0.5, 0.5, 0.6), @@ -515,7 +471,7 @@ class LeagueRankWindow(bui.MainWindow): size=(200, 80), icon=bui.gettexture('medalSilver'), autoselect=True, - on_activate_call=bui.WeakCallStrict(self._on_trophies_press), + on_activate_call=bui.WeakCall(self._on_trophies_press), left_widget=self._back_button, color=(0.5, 0.5, 0.6), textcolor=(0.7, 0.7, 0.8), @@ -562,9 +518,7 @@ class LeagueRankWindow(bui.MainWindow): icon_color=(0.5, 0, 0.5), label=bui.Lstr(resource='coopSelectWindow.activityText'), autoselect=True, - on_activate_call=bui.WeakCallStrict( - self._on_activity_mult_press - ), + on_activate_call=bui.WeakCall(self._on_activity_mult_press), left_widget=self._back_button, color=(0.5, 0.5, 0.6), textcolor=(0.7, 0.7, 0.8), @@ -596,9 +550,7 @@ class LeagueRankWindow(bui.MainWindow): icon_color=(0.3, 0, 0.3), label=bui.Lstr(resource='league.upToDateBonusText'), autoselect=True, - on_activate_call=bui.WeakCallStrict( - self._on_up_to_date_bonus_press - ), + on_activate_call=bui.WeakCall(self._on_up_to_date_bonus_press), left_widget=self._back_button, color=(0.5, 0.5, 0.6), textcolor=(0.7, 0.7, 0.8), @@ -647,64 +599,6 @@ class LeagueRankWindow(bui.MainWindow): maxwidth=tally_maxwidth, ) - self._president_button = bui.buttonwidget( - parent=w_parent, - id=f'{self.main_window_id_prefix}|president', - label='', - position=(self._xoffs + h2 - 60, v2 - 100), - color=(0.7, 0.55, 0.9), - texture=bui.gettexture('buttonSquareWide'), - opacity=0.3, - size=(200, 80), - autoselect=True, - on_activate_call=bui.WeakCallStrict(self._on_president_press), - ) - self._president_label = bui.textwidget( - parent=w_parent, - text=bui.Lstr(resource='league.leaguePresidentText'), - flatness=1.0, - shadow=0.0, - color=(0.6, 0.6, 1, 0.7), - draw_controller=self._president_button, - scale=0.5, - h_align='center', - v_align='center', - maxwidth=140, - position=(self._xoffs + h2 - 60 + 100, v2 - 100 + 59), - size=(0, 0), - ) - self._president_name = bui.textwidget( - parent=w_parent, - text='-', - draw_controller=self._president_button, - color=(0.6, 0.6, 1, 0.2), - flatness=1.0, - shadow=0.0, - h_align='center', - v_align='center', - maxwidth=120, - position=(self._xoffs + h2 - 60 + 100, v2 - 100 + 34), - size=(0, 0), - ) - self._president_star1 = bui.imagewidget( - parent=w_parent, - draw_controller=self._president_button, - texture=bui.gettexture('star'), - color=(0.7, 0.55, 0.9), - opacity=0.2, - position=(self._xoffs + h2 - 60 + 5, v2 - 100 + 17), - size=(32, 32), - ) - self._president_star1 = bui.imagewidget( - parent=w_parent, - draw_controller=self._president_button, - texture=bui.gettexture('star'), - color=(0.7, 0.55, 0.9), - opacity=0.2, - position=(self._xoffs + h2 - 60 + 200 - 5 - 32, v2 - 100 + 17), - size=(32, 32), - ) - self._season_show_text = bui.textwidget( parent=w_parent, position=(self._xoffs + 390 - 15, v - 20), @@ -854,44 +748,7 @@ class LeagueRankWindow(bui.MainWindow): textcolor=(0.7, 0.7, 0.8), size=(230, 60), autoselect=True, - on_activate_call=bui.WeakCallStrict(self._on_more_press), - ) - - def _on_president_press(self) -> None: - import bacommon.docui.v1 as dui1 - - from bauiv1lib.league.presidency import LeaguePresidencyUIController - from bauiv1lib.connectivity import wait_for_connectivity - - # No-op if we're not in control. - if not self.main_window_has_control(): - return - - plus = bui.app.plus - assert plus is not None - - # We should be signed in at this point, but let's be sure. - if plus.accounts.primary is None: - bui.screenmessage( - bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) - ) - bui.getsound('error').play() - return - - # Wait for connectivity if need be, then bring up a cloud based - # doc-ui window for showing/futzing-with league president stuff. - wait_for_connectivity( - on_connected=lambda: self.main_window_replace( - bui.CallStrict( - LeaguePresidencyUIController().create_window, - dui1.Request('/', args={'season': self._season}), - origin_widget=self._president_button, - auxiliary_style=False, - ), - extra_type_id=( - LeaguePresidencyUIController - ).get_window_extra_type_id(), - ) + on_activate_call=bui.WeakCall(self._on_more_press), ) def _on_more_press(self) -> None: @@ -1036,9 +893,7 @@ class LeagueRankWindow(bui.MainWindow): width=150, button_size=(200, 50), choices=season_choices, - on_value_change_call=bui.WeakCallPartial( - self._on_season_change - ), + on_value_change_call=bui.WeakCall(self._on_season_change), choices_display=season_choices_display, current_choice=self._season, ) @@ -1287,7 +1142,7 @@ class LeagueRankWindow(bui.MainWindow): widget.delete() self._power_ranking_score_widgets = [] - scores: list = data['scores'] if data is not None else [] + scores = data['scores'] if data is not None else [] tally_color = (0.5, 0.6, 0.8) w_parent = self._subcontainer v2 = self._power_ranking_score_v @@ -1347,7 +1202,7 @@ class LeagueRankWindow(bui.MainWindow): self._power_ranking_score_widgets.append(txt) bui.textwidget( edit=txt, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._show_account_info, score[4], txt ), ) diff --git a/dist/ba_data/python/bauiv1lib/mainmenu.py b/dist/ba_data/python/bauiv1lib/mainmenu.py index d4b7af0..f0e1bb3 100644 --- a/dist/ba_data/python/bauiv1lib/mainmenu.py +++ b/dist/ba_data/python/bauiv1lib/mainmenu.py @@ -75,7 +75,9 @@ class MainMenuWindow(bui.MainWindow): create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget, + # id_prefix=id_prefix, ), + # restore_selection=True, ) @override @@ -89,12 +91,13 @@ class MainMenuWindow(bui.MainWindow): import bauiv1lib.getremote as _unused import bauiv1lib.confirm as _unused2 import bauiv1lib.account.settings as _unused5 - import bauiv1lib.credits as _unused6 - import bauiv1lib.help as _unused7 - import bauiv1lib.settings.allsettings as _unused8 - import bauiv1lib.gather as _unused9 - import bauiv1lib.watch as _unused10 - import bauiv1lib.play as _unused11 + import bauiv1lib.store.browser as _unused6 + import bauiv1lib.credits as _unused7 + import bauiv1lib.help as _unused8 + import bauiv1lib.settings.allsettings as _unused9 + import bauiv1lib.gather as _unused10 + import bauiv1lib.watch as _unused11 + import bauiv1lib.play as _unused12 def _show_remote_app_info_on_first_launch(self) -> None: app = bui.app @@ -253,7 +256,7 @@ class MainMenuWindow(bui.MainWindow): text=( f'{app.env.engine_version}' f' build {app.env.engine_build_number}.' - f' Copyright 2011-2026 Eric Froemling.' + f' Copyright 2011-2025 Eric Froemling.' ), h_align='center', v_align='center', diff --git a/dist/ba_data/python/bauiv1lib/party.py b/dist/ba_data/python/bauiv1lib/party.py index dbe1453..d1628f7 100644 --- a/dist/ba_data/python/bauiv1lib/party.py +++ b/dist/ba_data/python/bauiv1lib/party.py @@ -21,9 +21,11 @@ if TYPE_CHECKING: class PartyWindow(bui.Window): """Party list/chat window.""" - def __init__(self, origin: Sequence[float] = (0, 0)): + def __del__(self) -> None: + bui.set_party_window_open(False) - self._uiopenstate = bui.UIOpenState('classicparty') + def __init__(self, origin: Sequence[float] = (0, 0)): + bui.set_party_window_open(True) self._r = 'partyWindow' self._popup_type: str | None = None self._popup_party_member_client_id: int | None = None @@ -89,7 +91,7 @@ class PartyWindow(bui.Window): label='...', autoselect=True, button_type='square', - on_activate_call=bui.WeakCallStrict(self._on_menu_button_press), + on_activate_call=bui.WeakCall(self._on_menu_button_press), color=(0.55, 0.73, 0.25), iconscale=1.2, ) @@ -211,7 +213,7 @@ class PartyWindow(bui.Window): self._name_widgets: list[bui.Widget] = [] self._roster: list[dict[str, Any]] | None = None self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() @@ -232,7 +234,7 @@ class PartyWindow(bui.Window): maxwidth=self._scroll_width * 0.94, shadow=0.3, flatness=1.0, - on_activate_call=bui.CallStrict(self._copy_msg, msg), + on_activate_call=bui.Call(self._copy_msg, msg), selectable=True, ) @@ -426,7 +428,7 @@ class PartyWindow(bui.Window): # client_id is more readily available though). bui.textwidget( edit=widget, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._on_party_member_press, self._roster[index]['client_id'], is_host, diff --git a/dist/ba_data/python/bauiv1lib/partyqueue.py b/dist/ba_data/python/bauiv1lib/partyqueue.py index 398c272..202b138 100644 --- a/dist/ba_data/python/bauiv1lib/partyqueue.py +++ b/dist/ba_data/python/bauiv1lib/partyqueue.py @@ -69,7 +69,7 @@ class PartyQueueWindow(bui.Window): ) bui.buttonwidget( edit=self._body_image, - on_activate_call=bui.WeakCallStrict( + on_activate_call=bui.WeakCall( parent.on_account_press, account_id, self._body_image ), ) @@ -131,7 +131,7 @@ class PartyQueueWindow(bui.Window): widget.delete() bui.pushcall( - bui.CallStrict( + bui.Call( kill_widgets, [ self._body_image, @@ -318,7 +318,7 @@ class PartyQueueWindow(bui.Window): # Update at roughly 30fps. self._update_timer = bui.AppTimer( - 0.033, bui.WeakCallStrict(self.update), repeat=True + 0.033, bui.WeakCall(self.update), repeat=True ) self.update() @@ -591,7 +591,7 @@ class PartyQueueWindow(bui.Window): 't': self._boost_tickets, 'q': self._queue_id, }, - callback=bui.WeakCallPartial(self.on_update_response), + callback=bui.WeakCall(self.on_update_response), ) # Let's not run these immediately (since they may be rapid-fire, # just bucket them until the next tick). @@ -654,7 +654,7 @@ class PartyQueueWindow(bui.Window): self._last_transaction_time = current_time plus.add_v1_account_transaction( {'type': 'PARTY_QUEUE_QUERY', 'q': self._queue_id}, - callback=bui.WeakCallPartial(self.on_update_response), + callback=bui.WeakCall(self.on_update_response), ) plus.run_v1_account_transactions() diff --git a/dist/ba_data/python/bauiv1lib/play.py b/dist/ba_data/python/bauiv1lib/play.py index dd509e0..5ad3c49 100644 --- a/dist/ba_data/python/bauiv1lib/play.py +++ b/dist/ba_data/python/bauiv1lib/play.py @@ -603,7 +603,7 @@ class PlayWindow(bui.MainWindow): assert plus is not None if plus.get_v1_account_state() != 'signed_in': - show_sign_in_prompt(origin_widget=self._coop_button) + show_sign_in_prompt() return self.main_window_replace( diff --git a/dist/ba_data/python/bauiv1lib/playlist/addgame.py b/dist/ba_data/python/bauiv1lib/playlist/addgame.py index 199fab3..b2457a0 100644 --- a/dist/ba_data/python/bauiv1lib/playlist/addgame.py +++ b/dist/ba_data/python/bauiv1lib/playlist/addgame.py @@ -58,11 +58,7 @@ class PlaylistAddGameWindow(bui.MainWindow): root_widget=bui.containerwidget( size=(self._width, self._height), scale=scale, - toolbar_visibility=( - 'menu_minimal' - if uiscale is bui.UIScale.SMALL - else 'menu_full' - ), + toolbar_visibility='menu_minimal', ), transition=transition, origin_widget=origin_widget, @@ -256,9 +252,7 @@ class PlaylistAddGameWindow(bui.MainWindow): v_align='center', color=(0.8, 0.8, 0.8, 1.0), maxwidth=self._scroll_width * 0.8, - on_select_call=bui.CallStrict( - self._set_selected_game_type, gametype - ), + on_select_call=bui.Call(self._set_selected_game_type, gametype), always_highlight=True, selectable=True, on_activate_call=_doit, @@ -283,35 +277,25 @@ class PlaylistAddGameWindow(bui.MainWindow): ) def _on_get_more_games_press(self) -> None: - import bacommon.docui.v1 as dui1 - - from bauiv1lib.docui import DocUIWindow from bauiv1lib.account.signin import show_sign_in_prompt - from bauiv1lib.store import StoreUIController - from bauiv1lib.connectivity import wait_for_connectivity + from bauiv1lib.store.browser import StoreBrowserWindow + + # No-op if we're not in control. + if not self.main_window_has_control(): + return plus = bui.app.plus assert plus is not None - if plus.accounts.primary is None: + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return - # Playlist editing happens in the regular non-auxiliary window - # stack so we can just pop up the regular auxiliary-mode store - # and it'll do the right thing and take us back to our editing - # when we close it. - wait_for_connectivity( - on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate( - win_type=DocUIWindow, - win_create_call=bui.CallStrict( - StoreUIController().create_window, - dui1.Request('/'), - origin_widget=self._get_more_games_button, - uiopenstateid='classicstore', - ), - win_extra_type_id=( - StoreUIController.get_window_extra_type_id() - ), + self.main_window_replace( + lambda: StoreBrowserWindow( + show_tab=StoreBrowserWindow.TabID.MINIGAMES, + origin_widget=self._get_more_games_button, + minimal_toolbars=True, ) ) diff --git a/dist/ba_data/python/bauiv1lib/playlist/browser.py b/dist/ba_data/python/bauiv1lib/playlist/browser.py index d296867..77b30d5 100644 --- a/dist/ba_data/python/bauiv1lib/playlist/browser.py +++ b/dist/ba_data/python/bauiv1lib/playlist/browser.py @@ -190,7 +190,7 @@ class PlaylistBrowserWindow(bui.MainWindow): # refresh). self._update() self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) @override @@ -527,12 +527,10 @@ class PlaylistBrowserWindow(bui.MainWindow): ) bui.buttonwidget( edit=btn, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._on_playlist_press, btn, name ), - on_select_call=bui.CallStrict( - self._on_playlist_select, name - ), + on_select_call=bui.Call(self._on_playlist_select, name), ) # Top row biases things up more to show header above it. diff --git a/dist/ba_data/python/bauiv1lib/playlist/customizebrowser.py b/dist/ba_data/python/bauiv1lib/playlist/customizebrowser.py index 1ba27a7..a8b2476 100644 --- a/dist/ba_data/python/bauiv1lib/playlist/customizebrowser.py +++ b/dist/ba_data/python/bauiv1lib/playlist/customizebrowser.py @@ -16,6 +16,8 @@ if TYPE_CHECKING: import bascenev1 as bs +REQUIRE_PRO = False + class PlaylistCustomizeBrowserWindow(bui.MainWindow): """Window for viewing a playlist.""" @@ -273,7 +275,7 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow): # Keep our lock images up to date/etc. self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() @@ -308,8 +310,12 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow): cfg.commit() def _update(self) -> None: + assert bui.app.classic is not None + have = bui.app.classic.accounts.have_pro_options() for lock in self._lock_images: - bui.imagewidget(edit=lock, opacity=0.0) # No more pro req. + bui.imagewidget( + edit=lock, opacity=0.0 if (have or not REQUIRE_PRO) else 1.0 + ) def _select(self, name: str, index: int) -> None: self._selected_playlist_name = name @@ -359,8 +365,8 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow): else (0.85, 0.85, 0.85, 1) ), always_highlight=True, - on_select_call=bui.CallStrict(self._select, pname, index), - on_activate_call=bui.CallStrict(self._edit_button.activate), + on_select_call=bui.Call(self._select, pname, index), + on_activate_call=bui.Call(self._edit_button.activate), selectable=True, ) # We don't give these widgets ids because we handle @@ -431,11 +437,17 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow): def _new_playlist(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.playlist.editcontroller import PlaylistEditController + from bauiv1lib.purchase import PurchaseWindow # No-op if we're not in control. if not self.main_window_has_control(): return + assert bui.app.classic is not None + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + # Clamp at our max playlist number. if len(bui.app.config[self._config_name_full]) > self._max_playlists: bui.screenmessage( @@ -459,7 +471,12 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow): def _edit_playlist(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.playlist.editcontroller import PlaylistEditController + from bauiv1lib.purchase import PurchaseWindow + assert bui.app.classic is not None + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return if self._selected_playlist_name is None: return if self._selected_playlist_name == '__default__': @@ -513,9 +530,7 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow): share.SharePlaylistImportWindow( origin_widget=self._import_button, - on_success_callback=bui.WeakCallStrict( - self._on_playlist_import_success - ), + on_success_callback=bui.WeakCall(self._on_playlist_import_success), ) def _on_playlist_import_success(self) -> None: @@ -535,9 +550,17 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow): share.SharePlaylistResultsWindow(name, response) def _share_playlist(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.purchase import PurchaseWindow + plus = bui.app.plus assert plus is not None + assert bui.app.classic is not None + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + # Gotta be signed in for this to work. if plus.get_v1_account_state() != 'signed_in': bui.screenmessage( @@ -563,7 +586,7 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow): 'playlistType': self._pvars.config_name, 'playlistName': self._selected_playlist_name, }, - callback=bui.WeakCallPartial( + callback=bui.WeakCall( self._on_share_playlist_response, self._selected_playlist_name ), ) @@ -571,8 +594,15 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow): bui.screenmessage(bui.Lstr(resource='sharingText')) def _delete_playlist(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.confirm import ConfirmWindow + assert bui.app.classic is not None + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + if self._selected_playlist_name is None: return if self._selected_playlist_name == '__default__': @@ -601,9 +631,17 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow): ) def _duplicate_playlist(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=cyclic-import + from bauiv1lib.purchase import PurchaseWindow + plus = bui.app.plus assert plus is not None + assert bui.app.classic is not None + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return if self._selected_playlist_name is None: return plst: list[dict[str, Any]] | None diff --git a/dist/ba_data/python/bauiv1lib/playlist/edit.py b/dist/ba_data/python/bauiv1lib/playlist/edit.py index 7c8c95b..88735e4 100644 --- a/dist/ba_data/python/bauiv1lib/playlist/edit.py +++ b/dist/ba_data/python/bauiv1lib/playlist/edit.py @@ -49,10 +49,8 @@ class PlaylistEditWindow(bui.MainWindow): if uiscale is bui.UIScale.SMALL else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 ), - toolbar_visibility=( - 'menu_minimal_no_back' - if uiscale is bui.UIScale.SMALL - else 'menu_full' + stack_offset=( + (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0) ), ), transition=transition, @@ -158,7 +156,7 @@ class PlaylistEditWindow(bui.MainWindow): position=(h, v), size=(110, 61.0 * scl), on_activate_call=self._add, - on_select_call=bui.CallStrict(self._set_ui_selection, 'add_button'), + on_select_call=bui.Call(self._set_ui_selection, 'add_button'), autoselect=True, button_type='square', color=b_color, @@ -174,7 +172,7 @@ class PlaylistEditWindow(bui.MainWindow): position=(h, v), size=(110, 61.0 * scl), on_activate_call=self._edit, - on_select_call=bui.CallStrict(self._set_ui_selection, 'editButton'), + on_select_call=bui.Call(self._set_ui_selection, 'editButton'), autoselect=True, button_type='square', color=b_color, @@ -232,7 +230,7 @@ class PlaylistEditWindow(bui.MainWindow): parent=self._root_widget, position=(160 + x_inset, v - scroll_height), highlight=False, - on_select_call=bui.CallStrict(self._set_ui_selection, 'gameList'), + on_select_call=bui.Call(self._set_ui_selection, 'gameList'), size=(self._scroll_width, (scroll_height - 15)), border_opacity=0.4, ) @@ -417,7 +415,7 @@ class PlaylistEditWindow(bui.MainWindow): txtw = bui.textwidget( parent=self._columnwidget, size=(self._width - 80, 30), - on_select_call=bui.CallStrict(self._select, index), + on_select_call=bui.Call(self._select, index), always_highlight=True, color=(0.8, 0.8, 0.8, 1.0), padding=0, diff --git a/dist/ba_data/python/bauiv1lib/playlist/editgame.py b/dist/ba_data/python/bauiv1lib/playlist/editgame.py index 9fc6869..f83b2a4 100644 --- a/dist/ba_data/python/bauiv1lib/playlist/editgame.py +++ b/dist/ba_data/python/bauiv1lib/playlist/editgame.py @@ -132,10 +132,8 @@ class PlaylistEditGameWindow(bui.MainWindow): if uiscale is bui.UIScale.SMALL else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 ), - toolbar_visibility=( - 'menu_minimal_no_back' - if uiscale is bui.UIScale.SMALL - else 'menu_full' + stack_offset=( + (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0) ), ), transition=transition, @@ -155,7 +153,7 @@ class PlaylistEditGameWindow(bui.MainWindow): autoselect=True, scale=1.0 if is_add else 0.75, text_scale=1.3, - on_activate_call=bui.CallStrict(self._cancel), + on_activate_call=bui.Call(self._cancel), ) bui.containerwidget(edit=self._root_widget, cancel_button=btn) @@ -250,7 +248,7 @@ class PlaylistEditGameWindow(bui.MainWindow): parent=self._subcontainer, size=(140, 60), position=(h + 448, v - 72), - on_activate_call=bui.CallStrict(self._select_map), + on_activate_call=bui.Call(self._select_map), scale=0.7, label=bui.Lstr(resource='mapSelectText'), ) @@ -361,7 +359,7 @@ class PlaylistEditGameWindow(bui.MainWindow): size=(28, 28), label='<', autoselect=True, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._choice_inc, setting.name, txt, setting, -1 ), repeat=True, @@ -372,7 +370,7 @@ class PlaylistEditGameWindow(bui.MainWindow): size=(28, 28), label='>', autoselect=True, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._choice_inc, setting.name, txt, setting, 1 ), repeat=True, @@ -412,7 +410,7 @@ class PlaylistEditGameWindow(bui.MainWindow): size=(28, 28), label='-', autoselect=True, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._inc, txt, min_value, @@ -429,7 +427,7 @@ class PlaylistEditGameWindow(bui.MainWindow): size=(28, 28), label='+', autoselect=True, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._inc, txt, min_value, @@ -478,7 +476,7 @@ class PlaylistEditGameWindow(bui.MainWindow): autoselect=True, textcolor=(0.8, 0.8, 0.8), value=value, - on_value_change_call=bui.CallPartial( + on_value_change_call=bui.Call( self._check_value_change, setting.name, txt ), ) @@ -505,9 +503,7 @@ class PlaylistEditGameWindow(bui.MainWindow): 'Error wiring up game-settings-select widget column.' ) - bui.buttonwidget( - edit=add_button, on_activate_call=bui.CallStrict(self._add) - ) + bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add)) bui.containerwidget( edit=self._root_widget, selected_child=add_button, diff --git a/dist/ba_data/python/bauiv1lib/playlist/mapselect.py b/dist/ba_data/python/bauiv1lib/playlist/mapselect.py index 8895f6c..ad60a2a 100644 --- a/dist/ba_data/python/bauiv1lib/playlist/mapselect.py +++ b/dist/ba_data/python/bauiv1lib/playlist/mapselect.py @@ -67,10 +67,8 @@ class PlaylistMapSelectWindow(bui.MainWindow): if uiscale is bui.UIScale.SMALL else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 ), - toolbar_visibility=( - 'menu_minimal_no_back' - if uiscale is bui.UIScale.SMALL - else 'menu_full' + stack_offset=( + (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0) ), ), transition=transition, @@ -232,7 +230,7 @@ class PlaylistMapSelectWindow(bui.MainWindow): mesh_transparent=mesh_transparent, label='', color=(1, 1, 1), - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._select_with_delay, self._maps[index][0] ), position=pos, @@ -290,12 +288,8 @@ class PlaylistMapSelectWindow(bui.MainWindow): ) def _on_store_press(self) -> None: - import bacommon.docui.v1 as dui1 - - from bauiv1lib.docui import DocUIWindow - from bauiv1lib.connectivity import wait_for_connectivity - from bauiv1lib.store import StoreUIController from bauiv1lib.account.signin import show_sign_in_prompt + from bauiv1lib.store.browser import StoreBrowserWindow # No-op if we're not in control. if not self.main_window_has_control(): @@ -304,28 +298,17 @@ class PlaylistMapSelectWindow(bui.MainWindow): plus = bui.app.plus assert plus is not None - if plus.accounts.primary is None: + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._selected_get_more_maps = True - # Playlist editing happens in the regular non-auxiliary window - # stack so we can just pop up the regular auxiliary-mode store - # and it'll do the right thing and take us back to our editing - # when we close it. - wait_for_connectivity( - on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate( - win_type=DocUIWindow, - win_create_call=bui.CallStrict( - StoreUIController().create_window, - dui1.Request('/'), - origin_widget=self._get_more_maps_button, - uiopenstateid='classicstore', - ), - win_extra_type_id=( - StoreUIController.get_window_extra_type_id() - ), + self.main_window_replace( + lambda: StoreBrowserWindow( + show_tab=StoreBrowserWindow.TabID.MAPS, + origin_widget=self._get_more_maps_button, + minimal_toolbars=True, ) ) @@ -341,4 +324,4 @@ class PlaylistMapSelectWindow(bui.MainWindow): def _select_with_delay(self, map_name: str) -> None: bui.lock_all_input() bui.apptimer(0.1, bui.unlock_all_input) - bui.apptimer(0.1, bui.WeakCallStrict(self._select, map_name)) + bui.apptimer(0.1, bui.WeakCall(self._select, map_name)) diff --git a/dist/ba_data/python/bauiv1lib/playlist/share.py b/dist/ba_data/python/bauiv1lib/playlist/share.py index 03fc597..9e429cf 100644 --- a/dist/ba_data/python/bauiv1lib/playlist/share.py +++ b/dist/ba_data/python/bauiv1lib/playlist/share.py @@ -67,7 +67,7 @@ class SharePlaylistImportWindow(SendInfoWindowLegacyModal): 'expire_time': time.time() + 5, 'code': bui.textwidget(query=self._text_field), }, - callback=bui.WeakCallPartial(self._on_import_response), + callback=bui.WeakCall(self._on_import_response), ) plus.run_v1_account_transactions() bui.screenmessage(bui.Lstr(resource='importingText')) diff --git a/dist/ba_data/python/bauiv1lib/playoptions.py b/dist/ba_data/python/bauiv1lib/playoptions.py index d333c0d..70fcae0 100644 --- a/dist/ba_data/python/bauiv1lib/playoptions.py +++ b/dist/ba_data/python/bauiv1lib/playoptions.py @@ -7,7 +7,6 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, override -from bacommon.analytics import ClassicAnalyticsEvent import bascenev1 as bs import bauiv1 as bui @@ -18,6 +17,8 @@ if TYPE_CHECKING: from bauiv1lib.play import PlaylistSelectContext +REQUIRE_PRO = False + class PlayOptionsWindow(PopupWindow): """A popup window for configuring play options.""" @@ -239,7 +240,7 @@ class PlayOptionsWindow(PopupWindow): position=(h, v), texture=bui.gettexture(tex_name if owned else 'empty'), mesh_opaque=mesh_opaque if owned else None, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( bui.screenmessage, desc, desc_color ), label='', @@ -315,9 +316,7 @@ class PlayOptionsWindow(PopupWindow): parent=self.root_widget, position=(100, 195 + y_offs), size=(290, 35), - on_activate_call=bui.WeakCallStrict( - self._custom_colors_names_press - ), + on_activate_call=bui.WeakCall(self._custom_colors_names_press), autoselect=True, textcolor=(0.8, 0.8, 0.8), label=bui.Lstr(resource='teamNamesColorText'), @@ -326,6 +325,16 @@ class PlayOptionsWindow(PopupWindow): edit=self._custom_colors_names_button, allow_preserve_selection=False, ) + + assert bui.app.classic is not None + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro(): + bui.imagewidget( + parent=self.root_widget, + size=(30, 30), + position=(95, 202 + y_offs), + texture=bui.gettexture('lock'), + draw_controller=self._custom_colors_names_button, + ) else: self._custom_colors_names_button = None @@ -430,17 +439,26 @@ class PlayOptionsWindow(PopupWindow): # Update now and once per second. self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() def _custom_colors_names_press(self) -> None: + from bauiv1lib.account.signin import show_sign_in_prompt from bauiv1lib.teamnamescolors import TeamNamesColorsWindow + from bauiv1lib.purchase import PurchaseWindow plus = bui.app.plus assert plus is not None assert bui.app.classic is not None + if REQUIRE_PRO and not bui.app.classic.accounts.have_pro(): + if plus.get_v1_account_state() != 'signed_in': + show_sign_in_prompt() + else: + PurchaseWindow(items=['pro']) + self._transition_out() + return assert self._custom_colors_names_button TeamNamesColorsWindow( scale_origin=( @@ -526,21 +544,6 @@ class PlayOptionsWindow(PopupWindow): if bs.app.classic is not None: bs.app.classic.save_ui_state() - # Log analytics for when teams/ffa sessions are started from the - # UI. - if self._sessiontype is bs.FreeForAllSession: - bui.app.analytics.submit_event( - ClassicAnalyticsEvent( - ClassicAnalyticsEvent.EventType.START_FFA_SESSION - ) - ) - elif self._sessiontype is bs.DualTeamSession: - bui.app.analytics.submit_event( - ClassicAnalyticsEvent( - ClassicAnalyticsEvent.EventType.START_TEAMS_SESSION - ) - ) - try: bs.new_host_session(self._sessiontype) except Exception: diff --git a/dist/ba_data/python/bauiv1lib/popup.py b/dist/ba_data/python/bauiv1lib/popup.py index b1f5774..dfc87d5 100644 --- a/dist/ba_data/python/bauiv1lib/popup.py +++ b/dist/ba_data/python/bauiv1lib/popup.py @@ -234,7 +234,7 @@ class PopupMenuWindow(PopupWindow): wdg = bui.textwidget( parent=self._columnwidget, size=(self._width - 40, 28), - on_select_call=bui.CallStrict(self._select, index), + on_select_call=bui.Call(self._select, index), click_activate=True, color=( (0.5, 0.5, 0.5, 0.5) @@ -277,7 +277,7 @@ class PopupMenuWindow(PopupWindow): if delegate is not None: # Call this in a timer so it doesn't interfere with us killing # our widgets and whatnot. - call = bui.CallStrict( + call = bui.Call( delegate.popup_menu_selected_choice, self, self._current_choice ) bui.apptimer(0, call) diff --git a/dist/ba_data/python/bauiv1lib/profile/browser.py b/dist/ba_data/python/bauiv1lib/profile/browser.py new file mode 100644 index 0000000..18a49f7 --- /dev/null +++ b/dist/ba_data/python/bauiv1lib/profile/browser.py @@ -0,0 +1,476 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to browsing player profiles.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, override + +import bauiv1 as bui +import bascenev1 as bs + +if TYPE_CHECKING: + from typing import Any, ClassVar + + +class ProfileBrowserWindow(bui.MainWindow): + """Window for browsing player profiles.""" + + # Keep track of this at the class level to share between instances. + selected_profile: ClassVar[str | None] = None + + def __init__( + self, + transition: str | None = 'in_right', + selected_profile: str | None = None, + origin_widget: bui.Widget | None = None, + minimal_toolbar: bool = False, + ): + self._minimal_toolbar = minimal_toolbar + back_label = bui.Lstr(resource='backText') + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + self._width = 800.0 if uiscale is bui.UIScale.SMALL else 600.0 + x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 + self._height = ( + 360.0 + if uiscale is bui.UIScale.SMALL + else 385.0 if uiscale is bui.UIScale.MEDIUM else 410.0 + ) + + # Need to handle out-transitions ourself for modal mode. + if origin_widget is not None: + self._transition_out = 'out_scale' + else: + self._transition_out = 'out_right' + + self._r = 'playerProfilesWindow' + + # Ensure we've got an account-profile in cases where we're signed in. + assert bui.app.classic is not None + bui.app.classic.accounts.ensure_have_account_player_profile() + + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 + + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height + top_extra), + toolbar_visibility=( + 'menu_minimal' + if (uiscale is bui.UIScale.SMALL or minimal_toolbar) + else 'menu_full' + ), + scale=( + 2.5 + if uiscale is bui.UIScale.SMALL + else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 + ), + stack_offset=( + (0, -14) if uiscale is bui.UIScale.SMALL else (0, 0) + ), + ), + transition=transition, + origin_widget=origin_widget, + ) + + if bui.app.ui_v1.uiscale is bui.UIScale.SMALL: + self._back_button = bui.get_special_widget('back_button') + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + else: + self._back_button = btn = bui.buttonwidget( + parent=self._root_widget, + id=f'{self.main_window_id_prefix}|back', + position=(40 + x_inset, self._height - 59), + size=(120, 60), + scale=0.8, + label=back_label, + button_type='back', + autoselect=True, + on_activate_call=self.main_window_back, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.buttonwidget( + edit=btn, + button_type='backSmall', + size=(60, 60), + label=bui.charstr(bui.SpecialChar.BACK), + ) + + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 36), + size=(0, 0), + text=bui.Lstr(resource=f'{self._r}.titleText'), + maxwidth=300, + color=bui.app.ui_v1.title_color, + scale=0.9, + h_align='center', + v_align='center', + ) + + scroll_height = self._height - 140.0 + self._scroll_width = self._width - (188 + x_inset * 2) + v = self._height - 84.0 + h = 50 + x_inset + b_color = (0.6, 0.53, 0.63) + + scl = ( + 1.055 + if uiscale is bui.UIScale.SMALL + else 1.18 if uiscale is bui.UIScale.MEDIUM else 1.3 + ) + v -= 70.0 * scl + self._new_button = bui.buttonwidget( + parent=self._root_widget, + id=f'{self.main_window_id_prefix}|new', + position=(h, v), + size=(80, 66.0 * scl), + on_activate_call=self._new_profile, + color=b_color, + button_type='square', + autoselect=True, + textcolor=(0.75, 0.7, 0.8), + text_scale=0.7, + label=bui.Lstr(resource=f'{self._r}.newButtonText'), + ) + v -= 70.0 * scl + self._edit_button = bui.buttonwidget( + parent=self._root_widget, + id=f'{self.main_window_id_prefix}|edit', + position=(h, v), + size=(80, 66.0 * scl), + on_activate_call=self._edit_profile, + color=b_color, + button_type='square', + autoselect=True, + textcolor=(0.75, 0.7, 0.8), + text_scale=0.7, + label=bui.Lstr(resource=f'{self._r}.editButtonText'), + ) + v -= 70.0 * scl + self._delete_button = bui.buttonwidget( + parent=self._root_widget, + id=f'{self.main_window_id_prefix}|delete', + position=(h, v), + size=(80, 66.0 * scl), + on_activate_call=self._delete_profile, + color=b_color, + button_type='square', + autoselect=True, + textcolor=(0.75, 0.7, 0.8), + text_scale=0.7, + label=bui.Lstr(resource=f'{self._r}.deleteButtonText'), + ) + + v = self._height - 87 + + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 71), + size=(0, 0), + text=bui.Lstr(resource=f'{self._r}.explanationText'), + color=bui.app.ui_v1.infotextcolor, + maxwidth=self._width * 0.83, + scale=0.6, + h_align='center', + v_align='center', + ) + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + highlight=False, + position=(140 + x_inset, v - scroll_height), + size=(self._scroll_width, scroll_height), + ) + bui.widget( + edit=self._scrollwidget, + autoselect=True, + left_widget=self._new_button, + ) + bui.containerwidget( + edit=self._root_widget, selected_child=self._scrollwidget + ) + self._subcontainer = bui.containerwidget( + parent=self._scrollwidget, + size=(self._scroll_width, 32), + background=False, + ) + v -= 255 + self._profiles: dict[str, dict[str, Any]] | None = None + if selected_profile is not None: + type(self).selected_profile = selected_profile + self._profile_widgets: list[bui.Widget] = [] + self._refresh() + + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + + minimal_toolbar = self._minimal_toolbar + + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, + origin_widget=origin_widget, + minimal_toolbar=minimal_toolbar, + ) + ) + + @override + def main_window_should_preserve_selection(self) -> bool: + return True + + # @override + # def main_window_do_save_shared_state(self, state: dict) -> None: + # state['selected_profile'] = self._selected_profile + + # @override + # def main_window_do_restore_shared_state(self, state: dict) -> None: + # pval = state.get('selected_profile') + # if isinstance(pval, str | None): + # print('RESTORING', pval) + # self._selected_profile = pval + + def _new_profile(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.profile.edit import EditProfileWindow + from bauiv1lib.purchase import PurchaseWindow + + # No-op if we're not the in-control main window. + if not self.main_window_has_control(): + return + + plus = bui.app.plus + assert plus is not None + + # Limit to a handful profiles if they don't have pro-options. + max_non_pro_profiles = plus.get_v1_account_misc_read_val('mnpp', 5) + assert self._profiles is not None + assert bui.app.classic is not None + if ( + bool(False) # Phasing out pro. + and not bui.app.classic.accounts.have_pro_options() + and len(self._profiles) >= max_non_pro_profiles + ): + PurchaseWindow( + items=['pro'], + header_text=bui.Lstr( + resource='unlockThisProfilesText', + subs=[('${NUM}', str(max_non_pro_profiles))], + ), + ) + return + + # Clamp at 100 profiles (otherwise the server will and that's less + # elegant looking). + if len(self._profiles) > 100: + bui.screenmessage( + bui.Lstr( + translate=( + 'serverResponses', + 'Max number of profiles reached.', + ) + ), + color=(1, 0, 0), + ) + bui.getsound('error').play() + return + + self.main_window_replace( + lambda: EditProfileWindow(existing_profile=None) + ) + + def _delete_profile(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib import confirm + + if self.selected_profile is None: + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) + ) + return + if self.selected_profile == '__account__': + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource=f'{self._r}.cantDeleteAccountProfileText'), + color=(1, 0, 0), + ) + return + confirm.ConfirmWindow( + bui.Lstr( + resource=f'{self._r}.deleteConfirmText', + subs=[('${PROFILE}', self.selected_profile)], + ), + self._do_delete_profile, + width=350, + ) + + def _do_delete_profile(self) -> None: + plus = bui.app.plus + assert plus is not None + + # Go back to default selection. + type(self).selected_profile = None + + plus.add_v1_account_transaction( + {'type': 'REMOVE_PLAYER_PROFILE', 'name': self.selected_profile} + ) + plus.run_v1_account_transactions() + bui.getsound('shieldDown').play() + self._refresh() + + # Select profile list. + bui.containerwidget( + edit=self._root_widget, selected_child=self._scrollwidget + ) + + def _edit_profile(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.profile.edit import EditProfileWindow + + # No-op if we're not in control. + if not self.main_window_has_control(): + return + + if self.selected_profile is None: + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) + ) + return + + self.main_window_replace( + lambda: EditProfileWindow(self.selected_profile) + ) + + def _select(self, name: str, index: int) -> None: + del index # Unused. + type(self).selected_profile = name + + def _refresh(self) -> None: + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + from efro.util import asserttype + from bascenev1 import PlayerProfilesChangedMessage + from bascenev1lib.actor import spazappearance + + assert bui.app.classic is not None + + plus = bui.app.plus + assert plus is not None + + old_selection = self.selected_profile + + # Delete old. + while self._profile_widgets: + self._profile_widgets.pop().delete() + self._profiles = bui.app.config.get('Player Profiles', {}) + assert self._profiles is not None + items = list(self._profiles.items()) + items.sort(key=lambda x: asserttype(x[0], str).lower()) + spazzes = spazappearance.get_appearances() + spazzes.sort() + icon_textures = [ + bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture) + for s in spazzes + ] + icon_tint_textures = [ + bui.gettexture( + bui.app.classic.spaz_appearances[s].icon_mask_texture + ) + for s in spazzes + ] + index = 0 + y_val = 35 * (len(self._profiles) - 1) + account_name: str | None + if plus.get_v1_account_state() == 'signed_in': + account_name = plus.get_v1_account_display_string() + else: + account_name = None + widget_to_select = None + for p_name, p_info in items: + if p_name == '__account__' and account_name is None: + continue + color, _highlight = bui.app.classic.get_player_profile_colors( + p_name + ) + scl = 1.1 + tval = ( + account_name + if p_name == '__account__' + else bui.app.classic.get_player_profile_icon(p_name) + p_name + ) + + try: + char_index = spazzes.index(p_info['character']) + except Exception: + char_index = spazzes.index('Spaz') + + assert isinstance(tval, str) + txtw = bui.textwidget( + parent=self._subcontainer, + id=f'{self.main_window_id_prefix}|profile{index}', + position=(5, y_val), + size=((self._width - 210) / scl, 28), + text=bui.Lstr(value=f' {tval}'), + h_align='left', + v_align='center', + on_select_call=bui.WeakCall(self._select, p_name, index), + maxwidth=self._scroll_width * 0.86, + corner_scale=scl, + color=bui.safecolor(color, 0.4), + always_highlight=True, + on_activate_call=bui.Call(self._edit_button.activate), + selectable=True, + ) + # We handle reselection of these manually; no need for ids. + bui.widget(edit=txtw, allow_preserve_selection=False) + + character = bui.imagewidget( + parent=self._subcontainer, + position=(0, y_val), + size=(30, 30), + color=(1, 1, 1), + mask_texture=bui.gettexture('characterIconMask'), + tint_color=color, + tint2_color=_highlight, + texture=icon_textures[char_index], + tint_texture=icon_tint_textures[char_index], + ) + if index == 0: + bui.widget(edit=txtw, up_widget=self._back_button) + if self.selected_profile is None: + type(self).selected_profile = p_name + bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) + self._profile_widgets.append(txtw) + self._profile_widgets.append(character) + + # Select/show this one if it was previously selected. + # (but defer till after this loop since our height is + # still changing). + if p_name == old_selection or widget_to_select is None: + widget_to_select = txtw + + index += 1 + y_val -= 35 + + bui.containerwidget( + edit=self._subcontainer, + size=(self._scroll_width, index * 35), + ) + if widget_to_select is not None: + bui.containerwidget( + edit=self._subcontainer, + selected_child=widget_to_select, + visible_child=widget_to_select, + ) + + # If there's a team-chooser in existence, tell it the profile-list + # has probably changed. + session = bs.get_foreground_host_session() + if session is not None: + session.handlemessage(PlayerProfilesChangedMessage()) diff --git a/dist/ba_data/python/bauiv1lib/profile/edit.py b/dist/ba_data/python/bauiv1lib/profile/edit.py index 2f652d1..275e269 100644 --- a/dist/ba_data/python/bauiv1lib/profile/edit.py +++ b/dist/ba_data/python/bauiv1lib/profile/edit.py @@ -1,43 +1,47 @@ # Released under the MIT License. See LICENSE for details. # -# pylint: disable=too-many-lines """Provides UI to edit a player profile.""" from __future__ import annotations import random -from typing import cast, override, TYPE_CHECKING +from typing import cast, override from bauiv1lib.colorpicker import ColorPicker from bauiv1lib.characterpicker import CharacterPickerDelegate from bauiv1lib.iconpicker import IconPickerDelegate -from bauiv1lib.connectivity import wait_for_connectivity import bauiv1 as bui import bascenev1 as bs -if TYPE_CHECKING: - from typing import Callable - class EditProfileWindow( bui.MainWindow, CharacterPickerDelegate, IconPickerDelegate ): """Window for editing a player profile.""" + def reload_window(self) -> None: + """Transitions out and recreates ourself.""" + + # Replace ourself with ourself, but keep the same back location. + assert self.main_window_back_state is not None + self.main_window_replace( + lambda: EditProfileWindow(self.getname()), + back_state=self.main_window_back_state, + ) + def __init__( self, existing_profile: str | None, - *, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, - on_profile_save: Callable[[str], None] | None = None, - on_profile_delete: Callable[[str], None] | None = None, ): + # FIXME: Tidy this up a bit. # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-locals assert bui.app.classic is not None + # print(f'EditProfileWindow({id(self)})') plus = bui.app.plus assert plus is not None @@ -47,8 +51,6 @@ class EditProfileWindow( self._spazzes: list[str] = [] self._icon_textures: list[bui.Texture] = [] self._icon_tint_textures: list[bui.Texture] = [] - self._on_profile_save = on_profile_save - self._on_profile_delete = on_profile_delete # Grab profile colors or pick random ones. ( @@ -56,17 +58,17 @@ class EditProfileWindow( self._highlight, ) = bui.app.classic.get_player_profile_colors(existing_profile) uiscale = bui.app.ui_v1.uiscale - self._width = width = 1000.0 if uiscale is bui.UIScale.SMALL else 680.0 - self._x_inset = x_inset = 140.0 if uiscale is bui.UIScale.SMALL else 0.0 + self._width = width = 880.0 if uiscale is bui.UIScale.SMALL else 680.0 + self._x_inset = x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 self._height = height = ( 500.0 if uiscale is bui.UIScale.SMALL - else 450.0 if uiscale is bui.UIScale.MEDIUM else 450.0 + else 400.0 if uiscale is bui.UIScale.MEDIUM else 450.0 ) - yoffs = 2 if uiscale is bui.UIScale.SMALL else 0 + yoffs = -42 if uiscale is bui.UIScale.SMALL else 0 spacing = 40 self._base_scale = ( - 1.72 + 2.0 if uiscale is bui.UIScale.SMALL else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 ) @@ -99,7 +101,6 @@ class EditProfileWindow( scale=0.8, label=bui.Lstr(resource='saveText'), ) - bui.widget(edit=save_button, left_widget=cancel_button) bui.widget(edit=cancel_button, right_widget=save_button) bui.containerwidget(edit=self._root_widget, start_button=btn) @@ -346,7 +347,7 @@ class EditProfileWindow( editable=True, padding=4, color=(0.9, 0.9, 0.9, 1.0), - on_return_press_call=bui.CallStrict(save_button.activate), + on_return_press_call=bui.Call(save_button.activate), ) # FIXME hard coded strings are bad @@ -403,7 +404,7 @@ class EditProfileWindow( self._update_clipped_name() self._clipped_name_timer = bui.AppTimer( - 0.333, bui.WeakCallStrict(self._update_clipped_name), repeat=True + 0.333, bui.WeakCall(self._update_clipped_name), repeat=True ) v -= spacing * 3.0 @@ -422,9 +423,7 @@ class EditProfileWindow( origin = self._color_button.get_screen_space_center() bui.buttonwidget( edit=self._color_button, - on_activate_call=bui.WeakCallStrict( - self._make_picker, 'color', origin - ), + on_activate_call=bui.WeakCall(self._make_picker, 'color', origin), ) bui.textwidget( parent=self._root_widget, @@ -494,7 +493,7 @@ class EditProfileWindow( origin = self._highlight_button.get_screen_space_center() bui.buttonwidget( edit=self._highlight_button, - on_activate_call=bui.WeakCallStrict( + on_activate_call=bui.WeakCall( self._make_picker, 'highlight', origin ), ) @@ -510,102 +509,8 @@ class EditProfileWindow( color=bui.app.ui_v1.title_color, maxwidth=120, ) - - if existing_profile is not None: - bui.buttonwidget( - parent=self._root_widget, - position=(self._width * 0.5 - 43, v - 185), - size=(140, 60), - autoselect=True, - scale=0.6, - color=( - (0.5, 0.5, 0.5) - if self._is_account_profile - else (0.65, 0.45, 0.5) - ), - textcolor=( - (0.5, 0.5, 0.5) - if self._is_account_profile - else (1.0, 0.5, 0.5) - ), - label=bui.Lstr(resource='deleteText'), - on_activate_call=bui.WeakCallStrict(self._delete_press), - enable_sound=not self._is_account_profile, - ) - self._update_character() - def reload_window(self) -> None: - """Transitions out and recreates ourself.""" - - # Replace ourself with ourself, but keep the same back location. - assert self.main_window_back_state is not None - self.main_window_replace( - lambda: EditProfileWindow(self.getname()), - back_state=self.main_window_back_state, - ) - - def _delete_press(self) -> None: - # pylint: disable=cyclic-import - from bauiv1lib.confirm import ConfirmWindow - - if self._is_account_profile: - bui.getsound('error').play() - bui.screenmessage( - bui.Lstr( - resource='playerProfilesWindow.cantDeleteAccountProfileText' - ), - color=(1, 0, 0), - ) - return - - if self._existing_profile is None: - bui.getsound('error').play() - bui.screenmessage( - bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) - ) - return - ConfirmWindow( - bui.Lstr( - resource='playerProfilesWindow.deleteConfirmText', - subs=[('${PROFILE}', self._existing_profile)], - ), - self._do_delete_profile, - width=350, - ) - - def _do_delete_profile(self) -> None: - plus = bui.app.plus - assert plus is not None - - assert self._existing_profile is not None - - # Play a death sound of the character. - classic = bui.app.classic - if classic is not None: - profiles = bui.app.config.get('Player Profiles', {}) - p_info = profiles.get(self._existing_profile) - if p_info: - char = p_info.get('character', 'Spaz') - appearance = classic.spaz_appearances.get(char) - if appearance: - bui.getsound(random.choice(appearance.death_sounds)).play() - - plus.add_v1_account_transaction( - {'type': 'REMOVE_PLAYER_PROFILE', 'name': self._existing_profile} - ) - - plus.run_v1_account_transactions() - bui.getsound('shieldDown').play() - - if self._on_profile_delete is not None: - try: - self._on_profile_delete(self._existing_profile) - except Exception: - bui.balog.exception('Error in _on_profile_delete cb.') - - self.main_window_back() - @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. @@ -613,8 +518,6 @@ class EditProfileWindow( # Pull things out of self here; if we do it within the lambda # we'll keep ourself alive which is bad. - on_profile_save = self._on_profile_save - on_profile_delete = self._on_profile_delete existing_profile = self._existing_profile return bui.BasicMainWindowState( @@ -622,8 +525,6 @@ class EditProfileWindow( transition=transition, origin_widget=origin_widget, existing_profile=existing_profile, - on_profile_save=on_profile_save, - on_profile_delete=on_profile_delete, ) ) @@ -747,30 +648,12 @@ class EditProfileWindow( @override def on_icon_picker_get_more_press(self) -> None: """User wants to get more icons.""" - import bacommon.docui.v1 as dui1 + from bauiv1lib.store.browser import StoreBrowserWindow - from bauiv1lib.store import StoreUIController - - if not self._ensure_signed_in( - origin_widget=bui.get_special_widget('store_button') - ): - return - - # Because profile editing is happening within an auxiliary - # window stack, we need to bring up the store as a regular - # non-auxiliary window pushed onto our stack. If we did the - # simple thing and triggered it as an auxiliary window then it - # would *replace* our stack and we wouldn't be able to get back - # to our editing. - wait_for_connectivity( - on_connected=lambda: self.main_window_replace( - bui.CallStrict( - StoreUIController().create_window, - dui1.Request('/'), - origin_widget=bui.get_special_widget('store_button'), - auxiliary_style=False, - ), - extra_type_id=StoreUIController.get_window_extra_type_id(), + self.main_window_replace( + lambda: StoreBrowserWindow( + minimal_toolbars=True, + show_tab=StoreBrowserWindow.TabID.ICONS, ) ) @@ -788,46 +671,14 @@ class EditProfileWindow( ) self._update_character() - def _ensure_signed_in( - self, *, origin_widget: bui.Widget | None = None - ) -> bool: - """Make sure we're signed in (requiring modern v2 accounts).""" - from bauiv1lib.account.signin import show_sign_in_prompt - - plus = bui.app.plus - if plus is None: - bui.screenmessage('This requires plus.', color=(1, 0, 0)) - bui.getsound('error').play() - return False - if plus.accounts.primary is None: - show_sign_in_prompt(origin_widget=origin_widget) - return False - return True - @override def on_character_picker_get_more_press(self) -> None: - import bacommon.docui.v1 as dui1 + from bauiv1lib.store.browser import StoreBrowserWindow - from bauiv1lib.store import StoreUIController - - if not self._ensure_signed_in( - origin_widget=bui.get_special_widget('store_button') - ): - return - - # Set this up as a non-auxiliary window so we can nav back to - # char editing (otherwise it would replace the whole inventory - # stack). Also don't set uiopenstateid in this case since we don't - # want store button to glow (since inventory button already is). - wait_for_connectivity( - on_connected=lambda: self.main_window_replace( - bui.CallStrict( - StoreUIController().create_window, - dui1.Request('/'), - origin_widget=bui.get_special_widget('store_button'), - auxiliary_style=False, - ), - extra_type_id=StoreUIController.get_window_extra_type_id(), + self.main_window_replace( + lambda: StoreBrowserWindow( + minimal_toolbars=True, + show_tab=StoreBrowserWindow.TabID.CHARACTERS, ) ) @@ -973,6 +824,9 @@ class EditProfileWindow( def save(self, transition_out: bool = True) -> bool: """Save has been selected.""" + # pylint: disable=cyclic-import + + from bauiv1lib.profile.browser import ProfileBrowserWindow # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: @@ -997,11 +851,8 @@ class EditProfileWindow( bui.getsound('error').play() return False - if self._on_profile_save is not None: - try: - self._on_profile_save(new_name) - except Exception: - bui.balog.exception('Error in _on_profile_save cb.') + # Set the profile-browser to have this one selected by default. + ProfileBrowserWindow.selected_profile = new_name if transition_out: bui.getsound('gunCocking').play() diff --git a/dist/ba_data/python/bauiv1lib/profile/upgrade.py b/dist/ba_data/python/bauiv1lib/profile/upgrade.py index 06a2900..0866f68 100644 --- a/dist/ba_data/python/bauiv1lib/profile/upgrade.py +++ b/dist/ba_data/python/bauiv1lib/profile/upgrade.py @@ -8,7 +8,7 @@ import time import weakref from typing import TYPE_CHECKING -import bacommon.classic +import bacommon.bs import bauiv1 as bui @@ -142,20 +142,20 @@ class ProfileUpgradeWindow(bui.Window): assert plus.accounts.primary is not None with plus.accounts.primary: plus.cloud.send_message_cb( - bacommon.classic.GlobalProfileCheckMessage(self._name), - on_response=bui.WeakCallPartial( + bacommon.bs.GlobalProfileCheckMessage(self._name), + on_response=bui.WeakCall( self._on_global_profile_check_response ), ) self._status: str | None = 'waiting' self._update_timer = bui.AppTimer( - 1.023, bui.WeakCallStrict(self._update), repeat=True + 1.023, bui.WeakCall(self._update), repeat=True ) self._update() def _on_global_profile_check_response( - self, response: bacommon.classic.GlobalProfileCheckResponse | Exception + self, response: bacommon.bs.GlobalProfileCheckResponse | Exception ) -> None: if isinstance(response, Exception): bui.textwidget( diff --git a/dist/ba_data/python/bauiv1lib/purchase.py b/dist/ba_data/python/bauiv1lib/purchase.py new file mode 100644 index 0000000..911cd2c --- /dev/null +++ b/dist/ba_data/python/bauiv1lib/purchase.py @@ -0,0 +1,209 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI related to purchasing items.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import bauiv1 as bui + +if TYPE_CHECKING: + from typing import Any + + +class PurchaseWindow(bui.Window): + """Window for purchasing one or more items.""" + + def __init__( + self, + items: list[str], + origin_widget: bui.Widget | None = None, + header_text: bui.Lstr | None = None, + ): + from bauiv1lib.store.item import instantiate_store_item_display + + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + store = bui.app.classic.store + + if header_text is None: + header_text = bui.Lstr( + resource='unlockThisText', + fallback_resource='unlockThisInTheStoreText', + ) + if len(items) != 1: + raise ValueError('expected exactly 1 item') + + self._idprefix = bui.app.ui_v1.new_id_prefix('purchase') + self._items = list(items) + self._width = 580 + self._height = 520 + uiscale = bui.app.ui_v1.uiscale + + if origin_widget is not None: + scale_origin = origin_widget.get_screen_space_center() + else: + scale_origin = None + + super().__init__( + root_widget=bui.containerwidget( + parent=bui.get_special_widget('overlay_stack'), + size=(self._width, self._height), + transition='in_scale', + toolbar_visibility='menu_store', + scale=( + 1.2 + if uiscale is bui.UIScale.SMALL + else 1.1 if uiscale is bui.UIScale.MEDIUM else 1.0 + ), + scale_origin_stack_offset=scale_origin, + stack_offset=( + (0, -15) if uiscale is bui.UIScale.SMALL else (0, 0) + ), + darken_behind=True, + ) + ) + self._is_double = False + self._title_text = bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 30), + size=(0, 0), + text=header_text, + h_align='center', + v_align='center', + maxwidth=self._width * 0.9 - 120, + scale=1.2, + color=(1, 0.8, 0.3, 1), + ) + size = store.get_store_item_display_size(items[0]) + display: dict[str, Any] = {} + instantiate_store_item_display( + items[0], + display, + idprefix=self._idprefix, + parent_widget=self._root_widget, + b_pos=( + self._width * 0.5 + - size[0] * 0.5 + + 10 + - ((size[0] * 0.5 + 30) if self._is_double else 0), + self._height * 0.5 + - size[1] * 0.5 + + 30 + + (20 if self._is_double else 0), + ), + b_width=size[0], + b_height=size[1], + button=False, + ) + + # Wire up the parts we need. + if self._is_double: + pass # not working + else: + if self._items == ['pro']: + price_str = plus.get_price(self._items[0]) + pyoffs = -15 + else: + pyoffs = 0 + price = self._price = plus.get_v1_account_misc_read_val( + 'price.' + str(items[0]), -1 + ) + price_str = bui.charstr(bui.SpecialChar.TICKET) + str(price) + self._price_text = bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, 150 + pyoffs), + size=(0, 0), + text=price_str, + h_align='center', + v_align='center', + maxwidth=self._width * 0.9, + scale=1.4, + color=(0.2, 1, 0.2), + ) + + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True + ) + + self._cancel_button = bui.buttonwidget( + parent=self._root_widget, + position=(50, 40), + size=(150, 60), + scale=1.0, + on_activate_call=self._cancel, + autoselect=True, + label=bui.Lstr(resource='cancelText'), + ) + self._purchase_button = bui.buttonwidget( + parent=self._root_widget, + position=(self._width - 200, 40), + size=(150, 60), + scale=1.0, + on_activate_call=self._purchase, + autoselect=True, + label=bui.Lstr(resource='store.purchaseText'), + ) + + bui.containerwidget( + edit=self._root_widget, + cancel_button=self._cancel_button, + start_button=self._purchase_button, + selected_child=self._purchase_button, + ) + + def _update(self) -> None: + can_die = False + + plus = bui.app.plus + assert plus is not None + + # We go away if we see that our target item is owned. + if self._items == ['pro']: + assert bui.app.classic is not None + if bui.app.classic.accounts.have_pro(): + can_die = True + else: + assert bui.app.classic is not None + if self._items[0] in bui.app.classic.purchases: + can_die = True + + if can_die: + bui.containerwidget(edit=self._root_widget, transition='out_scale') + + def _purchase(self) -> None: + + plus = bui.app.plus + assert plus is not None + classic = bui.app.classic + assert classic is not None + + if self._items == ['pro']: + plus.purchase('pro') + else: + ticket_count: int | None + try: + ticket_count = classic.tickets + except Exception: + ticket_count = None + if ticket_count is not None and ticket_count < self._price: + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='notEnoughTicketsText'), + color=(1, 0, 0), + ) + return + + def do_it() -> None: + assert plus is not None + + plus.in_game_purchase(self._items[0], self._price) + + bui.getsound('swish').play() + do_it() + + def _cancel(self) -> None: + bui.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/dist/ba_data/python/bauiv1lib/qrcode.py b/dist/ba_data/python/bauiv1lib/qrcode.py index aac2546..c76749c 100644 --- a/dist/ba_data/python/bauiv1lib/qrcode.py +++ b/dist/ba_data/python/bauiv1lib/qrcode.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides functionality for displaying QR codes.""" - from __future__ import annotations from typing import override diff --git a/dist/ba_data/python/bauiv1lib/radiogroup.py b/dist/ba_data/python/bauiv1lib/radiogroup.py index e1236d3..6492786 100644 --- a/dist/ba_data/python/bauiv1lib/radiogroup.py +++ b/dist/ba_data/python/bauiv1lib/radiogroup.py @@ -33,7 +33,7 @@ def make_radio_group( edit=check_box, value=(value == value_names[i]), is_radio_button=True, - on_value_change_call=bui.CallPartial( + on_value_change_call=bui.Call( _radio_press, value_names[i], [c for c in check_boxes if c != check_box], diff --git a/dist/ba_data/python/bauiv1lib/resourcetypeinfo.py b/dist/ba_data/python/bauiv1lib/resourcetypeinfo.py index a3bb211..c5eb343 100644 --- a/dist/ba_data/python/bauiv1lib/resourcetypeinfo.py +++ b/dist/ba_data/python/bauiv1lib/resourcetypeinfo.py @@ -22,9 +22,6 @@ class ResourceTypeInfoWindow(PopupWindow): origin_widget: bui.Widget, ): assert bui.app.classic is not None - - self._uiopenstate = bui.UIOpenState(f'resourcetypeinfo{resource_type}') - uiscale = bui.app.ui_v1.uiscale scale = ( 2.0 @@ -90,9 +87,7 @@ class ResourceTypeInfoWindow(PopupWindow): label=bui.Lstr(resource='tokens.getTokensText'), size=(bwidth, bheight), autoselect=True, - on_activate_call=bui.WeakCallStrict( - self._on_get_tokens_press - ), + on_activate_call=bui.WeakCall(self._on_get_tokens_press), ) elif resource_type == 'trophies': diff --git a/dist/ba_data/python/bauiv1lib/sendinfo.py b/dist/ba_data/python/bauiv1lib/sendinfo.py index 7eb3281..ddb63cc 100644 --- a/dist/ba_data/python/bauiv1lib/sendinfo.py +++ b/dist/ba_data/python/bauiv1lib/sendinfo.py @@ -377,7 +377,7 @@ class SendInfoWindowLegacyModal(bui.Window): async def _send_info(description: str) -> None: - from bacommon.classic import SendInfoMessage + from bacommon.bs import SendInfoMessage plus = bui.app.plus assert plus is not None diff --git a/dist/ba_data/python/bauiv1lib/settings/advanced.py b/dist/ba_data/python/bauiv1lib/settings/advanced.py index 4dd4420..d7a3f2b 100644 --- a/dist/ba_data/python/bauiv1lib/settings/advanced.py +++ b/dist/ba_data/python/bauiv1lib/settings/advanced.py @@ -10,9 +10,9 @@ import logging from typing import TYPE_CHECKING, override from bacommon.locale import LocaleResolved -import bauiv1 as bui -from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top from bauiv1lib.popup import PopupMenu +from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top +import bauiv1 as bui if TYPE_CHECKING: from typing import Any @@ -216,14 +216,14 @@ class AdvancedSettingsWindow(bui.MainWindow): # Rebuild periodically to pick up language changes/additions/etc. self._rebuild_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._rebuild), repeat=True + 1.0, bui.WeakCall(self._rebuild), repeat=True ) # Fetch the list of completed languages. bui.app.classic.master_server_v1_get( 'bsLangGetCompleted', {'b': app.env.engine_build_number}, - callback=bui.WeakCallPartial(self._completed_langs_cb), + callback=bui.WeakCall(self._completed_langs_cb), ) @override @@ -414,10 +414,10 @@ class AdvancedSettingsWindow(bui.MainWindow): button_id=f'{self.main_window_id_prefix}|language', position=(210, v - 19), width=250, - opening_call=bui.WeakCallStrict(self._on_menu_open), - closing_call=bui.WeakCallStrict(self._on_menu_close), + opening_call=bui.WeakCall(self._on_menu_open), + closing_call=bui.WeakCall(self._on_menu_close), autoselect=True, - on_value_change_call=bui.WeakCallPartial(self._on_menu_choice), + on_value_change_call=bui.WeakCall(self._on_menu_choice), choices=['Auto'] + available_languages, button_size=(300, 60), choices_display=( @@ -478,7 +478,7 @@ class AdvancedSettingsWindow(bui.MainWindow): subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), autoselect=True, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( bui.open_url, 'https://legacy.ballistica.net/translate' ), ) @@ -509,7 +509,7 @@ class AdvancedSettingsWindow(bui.MainWindow): textcolor=(0.8, 0.8, 0.8), value=lang_inform, text=bui.Lstr(resource=f'{self._r}.translationInformMe'), - on_value_change_call=bui.WeakCallPartial( + on_value_change_call=bui.WeakCall( self._on_lang_inform_value_change ), ) @@ -686,7 +686,7 @@ class AdvancedSettingsWindow(bui.MainWindow): autoselect=True, label=bui.Lstr(resource=f'{self._r}.moddingGuideText'), text_scale=1.0, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( bui.open_url, 'https://ballistica.net/wiki/modding-guide' ), ) @@ -913,7 +913,7 @@ class AdvancedSettingsWindow(bui.MainWindow): self.main_window_save_shared_state() - bui.apptimer(0.1, bui.WeakCallStrict(self._rebuild)) + bui.apptimer(0.1, bui.WeakCall(self._rebuild)) def _completed_langs_cb(self, results: dict[str, Any] | None) -> None: if results is not None and results['langs'] is not None: @@ -922,4 +922,4 @@ class AdvancedSettingsWindow(bui.MainWindow): else: self._complete_langs_list = None self._complete_langs_error = True - bui.apptimer(0.001, bui.WeakCallStrict(self._update_lang_status)) + bui.apptimer(0.001, bui.WeakCall(self._update_lang_status)) diff --git a/dist/ba_data/python/bauiv1lib/settings/allsettings.py b/dist/ba_data/python/bauiv1lib/settings/allsettings.py index 2f92110..7065061 100644 --- a/dist/ba_data/python/bauiv1lib/settings/allsettings.py +++ b/dist/ba_data/python/bauiv1lib/settings/allsettings.py @@ -27,8 +27,6 @@ class AllSettingsWindow(bui.MainWindow): # have a visual hitch when the user taps them. bui.app.threadpool.submit_no_wait(self._preload_modules) - self._uiopenstate = bui.UIOpenState('settings') - bui.set_analytics_screen('Settings Window') assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale @@ -247,10 +245,7 @@ class AllSettingsWindow(bui.MainWindow): return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget - ), - # Keeps our icon glowing as long as this is in the back - # stack. - uiopenstate=self._uiopenstate, + ) ) @override diff --git a/dist/ba_data/python/bauiv1lib/settings/audio.py b/dist/ba_data/python/bauiv1lib/settings/audio.py index 97c897a..14bf15c 100644 --- a/dist/ba_data/python/bauiv1lib/settings/audio.py +++ b/dist/ba_data/python/bauiv1lib/settings/audio.py @@ -214,8 +214,7 @@ class AudioSettingsWindow(bui.MainWindow): color=(0.5, 1, 0.5), ) bui.apptimer( - 1.0, - bui.CallStrict(bui.request_permission, bui.Permission.STORAGE), + 1.0, bui.Call(bui.request_permission, bui.Permission.STORAGE) ) return diff --git a/dist/ba_data/python/bauiv1lib/settings/benchmarks.py b/dist/ba_data/python/bauiv1lib/settings/benchmarks.py index 522ece9..4296108 100644 --- a/dist/ba_data/python/bauiv1lib/settings/benchmarks.py +++ b/dist/ba_data/python/bauiv1lib/settings/benchmarks.py @@ -266,9 +266,7 @@ class BenchmarksAndStressTestsWindow(bui.MainWindow): size=(28, 28), label='-', autoselect=True, - on_activate_call=bui.CallStrict( - self._stress_test_player_count_decrement - ), + on_activate_call=bui.Call(self._stress_test_player_count_decrement), repeat=True, enable_sound=True, ) @@ -279,9 +277,7 @@ class BenchmarksAndStressTestsWindow(bui.MainWindow): size=(28, 28), label='+', autoselect=True, - on_activate_call=bui.CallStrict( - self._stress_test_player_count_increment - ), + on_activate_call=bui.Call(self._stress_test_player_count_increment), repeat=True, enable_sound=True, ) @@ -317,7 +313,7 @@ class BenchmarksAndStressTestsWindow(bui.MainWindow): size=(28, 28), label='-', autoselect=True, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._stress_test_round_duration_decrement ), repeat=True, @@ -330,7 +326,7 @@ class BenchmarksAndStressTestsWindow(bui.MainWindow): size=(28, 28), label='+', autoselect=True, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._stress_test_round_duration_increment ), repeat=True, diff --git a/dist/ba_data/python/bauiv1lib/settings/gamepad.py b/dist/ba_data/python/bauiv1lib/settings/gamepad.py index 2c4aad7..48740f0 100644 --- a/dist/ba_data/python/bauiv1lib/settings/gamepad.py +++ b/dist/ba_data/python/bauiv1lib/settings/gamepad.py @@ -585,8 +585,8 @@ class GamepadSettingsWindow(bui.MainWindow): if 'analogStickUD' + self._ext in self._settings else 6 if self._is_secondary else None ) - assert isinstance(sval1, int | None) - assert isinstance(sval2, int | None) + assert isinstance(sval1, (int, type(None))) + assert isinstance(sval2, (int, type(None))) if sval1 is not None and sval2 is not None: return ( self._inputdevice.get_axis_name(sval1) @@ -630,7 +630,7 @@ class GamepadSettingsWindow(bui.MainWindow): if 'dpad' + self._ext in self._settings else 2 if self._is_secondary else None ) - assert isinstance(dpadnum, int | None) + assert isinstance(dpadnum, (int, type(None))) if dpadnum is not None: return bui.Lstr( value='${A} ${B}', @@ -811,7 +811,7 @@ class GamepadSettingsWindow(bui.MainWindow): self._textwidgets[button] = txt bui.buttonwidget( edit=btn, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( AwaitGamepadInputWindow, self._inputdevice, button, @@ -1037,11 +1037,9 @@ class AwaitGamepadInputWindow(bui.Window): text=str(self._counter), ) self._decrement_timer: bui.AppTimer | None = bui.AppTimer( - 1.0, bui.CallStrict(self._decrement), repeat=True - ) - bs.capture_game_controller_input( - bui.WeakCallPartial(self._event_callback) + 1.0, bui.Call(self._decrement), repeat=True ) + bs.capture_game_controller_input(bui.WeakCall(self._event_callback)) def die(self) -> None: """Kill the window.""" diff --git a/dist/ba_data/python/bauiv1lib/settings/gamepadadvanced.py b/dist/ba_data/python/bauiv1lib/settings/gamepadadvanced.py index 9738505..ab06216 100644 --- a/dist/ba_data/python/bauiv1lib/settings/gamepadadvanced.py +++ b/dist/ba_data/python/bauiv1lib/settings/gamepadadvanced.py @@ -415,7 +415,7 @@ class GamepadAdvancedSettingsWindow(bui.Window): label=bui.Lstr(resource=f'{self._r}.clearText'), size=(110, 50), scale=0.7, - on_activate_call=bui.CallStrict(self._clear_control, control), + on_activate_call=bui.Call(self._clear_control, control), ) bui.widget(edit=btn, right_widget=btn2) @@ -439,7 +439,7 @@ class GamepadAdvancedSettingsWindow(bui.Window): self._textwidgets[control] = txt bui.buttonwidget( edit=btn, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( AwaitGamepadInputWindow, self._parent_window.get_input(), control, @@ -509,7 +509,7 @@ class GamepadAdvancedSettingsWindow(bui.Window): position=(330 + x_offset, position[1] + 4), size=(28, 28), label='-', - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._inc, control, min_val, max_val, -increment ), repeat=True, @@ -521,7 +521,7 @@ class GamepadAdvancedSettingsWindow(bui.Window): position=(380 + x_offset, position[1] + 4), size=(28, 28), label='+', - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._inc, control, min_val, max_val, increment ), repeat=True, diff --git a/dist/ba_data/python/bauiv1lib/settings/gamepadselect.py b/dist/ba_data/python/bauiv1lib/settings/gamepadselect.py index 5af7af9..dbbf268 100644 --- a/dist/ba_data/python/bauiv1lib/settings/gamepadselect.py +++ b/dist/ba_data/python/bauiv1lib/settings/gamepadselect.py @@ -106,7 +106,7 @@ class GamepadSelectWindow(bui.MainWindow): ) bs.capture_game_controller_input( - bui.WeakCallPartial(self.gamepad_configure_callback) + bui.WeakCall(self.gamepad_configure_callback) ) def __del__(self) -> None: diff --git a/dist/ba_data/python/bauiv1lib/settings/graphics.py b/dist/ba_data/python/bauiv1lib/settings/graphics.py index a898619..e538d3f 100644 --- a/dist/ba_data/python/bauiv1lib/settings/graphics.py +++ b/dist/ba_data/python/bauiv1lib/settings/graphics.py @@ -455,7 +455,7 @@ class GraphicsSettingsWindow(bui.MainWindow): # Make a timer to update our controls in case the config changes # under us. self._update_timer = bui.AppTimer( - 0.25, bui.WeakCallStrict(self._update_controls), repeat=True + 0.25, bui.WeakCall(self._update_controls), repeat=True ) @override diff --git a/dist/ba_data/python/bauiv1lib/settings/keyboard.py b/dist/ba_data/python/bauiv1lib/settings/keyboard.py index 73b4072..63f3cc1 100644 --- a/dist/ba_data/python/bauiv1lib/settings/keyboard.py +++ b/dist/ba_data/python/bauiv1lib/settings/keyboard.py @@ -310,7 +310,7 @@ class ConfigKeyboardWindow(bui.MainWindow): bui.buttonwidget( edit=btn, autoselect=True, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( AwaitKeyboardInputWindow, button, txt, self._settings ), ) @@ -468,7 +468,7 @@ class AwaitKeyboardInputWindow(bui.Window): self._decrement_timer: bui.AppTimer | None = bui.AppTimer( 1.0, self._decrement, repeat=True ) - bs.capture_keyboard_input(bui.WeakCallPartial(self._button_callback)) + bs.capture_keyboard_input(bui.WeakCall(self._button_callback)) def __del__(self) -> None: bs.release_keyboard_input() diff --git a/dist/ba_data/python/bauiv1lib/settings/nettesting.py b/dist/ba_data/python/bauiv1lib/settings/nettesting.py index 018d05b..b6a521f 100644 --- a/dist/ba_data/python/bauiv1lib/settings/nettesting.py +++ b/dist/ba_data/python/bauiv1lib/settings/nettesting.py @@ -163,9 +163,7 @@ class NetTestingWindow(bui.MainWindow): # Pass a weak-ref to this window so we don't keep it alive # if we back out before it completes. Also set is as daemon # so it doesn't keep the app running if the user is trying to quit. - Thread( - target=bui.CallStrict(_run_diagnostics, weakref.ref(self)) - ).start() + Thread(target=bui.Call(_run_diagnostics, weakref.ref(self))).start() @override def get_main_window_state(self) -> bui.MainWindowState: diff --git a/dist/ba_data/python/bauiv1lib/settings/plugins.py b/dist/ba_data/python/bauiv1lib/settings/plugins.py index 9fdcfdb..c6b78b2 100644 --- a/dist/ba_data/python/bauiv1lib/settings/plugins.py +++ b/dist/ba_data/python/bauiv1lib/settings/plugins.py @@ -35,7 +35,6 @@ class PluginWindow(bui.MainWindow): transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): - # pylint: disable=too-many-statements # pylint: disable=too-many-locals app = bui.app @@ -55,7 +54,7 @@ class PluginWindow(bui.MainWindow): # screen shape at small ui scale. screensize = bui.get_virtual_screen_size() scale = ( - 1.7 + 1.9 if uiscale is bui.UIScale.SMALL else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 ) @@ -122,21 +121,16 @@ class PluginWindow(bui.MainWindow): size=(0, 0), text=bui.Lstr(resource='pluginsText'), color=app.ui_v1.title_color, - maxwidth=140, + maxwidth=170, h_align='center', v_align='center', ) - settings_button_x = self._width * 0.5 + self._scroll_width * 0.5 - 40 - if uiscale is bui.UIScale.SMALL: - # In small UI there's stuff top right we need to avoid. - if bui.in_main_menu(): - # Squads button - settings_button_x -= 65 - else: - # Squads and settings buttons - settings_button_x -= 115 - + settings_button_x = ( + self._width * 0.5 + + self._scroll_width * 0.5 + - (100 if uiscale is bui.UIScale.SMALL else 40) + ) button_row_yoffs = yoffs + (-2 if uiscale is bui.UIScale.SMALL else 10) self._num_plugins_text = bui.textwidget( @@ -156,7 +150,7 @@ class PluginWindow(bui.MainWindow): size=(130, 60), label=bui.Lstr(resource='allText'), autoselect=True, - on_activate_call=bui.WeakCallStrict(self._show_category_options), + on_activate_call=bui.WeakCall(self._show_category_options), color=(0.55, 0.73, 0.25), iconscale=1.2, ) @@ -384,7 +378,7 @@ class PluginWindow(bui.MainWindow): ), position=(10, item_y), size=(self._scroll_width - 40, 50), - on_value_change_call=bui.CallPartial( + on_value_change_call=bui.Call( self._check_value_changed, plugspec ), textcolor=( @@ -408,9 +402,7 @@ class PluginWindow(bui.MainWindow): ) bui.buttonwidget( edit=button, - on_activate_call=bui.CallStrict( - plugin.show_settings_ui, button - ), + on_activate_call=bui.Call(plugin.show_settings_ui, button), ) else: button = None diff --git a/dist/ba_data/python/bauiv1lib/settings/testing.py b/dist/ba_data/python/bauiv1lib/settings/testing.py index b439d99..712ce93 100644 --- a/dist/ba_data/python/bauiv1lib/settings/testing.py +++ b/dist/ba_data/python/bauiv1lib/settings/testing.py @@ -171,9 +171,7 @@ class TestingWindow(bui.MainWindow): left_widget=self._back_button, button_type='square', label='-', - on_activate_call=bui.CallStrict( - self._on_minus_press, entry['name'] - ), + on_activate_call=bui.Call(self._on_minus_press, entry['name']), ) if i == 0: bui.widget(edit=btn, up_widget=self._back_button) @@ -194,9 +192,7 @@ class TestingWindow(bui.MainWindow): repeat=True, button_type='square', label='+', - on_activate_call=bui.CallStrict( - self._on_plus_press, entry['name'] - ), + on_activate_call=bui.Call(self._on_plus_press, entry['name']), ) if i == 0: bui.widget(edit=btn, up_widget=self._back_button) diff --git a/dist/ba_data/python/bauiv1lib/settings/touchscreen.py b/dist/ba_data/python/bauiv1lib/settings/touchscreen.py index 5ea269a..580097a 100644 --- a/dist/ba_data/python/bauiv1lib/settings/touchscreen.py +++ b/dist/ba_data/python/bauiv1lib/settings/touchscreen.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """UI settings functionality related to touchscreens.""" - from __future__ import annotations from typing import override diff --git a/dist/ba_data/python/bauiv1lib/soundtrack/browser.py b/dist/ba_data/python/bauiv1lib/soundtrack/browser.py index 2a78067..6ab5c52 100644 --- a/dist/ba_data/python/bauiv1lib/soundtrack/browser.py +++ b/dist/ba_data/python/bauiv1lib/soundtrack/browser.py @@ -13,6 +13,8 @@ import bauiv1 as bui if TYPE_CHECKING: from typing import Any +REQUIRE_PRO = False + class SoundtrackBrowserWindow(bui.MainWindow): """Window for browsing soundtracks.""" @@ -223,7 +225,7 @@ class SoundtrackBrowserWindow(bui.MainWindow): # Keep our lock images up to date/etc. self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() @@ -282,8 +284,14 @@ class SoundtrackBrowserWindow(bui.MainWindow): self._save_state() def _update(self) -> None: + have_pro = ( + bui.app.classic is None + or bui.app.classic.accounts.have_pro_options() + ) for lock in self._lock_images: - bui.imagewidget(edit=lock, opacity=0.0 if bool(True) else 1.0) + bui.imagewidget( + edit=lock, opacity=0.0 if (have_pro or not REQUIRE_PRO) else 1.0 + ) def _do_delete_soundtrack(self) -> None: cfg = bui.app.config @@ -301,8 +309,15 @@ class SoundtrackBrowserWindow(bui.MainWindow): def _delete_soundtrack(self) -> None: # pylint: disable=cyclic-import + from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.confirm import ConfirmWindow + if REQUIRE_PRO and ( + bui.app.classic is not None + and not bui.app.classic.accounts.have_pro_options() + ): + PurchaseWindow(items=['pro']) + return if self._selected_soundtrack is None: return if self._selected_soundtrack == '__default__': @@ -323,7 +338,15 @@ class SoundtrackBrowserWindow(bui.MainWindow): ) def _duplicate_soundtrack(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.purchase import PurchaseWindow + if REQUIRE_PRO and ( + bui.app.classic is not None + and not bui.app.classic.accounts.have_pro_options() + ): + PurchaseWindow(items=['pro']) + return cfg = bui.app.config cfg.setdefault('Soundtracks', {}) @@ -385,16 +408,34 @@ class SoundtrackBrowserWindow(bui.MainWindow): ) def _edit_soundtrack_with_sound(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.purchase import PurchaseWindow + + if REQUIRE_PRO and ( + bui.app.classic is not None + and not bui.app.classic.accounts.have_pro_options() + ): + PurchaseWindow(items=['pro']) + return bui.getsound('swish').play() self._edit_soundtrack() def _edit_soundtrack(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.soundtrack.edit import SoundtrackEditWindow # no-op if we don't have control. if not self.main_window_has_control(): return + if REQUIRE_PRO and ( + bui.app.classic is not None + and not bui.app.classic.accounts.have_pro_options() + ): + PurchaseWindow(items=['pro']) + return + if self._selected_soundtrack is None: return @@ -448,7 +489,7 @@ class SoundtrackBrowserWindow(bui.MainWindow): v_align='center', maxwidth=self._width - 110, always_highlight=True, - on_select_call=bui.WeakCallStrict(self._select, pname, index), + on_select_call=bui.WeakCall(self._select, pname, index), on_activate_call=self._edit_soundtrack_with_sound, selectable=True, ) @@ -489,7 +530,7 @@ class SoundtrackBrowserWindow(bui.MainWindow): # Eww need to run this in a timer so it happens after our select # callbacks. With a small-enough time sometimes it happens before # anyway. Ew. need a way to just schedule a callable i guess. - bui.apptimer(0.1, bui.WeakCallStrict(self._set_allow_changing)) + bui.apptimer(0.1, bui.WeakCall(self._set_allow_changing)) def _set_allow_changing(self) -> None: self._allow_changing_soundtracks = True @@ -498,12 +539,21 @@ class SoundtrackBrowserWindow(bui.MainWindow): self._select(self._selected_soundtrack, self._selected_soundtrack_index) def _new_soundtrack(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.soundtrack.edit import SoundtrackEditWindow # no-op if we're not in control. if not self.main_window_has_control(): return + if REQUIRE_PRO and ( + bui.app.classic is not None + and not bui.app.classic.accounts.have_pro_options() + ): + PurchaseWindow(items=['pro']) + return + self.main_window_replace( lambda: SoundtrackEditWindow(existing_soundtrack=None) ) diff --git a/dist/ba_data/python/bauiv1lib/soundtrack/edit.py b/dist/ba_data/python/bauiv1lib/soundtrack/edit.py index f79248f..1dfd58a 100644 --- a/dist/ba_data/python/bauiv1lib/soundtrack/edit.py +++ b/dist/ba_data/python/bauiv1lib/soundtrack/edit.py @@ -313,7 +313,7 @@ class SoundtrackEditWindow(bui.MainWindow): size=(230, 32), label=self._get_entry_button_display_name(entry), text_scale=0.6, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._get_entry, song_type, entry, type_name ), icon=( @@ -364,9 +364,7 @@ class SoundtrackEditWindow(bui.MainWindow): size=(50, 32), label=bui.Lstr(resource=f'{self._r}.testText'), text_scale=0.6, - on_activate_call=bui.CallStrict( - self._test, bs.MusicType(song_type) - ), + on_activate_call=bui.Call(self._test, bs.MusicType(song_type)), up_widget=( prev_test_button if prev_test_button is not None @@ -431,7 +429,7 @@ class SoundtrackEditWindow(bui.MainWindow): } new_win = self.main_window_replace( lambda: music.get_music_player().select_entry( - bui.CallPartial(self._restore_editor, state, song_type), + bui.Call(self._restore_editor, state, song_type), entry, selection_target_name, ) diff --git a/dist/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py b/dist/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py index 281dd87..4061aef 100644 --- a/dist/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py +++ b/dist/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Provides UI for selecting soundtrack entry types.""" - from __future__ import annotations import copy diff --git a/dist/ba_data/python/bauiv1lib/soundtrack/macmusicapp.py b/dist/ba_data/python/bauiv1lib/soundtrack/macmusicapp.py index 65af31e..2c47085 100644 --- a/dist/ba_data/python/bauiv1lib/soundtrack/macmusicapp.py +++ b/dist/ba_data/python/bauiv1lib/soundtrack/macmusicapp.py @@ -126,7 +126,7 @@ class MacMusicAppPlaylistSelectWindow(bui.MainWindow): v_align='center', maxwidth=self._width - 110, selectable=True, - on_activate_call=bui.CallStrict(self._sel, playlist), + on_activate_call=bui.Call(self._sel, playlist), click_activate=True, ) bui.widget(edit=txt, show_buffer_top=40, show_buffer_bottom=40) diff --git a/dist/ba_data/python/bauiv1lib/store.py b/dist/ba_data/python/bauiv1lib/store.py deleted file mode 100644 index be521f0..0000000 --- a/dist/ba_data/python/bauiv1lib/store.py +++ /dev/null @@ -1,59 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Shiny new doc-ui based store.""" - -from __future__ import annotations - -from typing import override, TYPE_CHECKING - -from bauiv1lib.docui import DocUIController - -import bauiv1 as bui - -if TYPE_CHECKING: - from bacommon.docui import DocUIRequest, DocUIResponse - - from bauiv1lib.docui import DocUILocalAction - - -class StoreUIController(DocUIController): - """DocUI setup for store.""" - - @override - def fulfill_request(self, request: DocUIRequest) -> DocUIResponse: - return self.fulfill_request_cloud(request, 'classicstore') - - @override - def local_action(self, action: DocUILocalAction) -> None: - - if action.name == 'get_tokens': - self._get_tokens(action) - elif action.name == 'restore_purchases': - self._restore_purchases() - else: - bui.screenmessage( - f'Invalid local-action "{action.name}".', color=(1, 0, 0) - ) - bui.getsound('error').play() - - def _restore_purchases(self) -> None: - - plus = bui.app.plus - assert plus is not None - - # We should always be signed in here. Make noise if not. - if plus.accounts.primary is None: - bui.screenmessage( - bui.Lstr(resource='notSignedInText'), color=(1, 0, 0) - ) - bui.getsound('error').play() - return - - plus.restore_purchases() - - def _get_tokens(self, action: DocUILocalAction) -> None: - from bauiv1lib.gettokens import show_get_tokens_window - - bui.getsound('swish').play() - - show_get_tokens_window(origin_widget=bui.existing(action.widget)) diff --git a/dist/ba_data/python/bauiv1lib/store/__init__.py b/dist/ba_data/python/bauiv1lib/store/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bauiv1lib/store/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bauiv1lib/store/browser.py b/dist/ba_data/python/bauiv1lib/store/browser.py new file mode 100644 index 0000000..78991e4 --- /dev/null +++ b/dist/ba_data/python/bauiv1lib/store/browser.py @@ -0,0 +1,1249 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI for browsing the store.""" +# pylint: disable=too-many-lines +from __future__ import annotations + +import os +import time +import copy +import math +import logging +import weakref +import datetime +from enum import Enum +from threading import Thread +from typing import TYPE_CHECKING, override + +from efro.util import utc_now +from efro.error import CommunicationError +import bacommon.cloud +import bauiv1 as bui + +if TYPE_CHECKING: + from typing import Any, Callable, Sequence + +MERCH_LINK_KEY = 'Merch Link' + + +class StoreBrowserWindow(bui.MainWindow): + """Window for browsing the store.""" + + class TabID(Enum): + """Our available tab types.""" + + # EXTRAS = 'extras' + MAPS = 'maps' + MINIGAMES = 'minigames' + CHARACTERS = 'characters' + ICONS = 'icons' + + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + *, + show_tab: StoreBrowserWindow.TabID | None = None, + minimal_toolbars: bool = False, + auxiliary_style: bool = True, + ): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + from bauiv1lib.tabs import TabRow + from bauiv1 import SpecialChar + + app = bui.app + assert app.classic is not None + uiscale = app.ui_v1.uiscale + + bui.set_analytics_screen('Store Window') + + self.button_infos: dict[str, dict[str, Any]] | None = None + self.update_buttons_timer: bui.AppTimer | None = None + self._status_textwidget_update_timer = None + + self._show_tab = show_tab + self._width = ( + 1800 + if uiscale is bui.UIScale.SMALL + else 1000 if uiscale is bui.UIScale.MEDIUM else 1120 + ) + self._height = ( + 1200 + if uiscale is bui.UIScale.SMALL + else 700 if uiscale is bui.UIScale.MEDIUM else 800 + ) + self._current_tab: StoreBrowserWindow.TabID | None = None + + self.request: Any = None + self._r = 'store' + self._last_buy_time: float | None = None + + # Do some fancy math to fill all available screen area up to the + # size of our backing container. This lets us fit to the exact + # screen shape at small ui scale. + screensize = bui.get_virtual_screen_size() + scale = ( + 1.5 + if uiscale is bui.UIScale.SMALL + else 0.9 if uiscale is bui.UIScale.MEDIUM else 0.8 + ) + + # Calc screen size in our local container space and clamp to a + # bit smaller than our container size. + target_width = min(self._width - 120, screensize[0] / scale) + target_height = min(self._height - 140, screensize[1] / scale) + + # To get top/left coords, go to the center of our window and + # offset by half the width/height of our target area. + yoffs = 0.5 * self._height + 0.5 * target_height + 30.0 + + self._scroll_width = target_width + self._scroll_height = target_height - 59 + self._scroll_bottom = yoffs - 87 - self._scroll_height + + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height), + toolbar_visibility=( + 'menu_store' + if (uiscale is bui.UIScale.SMALL or minimal_toolbars) + else 'menu_full' + ), + toolbar_cancel_button_style=( + 'close' if auxiliary_style else 'back' + ), + scale=scale, + ), + transition=transition, + origin_widget=origin_widget, + # We're affected by screen size only at small ui-scale. + refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, + ) + + self._back_button = btn = bui.buttonwidget( + parent=self._root_widget, + id=f'{self.main_window_id_prefix}|back', + position=(70, yoffs - 37), + size=(60, 60), + scale=1.1, + autoselect=True, + label=bui.charstr( + SpecialChar.CLOSE if auxiliary_style else SpecialChar.BACK + ), + button_type=None if auxiliary_style else 'backSmall', + on_activate_call=self.main_window_back, + ) + + if uiscale is bui.UIScale.SMALL: + self._back_button.delete() + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + else: + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + + if ( + app.classic.platform in ['mac', 'ios'] + and app.classic.subplatform == 'appstore' + ): + bui.buttonwidget( + parent=self._root_widget, + id=f'{self.main_window_id_prefix}|restorepurchases', + position=(self._width * 0.5 - 70, 16), + size=(230, 50), + scale=0.65, + on_activate_call=bui.WeakCall(self._restore_purchases), + color=(0.35, 0.3, 0.4), + selectable=False, + textcolor=(0.55, 0.5, 0.6), + label=bui.Lstr( + resource='getTicketsWindow.restorePurchasesText' + ), + ) + + bui.textwidget( + parent=self._root_widget, + position=( + ( + self._width * 0.5 + + ( + (self._scroll_width * -0.5 + 90.0) + if uiscale is bui.UIScale.SMALL + else 0.0 + ) + ), + yoffs - (62 if uiscale is bui.UIScale.SMALL else -3.0), + ), + size=(0, 0), + color=app.ui_v1.title_color, + scale=1.1 if uiscale is bui.UIScale.SMALL else 1.3, + h_align='left' if uiscale is bui.UIScale.SMALL else 'center', + v_align='center', + text=bui.Lstr(resource='storeText'), + maxwidth=100 if uiscale is bui.UIScale.SMALL else 290, + ) + + tabs_def = [ + # (self.TabID.EXTRAS, bui.Lstr(resource=f'{self._r}.extrasText')), + (self.TabID.MAPS, bui.Lstr(resource=f'{self._r}.mapsText')), + ( + self.TabID.MINIGAMES, + bui.Lstr(resource=f'{self._r}.miniGamesText'), + ), + ( + self.TabID.CHARACTERS, + bui.Lstr(resource=f'{self._r}.charactersText'), + ), + (self.TabID.ICONS, bui.Lstr(resource=f'{self._r}.iconsText')), + ] + + tab_inset = 200 if uiscale is bui.UIScale.SMALL else 100 + self._tab_row = TabRow( + self._root_widget, + tabs_def, + idprefix=self.main_window_id_prefix, + size=(self._scroll_width - 2.0 * tab_inset, 50), + pos=( + self._width * 0.5 - self._scroll_width * 0.5 + tab_inset, + self._scroll_bottom + self._scroll_height - 4.0, + ), + on_select_call=self._set_tab, + ) + + self._purchasable_count_widgets: dict[ + StoreBrowserWindow.TabID, dict[str, Any] + ] = {} + + # Create our purchasable-items tags and have them update over time. + for tab_id, tab in self._tab_row.tabs.items(): + pos = tab.position + size = tab.size + button = tab.button + rad = 10 + center = (pos[0] + 0.1 * size[0], pos[1] + 0.9 * size[1]) + img = bui.imagewidget( + parent=self._root_widget, + position=(center[0] - rad * 1.1, center[1] - rad * 1.2), + size=(rad * 2.4, rad * 2.4), + texture=bui.gettexture('circleShadow'), + color=(1, 0, 0), + ) + txt = bui.textwidget( + parent=self._root_widget, + position=center, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=1.4 * rad, + scale=0.6, + shadow=1.0, + flatness=1.0, + ) + rad = 20 + sale_img = bui.imagewidget( + parent=self._root_widget, + position=(center[0] - rad, center[1] - rad), + size=(rad * 2, rad * 2), + draw_controller=button, + texture=bui.gettexture('circleZigZag'), + color=(0.5, 0, 1.0), + ) + sale_title_text = bui.textwidget( + parent=self._root_widget, + position=(center[0], center[1] + 0.24 * rad), + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=button, + maxwidth=1.4 * rad, + scale=0.6, + shadow=0.0, + flatness=1.0, + color=(0, 1, 0), + ) + sale_time_text = bui.textwidget( + parent=self._root_widget, + position=(center[0], center[1] - 0.29 * rad), + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=button, + maxwidth=1.4 * rad, + scale=0.4, + shadow=0.0, + flatness=1.0, + color=(0, 1, 0), + ) + self._purchasable_count_widgets[tab_id] = { + 'img': img, + 'text': txt, + 'sale_img': sale_img, + 'sale_title_text': sale_title_text, + 'sale_time_text': sale_time_text, + } + self._tab_update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update_tabs), repeat=True + ) + self._update_tabs() + + if uiscale is bui.UIScale.SMALL: + first_tab_button = self._tab_row.tabs[tabs_def[0][0]].button + last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button + bui.widget( + edit=first_tab_button, + left_widget=bui.get_special_widget('back_button'), + up_widget=bui.get_special_widget('back_button'), + ) + bui.widget( + edit=last_tab_button, + up_widget=bui.get_special_widget('tickets_meter'), + right_widget=bui.get_special_widget('tickets_meter'), + ) + + self._scrollwidget: bui.Widget | None = None + self._status_textwidget: bui.Widget | None = None + + # Restore/set tab. + try: + current_tab = self.TabID(bui.app.config.get('Store Tab')) + except ValueError: + current_tab = self.TabID.CHARACTERS + self._set_tab(current_tab) + + def _restore_purchases(self) -> None: + from bauiv1lib.account.signin import show_sign_in_prompt + + plus = bui.app.plus + assert plus is not None + if plus.accounts.primary is None: + show_sign_in_prompt() + else: + plus.restore_purchases() + + def _update_tabs(self) -> None: + assert bui.app.classic is not None + store = bui.app.classic.store + + if not self._root_widget: + return + for tab_id, tab_data in list(self._purchasable_count_widgets.items()): + sale_time = store.get_available_sale_time(tab_id.value) + + if sale_time is not None: + bui.textwidget( + edit=tab_data['sale_title_text'], + text=bui.Lstr(resource='store.saleText'), + ) + bui.textwidget( + edit=tab_data['sale_time_text'], + text=bui.timestring(sale_time / 1000.0, centi=False), + ) + bui.imagewidget(edit=tab_data['sale_img'], opacity=1.0) + count = 0 + else: + bui.textwidget(edit=tab_data['sale_title_text'], text='') + bui.textwidget(edit=tab_data['sale_time_text'], text='') + bui.imagewidget(edit=tab_data['sale_img'], opacity=0.0) + count = store.get_available_purchase_count(tab_id.value) + + if count > 0: + bui.textwidget(edit=tab_data['text'], text=str(count)) + bui.imagewidget(edit=tab_data['img'], opacity=1.0) + else: + bui.textwidget(edit=tab_data['text'], text='') + bui.imagewidget(edit=tab_data['img'], opacity=0.0) + + def _set_tab(self, tab_id: TabID) -> None: + if self._current_tab is tab_id: + return + self._current_tab = tab_id + + # We wanna preserve our current tab between runs. + cfg = bui.app.config + cfg['Store Tab'] = tab_id.value + cfg.commit() + + # Update tab colors based on which is selected. + self._tab_row.update_appearance(tab_id) + + # (Re)create scroll widget. + if self._scrollwidget: + self._scrollwidget.delete() + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + highlight=False, + size=(self._scroll_width, self._scroll_height), + position=( + self._width * 0.5 - self._scroll_width * 0.5, + self._scroll_bottom, + ), + claims_left_right=True, + selection_loops_to_parent=True, + border_opacity=0.4, + ) + + # NOTE: this stuff is modified by the _Store class. Should maybe + # clean that up. + self.button_infos = {} + self.update_buttons_timer = None + + # Show status over top. + if self._status_textwidget: + self._status_textwidget.delete() + self._status_textwidget = bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + size=(0, 0), + color=(1, 0.7, 1, 0.5), + h_align='center', + v_align='center', + text=bui.Lstr(resource=f'{self._r}.loadingText'), + maxwidth=self._scroll_width * 0.9, + ) + + # Kick off a server request. + self.request = _Request(self, tab_id) + + # Actually start the purchase locally. + def _purchase_check_result( + self, item: str, is_ticket_purchase: bool, result: dict[str, Any] | None + ) -> None: + plus = bui.app.plus + assert plus is not None + if result is None: + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='internal.unavailableNoConnectionText'), + color=(1, 0, 0), + ) + else: + if is_ticket_purchase: + if result['allow']: + price = plus.get_v1_account_misc_read_val( + 'price.' + item, None + ) + if ( + price is None + or not isinstance(price, int) + or price <= 0 + ): + print( + 'Error; got invalid local price of', + price, + 'for item', + item, + ) + bui.getsound('error').play() + else: + bui.getsound('click01').play() + plus.in_game_purchase(item, price) + else: + if result['reason'] == 'versionTooOld': + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( + resource='getTicketsWindow.versionTooOldText' + ), + color=(1, 0, 0), + ) + else: + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( + resource='getTicketsWindow.unavailableText' + ), + color=(1, 0, 0), + ) + # Real in-app purchase. + else: + if result['allow']: + plus.purchase(item) + else: + if result['reason'] == 'versionTooOld': + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( + resource='getTicketsWindow.versionTooOldText' + ), + color=(1, 0, 0), + ) + else: + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( + resource='getTicketsWindow.unavailableText' + ), + color=(1, 0, 0), + ) + + def _do_purchase_check( + self, item: str, is_ticket_purchase: bool = False + ) -> None: + app = bui.app + if app.classic is None: + logging.warning('_do_purchase_check() requires classic.') + return + + # Here we ping the server to ask if it's valid for us to + # purchase this. Better to fail now than after we've + # paid locally. + + app.classic.master_server_v1_get( + 'bsAccountPurchaseCheck', + { + 'item': item, + 'platform': app.classic.platform, + 'subplatform': app.classic.subplatform, + 'version': app.env.engine_version, + 'buildNumber': app.env.engine_build_number, + 'purchaseType': 'ticket' if is_ticket_purchase else 'real', + }, + callback=bui.WeakCall( + self._purchase_check_result, item, is_ticket_purchase + ), + ) + + def buy(self, item: str) -> None: + """Attempt to purchase the provided item.""" + from bauiv1lib.account.signin import show_sign_in_prompt + from bauiv1lib.confirm import ConfirmWindow + + assert bui.app.classic is not None + store = bui.app.classic.store + + plus = bui.app.plus + assert plus is not None + classic = bui.app.classic + assert classic is not None + + # Prevent pressing buy within a few seconds of the last press + # (gives the buttons time to disable themselves and whatnot). + curtime = bui.apptime() + if ( + self._last_buy_time is not None + and (curtime - self._last_buy_time) < 2.0 + ): + bui.getsound('error').play() + else: + if plus.accounts.primary is None: + show_sign_in_prompt() + else: + self._last_buy_time = curtime + + # Merch is a special case - just a link. + if item == 'merch': + url = bui.app.config.get('Merch Link') + if isinstance(url, str): + bui.open_url(url) + + # Pro is an actual IAP, and the rest are ticket purchases. + elif item == 'pro': + bui.getsound('click01').play() + + # Purchase either pro or pro_sale depending on whether + # there is a sale going on. + self._do_purchase_check( + 'pro' + if store.get_available_sale_time('extras') is None + else 'pro_sale' + ) + else: + price = plus.get_v1_account_misc_read_val( + 'price.' + item, None + ) + our_tickets = classic.tickets + if price is not None and our_tickets < price: + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='notEnoughTicketsText'), + color=(1, 0, 0), + ) + # gettickets.show_get_tickets_prompt() + else: + + def do_it() -> None: + self._do_purchase_check( + item, is_ticket_purchase=True + ) + + bui.getsound('swish').play() + ConfirmWindow( + bui.Lstr( + resource='store.purchaseConfirmText', + subs=[ + ( + '${ITEM}', + store.get_store_item_name_translated( + item + ), + ) + ], + ), + width=400, + height=120, + action=do_it, + ok_text=bui.Lstr( + resource='store.purchaseText', + fallback_resource='okText', + ), + ) + + def _print_already_own(self, charname: str) -> None: + bui.screenmessage( + bui.Lstr( + resource=f'{self._r}.alreadyOwnText', + subs=[('${NAME}', charname)], + ), + color=(1, 0, 0), + ) + bui.getsound('error').play() + + def update_buttons(self) -> None: + """Update our buttons.""" + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + from bauiv1 import SpecialChar + + assert bui.app.classic is not None + store = bui.app.classic.store + + plus = bui.app.plus + assert plus is not None + classic = bui.app.classic + assert classic is not None + + if not self._root_widget: + return + + sales_raw = plus.get_v1_account_misc_read_val('sales', {}) + sales = {} + try: + # Look at the current set of sales; filter any with time remaining. + for sale_item, sale_info in list(sales_raw.items()): + to_end = ( + datetime.datetime.fromtimestamp( + sale_info['e'], datetime.UTC + ) + - utc_now() + ).total_seconds() + if to_end > 0: + sales[sale_item] = { + 'to_end': to_end, + 'original_price': sale_info['op'], + } + except Exception: + logging.exception('Error parsing sales.') + + assert self.button_infos is not None + for b_type, b_info in self.button_infos.items(): + if b_type == 'merch': + purchased = False + elif b_type in ['upgrades.pro', 'pro']: + assert bui.app.classic is not None + purchased = bui.app.classic.accounts.have_pro() + else: + assert bui.app.classic is not None + purchased = b_type in bui.app.classic.purchases + + sale_opacity = 0.0 + sale_title_text: str | bui.Lstr = '' + sale_time_text: str | bui.Lstr = '' + + call: Callable | None + if purchased: + title_color = (0.8, 0.7, 0.9, 1.0) + color = (0.63, 0.55, 0.78) + extra_image_opacity = 0.5 + call = bui.WeakCall(self._print_already_own, b_info['name']) + price_text = '' + price_text_left = '' + price_text_right = '' + show_purchase_check = True + description_color: Sequence[float] = (0.4, 1.0, 0.4, 0.4) + description_color2: Sequence[float] = (0.0, 0.0, 0.0, 0.0) + price_color = (0.5, 1, 0.5, 0.3) + else: + title_color = (0.7, 0.9, 0.7, 1.0) + color = (0.4, 0.8, 0.1) + extra_image_opacity = 1.0 + call = b_info['call'] if 'call' in b_info else None + if b_type == 'merch': + price_text = '' + price_text_left = '' + price_text_right = '' + elif b_type in ['upgrades.pro', 'pro']: + sale_time = store.get_available_sale_time('extras') + if sale_time is not None: + priceraw = plus.get_price('pro') + price_text_left = ( + priceraw if priceraw is not None else '?' + ) + priceraw = plus.get_price('pro_sale') + price_text_right = ( + priceraw if priceraw is not None else '?' + ) + sale_opacity = 1.0 + price_text = '' + sale_title_text = bui.Lstr(resource='store.saleText') + sale_time_text = bui.timestring( + sale_time / 1000.0, centi=False + ) + else: + priceraw = plus.get_price('pro') + price_text = priceraw if priceraw is not None else '?' + price_text_left = '' + price_text_right = '' + else: + price = plus.get_v1_account_misc_read_val( + 'price.' + b_type, 0 + ) + + # Color the button differently if we cant afford this. + if plus.accounts.primary is not None: + if classic.tickets < price: + color = (0.6, 0.61, 0.6) + price_text = bui.charstr(bui.SpecialChar.TICKET) + str( + plus.get_v1_account_misc_read_val( + 'price.' + b_type, '?' + ) + ) + price_text_left = '' + price_text_right = '' + + # TESTING: + if b_type in sales: + sale_opacity = 1.0 + price_text_left = bui.charstr(SpecialChar.TICKET) + str( + sales[b_type]['original_price'] + ) + price_text_right = price_text + price_text = '' + sale_title_text = bui.Lstr(resource='store.saleText') + sale_time_text = bui.timestring( + sales[b_type]['to_end'], centi=False + ) + + description_color = (0.5, 1.0, 0.5) + description_color2 = (0.3, 1.0, 1.0) + price_color = (0.2, 1, 0.2, 1.0) + show_purchase_check = False + + if 'title_text' in b_info: + bui.textwidget(edit=b_info['title_text'], color=title_color) + if 'purchase_check' in b_info: + bui.imagewidget( + edit=b_info['purchase_check'], + opacity=1.0 if show_purchase_check else 0.0, + ) + if 'price_widget' in b_info: + bui.textwidget( + edit=b_info['price_widget'], + text=price_text, + color=price_color, + ) + if 'price_widget_left' in b_info: + bui.textwidget( + edit=b_info['price_widget_left'], text=price_text_left + ) + if 'price_widget_right' in b_info: + bui.textwidget( + edit=b_info['price_widget_right'], text=price_text_right + ) + if 'price_slash_widget' in b_info: + bui.imagewidget( + edit=b_info['price_slash_widget'], opacity=sale_opacity + ) + if 'sale_bg_widget' in b_info: + bui.imagewidget( + edit=b_info['sale_bg_widget'], opacity=sale_opacity + ) + if 'sale_title_widget' in b_info: + bui.textwidget( + edit=b_info['sale_title_widget'], text=sale_title_text + ) + if 'sale_time_widget' in b_info: + bui.textwidget( + edit=b_info['sale_time_widget'], text=sale_time_text + ) + if 'button' in b_info: + bui.buttonwidget( + edit=b_info['button'], color=color, on_activate_call=call + ) + if 'extra_backings' in b_info: + for bck in b_info['extra_backings']: + bui.imagewidget( + edit=bck, color=color, opacity=extra_image_opacity + ) + if 'extra_images' in b_info: + for img in b_info['extra_images']: + bui.imagewidget(edit=img, opacity=extra_image_opacity) + if 'extra_texts' in b_info: + for etxt in b_info['extra_texts']: + bui.textwidget(edit=etxt, color=description_color) + if 'extra_texts_2' in b_info: + for etxt in b_info['extra_texts_2']: + bui.textwidget(edit=etxt, color=description_color2) + if 'descriptionText' in b_info: + bui.textwidget( + edit=b_info['descriptionText'], color=description_color + ) + + def _on_response(self, data: dict[str, Any] | None) -> None: + + # clear status text.. + if self._status_textwidget: + self._status_textwidget.delete() + self._status_textwidget_update_timer = None + + if data is None: + self._status_textwidget = bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + size=(0, 0), + scale=1.3, + transition_delay=0.1, + color=(1, 0.3, 0.3, 1.0), + h_align='center', + v_align='center', + text=bui.Lstr(resource=f'{self._r}.loadErrorText'), + maxwidth=self._scroll_width * 0.9, + ) + else: + + if self._current_tab in ( + # self.TabID.EXTRAS, + self.TabID.MINIGAMES, + self.TabID.CHARACTERS, + self.TabID.MAPS, + self.TabID.ICONS, + ): + store = _Store(self, data, self._scroll_width) + assert self._scrollwidget is not None + store.instantiate( + scrollwidget=self._scrollwidget, + tab_button=self._tab_row.tabs[self._current_tab].button, + ) + # Most of our UI won't exist until this point so we need + # to explicitly restore state for selection restore to + # work. + # + # Note to self: perhaps we should *not* do this if + # significant time has passed since the window was made + # or if input commands have happened. + self.main_window_restore_shared_state() + + else: + cnt = bui.containerwidget( + parent=self._scrollwidget, + scale=1.0, + size=(self._scroll_width, self._scroll_height * 0.95), + background=False, + claims_left_right=True, + selection_loops_to_parent=True, + ) + self._status_textwidget = bui.textwidget( + parent=cnt, + position=( + self._scroll_width * 0.5, + self._scroll_height * 0.5, + ), + size=(0, 0), + scale=1.3, + transition_delay=0.1, + color=(1, 1, 0.3, 1.0), + h_align='center', + v_align='center', + text=bui.Lstr(resource=f'{self._r}.comingSoonText'), + maxwidth=self._scroll_width * 0.9, + ) + + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + @override + def main_window_should_preserve_selection(self) -> bool: + return True + + +def _check_merch_availability_in_bg_thread() -> None: + # pylint: disable=cell-var-from-loop + + # Merch is available from some countries only. Make a reasonable + # check to ask the master-server about this at launch and store the + # results. + plus = bui.app.plus + assert plus is not None + + for _i in range(15): + try: + if plus.cloud.is_connected(): + response = plus.cloud.send_message( + bacommon.cloud.MerchAvailabilityMessage() + ) + + def _store_in_logic_thread() -> None: + cfg = bui.app.config + current = cfg.get(MERCH_LINK_KEY) + if not isinstance(current, str | None): + current = None + if current != response.url: + cfg[MERCH_LINK_KEY] = response.url + cfg.commit() + + # If we successfully get a response, kick it over to the + # logic thread to store and we're done. + bui.pushcall(_store_in_logic_thread, from_other_thread=True) + return + except CommunicationError: + pass + except Exception: + logging.warning( + 'Unexpected error in merch-availability-check.', exc_info=True + ) + time.sleep(1.1934) # A bit randomized to avoid aliasing. + + +class _Store: + def __init__( + self, + store_window: StoreBrowserWindow, + sdata: dict[str, Any], + width: float, + ): + assert bui.app.classic is not None + cstore = bui.app.classic.store + + self._store_window = store_window + self._width = width + store_data = cstore.get_store_layout() + self._tab = sdata['tab'] + self._sections = copy.deepcopy(store_data[sdata['tab']]) + self._height: float | None = None + + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + + # Pre-calc a few things and add them to store-data. + for section in self._sections: + if self._tab == 'characters': + dummy_name = 'characters.foo' + elif self._tab == 'extras': + dummy_name = 'pro' + elif self._tab == 'maps': + dummy_name = 'maps.foo' + elif self._tab == 'icons': + dummy_name = 'icons.foo' + else: + dummy_name = '' + section['button_size'] = cstore.get_store_item_display_size( + dummy_name + ) + section['v_spacing'] = ( + -25 + if (self._tab == 'extras' and uiscale is bui.UIScale.SMALL) + else -17 if self._tab == 'characters' else 0 + ) + if 'title' not in section: + section['title'] = '' + section['x_offs'] = 0.0 + # section['x_offs'] = ( + # 130 + # if self._tab == 'extras' + # else 270 if self._tab == 'maps' else 0 + # ) + section['y_offs'] = ( + 20 + if ( + self._tab == 'extras' + and uiscale is bui.UIScale.SMALL + and bui.app.config.get('Merch Link') + ) + else ( + 55 + if (self._tab == 'extras' and uiscale is bui.UIScale.SMALL) + else -20 if self._tab == 'icons' else 0 + ) + ) + + def instantiate( + self, scrollwidget: bui.Widget, tab_button: bui.Widget + ) -> None: + """Create the store.""" + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + from bauiv1lib.store.item import ( + instantiate_store_item_display, + ) + + title_spacing = 40 + button_border = 20 + button_spacing = 4 + boffs_h = 0.0 + self._height = 80.0 + + # Calc total height. + for i, section in enumerate(self._sections): + if section['title'] != '': + assert self._height is not None + self._height += title_spacing + b_width, b_height = section['button_size'] + b_count = len(section['items']) + b_column_count = min( + b_count, + int(math.floor(self._width / (b_width + button_spacing))), + ) + b_row_count = int(math.ceil(b_count / b_column_count)) + b_height_total = ( + 2 * button_border + + b_row_count * b_height + + (b_row_count - 1) * section['v_spacing'] + ) + self._height += b_height_total + + assert self._height is not None + cnt2 = bui.containerwidget( + parent=scrollwidget, + scale=1.0, + size=(self._width, self._height), + background=False, + claims_left_right=True, + selection_loops_to_parent=True, + ) + v = self._height - 20 + + if self._tab == 'characters': + txt = bui.Lstr( + resource='store.howToSwitchCharactersText', + subs=[ + ( + '${SETTINGS}', + bui.Lstr(resource='inventoryText'), + ), + ( + '${PLAYER_PROFILES}', + bui.Lstr(resource='playerProfilesWindow.titleText'), + ), + ], + ) + bui.textwidget( + parent=cnt2, + text=txt, + size=(0, 0), + position=(self._width * 0.5, self._height - 28), + h_align='center', + v_align='center', + color=(0.7, 1, 0.7, 0.4), + scale=0.7, + shadow=0, + flatness=1.0, + maxwidth=700, + transition_delay=0.4, + ) + elif self._tab == 'icons': + txt = bui.Lstr( + resource='store.howToUseIconsText', + subs=[ + ( + '${SETTINGS}', + bui.Lstr(resource='mainMenu.settingsText'), + ), + ( + '${PLAYER_PROFILES}', + bui.Lstr(resource='playerProfilesWindow.titleText'), + ), + ], + ) + bui.textwidget( + parent=cnt2, + text=txt, + size=(0, 0), + position=(self._width * 0.5, self._height - 28), + h_align='center', + v_align='center', + color=(0.7, 1, 0.7, 0.4), + scale=0.7, + shadow=0, + flatness=1.0, + maxwidth=700, + transition_delay=0.4, + ) + elif self._tab == 'maps': + assert self._width is not None + assert self._height is not None + txt = bui.Lstr(resource='store.howToUseMapsText') + bui.textwidget( + parent=cnt2, + text=txt, + size=(0, 0), + position=(self._width * 0.5, self._height - 28), + h_align='center', + v_align='center', + color=(0.7, 1, 0.7, 0.4), + scale=0.7, + shadow=0, + flatness=1.0, + maxwidth=700, + transition_delay=0.4, + ) + + prev_row_buttons: list | None = None + this_row_buttons = [] + + delay = 0.3 + for section in self._sections: + if section['title'] != '': + bui.textwidget( + parent=cnt2, + position=( + self._width * 0.5, + v - title_spacing * 0.8, + ), + size=(0, 0), + scale=1.0, + transition_delay=delay, + color=(0.7, 0.9, 0.7, 1), + h_align='center', + v_align='center', + text=bui.Lstr(resource=section['title']), + maxwidth=self._width * 0.7, + ) + v -= title_spacing + delay = max(0.100, delay - 0.100) + v -= button_border + b_width, b_height = section['button_size'] + b_count = len(section['items']) + b_column_count = min( + b_count, + int(math.floor(self._width / (b_width + button_spacing))), + ) + + col = 0 + item: dict[str, Any] + assert self._store_window.button_infos is not None + for i, item_name in enumerate(section['items']): + item = self._store_window.button_infos[item_name] = {} + item['call'] = bui.WeakCall(self._store_window.buy, item_name) + boffs_h2 = section.get('x_offs', 0.0) + boffs_v2 = section.get('y_offs', 0.0) + + # Calc the diff between the space we use and + # the space available and nudge us right by + # half that to center things. + boffs_h2 += 0.5 * ( + self._width - ((b_width + button_spacing) * b_column_count) + ) + + b_pos = ( + boffs_h + boffs_h2 + (b_width + button_spacing) * col, + v - b_height + boffs_v2, + ) + instantiate_store_item_display( + item_name, + item, + idprefix=self._store_window.main_window_id_prefix, + parent_widget=cnt2, + b_pos=b_pos, + boffs_h=boffs_h, + b_width=b_width, + b_height=b_height, + boffs_h2=boffs_h2, + boffs_v2=boffs_v2, + delay=delay, + ) + btn = item['button'] + delay = max(0.1, delay - 0.1) + this_row_buttons.append(btn) + + # Wire this button to the equivalent in the + # previous row. + if prev_row_buttons is not None: + if len(prev_row_buttons) > col: + bui.widget( + edit=btn, + up_widget=prev_row_buttons[col], + ) + bui.widget( + edit=prev_row_buttons[col], + down_widget=btn, + ) + + # If we're the last button in our row, + # wire any in the previous row past + # our position to go to us if down is + # pressed. + if col + 1 == b_column_count or i == b_count - 1: + for b_prev in prev_row_buttons[col + 1 :]: + bui.widget(edit=b_prev, down_widget=btn) + else: + bui.widget(edit=btn, up_widget=prev_row_buttons[-1]) + else: + bui.widget(edit=btn, up_widget=tab_button) + + col += 1 + if col == b_column_count or i == b_count - 1: + prev_row_buttons = this_row_buttons + this_row_buttons = [] + col = 0 + v -= b_height + if i < b_count - 1: + v -= section['v_spacing'] + + v -= button_border + + # Set a timer to update these buttons periodically + # as long as we're alive (so if we buy one it will + # grey out, etc). + self._store_window.update_buttons_timer = bui.AppTimer( + 0.5, + bui.WeakCall(self._store_window.update_buttons), + repeat=True, + ) + + # Also update them immediately. + self._store_window.update_buttons() + + +class _Request: + def __init__( + self, window: StoreBrowserWindow, tab_id: StoreBrowserWindow.TabID + ): + self._window = weakref.ref(window) + data = {'tab': tab_id.value} + bui.apptimer(0.1, bui.WeakCall(self._on_response, data)) + + def _on_response(self, data: dict[str, Any] | None) -> None: + # FIXME: clean this up. + # pylint: disable=protected-access + window = self._window() + if window is not None and (window.request is self): + window.request = None + window._on_response(data) + + +# Slight hack; start checking merch availability in the bg (but only if +# it looks like we've been imported for use in a running app; don't want +# to do this during docs generation/etc.) + +# NOTE: Disabling this for now since we're not showing the merch section +# (and want to purge all use of daemon threads). + +# TODO: Should wire this up explicitly to app bootstrapping; not good to +# be kicking off work at module import time. +if ( + bool(False) + and os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') != '1' + and bui.app.state is not bui.AppState.NOT_STARTED +): + Thread(target=_check_merch_availability_in_bg_thread).start() diff --git a/dist/ba_data/python/bauiv1lib/store/item.py b/dist/ba_data/python/bauiv1lib/store/item.py new file mode 100644 index 0000000..f598256 --- /dev/null +++ b/dist/ba_data/python/bauiv1lib/store/item.py @@ -0,0 +1,729 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to UI items.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import bascenev1 as bs +import bauiv1 as bui + +if TYPE_CHECKING: + from typing import Any + + +def instantiate_store_item_display( + item_name: str, + item: dict[str, Any], + *, + parent_widget: bui.Widget, + b_pos: tuple[float, float], + b_width: float, + b_height: float, + idprefix: str, + boffs_h: float = 0.0, + boffs_h2: float = 0.0, + boffs_v2: float = 0, + delay: float = 0.0, + button: bool = True, +) -> None: + """(internal)""" + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + assert bui.app.classic is not None + store = bui.app.classic.store + + plus = bui.app.plus + assert plus is not None + + del boffs_h # unused arg + del boffs_h2 # unused arg + del boffs_v2 # unused arg + item_info = store.get_store_item(item_name) + title_v = 0.24 + price_v = 0.145 + base_text_scale = 1.0 + + item['name'] = title = store.get_store_item_name_translated(item_name) + + btn: bui.Widget | None + + # Hack; showbuffer stuff isn't working well when we're showing merch. + showbuffer = 10 if item_name in {'merch', 'pro', 'pro_sale'} else 76.0 + + if button: + item['button'] = btn = bui.buttonwidget( + parent=parent_widget, + id=f'{idprefix}|store_item.{item_name}', + position=b_pos, + transition_delay=delay, + show_buffer_top=showbuffer, + enable_sound=False, + button_type='square', + size=(b_width, b_height), + autoselect=True, + label='', + ) + bui.widget(edit=btn, show_buffer_bottom=showbuffer) + else: + btn = None + + b_offs_x = -0.015 * b_width + check_pos = 0.76 + + icon_tex = None + tint_tex = None + tint_color = None + tint2_color = None + tex_name: str | None = None + desc: bui.Lstr | None = None + modes: bui.Lstr | None = None + + if item_name.startswith('characters.'): + assert bui.app.classic is not None + character = bui.app.classic.spaz_appearances[item_info['character']] + tint_color = ( + item_info['color'] + if 'color' in item_info + else ( + character.default_color + if character.default_color is not None + else (1, 1, 1) + ) + ) + tint2_color = ( + item_info['highlight'] + if 'highlight' in item_info + else ( + character.default_highlight + if character.default_highlight is not None + else (1, 1, 1) + ) + ) + icon_tex = character.icon_texture + tint_tex = character.icon_mask_texture + title_v = 0.255 + price_v = 0.145 + elif item_name == 'merch': + base_text_scale = 0.6 + title_v = 0.85 + price_v = 0.15 + elif item_name in ['upgrades.pro', 'pro']: + base_text_scale = 0.6 + title_v = 0.85 + price_v = 0.15 + elif item_name.startswith('maps.'): + map_type = item_info['map_type'] + tex_name = map_type.get_preview_texture_name() + title_v = 0.312 + price_v = 0.17 + + elif item_name.startswith('games.'): + gametype = item_info['gametype'] + modes_l = [] + if gametype.supports_session_type(bs.CoopSession): + modes_l.append(bui.Lstr(resource='playModes.coopText')) + if gametype.supports_session_type(bs.DualTeamSession): + modes_l.append(bui.Lstr(resource='playModes.teamsText')) + if gametype.supports_session_type(bs.FreeForAllSession): + modes_l.append(bui.Lstr(resource='playModes.freeForAllText')) + + if len(modes_l) == 3: + modes = bui.Lstr( + value='${A}, ${B}, ${C}', + subs=[ + ('${A}', modes_l[0]), + ('${B}', modes_l[1]), + ('${C}', modes_l[2]), + ], + ) + elif len(modes_l) == 2: + modes = bui.Lstr( + value='${A}, ${B}', + subs=[('${A}', modes_l[0]), ('${B}', modes_l[1])], + ) + elif len(modes_l) == 1: + modes = modes_l[0] + else: + raise RuntimeError() + desc = gametype.get_description_display_string(bs.CoopSession) + tex_name = item_info['previewTex'] + base_text_scale = 0.8 + title_v = 0.48 + price_v = 0.17 + elif item_name == 'upgrades.infinite_runaround': + base_text_scale = 0.8 + desc = bui.Lstr( + translate=( + 'gameDescriptions', + 'Prevent enemies from reaching the exit.', + ) + ) + modes = bui.Lstr(resource='playModes.coopText') + tex_name = 'towerDPreview' + title_v = 0.48 + price_v = 0.17 + elif item_name == 'upgrades.infinite_onslaught': + base_text_scale = 0.8 + desc = bui.Lstr( + translate=( + 'gameDescriptions', + 'Defeat all enemies.', + ) + ) + modes = bui.Lstr(resource='playModes.coopText') + tex_name = 'doomShroomPreview' + title_v = 0.48 + price_v = 0.17 + + elif item_name.startswith('icons.'): + base_text_scale = 1.5 + price_v = 0.2 + check_pos = 0.6 + + if item_name.startswith('characters.'): + frame_size = b_width * 0.7 + im_dim = frame_size * (100.0 / 113.0) + im_pos = ( + b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.57 - im_dim * 0.5, + ) + mask_texture = bui.gettexture('characterIconMask') + assert icon_tex is not None + assert tint_tex is not None + bui.imagewidget( + parent=parent_widget, + position=im_pos, + size=(im_dim, im_dim), + color=(1, 1, 1), + transition_delay=delay, + mask_texture=mask_texture, + draw_controller=btn, + texture=bui.gettexture(icon_tex), + tint_texture=bui.gettexture(tint_tex), + tint_color=tint_color, + tint2_color=tint2_color, + ) + + if item_name == 'merch': + frame_size = b_width * 0.65 + im_dim = frame_size * (100.0 / 113.0) + im_pos = ( + b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.47 - im_dim * 0.5, + ) + bui.imagewidget( + parent=parent_widget, + position=im_pos, + size=(im_dim, im_dim), + transition_delay=delay, + draw_controller=btn, + opacity=1.0, + texture=bui.gettexture('merch'), + ) + + if item_name in ['pro', 'upgrades.pro']: + frame_size = b_width * 0.5 + im_dim = frame_size * (100.0 / 113.0) + im_pos = ( + b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.5 - im_dim * 0.5, + ) + bui.imagewidget( + parent=parent_widget, + position=im_pos, + size=(im_dim, im_dim), + transition_delay=delay, + draw_controller=btn, + color=(0.3, 0.0, 0.3), + opacity=0.3, + texture=bui.gettexture('logo'), + ) + txt = bui.Lstr(resource='store.bombSquadProNewDescriptionText') + + item['descriptionText'] = bui.textwidget( + parent=parent_widget, + text=txt, + position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.69), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale * 0.75, + maxwidth=b_width * 0.75, + max_height=b_height * 0.2, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + color=(0.3, 1, 0.3), + ) + + extra_backings = item['extra_backings'] = [] + extra_images = item['extra_images'] = [] + extra_texts = item['extra_texts'] = [] + extra_texts_2 = item['extra_texts_2'] = [] + + backing_color = (0.5, 0.8, 0.3) if button else (0.6, 0.5, 0.65) + b_square_texture = bui.gettexture('buttonSquare') + char_mask_texture = bui.gettexture('characterIconMask') + + pos = (0.17, 0.43) + tile_size = (b_width * 0.16 * 1.2, b_width * 0.2 * 1.2) + tile_pos = (b_pos[0] + b_width * pos[0], b_pos[1] + b_height * pos[1]) + extra_backings.append( + bui.imagewidget( + parent=parent_widget, + position=( + tile_pos[0] - tile_size[0] * 0.5, + tile_pos[1] - tile_size[1] * 0.5, + ), + size=tile_size, + transition_delay=delay, + draw_controller=btn, + color=backing_color, + texture=b_square_texture, + ) + ) + im_size = tile_size[0] * 0.8 + extra_images.append( + bui.imagewidget( + parent=parent_widget, + position=( + tile_pos[0] - im_size * 0.5, + tile_pos[1] - im_size * 0.4, + ), + size=(im_size, im_size), + transition_delay=delay, + draw_controller=btn, + color=(1, 1, 1), + texture=bui.gettexture('ticketsMore'), + ) + ) + bonus_tickets = str( + plus.get_v1_account_misc_read_val('proBonusTickets', 100) + ) + extra_texts.append( + bui.textwidget( + parent=parent_widget, + draw_controller=btn, + position=( + tile_pos[0] - tile_size[0] * 0.03, + tile_pos[1] - tile_size[1] * 0.25, + ), + size=(0, 0), + color=(0.6, 1, 0.6), + transition_delay=delay, + h_align='center', + v_align='center', + maxwidth=tile_size[0] * 0.7, + scale=0.55, + text=bui.Lstr( + resource='getTicketsWindow.ticketsText', + subs=[('${COUNT}', bonus_tickets)], + ), + flatness=1.0, + shadow=0.0, + ) + ) + + for charname, pos in [ + ('Kronk', (0.32, 0.45)), + ('Zoe', (0.425, 0.4)), + ('Jack Morgan', (0.555, 0.45)), + ('Mel', (0.645, 0.4)), + ]: + tile_size = (b_width * 0.16 * 0.9, b_width * 0.2 * 0.9) + tile_pos = ( + b_pos[0] + b_width * pos[0], + b_pos[1] + b_height * pos[1], + ) + assert bui.app.classic is not None + character = bui.app.classic.spaz_appearances[charname] + extra_backings.append( + bui.imagewidget( + parent=parent_widget, + position=( + tile_pos[0] - tile_size[0] * 0.5, + tile_pos[1] - tile_size[1] * 0.5, + ), + size=tile_size, + transition_delay=delay, + draw_controller=btn, + color=backing_color, + texture=b_square_texture, + ) + ) + im_size = tile_size[0] * 0.7 + extra_images.append( + bui.imagewidget( + parent=parent_widget, + position=( + tile_pos[0] - im_size * 0.53, + tile_pos[1] - im_size * 0.35, + ), + size=(im_size, im_size), + transition_delay=delay, + draw_controller=btn, + color=(1, 1, 1), + texture=bui.gettexture(character.icon_texture), + tint_texture=bui.gettexture(character.icon_mask_texture), + tint_color=character.default_color, + tint2_color=character.default_highlight, + mask_texture=char_mask_texture, + ) + ) + extra_texts.append( + bui.textwidget( + parent=parent_widget, + draw_controller=btn, + position=( + tile_pos[0] - im_size * 0.03, + tile_pos[1] - im_size * 0.51, + ), + size=(0, 0), + color=(0.6, 1, 0.6), + transition_delay=delay, + h_align='center', + v_align='center', + maxwidth=tile_size[0] * 0.7, + scale=0.55, + text=bui.Lstr(translate=('characterNames', charname)), + flatness=1.0, + shadow=0.0, + ) + ) + + # If we have a 'total-worth' item-id for this id, show that price so + # the user knows how much this is worth. + total_worth_item = plus.get_v1_account_misc_read_val('twrths', {}).get( + item_name + ) + total_worth_price: str | None + if total_worth_item is not None: + price = plus.get_price(total_worth_item) + total_worth_price = ( + store.get_clean_price(price) if price is not None else '??' + ) + else: + total_worth_price = None + + if total_worth_price is not None: + total_worth_text = bui.Lstr( + resource='store.totalWorthText', + subs=[('${TOTAL_WORTH}', total_worth_price)], + ) + extra_texts_2.append( + bui.textwidget( + parent=parent_widget, + text=total_worth_text, + position=( + b_pos[0] + b_width * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.25, + ), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale * 0.45, + maxwidth=b_width * 0.5, + size=(0, 0), + h_align='center', + v_align='center', + shadow=1.0, + flatness=1.0, + draw_controller=btn, + color=(0.3, 1, 1), + ) + ) + + mesh_opaque = bui.getmesh('level_select_button_opaque') + mesh_transparent = bui.getmesh('level_select_button_transparent') + mask_tex = bui.gettexture('mapPreviewMask') + for levelname, preview_tex_name, pos in [ + ('Infinite Onslaught', 'doomShroomPreview', (0.80, 0.48)), + ('Infinite Runaround', 'towerDPreview', (0.80, 0.32)), + ]: + tile_size = (b_width * 0.2, b_width * 0.13) + tile_pos = ( + b_pos[0] + b_width * pos[0], + b_pos[1] + b_height * pos[1], + ) + im_size = tile_size[0] * 0.8 + extra_backings.append( + bui.imagewidget( + parent=parent_widget, + position=( + tile_pos[0] - tile_size[0] * 0.5, + tile_pos[1] - tile_size[1] * 0.5, + ), + size=tile_size, + transition_delay=delay, + draw_controller=btn, + color=backing_color, + texture=b_square_texture, + ) + ) + + # Hack - gotta draw two transparent versions to avoid z issues. + for mod in mesh_opaque, mesh_transparent: + extra_images.append( + bui.imagewidget( + parent=parent_widget, + position=( + tile_pos[0] - im_size * 0.52, + tile_pos[1] - im_size * 0.2, + ), + size=(im_size, im_size * 0.5), + transition_delay=delay, + mesh_transparent=mod, + mask_texture=mask_tex, + draw_controller=btn, + texture=bui.gettexture(preview_tex_name), + ) + ) + + extra_texts.append( + bui.textwidget( + parent=parent_widget, + draw_controller=btn, + position=( + tile_pos[0] - im_size * 0.03, + tile_pos[1] - im_size * 0.2, + ), + size=(0, 0), + color=(0.6, 1, 0.6), + transition_delay=delay, + h_align='center', + v_align='center', + maxwidth=tile_size[0] * 0.7, + scale=0.55, + text=bui.Lstr(translate=('coopLevelNames', levelname)), + flatness=1.0, + shadow=0.0, + ) + ) + + if item_name.startswith('icons.'): + item['icon_text'] = bui.textwidget( + parent=parent_widget, + text=item_info['icon'], + position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.5), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale * 2.0, + maxwidth=b_width * 0.9, + max_height=b_height * 0.9, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + ) + + if item_name.startswith('maps.'): + frame_size = b_width * 0.9 + im_dim = frame_size * (100.0 / 113.0) + im_pos = ( + b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.62 - im_dim * 0.25, + ) + mesh_opaque = bui.getmesh('level_select_button_opaque') + mesh_transparent = bui.getmesh('level_select_button_transparent') + mask_tex = bui.gettexture('mapPreviewMask') + assert tex_name is not None + bui.imagewidget( + parent=parent_widget, + position=im_pos, + size=(im_dim, im_dim * 0.5), + transition_delay=delay, + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, + mask_texture=mask_tex, + draw_controller=btn, + texture=bui.gettexture(tex_name), + ) + + if item_name.startswith('games.') or item_name in ( + 'upgrades.infinite_runaround', + 'upgrades.infinite_onslaught', + ): + frame_size = b_width * 0.8 + im_dim = frame_size * (100.0 / 113.0) + im_pos = ( + b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.72 - im_dim * 0.25, + ) + mesh_opaque = bui.getmesh('level_select_button_opaque') + mesh_transparent = bui.getmesh('level_select_button_transparent') + mask_tex = bui.gettexture('mapPreviewMask') + assert tex_name is not None + bui.imagewidget( + parent=parent_widget, + position=im_pos, + size=(im_dim, im_dim * 0.5), + transition_delay=delay, + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, + mask_texture=mask_tex, + draw_controller=btn, + texture=bui.gettexture(tex_name), + ) + item['descriptionText'] = bui.textwidget( + parent=parent_widget, + text=desc, + position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.36), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale * 0.78, + maxwidth=b_width * 0.8, + max_height=b_height * 0.14, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + flatness=1.0, + shadow=0.0, + color=(0.6, 1, 0.6), + ) + item['gameModesText'] = bui.textwidget( + parent=parent_widget, + text=modes, + position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.26), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale * 0.65, + maxwidth=b_width * 0.8, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + shadow=0, + flatness=1.0, + color=(0.6, 0.8, 0.6), + ) + + if not item_name.startswith('icons.'): + item['title_text'] = bui.textwidget( + parent=parent_widget, + text=title, + position=( + b_pos[0] + b_width * 0.5 + b_offs_x, + b_pos[1] + b_height * title_v, + ), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale, + maxwidth=b_width * 0.8, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + color=(0.7, 0.9, 0.7, 1.0), + ) + + item['purchase_check'] = bui.imagewidget( + parent=parent_widget, + position=(b_pos[0] + b_width * check_pos, b_pos[1] + b_height * 0.05), + transition_delay=delay, + mesh_transparent=bui.getmesh('checkTransparent'), + opacity=0.0, + size=(60, 60), + color=(0.6, 0.5, 0.8), + draw_controller=btn, + texture=bui.gettexture('uiAtlas'), + ) + item['price_widget'] = bui.textwidget( + parent=parent_widget, + text='', + position=( + b_pos[0] + b_width * 0.5 + b_offs_x, + b_pos[1] + b_height * price_v, + ), + transition_delay=delay, + scale=b_width * (1.0 / 300.0) * base_text_scale, + maxwidth=b_width * 0.9, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + color=(0.2, 1, 0.2, 1.0), + ) + item['price_widget_left'] = bui.textwidget( + parent=parent_widget, + text='', + position=( + b_pos[0] + b_width * 0.33 + b_offs_x, + b_pos[1] + b_height * price_v, + ), + transition_delay=delay, + scale=b_width * (1.0 / 300.0) * base_text_scale, + maxwidth=b_width * 0.3, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + color=(0.2, 1, 0.2, 0.5), + ) + item['price_widget_right'] = bui.textwidget( + parent=parent_widget, + text='', + position=( + b_pos[0] + b_width * 0.66 + b_offs_x, + b_pos[1] + b_height * price_v, + ), + transition_delay=delay, + scale=1.1 * b_width * (1.0 / 300.0) * base_text_scale, + maxwidth=b_width * 0.3, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + color=(0.2, 1, 0.2, 1.0), + ) + item['price_slash_widget'] = bui.imagewidget( + parent=parent_widget, + position=( + b_pos[0] + b_width * 0.33 + b_offs_x - 36, + b_pos[1] + b_height * price_v - 35, + ), + transition_delay=delay, + texture=bui.gettexture('slash'), + opacity=0.0, + size=(70, 70), + draw_controller=btn, + color=(1, 0, 0), + ) + badge_rad = 44 + badge_center = ( + b_pos[0] + b_width * 0.1 + b_offs_x, + b_pos[1] + b_height * 0.87, + ) + item['sale_bg_widget'] = bui.imagewidget( + parent=parent_widget, + position=(badge_center[0] - badge_rad, badge_center[1] - badge_rad), + opacity=0.0, + transition_delay=delay, + texture=bui.gettexture('circleZigZag'), + draw_controller=btn, + size=(badge_rad * 2, badge_rad * 2), + color=(0.5, 0, 1), + ) + item['sale_title_widget'] = bui.textwidget( + parent=parent_widget, + position=(badge_center[0], badge_center[1] + 12), + transition_delay=delay, + scale=1.0, + maxwidth=badge_rad * 1.6, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + shadow=0.0, + flatness=1.0, + color=(0, 1, 0), + ) + item['sale_time_widget'] = bui.textwidget( + parent=parent_widget, + position=(badge_center[0], badge_center[1] - 12), + transition_delay=delay, + scale=0.7, + maxwidth=badge_rad * 1.6, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + shadow=0.0, + flatness=1.0, + color=(0.0, 1, 0.0, 1), + ) diff --git a/dist/ba_data/python/bauiv1lib/tabs.py b/dist/ba_data/python/bauiv1lib/tabs.py index a4d85c3..a3d298d 100644 --- a/dist/ba_data/python/bauiv1lib/tabs.py +++ b/dist/ba_data/python/bauiv1lib/tabs.py @@ -62,7 +62,7 @@ class TabRow[T: Enum]: size=size, label=tab_label, enable_sound=False, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._tick_and_call, on_select_call, tab_id ), ) diff --git a/dist/ba_data/python/bauiv1lib/teamnamescolors.py b/dist/ba_data/python/bauiv1lib/teamnamescolors.py index ed90fb7..4e4622e 100644 --- a/dist/ba_data/python/bauiv1lib/teamnamescolors.py +++ b/dist/ba_data/python/bauiv1lib/teamnamescolors.py @@ -73,7 +73,7 @@ class TeamNamesColorsWindow(PopupWindow): id=f'{self._idprefix}|colorbutton{i}', autoselect=True, position=(50, 0 + 195 - 90 * i), - on_activate_call=bui.CallStrict(self._color_click, i), + on_activate_call=bui.Call(self._color_click, i), size=(70, 70), color=self._colors[i], label='', diff --git a/dist/ba_data/python/bauiv1lib/template.py b/dist/ba_data/python/bauiv1lib/template.py index 9687d8e..c9e25cb 100644 --- a/dist/ba_data/python/bauiv1lib/template.py +++ b/dist/ba_data/python/bauiv1lib/template.py @@ -212,7 +212,7 @@ class TemplateMainWindow(bui.MainWindow): autoselect=True, size=(button_width, 60), label=f'Template{child_dummy_data}', - on_activate_call=bui.WeakCallStrict( + on_activate_call=bui.WeakCall( self._child_press, child_dummy_data ), ) diff --git a/dist/ba_data/python/bauiv1lib/tournamententry.py b/dist/ba_data/python/bauiv1lib/tournamententry.py index 1717a53..b09cf5a 100644 --- a/dist/ba_data/python/bauiv1lib/tournamententry.py +++ b/dist/ba_data/python/bauiv1lib/tournamententry.py @@ -7,10 +7,8 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, override -from bacommon.analytics import ClassicAnalyticsEvent -import bauiv1 as bui - from bauiv1lib.popup import PopupWindow +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable @@ -363,7 +361,7 @@ class TournamentEntryWindow(PopupWindow): self._fg_state = bui.app.fg_state self._running_query = False self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self._update), repeat=True + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() self._restore_state() @@ -435,9 +433,7 @@ class TournamentEntryWindow(PopupWindow): else 'retry entry window' ) }, - callback=bui.WeakCallPartial( - self._on_tournament_query_response - ), + callback=bui.WeakCall(self._on_tournament_query_response), ) self._last_query_time = bui.apptime() self._running_query = True @@ -592,13 +588,6 @@ class TournamentEntryWindow(PopupWindow): self._launched = True launched = False - bui.app.analytics.submit_event( - ClassicAnalyticsEvent( - ClassicAnalyticsEvent.EventType.START_TOURNEY_COOP_SESSION, - extra=self._tournament_info.get('game'), - ) - ) - # If they gave us an existing, non-consistent practice activity, # just restart it. if ( @@ -762,7 +751,7 @@ class TournamentEntryWindow(PopupWindow): assert bui.app.plus is not None bui.app.plus.ads.show_ad_2( 'tournament_entry', - on_completion_call=bui.WeakCallPartial(self._on_ad_complete), + on_completion_call=bui.WeakCall(self._on_ad_complete), ) def _on_practice_press(self) -> None: diff --git a/dist/ba_data/python/bauiv1lib/tournamentscores.py b/dist/ba_data/python/bauiv1lib/tournamentscores.py index e334742..227e1c6 100644 --- a/dist/ba_data/python/bauiv1lib/tournamentscores.py +++ b/dist/ba_data/python/bauiv1lib/tournamentscores.py @@ -114,7 +114,7 @@ class TournamentScoresWindow(PopupWindow): 'numScores': 50, 'source': 'scores window', }, - callback=bui.WeakCallPartial(self._on_tournament_query_response), + callback=bui.WeakCall(self._on_tournament_query_response), ) def _on_tournament_query_response( @@ -200,7 +200,7 @@ class TournamentScoresWindow(PopupWindow): bui.textwidget( edit=txt, - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._show_player_info, entry, txt ), ) diff --git a/dist/ba_data/python/bauiv1lib/watch.py b/dist/ba_data/python/bauiv1lib/watch.py index 35375f3..2b8d923 100644 --- a/dist/ba_data/python/bauiv1lib/watch.py +++ b/dist/ba_data/python/bauiv1lib/watch.py @@ -405,7 +405,7 @@ class WatchWindow(bui.MainWindow): bs.new_host_session(mainmenu.MainMenuSession) - bui.fade_screen(False, endcall=bui.CallStrict(bui.pushcall, do_it)) + bui.fade_screen(False, endcall=bui.Call(bui.pushcall, do_it)) bui.containerwidget(edit=self._root_widget, transition='out_left') def _on_my_replay_rename_press(self) -> None: @@ -458,7 +458,7 @@ class WatchWindow(bui.MainWindow): parent=cnt, id=f'{self.main_window_id_prefix}|replayrenamecancel', label=bui.Lstr(resource='cancelText'), - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( lambda c: bui.containerwidget(edit=c, transition='out_scale'), cnt, ), @@ -472,7 +472,7 @@ class WatchWindow(bui.MainWindow): label=bui.Lstr(resource=f'{self._r}.renameText'), size=(180, 60), position=(c_width - 230, 30), - on_activate_call=bui.CallStrict( + on_activate_call=bui.Call( self._rename_my_replay, self._my_replay_selected ), autoselect=True, @@ -558,7 +558,7 @@ class WatchWindow(bui.MainWindow): ) ], ), - bui.CallStrict(self._delete_replay, self._my_replay_selected), + bui.Call(self._delete_replay, self._my_replay_selected), width=450, height=150, ) @@ -616,7 +616,7 @@ class WatchWindow(bui.MainWindow): (1.0, 1, 0.4) if name == '__lastReplay.brp' else (1, 1, 1) ), always_highlight=True, - on_select_call=bui.CallStrict(self._on_my_replay_select, name), + on_select_call=bui.Call(self._on_my_replay_select, name), on_activate_call=self._my_replays_watch_replay_button.activate, text=self._get_replay_display_name(name), h_align='left', diff --git a/dist/ba_data/python/efro/dataclassio/_base.py b/dist/ba_data/python/efro/dataclassio/_base.py index 51e7fde..c8a3969 100644 --- a/dist/ba_data/python/efro/dataclassio/_base.py +++ b/dist/ba_data/python/efro/dataclassio/_base.py @@ -93,28 +93,21 @@ class IOMultiType[EnumT: Enum]: """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 type-specific data for subclasses - (``MessagePing``, ``MessageChat``, etc.) + 'Message' type) in annotations, with dataclassio automatically + serializing & deserializing dataclass subclasses based on their type + ('MessagePing', 'MessageChat', etc.) - Standard usage involves a 'registry' class inheriting from this one - and dataclass classes inheriting from that registry class. - Dataclassio will then do the right thing when that registry class is - used in type annotations. + 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. - For an example multitype class (useful to use as a starting point - for your own) see :class:`efro.dataclassio.templatemultitype`. + See tests/test_efro/test_dataclassio.py for examples. """ @classmethod def get_type(cls, type_id: EnumT) -> type[Self]: - """Return a specific subclass given a type-id. - - Should be overridden by child classes. Generally, users of the - class should call - :meth:`~efro.dataclassio.IOMultiType.get_type_cached()` instead - of this, as it is more efficient. - """ + """Return a specific subclass given a type-id.""" raise NotImplementedError() @final @@ -227,10 +220,6 @@ class IOAttrs: #: minute boundaries (see :meth:`efro.util.utc_this_minute()`). whole_minutes: bool = False - #: If ``True``, requires ``datetime.datetime`` values to lie exactly on - #: second boundaries (see :meth:`efro.util.utc_this_second()`). - whole_seconds: bool = False - #: If ``True``, values of type ``datetime.datetime`` (in json codec) #: and ``datetime.timedelta`` (in all codecs) will be stored as single #: float timestamp/seconds values instead of the default list of @@ -267,12 +256,6 @@ class IOAttrs: #: editing the value. Does not actually affect value input/output. multiline: bool | None = None - #: If provided for a string, hints whether the value should be - #: edited as distinct options in something like a popup menu instead - #: of as a text field. Can be referenced when creating UI for - #: editing the value. Does not actually affect value input/output. - edit_as_options: bool | None = None - def __init__( self, storagename: str | None = storagename, @@ -281,16 +264,12 @@ class IOAttrs: whole_days: bool = whole_days, whole_hours: bool = whole_hours, whole_minutes: bool = whole_minutes, - whole_seconds: bool = whole_seconds, float_times: bool = float_times, soft_default: Any = MISSING, soft_default_factory: Callable[[], Any] | _MissingType = MISSING, enum_fallback: Enum | None = None, multiline: bool | None = None, - edit_as_options: bool | None = None, ): - # pylint: disable=too-many-branches - # Only store values that differ from class defaults to keep # our instances nice and lean. cls = type(self) @@ -304,8 +283,6 @@ class IOAttrs: self.whole_hours = whole_hours if whole_minutes != cls.whole_minutes: self.whole_minutes = whole_minutes - if whole_seconds != cls.whole_seconds: - self.whole_seconds = whole_seconds if float_times != cls.float_times: self.float_times = float_times if soft_default is not cls.soft_default: @@ -327,8 +304,6 @@ class IOAttrs: self.enum_fallback = enum_fallback if multiline is not cls.multiline: self.multiline = multiline - if edit_as_options is not cls.multiline: - self.edit_as_options = edit_as_options def validate_for_field(self, cls: type, field: dataclasses.Field) -> None: """Ensure the IOAttrs is ok to use with provided field.""" @@ -346,7 +321,7 @@ class IOAttrs: and self.soft_default_factory is self.MISSING ): raise TypeError( - f'Field \'{field.name}\' of {cls} has' + f'Field {field.name} of {cls} has' f' neither a default nor a default_factory' f' and IOAttrs contains neither a soft_default' f' nor a soft_default_factory;' @@ -382,11 +357,6 @@ class IOAttrs: raise ValueError( f'Value {value} at {fieldpath}' f' is not a whole minute.' ) - elif self.whole_seconds: - if any(x != 0 for x in (value.microsecond,)): - raise ValueError( - f'Value {value} at {fieldpath}' f' is not a whole second.' - ) class TypeNotPresentError(TypeError): diff --git a/dist/ba_data/python/efro/dataclassio/_prep.py b/dist/ba_data/python/efro/dataclassio/_prep.py index baf1d87..4e9cd1d 100644 --- a/dist/ba_data/python/efro/dataclassio/_prep.py +++ b/dist/ba_data/python/efro/dataclassio/_prep.py @@ -266,180 +266,176 @@ class PrepSession: # pylint: disable=too-many-branches # pylint: disable=too-many-statements - if not TYPE_CHECKING: + if recursion_level > MAX_RECURSION: + raise RuntimeError('Max recursion exceeded.') - if recursion_level > MAX_RECURSION: - raise RuntimeError('Max recursion exceeded.') + origin = _get_origin(anntype) - 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 - # 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 + if origin is typing.Union or origin is types.UnionType: + self.prep_union( + cls, attrname, anntype, recursion_level=recursion_level + 1 + ) + return - if origin is typing.Union or origin is types.UnionType: - self.prep_union( - cls, attrname, anntype, recursion_level=recursion_level + 1 - ) - return + if anntype is typing.Any: + return - if anntype is typing.Any: - return + # Everything below this point assumes the annotation type + # resolves to a concrete type. + if not isinstance(origin, type): + raise TypeError( + f'Unsupported type found for \'{attrname}\' on {cls}:' + f' {anntype}' + ) - # Everything below this point assumes the annotation type - # resolves to a concrete type. - if not isinstance(origin, type): - raise TypeError( - f'Unsupported type found for \'{attrname}\' on {cls}:' - f' {anntype}' - ) + # If a soft_default value/factory was passed, we do some basic + # type checking on the top-level value here. We also run full + # recursive validation on values later during inputting, but + # this should catch at least some errors early on, which can be + # useful since soft_defaults are not static type checked. + if ioattrs is not None: + have_soft_default = False + soft_default: Any = None + if ioattrs.soft_default is not ioattrs.MISSING: + have_soft_default = True + soft_default = ioattrs.soft_default + elif ioattrs.soft_default_factory is not ioattrs.MISSING: + assert callable(ioattrs.soft_default_factory) + have_soft_default = True + soft_default = ioattrs.soft_default_factory() - # If a soft_default value/factory was passed, we do some - # basic type checking on the top-level value here. We also - # run full recursive validation on values later during - # inputting, but this should catch at least some errors - # early on, which can be useful since soft_defaults are not - # static type checked. - if ioattrs is not None: - have_soft_default = False - soft_default: Any = None - if ioattrs.soft_default is not ioattrs.MISSING: - have_soft_default = True - soft_default = ioattrs.soft_default - elif ioattrs.soft_default_factory is not ioattrs.MISSING: - assert callable(ioattrs.soft_default_factory) - have_soft_default = True - soft_default = ioattrs.soft_default_factory() - - # Do a simple type check for the top level to catch basic - # soft_default mismatches early; full check will happen at - # input time. - if have_soft_default: - if not isinstance(soft_default, origin): - raise TypeError( - f'{cls} attr {attrname} has type {origin}' - f' but soft_default value is type' - f' {type(soft_default)}' - ) - - if origin in SIMPLE_TYPES: - return - - # For sets and lists, check out their single contained type (if - # any). - if origin in (list, set): - childtypes = typing.get_args(anntype) - if len(childtypes) == 0: - # This is equivalent to Any; nothing else needs - # checking. - return - if len(childtypes) > 1: + # Do a simple type check for the top level to catch basic + # soft_default mismatches early; full check will happen at + # input time. + if have_soft_default: + if not isinstance(soft_default, origin): raise TypeError( - f'Unrecognized typing arg count {len(childtypes)}' - f" for {anntype} attr '{attrname}' on {cls}" + f'{cls} attr {attrname} has type {origin}' + f' but soft_default value is type {type(soft_default)}' ) + + if origin in SIMPLE_TYPES: + return + + # For sets and lists, check out their single contained type (if + # any). + if origin in (list, set): + childtypes = typing.get_args(anntype) + if len(childtypes) == 0: + # This is equivalent to Any; nothing else needs + # checking. + return + if len(childtypes) > 1: + raise TypeError( + f'Unrecognized typing arg count {len(childtypes)}' + f" for {anntype} attr '{attrname}' on {cls}" + ) + self.prep_type( + cls, + attrname, + childtypes[0], + ioattrs=None, + recursion_level=recursion_level + 1, + ) + return + + if origin is dict: + childtypes = typing.get_args(anntype) + assert len(childtypes) in (0, 2) + + # For key types we support Any, str, int, + # and Enums with uniform str/int values. + if not childtypes or childtypes[0] is typing.Any: + # 'Any' needs no further checks (just checked + # per-instance). + pass + elif childtypes[0] in (str, int): + # str and int are all good as keys. + pass + elif issubclass(childtypes[0], Enum): + # Allow our usual str or int enum types as keys. + self.prep_enum(childtypes[0], ioattrs=None) + else: + raise TypeError( + f'Dict key type {childtypes[0]} for \'{attrname}\'' + f' on {cls.__name__} is not supported by dataclassio.' + ) + + # For value types we support any of our normal types. + if not childtypes or _get_origin(childtypes[1]) is typing.Any: + # 'Any' needs no further checks (just checked + # per-instance). + pass + else: self.prep_type( cls, attrname, - childtypes[0], + childtypes[1], ioattrs=None, recursion_level=recursion_level + 1, ) - return + return - if origin is dict: - childtypes = typing.get_args(anntype) - assert len(childtypes) in (0, 2) + # For Tuples, simply check individual member types. (and, for + # now, explicitly disallow zero member types or usage of + # ellipsis) + if origin is tuple: + childtypes = typing.get_args(anntype) + if not childtypes: + raise TypeError( + f'Tuple at \'{attrname}\'' + f' has no type args; dataclassio requires type args.' + ) + if childtypes[-1] is ...: + raise TypeError( + f'Found ellipsis as part of type for' + f' \'{attrname}\' on {cls.__name__};' + f' these are not' + f' supported by dataclassio.' + ) + for childtype in childtypes: + self.prep_type( + cls, + attrname, + childtype, + ioattrs=None, + recursion_level=recursion_level + 1, + ) + return - # For key types we support Any, str, int, - # and Enums with uniform str/int values. - if not childtypes or childtypes[0] is typing.Any: - # 'Any' needs no further checks (just checked - # per-instance). - pass - elif childtypes[0] in (str, int): - # str and int are all good as keys. - pass - elif issubclass(childtypes[0], Enum): - # Allow our usual str or int enum types as keys. - self.prep_enum(childtypes[0], ioattrs=None) - else: - raise TypeError( - f'Dict key type {childtypes[0]} for \'{attrname}\'' - f' on {cls.__name__} is not supported by dataclassio.' - ) + if issubclass(origin, Enum): + self.prep_enum(origin, ioattrs=ioattrs) + return - # For value types we support any of our normal types. - if not childtypes or _get_origin(childtypes[1]) is typing.Any: - # 'Any' needs no further checks (just checked - # per-instance). - pass - else: - self.prep_type( - cls, - attrname, - childtypes[1], - ioattrs=None, - recursion_level=recursion_level + 1, - ) - return + # We allow datetime objects (and google's extended subclass of + # them used in firestore, which is why we don't look for exact + # type here). + if issubclass(origin, datetime.datetime): + return - # For Tuples, simply check individual member types. (and, for - # now, explicitly disallow zero member types or usage of - # ellipsis) - if origin is tuple: - childtypes = typing.get_args(anntype) - if not childtypes: - raise TypeError( - f'Tuple at \'{attrname}\'' - f' has no type args; dataclassio requires type args.' - ) - if childtypes[-1] is ...: - raise TypeError( - f'Found ellipsis as part of type for' - f' \'{attrname}\' on {cls.__name__};' - f' these are not' - f' supported by dataclassio.' - ) - for childtype in childtypes: - self.prep_type( - cls, - attrname, - childtype, - ioattrs=None, - recursion_level=recursion_level + 1, - ) - return + # We support datetime.timedelta. + if issubclass(origin, datetime.timedelta): + return - if issubclass(origin, Enum): - self.prep_enum(origin, ioattrs=ioattrs) - return + if dataclasses.is_dataclass(origin): + self.prep_dataclass(origin, recursion_level=recursion_level + 1) + return - # We allow datetime objects (and google's extended subclass of - # them used in firestore, which is why we don't look for exact - # type here). - if issubclass(origin, datetime.datetime): - return + if origin is bytes: + return - # We support datetime.timedelta. - if issubclass(origin, datetime.timedelta): - return - - if dataclasses.is_dataclass(origin): - self.prep_dataclass(origin, recursion_level=recursion_level + 1) - return - - if origin is bytes: - return - - raise TypeError( - f"Attr '{attrname}' on {cls.__name__} contains" - f" type '{anntype}'" - f' which is not supported by dataclassio.' - ) + raise TypeError( + f"Attr '{attrname}' on {cls.__name__} contains" + f" type '{anntype}'" + f' which is not supported by dataclassio.' + ) def prep_union( self, cls: type, attrname: str, anntype: Any, recursion_level: int diff --git a/dist/ba_data/python/efro/dataclassio/templatemultitype.py b/dist/ba_data/python/efro/dataclassio/templatemultitype.py index 36889c1..9e91853 100644 --- a/dist/ba_data/python/efro/dataclassio/templatemultitype.py +++ b/dist/ba_data/python/efro/dataclassio/templatemultitype.py @@ -1,21 +1,23 @@ # Released under the MIT License. See LICENSE for details. # -"""Template for a multi-type class setup. +"""Template for an IOMultitype setup. To use this template, simply copy the contents of this module somewhere -and then replace 'TemplateMultiType' with 'MyAwesomeTypeName' or -whatnot. +and then replace 'TemplateMultiType' with 'YourType'. """ from __future__ import annotations -from typing import assert_never, override +from typing import TYPE_CHECKING, assert_never, override from enum import Enum from dataclasses import dataclass from efro.dataclassio import ioprepped, IOMultiType +if TYPE_CHECKING: + pass + class TemplateMultiTypeTypeID(Enum): """Type ID for each of our subclasses.""" diff --git a/dist/ba_data/python/efro/debug.py b/dist/ba_data/python/efro/debug.py index 2d87280..a62ee87 100644 --- a/dist/ba_data/python/efro/debug.py +++ b/dist/ba_data/python/efro/debug.py @@ -10,7 +10,6 @@ For this reason, these methods should NEVER be called in production code. Enable them only for debugging situations and be aware that their use may itself cause problems. The same is true for the gc module itself. """ - from __future__ import annotations import os diff --git a/dist/ba_data/python/efro/error.py b/dist/ba_data/python/efro/error.py index d294404..022d824 100644 --- a/dist/ba_data/python/efro/error.py +++ b/dist/ba_data/python/efro/error.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Common errors and related functionality.""" - from __future__ import annotations from typing import TYPE_CHECKING, override diff --git a/dist/ba_data/python/efro/logging.py b/dist/ba_data/python/efro/logging.py index 5fe1876..3b431cd 100644 --- a/dist/ba_data/python/efro/logging.py +++ b/dist/ba_data/python/efro/logging.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Logging functionality.""" - from __future__ import annotations import sys diff --git a/dist/ba_data/python/efro/message/_receiver.py b/dist/ba_data/python/efro/message/_receiver.py index 1763b58..efb9947 100644 --- a/dist/ba_data/python/efro/message/_receiver.py +++ b/dist/ba_data/python/efro/message/_receiver.py @@ -118,14 +118,14 @@ class MessageReceiver: # Return types can be a single type or a union of types. if isinstance(ret, (_GenericAlias, types.UnionType)): targs = get_args(ret) - if not all(isinstance(a, type | None) for a in targs): + if not all(isinstance(a, (type, type(None))) for a in targs): raise TypeError( f'expected only types for "return" annotation;' f' got {targs}.' ) responsetypes = targs else: - if not isinstance(ret, type | None): + if not isinstance(ret, (type, type(None))): raise TypeError( f'expected one or more types for' f' "return" annotation; got a {type(ret)}.' diff --git a/dist/ba_data/python/efro/terminal.py b/dist/ba_data/python/efro/terminal.py index 4ddbcf1..0ccfadb 100644 --- a/dist/ba_data/python/efro/terminal.py +++ b/dist/ba_data/python/efro/terminal.py @@ -1,7 +1,6 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to terminal IO.""" - from __future__ import annotations import sys @@ -315,7 +314,7 @@ class ClrNever(ClrBase): _envval = os.environ.get('EFRO_TERMCOLORS') -color_enabled: bool = ( # pylint: disable=invalid-name +color_enabled: bool = ( True if _envval == '1' else False if _envval == '0' else _default_color_enabled() diff --git a/dist/ba_data/python/efro/threadpool.py b/dist/ba_data/python/efro/threadpool.py index 685263b..5a763b6 100644 --- a/dist/ba_data/python/efro/threadpool.py +++ b/dist/ba_data/python/efro/threadpool.py @@ -10,8 +10,6 @@ import threading from typing import TYPE_CHECKING, ParamSpec from concurrent.futures import ThreadPoolExecutor -from efro.util import strip_exception_tracebacks - if TYPE_CHECKING: from typing import Any, Callable from concurrent.futures import Future @@ -85,8 +83,5 @@ class ThreadPoolExecutorEx(ThreadPoolExecutor): self.no_wait_count -= 1 try: fut.result() - except Exception as exc: + except Exception: logger.exception('Error in work submitted via submit_no_wait().') - # We're done with this exception, so strip its traceback to - # avoid reference cycles. - strip_exception_tracebacks(exc) diff --git a/dist/ba_data/python/efro/util.py b/dist/ba_data/python/efro/util.py index 9ded139..aa2cd7b 100644 --- a/dist/ba_data/python/efro/util.py +++ b/dist/ba_data/python/efro/util.py @@ -162,20 +162,6 @@ def utc_this_minute() -> datetime.datetime: ) -def utc_this_second() -> datetime.datetime: - """Get offset-aware beginning of current second in the utc time zone.""" - now = datetime.datetime.now(datetime.UTC) - return datetime.datetime( - year=now.year, - month=now.month, - day=now.day, - hour=now.hour, - minute=now.minute, - second=now.second, - tzinfo=now.tzinfo, - ) - - def empty_weakref[T](objtype: type[T]) -> weakref.ref[T]: """Return an invalidated weak-reference for the specified type.""" # At runtime, all weakrefs are the same; our type arg is just @@ -362,39 +348,36 @@ def dispatchmethod[ArgT, RetT]( """ from functools import singledispatch, update_wrapper + origwrapper: Any = singledispatch(func) + + # Pull this out so hopefully origwrapper can die, + # otherwise we reference origwrapper in our wrapper. + dispatch = origwrapper.dispatch + + # All we do here is recreate the end of functools.singledispatch + # where it returns a wrapper except instead of the wrapper using the + # first arg to the function ours uses the second (to skip 'self'). + # This was made against Python 3.7; we should probably check up on + # this in later versions in case anything has changed. + # (or hopefully they'll add this functionality to their version) + # NOTE: sounds like we can use functools singledispatchmethod in 3.8 + def wrapper(*args: Any, **kw: Any) -> Any: + if not args or len(args) < 2: + raise TypeError( + f'{funcname} requires at least ' '2 positional arguments' + ) + + return dispatch(args[1].__class__)(*args, **kw) + + funcname = getattr(func, '__name__', 'dispatchmethod method') + wrapper.register = origwrapper.register # type: ignore + wrapper.dispatch = dispatch # type: ignore + wrapper.registry = origwrapper.registry # type: ignore # pylint: disable=protected-access - # pylint: disable=no-else-return - - if TYPE_CHECKING: - return cast(DispatchMethodWrapper, None) - else: - origwrapper: Any = singledispatch(func) - - # Pull this out so hopefully origwrapper can die, - # otherwise we reference origwrapper in our wrapper. - dispatch = origwrapper.dispatch - - # All we do here is recreate the end of functools.singledispatch - # where it returns a wrapper except instead of the wrapper using the - # first arg to the function ours uses the second (to skip 'self'). - # This was made against Python 3.7; we should probably check up on - # this in later versions in case anything has changed. - # (or hopefully they'll add this functionality to their version) - # NOTE: sounds like we can use functools singledispatchmethod in 3.8 - def wrapper(*args: Any, **kw: Any) -> Any: - if not args or len(args) < 2: - raise TypeError( - f'{funcname} requires at least ' '2 positional arguments' - ) - return dispatch(args[1].__class__)(*args, **kw) - - funcname = getattr(func, '__name__', 'dispatchmethod method') - wrapper.register = origwrapper.register - wrapper.dispatch = dispatch - wrapper.registry = origwrapper.registry - wrapper._clear_cache = origwrapper._clear_cache - update_wrapper(wrapper, func) - return cast(DispatchMethodWrapper, wrapper) + wrapper._clear_cache = origwrapper._clear_cache # type: ignore + update_wrapper(wrapper, func) + # pylint: enable=protected-access + return cast(DispatchMethodWrapper, wrapper) def valuedispatch[ValT, RetT]( @@ -596,7 +579,7 @@ def asserttype_o[T](obj: Any, typ: type[T]) -> T | None: failures are not expected. Otherwise use checktype. """ assert isinstance(typ, type), 'only actual types accepted' - assert isinstance(obj, typ | None) + assert isinstance(obj, (typ, type(None))) return obj @@ -619,7 +602,7 @@ def checktype_o[T](obj: Any, typ: type[T]) -> T | None: on failure. Use asserttype for more efficient (but less safe) equivalent. """ assert isinstance(typ, type), 'only actual types accepted' - if not isinstance(obj, typ | None): + if not isinstance(obj, (typ, type(None))): raise TypeError(f'Expected a {typ} or None; got a {type(obj)}.') return obj @@ -645,7 +628,7 @@ def warntype_o[T](obj: Any, typ: type[T]) -> T | None: not what is expected. """ assert isinstance(typ, type), 'only actual types accepted' - if not isinstance(obj, typ | None): + if not isinstance(obj, (typ, type(None))): import logging logging.warning( @@ -840,10 +823,7 @@ def set_canonical_module_names(module_globals: dict[str, Any]) -> None: def timedelta_str( - timeval: datetime.timedelta | float | int, - *, - maxparts: int = 2, - decimals: int = 0, + timeval: datetime.timedelta | float, *, maxparts: int = 2, decimals: int = 0 ) -> str: """Return a simple human readable time string for a length of time. @@ -860,7 +840,7 @@ def timedelta_str( """ # pylint: disable=too-many-locals - if isinstance(timeval, float | int): + if isinstance(timeval, float): timevalfin = datetime.timedelta(seconds=timeval) else: timevalfin = timeval @@ -1028,8 +1008,7 @@ def weighted_choice[T](*args: tuple[T, float]) -> T: items: tuple[T] weights: tuple[float] items, weights = zip(*args) - val: T = random.choices(items, weights=weights)[0] - return val + return random.choices(items, weights=weights)[0] def prune_empty_dirs(prunedir: str) -> None: @@ -1089,17 +1068,3 @@ def strip_exception_tracebacks(exc: BaseException) -> None: cause = getattr(e, '__cause__', None) if cause is not None: stack.append(cause) - - -def secure_id() -> str: - """Generate a 20 char cryptographically secure string. - - Basically what firestore does for its random document ids. - If its good enough for firestore its good enough for us. - """ - import secrets - import string - - alphabet = string.ascii_letters + string.digits # 62 chars - - return ''.join(secrets.choice(alphabet) for _ in range(20)) diff --git a/dist/bombsquad_headless b/dist/bombsquad_headless index aa48416..bb17ac3 100644 Binary files a/dist/bombsquad_headless and b/dist/bombsquad_headless differ