Merge pull request #131 from imayushsaini/dev
Some checks failed
CI / run_server_binary (push) Has been cancelled

syncing 1.7.60 ballistica binary
This commit is contained in:
Ayush Saini 2026-06-27 16:18:07 +05:30 committed by GitHub
commit ff357460e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
259 changed files with 10138 additions and 7583 deletions

View file

@ -1,8 +1,9 @@
#!/usr/bin/env python3.13 #!/usr/bin/env -S python3.13 -OB
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
"""BallisticaKit server manager.""" """BallisticaKit server manager."""
from __future__ import annotations from __future__ import annotations
import os import os
@ -11,6 +12,7 @@ import time
import json import json
import signal import signal
import tomllib import tomllib
import logging
import subprocess import subprocess
import platform import platform
from pathlib import Path from pathlib import Path
@ -25,19 +27,39 @@ sys.path += [
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')), str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')),
] ]
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 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
if TYPE_CHECKING: if TYPE_CHECKING:
from types import FrameType from types import FrameType
from bacommon.servermanager import ServerCommand from bacommon.servermanager import ServerCommand
VERSION_STR = '1.3.2' VERSION_STR = '1.3.5'
# Version history: # 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 # 1.3.2
# #
# - Updated to use Python 3.12. # - Updated to use Python 3.12.
@ -413,8 +435,7 @@ class ServerManagerApp:
raise CleanError('Expected a config path as next arg.') raise CleanError('Expected a config path as next arg.')
path = sys.argv[i + 1] path = sys.argv[i + 1]
if not os.path.exists(path): if not os.path.exists(path):
raise CleanError( raise CleanError(f"Supplied path does not exist: '{path}'.")
f"Supplied path does not exist: '{path}'.")
# We need an abs path because we may be in a different # We need an abs path because we may be in a different
# cwd currently than we will be during the run. # cwd currently than we will be during the run.
self._user_provided_config_path = os.path.abspath(path) self._user_provided_config_path = os.path.abspath(path)
@ -702,6 +723,14 @@ class ServerManagerApp:
# instead? # instead?
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1' 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 # Set an environment var to change the device name. Device name
# is used while making connection with master server, # is used while making connection with master server,
# cloud-console recognize us with this name. # cloud-console recognize us with this name.
@ -721,7 +750,7 @@ class ServerManagerApp:
# Launch! # Launch!
try: try:
self._subprocess = subprocess.Popen( self._subprocess = subprocess.Popen(
[binary_name, '--config-dir', self._ba_root_path], [binary_name, '--config-dir', self._ba_root_path] + extra_args,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
cwd='dist', cwd='dist',
) )
@ -799,26 +828,43 @@ class ServerManagerApp:
bincfg = {} bincfg = {}
# Some of our config values translate directly into the # Some of our config values translate directly into the
# ballisticakit config file; the rest we pass at runtime. # ballisticakit config file; the rest we pass at runtime.
bincfg['Port'] = int(os.environ.get('PORT', self._config.port)) bincfg['Port'] = int(os.environ.get('PORT', self._config.port))
bincfg['Auto Balance Teams'] = self._config.auto_balance_teams bincfg['Auto Balance Teams'] = self._config.auto_balance_teams
bincfg['Show Tutorial'] = self._config.show_tutorial bincfg['Show Tutorial'] = self._config.show_tutorial
binkey = 'SceneV1 Host Protocol'
if self._config.protocol_version is not None: if self._config.protocol_version is not None:
bincfg['SceneV1 Host Protocol'] = self._config.protocol_version bincfg[binkey] = self._config.protocol_version
if self._config.team_names is not None: elif binkey in bincfg:
bincfg['Custom Team Names'] = self._config.team_names del bincfg[binkey]
elif 'Custom Team Names' in bincfg:
del bincfg['Custom Team Names']
binkey = 'Custom Team Names'
if self._config.team_names is not None:
bincfg[binkey] = self._config.team_names
elif binkey in bincfg:
del bincfg[binkey]
binkey = 'Custom Team Colors'
if self._config.team_colors is not None: if self._config.team_colors is not None:
bincfg['Custom Team Colors'] = self._config.team_colors bincfg[binkey] = self._config.team_colors
elif 'Custom Team Colors' in bincfg: elif binkey in bincfg:
del bincfg['Custom Team Colors'] del bincfg[binkey]
bincfg['Idle Exit Minutes'] = self._config.idle_exit_minutes 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: with open(cfgpath, 'w', encoding='utf-8') as outfile:
outfile.write(json.dumps(bincfg)) outfile.write(json.dumps(bincfg))

View file

@ -7,6 +7,7 @@ directly. Instead one should use purpose-built packages such as
:mod:`bascenev1` or :mod:`bauiv1` which themselves import various :mod:`bascenev1` or :mod:`bauiv1` which themselves import various
functionality from here and reexpose it in a more focused way. functionality from here and reexpose it in a more focused way.
""" """
# pylint: disable=redefined-builtin # pylint: disable=redefined-builtin
# ba_meta require api 9 # ba_meta require api 9
@ -15,14 +16,14 @@ functionality from here and reexpose it in a more focused way.
# from other modules/packages. Code *within* this package should import # from other modules/packages. Code *within* this package should import
# things from this package's submodules directly to reduce the chance of # things from this package's submodules directly to reduce the chance of
# dependency loops. The exception is TYPE_CHECKING blocks and # dependency loops. The exception is TYPE_CHECKING blocks and
# annotations since those aren't evaluated at runtime. # annotations - since those aren't evaluated at runtime, it is cleaner
# looking to use top level names directly.
import _babase import _babase
from _babase import ( from _babase import (
add_clean_frame_callback, add_clean_frame_callback,
allows_ticket_sales, allows_ticket_sales,
android_get_external_files_dir, android_get_external_files_dir,
app_instance_uuid,
appname, appname,
appnameupper, appnameupper,
apptime, apptime,
@ -125,6 +126,7 @@ from _babase import (
) )
from babase._accountv2 import AccountV2Handle, AccountV2Subsystem from babase._accountv2 import AccountV2Handle, AccountV2Subsystem
from babase._analytics import AnalyticsSubsystem
from babase._app import App, AppState from babase._app import App, AppState
from babase._appcomponent import AppComponentSubsystem from babase._appcomponent import AppComponentSubsystem
from babase._appconfig import commit_app_config from babase._appconfig import commit_app_config
@ -134,68 +136,71 @@ from babase._appsubsystem import AppSubsystem
from babase._appmodeselector import AppModeSelector from babase._appmodeselector import AppModeSelector
from babase._appconfig import AppConfig from babase._appconfig import AppConfig
from babase._apputils import ( from babase._apputils import (
AppHealthSubsystem,
get_remote_app_name,
handle_leftover_v1_cloud_log_file, handle_leftover_v1_cloud_log_file,
is_browser_likely_available, is_browser_likely_available,
get_remote_app_name,
AppHealthSubsystem,
utc_now_cloud, utc_now_cloud,
) )
from babase._cloud import CloudSubscription from babase._cloud import CloudSubscription
from babase._devconsole import ( from babase._devconsole import (
DevConsoleButtonDef, DevConsoleButtonDef,
DevConsoleSubsystem,
DevConsoleTab, DevConsoleTab,
DevConsoleTabEntry, DevConsoleTabEntry,
DevConsoleSubsystem,
) )
from babase._discord import DiscordSubsystem from babase._discord import DiscordSubsystem
from babase._emptyappmode import EmptyAppMode from babase._emptyappmode import EmptyAppMode
from babase._error import ( from babase._error import (
ActivityNotFoundError,
ActorNotFoundError,
ContextError, ContextError,
DelegateNotFoundError,
InputDeviceNotFoundError,
MapNotFoundError,
NodeNotFoundError,
NotFoundError, NotFoundError,
PlayerNotFoundError, PlayerNotFoundError,
SessionPlayerNotFoundError,
NodeNotFoundError,
ActorNotFoundError,
InputDeviceNotFoundError,
WidgetNotFoundError,
ActivityNotFoundError,
TeamNotFoundError,
MapNotFoundError,
SessionTeamNotFoundError,
SessionNotFoundError, SessionNotFoundError,
DelegateNotFoundError, SessionPlayerNotFoundError,
SessionTeamNotFoundError,
TeamNotFoundError,
WidgetNotFoundError,
) )
from babase._gc import GarbageCollectionSubsystem from babase._gc import GarbageCollectionSubsystem
from babase._general import ( from babase._general import (
DisplayTime,
AppTime, AppTime,
WeakCall,
Call, Call,
existing, CallPartial,
CallStrict,
DisplayTime,
Existable, Existable,
verify_object_death, WeakCall,
storagename, WeakCallPartial,
getclass, WeakCallStrict,
existing,
get_type_name, get_type_name,
getclass,
storagename,
verify_object_death,
) )
from babase._language import Lstr, LanguageSubsystem from babase._language import LanguageSubsystem, Lstr
from babase._locale import LocaleSubsystem from babase._locale import LocaleSubsystem
from babase._logging import ( from babase._logging import (
balog,
accountlog, accountlog,
applog, applog,
balog,
lifecyclelog, lifecyclelog,
netlog, netlog,
uilog, uilog,
) )
from babase._login import LoginAdapter, LoginInfo from babase._login import LoginAdapter, LoginInfo
from babase._mgen.enums import ( from babase._mgen.enums import (
Permission,
SpecialChar,
InputType, InputType,
UIScale, Permission,
QuitType, QuitType,
SpecialChar,
UIScale,
) )
from babase._math import normalized_color, is_point_in_box, vec3validate from babase._math import normalized_color, is_point_in_box, vec3validate
from babase._meta import MetadataSubsystem from babase._meta import MetadataSubsystem
@ -216,6 +221,7 @@ __all__ = [
'ActorNotFoundError', 'ActorNotFoundError',
'allows_ticket_sales', 'allows_ticket_sales',
'add_clean_frame_callback', 'add_clean_frame_callback',
'AnalyticsSubsystem',
'android_get_external_files_dir', 'android_get_external_files_dir',
'app', 'app',
'App', 'App',
@ -227,7 +233,6 @@ __all__ = [
'AppIntentExec', 'AppIntentExec',
'AppMode', 'AppMode',
'AppState', 'AppState',
'app_instance_uuid',
'applog', 'applog',
'appname', 'appname',
'appnameupper', 'appnameupper',
@ -242,6 +247,8 @@ __all__ = [
'atexit', 'atexit',
'balog', 'balog',
'Call', 'Call',
'CallPartial',
'CallStrict',
'fullscreen_control_available', 'fullscreen_control_available',
'fullscreen_control_get', 'fullscreen_control_get',
'fullscreen_control_key_shortcut', 'fullscreen_control_key_shortcut',
@ -391,6 +398,8 @@ __all__ = [
'vec3validate', 'vec3validate',
'verify_object_death', 'verify_object_death',
'WeakCall', 'WeakCall',
'WeakCallPartial',
'WeakCallStrict',
'WidgetNotFoundError', 'WidgetNotFoundError',
'workspaces_in_use', 'workspaces_in_use',
'WorkspaceSubsystem', 'WorkspaceSubsystem',

View file

@ -4,9 +4,11 @@
from __future__ import annotations from __future__ import annotations
import time
import hashlib import hashlib
import logging import logging
from functools import partial from functools import partial
from dataclasses import dataclass
from typing import TYPE_CHECKING, assert_never from typing import TYPE_CHECKING, assert_never
from efro.error import CommunicationError from efro.error import CommunicationError
@ -19,6 +21,8 @@ import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable from typing import Any, Callable
import bacommon.cloud
from babase._login import LoginAdapter, LoginInfo from babase._login import LoginAdapter, LoginInfo
@ -62,6 +66,9 @@ class AccountV2Subsystem:
Callable[[AccountV2Handle | None], None] Callable[[AccountV2Handle | None], None]
] = CallbackSet() ] = CallbackSet()
# Request state per global-app-instance-id
self._auth_requests: dict[str, _AuthRequest] = {}
adapter: LoginAdapter adapter: LoginAdapter
if _babase.using_google_play_game_services(): if _babase.using_google_play_game_services():
adapter = LoginAdapterGPGS() adapter = LoginAdapterGPGS()
@ -104,6 +111,9 @@ class AccountV2Subsystem:
""" """
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
# Blow away any outstanding auth-requests.
self._auth_requests = {}
# Inform the base layer of new names/etc. # Inform the base layer of new names/etc.
if account is not None: if account is not None:
_babase.set_account_sign_in_state(True, account.tag) _babase.set_account_sign_in_state(True, account.tag)
@ -201,6 +211,78 @@ class AccountV2Subsystem:
self._initial_sign_in_completed = True self._initial_sign_in_completed = True
_babase.app.on_initial_sign_in_complete() _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 @staticmethod
def _hashstr(val: str) -> str: def _hashstr(val: str) -> str:
md5 = hashlib.md5() md5 = hashlib.md5()
@ -501,3 +583,10 @@ class AccountV2Handle:
This allows cloud messages to be sent on our behalf. This allows cloud messages to be sent on our behalf.
""" """
@dataclass
class _AuthRequest:
expire_time: float
error: str | None
token: str | None

View file

@ -0,0 +1,73 @@
# 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

View file

@ -2,10 +2,12 @@
# #
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
"""Functionality related to the high level state of the app.""" """Functionality related to the high level state of the app."""
from __future__ import annotations from __future__ import annotations
import os import os
import time import time
import asyncio
import logging import logging
from enum import Enum from enum import Enum
from functools import partial from functools import partial
@ -28,12 +30,12 @@ from babase._appmodeselector import AppModeSelector
from babase._appintent import AppIntentDefault, AppIntentExec from babase._appintent import AppIntentDefault, AppIntentExec
from babase._stringedit import StringEditSubsystem from babase._stringedit import StringEditSubsystem
from babase._devconsole import DevConsoleSubsystem from babase._devconsole import DevConsoleSubsystem
from babase._analytics import AnalyticsSubsystem
from babase._appconfig import AppConfig from babase._appconfig import AppConfig
from babase._logging import lifecyclelog, applog from babase._logging import lifecyclelog, applog
from babase._gc import GarbageCollectionSubsystem from babase._gc import GarbageCollectionSubsystem
if TYPE_CHECKING: if TYPE_CHECKING:
import asyncio
from typing import Any, Callable, Coroutine, Generator, Awaitable from typing import Any, Callable, Coroutine, Generator, Awaitable
from concurrent.futures import Future from concurrent.futures import Future
@ -152,6 +154,9 @@ class App:
#: Subsystem for wrangling the dev-console UI. #: Subsystem for wrangling the dev-console UI.
self.devconsole: DevConsoleSubsystem = DevConsoleSubsystem() self.devconsole: DevConsoleSubsystem = DevConsoleSubsystem()
#: Subsystem for wrangling analytics.
self.analytics: AnalyticsSubsystem = AnalyticsSubsystem()
#: Incremented each time the app leaves the #: Incremented each time the app leaves the
#: :attr:`~babase.AppState.SUSPENDED` state. This can be a simple #: :attr:`~babase.AppState.SUSPENDED` state. This can be a simple
#: way to determine if network data should be refreshed/etc. #: way to determine if network data should be refreshed/etc.
@ -237,7 +242,6 @@ class App:
loop. Hopefully this situation will be improved in the future loop. Hopefully this situation will be improved in the future
with a unified event loop. with a unified event loop.
""" """
assert _babase.in_logic_thread()
assert self._asyncio_loop is not None assert self._asyncio_loop is not None
return self._asyncio_loop return self._asyncio_loop
@ -281,8 +285,9 @@ class App:
def mode_selector(self, selector: babase.AppModeSelector) -> None: def mode_selector(self, selector: babase.AppModeSelector) -> None:
self._mode_selector = selector self._mode_selector = selector
def _on_task_done(self, task: asyncio.Task) -> None: def _on_task_done(self, task: Any) -> None:
# Report any errors that occurred. # Report any errors that occurred.
assert isinstance(task, asyncio.Task)
try: try:
exc = task.exception() exc = task.exception()
if exc is not None: if exc is not None:
@ -1028,7 +1033,6 @@ class App:
) )
async def _shutdown(self) -> None: async def _shutdown(self) -> None:
import asyncio
_babase.lock_all_input() _babase.lock_all_input()
try: try:
@ -1054,13 +1058,16 @@ class App:
self, coro: Coroutine[None, None, None] self, coro: Coroutine[None, None, None]
) -> None: ) -> None:
"""Run a shutdown task; report errors and abort if taking too long.""" """Run a shutdown task; report errors and abort if taking too long."""
import asyncio
task = asyncio.create_task(coro) task = asyncio.create_task(coro)
try: try:
await asyncio.wait_for(task, self.SHUTDOWN_TASK_TIMEOUT_SECONDS) 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: except Exception:
logging.exception('Error in shutdown task (%s).', coro) # Go with full ugly stack trace for anything unexpected.
logging.exception('Error in shutdown task %s.', coro)
def _on_suspend(self) -> None: def _on_suspend(self) -> None:
"""Called when the app goes to a suspended state.""" """Called when the app goes to a suspended state."""
@ -1136,7 +1143,6 @@ class App:
) )
async def _wait_for_shutdown_suppressions(self) -> None: async def _wait_for_shutdown_suppressions(self) -> None:
import asyncio
# Spin and wait for anything blocking shutdown to complete. # Spin and wait for anything blocking shutdown to complete.
starttime = _babase.apptime() starttime = _babase.apptime()
@ -1153,7 +1159,6 @@ class App:
) )
async def _fade_and_shutdown_graphics(self) -> None: async def _fade_and_shutdown_graphics(self) -> None:
import asyncio
# Kick off a short fade and give it time to complete. # Kick off a short fade and give it time to complete.
lifecyclelog.info('fade-and-shutdown-graphics begin') lifecyclelog.info('fade-and-shutdown-graphics begin')
@ -1189,7 +1194,6 @@ class App:
lifecyclelog.info('fade-and-shutdown-graphics end') lifecyclelog.info('fade-and-shutdown-graphics end')
async def _fade_and_shutdown_audio(self) -> None: async def _fade_and_shutdown_audio(self) -> None:
import asyncio
# Tell the audio system to go down and give it a bit of # Tell the audio system to go down and give it a bit of
# time to do so gracefully. # time to do so gracefully.

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Provides the AppComponent class.""" """Provides the AppComponent class."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Provides the AppConfig class.""" """Provides the AppConfig class."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -10,7 +11,7 @@ import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any
_g_pending_apply = False # pylint: disable=invalid-name _g_pending_apply = False
class AppConfig(dict): class AppConfig(dict):

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Provides AppIntent functionality.""" """Provides AppIntent functionality."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Provides AppMode functionality.""" """Provides AppMode functionality."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Contains AppModeSelector base class.""" """Contains AppModeSelector base class."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View file

@ -1,11 +1,11 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Provides the AppSubsystem base class.""" """Provides the AppSubsystem base class."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from babase import UIScale from babase import UIScale

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Utility functionality related to the overall operation of the app.""" """Utility functionality related to the overall operation of the app."""
from __future__ import annotations from __future__ import annotations
import os import os

View file

@ -23,8 +23,8 @@ if TYPE_CHECKING:
import babase import babase
# Our timer and event loop for the ballistica logic thread. # Our timer and event loop for the ballistica logic thread.
_asyncio_timer: babase.AppTimer | None = None _g_asyncio_timer: babase.AppTimer | None = None
_asyncio_event_loop: asyncio.AbstractEventLoop | None = None _g_asyncio_event_loop: asyncio.AbstractEventLoop | None = None
DEBUG_TIMING = os.environ.get('BA_DEBUG_TIMING') == '1' DEBUG_TIMING = os.environ.get('BA_DEBUG_TIMING') == '1'
@ -46,12 +46,12 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
except RuntimeError: except RuntimeError:
pass pass
global _asyncio_event_loop global _g_asyncio_event_loop
_asyncio_event_loop = asyncio.new_event_loop() _g_asyncio_event_loop = asyncio.new_event_loop()
_asyncio_event_loop.set_default_executor(babase.app.threadpool) _g_asyncio_event_loop.set_default_executor(babase.app.threadpool)
# Try to avoid reference loops from exceptions. # Try to avoid reference loops from exceptions.
_asyncio_event_loop.set_exception_handler(_exception_handler) _g_asyncio_event_loop.set_exception_handler(_exception_handler)
# Ideally we should integrate asyncio into our C++ Thread class's # Ideally we should integrate asyncio into our C++ Thread class's
# low level event loop so that asyncio timers/sockets/etc. could # 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/ # See https://stackoverflow.com/questions/29782377/
# is-it-possible-to-run-only-a-single-step-of-the-asyncio-event-loop # is-it-possible-to-run-only-a-single-step-of-the-asyncio-event-loop
def run_cycle() -> None: def run_cycle() -> None:
assert _asyncio_event_loop is not None assert _g_asyncio_event_loop is not None
_asyncio_event_loop.call_soon(_asyncio_event_loop.stop) _g_asyncio_event_loop.call_soon(_g_asyncio_event_loop.stop)
starttime = time.monotonic() if DEBUG_TIMING else 0 starttime = time.monotonic() if DEBUG_TIMING else 0
_asyncio_event_loop.run_forever() _g_asyncio_event_loop.run_forever()
endtime = time.monotonic() if DEBUG_TIMING else 0 endtime = time.monotonic() if DEBUG_TIMING else 0
# Let's aim to have nothing take longer than 1/120 of a second. # 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, warn_time,
) )
global _asyncio_timer global _g_asyncio_timer
_asyncio_timer = _babase.AppTimer(1.0 / 30.0, run_cycle, repeat=True) _g_asyncio_timer = _babase.AppTimer(1.0 / 30.0, run_cycle, repeat=True)
if bool(False): if bool(False):
async def aio_test() -> None: async def aio_test() -> None:
print('TEST AIO TASK STARTING') print('TEST AIO TASK STARTING')
assert _asyncio_event_loop is not None assert _g_asyncio_event_loop is not None
assert asyncio.get_running_loop() is _asyncio_event_loop assert asyncio.get_running_loop() is _g_asyncio_event_loop
await asyncio.sleep(2.0) await asyncio.sleep(2.0)
print('TEST AIO TASK ENDING') print('TEST AIO TASK ENDING')
_testtask = _asyncio_event_loop.create_task(aio_test()) _testtask = _g_asyncio_event_loop.create_task(aio_test())
return _asyncio_event_loop return _g_asyncio_event_loop
def _exception_handler( def _exception_handler(

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Cloud related functionality.""" """Cloud related functionality."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Dev-Console functionality.""" """Dev-Console functionality."""
from __future__ import annotations from __future__ import annotations
import os import os

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Predefined tabs for the dev console.""" """Predefined tabs for the dev console."""
from __future__ import annotations from __future__ import annotations
import math import math

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
"""Functionality related to discord sdk integration""" """Functionality related to discord sdk integration"""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override

View file

@ -1,12 +1,11 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Provides AppMode functionality.""" """Provides AppMode functionality."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override
# from bacommon.app import AppExperience
import _babase import _babase
from babase._appmode import AppMode from babase._appmode import AppMode
from babase._appintent import AppIntentExec, AppIntentDefault from babase._appintent import AppIntentExec, AppIntentDefault

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Environment related functionality.""" """Environment related functionality."""
from __future__ import annotations from __future__ import annotations
import os import os
@ -158,26 +159,6 @@ def on_main_thread_start_app() -> None:
# situations. # situations.
__main__.__builtins__.help = _CustomHelper() __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 # Kick off networking bootstrapping. We do this here instead of in
# our app net-subsystem so that it can proceed in parallel with the # our app net-subsystem so that it can proceed in parallel with the
# rest of our bootstrapping (as networking stuff is often an overall # rest of our bootstrapping (as networking stuff is often an overall
@ -647,5 +628,5 @@ class _CustomHelper:
'Interactive help is not available in this environment.\n' 'Interactive help is not available in this environment.\n'
'Type help(object) for help about object.' 'Type help(object) for help about object.'
) )
return None return
return pydoc.help(*args, **kwds) pydoc.help(*args, **kwds)

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Utility functionality related to the overall operation of the app.""" """Utility functionality related to the overall operation of the app."""
from __future__ import annotations from __future__ import annotations
import gc import gc

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Utility snippets applying to generic Python code.""" """Utility snippets applying to generic Python code."""
from __future__ import annotations from __future__ import annotations
import sys import sys
@ -9,6 +10,7 @@ import weakref
import random import random
import logging import logging
import inspect import inspect
import warnings
from typing import TYPE_CHECKING, TypeVar, Protocol, NewType, override from typing import TYPE_CHECKING, TypeVar, Protocol, NewType, override
from efro.terminal import Clr from efro.terminal import Clr
@ -17,7 +19,7 @@ import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
import functools import functools
from typing import Any from typing import Any, Callable
# Declare distinct types for different time measurements we use so the # Declare distinct types for different time measurements we use so the
@ -93,160 +95,351 @@ def get_type_name(cls: type) -> str:
return f'{cls.__module__}.{cls.__qualname__}' return f'{cls.__module__}.{cls.__qualname__}'
class _WeakCall: # Note: Something here is wonky with pylint, possibly related to our
"""Wrap a callable and arguments into a single callable object. # 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:
When passed a bound method as the callable, the instance portion of class WeakCallPartial:
it is weak-referenced, meaning the underlying instance is free to """Wrap a callable and arguments into a single callable object.
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 When passed a bound method as the callable, the instance portion of
some point in the future if it happens to still exist. 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.
**EXAMPLE A:** This code will create a ``FooClass`` instance and Think of this as a handy way to tell an object to do something at
call its ``bar()`` method 5 seconds later; it will be kept alive some point in the future if it happens to still exist.
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() **EXAMPLE A:** This code will create a ``FooClass`` instance and
babase.apptimer(5.0, foo.bar) call its ``bar()`` method 5 seconds later; it will be kept alive
foo = None even though we overwrite its variable with None because the bound
method we pass as a timer callback (``foo.bar``) strong-references
it::
**EXAMPLE B:** This code will *not* keep our object alive; it will foo = FooClass()
die when we overwrite it with ``None`` and the timer will be a no-op babase.apptimer(5.0, foo.bar)
when it fires:: foo = None
foo = FooClass() **EXAMPLE B:** This code will *not* keep our object alive; it will
babase.apptimer(5.0, ba.WeakCall(foo.bar)) die when we overwrite it with ``None`` and the timer will be a no-op
foo = None when it fires::
**EXAMPLE C:** Wrap a method call with some positional and keyword foo = FooClass()
args:: babase.apptimer(5.0, ba.WeakCall(foo.bar))
foo = None
myweakcall = babase.WeakCall(self.dostuff, argval1, **EXAMPLE C:** Wrap a method call with some positional and keyword
namedarg=argval2) args::
# Now we have a single callable to run that whole mess. myweakcall = babase.WeakCall(self.dostuff, argval1,
# The same as calling myobj.dostuff(argval1, namedarg=argval2) namedarg=argval2)
# (provided my_obj still exists; this will do nothing otherwise).
myweakcall()
Note: additional args and keywords you provide to the weak-call # Now we have a single callable to run that whole mess.
constructor are stored as regular strong-references; you'll need to # The same as calling myobj.dostuff(argval1, namedarg=argval2)
wrap them in weakrefs manually if desired. # (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'<babase.WeakCall object; _call={self._call!r}'
f' _args={self._args!r} _keywds={self._keywds!r}>'
)
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'<babase.Call object; _call={self.call!r}'
f' _args={self.args!r} _keywds={self.keywds!r}>'
)
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'<babase.WeakCall object; _call={self._call!r}'
f' _args={self._args!r} _keywds={self._keywds!r}>'
)
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'<babase.Call object; _call={self.call!r}'
f' _args={self.args!r} _keywds={self.keywds!r}>'
)
# 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.
""" """
# Optimize performance a bit; we shouldn't need to be super dynamic. __slots__ = ('call', 'args', 'kwargs')
__slots__ = ['_call', '_args', '_keywds']
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'<babase.Call object; call={self.call!r},'
f' args={self.args!r}, kwargs={self.kwargs!r}>'
)
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')
_did_invalid_call_warning = False _did_invalid_call_warning = False
def __init__(self, *args: Any, **keywds: Any) -> None: def __init__(
if hasattr(args[0], '__func__'): self, call: Callable[P, T], /, *args: P.args, **kwargs: P.kwargs
self._call = WeakMethod(args[0]) ) -> 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
else: else:
app = _babase.app app = _babase.app
if not self._did_invalid_call_warning: if not self._did_invalid_call_warning:
logging.warning( logging.warning(
'Warning: callable passed to babase.WeakCall() is not' 'Warning: callable passed to WeakCallStrict() is not'
' weak-referencable (%s); use functools.partial instead' ' weak-referencable (%r); use regular CallStrict() instead'
' to avoid this warning.', ' to avoid this warning.',
args[0], args[0],
stack_info=True, stack_info=True,
) )
type(self)._did_invalid_call_warning = True type(self)._did_invalid_call_warning = True
self._call = args[0] self.call = call
self._args = args[1:] self.args = args
self._keywds = keywds self.kwargs = kwargs
def __call__(self, *args_extra: Any) -> Any: def __call__(self) -> T:
return self._call(*self._args + args_extra, **self._keywds) return self.call(*self.args, **self.kwargs) # type: ignore
@override @override
def __str__(self) -> str: def __repr__(self) -> str:
return ( return (
'<ba.WeakCall object; _call=' f'<babase.WeakCall object; call={self.call!r},'
+ str(self._call) f' args={self.args!r}, kwargs={self.kwargs!r}>'
+ ' _args='
+ str(self._args)
+ ' _keywds='
+ str(self._keywds)
+ '>'
) )
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 (
'<ba.Call object; _call='
+ str(self._call)
+ ' _args='
+ str(self._args)
+ ' _keywds='
+ str(self._keywds)
+ '>'
)
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: class WeakMethod:
"""A weak-referenced bound method. """A weak-referenced bound method.
@ -255,22 +448,22 @@ class WeakMethod:
""" """
# Optimize performance a bit; we shouldn't need to be super dynamic. # Optimize performance a bit; we shouldn't need to be super dynamic.
__slots__ = ['_func', '_obj'] __slots__ = ['func', 'obj']
def __init__(self, call: types.MethodType): def __init__(self, call: types.MethodType):
assert isinstance(call, types.MethodType) assert isinstance(call, types.MethodType)
self._func = call.__func__ self.func = call.__func__
self._obj = weakref.ref(call.__self__) self.obj = weakref.ref(call.__self__)
def __call__(self, *args: Any, **keywds: Any) -> Any: def __call__(self, *args: Any, **keywds: Any) -> Any:
obj = self._obj() obj: Any = self.obj()
if obj is None: if obj is None:
return None return None
return self._func(*((obj,) + args), **keywds) return self.func(*((obj,) + args), **keywds)
@override @override
def __str__(self) -> str: def __repr__(self) -> str:
return '<ba.WeakMethod object; call=' + str(self._func) + '>' return f'<babase.WeakMethod object; func={self.func!r}>'
def verify_object_death(obj: object) -> None: def verify_object_death(obj: object) -> None:
@ -292,7 +485,7 @@ def verify_object_death(obj: object) -> None:
# Make this timer in an empty context; don't want it dying with the # Make this timer in an empty context; don't want it dying with the
# scene/etc. # scene/etc.
with _babase.ContextRef.empty(): with _babase.ContextRef.empty():
_babase.apptimer(delay, Call(_verify_object_death, ref)) _babase.apptimer(delay, CallStrict(_verify_object_death, ref))
def _verify_object_death(wref: weakref.ref) -> None: def _verify_object_death(wref: weakref.ref) -> None:

View file

@ -9,6 +9,7 @@ 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 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. and type-checking magic to happen and most issues will be caught immediately.
""" """
# (most of these are self-explanatory) # (most of these are self-explanatory)
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
from __future__ import annotations from __future__ import annotations
@ -42,7 +43,7 @@ def get_v2_account_id() -> str | None:
if account is not None: if account is not None:
accountid = account.accountid accountid = account.accountid
# (Avoids mypy complaints when plus is not present) # (Avoids mypy complaints when plus is not present)
assert isinstance(accountid, (str, type(None))) assert isinstance(accountid, str | None)
return accountid return accountid
return None return None
except Exception: except Exception:
@ -461,3 +462,36 @@ def copy_dev_console_history() -> None:
_babase.clipboard_set_text('\n'.join(lines)) _babase.clipboard_set_text('\n'.join(lines))
_babase.screenmessage(Lstr(resource='copyConfirmText'), color=(0, 1, 0)) _babase.screenmessage(Lstr(resource='copyConfirmText'), color=(0, 1, 0))
_babase.getsimplesound('gunCocking').play() _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,
)

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Language related functionality.""" """Language related functionality."""
from __future__ import annotations from __future__ import annotations
import os import os
@ -561,7 +562,7 @@ class Lstr:
You should avoid doing this as much as possible and instead pass You should avoid doing this as much as possible and instead pass
and store ``Lstr`` values. and store ``Lstr`` values.
""" """
return _babase.evaluate_lstr(self._get_json()) return _babase.evaluate_lstr(self.as_json())
def is_flat_value(self) -> bool: def is_flat_value(self) -> bool:
"""Return whether this instance represents a 'flat' value. """Return whether this instance represents a 'flat' value.
@ -573,22 +574,13 @@ class Lstr:
""" """
return bool('v' in self.args and not self.args.get('s', [])) return bool('v' in self.args and not self.args.get('s', []))
def _get_json(self) -> str: def as_json(self) -> str:
try: """Return the json dict representation of the Lstr."""
return json.dumps(self.args, separators=(',', ':')) 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'<ba.Lstr: {self._get_json()}>'
@override @override
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<ba.Lstr: {self._get_json()}>' return f'<babase.Lstr: {self.as_json()}>'
@staticmethod @staticmethod
def from_json(json_string: str) -> babase.Lstr: def from_json(json_string: str) -> babase.Lstr:
@ -616,7 +608,7 @@ def _add_to_attr_dict(dst: AttrDict, src: dict) -> None:
) )
_add_to_attr_dict(dst_dict, value) _add_to_attr_dict(dst_dict, value)
else: else:
if not isinstance(value, (float, int, bool, str, str, type(None))): if not isinstance(value, float | int | bool | str | None):
raise TypeError( raise TypeError(
"invalid value type for res '" "invalid value type for res '"
+ key + key

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Locale related functionality.""" """Locale related functionality."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, override, assert_never from typing import TYPE_CHECKING, override, assert_never
@ -144,6 +145,7 @@ class LocaleSubsystem(AppSubsystem):
or rlocale is cls.TAMIL or rlocale is cls.TAMIL
or rlocale is cls.THAI or rlocale is cls.THAI
or rlocale is cls.VIETNAMESE or rlocale is cls.VIETNAMESE
or rlocale is cls.JAPANESE
): ):
# Return True only if we can display full unicode. # Return True only if we can display full unicode.
return _babase.supports_unicode_display() return _babase.supports_unicode_display()

View file

@ -85,7 +85,9 @@ class Permission(Enum):
class SpecialChar(Enum): class SpecialChar(Enum):
"""Special characters the game can print.""" """Special characters the engine can diplay. Note that this currently
needs to be manually kept in sync with bacommon.text.SpecialChar.
"""
DOWN_ARROW = 0 DOWN_ARROW = 0
UP_ARROW = 1 UP_ARROW = 1
@ -185,3 +187,7 @@ class SpecialChar(Enum):
MIKIROG = 95 MIKIROG = 95
V2_LOGO = 96 V2_LOGO = 96
CLOSE = 97 CLOSE = 97
SANTA_HAT = 98
POTATO = 99
PALM_TREE = 100
BOXING_GLOVE = 101

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Networking related functionality.""" """Networking related functionality."""
from __future__ import annotations from __future__ import annotations
import socket import socket

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""UI related bits of babase.""" """UI related bits of babase."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to modding.""" """Functionality related to modding."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View file

@ -1,12 +1,13 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Various functionality related to achievements.""" """Various functionality related to achievements."""
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from bacommon.bs import ClassicChestAppearance from bacommon.classic import ClassicChestAppearance
from baclassic._chest import ( from baclassic._chest import (
CHEST_APPEARANCE_DISPLAY_INFOS, CHEST_APPEARANCE_DISPLAY_INFOS,
CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
@ -727,7 +728,10 @@ class Achievement:
) )
def get_award_chest_type(self) -> ClassicChestAppearance: def get_award_chest_type(self) -> ClassicChestAppearance:
"""Return the type of chest given for this achievement.""" """Return the type of chest given for this achievement.
:meta private:
"""
# For now just map our old ticket values to chest types. # For now just map our old ticket values to chest types.
# Can add distinct values if need be later. # Can add distinct values if need be later.
@ -1520,5 +1524,7 @@ class Achievement:
for actor in objs: for actor in objs:
bascenev1.timer( bascenev1.timer(
out_time + 1.000, out_time + 1.000,
babase.WeakCall(actor.handlemessage, bascenev1.DieMessage()), babase.WeakCallStrict(
actor.handlemessage, bascenev1.DieMessage()
),
) )

View file

@ -1,227 +0,0 @@
# 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.

View file

@ -1,6 +1,6 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to analytics.""" """Functionality related to classic analytics."""
from __future__ import annotations from __future__ import annotations

View file

@ -1,5 +1,6 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
# pylint: disable=too-many-lines
"""Contains ClassicAppMode.""" """Contains ClassicAppMode."""
from __future__ import annotations from __future__ import annotations
@ -11,11 +12,11 @@ from functools import partial
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override
from efro.error import CommunicationError from efro.error import CommunicationError
import bacommon.bs import bacommon.clienteffect as clfx
import bacommon.classic
from babase import AppMode from babase import AppMode
import bauiv1 as bui import bauiv1 as bui
from bauiv1lib.connectivity import wait_for_connectivity from bauiv1lib.connectivity import wait_for_connectivity
from bauiv1lib.account.signin import show_sign_in_prompt
import _baclassic import _baclassic
@ -233,19 +234,19 @@ class ClassicAppMode(AppMode):
if item_id.startswith('tokens'): if item_id.startswith('tokens'):
if item_id == 'tokens1': if item_id == 'tokens1':
tokens = bacommon.bs.TOKENS1_COUNT tokens = bacommon.classic.TOKENS1_COUNT
tokens_str = str(tokens) tokens_str = str(tokens)
anim_time = 2.0 anim_time = 2.0
elif item_id == 'tokens2': elif item_id == 'tokens2':
tokens = bacommon.bs.TOKENS2_COUNT tokens = bacommon.classic.TOKENS2_COUNT
tokens_str = str(tokens) tokens_str = str(tokens)
anim_time = 2.5 anim_time = 2.5
elif item_id == 'tokens3': elif item_id == 'tokens3':
tokens = bacommon.bs.TOKENS3_COUNT tokens = bacommon.classic.TOKENS3_COUNT
tokens_str = str(tokens) tokens_str = str(tokens)
anim_time = 3.0 anim_time = 3.0
elif item_id == 'tokens4': elif item_id == 'tokens4':
tokens = bacommon.bs.TOKENS4_COUNT tokens = bacommon.classic.TOKENS4_COUNT
tokens_str = str(tokens) tokens_str = str(tokens)
anim_time = 3.5 anim_time = 3.5
else: else:
@ -257,21 +258,19 @@ class ClassicAppMode(AppMode):
) )
assert bui.app.classic is not None assert bui.app.classic is not None
effects: list[bacommon.bs.ClientEffect] = [ effects: list[clfx.Effect] = [
bacommon.bs.ClientEffectTokensAnimation( clfx.TokensAnimation(
duration=anim_time, duration=anim_time,
startvalue=self._last_tokens_value, startvalue=self._last_tokens_value,
endvalue=self._last_tokens_value + tokens, endvalue=self._last_tokens_value + tokens,
), ),
bacommon.bs.ClientEffectDelay(anim_time), clfx.Delay(anim_time),
bacommon.bs.ClientEffectScreenMessage( clfx.LegacyScreenMessage(
message='You got ${COUNT} tokens!', message='You got ${COUNT} tokens!',
subs=['${COUNT}', tokens_str], subs=['${COUNT}', tokens_str],
color=(0, 1, 0), color=(0, 1, 0),
), ),
bacommon.bs.ClientEffectSound( clfx.PlaySound(clfx.Sound.CASH_REGISTER),
sound=bacommon.bs.ClientEffectSound.Sound.CASH_REGISTER
),
] ]
bui.app.classic.run_bs_client_effects(effects) bui.app.classic.run_bs_client_effects(effects)
@ -345,14 +344,14 @@ class ClassicAppMode(AppMode):
with plus.accounts.primary: with plus.accounts.primary:
plus.cloud.send_message_cb( plus.cloud.send_message_cb(
bacommon.bs.GetClassicPurchasesMessage(), bacommon.classic.GetClassicPurchasesMessage(),
on_response=bui.WeakCall( on_response=bui.WeakCallPartial(
self._on_get_classic_purchases_response self._on_get_classic_purchases_response
), ),
) )
def _on_get_classic_purchases_response( def _on_get_classic_purchases_response(
self, response: bacommon.bs.GetClassicPurchasesResponse | Exception self, response: bacommon.classic.GetClassicPurchasesResponse | Exception
) -> None: ) -> None:
assert self._purchase_request_in_flight assert self._purchase_request_in_flight
self._purchase_request_in_flight = False self._purchase_request_in_flight = False
@ -471,6 +470,7 @@ class ClassicAppMode(AppMode):
chest_1_ad_allow_time=-1.0, chest_1_ad_allow_time=-1.0,
chest_2_ad_allow_time=-1.0, chest_2_ad_allow_time=-1.0,
chest_3_ad_allow_time=-1.0, chest_3_ad_allow_time=-1.0,
store_style='',
) )
self._have_account_values = False self._have_account_values = False
self._update_ui_live_state() self._update_ui_live_state()
@ -505,7 +505,7 @@ class ClassicAppMode(AppMode):
print(f'GOT SUB TEST UPDATE: {val}') print(f'GOT SUB TEST UPDATE: {val}')
def _on_classic_account_data_change( def _on_classic_account_data_change(
self, val: bacommon.bs.ClassicAccountLiveData self, val: bacommon.classic.ClassicLiveAccountClientData
) -> None: ) -> None:
achp = round(val.achievements / max(val.achievements_total, 1) * 100.0) achp = round(val.achievements / max(val.achievements_total, 1) * 100.0)
@ -666,6 +666,7 @@ class ClassicAppMode(AppMode):
if chest3 is None or chest3.ad_allow_time is None if chest3 is None or chest3.ad_allow_time is None
else chest3.ad_allow_time.timestamp() else chest3.ad_allow_time.timestamp()
), ),
store_style=val.store_style.value,
) )
# Note that we have values and updated faded state accordingly. # Note that we have values and updated faded state accordingly.
@ -723,44 +724,56 @@ class ClassicAppMode(AppMode):
def _root_ui_achievements_press(self) -> None: def _root_ui_achievements_press(self) -> None:
from bauiv1lib.achievements import AchievementsWindow from bauiv1lib.achievements import AchievementsWindow
if not self._ensure_signed_in_v1(): btn = bui.get_special_widget('achievements_button')
if not self._ensure_signed_in(origin_widget=btn):
return return
wait_for_connectivity( wait_for_connectivity(
on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate( on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate(
win_type=AchievementsWindow, win_type=AchievementsWindow,
win_create_call=lambda: AchievementsWindow( win_create_call=lambda: AchievementsWindow(origin_widget=btn),
origin_widget=bui.get_special_widget('achievements_button')
),
) )
) )
def _root_ui_inbox_press(self) -> None: def _root_ui_inbox_press(self) -> None:
from bauiv1lib.inbox import InboxWindow from bauiv1lib.inbox import InboxWindow
if not self._ensure_signed_in(): btn = bui.get_special_widget('inbox_button')
if not self._ensure_signed_in(origin_widget=btn):
return return
wait_for_connectivity( wait_for_connectivity(
on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate( on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate(
win_type=InboxWindow, win_type=InboxWindow,
win_create_call=lambda: InboxWindow( win_create_call=lambda: InboxWindow(origin_widget=btn),
origin_widget=bui.get_special_widget('inbox_button')
),
) )
) )
def _root_ui_store_press(self) -> None: def _root_ui_store_press(self) -> None:
from bauiv1lib.store.browser import StoreBrowserWindow import bacommon.docui.v1 as dui1
if not self._ensure_signed_in_v1(): 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):
return return
# Pop up an auxiliary window wherever we are in the nav stack.
wait_for_connectivity( wait_for_connectivity(
on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate( on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate(
win_type=StoreBrowserWindow, win_type=DocUIWindow,
win_create_call=lambda: StoreBrowserWindow( win_create_call=bui.CallStrict(
origin_widget=bui.get_special_widget('store_button') StoreUIController().create_window,
dui1.Request('/'),
origin_widget=btn,
uiopenstateid='classicstore',
),
win_extra_type_id=(
StoreUIController.get_window_extra_type_id()
), ),
) )
) )
@ -782,80 +795,76 @@ class ClassicAppMode(AppMode):
def _root_ui_trophy_meter_press(self) -> None: def _root_ui_trophy_meter_press(self) -> None:
from bauiv1lib.league.rankwindow import LeagueRankWindow from bauiv1lib.league.rankwindow import LeagueRankWindow
if not self._ensure_signed_in_v1(): btn = bui.get_special_widget('trophy_meter')
if not self._ensure_signed_in(origin_widget=btn):
return return
bui.app.ui_v1.auxiliary_window_activate( bui.app.ui_v1.auxiliary_window_activate(
win_type=LeagueRankWindow, win_type=LeagueRankWindow,
win_create_call=lambda: LeagueRankWindow( win_create_call=lambda: LeagueRankWindow(origin_widget=btn),
origin_widget=bui.get_special_widget('trophy_meter')
),
) )
def _root_ui_level_meter_press(self) -> None: def _root_ui_level_meter_press(self) -> None:
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
ResourceTypeInfoWindow( btn = bui.get_special_widget('level_meter')
'xp', origin_widget=bui.get_special_widget('level_meter')
)
def _root_ui_inventory_press(self) -> None: if not self._ensure_signed_in(origin_widget=btn):
from bauiv1lib.inventory import InventoryWindow
if not self._ensure_signed_in_v1():
return 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( bui.app.ui_v1.auxiliary_window_activate(
win_type=InventoryWindow, win_type=DocUIWindow,
win_create_call=lambda: InventoryWindow( win_create_call=bui.CallStrict(
origin_widget=bui.get_special_widget('inventory_button') 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(),
) )
def _ensure_signed_in(self) -> bool: def _ensure_signed_in(self, *, origin_widget: bui.Widget | None) -> bool:
"""Make sure we're signed in (requiring modern v2 accounts).""" """Make sure we're signed in (requiring modern v2 accounts)."""
from bauiv1lib.account.signin import show_sign_in_prompt
plus = bui.app.plus plus = bui.app.plus
if plus is None: if plus is None:
bui.screenmessage('This requires plus.', color=(1, 0, 0)) bui.screenmessage('This requires plus.', color=(1, 0, 0))
bui.getsound('error').play() bui.getsound('error').play()
return False return False
if plus.accounts.primary is None: if plus.accounts.primary is None:
show_sign_in_prompt() show_sign_in_prompt(origin_widget=origin_widget)
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 False
return True return True
def _root_ui_get_tokens_press(self) -> None: def _root_ui_get_tokens_press(self) -> None:
from bauiv1lib.gettokens import GetTokensWindow from bauiv1lib.gettokens import GetTokensWindow, show_get_tokens_window
if not self._ensure_signed_in(): btn = bui.get_special_widget('get_tokens_button')
if not self._ensure_signed_in(origin_widget=btn):
return return
bui.app.ui_v1.auxiliary_window_activate( if bool(True):
win_type=GetTokensWindow, show_get_tokens_window(origin_widget=btn, toggle=True)
win_create_call=lambda: GetTokensWindow( else:
origin_widget=bui.get_special_widget('get_tokens_button') bui.app.ui_v1.auxiliary_window_activate(
), win_type=GetTokensWindow,
) win_create_call=lambda: GetTokensWindow(origin_widget=btn),
)
def _root_ui_chest_slot_pressed(self, index: int) -> None: def _root_ui_chest_slot_pressed(self, index: int) -> None:
from bauiv1lib.chest import ( from bauiv1lib.chest import ChestWindow
ChestWindow0,
ChestWindow1,
ChestWindow2,
ChestWindow3,
)
widgetid: Literal[ widgetid: Literal[
'chest_0_button', 'chest_0_button',
@ -866,16 +875,20 @@ class ClassicAppMode(AppMode):
winclass: type[ChestWindow] winclass: type[ChestWindow]
if index == 0: if index == 0:
widgetid = 'chest_0_button' widgetid = 'chest_0_button'
winclass = ChestWindow0 winclass = ChestWindow
extratypeid = '0'
elif index == 1: elif index == 1:
widgetid = 'chest_1_button' widgetid = 'chest_1_button'
winclass = ChestWindow1 winclass = ChestWindow
extratypeid = '1'
elif index == 2: elif index == 2:
widgetid = 'chest_2_button' widgetid = 'chest_2_button'
winclass = ChestWindow2 winclass = ChestWindow
extratypeid = '2'
elif index == 3: elif index == 3:
widgetid = 'chest_3_button' widgetid = 'chest_3_button'
winclass = ChestWindow3 winclass = ChestWindow
extratypeid = '3'
else: else:
raise RuntimeError(f'Invalid index {index}') raise RuntimeError(f'Invalid index {index}')
@ -886,6 +899,7 @@ class ClassicAppMode(AppMode):
index=index, index=index,
origin_widget=bui.get_special_widget(widgetid), origin_widget=bui.get_special_widget(widgetid),
), ),
win_extra_type_id=extratypeid,
) )
) )
@ -953,16 +967,26 @@ class ClassicAppMode(AppMode):
return [ return [
bui.DevConsoleButtonDef( bui.DevConsoleButtonDef(
'MainWindow Template', 'MainWindow Template',
bui.WeakCall(self._main_win_template_press), bui.WeakCallStrict(self._main_win_template_press),
), ),
bui.DevConsoleButtonDef( bui.DevConsoleButtonDef(
'CloudUI Test', bui.WeakCall(self._cloud_ui_test_press) 'DocUI Test', bui.WeakCallStrict(self._doc_ui_test_press)
), ),
] ]
def _main_win_template_press(self) -> None: def _main_win_template_press(self) -> None:
from bauiv1lib.template import show_template_main_window 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. # Unintuitively, swish sounds come from buttons, not windows.
# And dev-console buttons don't make sounds. So we need to # And dev-console buttons don't make sounds. So we need to
# explicitly do so here. # explicitly do so here.
@ -970,12 +994,22 @@ class ClassicAppMode(AppMode):
show_template_main_window() show_template_main_window()
def _cloud_ui_test_press(self) -> None: def _doc_ui_test_press(self) -> None:
from bauiv1 import show_cloud_ui_window 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
# Unintuitively, swish sounds come from buttons, not windows. # Unintuitively, swish sounds come from buttons, not windows.
# And dev-console buttons don't make sounds. So we need to # And dev-console buttons don't make sounds. So we need to
# explicitly do so here. # explicitly do so here.
bui.getsound('swish').play() bui.getsound('swish').play()
show_cloud_ui_window() show_test_doc_ui_window()

View file

@ -3,12 +3,15 @@
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
"""Provides classic app subsystem.""" """Provides classic app subsystem."""
from __future__ import annotations from __future__ import annotations
import time
import random import random
import logging import logging
import weakref import weakref
from typing import TYPE_CHECKING, override, assert_never from dataclasses import dataclass
from typing import TYPE_CHECKING, override, assert_never, final
from efro.dataclassio import dataclass_from_dict from efro.dataclassio import dataclass_from_dict
import babase import babase
@ -25,9 +28,12 @@ from baclassic._store import StoreSubsystem
from baclassic import _input from baclassic import _input
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Any, Sequence import datetime
from typing import Callable, Any, Sequence, Awaitable
import bacommon.bs import bacommon.classic
import bacommon.clienteffect as clfx
import bacommon.clouddialog.basic as bcdlg
from bascenev1lib.actor import spazappearance from bascenev1lib.actor import spazappearance
from bauiv1lib.party import PartyWindow from bauiv1lib.party import PartyWindow
@ -36,16 +42,72 @@ if TYPE_CHECKING:
class ClassicAppSubsystem(babase.AppSubsystem): class ClassicAppSubsystem(babase.AppSubsystem):
"""Subsystem for classic functionality in the app. """Subsystem for classic bombsquad functionality in the app.
The single shared instance of this app can be accessed at The single shared instance of this app can be accessed at
babase.app.classic. Note that it is possible for babase.app.classic to babase.app.classic. Note that it is possible for babase.app.classic
be None if the classic package is not present, and code should handle to be None if the classic package is not present, and futureproof
that case gracefully. code should handle that case gracefully.
""" """
# pylint: disable=too-many-public-methods # 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 from baclassic._music import MusicPlayMode
def __init__(self) -> None: def __init__(self) -> None:
@ -92,6 +154,21 @@ class ClassicAppSubsystem(babase.AppSubsystem):
# Server Mode. # Server Mode.
self.server: ServerController | None = None 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_have_new = False
self.log_upload_timer_started = False self.log_upload_timer_started = False
self.printed_live_object_warning = False self.printed_live_object_warning = False
@ -126,8 +203,48 @@ class ClassicAppSubsystem(babase.AppSubsystem):
self.pro_sale_start_time: int | None = None self.pro_sale_start_time: int | None = None
self.pro_sale_start_val: 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: def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None:
"""(internal)""" """:meta private:"""
# If there's no main window up, just call immediately. # If there's no main window up, just call immediately.
if not babase.app.ui_v1.has_main_window(): if not babase.app.ui_v1.has_main_window():
@ -171,7 +288,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
return self._env['platform'] return self._env['platform']
def scene_v1_protocol_version(self) -> int: def scene_v1_protocol_version(self) -> int:
"""(internal)""" """:meta private:"""
return bascenev1.protocol_version() return bascenev1.protocol_version()
@property @property
@ -408,7 +525,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
# Otherwise just force the issue. # Otherwise just force the issue.
else: else:
babase.pushcall( babase.pushcall(
babase.Call(bascenev1.new_host_session, MainMenuSession) babase.CallStrict(bascenev1.new_host_session, MainMenuSession)
) )
def getmaps(self, playtype: str) -> list[str]: def getmaps(self, playtype: str) -> list[str]:
@ -461,7 +578,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
) )
def game_begin_analytics(self) -> None: def game_begin_analytics(self) -> None:
"""(internal)""" """:meta private:"""
from baclassic import _analytics from baclassic import _analytics
_analytics.game_begin_analytics() _analytics.game_begin_analytics()
@ -653,11 +770,11 @@ class ClassicAppSubsystem(babase.AppSubsystem):
return bascenev1.get_player_profile_colors(profilename, profiles) return bascenev1.get_player_profile_colors(profilename, profiles)
def get_foreground_host_session(self) -> bascenev1.Session | None: def get_foreground_host_session(self) -> bascenev1.Session | None:
"""(internal)""" """:meta private:"""
return bascenev1.get_foreground_host_session() return bascenev1.get_foreground_host_session()
def get_foreground_host_activity(self) -> bascenev1.Activity | None: def get_foreground_host_activity(self) -> bascenev1.Activity | None:
"""(internal)""" """:meta private:"""
return bascenev1.get_foreground_host_activity() return bascenev1.get_foreground_host_activity()
def value_test( def value_test(
@ -666,26 +783,26 @@ class ClassicAppSubsystem(babase.AppSubsystem):
change: float | None = None, change: float | None = None,
absolute: float | None = None, absolute: float | None = None,
) -> float: ) -> float:
"""(internal)""" """:meta private:"""
return _baclassic.value_test(arg, change, absolute) return _baclassic.value_test(arg, change, absolute)
def set_master_server_source(self, source: int) -> None: def set_master_server_source(self, source: int) -> None:
"""(internal)""" """:meta private:"""
bascenev1.set_master_server_source(source) bascenev1.set_master_server_source(source)
def get_game_port(self) -> int: def get_game_port(self) -> int:
"""(internal)""" """:meta private:"""
return bascenev1.get_game_port() return bascenev1.get_game_port()
def v2_upgrade_window(self, login_name: str, code: str) -> None: def v2_upgrade_window(self, login_name: str, code: str) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.v2upgrade import V2UpgradeWindow from bauiv1lib.v2upgrade import V2UpgradeWindow
V2UpgradeWindow(login_name, code) V2UpgradeWindow(login_name, code)
def server_dialog(self, delay: float, data: dict[str, Any]) -> None: def server_dialog(self, delay: float, data: dict[str, Any]) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.serverdialog import ( from bauiv1lib.serverdialog import (
ServerDialogData, ServerDialogData,
ServerDialogWindow, ServerDialogWindow,
@ -702,17 +819,17 @@ class ClassicAppSubsystem(babase.AppSubsystem):
if sddata is not None: if sddata is not None:
babase.apptimer( babase.apptimer(
delay, delay,
babase.Call(ServerDialogWindow, sddata), babase.CallStrict(ServerDialogWindow, sddata),
) )
def show_url_window(self, address: str) -> None: def show_url_window(self, address: str) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.url import ShowURLWindow from bauiv1lib.url import ShowURLWindow
ShowURLWindow(address) ShowURLWindow(address)
def quit_window(self, quit_type: babase.QuitType) -> None: def quit_window(self, quit_type: babase.QuitType) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.confirm import QuitWindow from bauiv1lib.confirm import QuitWindow
QuitWindow(quit_type) QuitWindow(quit_type)
@ -728,7 +845,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
offset: tuple[float, float] = (0.0, 0.0), offset: tuple[float, float] = (0.0, 0.0),
on_close_call: Callable[[], Any] | None = None, on_close_call: Callable[[], Any] | None = None,
) -> None: ) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.tournamententry import TournamentEntryWindow from bauiv1lib.tournamententry import TournamentEntryWindow
TournamentEntryWindow( TournamentEntryWindow(
@ -742,7 +859,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
) )
def get_main_menu_session(self) -> type[bascenev1.Session]: def get_main_menu_session(self) -> type[bascenev1.Session]:
"""(internal)""" """:meta private:"""
from bascenev1lib.mainmenu import MainMenuSession from bascenev1lib.mainmenu import MainMenuSession
return MainMenuSession return MainMenuSession
@ -751,10 +868,13 @@ class ClassicAppSubsystem(babase.AppSubsystem):
self, self,
transition: str = 'in_right', transition: str = 'in_right',
origin_widget: bauiv1.Widget | None = None, origin_widget: bauiv1.Widget | None = None,
selected_profile: str | None = None, # selected_profile: str | None = None,
) -> None: ) -> None:
"""Pop up a browser window from within a game.""" """Pop up a browser window from within a game."""
from bauiv1lib.profile.browser import ProfileBrowserWindow import bacommon.docui.v1 as dui1
# from bauiv1lib.profile.browser import ProfileBrowserWindow
from bauiv1lib.inventory import InventoryUIController
main_window = babase.app.ui_v1.get_main_window() main_window = babase.app.ui_v1.get_main_window()
if main_window is not None: if main_window is not None:
@ -765,15 +885,16 @@ class ClassicAppSubsystem(babase.AppSubsystem):
return return
babase.app.ui_v1.set_main_window( babase.app.ui_v1.set_main_window(
ProfileBrowserWindow( InventoryUIController(player_profiles_only=True).create_window(
dui1.Request('/'),
uiopenstateid='classicinventory',
transition=transition, transition=transition,
selected_profile=selected_profile,
origin_widget=origin_widget, origin_widget=origin_widget,
minimal_toolbar=True,
), ),
is_top_level=True, is_top_level=True,
back_state=None, back_state=None,
suppress_warning=True, suppress_warning=True,
extra_type_id=InventoryUIController.get_window_extra_type_id(),
) )
def preload_map_preview_media(self) -> None: def preload_map_preview_media(self) -> None:
@ -789,7 +910,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
logging.exception('Error preloading map preview media.') logging.exception('Error preloading map preview media.')
def party_icon_activate(self, origin: Sequence[float]) -> None: def party_icon_activate(self, origin: Sequence[float]) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.party import PartyWindow from bauiv1lib.party import PartyWindow
from babase import app from babase import app
@ -810,7 +931,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
self.party_window = weakref.ref(PartyWindow(origin=origin)) self.party_window = weakref.ref(PartyWindow(origin=origin))
def request_main_ui(self) -> None: def request_main_ui(self) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.ingamemenu import InGameMenuWindow from bauiv1lib.ingamemenu import InGameMenuWindow
assert babase.app is not None assert babase.app is not None
@ -835,6 +956,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
suppress_warning=True, suppress_warning=True,
# Reset selections to default for consistency. # Reset selections to default for consistency.
restore_shared_state=False, restore_shared_state=False,
extra_type_id='',
) )
def save_ui_state(self) -> None: def save_ui_state(self) -> None:
@ -876,11 +998,17 @@ class ClassicAppSubsystem(babase.AppSubsystem):
is_top_level=True, is_top_level=True,
back_state=None, back_state=None,
suppress_warning=True, suppress_warning=True,
extra_type_id='',
) )
else: else:
# If there's a saved ui state, restore that. # If there's a saved ui state, restore that.
if self.saved_ui_state is not None: if self.saved_ui_state is not None:
app.ui_v1.restore_main_window_state(self.saved_ui_state) 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: else:
# Otherwise start fresh at the main menu. # Otherwise start fresh at the main menu.
from bauiv1lib.mainmenu import MainMenuWindow from bauiv1lib.mainmenu import MainMenuWindow
@ -890,25 +1018,32 @@ class ClassicAppSubsystem(babase.AppSubsystem):
is_top_level=True, is_top_level=True,
back_state=None, back_state=None,
suppress_warning=True, suppress_warning=True,
extra_type_id='',
) )
@staticmethod @staticmethod
def run_bs_client_effects( def run_bs_client_effects(
effects: list[bacommon.bs.ClientEffect], delay: float = 0.0 effects: list[clfx.Effect], delay: float = 0.0
) -> None: ) -> None:
"""Run client effects sent from the master server.""" """Run client effects sent from the master server.
:meta private:
"""
from baclassic._clienteffect import run_bs_client_effects from baclassic._clienteffect import run_bs_client_effects
run_bs_client_effects(effects, delay=delay) run_bs_client_effects(effects, delay=delay)
@staticmethod @staticmethod
def basic_client_ui_button_label_str( def basic_client_ui_button_label_str(
label: bacommon.bs.BasicCloudDialog.ButtonLabel, label: bcdlg.ButtonLabel,
) -> babase.Lstr: ) -> babase.Lstr:
"""Given a client-ui label, return an Lstr.""" """Given a client-ui label, return an Lstr.
import bacommon.bs
cls = bacommon.bs.BasicCloudDialog.ButtonLabel :meta private:
"""
import bacommon.clouddialog.basic as bcdlg
cls = bcdlg.ButtonLabel
if label is cls.UNKNOWN: if label is cls.UNKNOWN:
# Server should not be sending us unknown stuff; make noise # Server should not be sending us unknown stuff; make noise
# if they do. # if they do.

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Benchmark/Stress-Test related functionality.""" """Benchmark/Stress-Test related functionality."""
from __future__ import annotations from __future__ import annotations
import random import random
@ -130,9 +131,9 @@ def _start_stress_test(args: _StressTestArgs) -> None:
appconfig['Team Tournament Playlist Randomize'] = 1 appconfig['Team Tournament Playlist Randomize'] = 1
babase.apptimer( babase.apptimer(
1.0, 1.0,
babase.Call( babase.CallStrict(
babase.pushcall, babase.pushcall,
babase.Call(bascenev1.new_host_session, DualTeamSession), babase.CallStrict(bascenev1.new_host_session, DualTeamSession),
), ),
) )
else: else:
@ -140,18 +141,22 @@ def _start_stress_test(args: _StressTestArgs) -> None:
appconfig['Free-for-All Playlist Randomize'] = 1 appconfig['Free-for-All Playlist Randomize'] = 1
babase.apptimer( babase.apptimer(
1.0, 1.0,
babase.Call( babase.CallStrict(
babase.pushcall, babase.pushcall,
babase.Call(bascenev1.new_host_session, FreeForAllSession), babase.CallStrict(
bascenev1.new_host_session, FreeForAllSession
),
), ),
) )
_baclassic.set_stress_testing(True, args.player_count, args.attract_mode) _baclassic.set_stress_testing(True, args.player_count, args.attract_mode)
classic.stress_test_update_timer = babase.AppTimer( classic.stress_test_update_timer = babase.AppTimer(
args.round_duration, babase.Call(_reset_stress_test, args) args.round_duration, babase.CallStrict(_reset_stress_test, args)
) )
if args.attract_mode: if args.attract_mode:
classic.stress_test_update_timer_2 = babase.AppTimer( classic.stress_test_update_timer_2 = babase.AppTimer(
0.48, babase.Call(_update_attract_mode_test, args), repeat=True 0.48,
babase.CallStrict(_update_attract_mode_test, args),
repeat=True,
) )
@ -176,7 +181,7 @@ def _reset_stress_test(args: _StressTestArgs) -> None:
# we just end back at the main menu. If things are idle there then # we just end back at the main menu. If things are idle there then
# we'll get sent back to a new stress test. # we'll get sent back to a new stress test.
if not args.attract_mode: if not args.attract_mode:
babase.apptimer(1.0, babase.Call(_start_stress_test, args)) babase.apptimer(1.0, babase.CallStrict(_start_stress_test, args))
def run_media_reload_benchmark() -> None: def run_media_reload_benchmark() -> None:
@ -200,8 +205,8 @@ def run_media_reload_benchmark() -> None:
color=(1, 1, 0), color=(1, 1, 0),
) )
babase.add_clean_frame_callback(babase.Call(doit, start_time)) babase.add_clean_frame_callback(babase.CallStrict(doit, start_time))
# The reload starts (should add a completion callback to the reload # The reload starts (should add a completion callback to the reload
# func to fix this). # func to fix this).
babase.apptimer(0.05, babase.Call(delay_add, babase.apptime())) babase.apptimer(0.05, babase.CallStrict(delay_add, babase.apptime()))

View file

@ -1,12 +1,13 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Chest related functionality.""" """Chest related functionality."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from bacommon.bs import ClassicChestAppearance from bacommon.classic import ClassicChestAppearance
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass

View file

@ -9,26 +9,25 @@ from typing import TYPE_CHECKING, assert_never
from efro.util import strict_partial from efro.util import strict_partial
import bacommon.bs
import bauiv1 import bauiv1
import _baclassic import _baclassic
if TYPE_CHECKING: if TYPE_CHECKING:
pass import bacommon.clienteffect as clfx
def run_bs_client_effects( def run_bs_client_effects(
effects: list[bacommon.bs.ClientEffect], delay: float = 0.0 effects: list[clfx.Effect], delay: float = 0.0
) -> None: ) -> None:
"""Run effects.""" """Run effects."""
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
from bacommon.bs import ClientEffectTypeID import bacommon.clienteffect as clfx
for effect in effects: for effect in effects:
effecttype = effect.get_type_id() effecttype = effect.get_type_id()
if effecttype is ClientEffectTypeID.SCREEN_MESSAGE: if effecttype is clfx.EffectTypeID.LEGACY_SCREEN_MESSAGE:
assert isinstance(effect, bacommon.bs.ClientEffectScreenMessage) assert isinstance(effect, clfx.LegacyScreenMessage)
textfin = bauiv1.Lstr( textfin = bauiv1.Lstr(
translate=('serverResponses', effect.message) translate=('serverResponses', effect.message)
).evaluate() ).evaluate()
@ -46,22 +45,33 @@ def run_bs_client_effects(
bauiv1.screenmessage, textfin, color=effect.color 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 ClientEffectTypeID.SOUND: elif effecttype is clfx.EffectTypeID.SOUND:
assert isinstance(effect, bacommon.bs.ClientEffectSound) assert isinstance(effect, clfx.PlaySound)
smcls = bacommon.bs.ClientEffectSound.Sound scls = clfx.Sound
soundfile: str | None = None soundfile: str | None = None
if effect.sound is smcls.UNKNOWN: if effect.sound is scls.UNKNOWN:
# Server should avoid sending us sounds we don't # Server should avoid sending us sounds we don't
# support. Make some noise if it happens. # support. Make some noise if it happens.
logging.error('Got unrecognized bacommon.bs.ClientEffectSound.') logging.error('Got unrecognized bacommon.classic.Sound.')
elif effect.sound is smcls.CASH_REGISTER: elif effect.sound is scls.CASH_REGISTER:
soundfile = 'cashRegister' soundfile = 'cashRegister'
elif effect.sound is smcls.ERROR: elif effect.sound is scls.ERROR:
soundfile = 'error' soundfile = 'error'
elif effect.sound is smcls.POWER_DOWN: elif effect.sound is scls.POWER_DOWN:
soundfile = 'powerdown01' soundfile = 'powerdown01'
elif effect.sound is smcls.GUN_COCKING: elif effect.sound is scls.GUN_COCKING:
soundfile = 'gunCocking' soundfile = 'gunCocking'
else: else:
assert_never(effect.sound) assert_never(effect.sound)
@ -73,14 +83,12 @@ def run_bs_client_effects(
), ),
) )
elif effecttype is ClientEffectTypeID.DELAY: elif effecttype is clfx.EffectTypeID.DELAY:
assert isinstance(effect, bacommon.bs.ClientEffectDelay) assert isinstance(effect, clfx.Delay)
delay += effect.seconds delay += effect.seconds
elif effecttype is ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION: elif effecttype is clfx.EffectTypeID.CHEST_WAIT_TIME_ANIMATION:
assert isinstance( assert isinstance(effect, clfx.ChestWaitTimeAnimation)
effect, bacommon.bs.ClientEffectChestWaitTimeAnimation
)
bauiv1.apptimer( bauiv1.apptimer(
delay, delay,
strict_partial( strict_partial(
@ -92,8 +100,8 @@ def run_bs_client_effects(
), ),
) )
elif effecttype is ClientEffectTypeID.TICKETS_ANIMATION: elif effecttype is clfx.EffectTypeID.TICKETS_ANIMATION:
assert isinstance(effect, bacommon.bs.ClientEffectTicketsAnimation) assert isinstance(effect, clfx.TicketsAnimation)
bauiv1.apptimer( bauiv1.apptimer(
delay, delay,
strict_partial( strict_partial(
@ -104,8 +112,8 @@ def run_bs_client_effects(
), ),
) )
elif effecttype is ClientEffectTypeID.TOKENS_ANIMATION: elif effecttype is clfx.EffectTypeID.TOKENS_ANIMATION:
assert isinstance(effect, bacommon.bs.ClientEffectTokensAnimation) assert isinstance(effect, clfx.TokensAnimation)
bauiv1.apptimer( bauiv1.apptimer(
delay, delay,
strict_partial( strict_partial(
@ -116,12 +124,11 @@ def run_bs_client_effects(
), ),
) )
elif effecttype is ClientEffectTypeID.UNKNOWN: elif effecttype is clfx.EffectTypeID.UNKNOWN:
# Server should not send us stuff we can't digest. Make # Server should not send us stuff we can't digest. Make
# some noise if it happens. # some noise if it happens.
logging.error( logging.error(
'Got unrecognized bacommon.bs.ClientEffect;' 'Got unrecognized bacommon.classic.Effect; should not happen.'
' should not happen.'
) )
else: else:

View file

@ -1,28 +1,33 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Display-item related functionality.""" """Display-item related functionality."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, assert_never
from efro.util import pairs_from_flat from efro.util import pairs_from_flat
import bacommon.bs import bacommon.displayitem as ditm
import bacommon.classic
import bauiv1 import bauiv1
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
# FIXME - migrate to use the doc-ui rendering for these instead.
def show_display_item( def show_display_item(
itemwrapper: bacommon.bs.DisplayItemWrapper, itemwrapper: ditm.Wrapper,
parent: bauiv1.Widget, parent: bauiv1.Widget,
pos: tuple[float, float], pos: tuple[float, float],
width: float, width: float,
debug: bool = False,
) -> None: ) -> None:
"""Create ui to depict a display-item.""" """Create ui to depict a display-item."""
# pylint: disable=too-many-locals
height = width * 0.666 # Let's go with 4:3 aspect ratio.
height = width * 0.75
# Silent no-op if our parent ui is dead. # Silent no-op if our parent ui is dead.
if not parent: if not parent:
@ -33,15 +38,24 @@ def show_display_item(
text_y_offs = 0.0 text_y_offs = 0.0
show_text = True show_text = True
if isinstance(itemwrapper.item, bacommon.bs.TicketsDisplayItem): itemtype = itemwrapper.item.get_type_id()
if itemtype is ditm.ItemTypeID.TICKETS:
img = 'tickets' img = 'tickets'
img_y_offs = width * 0.11 img_y_offs = width * 0.11
text_y_offs = width * -0.15 text_y_offs = width * -0.15
elif isinstance(itemwrapper.item, bacommon.bs.TokensDisplayItem): 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:
img = 'coin' img = 'coin'
img_y_offs = width * 0.11 img_y_offs = width * 0.11
text_y_offs = width * -0.15 text_y_offs = width * -0.15
elif isinstance(itemwrapper.item, bacommon.bs.ChestDisplayItem): elif itemtype is ditm.ItemTypeID.CHEST:
assert isinstance(
itemwrapper.item, bacommon.classic.ClassicChestDisplayItem
)
from baclassic._chest import ( from baclassic._chest import (
CHEST_APPEARANCE_DISPLAY_INFOS, CHEST_APPEARANCE_DISPLAY_INFOS,
CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
@ -63,9 +77,14 @@ def show_display_item(
tint_color=c_info.tint, tint_color=c_info.tint,
tint2_color=c_info.tint2, tint2_color=c_info.tint2,
) )
elif (
itemtype is ditm.ItemTypeID.TEST or itemtype is ditm.ItemTypeID.UNKNOWN
):
pass
else:
assert_never(itemtype)
# Enable this for testing spacing. if debug:
if bool(False):
bauiv1.imagewidget( bauiv1.imagewidget(
parent=parent, parent=parent,
position=( position=(

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Hooks for C++ layer to use for ClassicAppMode.""" """Hooks for C++ layer to use for ClassicAppMode."""
from __future__ import annotations from __future__ import annotations
import logging import logging

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Input related functionality""" """Input related functionality"""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Music related functionality.""" """Music related functionality."""
from __future__ import annotations from __future__ import annotations
import copy import copy

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Networking related functionality.""" """Networking related functionality."""
from __future__ import annotations from __future__ import annotations
import zlib import zlib
@ -14,7 +15,7 @@ from typing import TYPE_CHECKING, override
from efro.error import CommunicationError from efro.error import CommunicationError
from efro.util import strip_exception_tracebacks from efro.util import strip_exception_tracebacks
import bacommon.bs import bacommon.classic
import babase import babase
import bascenev1 import bascenev1
@ -113,7 +114,7 @@ class MasterServerV1CallThread(threading.Thread):
dataenc = urllib.parse.urlencode(self._data) dataenc = urllib.parse.urlencode(self._data)
mresponse = plus.cloud.send_message( mresponse = plus.cloud.send_message(
bacommon.bs.LegacyRequest( bacommon.classic.LegacyRequest(
self._request, self._request,
self._request_type, self._request_type,
classic.legacy_user_agent_string, classic.legacy_user_agent_string,
@ -168,7 +169,7 @@ class MasterServerV1CallThread(threading.Thread):
if self._callback is not None: if self._callback is not None:
babase.pushcall( babase.pushcall(
babase.Call(self._run_callback, response_data), babase.CallStrict(self._run_callback, response_data),
from_other_thread=True, from_other_thread=True,
) )

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to running the game in server-mode.""" """Functionality related to running the game in server-mode."""
from __future__ import annotations from __future__ import annotations
import sys import sys
@ -107,6 +108,12 @@ class ServerController:
self._playlist_fetch_got_response = False self._playlist_fetch_got_response = False
self._playlist_fetch_code = -1 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 # Now sit around doing any pre-launch prep such as waiting for
# account sign-in or fetching playlists; this will kick off the # account sign-in or fetching playlists; this will kick off the
# session once done. # session once done.
@ -427,8 +434,6 @@ class ServerController:
classic.teams_series_length = self._config.teams_series_length classic.teams_series_length = self._config.teams_series_length
classic.ffa_series_length = self._config.ffa_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( bascenev1.set_enable_default_kick_voting(
self._config.enable_default_kick_voting self._config.enable_default_kick_voting
) )
@ -451,7 +456,6 @@ class ServerController:
bascenev1.set_player_rejoin_cooldown( bascenev1.set_player_rejoin_cooldown(
self._config.player_rejoin_cooldown self._config.player_rejoin_cooldown
) )
bascenev1.set_max_players_override( bascenev1.set_max_players_override(
self._config.session_max_players_override self._config.session_max_players_override
) )
@ -470,6 +474,6 @@ class ServerController:
bascenev1.new_host_session(sessiontype) bascenev1.new_host_session(sessiontype)
# Run an access check if we're trying to make a public party. # Run an access check if we're trying to make a public party.
if not self._ran_access_check : if self._config.party_is_public and not self._ran_access_check:
self._run_access_check() self._run_access_check()
self._ran_access_check = True self._ran_access_check = True

View file

@ -292,6 +292,18 @@ class StoreSubsystem:
'icons.explodinary': { 'icons.explodinary': {
'icon': babase.charstr(babase.SpecialChar.EXPLODINARY_LOGO) '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 return babase.app.classic.store_items
@ -569,7 +581,7 @@ class StoreSubsystem:
def get_unowned_maps(self) -> list[str]: def get_unowned_maps(self) -> list[str]:
"""Return the list of local maps not owned by the current account.""" """Return the list of local maps not owned by the current account."""
classic = babase.app.classic classic = babase.app.classic
purchases = classic.purchases if classic is not None else set() purchases = classic.purchases if classic is not None else frozenset()
unowned_maps: set[str] = set() unowned_maps: set[str] = set()
if babase.app.env.gui: if babase.app.env.gui:
for map_section in self.get_store_layout()['maps']: for map_section in self.get_store_layout()['maps']:
@ -583,7 +595,9 @@ class StoreSubsystem:
"""Return present game types not owned by the current account.""" """Return present game types not owned by the current account."""
try: try:
classic = babase.app.classic classic = babase.app.classic
purchases = classic.purchases if classic is not None else set() purchases = (
classic.purchases if classic is not None else frozenset()
)
unowned_games: set[type[bascenev1.GameActivity]] = set() unowned_games: set[type[bascenev1.GameActivity]] = set()
if babase.app.env.gui: if babase.app.env.gui:
for section in self.get_store_layout()['minigames']: for section in self.get_store_layout()['minigames']:

View file

@ -3,6 +3,7 @@
"""Functionality related to classic game tips. """Functionality related to classic game tips.
These can be shown at opportune times such as between rounds.""" These can be shown at opportune times such as between rounds."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from bacommon.bs import ClassicChestAppearance from bacommon.classic import ClassicChestAppearance
import babase import babase
import bauiv1 import bauiv1
import bascenev1 import bascenev1

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Music playback functionality using the Mac Music (formerly iTunes) app.""" """Music playback functionality using the Mac Music (formerly iTunes) app."""
from __future__ import annotations from __future__ import annotations
import logging import logging
@ -95,7 +96,7 @@ class _MacMusicAppThread(threading.Thread):
def do_print() -> None: def do_print() -> None:
babase.apptimer( babase.apptimer(
0.5, 0.5,
babase.Call( babase.CallStrict(
babase.screenmessage, babase.screenmessage,
babase.Lstr(resource='usingItunesText'), babase.Lstr(resource='usingItunesText'),
(0, 1, 0), (0, 1, 0),
@ -198,7 +199,9 @@ class _MacMusicAppThread(threading.Thread):
except Exception as exc: except Exception as exc:
print('Error getting iTunes playlists:', exc) print('Error getting iTunes playlists:', exc)
playlists = [] playlists = []
babase.pushcall(babase.Call(target, playlists), from_other_thread=True) babase.pushcall(
babase.CallStrict(target, playlists), from_other_thread=True
)
def _handle_play_command(self, target: str | None) -> None: def _handle_play_command(self, target: str | None) -> None:
if target is None: if target is None:
@ -246,7 +249,7 @@ class _MacMusicAppThread(threading.Thread):
pass pass
else: else:
babase.pushcall( babase.pushcall(
babase.Call( babase.CallStrict(
babase.screenmessage, babase.screenmessage,
babase.app.lang.get_resource('playlistNotFoundText') babase.app.lang.get_resource('playlistNotFoundText')
+ ': \'' + ': \''

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Music playback using OS functionality exposed through the C++ layer.""" """Music playback using OS functionality exposed through the C++ layer."""
from __future__ import annotations from __future__ import annotations
import os import os
@ -151,7 +152,7 @@ class _PickFolderSongThread(threading.Thread):
).evaluate() ).evaluate()
) )
babase.pushcall( babase.pushcall(
babase.Call(self._callback, all_files, None), babase.CallStrict(self._callback, all_files, None),
from_other_thread=True, from_other_thread=True,
) )
except Exception as exc: except Exception as exc:
@ -162,6 +163,6 @@ class _PickFolderSongThread(threading.Thread):
except Exception: except Exception:
err_str = '<ENCERR4523>' err_str = '<ENCERR4523>'
babase.pushcall( babase.pushcall(
babase.Call(self._callback, self._path, err_str), babase.CallStrict(self._callback, self._path, err_str),
from_other_thread=True, from_other_thread=True,
) )

View file

@ -0,0 +1,75 @@
# 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

View file

@ -1,6 +1,12 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to cloud based assets.""" """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.
"""
from __future__ import annotations from __future__ import annotations

View file

@ -1,6 +1,12 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to the bacloud tool.""" """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.
"""
from __future__ import annotations from __future__ import annotations
@ -14,7 +20,7 @@ if TYPE_CHECKING:
# Version is sent to the master-server with all commands. Can be incremented # 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. # if we need to change behavior server-side to go along with client changes.
BACLOUD_VERSION = 13 BACLOUD_VERSION = 14
def asset_file_cache_path(filehash: str) -> str: def asset_file_cache_path(filehash: str) -> str:
@ -91,12 +97,30 @@ class ResponseData:
#: response processing (including error handling) occurs. #: response processing (including error handling) occurs.
message: Annotated[str | None, IOAttrs('m', store_default=False)] = None message: Annotated[str | None, IOAttrs('m', store_default=False)] = None
#: End arg for message print() call. #: Value for the 'end' arg of the message print() call.
message_end: Annotated[str, IOAttrs('m_end', store_default=False)] = '\n' message_end: Annotated[str, IOAttrs('m_end', store_default=False)] = '\n'
#: If present, client should abort with this error message. #: 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.
error: Annotated[str | None, IOAttrs('e', store_default=False)] = None 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 #: How long to wait before proceeding with remaining response (can
#: be useful when waiting for server progress in a loop). #: be useful when waiting for server progress in a loop).
delay_seconds: Annotated[float, IOAttrs('d', store_default=False)] = 0.0 delay_seconds: Annotated[float, IOAttrs('d', store_default=False)] = 0.0
@ -170,6 +194,17 @@ class ResponseData:
#: End arg for end_message print() call. #: End arg for end_message print() call.
end_message_end: Annotated[str, IOAttrs('eme', store_default=False)] = '\n' 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 #: If present, this command is run with these args at the end of
#: response processing. #: response processing.
end_command: Annotated[ end_command: Annotated[

File diff suppressed because it is too large Load diff

View file

@ -1,139 +0,0 @@
# 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',
]

View file

@ -1,180 +0,0 @@
# 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

View file

@ -1,308 +0,0 @@
# 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'

View file

@ -1,180 +0,0 @@
# 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

View file

@ -1,6 +1,12 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to game builds.""" """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.
"""
from __future__ import annotations from __future__ import annotations

View file

@ -0,0 +1,71 @@
# 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',
]

View file

@ -10,12 +10,12 @@ from dataclasses import dataclass
from typing import Annotated from typing import Annotated
from efro.dataclassio import ioprepped, IOAttrs from efro.dataclassio import ioprepped, IOAttrs
from bacommon.bs._chest import ClassicChestAppearance from bacommon.classic._chest import ClassicChestAppearance
@ioprepped @ioprepped
@dataclass @dataclass
class ClassicAccountLiveData: class ClassicLiveAccountClientData:
"""Live account data fed to the client in the bs classic app mode.""" """Live account data fed to the client in the bs classic app mode."""
@dataclass @dataclass
@ -44,6 +44,12 @@ class ClassicAccountLiveData:
ASK_FOR_REVIEW = 'r' ASK_FOR_REVIEW = 'r'
class StoreStyle(Enum):
"""Special looks for the store."""
NORMAL = 'n'
SANTA = 's'
tickets: Annotated[int, IOAttrs('ti')] tickets: Annotated[int, IOAttrs('ti')]
tokens: Annotated[int, IOAttrs('to')] tokens: Annotated[int, IOAttrs('to')]
@ -71,3 +77,7 @@ class ClassicAccountLiveData:
purchases_state: Annotated[str | None, IOAttrs('p')] purchases_state: Annotated[str | None, IOAttrs('p')]
flags: Annotated[set[Flag], IOAttrs('f', soft_default_factory=set)] flags: Annotated[set[Flag], IOAttrs('f', soft_default_factory=set)]
store_style: Annotated[
StoreStyle, IOAttrs('s', enum_fallback=StoreStyle.NORMAL)
]

View file

@ -5,7 +5,11 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import Enum
from typing import assert_never from typing import assert_never, Annotated, override
from dataclasses import dataclass
from efro.dataclassio import ioprepped, IOAttrs
import bacommon.displayitem as ditm
class ClassicChestAppearance(Enum): class ClassicChestAppearance(Enum):
@ -24,7 +28,7 @@ class ClassicChestAppearance(Enum):
def pretty_name(self) -> str: def pretty_name(self) -> str:
"""Pretty name for the chest in English.""" """Pretty name for the chest in English."""
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
cls = type(self) cls = ClassicChestAppearance
if self is cls.UNKNOWN: if self is cls.UNKNOWN:
return 'Unknown Chest' return 'Unknown Chest'
@ -44,3 +48,20 @@ class ClassicChestAppearance(Enum):
return 'L6 Chest' return 'L6 Chest'
assert_never(self) 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, []

View file

@ -0,0 +1,28 @@
# 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, []

View file

@ -5,125 +5,38 @@
from __future__ import annotations from __future__ import annotations
import datetime import datetime
from enum import Enum
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Annotated, override from typing import Annotated, override
from efro.dataclassio import ioprepped, IOAttrs from efro.dataclassio import ioprepped, IOAttrs
from efro.message import Message, Response from efro.message import Message, Response
from bacommon.bs._displayitem import DisplayItemWrapper import bacommon.displayitem as ditm
from bacommon.bs._clienteffect import ClientEffect import bacommon.clouddialog as cdlg
from bacommon.bs._clouddialog import CloudDialogAction, CloudDialogWrapper import bacommon.clienteffect as clfx
from bacommon.bs._chest import ClassicChestAppearance from bacommon.classic._chest import ClassicChestAppearance
@ioprepped @ioprepped
@dataclass @dataclass
class ChestActionMessage(Message): class GetClassicLeaguePresidentButtonInfoMessage(Message):
"""Request action about a chest.""" """Curious who is president of my league?.."""
class Action(Enum): season: Annotated[str | None, IOAttrs('s')]
"""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 @override
@classmethod @classmethod
def get_response_types(cls) -> list[type[Response] | None]: def get_response_types(cls) -> list[type[Response] | None]:
return [ChestActionResponse] return [GetClassicLeaguePresidentButtonInfoResponse]
@ioprepped @ioprepped
@dataclass @dataclass
class ChestActionResponse(Response): class GetClassicLeaguePresidentButtonInfoResponse(Response):
"""Here's the results of that action you asked for, boss.""" """Here's that info about the president you asked for boss."""
# Tokens that were actually charged. # Lstr for the name shown on the button.
tokens_charged: Annotated[int, IOAttrs('t')] = 0 name: Annotated[str | None, IOAttrs('n')]
# 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 @ioprepped
@ -183,7 +96,7 @@ class InboxRequestMessage(Message):
class InboxRequestResponse(Response): class InboxRequestResponse(Response):
"""Here's that inbox contents you asked for, boss.""" """Here's that inbox contents you asked for, boss."""
wrappers: Annotated[list[CloudDialogWrapper], IOAttrs('w')] wrappers: Annotated[list[cdlg.Wrapper], IOAttrs('w')]
# Printable error if something goes wrong. # Printable error if something goes wrong.
error: Annotated[str | None, IOAttrs('e')] = None error: Annotated[str | None, IOAttrs('e')] = None
@ -241,7 +154,7 @@ class ChestInfoResponse(Response):
"""A possible set of prizes for this chest.""" """A possible set of prizes for this chest."""
weight: Annotated[float, IOAttrs('w')] weight: Annotated[float, IOAttrs('w')]
contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')] contents: Annotated[list[ditm.Wrapper], IOAttrs('c')]
appearance: Annotated[ appearance: Annotated[
ClassicChestAppearance, ClassicChestAppearance,
@ -307,7 +220,7 @@ class ScoreSubmitResponse(Response):
"""Did something to that inbox entry, boss.""" """Did something to that inbox entry, boss."""
# Things we should show on our end. # Things we should show on our end.
effects: Annotated[list[ClientEffect], IOAttrs('fx')] effects: Annotated[list[clfx.Effect], IOAttrs('fx')]
@ioprepped @ioprepped
@ -330,7 +243,7 @@ class SendInfoResponse(Response):
handled: Annotated[bool, IOAttrs('v')] handled: Annotated[bool, IOAttrs('v')]
message: Annotated[str | None, IOAttrs('m', store_default=False)] = None message: Annotated[str | None, IOAttrs('m', store_default=False)] = None
effects: Annotated[ effects: Annotated[list[clfx.Effect], IOAttrs('e', store_default=False)] = (
list[ClientEffect], IOAttrs('e', store_default=False) field(default_factory=list)
] = field(default_factory=list) )
legacy_code: Annotated[str | None, IOAttrs('l', store_default=False)] = None legacy_code: Annotated[str | None, IOAttrs('l', store_default=False)] = None

View file

@ -0,0 +1,224 @@
# 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

View file

@ -1,6 +1,12 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to cloud functionality.""" """Cloud 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 from __future__ import annotations
@ -10,9 +16,13 @@ from typing import TYPE_CHECKING, Annotated, override
from efro.message import Message, Response from efro.message import Message, Response
from efro.dataclassio import ioprepped, IOAttrs from efro.dataclassio import ioprepped, IOAttrs
from bacommon.analytics import AnalyticsEvent
from bacommon.securedata import SecureDataChecker from bacommon.securedata import SecureDataChecker
from bacommon.transfer import DirectoryManifest from bacommon.transfer import DirectoryManifest
from bacommon.login import LoginType from bacommon.login import LoginType
from bacommon.docui import DocUIRequest, DocUIResponse
import bacommon.displayitem as ditm
import bacommon.clienteffect as clfx
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
@ -357,3 +367,120 @@ class CloudValsResponse(Response):
"""Here's them cloud vals ya asked for, boss.""" """Here's them cloud vals ya asked for, boss."""
vals: Annotated[CloudVals, IOAttrs('v')] 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')]

View file

@ -0,0 +1,29 @@
# 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',
]

View file

@ -0,0 +1,142 @@
# 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')]

View file

@ -0,0 +1,233 @@
# 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
)
)

View file

@ -1,6 +1,12 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""DisplayItem related functionality.""" """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.
"""
from __future__ import annotations from __future__ import annotations
@ -11,20 +17,19 @@ from typing import Annotated, override, assert_never
from efro.util import pairs_to_flat from efro.util import pairs_to_flat
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType 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.""" """Type ID for each of our subclasses."""
UNKNOWN = 'u' UNKNOWN = 'u'
TICKETS = 't' TICKETS = 't'
TICKETS_PURPLE = 'tp'
TOKENS = 'k' TOKENS = 'k'
TEST = 's' TEST = 's'
CHEST = 'c' CHEST = 'c'
class DisplayItem(IOMultiType[DisplayItemTypeID]): class Item(IOMultiType[ItemTypeID]):
"""Some amount of something that can be shown or described. """Some amount of something that can be shown or described.
Used to depict chest contents, inventory, rewards, etc. Used to depict chest contents, inventory, rewards, etc.
@ -32,7 +37,7 @@ class DisplayItem(IOMultiType[DisplayItemTypeID]):
@override @override
@classmethod @classmethod
def get_type_id(cls) -> DisplayItemTypeID: def get_type_id(cls) -> ItemTypeID:
# Require child classes to supply this themselves. If we did a # Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import # full type registry/lookup here it would require us to import
# everything and would prevent lazy loading. # everything and would prevent lazy loading.
@ -40,21 +45,25 @@ class DisplayItem(IOMultiType[DisplayItemTypeID]):
@override @override
@classmethod @classmethod
def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]: def get_type(cls, type_id: ItemTypeID) -> type[Item]:
"""Return the subclass for each of our type-ids.""" """Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
t = DisplayItemTypeID t = ItemTypeID
if type_id is t.UNKNOWN: if type_id is t.UNKNOWN:
return UnknownDisplayItem return Unknown
if type_id is t.TICKETS: if type_id is t.TICKETS:
return TicketsDisplayItem return Tickets
if type_id is t.TICKETS_PURPLE:
return PurpleTickets
if type_id is t.TOKENS: if type_id is t.TOKENS:
return TokensDisplayItem return Tokens
if type_id is t.TEST: if type_id is t.TEST:
return TestDisplayItem return Test
if type_id is t.CHEST: if type_id is t.CHEST:
return ChestDisplayItem from bacommon.classic._chest import ClassicChestDisplayItem
return ClassicChestDisplayItem
# Important to make sure we provide all types. # Important to make sure we provide all types.
assert_never(type_id) assert_never(type_id)
@ -62,31 +71,34 @@ class DisplayItem(IOMultiType[DisplayItemTypeID]):
def get_description(self) -> tuple[str, list[tuple[str, str]]]: def get_description(self) -> tuple[str, list[tuple[str, str]]]:
"""Return a string description and subs for the item. """Return a string description and subs for the item.
These decriptions are baked into the DisplayItemWrapper and Will be translated on the client using the 'displayItemNames'
Lstr category.
These decriptions are baked into the display-item wrapper and
should be accessed from there when available. This allows should be accessed from there when available. This allows
clients to give descriptions even for newer display items they clients to give descriptions even for newer display item types
don't recognize. they don't recognize.
""" """
raise NotImplementedError() raise NotImplementedError()
# Implement fallbacks so client can digest item lists even if they # Implement fallbacks so client can digest item lists even if they
# contain unrecognized stuff. DisplayItemWrapper contains basic # contain unrecognized stuff. The wrapper contains basic
# baked down info that they can still use in such cases. # baked down info that they can still use in such cases.
@override @override
@classmethod @classmethod
def get_unknown_type_fallback(cls) -> DisplayItem: def get_unknown_type_fallback(cls) -> Item:
return UnknownDisplayItem() return Unknown()
@ioprepped @ioprepped
@dataclass @dataclass
class UnknownDisplayItem(DisplayItem): class Unknown(Item):
"""Something we don't know how to display.""" """Something we don't know how to display."""
@override @override
@classmethod @classmethod
def get_type_id(cls) -> DisplayItemTypeID: def get_type_id(cls) -> ItemTypeID:
return DisplayItemTypeID.UNKNOWN return ItemTypeID.UNKNOWN
@override @override
def get_description(self) -> tuple[str, list[tuple[str, str]]]: def get_description(self) -> tuple[str, list[tuple[str, str]]]:
@ -94,23 +106,23 @@ class UnknownDisplayItem(DisplayItem):
# Make noise but don't break. # Make noise but don't break.
logging.exception( logging.exception(
'UnknownDisplayItem.get_description() should never be called.' 'Unknown.get_description() should never be called.'
' Always access descriptions on the DisplayItemWrapper.' ' Always access descriptions on the display-item wrapper.'
) )
return 'Unknown', [] return 'Unknown', []
@ioprepped @ioprepped
@dataclass @dataclass
class TicketsDisplayItem(DisplayItem): class Tickets(Item):
"""Some amount of tickets.""" """Some amount of tickets."""
count: Annotated[int, IOAttrs('c')] count: Annotated[int, IOAttrs('c')]
@override @override
@classmethod @classmethod
def get_type_id(cls) -> DisplayItemTypeID: def get_type_id(cls) -> ItemTypeID:
return DisplayItemTypeID.TICKETS return ItemTypeID.TICKETS
@override @override
def get_description(self) -> tuple[str, list[tuple[str, str]]]: def get_description(self) -> tuple[str, list[tuple[str, str]]]:
@ -119,15 +131,32 @@ class TicketsDisplayItem(DisplayItem):
@ioprepped @ioprepped
@dataclass @dataclass
class TokensDisplayItem(DisplayItem): 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):
"""Some amount of tokens.""" """Some amount of tokens."""
count: Annotated[int, IOAttrs('c')] count: Annotated[int, IOAttrs('c')]
@override @override
@classmethod @classmethod
def get_type_id(cls) -> DisplayItemTypeID: def get_type_id(cls) -> ItemTypeID:
return DisplayItemTypeID.TOKENS return ItemTypeID.TOKENS
@override @override
def get_description(self) -> tuple[str, list[tuple[str, str]]]: def get_description(self) -> tuple[str, list[tuple[str, str]]]:
@ -136,47 +165,34 @@ class TokensDisplayItem(DisplayItem):
@ioprepped @ioprepped
@dataclass @dataclass
class TestDisplayItem(DisplayItem): class Test(Item):
"""Fills usable space for a display-item - good for calibration.""" """Fills usable space for a display-item - good for calibration."""
@override @override
@classmethod @classmethod
def get_type_id(cls) -> DisplayItemTypeID: def get_type_id(cls) -> ItemTypeID:
return DisplayItemTypeID.TEST return ItemTypeID.TEST
@override @override
def get_description(self) -> tuple[str, list[tuple[str, str]]]: def get_description(self) -> tuple[str, list[tuple[str, str]]]:
return 'Test Display Item Here', [] return 'Test', []
@ioprepped @ioprepped
@dataclass @dataclass
class ChestDisplayItem(DisplayItem): class Wrapper:
"""Display a chest.""" """Wraps a display-item and some baked out info.
appearance: Annotated[ClassicChestAppearance, IOAttrs('a')] This allows clients to at least give descriptions of new
display-item types they may not have locally.
"""
@override item: Annotated[Item, IOAttrs('i')]
@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: Annotated[str, IOAttrs('d')]
description_subs: Annotated[list[str] | None, IOAttrs('s')] description_subs: Annotated[list[str] | None, IOAttrs('s')]
@classmethod @classmethod
def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper: def for_item(cls, item: Item) -> Wrapper:
"""Convenience method to wrap a DisplayItem.""" """Convenience method to wrap a display-item."""
desc, subs = item.get_description() desc, subs = item.get_description()
return DisplayItemWrapper(item, desc, pairs_to_flat(subs)) return Wrapper(item, desc, pairs_to_flat(subs))

View file

@ -0,0 +1,30 @@
# 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',
]

View file

@ -0,0 +1,172 @@
# 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

783
dist/ba_data/python/bacommon/docui/v1.py vendored Normal file
View file

@ -0,0 +1,783 @@
# 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

View file

@ -71,6 +71,7 @@ class Locale(Enum):
VENETIAN = 'venetn' VENETIAN = 'venetn'
VIETNAMESE = 'viet' VIETNAMESE = 'viet'
KAZAKH = 'kazk' KAZAKH = 'kazk'
JAPANESE = 'jpn'
# Note: We use if-statement chains here so we can use assert_never() # 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 # to ensure we cover all existing values. But we cache lookups so
@ -87,7 +88,7 @@ class Locale(Enum):
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
cls = type(self) cls = Locale
if self is cls.ENGLISH: if self is cls.ENGLISH:
return 'English' return 'English'
@ -175,6 +176,8 @@ class Locale(Enum):
return 'Vietnamese' return 'Vietnamese'
if self is cls.KAZAKH: if self is cls.KAZAKH:
return 'Kazakh' return 'Kazakh'
if self is cls.JAPANESE:
return 'Japanese'
# Make sure we've covered all cases. # Make sure we've covered all cases.
assert_never(self) assert_never(self)
@ -206,7 +209,7 @@ class Locale(Enum):
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
cls = type(self) cls = Locale
if self is cls.ENGLISH: if self is cls.ENGLISH:
return 'English' return 'English'
@ -296,6 +299,8 @@ class Locale(Enum):
return 'Vietnamese' return 'Vietnamese'
if self is cls.KAZAKH: if self is cls.KAZAKH:
return 'Kazakh' return 'Kazakh'
if self is cls.JAPANESE:
return 'Japanese'
# Make sure we've covered all cases. # Make sure we've covered all cases.
assert_never(self) assert_never(self)
@ -306,7 +311,7 @@ class Locale(Enum):
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
cls = type(self) cls = Locale
R = LocaleResolved R = LocaleResolved
if self is cls.ENGLISH: if self is cls.ENGLISH:
@ -389,6 +394,8 @@ class Locale(Enum):
return R.VIETNAMESE return R.VIETNAMESE
if self is cls.KAZAKH: if self is cls.KAZAKH:
return R.KAZAKH return R.KAZAKH
if self is cls.JAPANESE:
return R.JAPANESE
# Make sure we're covering all cases. # Make sure we're covering all cases.
assert_never(self) assert_never(self)
@ -444,6 +451,7 @@ class LocaleResolved(Enum):
VENETIAN = 'venetn' VENETIAN = 'venetn'
VIETNAMESE = 'viet' VIETNAMESE = 'viet'
KAZAKH = 'kazk' KAZAKH = 'kazk'
JAPANESE = 'jpn'
# Note: We use if-statement chains here so we can use assert_never() # 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 # to ensure we cover all existing values. But we cache lookups so
@ -464,7 +472,7 @@ class LocaleResolved(Enum):
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
cls = type(self) cls = LocaleResolved
if self is cls.ENGLISH: if self is cls.ENGLISH:
return Locale.ENGLISH return Locale.ENGLISH
@ -546,6 +554,8 @@ class LocaleResolved(Enum):
return Locale.VIETNAMESE return Locale.VIETNAMESE
if self is cls.KAZAKH: if self is cls.KAZAKH:
return Locale.KAZAKH return Locale.KAZAKH
if self is cls.JAPANESE:
return Locale.JAPANESE
# Make sure we're covering all cases. # Make sure we're covering all cases.
assert_never(self) assert_never(self)
@ -561,7 +571,7 @@ class LocaleResolved(Enum):
""" """
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
cls = type(self) cls = LocaleResolved
val: str | None = None val: str | None = None
@ -647,6 +657,8 @@ class LocaleResolved(Enum):
val = 'vi' val = 'vi'
elif self is cls.KAZAKH: elif self is cls.KAZAKH:
val = 'kk' val = 'kk'
elif self is cls.JAPANESE:
val = 'ja'
else: else:
# Make sure we cover all cases. # Make sure we cover all cases.
assert_never(self) assert_never(self)
@ -668,9 +680,9 @@ class LocaleResolved(Enum):
return val return val
@classmethod @staticmethod
@lru_cache(maxsize=128) @lru_cache(maxsize=128)
def from_tag(cls, tag: str) -> LocaleResolved: def from_tag(tag: str) -> LocaleResolved:
"""Return a locale for a given string tag. """Return a locale for a given string tag.
Tags can be provided in BCP 47 form ('en-US') or POSIX locale Tags can be provided in BCP 47 form ('en-US') or POSIX locale
@ -680,6 +692,8 @@ class LocaleResolved(Enum):
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
cls = LocaleResolved
# POSIX locale strings can contain a dot followed by an # POSIX locale strings can contain a dot followed by an
# encoding. Strip that off. # encoding. Strip that off.
tag2 = tag.split('.')[0] tag2 = tag.split('.')[0]
@ -838,6 +852,8 @@ class LocaleResolved(Enum):
return cls.VIETNAMESE return cls.VIETNAMESE
if lang == 'kk': if lang == 'kk':
return cls.KAZAKH return cls.KAZAKH
if lang == 'ja':
return cls.JAPANESE
# Make noise if we come across something unexpected so we can # Make noise if we come across something unexpected so we can
# add it. # add it.

View file

@ -108,6 +108,11 @@ class LoggerControlConfig:
for logname in existinglognames: for logname in existinglognames:
logger = logging.getLogger(logname) logger = logging.getLogger(logname)
if logger.getEffectiveLevel() != self.get_effective_level(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( logging.error(
'loggercontrol effective-level sanity check failed;' 'loggercontrol effective-level sanity check failed;'
' expected logger %s to have effective level %s' ' expected logger %s to have effective level %s'

View file

@ -48,7 +48,7 @@ class ClientLoggerName(Enum):
"""Return a short description for the logger.""" """Return a short description for the logger."""
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
cls = type(self) cls = ClientLoggerName
if self is cls.BA: if self is cls.BA:
return 'top level Ballistica logger - use to adjust everything' return 'top level Ballistica logger - use to adjust everything'
if self is cls.ENV: if self is cls.ENV:

View file

@ -32,7 +32,7 @@ class LoginType(Enum):
@property @property
def displayname(self) -> str: def displayname(self) -> str:
"""A human readable name for this value.""" """A human readable name for this value."""
cls = type(self) cls = LoginType
match self: match self:
case cls.EMAIL: case cls.EMAIL:
return 'Email/Password' return 'Email/Password'
@ -44,7 +44,7 @@ class LoginType(Enum):
@property @property
def displaynameshort(self) -> str: def displaynameshort(self) -> str:
"""A short human readable name for this value.""" """A short human readable name for this value."""
cls = type(self) cls = LoginType
match self: match self:
case cls.EMAIL: case cls.EMAIL:
return 'Email' return 'Email'

View file

@ -1,6 +1,12 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Network related data and functionality.""" """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.
"""
from __future__ import annotations from __future__ import annotations

View file

@ -1,6 +1,12 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to verifying ballistica server generated data.""" """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.
"""
import datetime import datetime
from dataclasses import dataclass from dataclasses import dataclass

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to the server manager script.""" """Functionality related to the server manager script."""
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import Enum
@ -29,13 +30,19 @@ class ServerConfig:
# If True, all connecting clients will be authenticated through the # If True, all connecting clients will be authenticated through the
# master server to screen for fake account info. Generally this # master server to screen for fake account info. Generally this
# should always be enabled unless you are hosting on a LAN with no # should always be enabled unless you are hosting on a LAN with no
# internet connection. # 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.
authenticate_clients: bool = True authenticate_clients: bool = True
# IDs of server admins. Server admins are not kickable through the # IDs of server admins. Server admins are not kickable through the
# default kick vote system and they are able to kick players without # default kick vote system and they are able to kick players without
# a vote. To get your account id, enter 'getaccountid' in # a vote. If protocol_version is set to 36 or newer this will use V2
# settings->advanced->enter-code. # 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
admins: list[str] = field(default_factory=list) admins: list[str] = field(default_factory=list)
# Whether the default kick-voting system is enabled. # Whether the default kick-voting system is enabled.
@ -175,7 +182,10 @@ class ServerConfig:
# Protocol version we host with. Currently the default is 33 which # Protocol version we host with. Currently the default is 33 which
# still allows older 1.4 game clients to connect. Explicitly setting # still allows older 1.4 game clients to connect. Explicitly setting
# to 35 no longer allows those clients but adds/fixes a few things # to 35 no longer allows those clients but adds/fixes a few things
# such as making camera shake properly work in net games. # 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.
protocol_version: int | None = None protocol_version: int | None = None
# (internal) stress-testing mode. # (internal) stress-testing mode.

121
dist/ba_data/python/bacommon/text.py vendored Normal file
View file

@ -0,0 +1,121 @@
# 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'

View file

@ -1,6 +1,12 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to transferring files/data.""" """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.
"""
from __future__ import annotations from __future__ import annotations

View file

@ -2,10 +2,10 @@
# #
"""Public types for assets-v1 workspaces. """Public types for assets-v1 workspaces.
These types may only be used server-side, but they are exposed here While this module is currently only used server-side, its source code
for reference when setting workspace config data by hand or for use can be useful as reference when setting workspace config data by hand or
in client-side workspace modification tools. There may be advanced for use in client-side workspace modification tools. There may be
settings that are not accessible through the UI/etc. advanced settings that are not accessible through the UI/etc.
""" """
from __future__ import annotations from __future__ import annotations
@ -18,7 +18,6 @@ from typing import TYPE_CHECKING, Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
from bacommon.locale import Locale from bacommon.locale import Locale
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
@ -85,8 +84,8 @@ class AssetsV1StringFileV1(AssetsV1StringFile):
NONE = 'none' NONE = 'none'
TITLE = 'title' TITLE = 'title'
INTENSE = 'intense' LOUD = 'loud'
SUBTLE = 'subtle' SOFT = 'soft'
@override @override
@classmethod @classmethod
@ -121,7 +120,7 @@ class AssetsV1PathValsTypeID(Enum):
"""Types of vals we can store for paths.""" """Types of vals we can store for paths."""
TEX_V1 = 'tex_v1' TEX_V1 = 'tex_v1'
# STR_V1 = 'str_v1' STR_V1 = 'str_v1'
class AssetsV1PathVals(IOMultiType[AssetsV1PathValsTypeID]): class AssetsV1PathVals(IOMultiType[AssetsV1PathValsTypeID]):
@ -151,6 +150,9 @@ class AssetsV1PathVals(IOMultiType[AssetsV1PathValsTypeID]):
if type_id is t.TEX_V1: if type_id is t.TEX_V1:
return AssetsV1PathValsTexV1 return AssetsV1PathValsTexV1
if type_id is t.STR_V1:
return AssetsV1PathValsStrV1
# Important to make sure we provide all types. # Important to make sure we provide all types.
assert_never(type_id) assert_never(type_id)
@ -176,3 +178,20 @@ class AssetsV1PathValsTexV1(AssetsV1PathVals):
@classmethod @classmethod
def get_type_id(cls) -> AssetsV1PathValsTypeID: def get_type_id(cls) -> AssetsV1PathValsTypeID:
return AssetsV1PathValsTypeID.TEX_V1 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

View file

@ -14,6 +14,7 @@ Ballistica can be used without explicitly configuring the environment in
order to integrate it in arbitrary Python environments, but this may order to integrate it in arbitrary Python environments, but this may
cause some features to be disabled or behave differently than expected. cause some features to be disabled or behave differently than expected.
""" """
from __future__ import annotations from __future__ import annotations
import os import os
@ -56,8 +57,8 @@ logger = logging.getLogger('ba.env')
# Build number and version of the ballistica binary we expect to be # Build number and version of the ballistica binary we expect to be
# using. # using.
TARGET_BALLISTICA_BUILD = 22584 TARGET_BALLISTICA_BUILD = 22714
TARGET_BALLISTICA_VERSION = '1.7.53' TARGET_BALLISTICA_VERSION = '1.7.61'
@dataclass @dataclass
@ -95,7 +96,7 @@ class EnvConfig:
#: stderr into the engine so they show up on in-app consoles, etc. #: stderr into the engine so they show up on in-app consoles, etc.
log_handler: LogHandler | None 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 initial_app_config: Any
#: Timestamp when we first started doing stuff. #: Timestamp when we first started doing stuff.

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to ads.""" """Functionality related to ads."""
from __future__ import annotations from __future__ import annotations
import time import time

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Provides plus app subsystem.""" """Provides plus app subsystem."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override
@ -13,7 +14,7 @@ from baplus._ads import AdsSubsystem
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Any from typing import Callable, Any
import bacommon.bs import bacommon.classic
from babase import AccountV2Subsystem from babase import AccountV2Subsystem
from baplus._cloud import CloudSubsystem from baplus._cloud import CloudSubsystem
@ -142,14 +143,6 @@ class PlusAppSubsystem(AppSubsystem):
""":meta private:""" """:meta private:"""
return _baplus.get_v1_account_state_num() 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 @staticmethod
def get_v1_account_type() -> str: def get_v1_account_type() -> str:
""":meta private:""" """:meta private:"""

View file

@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, overload
from efro.error import CommunicationError from efro.error import CommunicationError
from efro.call import CallbackSet from efro.call import CallbackSet
from efro.dataclassio import dataclass_from_dict, dataclass_to_dict from efro.dataclassio import dataclass_from_dict, dataclass_to_dict
import bacommon.bs import bacommon.classic
import bacommon.cloud import bacommon.cloud
import babase import babase
@ -19,7 +19,8 @@ if TYPE_CHECKING:
from typing import Callable, Any from typing import Callable, Any
from efro.message import Message, Response, BoolResponse from efro.message import Message, Response, BoolResponse
import bacommon.bs import bacommon.classic
import bacommon.clouddialog as cdlg
# TODO: Should make it possible to define a protocol in bacommon.cloud and # TODO: Should make it possible to define a protocol in bacommon.cloud and
@ -36,6 +37,8 @@ class CloudSubsystem(babase.AppSubsystem):
""" """
#: General engine config values provided by the cloud. #: General engine config values provided by the cloud.
#:
#: :meta private:
vals: bacommon.cloud.CloudVals vals: bacommon.cloud.CloudVals
def __init__(self) -> None: def __init__(self) -> None:
@ -214,9 +217,9 @@ class CloudSubsystem(babase.AppSubsystem):
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
msg: bacommon.bs.GetClassicPurchasesMessage, msg: bacommon.classic.GetClassicPurchasesMessage,
on_response: Callable[ on_response: Callable[
[bacommon.bs.GetClassicPurchasesResponse | Exception], None [bacommon.classic.GetClassicPurchasesResponse | Exception], None
], ],
) -> None: ... ) -> None: ...
@ -232,61 +235,59 @@ class CloudSubsystem(babase.AppSubsystem):
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
msg: bacommon.bs.PrivatePartyMessage, msg: bacommon.classic.PrivatePartyMessage,
on_response: Callable[ on_response: Callable[
[bacommon.bs.PrivatePartyResponse | Exception], None [bacommon.classic.PrivatePartyResponse | Exception], None
], ],
) -> None: ... ) -> None: ...
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
msg: bacommon.bs.InboxRequestMessage, msg: bacommon.classic.InboxRequestMessage,
on_response: Callable[ on_response: Callable[
[bacommon.bs.InboxRequestResponse | Exception], None [bacommon.classic.InboxRequestResponse | Exception], None
], ],
) -> None: ... ) -> None: ...
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
msg: bacommon.bs.CloudDialogActionMessage, msg: cdlg.ActionMessage,
on_response: Callable[[cdlg.ActionResponse | Exception], None],
) -> None: ...
@overload
def send_message_cb(
self,
msg: bacommon.classic.ChestInfoMessage,
on_response: Callable[ on_response: Callable[
[bacommon.bs.CloudDialogActionResponse | Exception], None [bacommon.classic.ChestInfoResponse | Exception], None
], ],
) -> None: ... ) -> None: ...
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
msg: bacommon.bs.ChestInfoMessage, msg: bacommon.cloud.ChestActionMessage,
on_response: Callable[ on_response: Callable[
[bacommon.bs.ChestInfoResponse | Exception], None [bacommon.cloud.ChestActionResponse | Exception], None
], ],
) -> None: ... ) -> None: ...
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
msg: bacommon.bs.ChestActionMessage, msg: bacommon.classic.GlobalProfileCheckMessage,
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], on_response: Callable[[BoolResponse | Exception], None],
) -> None: ... ) -> None: ...
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
msg: bacommon.bs.ScoreSubmitMessage, msg: bacommon.classic.ScoreSubmitMessage,
on_response: Callable[ on_response: Callable[
[bacommon.bs.ScoreSubmitResponse | Exception], None [bacommon.classic.ScoreSubmitResponse | Exception], None
], ],
) -> None: ... ) -> None: ...
@ -308,6 +309,39 @@ class CloudSubsystem(babase.AppSubsystem):
], ],
) -> None: ... ) -> 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( def send_message_cb(
self, self,
msg: Message, msg: Message,
@ -339,8 +373,13 @@ class CloudSubsystem(babase.AppSubsystem):
@overload @overload
def send_message( def send_message(
self, msg: bacommon.bs.LegacyRequest self, msg: bacommon.classic.LegacyRequest
) -> bacommon.bs.LegacyResponse: ... ) -> bacommon.classic.LegacyResponse: ...
@overload
def send_message(
self, msg: bacommon.cloud.FulfillDocUIRequest
) -> bacommon.cloud.FulfillDocUIResponse: ...
def send_message(self, msg: Message) -> Response | None: def send_message(self, msg: Message) -> Response | None:
"""Synchronously send a message to the cloud. """Synchronously send a message to the cloud.
@ -353,8 +392,8 @@ class CloudSubsystem(babase.AppSubsystem):
@overload @overload
async def send_message_async( async def send_message_async(
self, msg: bacommon.bs.SendInfoMessage self, msg: bacommon.classic.SendInfoMessage
) -> bacommon.bs.SendInfoResponse: ... ) -> bacommon.classic.SendInfoResponse: ...
@overload @overload
async def send_message_async( async def send_message_async(
@ -383,9 +422,14 @@ class CloudSubsystem(babase.AppSubsystem):
def subscribe_classic_account_data( def subscribe_classic_account_data(
self, self,
updatecall: Callable[[bacommon.bs.ClassicAccountLiveData], None], updatecall: Callable[
[bacommon.classic.ClassicLiveAccountClientData], None
],
) -> babase.CloudSubscription: ) -> babase.CloudSubscription:
"""Subscribe to classic account data.""" """Subscribe to classic account data.
:meta private:
"""
raise NotImplementedError( raise NotImplementedError(
'Cloud functionality is not present in this build.' 'Cloud functionality is not present in this build.'
) )

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Snippets of code for use by the c++ layer.""" """Snippets of code for use by the c++ layer."""
# (most of these are self-explanatory) # (most of these are self-explanatory)
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
from __future__ import annotations from __future__ import annotations

View file

@ -16,7 +16,6 @@ import logging
# other modules; the goal is to let most simple mods rely solely on this # other modules; the goal is to let most simple mods rely solely on this
# module to keep things simple. # module to keep things simple.
# from efro.util import set_canonical_module_names
from babase import ( from babase import (
ActivityNotFoundError, ActivityNotFoundError,
add_clean_frame_callback, add_clean_frame_callback,
@ -32,6 +31,8 @@ from babase import (
apptimer, apptimer,
AppTimer, AppTimer,
Call, Call,
CallPartial,
CallStrict,
ContextError, ContextError,
ContextRef, ContextRef,
displaytime, displaytime,
@ -63,6 +64,8 @@ from babase import (
unlock_all_input, unlock_all_input,
Vec3, Vec3,
WeakCall, WeakCall,
WeakCallPartial,
WeakCallStrict,
) )
from _bascenev1 import ( from _bascenev1 import (
@ -275,6 +278,8 @@ __all__ = [
'BaseTimer', 'BaseTimer',
'BoolSetting', 'BoolSetting',
'Call', 'Call',
'CallPartial',
'CallStrict',
'cameraflash', 'cameraflash',
'camerashake', 'camerashake',
'Campaign', 'Campaign',
@ -475,15 +480,11 @@ __all__ = [
'unlock_all_input', 'unlock_all_input',
'Vec3', 'Vec3',
'WeakCall', 'WeakCall',
'WeakCallPartial',
'WeakCallStrict',
'WinnerGroup', '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 # Sanity check: we want to keep ballistica's dependencies and
# bootstrapping order clearly defined; let's check a few particular # bootstrapping order clearly defined; let's check a few particular
# modules to make sure they never directly or indirectly import us # modules to make sure they never directly or indirectly import us

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Defines Activity class.""" """Defines Activity class."""
from __future__ import annotations from __future__ import annotations
import weakref import weakref
@ -12,9 +13,8 @@ import _bascenev1
from bascenev1._dependency import DependencyComponent from bascenev1._dependency import DependencyComponent
from bascenev1._messages import UNHANDLED from bascenev1._messages import UNHANDLED
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any, Self
import bascenev1 import bascenev1
@ -192,7 +192,7 @@ class Activity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
session = self._session() session = self._session()
if session is not None: if session is not None:
babase.pushcall( babase.pushcall(
babase.Call( babase.CallStrict(
session.transitioning_out_activity_was_freed, session.transitioning_out_activity_was_freed,
self.can_show_ad_on_death, self.can_show_ad_on_death,
) )
@ -286,7 +286,7 @@ class Activity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
ref = weakref.ref(self) ref = weakref.ref(self)
self._activity_death_check_timer = babase.AppTimer( self._activity_death_check_timer = babase.AppTimer(
5.0, 5.0,
babase.Call(self._check_activity_death, ref, [0]), babase.CallStrict(self._check_activity_death, ref, [0]),
repeat=True, repeat=True,
) )
@ -722,7 +722,7 @@ class Activity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
@classmethod @classmethod
def _check_activity_death( def _check_activity_death(
cls, activity_ref: weakref.ref[Activity], counter: list[int] cls, activity_ref: weakref.ref[Self], counter: list[int]
) -> None: ) -> None:
"""Sanity check to make sure an Activity was destroyed properly. """Sanity check to make sure an Activity was destroyed properly.

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Some handy base class and special purpose Activity types.""" """Some handy base class and special purpose Activity types."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override
@ -14,7 +15,6 @@ from bascenev1._player import EmptyPlayer
from bascenev1._team import EmptyTeam from bascenev1._team import EmptyTeam
from bascenev1._music import MusicType, setmusic from bascenev1._music import MusicType, setmusic
if TYPE_CHECKING: if TYPE_CHECKING:
import bascenev1 import bascenev1
from bascenev1._lobby import JoinInfo from bascenev1._lobby import JoinInfo
@ -54,7 +54,7 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
babase.unlock_all_input() babase.unlock_all_input()
assert babase.app.plus is not None assert babase.app.plus is not None
call = babase.Call(_bascenev1.new_host_session, main_menu_session) call = babase.CallStrict(_bascenev1.new_host_session, main_menu_session)
if classic.can_show_interstitial(): if classic.can_show_interstitial():
plus.ads.call_after_ad(call) plus.ads.call_after_ad(call)
else: else:
@ -172,7 +172,7 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
# If we're still kicking at the end of our assign-delay, assign this # If we're still kicking at the end of our assign-delay, assign this
# guy's input to trigger us. # guy's input to trigger us.
_bascenev1.timer( _bascenev1.timer(
time_till_assign, babase.WeakCall(self._safe_assign, player) time_till_assign, babase.WeakCallStrict(self._safe_assign, player)
) )
@override @override

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to co-op campaigns.""" """Functionality related to co-op campaigns."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View file

@ -19,7 +19,9 @@ class Collision:
@property @property
def position(self) -> bascenev1.Vec3: def position(self) -> bascenev1.Vec3:
"""The position of the current collision.""" """The position of the current collision."""
return babase.Vec3(_bascenev1.get_collision_info('position')) out = babase.Vec3(_bascenev1.get_collision_info('position'))
assert isinstance(out, babase.Vec3)
return out
@property @property
def sourcenode(self) -> bascenev1.Node: def sourcenode(self) -> bascenev1.Node:
@ -30,7 +32,7 @@ class Collision:
start of the collision callback). start of the collision callback).
""" """
node = _bascenev1.get_collision_info('sourcenode') node = _bascenev1.get_collision_info('sourcenode')
assert isinstance(node, (_bascenev1.Node, type(None))) assert isinstance(node, _bascenev1.Node | None)
if not node: if not node:
raise babase.NodeNotFoundError() raise babase.NodeNotFoundError()
return node return node
@ -45,7 +47,7 @@ class Collision:
currently-colliding node. currently-colliding node.
""" """
node = _bascenev1.get_collision_info('opposingnode') node = _bascenev1.get_collision_info('opposingnode')
assert isinstance(node, (_bascenev1.Node, type(None))) assert isinstance(node, _bascenev1.Node | None)
if not node: if not node:
raise babase.NodeNotFoundError() raise babase.NodeNotFoundError()
return node return node

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to co-op games.""" """Functionality related to co-op games."""
from __future__ import annotations from __future__ import annotations
import logging import logging
@ -64,11 +65,11 @@ class CoopGameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
if not arcade_or_demo: if not arcade_or_demo:
_bascenev1.timer( _bascenev1.timer(
3.8, babase.WeakCall(self._show_remaining_achievements) 3.8, babase.WeakCallStrict(self._show_remaining_achievements)
) )
# Preload achievement images in case we get some. # Preload achievement images in case we get some.
_bascenev1.timer(2.0, babase.WeakCall(self._preload_achievements)) _bascenev1.timer(2.0, babase.WeakCallStrict(self._preload_achievements))
# FIXME: this is now redundant with activityutils.getscoreconfig(); # FIXME: this is now redundant with activityutils.getscoreconfig();
# need to kill this. # need to kill this.
@ -232,7 +233,7 @@ class CoopGameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
"""Set up a beeping noise to play when any players are near death.""" """Set up a beeping noise to play when any players are near death."""
self._life_warning_beep = None self._life_warning_beep = None
self._life_warning_beep_timer = _bascenev1.Timer( self._life_warning_beep_timer = _bascenev1.Timer(
1.0, babase.WeakCall(self._update_life_warning), repeat=True 1.0, babase.WeakCallStrict(self._update_life_warning), repeat=True
) )
def _update_life_warning(self) -> None: def _update_life_warning(self) -> None:

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to coop-mode sessions.""" """Functionality related to coop-mode sessions."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override
@ -185,7 +186,9 @@ class CoopSession(Session):
def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None: def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
super().on_player_leave(sessionplayer) super().on_player_leave(sessionplayer)
_bascenev1.timer(2.0, babase.WeakCall(self._handle_empty_activity)) _bascenev1.timer(
2.0, babase.WeakCallStrict(self._handle_empty_activity)
)
def _handle_empty_activity(self) -> None: def _handle_empty_activity(self) -> None:
"""Handle cases where all players have left the current activity.""" """Handle cases where all players have left the current activity."""
@ -358,7 +361,7 @@ class CoopSession(Session):
{ {
'label': babase.Lstr(resource='restartText'), 'label': babase.Lstr(resource='restartText'),
'resume_on_call': False, 'resume_on_call': False,
'call': babase.WeakCall( 'call': babase.WeakCallPartial(
self._on_tournament_restart_menu_press self._on_tournament_restart_menu_press
), ),
} }
@ -367,7 +370,7 @@ class CoopSession(Session):
self._custom_menu_ui = [ self._custom_menu_ui = [
{ {
'label': babase.Lstr(resource='restartText'), 'label': babase.Lstr(resource='restartText'),
'call': babase.WeakCall(self.restart), 'call': babase.WeakCallStrict(self.restart),
} }
] ]

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Debugging functionality.""" """Debugging functionality."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to teams sessions.""" """Functionality related to teams sessions."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Provides GameActivity class.""" """Provides GameActivity class."""
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
from __future__ import annotations from __future__ import annotations
@ -405,7 +406,9 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
'tournamentIDs': [tournament_id], 'tournamentIDs': [tournament_id],
'source': 'in-game time remaining query', 'source': 'in-game time remaining query',
}, },
callback=babase.WeakCall(self._on_tournament_query_response), callback=babase.WeakCallPartial(
self._on_tournament_query_response
),
) )
def _on_tournament_query_response( def _on_tournament_query_response(
@ -805,7 +808,7 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
player.customdata['respawn_timer'] = _bascenev1.Timer( player.customdata['respawn_timer'] = _bascenev1.Timer(
respawn_time, respawn_time,
babase.WeakCall(self.spawn_player_if_exists, player), babase.WeakCallStrict(self.spawn_player_if_exists, player),
) )
player.customdata['respawn_icon'] = RespawnIcon( player.customdata['respawn_icon'] = RespawnIcon(
player, respawn_time player, respawn_time
@ -902,7 +905,7 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
self._powerup_drop_timer = _bascenev1.Timer( self._powerup_drop_timer = _bascenev1.Timer(
DEFAULT_POWERUP_INTERVAL, DEFAULT_POWERUP_INTERVAL,
babase.WeakCall(self._standard_drop_powerups), babase.WeakCallStrict(self._standard_drop_powerups),
repeat=True, repeat=True,
) )
self._standard_drop_powerups() self._standard_drop_powerups()
@ -927,7 +930,7 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
points = self.map.powerup_spawn_points points = self.map.powerup_spawn_points
for i in range(len(points)): for i in range(len(points)):
_bascenev1.timer( _bascenev1.timer(
i * 0.4, babase.WeakCall(self._standard_drop_powerup, i) i * 0.4, babase.WeakCallStrict(self._standard_drop_powerup, i)
) )
def _setup_standard_tnt_drops(self) -> None: def _setup_standard_tnt_drops(self) -> None:
@ -953,7 +956,9 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
return return
self._standard_time_limit_time = int(duration) self._standard_time_limit_time = int(duration)
self._standard_time_limit_timer = _bascenev1.Timer( self._standard_time_limit_timer = _bascenev1.Timer(
1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True 1.0,
babase.WeakCallStrict(self._standard_time_limit_tick),
repeat=True,
) )
self._standard_time_limit_text = NodeActor( self._standard_time_limit_text = NodeActor(
_bascenev1.newnode( _bascenev1.newnode(
@ -1043,7 +1048,9 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
# then we have to mess with contexts and whatnot since its currently # then we have to mess with contexts and whatnot since its currently
# not available in activity contexts. :-/ # not available in activity contexts. :-/
self._tournament_time_limit_timer = _bascenev1.BaseTimer( self._tournament_time_limit_timer = _bascenev1.BaseTimer(
1.0, babase.WeakCall(self._tournament_time_limit_tick), repeat=True 1.0,
babase.WeakCallStrict(self._tournament_time_limit_tick),
repeat=True,
) )
self._tournament_time_limit_title_text = NodeActor( self._tournament_time_limit_title_text = NodeActor(
_bascenev1.newnode( _bascenev1.newnode(

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to game results.""" """Functionality related to game results."""
from __future__ import annotations from __future__ import annotations
import copy import copy

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Snippets of code for use by the c++ layer.""" """Snippets of code for use by the c++ layer."""
# (most of these are self-explanatory) # (most of these are self-explanatory)
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
from __future__ import annotations from __future__ import annotations

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Functionality related to individual levels in a campaign.""" """Functionality related to individual levels in a campaign."""
from __future__ import annotations from __future__ import annotations
import copy import copy
@ -118,10 +119,11 @@ class Level:
def get_high_scores(self) -> dict: def get_high_scores(self) -> dict:
"""Return the current high scores for this level.""" """Return the current high scores for this level."""
config = self._get_config_dict() config = self._get_config_dict()
high_scores_key = 'High Scores' + self.get_score_version_string() high_scores_key = f'High Scores{self.get_score_version_string()}'
if high_scores_key not in config: val = config.get(high_scores_key)
return {} if isinstance(val, dict):
return copy.deepcopy(config[high_scores_key]) return copy.deepcopy(val)
return {}
def set_high_scores(self, high_scores: dict) -> None: def set_high_scores(self, high_scores: dict) -> None:
"""Set high scores for this level.""" """Set high scores for this level."""

View file

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Implements lobby system for gathering before games, char select, etc.""" """Implements lobby system for gathering before games, char select, etc."""
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
from __future__ import annotations from __future__ import annotations
@ -118,7 +119,7 @@ class JoinInfo:
) )
self._timer = _bascenev1.Timer( self._timer = _bascenev1.Timer(
4.0, babase.WeakCall(self._update), repeat=True 4.0, babase.WeakCallStrict(self._update), repeat=True
) )
def _update_for_keyboard(self, keyboard: bascenev1.InputDevice) -> None: def _update_for_keyboard(self, keyboard: bascenev1.InputDevice) -> None:
@ -338,10 +339,10 @@ class Chooser:
if inputdevice.is_controller_app and '_random' in profilenames: if inputdevice.is_controller_app and '_random' in profilenames:
return profilenames.index('_random') return profilenames.index('_random')
# If its a client connection, for now just force # If its a client connection, for now just force the account
# the account profile if possible.. (need to provide a # profile if possible. (need to provide a way for clients to
# way for clients to specify/remember their default # specify/remember their default profile on remote servers that
# profile on remote servers that do not already know them). # do not already know them).
if inputdevice.is_remote_client and '__account__' in profilenames: if inputdevice.is_remote_client and '__account__' in profilenames:
return profilenames.index('__account__') return profilenames.index('__account__')
@ -609,25 +610,29 @@ class Chooser:
if not ready: if not ready:
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
babase.InputType.LEFT_PRESS, babase.InputType.LEFT_PRESS,
babase.Call(self.handlemessage, ChangeMessage('team', -1)), babase.CallStrict(
self.handlemessage, ChangeMessage('team', -1)
),
) )
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
babase.InputType.RIGHT_PRESS, babase.InputType.RIGHT_PRESS,
babase.Call(self.handlemessage, ChangeMessage('team', 1)), babase.CallStrict(self.handlemessage, ChangeMessage('team', 1)),
) )
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
babase.InputType.BOMB_PRESS, babase.InputType.BOMB_PRESS,
babase.Call(self.handlemessage, ChangeMessage('character', 1)), babase.CallStrict(
self.handlemessage, ChangeMessage('character', 1)
),
) )
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
babase.InputType.UP_PRESS, babase.InputType.UP_PRESS,
babase.Call( babase.CallStrict(
self.handlemessage, ChangeMessage('profileindex', -1) self.handlemessage, ChangeMessage('profileindex', -1)
), ),
) )
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
babase.InputType.DOWN_PRESS, babase.InputType.DOWN_PRESS,
babase.Call( babase.CallStrict(
self.handlemessage, ChangeMessage('profileindex', 1) self.handlemessage, ChangeMessage('profileindex', 1)
), ),
) )
@ -637,7 +642,9 @@ class Chooser:
babase.InputType.PICK_UP_PRESS, babase.InputType.PICK_UP_PRESS,
babase.InputType.PUNCH_PRESS, babase.InputType.PUNCH_PRESS,
), ),
babase.Call(self.handlemessage, ChangeMessage('ready', 1)), babase.CallStrict(
self.handlemessage, ChangeMessage('ready', 1)
),
) )
self._ready = False self._ready = False
self._update_text() self._update_text()
@ -662,7 +669,9 @@ class Chooser:
babase.InputType.PICK_UP_PRESS, babase.InputType.PICK_UP_PRESS,
babase.InputType.PUNCH_PRESS, babase.InputType.PUNCH_PRESS,
), ),
babase.Call(self.handlemessage, ChangeMessage('ready', 0)), babase.CallStrict(
self.handlemessage, ChangeMessage('ready', 0)
),
) )
# Store the last profile picked by this input for reuse. # Store the last profile picked by this input for reuse.

Some files were not shown because too many files have changed in this diff Show more