mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-10-20 00:00:39 +00:00
syncing changes from ballistica/master
This commit is contained in:
parent
2a07c0c840
commit
8913080562
227 changed files with 15756 additions and 12772 deletions
15
config.toml
15
config.toml
|
|
@ -18,10 +18,11 @@ party_name = "BombSquad Community Server"
|
|||
# internet connection.
|
||||
#authenticate_clients = true
|
||||
|
||||
# IDs of server admins. Server admins are not kickable through the default
|
||||
# kick vote system and they are able to kick players without a vote. To get
|
||||
# your account id, enter 'getaccountid' in settings->advanced->enter-code.
|
||||
admins = ["pb-yOuRAccOuNtIdHErE", "pb-aNdMayBeAnotherHeRE"]
|
||||
# IDs of server admins. Server admins are not kickable through the
|
||||
# default kick vote system and they are able to kick players without
|
||||
# a vote. To get your account id, enter 'getaccountid' in
|
||||
# settings->advanced->enter-code.
|
||||
#admins = ["pb-yOuRAccOuNtIdHErE", "pb-aNdMayBeAnotherHeRE"]
|
||||
|
||||
# Whether the default kick-voting system is enabled.
|
||||
#enable_default_kick_voting = true
|
||||
|
|
@ -108,9 +109,9 @@ playlist_code = 12345
|
|||
# get 4 wins)
|
||||
teams_series_length = 7
|
||||
|
||||
# Points to win in free-for-all mode (Points are awarded per game based on
|
||||
# performance)
|
||||
ffa_series_length = 24
|
||||
# Points to win in free-for-all mode (Points are awarded per game
|
||||
# based on performance)
|
||||
#ffa_series_length = 24
|
||||
|
||||
# If you have a custom stats webpage for your server, you can use
|
||||
# this to provide a convenient in-game link to it in the
|
||||
|
|
|
|||
40
dist/ba_data/python/babase/__init__.py
vendored
40
dist/ba_data/python/babase/__init__.py
vendored
|
|
@ -9,6 +9,8 @@ a more focused way.
|
|||
"""
|
||||
# pylint: disable=redefined-builtin
|
||||
|
||||
# ba_meta require api 9
|
||||
|
||||
# The stuff we expose here at the top level is our 'public' api for use
|
||||
# from other modules/packages. Code *within* this package should import
|
||||
# things from this package's submodules directly to reduce the chance of
|
||||
|
|
@ -17,11 +19,12 @@ a more focused way.
|
|||
|
||||
from efro.util import set_canonical_module_names
|
||||
|
||||
|
||||
import _babase
|
||||
from _babase import (
|
||||
add_clean_frame_callback,
|
||||
allows_ticket_sales,
|
||||
android_get_external_files_dir,
|
||||
app_instance_uuid,
|
||||
appname,
|
||||
appnameupper,
|
||||
apptime,
|
||||
|
|
@ -55,12 +58,16 @@ from _babase import (
|
|||
get_replays_dir,
|
||||
get_string_height,
|
||||
get_string_width,
|
||||
get_ui_scale,
|
||||
get_v1_cloud_log_file_path,
|
||||
get_virtual_safe_area_size,
|
||||
get_virtual_screen_size,
|
||||
getsimplesound,
|
||||
has_user_run_commands,
|
||||
have_chars,
|
||||
have_permission,
|
||||
in_logic_thread,
|
||||
in_main_menu,
|
||||
increment_analytics_count,
|
||||
invoke_main_menu,
|
||||
is_os_playing_music,
|
||||
|
|
@ -86,6 +93,7 @@ from _babase import (
|
|||
overlay_web_browser_is_supported,
|
||||
overlay_web_browser_open_url,
|
||||
print_load_info,
|
||||
push_back_press,
|
||||
pushcall,
|
||||
quit,
|
||||
reload_media,
|
||||
|
|
@ -95,7 +103,9 @@ from _babase import (
|
|||
set_analytics_screen,
|
||||
set_low_level_config_value,
|
||||
set_thread_name,
|
||||
set_ui_account_state,
|
||||
set_ui_input_device,
|
||||
set_ui_scale,
|
||||
show_progress_bar,
|
||||
shutdown_suppress_begin,
|
||||
shutdown_suppress_end,
|
||||
|
|
@ -103,8 +113,11 @@ from _babase import (
|
|||
SimpleSound,
|
||||
supports_max_fps,
|
||||
supports_vsync,
|
||||
supports_unicode_display,
|
||||
unlock_all_input,
|
||||
update_internal_logger_levels,
|
||||
user_agent_string,
|
||||
user_ran_commands,
|
||||
Vec3,
|
||||
workspaces_in_use,
|
||||
)
|
||||
|
|
@ -123,7 +136,9 @@ from babase._apputils import (
|
|||
garbage_collect,
|
||||
get_remote_app_name,
|
||||
AppHealthMonitor,
|
||||
utc_now_cloud,
|
||||
)
|
||||
from babase._cloud import CloudSubscription
|
||||
from babase._devconsole import (
|
||||
DevConsoleTab,
|
||||
DevConsoleTabEntry,
|
||||
|
|
@ -162,10 +177,9 @@ from babase._general import (
|
|||
get_type_name,
|
||||
)
|
||||
from babase._language import Lstr, LanguageSubsystem
|
||||
from babase._logging import balog, applog, lifecyclelog
|
||||
from babase._login import LoginAdapter, LoginInfo
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
# (PyCharm inspection bug?)
|
||||
from babase._mgen.enums import (
|
||||
Permission,
|
||||
SpecialChar,
|
||||
|
|
@ -180,6 +194,7 @@ from babase._plugin import PluginSpec, Plugin, PluginSubsystem
|
|||
from babase._stringedit import StringEditAdapter, StringEditSubsystem
|
||||
from babase._text import timestring
|
||||
|
||||
|
||||
_babase.app = app = App()
|
||||
app.postinit()
|
||||
|
||||
|
|
@ -188,6 +203,7 @@ __all__ = [
|
|||
'AccountV2Subsystem',
|
||||
'ActivityNotFoundError',
|
||||
'ActorNotFoundError',
|
||||
'allows_ticket_sales',
|
||||
'add_clean_frame_callback',
|
||||
'android_get_external_files_dir',
|
||||
'app',
|
||||
|
|
@ -199,6 +215,8 @@ __all__ = [
|
|||
'AppIntentDefault',
|
||||
'AppIntentExec',
|
||||
'AppMode',
|
||||
'app_instance_uuid',
|
||||
'applog',
|
||||
'appname',
|
||||
'appnameupper',
|
||||
'AppModeSelector',
|
||||
|
|
@ -209,6 +227,7 @@ __all__ = [
|
|||
'apptimer',
|
||||
'AppTimer',
|
||||
'asset_loads_allowed',
|
||||
'balog',
|
||||
'Call',
|
||||
'fullscreen_control_available',
|
||||
'fullscreen_control_get',
|
||||
|
|
@ -218,6 +237,7 @@ __all__ = [
|
|||
'clipboard_get_text',
|
||||
'clipboard_has_text',
|
||||
'clipboard_is_supported',
|
||||
'CloudSubscription',
|
||||
'clipboard_set_text',
|
||||
'commit_app_config',
|
||||
'ContextCall',
|
||||
|
|
@ -250,8 +270,11 @@ __all__ = [
|
|||
'get_replays_dir',
|
||||
'get_string_height',
|
||||
'get_string_width',
|
||||
'get_v1_cloud_log_file_path',
|
||||
'get_type_name',
|
||||
'get_ui_scale',
|
||||
'get_virtual_safe_area_size',
|
||||
'get_virtual_screen_size',
|
||||
'get_v1_cloud_log_file_path',
|
||||
'getclass',
|
||||
'getsimplesound',
|
||||
'handle_leftover_v1_cloud_log_file',
|
||||
|
|
@ -259,6 +282,7 @@ __all__ = [
|
|||
'have_chars',
|
||||
'have_permission',
|
||||
'in_logic_thread',
|
||||
'in_main_menu',
|
||||
'increment_analytics_count',
|
||||
'InputDeviceNotFoundError',
|
||||
'InputType',
|
||||
|
|
@ -269,6 +293,7 @@ __all__ = [
|
|||
'is_point_in_box',
|
||||
'is_xcode_build',
|
||||
'LanguageSubsystem',
|
||||
'lifecyclelog',
|
||||
'lock_all_input',
|
||||
'LoginAdapter',
|
||||
'LoginInfo',
|
||||
|
|
@ -305,6 +330,7 @@ __all__ = [
|
|||
'print_error',
|
||||
'print_exception',
|
||||
'print_load_info',
|
||||
'push_back_press',
|
||||
'pushcall',
|
||||
'quit',
|
||||
'QuitType',
|
||||
|
|
@ -318,7 +344,9 @@ __all__ = [
|
|||
'set_analytics_screen',
|
||||
'set_low_level_config_value',
|
||||
'set_thread_name',
|
||||
'set_ui_account_state',
|
||||
'set_ui_input_device',
|
||||
'set_ui_scale',
|
||||
'show_progress_bar',
|
||||
'shutdown_suppress_begin',
|
||||
'shutdown_suppress_end',
|
||||
|
|
@ -330,11 +358,15 @@ __all__ = [
|
|||
'StringEditSubsystem',
|
||||
'supports_max_fps',
|
||||
'supports_vsync',
|
||||
'supports_unicode_display',
|
||||
'TeamNotFoundError',
|
||||
'timestring',
|
||||
'UIScale',
|
||||
'unlock_all_input',
|
||||
'update_internal_logger_levels',
|
||||
'user_agent_string',
|
||||
'user_ran_commands',
|
||||
'utc_now_cloud',
|
||||
'utf8_all',
|
||||
'Vec3',
|
||||
'vec3validate',
|
||||
|
|
|
|||
88
dist/ba_data/python/babase/_accountv2.py
vendored
88
dist/ba_data/python/babase/_accountv2.py
vendored
|
|
@ -10,16 +10,16 @@ from functools import partial
|
|||
from typing import TYPE_CHECKING, assert_never
|
||||
|
||||
from efro.error import CommunicationError
|
||||
from efro.call import CallbackSet
|
||||
from bacommon.login import LoginType
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
from babase._login import LoginAdapter, LoginInfo
|
||||
|
||||
|
||||
DEBUG_LOG = False
|
||||
logger = logging.getLogger('ba.accountv2')
|
||||
|
||||
|
||||
class AccountV2Subsystem:
|
||||
|
|
@ -31,10 +31,22 @@ class AccountV2Subsystem:
|
|||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
from babase._login import LoginAdapterGPGS, LoginAdapterGameCenter
|
||||
|
||||
# Whether or not everything related to an initial login
|
||||
# (or lack thereof) has completed. This includes things like
|
||||
# Register to be informed when connectivity changes.
|
||||
plus = _babase.app.plus
|
||||
self._connectivity_changed_cb = (
|
||||
None
|
||||
if plus is None
|
||||
else plus.cloud.on_connectivity_changed_callbacks.register(
|
||||
self._on_cloud_connectivity_changed
|
||||
)
|
||||
)
|
||||
|
||||
# Whether or not everything related to an initial sign in (or
|
||||
# lack thereof) has completed. This includes things like
|
||||
# workspace syncing. Completion of this is what flips the app
|
||||
# into 'running' state.
|
||||
self._initial_sign_in_completed = False
|
||||
|
|
@ -46,6 +58,9 @@ class AccountV2Subsystem:
|
|||
self._implicit_signed_in_adapter: LoginAdapter | None = None
|
||||
self._implicit_state_changed = False
|
||||
self._can_do_auto_sign_in = True
|
||||
self.on_primary_account_changed_callbacks: CallbackSet[
|
||||
Callable[[AccountV2Handle | None], None]
|
||||
] = CallbackSet()
|
||||
|
||||
adapter: LoginAdapter
|
||||
if _babase.using_google_play_game_services():
|
||||
|
|
@ -64,11 +79,11 @@ class AccountV2Subsystem:
|
|||
def have_primary_credentials(self) -> bool:
|
||||
"""Are credentials currently set for the primary app account?
|
||||
|
||||
Note that this does not mean these credentials are currently valid;
|
||||
only that they exist. If/when credentials are validated, the 'primary'
|
||||
account handle will be set.
|
||||
Note that this does not mean these credentials have been checked
|
||||
for validity; only that they exist. If/when credentials are
|
||||
validated, the 'primary' account handle will be set.
|
||||
"""
|
||||
raise NotImplementedError('This should be overridden.')
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def primary(self) -> AccountV2Handle | None:
|
||||
|
|
@ -85,6 +100,13 @@ class AccountV2Subsystem:
|
|||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Fire any registered callbacks.
|
||||
for call in self.on_primary_account_changed_callbacks.getcalls():
|
||||
try:
|
||||
call(account)
|
||||
except Exception:
|
||||
logging.exception('Error in primary-account-changed callback.')
|
||||
|
||||
# Currently don't do anything special on sign-outs.
|
||||
if account is None:
|
||||
return
|
||||
|
|
@ -105,9 +127,9 @@ class AccountV2Subsystem:
|
|||
on_completed=self._on_set_active_workspace_completed,
|
||||
)
|
||||
else:
|
||||
# Don't activate workspaces if we've already told the game
|
||||
# that initial-log-in is done or if we've already kicked
|
||||
# off a workspace load.
|
||||
# Don't activate workspaces if we've already told the
|
||||
# game that initial-log-in is done or if we've already
|
||||
# kicked off a workspace load.
|
||||
_babase.screenmessage(
|
||||
f'\'{account.workspacename}\''
|
||||
f' will be activated at next app launch.',
|
||||
|
|
@ -242,19 +264,18 @@ class AccountV2Subsystem:
|
|||
# generally this means the user has explicitly signed in/out or
|
||||
# switched accounts within that back-end.
|
||||
if prev_state != new_state:
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'AccountV2: Implicit state changed (%s -> %s);'
|
||||
' will update app sign-in state accordingly.',
|
||||
prev_state,
|
||||
new_state,
|
||||
)
|
||||
logger.debug(
|
||||
'Implicit state changed (%s -> %s);'
|
||||
' will update app sign-in state accordingly.',
|
||||
prev_state,
|
||||
new_state,
|
||||
)
|
||||
self._implicit_state_changed = True
|
||||
|
||||
# We may want to auto-sign-in based on this new state.
|
||||
self._update_auto_sign_in()
|
||||
|
||||
def on_cloud_connectivity_changed(self, connected: bool) -> None:
|
||||
def _on_cloud_connectivity_changed(self, connected: bool) -> None:
|
||||
"""Should be called with cloud connectivity changes."""
|
||||
del connected # Unused.
|
||||
assert _babase.in_logic_thread()
|
||||
|
|
@ -264,11 +285,11 @@ class AccountV2Subsystem:
|
|||
|
||||
def do_get_primary(self) -> AccountV2Handle | None:
|
||||
"""Internal - should be overridden by subclass."""
|
||||
raise NotImplementedError('This should be overridden.')
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_primary_credentials(self, credentials: str | None) -> None:
|
||||
"""Set credentials for the primary app account."""
|
||||
raise NotImplementedError('This should be overridden.')
|
||||
raise NotImplementedError()
|
||||
|
||||
def _update_auto_sign_in(self) -> None:
|
||||
plus = _babase.app.plus
|
||||
|
|
@ -279,11 +300,9 @@ class AccountV2Subsystem:
|
|||
if self._implicit_signed_in_adapter is None:
|
||||
# If implicit back-end has signed out, we follow suit
|
||||
# immediately; no need to wait for network connectivity.
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'AccountV2: Signing out as result'
|
||||
' of implicit state change...',
|
||||
)
|
||||
logger.debug(
|
||||
'Signing out as result of implicit state change...',
|
||||
)
|
||||
plus.accounts.set_primary_credentials(None)
|
||||
self._implicit_state_changed = False
|
||||
|
||||
|
|
@ -300,11 +319,9 @@ class AccountV2Subsystem:
|
|||
# switching accounts via the back-end). NOTE: should
|
||||
# test case where we don't have connectivity here.
|
||||
if plus.cloud.is_connected():
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'AccountV2: Signing in as result'
|
||||
' of implicit state change...',
|
||||
)
|
||||
logger.debug(
|
||||
'Signing in as result of implicit state change...',
|
||||
)
|
||||
self._implicit_signed_in_adapter.sign_in(
|
||||
self._on_explicit_sign_in_completed,
|
||||
description='implicit state change',
|
||||
|
|
@ -335,10 +352,9 @@ class AccountV2Subsystem:
|
|||
and not signed_in_v2
|
||||
and self._implicit_signed_in_adapter is not None
|
||||
):
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'AccountV2: Signing in due to on-launch-auto-sign-in...',
|
||||
)
|
||||
logger.debug(
|
||||
'Signing in due to on-launch-auto-sign-in...',
|
||||
)
|
||||
self._can_do_auto_sign_in = False # Only ATTEMPT once
|
||||
self._implicit_signed_in_adapter.sign_in(
|
||||
self._on_implicit_sign_in_completed, description='auto-sign-in'
|
||||
|
|
|
|||
225
dist/ba_data/python/babase/_app.py
vendored
225
dist/ba_data/python/babase/_app.py
vendored
|
|
@ -9,9 +9,10 @@ import logging
|
|||
from enum import Enum
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, TypeVar, override
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import RLock
|
||||
|
||||
from efro.threadpool import ThreadPoolExecutorPlus
|
||||
|
||||
import _babase
|
||||
from babase._language import LanguageSubsystem
|
||||
from babase._plugin import PluginSubsystem
|
||||
|
|
@ -23,6 +24,8 @@ from babase._appmodeselector import AppModeSelector
|
|||
from babase._appintent import AppIntentDefault, AppIntentExec
|
||||
from babase._stringedit import StringEditSubsystem
|
||||
from babase._devconsole import DevConsoleSubsystem
|
||||
from babase._appconfig import AppConfig
|
||||
from babase._logging import lifecyclelog, applog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import asyncio
|
||||
|
|
@ -36,9 +39,9 @@ if TYPE_CHECKING:
|
|||
# __FEATURESET_APP_SUBSYSTEM_IMPORTS_BEGIN__
|
||||
# This section generated by batools.appmodule; do not edit.
|
||||
|
||||
from baclassic import ClassicSubsystem
|
||||
from baplus import PlusSubsystem
|
||||
from bauiv1 import UIV1Subsystem
|
||||
from baclassic import ClassicAppSubsystem
|
||||
from baplus import PlusAppSubsystem
|
||||
from bauiv1 import UIV1AppSubsystem
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_IMPORTS_END__
|
||||
|
||||
|
|
@ -65,9 +68,10 @@ class App:
|
|||
health_monitor: AppHealthMonitor
|
||||
|
||||
# How long we allow shutdown tasks to run before killing them.
|
||||
# Currently the entire app hard-exits if shutdown takes 10 seconds,
|
||||
# so we need to keep it under that.
|
||||
SHUTDOWN_TASK_TIMEOUT_SECONDS = 5
|
||||
# Currently the entire app hard-exits if shutdown takes 15 seconds,
|
||||
# so we need to keep it under that. Staying above 10 should allow
|
||||
# 10 second network timeouts to happen though.
|
||||
SHUTDOWN_TASK_TIMEOUT_SECONDS = 12
|
||||
|
||||
class State(Enum):
|
||||
"""High level state the app can be in."""
|
||||
|
|
@ -137,11 +141,11 @@ class App:
|
|||
|
||||
# Ask our default app modes to handle it.
|
||||
# (generated from 'default_app_modes' in projectconfig).
|
||||
import bascenev1
|
||||
import baclassic
|
||||
import babase
|
||||
|
||||
for appmode in [
|
||||
bascenev1.SceneV1AppMode,
|
||||
baclassic.ClassicAppMode,
|
||||
babase.EmptyAppMode,
|
||||
]:
|
||||
if appmode.can_handle_intent(intent):
|
||||
|
|
@ -164,6 +168,11 @@ class App:
|
|||
if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
|
||||
return
|
||||
|
||||
# Wrap our raw app config in our special wrapper and pass it to
|
||||
# the native layer.
|
||||
self.config = AppConfig(_babase.get_initial_app_config())
|
||||
_babase.set_app_config(self.config)
|
||||
|
||||
self.env: babase.Env = _babase.Env()
|
||||
self.state = self.State.NOT_STARTED
|
||||
|
||||
|
|
@ -171,7 +180,7 @@ class App:
|
|||
# processing. It should also be passed to any additional asyncio
|
||||
# loops we create so that everything shares the same single set
|
||||
# of worker threads.
|
||||
self.threadpool = ThreadPoolExecutor(
|
||||
self.threadpool = ThreadPoolExecutorPlus(
|
||||
thread_name_prefix='baworker',
|
||||
initializer=self._thread_pool_thread_init,
|
||||
)
|
||||
|
|
@ -205,11 +214,11 @@ class App:
|
|||
self._asyncio_loop: asyncio.AbstractEventLoop | None = None
|
||||
self._asyncio_tasks: set[asyncio.Task] = set()
|
||||
self._asyncio_timer: babase.AppTimer | None = None
|
||||
self._config: babase.AppConfig | None = None
|
||||
self._pending_intent: AppIntent | None = None
|
||||
self._intent: AppIntent | None = None
|
||||
self._mode: AppMode | None = None
|
||||
self._mode_selector: babase.AppModeSelector | None = None
|
||||
self._mode_instances: dict[type[AppMode], AppMode] = {}
|
||||
self._mode: AppMode | None = None
|
||||
self._shutdown_task: asyncio.Task[None] | None = None
|
||||
self._shutdown_tasks: list[Coroutine[None, None, None]] = [
|
||||
self._wait_for_shutdown_suppressions(),
|
||||
|
|
@ -250,6 +259,12 @@ class App:
|
|||
"""
|
||||
return _babase.app_is_active()
|
||||
|
||||
@property
|
||||
def mode(self) -> AppMode | None:
|
||||
"""The app's current mode."""
|
||||
assert _babase.in_logic_thread()
|
||||
return self._mode
|
||||
|
||||
@property
|
||||
def asyncio_loop(self) -> asyncio.AbstractEventLoop:
|
||||
"""The logic thread's asyncio event loop.
|
||||
|
|
@ -289,7 +304,7 @@ class App:
|
|||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Hold a strong reference to the task until it is done.
|
||||
# We hold a strong reference to the task until it is done.
|
||||
# Otherwise it is possible for it to be garbage collected and
|
||||
# disappear midway if the caller does not hold on to the
|
||||
# returned task, which seems like a great way to introduce
|
||||
|
|
@ -311,12 +326,6 @@ class App:
|
|||
|
||||
self._asyncio_tasks.remove(task)
|
||||
|
||||
@property
|
||||
def config(self) -> babase.AppConfig:
|
||||
"""The babase.AppConfig instance representing the app's config state."""
|
||||
assert self._config is not None
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def mode_selector(self) -> babase.AppModeSelector:
|
||||
"""Controls which app-modes are used for handling given intents.
|
||||
|
|
@ -387,19 +396,19 @@ class App:
|
|||
# This section generated by batools.appmodule; do not edit.
|
||||
|
||||
@property
|
||||
def classic(self) -> ClassicSubsystem | None:
|
||||
def classic(self) -> ClassicAppSubsystem | None:
|
||||
"""Our classic subsystem (if available)."""
|
||||
return self._get_subsystem_property(
|
||||
'classic', self._create_classic_subsystem
|
||||
) # type: ignore
|
||||
|
||||
@staticmethod
|
||||
def _create_classic_subsystem() -> ClassicSubsystem | None:
|
||||
def _create_classic_subsystem() -> ClassicAppSubsystem | None:
|
||||
# pylint: disable=cyclic-import
|
||||
try:
|
||||
from baclassic import ClassicSubsystem
|
||||
from baclassic import ClassicAppSubsystem
|
||||
|
||||
return ClassicSubsystem()
|
||||
return ClassicAppSubsystem()
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception:
|
||||
|
|
@ -407,19 +416,19 @@ class App:
|
|||
return None
|
||||
|
||||
@property
|
||||
def plus(self) -> PlusSubsystem | None:
|
||||
def plus(self) -> PlusAppSubsystem | None:
|
||||
"""Our plus subsystem (if available)."""
|
||||
return self._get_subsystem_property(
|
||||
'plus', self._create_plus_subsystem
|
||||
) # type: ignore
|
||||
|
||||
@staticmethod
|
||||
def _create_plus_subsystem() -> PlusSubsystem | None:
|
||||
def _create_plus_subsystem() -> PlusAppSubsystem | None:
|
||||
# pylint: disable=cyclic-import
|
||||
try:
|
||||
from baplus import PlusSubsystem
|
||||
from baplus import PlusAppSubsystem
|
||||
|
||||
return PlusSubsystem()
|
||||
return PlusAppSubsystem()
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception:
|
||||
|
|
@ -427,19 +436,19 @@ class App:
|
|||
return None
|
||||
|
||||
@property
|
||||
def ui_v1(self) -> UIV1Subsystem:
|
||||
def ui_v1(self) -> UIV1AppSubsystem:
|
||||
"""Our ui_v1 subsystem (always available)."""
|
||||
return self._get_subsystem_property(
|
||||
'ui_v1', self._create_ui_v1_subsystem
|
||||
) # type: ignore
|
||||
|
||||
@staticmethod
|
||||
def _create_ui_v1_subsystem() -> UIV1Subsystem:
|
||||
def _create_ui_v1_subsystem() -> UIV1AppSubsystem:
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
from bauiv1 import UIV1Subsystem
|
||||
from bauiv1 import UIV1AppSubsystem
|
||||
|
||||
return UIV1Subsystem()
|
||||
return UIV1AppSubsystem()
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_PROPERTIES_END__
|
||||
|
||||
|
|
@ -481,18 +490,6 @@ class App:
|
|||
"""
|
||||
_babase.run_app()
|
||||
|
||||
def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
|
||||
"""Submit a call to the app threadpool where result is not needed.
|
||||
|
||||
Normally, doing work in a thread-pool involves creating a future
|
||||
and waiting for its result, which is an important step because it
|
||||
propagates any Exceptions raised by the submitted work. When the
|
||||
result in not important, however, this call can be used. The app
|
||||
will log any exceptions that occur.
|
||||
"""
|
||||
fut = self.threadpool.submit(call)
|
||||
fut.add_done_callback(self._threadpool_no_wait_done)
|
||||
|
||||
def set_intent(self, intent: AppIntent) -> None:
|
||||
"""Set the intent for the app.
|
||||
|
||||
|
|
@ -510,7 +507,7 @@ class App:
|
|||
|
||||
# Do the actual work of calcing our app-mode/etc. in a bg thread
|
||||
# since it may block for a moment to load modules/etc.
|
||||
self.threadpool_submit_no_wait(partial(self._set_intent, intent))
|
||||
self.threadpool.submit_no_wait(self._set_intent, intent)
|
||||
|
||||
def push_apply_app_config(self) -> None:
|
||||
"""Internal. Use app.config.apply() to apply app config changes."""
|
||||
|
|
@ -566,12 +563,6 @@ class App:
|
|||
if self._mode is not None:
|
||||
self._mode.on_app_active_changed()
|
||||
|
||||
def read_config(self) -> None:
|
||||
"""(internal)"""
|
||||
from babase._appconfig import read_app_config
|
||||
|
||||
self._config = read_app_config()
|
||||
|
||||
def handle_deep_link(self, url: str) -> None:
|
||||
"""Handle a deep link URL."""
|
||||
from babase._language import Lstr
|
||||
|
|
@ -610,18 +601,71 @@ class App:
|
|||
self._initial_sign_in_completed = True
|
||||
self._update_state()
|
||||
|
||||
def set_ui_scale(self, scale: babase.UIScale) -> None:
|
||||
"""Change ui-scale on the fly.
|
||||
|
||||
Currently this is mainly for debugging and will not be called as
|
||||
part of normal app operation.
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Apply to the native layer.
|
||||
_babase.set_ui_scale(scale.name.lower())
|
||||
|
||||
# Inform all subsystems that something screen-related has
|
||||
# changed. We assume subsystems won't be added at this point so
|
||||
# we can use the list directly.
|
||||
assert self._subsystem_registration_ended
|
||||
for subsystem in self._subsystems:
|
||||
try:
|
||||
subsystem.on_ui_scale_change()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_ui_scale_change() for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
def on_screen_size_change(self) -> None:
|
||||
"""Screen size has changed."""
|
||||
|
||||
# Inform all app subsystems in the same order they were inited.
|
||||
# Operate on a copy of the list here because this can be called
|
||||
# while subsystems are still being added.
|
||||
for subsystem in self._subsystems.copy():
|
||||
try:
|
||||
subsystem.on_screen_size_change()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_screen_size_change() for subsystem %s.',
|
||||
subsystem,
|
||||
)
|
||||
|
||||
def _set_intent(self, intent: AppIntent) -> None:
|
||||
from babase._appmode import AppMode
|
||||
|
||||
# This should be happening in a bg thread.
|
||||
assert not _babase.in_logic_thread()
|
||||
try:
|
||||
# Ask the selector what app-mode to use for this intent.
|
||||
if self.mode_selector is None:
|
||||
raise RuntimeError('No AppModeSelector set.')
|
||||
modetype = self.mode_selector.app_mode_for_intent(intent)
|
||||
|
||||
# NOTE: Since intents are somewhat high level things, should
|
||||
# we do some universal thing like a screenmessage saying
|
||||
# 'The app cannot handle that request' on failure?
|
||||
modetype: type[AppMode] | None
|
||||
|
||||
# Special case - for testing we may force a specific
|
||||
# app-mode to handle this intent instead of going through our
|
||||
# usual selector.
|
||||
forced_mode_type = getattr(intent, '_force_app_mode_handler', None)
|
||||
if isinstance(forced_mode_type, type) and issubclass(
|
||||
forced_mode_type, AppMode
|
||||
):
|
||||
modetype = forced_mode_type
|
||||
else:
|
||||
modetype = self.mode_selector.app_mode_for_intent(intent)
|
||||
|
||||
# NOTE: Since intents are somewhat high level things,
|
||||
# perhaps we should do some universal thing like a
|
||||
# screenmessage saying 'The app cannot handle the request'
|
||||
# on failure.
|
||||
|
||||
if modetype is None:
|
||||
raise RuntimeError(
|
||||
|
|
@ -640,7 +684,9 @@ class App:
|
|||
|
||||
# Ok; seems legit. Now instantiate the mode if necessary and
|
||||
# kick back to the logic thread to apply.
|
||||
mode = modetype()
|
||||
mode = self._mode_instances.get(modetype)
|
||||
if mode is None:
|
||||
self._mode_instances[modetype] = mode = modetype()
|
||||
_babase.pushcall(
|
||||
partial(self._apply_intent, intent, mode),
|
||||
from_other_thread=True,
|
||||
|
|
@ -661,7 +707,7 @@ class App:
|
|||
return
|
||||
|
||||
# If the app-mode for this intent is different than the active
|
||||
# one, switch.
|
||||
# one, switch modes.
|
||||
if type(mode) is not type(self._mode):
|
||||
if self._mode is None:
|
||||
is_initial_mode = True
|
||||
|
|
@ -673,6 +719,18 @@ class App:
|
|||
logging.exception(
|
||||
'Error deactivating app-mode %s.', self._mode
|
||||
)
|
||||
|
||||
# Reset all subsystems. We assume subsystems won't be added
|
||||
# at this point so we can use the list directly.
|
||||
assert self._subsystem_registration_ended
|
||||
for subsystem in self._subsystems:
|
||||
try:
|
||||
subsystem.reset()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in reset() for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
self._mode = mode
|
||||
try:
|
||||
mode.on_activate()
|
||||
|
|
@ -750,14 +808,14 @@ class App:
|
|||
self.meta.start_scan(scan_complete_cb=self._on_meta_scan_complete)
|
||||
|
||||
# Inform all app subsystems in the same order they were inited.
|
||||
# Operate on a copy here because subsystems can still be added
|
||||
# at this point.
|
||||
# Operate on a copy of the list here because subsystems can
|
||||
# still be added at this point.
|
||||
for subsystem in self._subsystems.copy():
|
||||
try:
|
||||
subsystem.on_app_loading()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_loading for subsystem %s.', subsystem
|
||||
'Error in on_app_loading() for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
# Normally plus tells us when initial sign-in is done. If plus
|
||||
|
|
@ -806,7 +864,7 @@ class App:
|
|||
subsystem.on_app_running()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_running for subsystem %s.', subsystem
|
||||
'Error in on_app_running() for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
# Cut off new subsystem additions at this point.
|
||||
|
|
@ -825,7 +883,7 @@ class App:
|
|||
def _apply_app_config(self) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
_babase.lifecyclelog('apply-app-config')
|
||||
lifecyclelog.info('apply-app-config')
|
||||
|
||||
# If multiple apply calls have been made, only actually apply
|
||||
# once.
|
||||
|
|
@ -842,7 +900,8 @@ class App:
|
|||
subsystem.do_apply_app_config()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in do_apply_app_config for subsystem %s.', subsystem
|
||||
'Error in do_apply_app_config() for subsystem %s.',
|
||||
subsystem,
|
||||
)
|
||||
|
||||
# Let the native layer do its thing.
|
||||
|
|
@ -856,7 +915,7 @@ class App:
|
|||
if self._native_shutdown_complete_called:
|
||||
if self.state is not self.State.SHUTDOWN_COMPLETE:
|
||||
self.state = self.State.SHUTDOWN_COMPLETE
|
||||
_babase.lifecyclelog('app state shutdown complete')
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
self._on_shutdown_complete()
|
||||
|
||||
# Shutdown trumps all. Though we can't start shutting down until
|
||||
|
|
@ -866,7 +925,8 @@ class App:
|
|||
# Entering shutdown state:
|
||||
if self.state is not self.State.SHUTTING_DOWN:
|
||||
self.state = self.State.SHUTTING_DOWN
|
||||
_babase.lifecyclelog('app state shutting down')
|
||||
applog.info('Shutting down...')
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
self._on_shutting_down()
|
||||
|
||||
elif self._native_suspended:
|
||||
|
|
@ -883,15 +943,16 @@ class App:
|
|||
if self._initial_sign_in_completed and self._meta_scan_completed:
|
||||
if self.state != self.State.RUNNING:
|
||||
self.state = self.State.RUNNING
|
||||
_babase.lifecyclelog('app state running')
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
if not self._called_on_running:
|
||||
self._called_on_running = True
|
||||
self._on_running()
|
||||
|
||||
# Entering or returning to loading state:
|
||||
elif self._init_completed:
|
||||
if self.state is not self.State.LOADING:
|
||||
self.state = self.State.LOADING
|
||||
_babase.lifecyclelog('app state loading')
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
if not self._called_on_loading:
|
||||
self._called_on_loading = True
|
||||
self._on_loading()
|
||||
|
|
@ -900,7 +961,7 @@ class App:
|
|||
elif self._native_bootstrapping_completed:
|
||||
if self.state is not self.State.INITING:
|
||||
self.state = self.State.INITING
|
||||
_babase.lifecyclelog('app state initing')
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
if not self._called_on_initing:
|
||||
self._called_on_initing = True
|
||||
self._on_initing()
|
||||
|
|
@ -909,7 +970,7 @@ class App:
|
|||
elif self._native_start_called:
|
||||
if self.state is not self.State.NATIVE_BOOTSTRAPPING:
|
||||
self.state = self.State.NATIVE_BOOTSTRAPPING
|
||||
_babase.lifecyclelog('app state native bootstrapping')
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
else:
|
||||
# Only logical possibility left is NOT_STARTED, in which
|
||||
# case we should not be getting called.
|
||||
|
|
@ -965,7 +1026,7 @@ class App:
|
|||
subsystem.on_app_suspend()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_suspend for subsystem %s.', subsystem
|
||||
'Error in on_app_suspend() for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
def _on_unsuspend(self) -> None:
|
||||
|
|
@ -979,7 +1040,7 @@ class App:
|
|||
subsystem.on_app_unsuspend()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_unsuspend for subsystem %s.', subsystem
|
||||
'Error in on_app_unsuspend() for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
def _on_shutting_down(self) -> None:
|
||||
|
|
@ -993,7 +1054,7 @@ class App:
|
|||
subsystem.on_app_shutdown()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_shutdown for subsystem %s.', subsystem
|
||||
'Error in on_app_shutdown() for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
# Now kick off any async shutdown task(s).
|
||||
|
|
@ -1011,7 +1072,7 @@ class App:
|
|||
subsystem.on_app_shutdown_complete()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_shutdown_complete for subsystem %s.',
|
||||
'Error in on_app_shutdown_complete() for subsystem %s.',
|
||||
subsystem,
|
||||
)
|
||||
|
||||
|
|
@ -1020,10 +1081,10 @@ class App:
|
|||
|
||||
# Spin and wait for anything blocking shutdown to complete.
|
||||
starttime = _babase.apptime()
|
||||
_babase.lifecyclelog('shutdown-suppress wait begin')
|
||||
lifecyclelog.info('shutdown-suppress-wait begin')
|
||||
while _babase.shutdown_suppress_count() > 0:
|
||||
await asyncio.sleep(0.001)
|
||||
_babase.lifecyclelog('shutdown-suppress wait end')
|
||||
lifecyclelog.info('shutdown-suppress-wait end')
|
||||
duration = _babase.apptime() - starttime
|
||||
if duration > 1.0:
|
||||
logging.warning(
|
||||
|
|
@ -1036,7 +1097,7 @@ class App:
|
|||
import asyncio
|
||||
|
||||
# Kick off a short fade and give it time to complete.
|
||||
_babase.lifecyclelog('fade-and-shutdown-graphics begin')
|
||||
lifecyclelog.info('fade-and-shutdown-graphics begin')
|
||||
_babase.fade_screen(False, time=0.15)
|
||||
await asyncio.sleep(0.15)
|
||||
|
||||
|
|
@ -1045,27 +1106,19 @@ class App:
|
|||
_babase.graphics_shutdown_begin()
|
||||
while not _babase.graphics_shutdown_is_complete():
|
||||
await asyncio.sleep(0.01)
|
||||
_babase.lifecyclelog('fade-and-shutdown-graphics end')
|
||||
lifecyclelog.info('fade-and-shutdown-graphics end')
|
||||
|
||||
async def _fade_and_shutdown_audio(self) -> None:
|
||||
import asyncio
|
||||
|
||||
# Tell the audio system to go down and give it a bit of
|
||||
# time to do so gracefully.
|
||||
_babase.lifecyclelog('fade-and-shutdown-audio begin')
|
||||
lifecyclelog.info('fade-and-shutdown-audio begin')
|
||||
_babase.audio_shutdown_begin()
|
||||
await asyncio.sleep(0.15)
|
||||
while not _babase.audio_shutdown_is_complete():
|
||||
await asyncio.sleep(0.01)
|
||||
_babase.lifecyclelog('fade-and-shutdown-audio end')
|
||||
|
||||
def _threadpool_no_wait_done(self, fut: Future) -> None:
|
||||
try:
|
||||
fut.result()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in work submitted via threadpool_submit_no_wait()'
|
||||
)
|
||||
lifecyclelog.info('fade-and-shutdown-audio end')
|
||||
|
||||
def _thread_pool_thread_init(self) -> None:
|
||||
# Help keep things clear in profiling tools/etc.
|
||||
|
|
|
|||
39
dist/ba_data/python/babase/_appconfig.py
vendored
39
dist/ba_data/python/babase/_appconfig.py
vendored
|
|
@ -3,7 +3,6 @@
|
|||
"""Provides the AppConfig class."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _babase
|
||||
|
|
@ -101,43 +100,6 @@ class AppConfig(dict):
|
|||
self.commit()
|
||||
|
||||
|
||||
def read_app_config() -> AppConfig:
|
||||
"""Read the app config."""
|
||||
import os
|
||||
import json
|
||||
|
||||
# NOTE: it is assumed that this only gets called once and the config
|
||||
# object will not change from here on out
|
||||
config_file_path = _babase.app.env.config_file_path
|
||||
config_contents = ''
|
||||
try:
|
||||
if os.path.exists(config_file_path):
|
||||
with open(config_file_path, encoding='utf-8') as infile:
|
||||
config_contents = infile.read()
|
||||
config = AppConfig(json.loads(config_contents))
|
||||
else:
|
||||
config = AppConfig()
|
||||
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"Error reading config file '%s' at time %.3f.\n"
|
||||
"Backing up broken config to'%s.broken'.",
|
||||
config_file_path,
|
||||
_babase.apptime(),
|
||||
config_file_path,
|
||||
)
|
||||
|
||||
try:
|
||||
import shutil
|
||||
|
||||
shutil.copyfile(config_file_path, config_file_path + '.broken')
|
||||
except Exception:
|
||||
logging.exception('Error copying broken config.')
|
||||
config = AppConfig()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def commit_app_config() -> None:
|
||||
"""Commit the config to persistent storage.
|
||||
|
||||
|
|
@ -145,6 +107,7 @@ def commit_app_config() -> None:
|
|||
|
||||
(internal)
|
||||
"""
|
||||
# FIXME - this should not require plus.
|
||||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
|
|
|
|||
18
dist/ba_data/python/babase/_appmode.py
vendored
18
dist/ba_data/python/babase/_appmode.py
vendored
|
|
@ -27,20 +27,22 @@ class AppMode:
|
|||
"""Return whether this mode can handle the provided intent.
|
||||
|
||||
For this to return True, the AppMode must claim to support the
|
||||
provided intent (via its _supports_intent() method) AND the
|
||||
provided intent (via its _can_handle_intent() method) AND the
|
||||
AppExperience associated with the AppMode must be supported by
|
||||
the current app and runtime environment.
|
||||
"""
|
||||
# FIXME: check AppExperience.
|
||||
return cls._supports_intent(intent)
|
||||
# TODO: check AppExperience against current environment.
|
||||
return cls._can_handle_intent(intent)
|
||||
|
||||
@classmethod
|
||||
def _supports_intent(cls, intent: AppIntent) -> bool:
|
||||
def _can_handle_intent(cls, intent: AppIntent) -> bool:
|
||||
"""Return whether our mode can handle the provided intent.
|
||||
|
||||
AppModes should override this to define what they can handle.
|
||||
Note that AppExperience does not have to be considered here; that
|
||||
is handled automatically by the can_handle_intent() call."""
|
||||
AppModes should override this to communicate what they can
|
||||
handle. Note that AppExperience does not have to be considered
|
||||
here; that is handled automatically by the can_handle_intent()
|
||||
call.
|
||||
"""
|
||||
raise NotImplementedError('AppMode subclasses must override this.')
|
||||
|
||||
def handle_intent(self, intent: AppIntent) -> None:
|
||||
|
|
@ -54,7 +56,7 @@ class AppMode:
|
|||
"""Called when the mode is being deactivated."""
|
||||
|
||||
def on_app_active_changed(self) -> None:
|
||||
"""Called when babase.app.active changes.
|
||||
"""Called when ba*.app.active changes while this mode is active.
|
||||
|
||||
The app-mode may want to take action such as pausing a running
|
||||
game in such cases.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class AppModeSelector:
|
||||
"""Defines which AppModes to use to handle given AppIntents.
|
||||
"""Defines which AppModes are available or used to handle given AppIntents.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
|
|
@ -29,4 +29,4 @@ class AppModeSelector:
|
|||
This may be called in a background thread, so avoid any calls
|
||||
limited to logic thread use/etc.
|
||||
"""
|
||||
raise NotImplementedError('app_mode_for_intent() should be overridden.')
|
||||
raise NotImplementedError()
|
||||
|
|
|
|||
21
dist/ba_data/python/babase/_appsubsystem.py
vendored
21
dist/ba_data/python/babase/_appsubsystem.py
vendored
|
|
@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
|
|||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
from babase import UIScale
|
||||
|
||||
|
||||
class AppSubsystem:
|
||||
|
|
@ -53,3 +53,22 @@ class AppSubsystem:
|
|||
|
||||
def do_apply_app_config(self) -> None:
|
||||
"""Called when the app config should be applied."""
|
||||
|
||||
def on_ui_scale_change(self) -> None:
|
||||
"""Called when screen ui-scale changes.
|
||||
|
||||
Will not be called for the initial ui scale.
|
||||
"""
|
||||
|
||||
def on_screen_size_change(self) -> None:
|
||||
"""Called when the screen size changes.
|
||||
|
||||
Will not be called for the initial screen size.
|
||||
"""
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the subsystem to a default state.
|
||||
|
||||
This is called when switching app modes, but may be called
|
||||
at other times too.
|
||||
"""
|
||||
|
|
|
|||
21
dist/ba_data/python/babase/_apputils.py
vendored
21
dist/ba_data/python/babase/_apputils.py
vendored
|
|
@ -11,25 +11,38 @@ from functools import partial
|
|||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from efro.log import LogLevel
|
||||
from efro.util import utc_now
|
||||
from efro.logging import LogLevel
|
||||
from efro.dataclassio import ioprepped, dataclass_to_json, dataclass_from_json
|
||||
|
||||
import _babase
|
||||
from babase._appsubsystem import AppSubsystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
from typing import Any, TextIO, Callable
|
||||
|
||||
import babase
|
||||
|
||||
|
||||
def utc_now_cloud() -> datetime.datetime:
|
||||
"""Returns estimated utc time regardless of local clock settings.
|
||||
|
||||
Applies offsets pulled from server communication/etc.
|
||||
"""
|
||||
# TODO: wire this up. Just using local time for now. Make sure that
|
||||
# BaseFeatureSet::TimeSinceEpochCloudSeconds() and this are synced
|
||||
# up.
|
||||
return utc_now()
|
||||
|
||||
|
||||
def is_browser_likely_available() -> bool:
|
||||
"""Return whether a browser likely exists on the current device.
|
||||
|
||||
category: General Utility Functions
|
||||
|
||||
If this returns False you may want to avoid calling babase.show_url()
|
||||
with any lengthy addresses. (ba.show_url() will display an address
|
||||
If this returns False you may want to avoid calling babase.open_url()
|
||||
with any lengthy addresses. (babase.open_url() will display an address
|
||||
as a string in a window if unable to bring up a browser, but that
|
||||
is only useful for simple URLs.)
|
||||
"""
|
||||
|
|
@ -115,7 +128,7 @@ def handle_v1_cloud_log() -> None:
|
|||
'userRanCommands': _babase.has_user_run_commands(),
|
||||
'time': _babase.apptime(),
|
||||
'userModded': _babase.workspaces_in_use(),
|
||||
'newsShow': plus.get_news_show(),
|
||||
'newsShow': plus.get_classic_news_show(),
|
||||
}
|
||||
|
||||
def response(data: Any) -> None:
|
||||
|
|
|
|||
197
dist/ba_data/python/babase/_cloud.py
vendored
197
dist/ba_data/python/babase/_cloud.py
vendored
|
|
@ -1,197 +1,26 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to the cloud."""
|
||||
|
||||
"""Cloud related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, overload
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _babase
|
||||
from babase._appsubsystem import AppSubsystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
||||
from efro.message import Message, Response
|
||||
import bacommon.cloud
|
||||
|
||||
DEBUG_LOG = False
|
||||
|
||||
# TODO: Should make it possible to define a protocol in bacommon.cloud and
|
||||
# autogenerate this. That would give us type safety between this and
|
||||
# internal protocols.
|
||||
pass
|
||||
|
||||
|
||||
class CloudSubsystem(AppSubsystem):
|
||||
"""Manages communication with cloud components."""
|
||||
class CloudSubscription:
|
||||
"""User handle to a subscription to some cloud data.
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""Property equivalent of CloudSubsystem.is_connected()."""
|
||||
return self.is_connected()
|
||||
Do not instantiate these directly; use the subscribe methods
|
||||
in *.app.plus.cloud to create them.
|
||||
"""
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Return whether a connection to the cloud is present.
|
||||
def __init__(self, subscription_id: int) -> None:
|
||||
self._subscription_id = subscription_id
|
||||
|
||||
This is a good indicator (though not for certain) that sending
|
||||
messages will succeed.
|
||||
"""
|
||||
return False # Needs to be overridden
|
||||
|
||||
def on_connectivity_changed(self, connected: bool) -> None:
|
||||
"""Called when cloud connectivity state changes."""
|
||||
if DEBUG_LOG:
|
||||
logging.debug('CloudSubsystem: Connectivity is now %s.', connected)
|
||||
|
||||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
# Inform things that use this.
|
||||
# (TODO: should generalize this into some sort of registration system)
|
||||
plus.accounts.on_cloud_connectivity_changed(connected)
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.LoginProxyRequestMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.cloud.LoginProxyRequestResponse | Exception], None
|
||||
],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.LoginProxyStateQueryMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.cloud.LoginProxyStateQueryResponse | Exception], None
|
||||
],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.LoginProxyCompleteMessage,
|
||||
on_response: Callable[[None | Exception], None],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.PingMessage,
|
||||
on_response: Callable[[bacommon.cloud.PingResponse | Exception], None],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.SignInMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.cloud.SignInResponse | Exception], None
|
||||
],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.ManageAccountMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.cloud.ManageAccountResponse | Exception], None
|
||||
],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: Message,
|
||||
on_response: Callable[[Any], None],
|
||||
) -> None:
|
||||
"""Asynchronously send a message to the cloud from the logic thread.
|
||||
|
||||
The provided on_response call will be run in the logic thread
|
||||
and passed either the response or the error that occurred.
|
||||
"""
|
||||
from babase._general import Call
|
||||
|
||||
del msg # Unused.
|
||||
|
||||
_babase.pushcall(
|
||||
Call(
|
||||
on_response,
|
||||
RuntimeError('Cloud functionality is not available.'),
|
||||
)
|
||||
)
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self, msg: bacommon.cloud.WorkspaceFetchMessage
|
||||
) -> bacommon.cloud.WorkspaceFetchResponse:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self, msg: bacommon.cloud.MerchAvailabilityMessage
|
||||
) -> bacommon.cloud.MerchAvailabilityResponse:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self, msg: bacommon.cloud.TestMessage
|
||||
) -> bacommon.cloud.TestResponse:
|
||||
...
|
||||
|
||||
def send_message(self, msg: Message) -> Response | None:
|
||||
"""Synchronously send a message to the cloud.
|
||||
|
||||
Must be called from a background thread.
|
||||
"""
|
||||
raise RuntimeError('Cloud functionality is not available.')
|
||||
|
||||
|
||||
def cloud_console_exec(code: str) -> None:
|
||||
"""Called by the cloud console to run code in the logic thread."""
|
||||
import sys
|
||||
import __main__
|
||||
|
||||
try:
|
||||
# First try it as eval.
|
||||
try:
|
||||
evalcode = compile(code, '<console>', 'eval')
|
||||
except SyntaxError:
|
||||
evalcode = None
|
||||
except Exception:
|
||||
# hmm; when we can't compile it as eval will we always get
|
||||
# syntax error?
|
||||
logging.exception(
|
||||
'unexpected error compiling code for cloud-console eval.'
|
||||
)
|
||||
evalcode = None
|
||||
if evalcode is not None:
|
||||
# pylint: disable=eval-used
|
||||
value = eval(evalcode, vars(__main__), vars(__main__))
|
||||
# For eval-able statements, print the resulting value if
|
||||
# it is not None (just like standard Python interpreter).
|
||||
if value is not None:
|
||||
print(repr(value), file=sys.stderr)
|
||||
|
||||
# Fall back to exec if we couldn't compile it as eval.
|
||||
else:
|
||||
execcode = compile(code, '<console>', 'exec')
|
||||
# pylint: disable=exec-used
|
||||
exec(execcode, vars(__main__), vars(__main__))
|
||||
except Exception:
|
||||
import traceback
|
||||
|
||||
apptime = _babase.apptime()
|
||||
print(f'Exec error at time {apptime:.2f}.', file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
|
||||
# This helps the logging system ship stderr back to the
|
||||
# cloud promptly.
|
||||
sys.stderr.flush()
|
||||
def __del__(self) -> None:
|
||||
if _babase.app.plus is not None:
|
||||
_babase.app.plus.cloud.unsubscribe(self._subscription_id)
|
||||
|
|
|
|||
106
dist/ba_data/python/babase/_devconsole.py
vendored
106
dist/ba_data/python/babase/_devconsole.py
vendored
|
|
@ -4,9 +4,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING, override
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _babase
|
||||
|
||||
|
|
@ -30,10 +30,27 @@ class DevConsoleTab:
|
|||
pos: tuple[float, float],
|
||||
size: tuple[float, float],
|
||||
call: Callable[[], Any] | None = None,
|
||||
*,
|
||||
h_anchor: Literal['left', 'center', 'right'] = 'center',
|
||||
label_scale: float = 1.0,
|
||||
corner_radius: float = 8.0,
|
||||
style: Literal['normal', 'dark'] = 'normal',
|
||||
style: Literal[
|
||||
'normal',
|
||||
'bright',
|
||||
'red',
|
||||
'red_bright',
|
||||
'purple',
|
||||
'purple_bright',
|
||||
'yellow',
|
||||
'yellow_bright',
|
||||
'blue',
|
||||
'blue_bright',
|
||||
'white',
|
||||
'white_bright',
|
||||
'black',
|
||||
'black_bright',
|
||||
] = 'normal',
|
||||
disabled: bool = False,
|
||||
) -> None:
|
||||
"""Add a button to the tab being refreshed."""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
|
|
@ -48,12 +65,14 @@ class DevConsoleTab:
|
|||
label_scale,
|
||||
corner_radius,
|
||||
style,
|
||||
disabled,
|
||||
)
|
||||
|
||||
def text(
|
||||
self,
|
||||
text: str,
|
||||
pos: tuple[float, float],
|
||||
*,
|
||||
h_anchor: Literal['left', 'center', 'right'] = 'center',
|
||||
h_align: Literal['left', 'center', 'right'] = 'center',
|
||||
v_align: Literal['top', 'center', 'bottom', 'none'] = 'center',
|
||||
|
|
@ -93,47 +112,6 @@ class DevConsoleTab:
|
|||
return _babase.dev_console_base_scale()
|
||||
|
||||
|
||||
class DevConsoleTabPython(DevConsoleTab):
|
||||
"""The Python dev-console tab."""
|
||||
|
||||
@override
|
||||
def refresh(self) -> None:
|
||||
self.python_terminal()
|
||||
|
||||
|
||||
class DevConsoleTabTest(DevConsoleTab):
|
||||
"""Test dev-console tab."""
|
||||
|
||||
@override
|
||||
def refresh(self) -> None:
|
||||
import random
|
||||
|
||||
self.button(
|
||||
f'FLOOP-{random.randrange(200)}',
|
||||
pos=(10, 10),
|
||||
size=(100, 30),
|
||||
h_anchor='left',
|
||||
label_scale=0.6,
|
||||
call=self.request_refresh,
|
||||
)
|
||||
self.button(
|
||||
f'FLOOP2-{random.randrange(200)}',
|
||||
pos=(120, 10),
|
||||
size=(100, 30),
|
||||
h_anchor='left',
|
||||
label_scale=0.6,
|
||||
style='dark',
|
||||
)
|
||||
self.text(
|
||||
'TestText',
|
||||
scale=0.8,
|
||||
pos=(15, 50),
|
||||
h_anchor='left',
|
||||
h_align='left',
|
||||
v_align='none',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevConsoleTabEntry:
|
||||
"""Represents a distinct tab in the dev-console."""
|
||||
|
|
@ -154,26 +132,50 @@ class DevConsoleSubsystem:
|
|||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from babase._devconsoletabs import (
|
||||
DevConsoleTabPython,
|
||||
DevConsoleTabAppModes,
|
||||
DevConsoleTabUI,
|
||||
DevConsoleTabLogging,
|
||||
DevConsoleTabTest,
|
||||
)
|
||||
|
||||
# All tabs in the dev-console. Add your own stuff here via
|
||||
# plugins or whatnot.
|
||||
self.tabs: list[DevConsoleTabEntry] = [
|
||||
DevConsoleTabEntry('Python', DevConsoleTabPython)
|
||||
DevConsoleTabEntry('Python', DevConsoleTabPython),
|
||||
DevConsoleTabEntry('AppModes', DevConsoleTabAppModes),
|
||||
DevConsoleTabEntry('UI', DevConsoleTabUI),
|
||||
DevConsoleTabEntry('Logging', DevConsoleTabLogging),
|
||||
]
|
||||
if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1':
|
||||
self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest))
|
||||
self.is_refreshing = False
|
||||
self._tab_instances: dict[str, DevConsoleTab] = {}
|
||||
|
||||
def do_refresh_tab(self, tabname: str) -> None:
|
||||
"""Called by the C++ layer when a tab should be filled out."""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# FIXME: We currently won't handle multiple tabs with the same
|
||||
# name. We should give a clean error or something in that case.
|
||||
tab: DevConsoleTab | None = None
|
||||
for tabentry in self.tabs:
|
||||
if tabentry.name == tabname:
|
||||
tab = tabentry.factory()
|
||||
break
|
||||
# Make noise if we have repeating tab names, as that breaks our
|
||||
# logic.
|
||||
if __debug__:
|
||||
alltabnames = set[str](tabentry.name for tabentry in self.tabs)
|
||||
if len(alltabnames) != len(self.tabs):
|
||||
logging.error(
|
||||
'Duplicate dev-console tab names found;'
|
||||
' tabs may behave unpredictably.'
|
||||
)
|
||||
|
||||
tab: DevConsoleTab | None = self._tab_instances.get(tabname)
|
||||
|
||||
# If we haven't instantiated this tab yet, do so.
|
||||
if tab is None:
|
||||
for tabentry in self.tabs:
|
||||
if tabentry.name == tabname:
|
||||
tab = self._tab_instances[tabname] = tabentry.factory()
|
||||
break
|
||||
|
||||
if tab is None:
|
||||
logging.error(
|
||||
|
|
|
|||
671
dist/ba_data/python/babase/_devconsoletabs.py
vendored
Normal file
671
dist/ba_data/python/babase/_devconsoletabs.py
vendored
Normal file
|
|
@ -0,0 +1,671 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Predefined tabs for the dev console."""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, override, TypeVar, Generic
|
||||
|
||||
import _babase
|
||||
|
||||
from babase._devconsole import DevConsoleTab
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Literal
|
||||
|
||||
from bacommon.loggercontrol import LoggerControlConfig
|
||||
from babase import AppMode
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class DevConsoleTabPython(DevConsoleTab):
|
||||
"""The Python dev-console tab."""
|
||||
|
||||
@override
|
||||
def refresh(self) -> None:
|
||||
self.python_terminal()
|
||||
|
||||
|
||||
class DevConsoleTabAppModes(DevConsoleTab):
|
||||
"""Tab to switch app modes."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._app_modes: list[type[AppMode]] | None = None
|
||||
self._app_modes_loading = False
|
||||
|
||||
def _on_app_modes_loaded(self, modes: list[type[AppMode]]) -> None:
|
||||
from babase._appintent import AppIntentDefault
|
||||
|
||||
intent = AppIntentDefault()
|
||||
|
||||
# Limit to modes that can handle default intents since that's
|
||||
# what we use.
|
||||
self._app_modes = [
|
||||
mode for mode in modes if mode.can_handle_intent(intent)
|
||||
]
|
||||
self.request_refresh()
|
||||
|
||||
@override
|
||||
def refresh(self) -> None:
|
||||
from babase import AppMode
|
||||
|
||||
# Kick off a load if applicable.
|
||||
if self._app_modes is None and not self._app_modes_loading:
|
||||
_babase.app.meta.load_exported_classes(
|
||||
AppMode, self._on_app_modes_loaded
|
||||
)
|
||||
|
||||
# Just say 'loading' if we don't have app-modes yet.
|
||||
if self._app_modes is None:
|
||||
self.text(
|
||||
'Loading...', pos=(0, 30), h_anchor='center', h_align='center'
|
||||
)
|
||||
return
|
||||
|
||||
bwidth = 260
|
||||
bpadding = 5
|
||||
|
||||
xoffs = -0.5 * bwidth * len(self._app_modes)
|
||||
|
||||
self.text(
|
||||
'Available AppModes:',
|
||||
scale=0.8,
|
||||
pos=(0, 75),
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
for i, mode in enumerate(self._app_modes):
|
||||
self.button(
|
||||
f'{mode.__module__}.{mode.__qualname__}',
|
||||
pos=(xoffs + i * bwidth + bpadding, 10),
|
||||
size=(bwidth - 2.0 * bpadding, 40),
|
||||
label_scale=0.6,
|
||||
call=partial(self._set_app_mode, mode),
|
||||
style=(
|
||||
'bright'
|
||||
if isinstance(_babase.app._mode, mode)
|
||||
else 'normal'
|
||||
),
|
||||
)
|
||||
|
||||
def _set_app_mode(self, mode: type[AppMode]) -> None:
|
||||
from babase._appintent import AppIntentDefault
|
||||
|
||||
intent = AppIntentDefault()
|
||||
|
||||
# Use private functionality to force a specific app-mode to
|
||||
# handle this intent. Note that this should never be done
|
||||
# outside of this explicit testing case. It is the app's job to
|
||||
# determine which app-mode should be used to handle a given
|
||||
# intent.
|
||||
setattr(intent, '_force_app_mode_handler', mode)
|
||||
|
||||
_babase.app.set_intent(intent)
|
||||
|
||||
# Slight hackish: need to wait a moment before refreshing to
|
||||
# pick up the newly current mode, as mode switches are
|
||||
# asynchronous.
|
||||
_babase.apptimer(0.1, self.request_refresh)
|
||||
|
||||
|
||||
class DevConsoleTabUI(DevConsoleTab):
|
||||
"""Tab to debug/test UI stuff."""
|
||||
|
||||
@override
|
||||
def refresh(self) -> None:
|
||||
from babase._mgen.enums import UIScale
|
||||
|
||||
xoffs = -375
|
||||
|
||||
self.text(
|
||||
'Make sure all UIs either fit in the virtual safe area'
|
||||
' or dynamically respond to screen size changes.',
|
||||
scale=0.6,
|
||||
pos=(xoffs + 15, 70),
|
||||
h_align='left',
|
||||
v_align='center',
|
||||
)
|
||||
|
||||
ui_overlay = _babase.get_draw_virtual_safe_area_bounds()
|
||||
self.button(
|
||||
'Virtual Safe Area ON' if ui_overlay else 'Virtual Safe Area OFF',
|
||||
pos=(xoffs + 10, 10),
|
||||
size=(200, 30),
|
||||
label_scale=0.6,
|
||||
call=self.toggle_ui_overlay,
|
||||
style='bright' if ui_overlay else 'normal',
|
||||
)
|
||||
x = 300
|
||||
self.text(
|
||||
'UI-Scale',
|
||||
pos=(xoffs + x - 5, 15),
|
||||
h_align='right',
|
||||
v_align='none',
|
||||
scale=0.6,
|
||||
)
|
||||
|
||||
bwidth = 100
|
||||
for scale in UIScale:
|
||||
self.button(
|
||||
scale.name.capitalize(),
|
||||
pos=(xoffs + x, 10),
|
||||
size=(bwidth, 30),
|
||||
label_scale=0.6,
|
||||
call=partial(_babase.app.set_ui_scale, scale),
|
||||
style=(
|
||||
'bright'
|
||||
if scale.name.lower() == _babase.get_ui_scale()
|
||||
else 'normal'
|
||||
),
|
||||
)
|
||||
x += bwidth + 2
|
||||
|
||||
def toggle_ui_overlay(self) -> None:
|
||||
"""Toggle UI overlay drawing."""
|
||||
_babase.set_draw_virtual_safe_area_bounds(
|
||||
not _babase.get_draw_virtual_safe_area_bounds()
|
||||
)
|
||||
self.request_refresh()
|
||||
|
||||
|
||||
class Table(Generic[T]):
|
||||
"""Used to show controls for arbitrarily large data in a grid form."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
entries: list[T],
|
||||
draw_entry_call: Callable[
|
||||
[int, T, DevConsoleTab, float, float, float, float], None
|
||||
],
|
||||
*,
|
||||
entry_width: float = 300.0,
|
||||
entry_height: float = 40.0,
|
||||
margin_left_right: float = 60.0,
|
||||
debug_bounds: bool = False,
|
||||
max_columns: int | None = None,
|
||||
) -> None:
|
||||
self._title = title
|
||||
self._entry_width = entry_width
|
||||
self._entry_height = entry_height
|
||||
self._margin_left_right = margin_left_right
|
||||
self._focus_entry_index = 0
|
||||
self._entries_per_page = 1
|
||||
self._debug_bounds = debug_bounds
|
||||
self._entries = entries
|
||||
self._draw_entry_call = draw_entry_call
|
||||
self._max_columns = max_columns
|
||||
|
||||
# Values updated on refresh (for aligning other custom
|
||||
# widgets/etc.)
|
||||
self.top_left: tuple[float, float] = (0.0, 0.0)
|
||||
self.top_right: tuple[float, float] = (0.0, 0.0)
|
||||
|
||||
def set_entries(self, entries: list[T]) -> None:
|
||||
"""Update table entries."""
|
||||
self._entries = entries
|
||||
|
||||
# Clamp focus to new entries.
|
||||
self._focus_entry_index = max(
|
||||
0, min(len(self._entries) - 1, self._focus_entry_index)
|
||||
)
|
||||
|
||||
def set_focus_entry_index(self, index: int) -> None:
|
||||
"""Explicitly set the focused entry.
|
||||
|
||||
This affects which page is shown at the next refresh.
|
||||
"""
|
||||
self._focus_entry_index = max(0, min(len(self._entries) - 1, index))
|
||||
|
||||
def refresh(self, tab: DevConsoleTab) -> None:
|
||||
"""Call to refresh the data."""
|
||||
# pylint: disable=too-many-locals
|
||||
|
||||
margin_top = 50.0
|
||||
margin_bottom = 10.0
|
||||
|
||||
# Update how much we can fit on a page based on our current size.
|
||||
max_entry_area_width = tab.width - (self._margin_left_right * 2.0)
|
||||
max_entry_area_height = tab.height - (margin_top + margin_bottom)
|
||||
columns = max(1, int(max_entry_area_width / self._entry_width))
|
||||
if self._max_columns is not None:
|
||||
columns = min(columns, self._max_columns)
|
||||
rows = max(1, int(max_entry_area_height / self._entry_height))
|
||||
self._entries_per_page = rows * columns
|
||||
|
||||
# See which page our focus index falls in.
|
||||
pagemax = math.ceil(len(self._entries) / self._entries_per_page)
|
||||
|
||||
page = self._focus_entry_index // self._entries_per_page
|
||||
entry_offset = page * self._entries_per_page
|
||||
|
||||
entries_on_this_page = min(
|
||||
self._entries_per_page, len(self._entries) - entry_offset
|
||||
)
|
||||
columns_on_this_page = math.ceil(entries_on_this_page / rows)
|
||||
rows_on_this_page = min(entries_on_this_page, rows)
|
||||
|
||||
# We attach things to the center so resizes are smooth but we do
|
||||
# some math in a left-centric way.
|
||||
center_to_left = tab.width * -0.5
|
||||
|
||||
# Center our columns.
|
||||
xoffs = 0.5 * (
|
||||
max_entry_area_width - columns_on_this_page * self._entry_width
|
||||
)
|
||||
|
||||
# Align everything to the bottom of the dev-console.
|
||||
#
|
||||
# UPDATE: Nevermind; top feels better. Keeping this code around
|
||||
# in case we ever want to make it an option though.
|
||||
if bool(False):
|
||||
yoffs = -1.0 * (
|
||||
tab.height
|
||||
- (
|
||||
rows_on_this_page * self._entry_height
|
||||
+ margin_top
|
||||
+ margin_bottom
|
||||
)
|
||||
)
|
||||
else:
|
||||
yoffs = 0
|
||||
|
||||
# Keep our corners up to date for user use.
|
||||
self.top_left = (center_to_left + xoffs, tab.height + yoffs)
|
||||
self.top_right = (
|
||||
self.top_left[0]
|
||||
+ self._margin_left_right * 2.0
|
||||
+ columns_on_this_page * self._entry_width,
|
||||
self.top_left[1],
|
||||
)
|
||||
|
||||
# Page left/right buttons.
|
||||
tab.button(
|
||||
'<',
|
||||
pos=(
|
||||
center_to_left + xoffs,
|
||||
yoffs + tab.height - margin_top - rows * self._entry_height,
|
||||
),
|
||||
size=(
|
||||
self._margin_left_right,
|
||||
rows * self._entry_height,
|
||||
),
|
||||
call=partial(self._page_left, tab),
|
||||
disabled=entry_offset == 0,
|
||||
)
|
||||
tab.button(
|
||||
'>',
|
||||
pos=(
|
||||
center_to_left
|
||||
+ xoffs
|
||||
+ self._margin_left_right
|
||||
+ columns_on_this_page * self._entry_width,
|
||||
yoffs + tab.height - margin_top - rows * self._entry_height,
|
||||
),
|
||||
size=(
|
||||
self._margin_left_right,
|
||||
rows * self._entry_height,
|
||||
),
|
||||
call=partial(self._page_right, tab),
|
||||
disabled=(
|
||||
entry_offset + entries_on_this_page >= len(self._entries)
|
||||
),
|
||||
)
|
||||
|
||||
for column in range(columns):
|
||||
for row in range(rows):
|
||||
entry_index = entry_offset + column * rows + row
|
||||
if entry_index >= len(self._entries):
|
||||
break
|
||||
|
||||
xpos = (
|
||||
xoffs + self._margin_left_right + self._entry_width * column
|
||||
)
|
||||
ypos = (
|
||||
yoffs
|
||||
+ tab.height
|
||||
- margin_top
|
||||
- self._entry_height * (row + 1.0)
|
||||
)
|
||||
# Draw debug bounds.
|
||||
if self._debug_bounds:
|
||||
tab.button(
|
||||
str(entry_index),
|
||||
pos=(
|
||||
center_to_left + xpos,
|
||||
ypos,
|
||||
),
|
||||
size=(self._entry_width, self._entry_height),
|
||||
# h_anchor='left',
|
||||
)
|
||||
# Run user drawing.
|
||||
self._draw_entry_call(
|
||||
entry_index,
|
||||
self._entries[entry_index],
|
||||
tab,
|
||||
center_to_left + xpos,
|
||||
ypos,
|
||||
self._entry_width,
|
||||
self._entry_height,
|
||||
)
|
||||
|
||||
if entry_index >= len(self._entries):
|
||||
break
|
||||
|
||||
tab.text(
|
||||
f'{self._title} ({page + 1}/{pagemax})',
|
||||
scale=0.8,
|
||||
pos=(0, yoffs + tab.height - margin_top * 0.5),
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
|
||||
def _page_right(self, tab: DevConsoleTab) -> None:
|
||||
# Set focus on the first entry in the page before the current.
|
||||
page = self._focus_entry_index // self._entries_per_page
|
||||
page += 1
|
||||
self.set_focus_entry_index(page * self._entries_per_page)
|
||||
tab.request_refresh()
|
||||
|
||||
def _page_left(self, tab: DevConsoleTab) -> None:
|
||||
# Set focus on the first entry in the page after the current.
|
||||
page = self._focus_entry_index // self._entries_per_page
|
||||
page -= 1
|
||||
self.set_focus_entry_index(page * self._entries_per_page)
|
||||
tab.request_refresh()
|
||||
|
||||
|
||||
class DevConsoleTabLogging(DevConsoleTab):
|
||||
"""Tab to wrangle logging levels."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
self._table = Table(
|
||||
title='Logging Levels',
|
||||
entry_width=800,
|
||||
entry_height=42,
|
||||
debug_bounds=False,
|
||||
entries=list[str](),
|
||||
draw_entry_call=self._draw_entry,
|
||||
max_columns=1,
|
||||
)
|
||||
|
||||
@override
|
||||
def refresh(self) -> None:
|
||||
assert self._table is not None
|
||||
|
||||
# Update table entries with the latest set of loggers (this can
|
||||
# change over time).
|
||||
self._table.set_entries(
|
||||
['root'] + sorted(logging.root.manager.loggerDict)
|
||||
)
|
||||
|
||||
# Draw the table.
|
||||
self._table.refresh(self)
|
||||
|
||||
# Draw our control buttons in the corners.
|
||||
tl = self._table.top_left
|
||||
tr = self._table.top_right
|
||||
bwidth = 140.0
|
||||
bheight = 30.0
|
||||
bvpad = 10.0
|
||||
self.button(
|
||||
'Reset',
|
||||
pos=(tl[0], tl[1] - bheight - bvpad),
|
||||
size=(bwidth, bheight),
|
||||
label_scale=0.6,
|
||||
call=self._reset,
|
||||
disabled=(
|
||||
not self._get_reset_logger_control_config().would_make_changes()
|
||||
),
|
||||
)
|
||||
self.button(
|
||||
'Cloud Control OFF',
|
||||
pos=(tr[0] - bwidth, tl[1] - bheight - bvpad),
|
||||
size=(bwidth, bheight),
|
||||
label_scale=0.6,
|
||||
disabled=True,
|
||||
)
|
||||
|
||||
def _get_reset_logger_control_config(self) -> LoggerControlConfig:
|
||||
from bacommon.logging import get_base_logger_control_config_client
|
||||
|
||||
return get_base_logger_control_config_client()
|
||||
|
||||
def _reset(self) -> None:
|
||||
|
||||
self._get_reset_logger_control_config().apply()
|
||||
|
||||
# Let the native layer know that levels changed.
|
||||
_babase.update_internal_logger_levels()
|
||||
|
||||
# Blow away any existing values in app-config.
|
||||
appconfig = _babase.app.config
|
||||
if 'Log Levels' in appconfig:
|
||||
del appconfig['Log Levels']
|
||||
appconfig.commit()
|
||||
|
||||
self.request_refresh()
|
||||
|
||||
def _set_entry_val(self, entry_index: int, entry: str, val: int) -> None:
|
||||
|
||||
from bacommon.logging import get_base_logger_control_config_client
|
||||
from bacommon.loggercontrol import LoggerControlConfig
|
||||
|
||||
# Focus on this entry with any interaction, so if we get resized
|
||||
# it'll still be visible.
|
||||
self._table.set_focus_entry_index(entry_index)
|
||||
|
||||
logging.getLogger(entry).setLevel(val)
|
||||
|
||||
# Let the native layer know that levels changed.
|
||||
_babase.update_internal_logger_levels()
|
||||
|
||||
# Store only changes compared to the base config.
|
||||
baseconfig = get_base_logger_control_config_client()
|
||||
config = LoggerControlConfig.from_current_loggers().diff(baseconfig)
|
||||
|
||||
appconfig = _babase.app.config
|
||||
appconfig['Log Levels'] = config.levels
|
||||
appconfig.commit()
|
||||
|
||||
self.request_refresh()
|
||||
|
||||
def _draw_entry(
|
||||
self,
|
||||
entry_index: int,
|
||||
entry: str,
|
||||
tab: DevConsoleTab,
|
||||
x: float,
|
||||
y: float,
|
||||
width: float,
|
||||
height: float,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
# pylint: disable=too-many-locals
|
||||
|
||||
xoffs = -15.0
|
||||
bwidth = 80.0
|
||||
btextscale = 0.5
|
||||
tab.text(
|
||||
entry,
|
||||
(
|
||||
x + width - bwidth * 6.5 - 10.0 + xoffs,
|
||||
y + height * 0.5,
|
||||
),
|
||||
h_align='right',
|
||||
scale=0.7,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(entry)
|
||||
level = logger.level
|
||||
index = 0
|
||||
effectivelevel = logger.getEffectiveLevel()
|
||||
# if entry != 'root' and level == logging.NOTSET:
|
||||
# # Show the level being inherited in NOTSET cases.
|
||||
# notsetlevelname = logging.getLevelName(logger.getEffectiveLevel())
|
||||
# if notsetlevelname == 'NOTSET':
|
||||
# notsetname = 'Not Set'
|
||||
# else:
|
||||
# notsetname = f'Not Set ({notsetlevelname.capitalize()})'
|
||||
# else:
|
||||
notsetname = 'Not Set'
|
||||
tab.button(
|
||||
notsetname,
|
||||
pos=(x + width - bwidth * 6.5 + xoffs + 1.0, y + 5.0),
|
||||
size=(bwidth * 1.0 - 2.0, height - 10),
|
||||
label_scale=btextscale,
|
||||
style='white_bright' if level == logging.NOTSET else 'black',
|
||||
call=partial(
|
||||
self._set_entry_val, entry_index, entry, logging.NOTSET
|
||||
),
|
||||
)
|
||||
index += 1
|
||||
tab.button(
|
||||
'Debug',
|
||||
pos=(x + width - bwidth * 5 + xoffs + 1.0, y + 5.0),
|
||||
size=(bwidth - 2.0, height - 10),
|
||||
label_scale=btextscale,
|
||||
style=(
|
||||
'white_bright'
|
||||
if level == logging.DEBUG
|
||||
else 'blue' if effectivelevel <= logging.DEBUG else 'black'
|
||||
),
|
||||
# style='bright' if level == logging.DEBUG else 'normal',
|
||||
call=partial(
|
||||
self._set_entry_val, entry_index, entry, logging.DEBUG
|
||||
),
|
||||
)
|
||||
index += 1
|
||||
tab.button(
|
||||
'Info',
|
||||
pos=(x + width - bwidth * 4 + xoffs + 1.0, y + 5.0),
|
||||
size=(bwidth - 2.0, height - 10),
|
||||
label_scale=btextscale,
|
||||
style=(
|
||||
'white_bright'
|
||||
if level == logging.INFO
|
||||
else 'white' if effectivelevel <= logging.INFO else 'black'
|
||||
),
|
||||
# style='bright' if level == logging.INFO else 'normal',
|
||||
call=partial(self._set_entry_val, entry_index, entry, logging.INFO),
|
||||
)
|
||||
index += 1
|
||||
tab.button(
|
||||
'Warning',
|
||||
pos=(x + width - bwidth * 3 + xoffs + 1.0, y + 5.0),
|
||||
size=(bwidth - 2.0, height - 10),
|
||||
label_scale=btextscale,
|
||||
style=(
|
||||
'white_bright'
|
||||
if level == logging.WARNING
|
||||
else 'yellow' if effectivelevel <= logging.WARNING else 'black'
|
||||
),
|
||||
call=partial(
|
||||
self._set_entry_val, entry_index, entry, logging.WARNING
|
||||
),
|
||||
)
|
||||
index += 1
|
||||
tab.button(
|
||||
'Error',
|
||||
pos=(x + width - bwidth * 2 + xoffs + 1.0, y + 5.0),
|
||||
size=(bwidth - 2.0, height - 10),
|
||||
label_scale=btextscale,
|
||||
style=(
|
||||
'white_bright'
|
||||
if level == logging.ERROR
|
||||
else 'red' if effectivelevel <= logging.ERROR else 'black'
|
||||
),
|
||||
call=partial(
|
||||
self._set_entry_val, entry_index, entry, logging.ERROR
|
||||
),
|
||||
)
|
||||
index += 1
|
||||
tab.button(
|
||||
'Critical',
|
||||
pos=(x + width - bwidth * 1 + xoffs + 1.0, y + 5.0),
|
||||
size=(bwidth - 2.0, height - 10),
|
||||
label_scale=btextscale,
|
||||
style=(
|
||||
'white_bright'
|
||||
if level == logging.CRITICAL
|
||||
else (
|
||||
'purple' if effectivelevel <= logging.CRITICAL else 'black'
|
||||
)
|
||||
),
|
||||
call=partial(
|
||||
self._set_entry_val, entry_index, entry, logging.CRITICAL
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class DevConsoleTabTest(DevConsoleTab):
|
||||
"""Test dev-console tab."""
|
||||
|
||||
@override
|
||||
def refresh(self) -> None:
|
||||
|
||||
self.button(
|
||||
f'FLOOP-{random.randrange(200)}',
|
||||
pos=(10, 10),
|
||||
size=(100, 30),
|
||||
h_anchor='left',
|
||||
label_scale=0.6,
|
||||
call=self.request_refresh,
|
||||
)
|
||||
self.button(
|
||||
f'FLOOP2-{random.randrange(200)}',
|
||||
pos=(120, 10),
|
||||
size=(100, 30),
|
||||
h_anchor='left',
|
||||
label_scale=0.6,
|
||||
style='bright',
|
||||
)
|
||||
self.text(
|
||||
'TestText',
|
||||
scale=0.8,
|
||||
pos=(15, 50),
|
||||
h_anchor='left',
|
||||
h_align='left',
|
||||
v_align='none',
|
||||
)
|
||||
|
||||
# Throw little bits of text in the corners to make sure
|
||||
# widths/heights are correct.
|
||||
self.text(
|
||||
'BL',
|
||||
scale=0.25,
|
||||
pos=(0, 0),
|
||||
h_anchor='left',
|
||||
h_align='left',
|
||||
v_align='bottom',
|
||||
)
|
||||
self.text(
|
||||
'BR',
|
||||
scale=0.25,
|
||||
pos=(self.width, 0),
|
||||
h_anchor='left',
|
||||
h_align='right',
|
||||
v_align='bottom',
|
||||
)
|
||||
self.text(
|
||||
'TL',
|
||||
scale=0.25,
|
||||
pos=(0, self.height),
|
||||
h_anchor='left',
|
||||
h_align='left',
|
||||
v_align='top',
|
||||
)
|
||||
self.text(
|
||||
'TR',
|
||||
scale=0.25,
|
||||
pos=(self.width, self.height),
|
||||
h_anchor='left',
|
||||
h_align='right',
|
||||
v_align='top',
|
||||
)
|
||||
13
dist/ba_data/python/babase/_emptyappmode.py
vendored
13
dist/ba_data/python/babase/_emptyappmode.py
vendored
|
|
@ -15,8 +15,9 @@ if TYPE_CHECKING:
|
|||
from babase import AppIntent
|
||||
|
||||
|
||||
# ba_meta export babase.AppMode
|
||||
class EmptyAppMode(AppMode):
|
||||
"""An empty app mode that can be used as a fallback/etc."""
|
||||
"""An AppMode that does not do much at all."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
|
|
@ -25,24 +26,24 @@ class EmptyAppMode(AppMode):
|
|||
|
||||
@override
|
||||
@classmethod
|
||||
def _supports_intent(cls, intent: AppIntent) -> bool:
|
||||
def _can_handle_intent(cls, intent: AppIntent) -> bool:
|
||||
# We support default and exec intents currently.
|
||||
return isinstance(intent, AppIntentExec | AppIntentDefault)
|
||||
|
||||
@override
|
||||
def handle_intent(self, intent: AppIntent) -> None:
|
||||
if isinstance(intent, AppIntentExec):
|
||||
_babase.empty_app_mode_handle_intent_exec(intent.code)
|
||||
_babase.empty_app_mode_handle_app_intent_exec(intent.code)
|
||||
return
|
||||
assert isinstance(intent, AppIntentDefault)
|
||||
_babase.empty_app_mode_handle_intent_default()
|
||||
_babase.empty_app_mode_handle_app_intent_default()
|
||||
|
||||
@override
|
||||
def on_activate(self) -> None:
|
||||
# Let the native layer do its thing.
|
||||
_babase.on_empty_app_mode_activate()
|
||||
_babase.empty_app_mode_activate()
|
||||
|
||||
@override
|
||||
def on_deactivate(self) -> None:
|
||||
# Let the native layer do its thing.
|
||||
_babase.on_empty_app_mode_deactivate()
|
||||
_babase.empty_app_mode_deactivate()
|
||||
|
|
|
|||
9
dist/ba_data/python/babase/_env.py
vendored
9
dist/ba_data/python/babase/_env.py
vendored
|
|
@ -9,11 +9,11 @@ import logging
|
|||
import warnings
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from efro.log import LogLevel
|
||||
from efro.logging import LogLevel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from efro.log import LogEntry, LogHandler
|
||||
from efro.logging import LogEntry, LogHandler
|
||||
|
||||
_g_babase_imported = False # pylint: disable=invalid-name
|
||||
_g_babase_app_started = False # pylint: disable=invalid-name
|
||||
|
|
@ -186,7 +186,10 @@ def _feed_logs_to_babase(log_handler: LogHandler) -> None:
|
|||
# Forward this along to the engine to display in the in-app
|
||||
# console, in the Android log, etc.
|
||||
_babase.emit_log(
|
||||
name=entry.name, level=entry.level.name, message=entry.message
|
||||
name=entry.name,
|
||||
level=entry.level.name,
|
||||
timestamp=entry.time.timestamp(),
|
||||
message=entry.message,
|
||||
)
|
||||
|
||||
# We also want to feed some logs to the old v1-cloud-log system.
|
||||
|
|
|
|||
22
dist/ba_data/python/babase/_general.py
vendored
22
dist/ba_data/python/babase/_general.py
vendored
|
|
@ -63,7 +63,7 @@ def existing(obj: ExistableT | None) -> ExistableT | None:
|
|||
For more info, see notes on 'existables' here:
|
||||
https://ballistica.net/wiki/Coding-Style-Guide
|
||||
"""
|
||||
assert obj is None or hasattr(obj, 'exists'), f'No "exists" on {obj}'
|
||||
assert obj is None or hasattr(obj, 'exists'), f'No "exists" attr on {obj}.'
|
||||
return obj if obj is not None and obj.exists() else None
|
||||
|
||||
|
||||
|
|
@ -156,6 +156,9 @@ class _WeakCall:
|
|||
to wrap them in weakrefs manually if desired.
|
||||
"""
|
||||
|
||||
# Optimize performance a bit; we shouldn't need to be super dynamic.
|
||||
__slots__ = ['_call', '_args', '_keywds']
|
||||
|
||||
_did_invalid_call_warning = False
|
||||
|
||||
def __init__(self, *args: Any, **keywds: Any) -> None:
|
||||
|
|
@ -173,9 +176,10 @@ class _WeakCall:
|
|||
'Warning: callable passed to babase.WeakCall() is not'
|
||||
' weak-referencable (%s); use functools.partial instead'
|
||||
' to avoid this warning.',
|
||||
args[0],
|
||||
stack_info=True,
|
||||
)
|
||||
self._did_invalid_call_warning = True
|
||||
type(self)._did_invalid_call_warning = True
|
||||
self._call = args[0]
|
||||
self._args = args[1:]
|
||||
self._keywds = keywds
|
||||
|
|
@ -214,6 +218,9 @@ class _Call:
|
|||
without keeping its object alive.
|
||||
"""
|
||||
|
||||
# Optimize performance a bit; we shouldn't need to be super dynamic.
|
||||
__slots__ = ['_call', '_args', '_keywds']
|
||||
|
||||
def __init__(self, *args: Any, **keywds: Any):
|
||||
"""Instantiate a Call.
|
||||
|
||||
|
|
@ -252,6 +259,14 @@ if TYPE_CHECKING:
|
|||
# type checking on both positional and keyword arguments (as of mypy
|
||||
# 1.11).
|
||||
|
||||
# FIXME: Actually, currently (as of Dec 2024) mypy doesn't fully
|
||||
# type check partial. The partial() call itself is checked, but the
|
||||
# resulting callable seems to be essentially untyped. We should
|
||||
# probably revise this stuff so that Call and WeakCall are for 100%
|
||||
# complete calls so we can fully type check them using ParamSpecs or
|
||||
# whatnot. We could then write a weak_partial() call if we actually
|
||||
# need that particular combination of functionality.
|
||||
|
||||
# Note: Something here is wonky with pylint, possibly related to our
|
||||
# custom pylint plugin. Disabling all checks seems to fix it.
|
||||
# pylint: disable=all
|
||||
|
|
@ -272,6 +287,9 @@ class WeakMethod:
|
|||
free to die. If called with a dead target, is simply a no-op.
|
||||
"""
|
||||
|
||||
# Optimize performance a bit; we shouldn't need to be super dynamic.
|
||||
__slots__ = ['_func', '_obj']
|
||||
|
||||
def __init__(self, call: types.MethodType):
|
||||
assert isinstance(call, types.MethodType)
|
||||
self._func = call.__func__
|
||||
|
|
|
|||
37
dist/ba_data/python/babase/_hooks.py
vendored
37
dist/ba_data/python/babase/_hooks.py
vendored
|
|
@ -430,3 +430,40 @@ def unsupported_controller_message(name: str) -> None:
|
|||
Lstr(resource='unsupportedControllerText', subs=[('${NAME}', name)]),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def copy_dev_console_history() -> None:
|
||||
"""Copy log history from the dev console."""
|
||||
import baenv
|
||||
from babase._language import Lstr
|
||||
|
||||
if not _babase.clipboard_is_supported():
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
'Clipboard not supported on this build.',
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
return
|
||||
|
||||
# This requires us to be running with a log-handler set up.
|
||||
envconfig = baenv.get_config()
|
||||
if envconfig.log_handler is None:
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
'Not available; standard engine logging is not enabled.',
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
return
|
||||
|
||||
# Just dump everything that's in the log-handler's cache.
|
||||
archive = envconfig.log_handler.get_cached()
|
||||
lines: list[str] = []
|
||||
stdnames = ('stdout', 'stderr')
|
||||
for entry in archive.entries:
|
||||
reltime = entry.time.timestamp() - envconfig.launch_time
|
||||
level_ex = '' if entry.name in stdnames else f' {entry.level.name}'
|
||||
lines.append(f'{reltime:.3f}{level_ex} {entry.name}: {entry.message}')
|
||||
|
||||
_babase.clipboard_set_text('\n'.join(lines))
|
||||
_babase.screenmessage(Lstr(resource='copyConfirmText'), color=(0, 1, 0))
|
||||
_babase.getsimplesound('gunCocking').play()
|
||||
|
|
|
|||
33
dist/ba_data/python/babase/_keyboard.py
vendored
33
dist/ba_data/python/babase/_keyboard.py
vendored
|
|
@ -1,33 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""On-screen Keyboard related functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class Keyboard:
|
||||
"""Chars definitions for on-screen keyboard.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Keyboards are discoverable by the meta-tag system
|
||||
and the user can select which one they want to use.
|
||||
On-screen keyboard uses chars from active babase.Keyboard.
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""Displays when user selecting this keyboard."""
|
||||
|
||||
chars: list[tuple[str, ...]]
|
||||
"""Used for row/column lengths."""
|
||||
|
||||
pages: dict[str, tuple[str, ...]]
|
||||
"""Extra chars like emojis."""
|
||||
|
||||
nums: tuple[str, ...]
|
||||
"""The 'num' page."""
|
||||
50
dist/ba_data/python/babase/_language.py
vendored
50
dist/ba_data/python/babase/_language.py
vendored
|
|
@ -44,8 +44,13 @@ class LanguageSubsystem(AppSubsystem):
|
|||
(which may differ from locale if the user sets a language, etc.)
|
||||
"""
|
||||
env = _babase.env()
|
||||
assert isinstance(env['locale'], str)
|
||||
return env['locale']
|
||||
locale = env.get('locale')
|
||||
if not isinstance(locale, str):
|
||||
logging.warning(
|
||||
'Seem to be running in a dummy env; returning en_US locale.'
|
||||
)
|
||||
locale = 'en_US'
|
||||
return locale
|
||||
|
||||
@property
|
||||
def language(self) -> str:
|
||||
|
|
@ -83,6 +88,8 @@ class LanguageSubsystem(AppSubsystem):
|
|||
for i, name in enumerate(names):
|
||||
if name == 'Chinesetraditional':
|
||||
names[i] = 'ChineseTraditional'
|
||||
elif name == 'Piratespeak':
|
||||
names[i] = 'PirateSpeak'
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
|
|
@ -431,7 +438,7 @@ class LanguageSubsystem(AppSubsystem):
|
|||
'Thai',
|
||||
'Tamil',
|
||||
}
|
||||
and not _babase.can_display_full_unicode()
|
||||
and not _babase.supports_unicode_display()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
|
@ -522,8 +529,10 @@ class Lstr:
|
|||
... subs=[('${NAME}', babase.Lstr(resource='res_b'))])
|
||||
"""
|
||||
|
||||
# pylint: disable=dangerous-default-value
|
||||
# noinspection PyDefaultArgument
|
||||
# This class is used a lot in UI stuff and doesn't need to be
|
||||
# flexible, so let's optimize its performance a bit.
|
||||
__slots__ = ['args']
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -531,29 +540,28 @@ class Lstr:
|
|||
resource: str,
|
||||
fallback_resource: str = '',
|
||||
fallback_value: str = '',
|
||||
subs: Sequence[tuple[str, str | Lstr]] = [],
|
||||
subs: Sequence[tuple[str, str | Lstr]] | None = None,
|
||||
) -> None:
|
||||
"""Create an Lstr from a string resource."""
|
||||
|
||||
# noinspection PyShadowingNames,PyDefaultArgument
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
translate: tuple[str, str],
|
||||
subs: Sequence[tuple[str, str | Lstr]] = [],
|
||||
subs: Sequence[tuple[str, str | Lstr]] | None = None,
|
||||
) -> None:
|
||||
"""Create an Lstr by translating a string in a category."""
|
||||
|
||||
# noinspection PyDefaultArgument
|
||||
@overload
|
||||
def __init__(
|
||||
self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = []
|
||||
self,
|
||||
*,
|
||||
value: str,
|
||||
subs: Sequence[tuple[str, str | Lstr]] | None = None,
|
||||
) -> None:
|
||||
"""Create an Lstr from a raw string value."""
|
||||
|
||||
# pylint: enable=redefined-outer-name, dangerous-default-value
|
||||
|
||||
def __init__(self, *args: Any, **keywds: Any) -> None:
|
||||
"""Instantiate a Lstr.
|
||||
|
||||
|
|
@ -581,14 +589,16 @@ class Lstr:
|
|||
if isinstance(self.args.get('value'), our_type):
|
||||
raise TypeError("'value' must be a regular string; not an Lstr")
|
||||
|
||||
if 'subs' in self.args:
|
||||
subs_new = []
|
||||
for key, value in keywds['subs']:
|
||||
if isinstance(value, our_type):
|
||||
subs_new.append((key, value.args))
|
||||
else:
|
||||
subs_new.append((key, value))
|
||||
self.args['subs'] = subs_new
|
||||
if 'subs' in keywds:
|
||||
subs = keywds.get('subs')
|
||||
subs_filtered = []
|
||||
if subs is not None:
|
||||
for key, value in keywds['subs']:
|
||||
if isinstance(value, our_type):
|
||||
subs_filtered.append((key, value.args))
|
||||
else:
|
||||
subs_filtered.append((key, value))
|
||||
self.args['subs'] = subs_filtered
|
||||
|
||||
# As of protocol 31 we support compact key names
|
||||
# ('t' instead of 'translate', etc). Convert as needed.
|
||||
|
|
|
|||
12
dist/ba_data/python/babase/_logging.py
vendored
Normal file
12
dist/ba_data/python/babase/_logging.py
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Logging functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
# Our standard set of loggers.
|
||||
balog = logging.getLogger('ba')
|
||||
applog = logging.getLogger('ba.app')
|
||||
lifecyclelog = logging.getLogger('ba.lifecycle')
|
||||
114
dist/ba_data/python/babase/_login.py
vendored
114
dist/ba_data/python/babase/_login.py
vendored
|
|
@ -17,8 +17,7 @@ import _babase
|
|||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
|
||||
DEBUG_LOG = False
|
||||
logger = logging.getLogger('ba.loginadapter')
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -94,20 +93,17 @@ class LoginAdapter:
|
|||
if state == self._implicit_login_state:
|
||||
return
|
||||
|
||||
if DEBUG_LOG:
|
||||
if state is None:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s implicit state changed;'
|
||||
' now signed out.',
|
||||
self.login_type.name,
|
||||
)
|
||||
else:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s implicit state changed;'
|
||||
' now signed in as %s.',
|
||||
self.login_type.name,
|
||||
state.display_name,
|
||||
)
|
||||
if state is None:
|
||||
logger.debug(
|
||||
'%s implicit state changed; now signed out.',
|
||||
self.login_type.name,
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
'%s implicit state changed; now signed in as %s.',
|
||||
self.login_type.name,
|
||||
state.display_name,
|
||||
)
|
||||
|
||||
self._implicit_login_state = state
|
||||
self._implicit_login_state_dirty = True
|
||||
|
|
@ -128,12 +124,11 @@ class LoginAdapter:
|
|||
only a reference to it is stored, not a copy.
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter got active logins %s.',
|
||||
self.login_type.name,
|
||||
{k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
|
||||
)
|
||||
logger.debug(
|
||||
'%s adapter got active logins %s.',
|
||||
self.login_type.name,
|
||||
{k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
|
||||
)
|
||||
|
||||
self._active_login_id = logins.get(self.login_type)
|
||||
self._update_back_end_active()
|
||||
|
|
@ -197,24 +192,21 @@ class LoginAdapter:
|
|||
self._last_sign_in_desc = description
|
||||
self._last_sign_in_time = now
|
||||
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter sign_in() called;'
|
||||
' fetching sign-in-token...',
|
||||
self.login_type.name,
|
||||
)
|
||||
logger.debug(
|
||||
'%s adapter sign_in() called; fetching sign-in-token...',
|
||||
self.login_type.name,
|
||||
)
|
||||
|
||||
def _got_sign_in_token_result(result: str | None) -> None:
|
||||
import bacommon.cloud
|
||||
|
||||
# Failed to get a sign-in-token.
|
||||
if result is None:
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter sign-in-token fetch failed;'
|
||||
' aborting sign-in.',
|
||||
self.login_type.name,
|
||||
)
|
||||
logger.debug(
|
||||
'%s adapter sign-in-token fetch failed;'
|
||||
' aborting sign-in.',
|
||||
self.login_type.name,
|
||||
)
|
||||
_babase.pushcall(
|
||||
partial(
|
||||
result_cb,
|
||||
|
|
@ -227,25 +219,22 @@ class LoginAdapter:
|
|||
# Got a sign-in token! Now pass it to the cloud which will use
|
||||
# it to verify our identity and give us app credentials on
|
||||
# success.
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter sign-in-token fetch succeeded;'
|
||||
' passing to cloud for verification...',
|
||||
self.login_type.name,
|
||||
)
|
||||
logger.debug(
|
||||
'%s adapter sign-in-token fetch succeeded;'
|
||||
' passing to cloud for verification...',
|
||||
self.login_type.name,
|
||||
)
|
||||
|
||||
def _got_sign_in_response(
|
||||
response: bacommon.cloud.SignInResponse | Exception,
|
||||
) -> None:
|
||||
# This likely means we couldn't communicate with the server.
|
||||
if isinstance(response, Exception):
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter got error'
|
||||
' sign-in response: %s',
|
||||
self.login_type.name,
|
||||
response,
|
||||
)
|
||||
logger.debug(
|
||||
'%s adapter got error sign-in response: %s',
|
||||
self.login_type.name,
|
||||
response,
|
||||
)
|
||||
_babase.pushcall(partial(result_cb, self, response))
|
||||
else:
|
||||
# This means our credentials were explicitly rejected.
|
||||
|
|
@ -254,12 +243,10 @@ class LoginAdapter:
|
|||
RuntimeError('Sign-in-token was rejected.')
|
||||
)
|
||||
else:
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter got successful'
|
||||
' sign-in response',
|
||||
self.login_type.name,
|
||||
)
|
||||
logger.debug(
|
||||
'%s adapter got successful sign-in response',
|
||||
self.login_type.name,
|
||||
)
|
||||
result2 = self.SignInResult(
|
||||
credentials=response.credentials
|
||||
)
|
||||
|
|
@ -305,12 +292,10 @@ class LoginAdapter:
|
|||
# any existing state so it can properly respond to this.
|
||||
if self._implicit_login_state_dirty and self._on_app_loading_called:
|
||||
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter sending'
|
||||
' implicit-state-changed to app.',
|
||||
self.login_type.name,
|
||||
)
|
||||
logger.debug(
|
||||
'%s adapter sending implicit-state-changed to app.',
|
||||
self.login_type.name,
|
||||
)
|
||||
|
||||
assert _babase.app.plus is not None
|
||||
_babase.pushcall(
|
||||
|
|
@ -331,12 +316,11 @@ class LoginAdapter:
|
|||
self._implicit_login_state.login_id == self._active_login_id
|
||||
)
|
||||
if was_active != is_active:
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter back-end-active is now %s.',
|
||||
self.login_type.name,
|
||||
is_active,
|
||||
)
|
||||
logger.debug(
|
||||
'%s adapter back-end-active is now %s.',
|
||||
self.login_type.name,
|
||||
is_active,
|
||||
)
|
||||
self.on_back_end_active_change(is_active)
|
||||
self._back_end_active = is_active
|
||||
|
||||
|
|
|
|||
24
dist/ba_data/python/babase/_meta.py
vendored
24
dist/ba_data/python/babase/_meta.py
vendored
|
|
@ -279,7 +279,7 @@ class DirectoryScan:
|
|||
except Exception:
|
||||
logging.exception("metascan: Error scanning '%s'.", subpath)
|
||||
|
||||
# Sort our results
|
||||
# Sort our results.
|
||||
for exportlist in self.results.exports.values():
|
||||
exportlist.sort()
|
||||
|
||||
|
|
@ -327,7 +327,11 @@ class DirectoryScan:
|
|||
meta_lines = {
|
||||
lnum: l[1:].split()
|
||||
for lnum, l in enumerate(flines)
|
||||
if '# ba_meta ' in l
|
||||
# Do a simple 'in' check for speed but then make sure its
|
||||
# also at the beginning of the line. This allows disabling
|
||||
# meta-lines and avoids false positives from code that
|
||||
# wrangles them.
|
||||
if ('# ba_meta' in l and l.strip().startswith('# ba_meta '))
|
||||
}
|
||||
is_top_level = len(subpath.parts) <= 1
|
||||
required_api = self._get_api_requirement(
|
||||
|
|
@ -384,12 +388,16 @@ class DirectoryScan:
|
|||
# meta_lines is just anything containing '# ba_meta '; make sure
|
||||
# the ba_meta is in the right place.
|
||||
if mline[0] != 'ba_meta':
|
||||
logging.warning(
|
||||
'metascan: %s:%d: malformed ba_meta statement.',
|
||||
subpath,
|
||||
lindex + 1,
|
||||
)
|
||||
self.results.announce_errors_occurred = True
|
||||
# Make an exception for this specific file, otherwise we
|
||||
# get lots of warnings about ba_meta showing up in weird
|
||||
# places here.
|
||||
if subpath.as_posix() != 'babase/_meta.py':
|
||||
logging.warning(
|
||||
'metascan: %s:%d: malformed ba_meta statement.',
|
||||
subpath,
|
||||
lindex + 1,
|
||||
)
|
||||
self.results.announce_errors_occurred = True
|
||||
elif (
|
||||
len(mline) == 4 and mline[1] == 'require' and mline[2] == 'api'
|
||||
):
|
||||
|
|
|
|||
4
dist/ba_data/python/babase/_mgen/enums.py
vendored
4
dist/ba_data/python/babase/_mgen/enums.py
vendored
|
|
@ -80,9 +80,9 @@ class UIScale(Enum):
|
|||
readable from an average distance.
|
||||
"""
|
||||
|
||||
LARGE = 0
|
||||
SMALL = 0
|
||||
MEDIUM = 1
|
||||
SMALL = 2
|
||||
LARGE = 2
|
||||
|
||||
|
||||
class Permission(Enum):
|
||||
|
|
|
|||
2
dist/ba_data/python/babase/_net.py
vendored
2
dist/ba_data/python/babase/_net.py
vendored
|
|
@ -33,7 +33,7 @@ class NetworkSubsystem:
|
|||
# that a nearby server has been pinged.
|
||||
self.zone_pings: dict[str, float] = {}
|
||||
|
||||
# For debugging.
|
||||
# For debugging/progress.
|
||||
self.v1_test_log: str = ''
|
||||
self.v1_ctest_results: dict[int, str] = {}
|
||||
self.connectivity_state = 'uninited'
|
||||
|
|
|
|||
2
dist/ba_data/python/babase/_plugin.py
vendored
2
dist/ba_data/python/babase/_plugin.py
vendored
|
|
@ -93,7 +93,7 @@ class PluginSubsystem(AppSubsystem):
|
|||
# that weren't covered by the meta stuff above, either creating
|
||||
# plugin-specs for them or clearing them out. This covers
|
||||
# plugins with api versions not matching ours, plugins without
|
||||
# ba_meta tags, and plugins that have since disappeared.
|
||||
# ba_*meta tags, and plugins that have since disappeared.
|
||||
assert isinstance(plugstates, dict)
|
||||
wrong_api_prefixes = [f'{m}.' for m in results.incorrect_api_modules]
|
||||
|
||||
|
|
|
|||
49
dist/ba_data/python/baclassic/__init__.py
vendored
49
dist/ba_data/python/baclassic/__init__.py
vendored
|
|
@ -1,38 +1,49 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Classic ballistica components.
|
||||
"""Components for the classic BombSquad experience.
|
||||
|
||||
This package is used as a 'dumping ground' for functionality that is
|
||||
necessary to keep legacy parts of the app working, but which may no
|
||||
longer be the best way to do things going forward.
|
||||
|
||||
New code should try to avoid using code from here when possible.
|
||||
|
||||
Functionality in this package should be exposed through the
|
||||
ClassicSubsystem. This allows type-checked code to go through the
|
||||
babase.app.classic singleton which forces it to explicitly handle the
|
||||
possibility of babase.app.classic being None. When code instead imports
|
||||
classic submodules directly, it is much harder to make it cleanly handle
|
||||
classic not being present.
|
||||
This package/feature-set contains functionality related to the classic
|
||||
BombSquad experience. Note that much legacy BombSquad code is still a
|
||||
bit tangled and thus this feature-set is largely inseperable from
|
||||
scenev1 and uiv1. Future feature-sets will be designed in a more modular
|
||||
way.
|
||||
"""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
|
||||
# Note: Code relying on classic should import things from here *only*
|
||||
# for type-checking and use the versions in app.classic at runtime; that
|
||||
# way type-checking will cleanly cover the classic-not-present case
|
||||
# (app.classic being None).
|
||||
# for type-checking and use the versions in ba*.app.classic at runtime;
|
||||
# that way type-checking will cleanly cover the classic-not-present case
|
||||
# (ba*.app.classic being None).
|
||||
import logging
|
||||
|
||||
from baclassic._subsystem import ClassicSubsystem
|
||||
from efro.util import set_canonical_module_names
|
||||
|
||||
from baclassic._appmode import ClassicAppMode
|
||||
from baclassic._appsubsystem import ClassicAppSubsystem
|
||||
from baclassic._achievement import Achievement, AchievementSubsystem
|
||||
from baclassic._chest import (
|
||||
ChestAppearanceDisplayInfo,
|
||||
CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
|
||||
CHEST_APPEARANCE_DISPLAY_INFOS,
|
||||
)
|
||||
from baclassic._displayitem import show_display_item
|
||||
|
||||
__all__ = [
|
||||
'ClassicSubsystem',
|
||||
'ChestAppearanceDisplayInfo',
|
||||
'CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT',
|
||||
'CHEST_APPEARANCE_DISPLAY_INFOS',
|
||||
'ClassicAppMode',
|
||||
'ClassicAppSubsystem',
|
||||
'Achievement',
|
||||
'AchievementSubsystem',
|
||||
'show_display_item',
|
||||
]
|
||||
|
||||
# We want stuff here to show up as packagename.Foo instead of
|
||||
# packagename._submodule.Foo.
|
||||
set_canonical_module_names(globals())
|
||||
|
||||
# Sanity check: we want to keep ballistica's dependencies and
|
||||
# bootstrapping order clearly defined; let's check a few particular
|
||||
# modules to make sure they never directly or indirectly import us
|
||||
|
|
|
|||
28
dist/ba_data/python/baclassic/_accountv1.py
vendored
28
dist/ba_data/python/baclassic/_accountv1.py
vendored
|
|
@ -125,7 +125,11 @@ class AccountV1Subsystem:
|
|||
if subset is not None:
|
||||
raise ValueError('invalid subset value: ' + str(subset))
|
||||
|
||||
if data['p']:
|
||||
# We used to give this bonus for pro, but on recent versions of
|
||||
# the game give it for everyone (since we are phasing out Pro).
|
||||
|
||||
# if data['p']:
|
||||
if bool(True):
|
||||
if babase.app.plus is None:
|
||||
pro_mult = 1.0
|
||||
else:
|
||||
|
|
@ -176,7 +180,9 @@ class AccountV1Subsystem:
|
|||
else {}
|
||||
)
|
||||
for item_name, item in list(store_items.items()):
|
||||
if item_name.startswith('icons.') and plus.get_purchased(item_name):
|
||||
if item_name.startswith(
|
||||
'icons.'
|
||||
) and plus.get_v1_account_product_purchased(item_name):
|
||||
icons.append(item['icon'])
|
||||
return icons
|
||||
|
||||
|
|
@ -227,13 +233,12 @@ class AccountV1Subsystem:
|
|||
if plus is None:
|
||||
return False
|
||||
|
||||
# Check our tickets-based pro upgrade and our two real-IAP based
|
||||
# upgrades. Also always unlock this stuff in ballistica-core builds.
|
||||
# Check various server-side purchases that mean we have pro.
|
||||
return bool(
|
||||
plus.get_purchased('upgrades.pro')
|
||||
or plus.get_purchased('static.pro')
|
||||
or plus.get_purchased('static.pro_sale')
|
||||
or 'ballistica' + 'kit' == babase.appname()
|
||||
plus.get_v1_account_product_purchased('gold_pass')
|
||||
or plus.get_v1_account_product_purchased('upgrades.pro')
|
||||
or plus.get_v1_account_product_purchased('static.pro')
|
||||
or plus.get_v1_account_product_purchased('static.pro_sale')
|
||||
)
|
||||
|
||||
def have_pro_options(self) -> bool:
|
||||
|
|
@ -247,10 +252,9 @@ class AccountV1Subsystem:
|
|||
if plus is None:
|
||||
return False
|
||||
|
||||
# We expose pro options if the server tells us to
|
||||
# (which is generally just when we own pro),
|
||||
# or also if we've been grandfathered in
|
||||
# or are using ballistica-core builds.
|
||||
# We expose pro options if the server tells us to (which is
|
||||
# generally just when we own pro), or also if we've been
|
||||
# grandfathered in.
|
||||
return self.have_pro() or bool(
|
||||
plus.get_v1_account_misc_read_val_2('proOptionsUnlocked', False)
|
||||
or babase.app.config.get('lc14292', 0) > 1
|
||||
|
|
|
|||
669
dist/ba_data/python/baclassic/_achievement.py
vendored
669
dist/ba_data/python/baclassic/_achievement.py
vendored
|
|
@ -6,6 +6,11 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bacommon.bs import ClassicChestAppearance
|
||||
from baclassic._chest import (
|
||||
CHEST_APPEARANCE_DISPLAY_INFOS,
|
||||
CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
|
||||
)
|
||||
import babase
|
||||
import bascenev1
|
||||
import bauiv1
|
||||
|
|
@ -86,429 +91,311 @@ class AchievementSubsystem:
|
|||
def _init_achievements(self) -> None:
|
||||
"""Fill in available achievements."""
|
||||
|
||||
achs = self.achievements
|
||||
|
||||
# 5
|
||||
achs.append(
|
||||
Achievement('In Control', 'achievementInControl', (1, 1, 1), '', 5)
|
||||
)
|
||||
# 15
|
||||
achs.append(
|
||||
self.achievements += [
|
||||
Achievement(
|
||||
'In Control',
|
||||
'achievementInControl',
|
||||
(1, 1, 1),
|
||||
'',
|
||||
award=5,
|
||||
),
|
||||
Achievement(
|
||||
'Sharing is Caring',
|
||||
'achievementSharingIsCaring',
|
||||
(1, 1, 1),
|
||||
'',
|
||||
15,
|
||||
)
|
||||
)
|
||||
# 10
|
||||
achs.append(
|
||||
award=15,
|
||||
),
|
||||
Achievement(
|
||||
'Dual Wielding', 'achievementDualWielding', (1, 1, 1), '', 10
|
||||
)
|
||||
)
|
||||
|
||||
# 10
|
||||
achs.append(
|
||||
'Dual Wielding',
|
||||
'achievementDualWielding',
|
||||
(1, 1, 1),
|
||||
'',
|
||||
award=10,
|
||||
),
|
||||
Achievement(
|
||||
'Free Loader', 'achievementFreeLoader', (1, 1, 1), '', 10
|
||||
)
|
||||
)
|
||||
# 20
|
||||
achs.append(
|
||||
'Free Loader',
|
||||
'achievementFreeLoader',
|
||||
(1, 1, 1),
|
||||
'',
|
||||
award=10,
|
||||
),
|
||||
Achievement(
|
||||
'Team Player', 'achievementTeamPlayer', (1, 1, 1), '', 20
|
||||
)
|
||||
)
|
||||
|
||||
# 5
|
||||
achs.append(
|
||||
'Team Player',
|
||||
'achievementTeamPlayer',
|
||||
(1, 1, 1),
|
||||
'',
|
||||
award=20,
|
||||
),
|
||||
Achievement(
|
||||
'Onslaught Training Victory',
|
||||
'achievementOnslaught',
|
||||
(1, 1, 1),
|
||||
'Default:Onslaught Training',
|
||||
5,
|
||||
)
|
||||
)
|
||||
# 5
|
||||
achs.append(
|
||||
award=5,
|
||||
),
|
||||
Achievement(
|
||||
'Off You Go Then',
|
||||
'achievementOffYouGo',
|
||||
(1, 1.1, 1.3),
|
||||
'Default:Onslaught Training',
|
||||
5,
|
||||
)
|
||||
)
|
||||
# 10
|
||||
achs.append(
|
||||
award=5,
|
||||
),
|
||||
Achievement(
|
||||
'Boxer',
|
||||
'achievementBoxer',
|
||||
(1, 0.6, 0.6),
|
||||
'Default:Onslaught Training',
|
||||
10,
|
||||
award=10,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# 10
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Rookie Onslaught Victory',
|
||||
'achievementOnslaught',
|
||||
(0.5, 1.4, 0.6),
|
||||
'Default:Rookie Onslaught',
|
||||
10,
|
||||
)
|
||||
)
|
||||
# 10
|
||||
achs.append(
|
||||
award=10,
|
||||
),
|
||||
Achievement(
|
||||
'Mine Games',
|
||||
'achievementMine',
|
||||
(1, 1, 1.4),
|
||||
'Default:Rookie Onslaught',
|
||||
10,
|
||||
)
|
||||
)
|
||||
# 15
|
||||
achs.append(
|
||||
award=10,
|
||||
),
|
||||
Achievement(
|
||||
'Flawless Victory',
|
||||
'achievementFlawlessVictory',
|
||||
(1, 1, 1),
|
||||
'Default:Rookie Onslaught',
|
||||
15,
|
||||
award=15,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# 10
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Rookie Football Victory',
|
||||
'achievementFootballVictory',
|
||||
(1.0, 1, 0.6),
|
||||
'Default:Rookie Football',
|
||||
10,
|
||||
)
|
||||
)
|
||||
# 10
|
||||
achs.append(
|
||||
award=10,
|
||||
),
|
||||
Achievement(
|
||||
'Super Punch',
|
||||
'achievementSuperPunch',
|
||||
(1, 1, 1.8),
|
||||
'Default:Rookie Football',
|
||||
10,
|
||||
)
|
||||
)
|
||||
# 15
|
||||
achs.append(
|
||||
award=10,
|
||||
),
|
||||
Achievement(
|
||||
'Rookie Football Shutout',
|
||||
'achievementFootballShutout',
|
||||
(1, 1, 1),
|
||||
'Default:Rookie Football',
|
||||
15,
|
||||
award=15,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# 15
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Pro Onslaught Victory',
|
||||
'achievementOnslaught',
|
||||
(0.3, 1, 2.0),
|
||||
'Default:Pro Onslaught',
|
||||
15,
|
||||
)
|
||||
)
|
||||
# 15
|
||||
achs.append(
|
||||
award=15,
|
||||
),
|
||||
Achievement(
|
||||
'Boom Goes the Dynamite',
|
||||
'achievementTNT',
|
||||
(1.4, 1.2, 0.8),
|
||||
'Default:Pro Onslaught',
|
||||
15,
|
||||
)
|
||||
)
|
||||
# 20
|
||||
achs.append(
|
||||
award=15,
|
||||
),
|
||||
Achievement(
|
||||
'Pro Boxer',
|
||||
'achievementBoxer',
|
||||
(2, 2, 0),
|
||||
'Default:Pro Onslaught',
|
||||
20,
|
||||
award=20,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# 15
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Pro Football Victory',
|
||||
'achievementFootballVictory',
|
||||
(1.3, 1.3, 2.0),
|
||||
'Default:Pro Football',
|
||||
15,
|
||||
)
|
||||
)
|
||||
# 15
|
||||
achs.append(
|
||||
award=15,
|
||||
),
|
||||
Achievement(
|
||||
'Super Mega Punch',
|
||||
'achievementSuperPunch',
|
||||
(2, 1, 0.6),
|
||||
'Default:Pro Football',
|
||||
15,
|
||||
)
|
||||
)
|
||||
# 20
|
||||
achs.append(
|
||||
award=15,
|
||||
),
|
||||
Achievement(
|
||||
'Pro Football Shutout',
|
||||
'achievementFootballShutout',
|
||||
(0.7, 0.7, 2.0),
|
||||
'Default:Pro Football',
|
||||
20,
|
||||
award=20,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# 15
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Pro Runaround Victory',
|
||||
'achievementRunaround',
|
||||
(1, 1, 1),
|
||||
'Default:Pro Runaround',
|
||||
15,
|
||||
)
|
||||
)
|
||||
# 20
|
||||
achs.append(
|
||||
award=15,
|
||||
),
|
||||
Achievement(
|
||||
'Precision Bombing',
|
||||
'achievementCrossHair',
|
||||
(1, 1, 1.3),
|
||||
'Default:Pro Runaround',
|
||||
20,
|
||||
award=20,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
# 25
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'The Wall',
|
||||
'achievementWall',
|
||||
(1, 0.7, 0.7),
|
||||
'Default:Pro Runaround',
|
||||
25,
|
||||
award=25,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# 30
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Uber Onslaught Victory',
|
||||
'achievementOnslaught',
|
||||
(2, 2, 1),
|
||||
'Default:Uber Onslaught',
|
||||
30,
|
||||
)
|
||||
)
|
||||
# 30
|
||||
achs.append(
|
||||
award=30,
|
||||
),
|
||||
Achievement(
|
||||
'Gold Miner',
|
||||
'achievementMine',
|
||||
(2, 1.6, 0.2),
|
||||
'Default:Uber Onslaught',
|
||||
30,
|
||||
award=30,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
# 30
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'TNT Terror',
|
||||
'achievementTNT',
|
||||
(2, 1.8, 0.3),
|
||||
'Default:Uber Onslaught',
|
||||
30,
|
||||
award=30,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# 30
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Uber Football Victory',
|
||||
'achievementFootballVictory',
|
||||
(1.8, 1.4, 0.3),
|
||||
'Default:Uber Football',
|
||||
30,
|
||||
)
|
||||
)
|
||||
# 30
|
||||
achs.append(
|
||||
award=30,
|
||||
),
|
||||
Achievement(
|
||||
'Got the Moves',
|
||||
'achievementGotTheMoves',
|
||||
(2, 1, 0),
|
||||
'Default:Uber Football',
|
||||
30,
|
||||
award=30,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
# 40
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Uber Football Shutout',
|
||||
'achievementFootballShutout',
|
||||
(2, 2, 0),
|
||||
'Default:Uber Football',
|
||||
40,
|
||||
award=40,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# 30
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Uber Runaround Victory',
|
||||
'achievementRunaround',
|
||||
(1.5, 1.2, 0.2),
|
||||
'Default:Uber Runaround',
|
||||
30,
|
||||
)
|
||||
)
|
||||
# 40
|
||||
achs.append(
|
||||
award=30,
|
||||
),
|
||||
Achievement(
|
||||
'The Great Wall',
|
||||
'achievementWall',
|
||||
(2, 1.7, 0.4),
|
||||
'Default:Uber Runaround',
|
||||
40,
|
||||
award=40,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
# 40
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Stayin\' Alive',
|
||||
'achievementStayinAlive',
|
||||
(2, 2, 1),
|
||||
'Default:Uber Runaround',
|
||||
40,
|
||||
award=40,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# 20
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Last Stand Master',
|
||||
'achievementMedalSmall',
|
||||
(2, 1.5, 0.3),
|
||||
'Default:The Last Stand',
|
||||
20,
|
||||
award=20,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
# 40
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Last Stand Wizard',
|
||||
'achievementMedalMedium',
|
||||
(2, 1.5, 0.3),
|
||||
'Default:The Last Stand',
|
||||
40,
|
||||
award=40,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
# 60
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Last Stand God',
|
||||
'achievementMedalLarge',
|
||||
(2, 1.5, 0.3),
|
||||
'Default:The Last Stand',
|
||||
60,
|
||||
award=60,
|
||||
hard_mode_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# 5
|
||||
achs.append(
|
||||
),
|
||||
Achievement(
|
||||
'Onslaught Master',
|
||||
'achievementMedalSmall',
|
||||
(0.7, 1, 0.7),
|
||||
'Challenges:Infinite Onslaught',
|
||||
5,
|
||||
)
|
||||
)
|
||||
# 15
|
||||
achs.append(
|
||||
award=5,
|
||||
),
|
||||
Achievement(
|
||||
'Onslaught Wizard',
|
||||
'achievementMedalMedium',
|
||||
(0.7, 1.0, 0.7),
|
||||
'Challenges:Infinite Onslaught',
|
||||
15,
|
||||
)
|
||||
)
|
||||
# 30
|
||||
achs.append(
|
||||
award=15,
|
||||
),
|
||||
Achievement(
|
||||
'Onslaught God',
|
||||
'achievementMedalLarge',
|
||||
(0.7, 1.0, 0.7),
|
||||
'Challenges:Infinite Onslaught',
|
||||
30,
|
||||
)
|
||||
)
|
||||
|
||||
# 5
|
||||
achs.append(
|
||||
award=30,
|
||||
),
|
||||
Achievement(
|
||||
'Runaround Master',
|
||||
'achievementMedalSmall',
|
||||
(1.0, 1.0, 1.2),
|
||||
'Challenges:Infinite Runaround',
|
||||
5,
|
||||
)
|
||||
)
|
||||
# 15
|
||||
achs.append(
|
||||
award=5,
|
||||
),
|
||||
Achievement(
|
||||
'Runaround Wizard',
|
||||
'achievementMedalMedium',
|
||||
(1.0, 1.0, 1.2),
|
||||
'Challenges:Infinite Runaround',
|
||||
15,
|
||||
)
|
||||
)
|
||||
# 30
|
||||
achs.append(
|
||||
award=15,
|
||||
),
|
||||
Achievement(
|
||||
'Runaround God',
|
||||
'achievementMedalLarge',
|
||||
(1.0, 1.0, 1.2),
|
||||
'Challenges:Infinite Runaround',
|
||||
30,
|
||||
)
|
||||
)
|
||||
award=30,
|
||||
),
|
||||
]
|
||||
|
||||
def award_local_achievement(self, achname: str) -> None:
|
||||
"""For non-game-based achievements such as controller-connection."""
|
||||
|
|
@ -652,14 +539,16 @@ class Achievement:
|
|||
self,
|
||||
name: str,
|
||||
icon_name: str,
|
||||
icon_color: Sequence[float],
|
||||
icon_color: tuple[float, float, float],
|
||||
level_name: str,
|
||||
*,
|
||||
award: int,
|
||||
hard_mode_only: bool = False,
|
||||
):
|
||||
self._name = name
|
||||
self._icon_name = icon_name
|
||||
self._icon_color: Sequence[float] = list(icon_color) + [1]
|
||||
assert len(icon_color) == 3
|
||||
self._icon_color = icon_color + (1.0,)
|
||||
self._level_name = level_name
|
||||
self._completion_banner_slot: int | None = None
|
||||
self._award = award
|
||||
|
|
@ -842,16 +731,48 @@ class Achievement:
|
|||
],
|
||||
)
|
||||
|
||||
def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
|
||||
"""Get the ticket award value for this achievement."""
|
||||
def get_award_chest_type(self) -> ClassicChestAppearance:
|
||||
"""Return the type of chest given for this achievement."""
|
||||
|
||||
# For now just map our old ticket values to chest types.
|
||||
# Can add distinct values if need be later.
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
return 0
|
||||
val: int = plus.get_v1_account_misc_read_val(
|
||||
'achAward.' + self._name, self._award
|
||||
) * _get_ach_mult(include_pro_bonus)
|
||||
assert isinstance(val, int)
|
||||
return val
|
||||
assert plus is not None
|
||||
t = plus.get_v1_account_misc_read_val(
|
||||
f'achAward.{self.name}', self._award
|
||||
)
|
||||
return (
|
||||
ClassicChestAppearance.L6
|
||||
if t >= 30
|
||||
else (
|
||||
ClassicChestAppearance.L5
|
||||
if t >= 25
|
||||
else (
|
||||
ClassicChestAppearance.L4
|
||||
if t >= 20
|
||||
else (
|
||||
ClassicChestAppearance.L3
|
||||
if t >= 15
|
||||
else (
|
||||
ClassicChestAppearance.L2
|
||||
if t >= 10
|
||||
else ClassicChestAppearance.L1
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
|
||||
# """Get the ticket award value for this achievement."""
|
||||
# plus = babase.app.plus
|
||||
# if plus is None:
|
||||
# return 0
|
||||
# val: int = plus.get_v1_account_misc_read_val(
|
||||
# 'achAward.' + self._name, self._award
|
||||
# ) * _get_ach_mult(include_pro_bonus)
|
||||
# assert isinstance(val, int)
|
||||
# return val
|
||||
|
||||
@property
|
||||
def power_ranking_value(self) -> int:
|
||||
|
|
@ -870,6 +791,7 @@ class Achievement:
|
|||
x: float,
|
||||
y: float,
|
||||
delay: float,
|
||||
*,
|
||||
outdelay: float | None = None,
|
||||
color: Sequence[float] | None = None,
|
||||
style: str = 'post_game',
|
||||
|
|
@ -1015,42 +937,72 @@ class Achievement:
|
|||
txtactor.node.rotate = 10
|
||||
objs.append(txtactor)
|
||||
|
||||
# Ticket-award.
|
||||
# Chest award.
|
||||
award_x = -100
|
||||
objs.append(
|
||||
Text(
|
||||
babase.charstr(babase.SpecialChar.TICKET),
|
||||
host_only=True,
|
||||
position=(x + award_x + 33, y + 7),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
scale=1.5,
|
||||
h_attach=h_attach,
|
||||
v_attach=v_attach,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=(1, 1, 1, 0.2 if hmo else 0.4),
|
||||
transition_delay=delay + 0.05,
|
||||
transition_out_delay=out_delay_fin,
|
||||
).autoretain()
|
||||
chesttype = self.get_award_chest_type()
|
||||
chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
|
||||
chesttype, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
|
||||
)
|
||||
objs.append(
|
||||
Text(
|
||||
'+' + str(self.get_award_ticket_value()),
|
||||
host_only=True,
|
||||
position=(x + award_x + 28, y + 16),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
scale=0.7,
|
||||
flatness=1,
|
||||
h_attach=h_attach,
|
||||
v_attach=v_attach,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=cl2,
|
||||
Image(
|
||||
# Provide magical extended dict version of texture
|
||||
# that Image actor supports.
|
||||
texture={
|
||||
'texture': bascenev1.gettexture(
|
||||
chestdisplayinfo.texclosed
|
||||
),
|
||||
'tint_texture': bascenev1.gettexture(
|
||||
chestdisplayinfo.texclosedtint
|
||||
),
|
||||
'tint_color': chestdisplayinfo.tint,
|
||||
'tint2_color': chestdisplayinfo.tint2,
|
||||
'mask_texture': None,
|
||||
},
|
||||
color=chestdisplayinfo.color + (0.5 if hmo else 1.0,),
|
||||
position=(x + award_x + 37, y + 12),
|
||||
scale=(32.0, 32.0),
|
||||
transition=Image.Transition.FADE_IN,
|
||||
transition_delay=delay + 0.05,
|
||||
transition_out_delay=out_delay_fin,
|
||||
host_only=True,
|
||||
attach=Image.Attach.TOP_LEFT,
|
||||
).autoretain()
|
||||
)
|
||||
|
||||
# objs.append(
|
||||
# Text(
|
||||
# babase.charstr(babase.SpecialChar.TICKET),
|
||||
# host_only=True,
|
||||
# position=(x + award_x + 33, y + 7),
|
||||
# transition=Text.Transition.FADE_IN,
|
||||
# scale=1.5,
|
||||
# h_attach=h_attach,
|
||||
# v_attach=v_attach,
|
||||
# h_align=Text.HAlign.CENTER,
|
||||
# v_align=Text.VAlign.CENTER,
|
||||
# color=(1, 1, 1, 0.2 if hmo else 0.4),
|
||||
# transition_delay=delay + 0.05,
|
||||
# transition_out_delay=out_delay_fin,
|
||||
# ).autoretain()
|
||||
# )
|
||||
# objs.append(
|
||||
# Text(
|
||||
# '+' + str(self.get_award_ticket_value()),
|
||||
# host_only=True,
|
||||
# position=(x + award_x + 28, y + 16),
|
||||
# transition=Text.Transition.FADE_IN,
|
||||
# scale=0.7,
|
||||
# flatness=1,
|
||||
# h_attach=h_attach,
|
||||
# v_attach=v_attach,
|
||||
# h_align=Text.HAlign.CENTER,
|
||||
# v_align=Text.VAlign.CENTER,
|
||||
# color=cl2,
|
||||
# transition_delay=delay + 0.05,
|
||||
# transition_out_delay=out_delay_fin,
|
||||
# ).autoretain()
|
||||
# )
|
||||
|
||||
else:
|
||||
complete = self.complete
|
||||
objs = []
|
||||
|
|
@ -1092,40 +1044,71 @@ class Achievement:
|
|||
else:
|
||||
if not complete:
|
||||
award_x = -100
|
||||
objs.append(
|
||||
Text(
|
||||
babase.charstr(babase.SpecialChar.TICKET),
|
||||
host_only=True,
|
||||
position=(x + award_x + 33, y + 7),
|
||||
transition=Text.Transition.IN_RIGHT,
|
||||
scale=1.5,
|
||||
h_attach=h_attach,
|
||||
v_attach=v_attach,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=(1, 1, 1, (0.1 if hmo else 0.2)),
|
||||
transition_delay=delay + 0.05,
|
||||
transition_out_delay=None,
|
||||
).autoretain()
|
||||
# objs.append(
|
||||
# Text(
|
||||
# babase.charstr(babase.SpecialChar.TICKET),
|
||||
# host_only=True,
|
||||
# position=(x + award_x + 33, y + 7),
|
||||
# transition=Text.Transition.IN_RIGHT,
|
||||
# scale=1.5,
|
||||
# h_attach=h_attach,
|
||||
# v_attach=v_attach,
|
||||
# h_align=Text.HAlign.CENTER,
|
||||
# v_align=Text.VAlign.CENTER,
|
||||
# color=(1, 1, 1, (0.1 if hmo else 0.2)),
|
||||
# transition_delay=delay + 0.05,
|
||||
# transition_out_delay=None,
|
||||
# ).autoretain()
|
||||
# )
|
||||
chesttype = self.get_award_chest_type()
|
||||
chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
|
||||
chesttype, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
|
||||
)
|
||||
objs.append(
|
||||
Text(
|
||||
'+' + str(self.get_award_ticket_value()),
|
||||
host_only=True,
|
||||
position=(x + award_x + 28, y + 16),
|
||||
transition=Text.Transition.IN_RIGHT,
|
||||
scale=0.7,
|
||||
flatness=1,
|
||||
h_attach=h_attach,
|
||||
v_attach=v_attach,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=(0.6, 0.6, 0.6, (0.2 if hmo else 0.4)),
|
||||
Image(
|
||||
# Provide magical extended dict version of texture
|
||||
# that Image actor supports.
|
||||
texture={
|
||||
'texture': bascenev1.gettexture(
|
||||
chestdisplayinfo.texclosed
|
||||
),
|
||||
'tint_texture': bascenev1.gettexture(
|
||||
chestdisplayinfo.texclosedtint
|
||||
),
|
||||
'tint_color': chestdisplayinfo.tint,
|
||||
'tint2_color': chestdisplayinfo.tint2,
|
||||
'mask_texture': None,
|
||||
},
|
||||
color=chestdisplayinfo.color
|
||||
+ (0.5 if hmo else 1.0,),
|
||||
position=(x + award_x + 38, y + 14),
|
||||
scale=(32.0, 32.0),
|
||||
transition=Image.Transition.IN_RIGHT,
|
||||
transition_delay=delay + 0.05,
|
||||
transition_out_delay=None,
|
||||
host_only=True,
|
||||
attach=attach,
|
||||
).autoretain()
|
||||
)
|
||||
|
||||
# objs.append(
|
||||
# Text(
|
||||
# '+' + str(self.get_award_ticket_value()),
|
||||
# host_only=True,
|
||||
# position=(x + award_x + 28, y + 16),
|
||||
# transition=Text.Transition.IN_RIGHT,
|
||||
# scale=0.7,
|
||||
# flatness=1,
|
||||
# h_attach=h_attach,
|
||||
# v_attach=v_attach,
|
||||
# h_align=Text.HAlign.CENTER,
|
||||
# v_align=Text.VAlign.CENTER,
|
||||
# color=(0.6, 0.6, 0.6, (0.2 if hmo else 0.4)),
|
||||
# transition_delay=delay + 0.05,
|
||||
# transition_out_delay=None,
|
||||
# ).autoretain()
|
||||
# )
|
||||
|
||||
# Show 'hard-mode-only' only over incomplete achievements
|
||||
# when that's the case.
|
||||
if hmo:
|
||||
|
|
@ -1457,68 +1440,70 @@ class Achievement:
|
|||
assert objt.node
|
||||
objt.node.host_only = True
|
||||
|
||||
objt = Text(
|
||||
babase.charstr(babase.SpecialChar.TICKET),
|
||||
position=(-120 - 170 + 5, 75 + y_offs - 20),
|
||||
front=True,
|
||||
v_attach=Text.VAttach.BOTTOM,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
transition=Text.Transition.IN_BOTTOM,
|
||||
vr_depth=base_vr_depth,
|
||||
transition_delay=in_time,
|
||||
transition_out_delay=out_time,
|
||||
flash=True,
|
||||
color=(0.5, 0.5, 0.5, 1),
|
||||
scale=3.0,
|
||||
).autoretain()
|
||||
objs.append(objt)
|
||||
assert objt.node
|
||||
objt.node.host_only = True
|
||||
# objt = Text(
|
||||
# babase.charstr(babase.SpecialChar.TICKET),
|
||||
# position=(-120 - 170 + 5, 75 + y_offs - 20),
|
||||
# front=True,
|
||||
# v_attach=Text.VAttach.BOTTOM,
|
||||
# h_align=Text.HAlign.CENTER,
|
||||
# v_align=Text.VAlign.CENTER,
|
||||
# transition=Text.Transition.IN_BOTTOM,
|
||||
# vr_depth=base_vr_depth,
|
||||
# transition_delay=in_time,
|
||||
# transition_out_delay=out_time,
|
||||
# flash=True,
|
||||
# color=(0.5, 0.5, 0.5, 1),
|
||||
# scale=3.0,
|
||||
# ).autoretain()
|
||||
# objs.append(objt)
|
||||
# assert objt.node
|
||||
# objt.node.host_only = True
|
||||
|
||||
# print('FIXME SHOW ACH CHEST3')
|
||||
# objt = Text(
|
||||
# '+' + str(self.get_award_ticket_value()),
|
||||
# position=(-120 - 180 + 5, 80 + y_offs - 20),
|
||||
# v_attach=Text.VAttach.BOTTOM,
|
||||
# front=True,
|
||||
# h_align=Text.HAlign.CENTER,
|
||||
# v_align=Text.VAlign.CENTER,
|
||||
# transition=Text.Transition.IN_BOTTOM,
|
||||
# vr_depth=base_vr_depth,
|
||||
# flatness=0.5,
|
||||
# shadow=1.0,
|
||||
# transition_delay=in_time,
|
||||
# transition_out_delay=out_time,
|
||||
# flash=True,
|
||||
# color=(0, 1, 0, 1),
|
||||
# scale=1.5,
|
||||
# ).autoretain()
|
||||
# objs.append(objt)
|
||||
|
||||
objt = Text(
|
||||
'+' + str(self.get_award_ticket_value()),
|
||||
position=(-120 - 180 + 5, 80 + y_offs - 20),
|
||||
v_attach=Text.VAttach.BOTTOM,
|
||||
front=True,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
transition=Text.Transition.IN_BOTTOM,
|
||||
vr_depth=base_vr_depth,
|
||||
flatness=0.5,
|
||||
shadow=1.0,
|
||||
transition_delay=in_time,
|
||||
transition_out_delay=out_time,
|
||||
flash=True,
|
||||
color=(0, 1, 0, 1),
|
||||
scale=1.5,
|
||||
).autoretain()
|
||||
objs.append(objt)
|
||||
assert objt.node
|
||||
objt.node.host_only = True
|
||||
|
||||
# Add the 'x 2' if we've got pro.
|
||||
if app.classic.accounts.have_pro():
|
||||
objt = Text(
|
||||
'x 2',
|
||||
position=(-120 - 180 + 45, 80 + y_offs - 50),
|
||||
v_attach=Text.VAttach.BOTTOM,
|
||||
front=True,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
transition=Text.Transition.IN_BOTTOM,
|
||||
vr_depth=base_vr_depth,
|
||||
flatness=0.5,
|
||||
shadow=1.0,
|
||||
transition_delay=in_time,
|
||||
transition_out_delay=out_time,
|
||||
flash=True,
|
||||
color=(0.4, 0, 1, 1),
|
||||
scale=0.9,
|
||||
).autoretain()
|
||||
objs.append(objt)
|
||||
assert objt.node
|
||||
objt.node.host_only = True
|
||||
# if app.classic.accounts.have_pro():
|
||||
# objt = Text(
|
||||
# 'x 2',
|
||||
# position=(-120 - 180 + 45, 80 + y_offs - 50),
|
||||
# v_attach=Text.VAttach.BOTTOM,
|
||||
# front=True,
|
||||
# h_align=Text.HAlign.CENTER,
|
||||
# v_align=Text.VAlign.CENTER,
|
||||
# transition=Text.Transition.IN_BOTTOM,
|
||||
# vr_depth=base_vr_depth,
|
||||
# flatness=0.5,
|
||||
# shadow=1.0,
|
||||
# transition_delay=in_time,
|
||||
# transition_out_delay=out_time,
|
||||
# flash=True,
|
||||
# color=(0.4, 0, 1, 1),
|
||||
# scale=0.9,
|
||||
# ).autoretain()
|
||||
# objs.append(objt)
|
||||
# assert objt.node
|
||||
# objt.node.host_only = True
|
||||
|
||||
objt = Text(
|
||||
self.description_complete,
|
||||
|
|
|
|||
24
dist/ba_data/python/baclassic/_ads.py
vendored
24
dist/ba_data/python/baclassic/_ads.py
vendored
|
|
@ -48,19 +48,7 @@ class AdsSubsystem:
|
|||
1.0,
|
||||
lambda: babase.screenmessage(
|
||||
babase.Lstr(
|
||||
resource='removeInGameAdsText',
|
||||
subs=[
|
||||
(
|
||||
'${PRO}',
|
||||
babase.Lstr(
|
||||
resource='store.bombSquadProNameText'
|
||||
),
|
||||
),
|
||||
(
|
||||
'${APP_NAME}',
|
||||
babase.Lstr(resource='titleText'),
|
||||
),
|
||||
],
|
||||
resource='removeInGameAdsTokenPurchaseText'
|
||||
),
|
||||
color=(1, 1, 0),
|
||||
),
|
||||
|
|
@ -100,8 +88,14 @@ class AdsSubsystem:
|
|||
# No ads without net-connections, etc.
|
||||
if not plus.can_show_ad():
|
||||
show = False
|
||||
if classic.accounts.have_pro():
|
||||
show = False # Pro disables interstitials.
|
||||
|
||||
# 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
|
||||
|
|
|
|||
46
dist/ba_data/python/baclassic/_appdelegate.py
vendored
46
dist/ba_data/python/baclassic/_appdelegate.py
vendored
|
|
@ -1,46 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Defines AppDelegate class for handling high level app functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
import bascenev1
|
||||
|
||||
|
||||
class AppDelegate:
|
||||
"""Defines handlers for high level app functionality.
|
||||
|
||||
Category: App Classes
|
||||
"""
|
||||
|
||||
def create_default_game_settings_ui(
|
||||
self,
|
||||
gameclass: type[bascenev1.GameActivity],
|
||||
sessiontype: type[bascenev1.Session],
|
||||
settings: dict | None,
|
||||
completion_call: Callable[[dict | None], None],
|
||||
) -> None:
|
||||
"""Launch a UI to configure the given game config.
|
||||
|
||||
It should manipulate the contents of config and call completion_call
|
||||
when done.
|
||||
"""
|
||||
# Replace the main window once we come up successfully.
|
||||
from bauiv1lib.playlist.editgame import PlaylistEditGameWindow
|
||||
|
||||
assert babase.app.classic is not None
|
||||
babase.app.ui_v1.clear_main_menu_window(transition='out_left')
|
||||
babase.app.ui_v1.set_main_menu_window(
|
||||
PlaylistEditGameWindow(
|
||||
gameclass,
|
||||
sessiontype,
|
||||
settings,
|
||||
completion_call=completion_call,
|
||||
).get_root_widget(),
|
||||
from_window=False, # Disable check since we don't know.
|
||||
)
|
||||
658
dist/ba_data/python/baclassic/_appmode.py
vendored
Normal file
658
dist/ba_data/python/baclassic/_appmode.py
vendored
Normal file
|
|
@ -0,0 +1,658 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Contains ClassicAppMode."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from bacommon.app import AppExperience
|
||||
import babase
|
||||
import bauiv1
|
||||
from bauiv1lib.connectivity import wait_for_connectivity
|
||||
from bauiv1lib.account.signin import show_sign_in_prompt
|
||||
|
||||
import _baclassic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any, Literal
|
||||
|
||||
from efro.call import CallbackRegistration
|
||||
import bacommon.cloud
|
||||
from bauiv1lib.chest import ChestWindow
|
||||
|
||||
|
||||
# ba_meta export babase.AppMode
|
||||
class ClassicAppMode(babase.AppMode):
|
||||
"""AppMode for the classic BombSquad experience."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._on_primary_account_changed_callback: (
|
||||
CallbackRegistration | None
|
||||
) = None
|
||||
self._on_connectivity_changed_callback: CallbackRegistration | None = (
|
||||
None
|
||||
)
|
||||
self._test_sub: babase.CloudSubscription | None = None
|
||||
self._account_data_sub: babase.CloudSubscription | None = None
|
||||
|
||||
self._have_account_values = False
|
||||
self._have_connectivity = False
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_app_experience(cls) -> AppExperience:
|
||||
return AppExperience.MELEE
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def _can_handle_intent(cls, intent: babase.AppIntent) -> bool:
|
||||
# We support default and exec intents currently.
|
||||
return isinstance(
|
||||
intent, babase.AppIntentExec | babase.AppIntentDefault
|
||||
)
|
||||
|
||||
@override
|
||||
def handle_intent(self, intent: babase.AppIntent) -> None:
|
||||
if isinstance(intent, babase.AppIntentExec):
|
||||
_baclassic.classic_app_mode_handle_app_intent_exec(intent.code)
|
||||
return
|
||||
assert isinstance(intent, babase.AppIntentDefault)
|
||||
_baclassic.classic_app_mode_handle_app_intent_default()
|
||||
|
||||
@override
|
||||
def on_activate(self) -> None:
|
||||
|
||||
# Let the native layer do its thing.
|
||||
_baclassic.classic_app_mode_activate()
|
||||
|
||||
app = babase.app
|
||||
plus = app.plus
|
||||
assert plus is not None
|
||||
|
||||
# Wire up the root ui to do what we want.
|
||||
ui = app.ui_v1
|
||||
ui.root_ui_calls[ui.RootUIElement.ACCOUNT_BUTTON] = (
|
||||
self._root_ui_account_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.MENU_BUTTON] = (
|
||||
self._root_ui_menu_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.SQUAD_BUTTON] = (
|
||||
self._root_ui_squad_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.SETTINGS_BUTTON] = (
|
||||
self._root_ui_settings_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.STORE_BUTTON] = (
|
||||
self._root_ui_store_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.INVENTORY_BUTTON] = (
|
||||
self._root_ui_inventory_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.GET_TOKENS_BUTTON] = (
|
||||
self._root_ui_get_tokens_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.INBOX_BUTTON] = (
|
||||
self._root_ui_inbox_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.TICKETS_METER] = (
|
||||
self._root_ui_tickets_meter_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.TOKENS_METER] = (
|
||||
self._root_ui_tokens_meter_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.TROPHY_METER] = (
|
||||
self._root_ui_trophy_meter_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.LEVEL_METER] = (
|
||||
self._root_ui_level_meter_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.ACHIEVEMENTS_BUTTON] = (
|
||||
self._root_ui_achievements_press
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_0] = partial(
|
||||
self._root_ui_chest_slot_pressed, 0
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_1] = partial(
|
||||
self._root_ui_chest_slot_pressed, 1
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_2] = partial(
|
||||
self._root_ui_chest_slot_pressed, 2
|
||||
)
|
||||
ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_3] = partial(
|
||||
self._root_ui_chest_slot_pressed, 3
|
||||
)
|
||||
|
||||
# We want to be informed when connectivity changes.
|
||||
self._on_connectivity_changed_callback = (
|
||||
plus.cloud.on_connectivity_changed_callbacks.register(
|
||||
self._update_for_connectivity_change
|
||||
)
|
||||
)
|
||||
# We want to be informed when primary account changes.
|
||||
self._on_primary_account_changed_callback = (
|
||||
plus.accounts.on_primary_account_changed_callbacks.register(
|
||||
self._update_for_primary_account
|
||||
)
|
||||
)
|
||||
# Establish subscriptions/etc. for any current primary account.
|
||||
self._update_for_primary_account(plus.accounts.primary)
|
||||
self._have_connectivity = plus.cloud.is_connected()
|
||||
self._update_for_connectivity_change(self._have_connectivity)
|
||||
|
||||
@override
|
||||
def on_deactivate(self) -> None:
|
||||
|
||||
classic = babase.app.classic
|
||||
|
||||
# Stop being informed of account changes.
|
||||
self._on_primary_account_changed_callback = None
|
||||
|
||||
# Remove anything following any current account.
|
||||
self._update_for_primary_account(None)
|
||||
|
||||
# Save where we were in the UI so we return there next time.
|
||||
if classic is not None:
|
||||
classic.save_ui_state()
|
||||
|
||||
# Let the native layer do its thing.
|
||||
_baclassic.classic_app_mode_deactivate()
|
||||
|
||||
@override
|
||||
def on_app_active_changed(self) -> None:
|
||||
# If we've gone inactive, bring up the main menu, which has the
|
||||
# side effect of pausing the action (when possible).
|
||||
if not babase.app.active:
|
||||
babase.invoke_main_menu()
|
||||
|
||||
def _update_for_primary_account(
|
||||
self, account: babase.AccountV2Handle | None
|
||||
) -> None:
|
||||
"""Update subscriptions/etc. for a new primary account state."""
|
||||
assert babase.in_logic_thread()
|
||||
plus = babase.app.plus
|
||||
|
||||
assert plus is not None
|
||||
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
|
||||
if account is not None:
|
||||
babase.set_ui_account_state(True, account.tag)
|
||||
else:
|
||||
babase.set_ui_account_state(False)
|
||||
|
||||
# For testing subscription functionality.
|
||||
if os.environ.get('BA_SUBSCRIPTION_TEST') == '1':
|
||||
if account is None:
|
||||
self._test_sub = None
|
||||
else:
|
||||
with account:
|
||||
self._test_sub = plus.cloud.subscribe_test(
|
||||
self._on_sub_test_update
|
||||
)
|
||||
else:
|
||||
self._test_sub = None
|
||||
|
||||
if account is None:
|
||||
classic.gold_pass = False
|
||||
classic.chest_dock_full = False
|
||||
classic.remove_ads = False
|
||||
self._account_data_sub = None
|
||||
_baclassic.set_root_ui_account_values(
|
||||
tickets=-1,
|
||||
tokens=-1,
|
||||
league_rank=-1,
|
||||
league_type='',
|
||||
achievements_percent_text='',
|
||||
level_text='',
|
||||
xp_text='',
|
||||
inbox_count_text='',
|
||||
gold_pass=False,
|
||||
chest_0_appearance='',
|
||||
chest_1_appearance='',
|
||||
chest_2_appearance='',
|
||||
chest_3_appearance='',
|
||||
chest_0_unlock_time=-1.0,
|
||||
chest_1_unlock_time=-1.0,
|
||||
chest_2_unlock_time=-1.0,
|
||||
chest_3_unlock_time=-1.0,
|
||||
chest_0_ad_allow_time=-1.0,
|
||||
chest_1_ad_allow_time=-1.0,
|
||||
chest_2_ad_allow_time=-1.0,
|
||||
chest_3_ad_allow_time=-1.0,
|
||||
)
|
||||
self._have_account_values = False
|
||||
self._update_ui_live_state()
|
||||
|
||||
else:
|
||||
with account:
|
||||
self._account_data_sub = (
|
||||
plus.cloud.subscribe_classic_account_data(
|
||||
self._on_classic_account_data_change
|
||||
)
|
||||
)
|
||||
|
||||
def _update_for_connectivity_change(self, connected: bool) -> None:
|
||||
"""Update when the app's connectivity state changes."""
|
||||
self._have_connectivity = connected
|
||||
self._update_ui_live_state()
|
||||
|
||||
def _update_ui_live_state(self) -> None:
|
||||
# We want to show ui elements faded if we don't have a live
|
||||
# connection to the master-server OR if we haven't received a
|
||||
# set of account values from them yet. If we just plug in raw
|
||||
# connectivity state here we get UI stuff un-fading a moment or
|
||||
# two before values appear (since the subscriptions have not
|
||||
# sent us any values yet) which looks odd.
|
||||
_baclassic.set_root_ui_have_live_values(
|
||||
self._have_connectivity and self._have_account_values
|
||||
)
|
||||
|
||||
def _on_sub_test_update(self, val: int | None) -> None:
|
||||
print(f'GOT SUB TEST UPDATE: {val}')
|
||||
|
||||
def _on_classic_account_data_change(
|
||||
self, val: bacommon.bs.ClassicAccountLiveData
|
||||
) -> None:
|
||||
# print('ACCOUNT CHANGED:', val)
|
||||
achp = round(val.achievements / max(val.achievements_total, 1) * 100.0)
|
||||
ibc = str(val.inbox_count)
|
||||
if val.inbox_count_is_max:
|
||||
ibc += '+'
|
||||
|
||||
chest0 = val.chests.get('0')
|
||||
chest1 = val.chests.get('1')
|
||||
chest2 = val.chests.get('2')
|
||||
chest3 = val.chests.get('3')
|
||||
|
||||
# Keep a few handy values on classic updated with the latest
|
||||
# data.
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
classic.remove_ads = val.remove_ads
|
||||
classic.gold_pass = val.gold_pass
|
||||
classic.chest_dock_full = (
|
||||
chest0 is not None
|
||||
and chest1 is not None
|
||||
and chest2 is not None
|
||||
and chest3 is not None
|
||||
)
|
||||
|
||||
_baclassic.set_root_ui_account_values(
|
||||
tickets=val.tickets,
|
||||
tokens=val.tokens,
|
||||
league_rank=(-1 if val.league_rank is None else val.league_rank),
|
||||
league_type=(
|
||||
'' if val.league_type is None else val.league_type.value
|
||||
),
|
||||
achievements_percent_text=f'{achp}%',
|
||||
level_text=str(val.level),
|
||||
xp_text=f'{val.xp}/{val.xpmax}',
|
||||
inbox_count_text=ibc,
|
||||
gold_pass=val.gold_pass,
|
||||
chest_0_appearance=(
|
||||
'' if chest0 is None else chest0.appearance.value
|
||||
),
|
||||
chest_1_appearance=(
|
||||
'' if chest1 is None else chest1.appearance.value
|
||||
),
|
||||
chest_2_appearance=(
|
||||
'' if chest2 is None else chest2.appearance.value
|
||||
),
|
||||
chest_3_appearance=(
|
||||
'' if chest3 is None else chest3.appearance.value
|
||||
),
|
||||
chest_0_unlock_time=(
|
||||
-1.0 if chest0 is None else chest0.unlock_time.timestamp()
|
||||
),
|
||||
chest_1_unlock_time=(
|
||||
-1.0 if chest1 is None else chest1.unlock_time.timestamp()
|
||||
),
|
||||
chest_2_unlock_time=(
|
||||
-1.0 if chest2 is None else chest2.unlock_time.timestamp()
|
||||
),
|
||||
chest_3_unlock_time=(
|
||||
-1.0 if chest3 is None else chest3.unlock_time.timestamp()
|
||||
),
|
||||
chest_0_ad_allow_time=(
|
||||
-1.0
|
||||
if chest0 is None or chest0.ad_allow_time is None
|
||||
else chest0.ad_allow_time.timestamp()
|
||||
),
|
||||
chest_1_ad_allow_time=(
|
||||
-1.0
|
||||
if chest1 is None or chest1.ad_allow_time is None
|
||||
else chest1.ad_allow_time.timestamp()
|
||||
),
|
||||
chest_2_ad_allow_time=(
|
||||
-1.0
|
||||
if chest2 is None or chest2.ad_allow_time is None
|
||||
else chest2.ad_allow_time.timestamp()
|
||||
),
|
||||
chest_3_ad_allow_time=(
|
||||
-1.0
|
||||
if chest3 is None or chest3.ad_allow_time is None
|
||||
else chest3.ad_allow_time.timestamp()
|
||||
),
|
||||
)
|
||||
|
||||
# Note that we have values and updated faded state accordingly.
|
||||
self._have_account_values = True
|
||||
self._update_ui_live_state()
|
||||
|
||||
def _root_ui_menu_press(self) -> None:
|
||||
from babase import push_back_press
|
||||
|
||||
ui = babase.app.ui_v1
|
||||
|
||||
# If *any* main-window is up, kill it and resume play.
|
||||
old_window = ui.get_main_window()
|
||||
if old_window is not None:
|
||||
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
classic.resume()
|
||||
|
||||
ui.clear_main_window()
|
||||
return
|
||||
|
||||
# Otherwise
|
||||
push_back_press()
|
||||
|
||||
def _root_ui_account_press(self) -> None:
|
||||
from bauiv1lib.account.settings import AccountSettingsWindow
|
||||
|
||||
self._auxiliary_window_nav(
|
||||
win_type=AccountSettingsWindow,
|
||||
win_create_call=lambda: AccountSettingsWindow(
|
||||
origin_widget=bauiv1.get_special_widget('account_button')
|
||||
),
|
||||
)
|
||||
|
||||
def _root_ui_squad_press(self) -> None:
|
||||
btn = bauiv1.get_special_widget('squad_button')
|
||||
center = btn.get_screen_space_center()
|
||||
if bauiv1.app.classic is not None:
|
||||
bauiv1.app.classic.party_icon_activate(center)
|
||||
else:
|
||||
logging.warning('party_icon_activate: no classic.')
|
||||
|
||||
def _root_ui_settings_press(self) -> None:
|
||||
from bauiv1lib.settings.allsettings import AllSettingsWindow
|
||||
|
||||
self._auxiliary_window_nav(
|
||||
win_type=AllSettingsWindow,
|
||||
win_create_call=lambda: AllSettingsWindow(
|
||||
origin_widget=bauiv1.get_special_widget('settings_button')
|
||||
),
|
||||
)
|
||||
|
||||
def _auxiliary_window_nav(
|
||||
self,
|
||||
win_type: type[bauiv1.MainWindow],
|
||||
win_create_call: Callable[[], bauiv1.MainWindow],
|
||||
) -> None:
|
||||
"""Navigate to or away from an Auxiliary window.
|
||||
|
||||
Auxiliary windows can be thought of as 'side quests' in the
|
||||
window hierarchy; places such as settings windows or league
|
||||
ranking windows that the user might want to visit without losing
|
||||
their place in the regular hierarchy.
|
||||
"""
|
||||
# pylint: disable=unidiomatic-typecheck
|
||||
|
||||
ui = babase.app.ui_v1
|
||||
|
||||
current_main_window = ui.get_main_window()
|
||||
|
||||
# Scan our ancestors for auxiliary states matching our type as
|
||||
# well as auxiliary states in general.
|
||||
aux_matching_state: bauiv1.MainWindowState | None = None
|
||||
aux_state: bauiv1.MainWindowState | None = None
|
||||
|
||||
if current_main_window is None:
|
||||
raise RuntimeError(
|
||||
'Not currently handling no-top-level-window case.'
|
||||
)
|
||||
|
||||
state = current_main_window.main_window_back_state
|
||||
while state is not None:
|
||||
assert state.window_type is not None
|
||||
if state.is_auxiliary:
|
||||
if state.window_type is win_type:
|
||||
aux_matching_state = state
|
||||
else:
|
||||
aux_state = state
|
||||
|
||||
state = state.parent
|
||||
|
||||
# If there's an ancestor auxiliary window-state matching our
|
||||
# type, back out past it (example: poking settings, navigating
|
||||
# down a level or two, and then poking settings again should
|
||||
# back out of settings).
|
||||
if aux_matching_state is not None:
|
||||
current_main_window.main_window_back_state = (
|
||||
aux_matching_state.parent
|
||||
)
|
||||
current_main_window.main_window_back()
|
||||
return
|
||||
|
||||
# If there's an ancestory auxiliary state *not* matching our
|
||||
# type, crop the state and swap in our new auxiliary UI
|
||||
# (example: poking settings, then poking account, then poking
|
||||
# back should end up where things were before the settings
|
||||
# poke).
|
||||
if aux_state is not None:
|
||||
# Blow away the window stack and build a fresh one.
|
||||
ui.clear_main_window()
|
||||
ui.set_main_window(
|
||||
win_create_call(),
|
||||
from_window=False, # Disable from-check.
|
||||
back_state=aux_state.parent,
|
||||
suppress_warning=True,
|
||||
is_auxiliary=True,
|
||||
)
|
||||
return
|
||||
|
||||
# Ok, no auxiliary states found. Now if current window is
|
||||
# auxiliary and the type matches, simply do a back.
|
||||
if (
|
||||
current_main_window.main_window_is_auxiliary
|
||||
and type(current_main_window) is win_type
|
||||
):
|
||||
current_main_window.main_window_back()
|
||||
return
|
||||
|
||||
# If current window is auxiliary but type doesn't match,
|
||||
# swap it out for our new auxiliary UI.
|
||||
if current_main_window.main_window_is_auxiliary:
|
||||
ui.clear_main_window()
|
||||
ui.set_main_window(
|
||||
win_create_call(),
|
||||
from_window=False, # Disable from-check.
|
||||
back_state=current_main_window.main_window_back_state,
|
||||
suppress_warning=True,
|
||||
is_auxiliary=True,
|
||||
)
|
||||
return
|
||||
|
||||
# Ok, no existing auxiliary stuff was found period. Just
|
||||
# navigate forward to this UI.
|
||||
current_main_window.main_window_replace(
|
||||
win_create_call(), is_auxiliary=True
|
||||
)
|
||||
|
||||
def _root_ui_achievements_press(self) -> None:
|
||||
from bauiv1lib.achievements import AchievementsWindow
|
||||
|
||||
if not self._ensure_signed_in_v1():
|
||||
return
|
||||
|
||||
wait_for_connectivity(
|
||||
on_connected=lambda: self._auxiliary_window_nav(
|
||||
win_type=AchievementsWindow,
|
||||
win_create_call=lambda: AchievementsWindow(
|
||||
origin_widget=bauiv1.get_special_widget(
|
||||
'achievements_button'
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def _root_ui_inbox_press(self) -> None:
|
||||
from bauiv1lib.inbox import InboxWindow
|
||||
|
||||
if not self._ensure_signed_in():
|
||||
return
|
||||
|
||||
wait_for_connectivity(
|
||||
on_connected=lambda: self._auxiliary_window_nav(
|
||||
win_type=InboxWindow,
|
||||
win_create_call=lambda: InboxWindow(
|
||||
origin_widget=bauiv1.get_special_widget('inbox_button')
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def _root_ui_store_press(self) -> None:
|
||||
from bauiv1lib.store.browser import StoreBrowserWindow
|
||||
|
||||
if not self._ensure_signed_in_v1():
|
||||
return
|
||||
|
||||
wait_for_connectivity(
|
||||
on_connected=lambda: self._auxiliary_window_nav(
|
||||
win_type=StoreBrowserWindow,
|
||||
win_create_call=lambda: StoreBrowserWindow(
|
||||
origin_widget=bauiv1.get_special_widget('store_button')
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def _root_ui_tickets_meter_press(self) -> None:
|
||||
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
|
||||
|
||||
ResourceTypeInfoWindow(
|
||||
'tickets', origin_widget=bauiv1.get_special_widget('tickets_meter')
|
||||
)
|
||||
|
||||
def _root_ui_tokens_meter_press(self) -> None:
|
||||
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
|
||||
|
||||
ResourceTypeInfoWindow(
|
||||
'tokens', origin_widget=bauiv1.get_special_widget('tokens_meter')
|
||||
)
|
||||
|
||||
def _root_ui_trophy_meter_press(self) -> None:
|
||||
from bauiv1lib.league.rankwindow import LeagueRankWindow
|
||||
|
||||
if not self._ensure_signed_in_v1():
|
||||
return
|
||||
|
||||
self._auxiliary_window_nav(
|
||||
win_type=LeagueRankWindow,
|
||||
win_create_call=lambda: LeagueRankWindow(
|
||||
origin_widget=bauiv1.get_special_widget('trophy_meter')
|
||||
),
|
||||
)
|
||||
|
||||
def _root_ui_level_meter_press(self) -> None:
|
||||
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
|
||||
|
||||
ResourceTypeInfoWindow(
|
||||
'xp', origin_widget=bauiv1.get_special_widget('level_meter')
|
||||
)
|
||||
|
||||
def _root_ui_inventory_press(self) -> None:
|
||||
from bauiv1lib.inventory import InventoryWindow
|
||||
|
||||
if not self._ensure_signed_in_v1():
|
||||
return
|
||||
|
||||
self._auxiliary_window_nav(
|
||||
win_type=InventoryWindow,
|
||||
win_create_call=lambda: InventoryWindow(
|
||||
origin_widget=bauiv1.get_special_widget('inventory_button')
|
||||
),
|
||||
)
|
||||
|
||||
def _ensure_signed_in(self) -> bool:
|
||||
"""Make sure we're signed in (requiring modern v2 accounts)."""
|
||||
plus = bauiv1.app.plus
|
||||
if plus is None:
|
||||
bauiv1.screenmessage('This requires plus.', color=(1, 0, 0))
|
||||
bauiv1.getsound('error').play()
|
||||
return False
|
||||
if plus.accounts.primary is None:
|
||||
show_sign_in_prompt()
|
||||
return False
|
||||
return True
|
||||
|
||||
def _ensure_signed_in_v1(self) -> bool:
|
||||
"""Make sure we're signed in (allowing legacy v1-only accounts)."""
|
||||
plus = bauiv1.app.plus
|
||||
if plus is None:
|
||||
bauiv1.screenmessage('This requires plus.', color=(1, 0, 0))
|
||||
bauiv1.getsound('error').play()
|
||||
return False
|
||||
if plus.get_v1_account_state() != 'signed_in':
|
||||
show_sign_in_prompt()
|
||||
return False
|
||||
return True
|
||||
|
||||
def _root_ui_get_tokens_press(self) -> None:
|
||||
from bauiv1lib.gettokens import GetTokensWindow
|
||||
|
||||
if not self._ensure_signed_in():
|
||||
return
|
||||
|
||||
self._auxiliary_window_nav(
|
||||
win_type=GetTokensWindow,
|
||||
win_create_call=lambda: GetTokensWindow(
|
||||
origin_widget=bauiv1.get_special_widget('get_tokens_button')
|
||||
),
|
||||
)
|
||||
|
||||
def _root_ui_chest_slot_pressed(self, index: int) -> None:
|
||||
from bauiv1lib.chest import (
|
||||
ChestWindow0,
|
||||
ChestWindow1,
|
||||
ChestWindow2,
|
||||
ChestWindow3,
|
||||
)
|
||||
|
||||
widgetid: Literal[
|
||||
'chest_0_button',
|
||||
'chest_1_button',
|
||||
'chest_2_button',
|
||||
'chest_3_button',
|
||||
]
|
||||
winclass: type[ChestWindow]
|
||||
if index == 0:
|
||||
widgetid = 'chest_0_button'
|
||||
winclass = ChestWindow0
|
||||
elif index == 1:
|
||||
widgetid = 'chest_1_button'
|
||||
winclass = ChestWindow1
|
||||
elif index == 2:
|
||||
widgetid = 'chest_2_button'
|
||||
winclass = ChestWindow2
|
||||
elif index == 3:
|
||||
widgetid = 'chest_3_button'
|
||||
winclass = ChestWindow3
|
||||
else:
|
||||
raise RuntimeError(f'Invalid index {index}')
|
||||
|
||||
wait_for_connectivity(
|
||||
on_connected=lambda: self._auxiliary_window_nav(
|
||||
win_type=winclass,
|
||||
win_create_call=lambda: winclass(
|
||||
index=index,
|
||||
origin_widget=bauiv1.get_special_widget(widgetid),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
@ -3,10 +3,10 @@
|
|||
"""Provides classic app subsystem."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, override
|
||||
import random
|
||||
import logging
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, override, assert_never
|
||||
|
||||
from efro.dataclassio import dataclass_from_dict
|
||||
import babase
|
||||
|
|
@ -26,15 +26,15 @@ from baclassic import _input
|
|||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any, Sequence
|
||||
|
||||
import bacommon.bs
|
||||
from bascenev1lib.actor import spazappearance
|
||||
from bauiv1lib.party import PartyWindow
|
||||
|
||||
from baclassic._appdelegate import AppDelegate
|
||||
from baclassic._servermode import ServerController
|
||||
from baclassic._net import MasterServerCallback
|
||||
|
||||
|
||||
class ClassicSubsystem(babase.AppSubsystem):
|
||||
class ClassicAppSubsystem(babase.AppSubsystem):
|
||||
"""Subsystem for classic functionality in the app.
|
||||
|
||||
The single shared instance of this app can be accessed at
|
||||
|
|
@ -45,7 +45,6 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from baclassic._music import MusicPlayMode
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -72,10 +71,14 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
self.stress_test_update_timer: babase.AppTimer | None = None
|
||||
self.stress_test_update_timer_2: babase.AppTimer | None = None
|
||||
self.value_test_defaults: dict = {}
|
||||
self.special_offer: dict | None = None
|
||||
self.ping_thread_count = 0
|
||||
self.allow_ticket_purchases: bool = True
|
||||
|
||||
# Classic-specific account state.
|
||||
self.remove_ads = False
|
||||
self.gold_pass = False
|
||||
self.chest_dock_full = False
|
||||
|
||||
# Main Menu.
|
||||
self.main_menu_did_initial_transition = False
|
||||
self.main_menu_last_news_fetch_time: float | None = None
|
||||
|
|
@ -93,17 +96,17 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
|
||||
# We include this extra hash with shared input-mapping names so
|
||||
# that we don't share mappings between differently-configured
|
||||
# systems. For instance, different android devices may give different
|
||||
# key values for the same controller type so we keep their mappings
|
||||
# distinct.
|
||||
# systems. For instance, different android devices may give
|
||||
# different key values for the same controller type so we keep
|
||||
# their mappings distinct.
|
||||
self.input_map_hash: str | None = None
|
||||
|
||||
# Maps.
|
||||
self.maps: dict[str, type[bascenev1.Map]] = {}
|
||||
|
||||
# Gameplay.
|
||||
self.teams_series_length = 7 # deprecated, left for old mods
|
||||
self.ffa_series_length = 24 # deprecated, left for old mods
|
||||
self.teams_series_length = 7 # Deprecated, left for old mods.
|
||||
self.ffa_series_length = 24 # Deprecated, left for old mods.
|
||||
self.coop_session_args: dict = {}
|
||||
|
||||
# UI.
|
||||
|
|
@ -111,8 +114,9 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
self.did_menu_intro = False # FIXME: Move to mainmenu class.
|
||||
self.main_menu_window_refresh_check_count = 0 # FIXME: Mv to mainmenu.
|
||||
self.invite_confirm_windows: list[Any] = [] # FIXME: Don't use Any.
|
||||
self.delegate: AppDelegate | None = None
|
||||
self.party_window: weakref.ref[PartyWindow] | None = None
|
||||
self.main_menu_resume_callbacks: list = []
|
||||
self.saved_ui_state: bauiv1.MainWindowState | None = None
|
||||
|
||||
# Store.
|
||||
self.store_layout: dict[str, list[dict[str, Any]]] | None = None
|
||||
|
|
@ -120,6 +124,16 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
self.pro_sale_start_time: int | None = None
|
||||
self.pro_sale_start_val: int | None = None
|
||||
|
||||
def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None:
|
||||
"""(internal)"""
|
||||
|
||||
# If there's no main window up, just call immediately.
|
||||
if not babase.app.ui_v1.has_main_window():
|
||||
with babase.ContextRef.empty():
|
||||
call()
|
||||
else:
|
||||
self.main_menu_resume_callbacks.append(call)
|
||||
|
||||
@property
|
||||
def platform(self) -> str:
|
||||
"""Name of the current platform.
|
||||
|
|
@ -154,8 +168,6 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
from bascenev1lib.actor import spazappearance
|
||||
from bascenev1lib import maps as stdmaps
|
||||
|
||||
from baclassic._appdelegate import AppDelegate
|
||||
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
|
|
@ -164,34 +176,13 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
|
||||
self.music.on_app_loading()
|
||||
|
||||
self.delegate = AppDelegate()
|
||||
|
||||
# Non-test, non-debug builds should generally be blessed; warn if not.
|
||||
# (so I don't accidentally release a build that can't play tourneys)
|
||||
# Non-test, non-debug builds should generally be blessed; warn
|
||||
# if not (so I don't accidentally release a build that can't
|
||||
# play tourneys).
|
||||
if not env.debug and not env.test and not plus.is_blessed():
|
||||
babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
|
||||
|
||||
# FIXME: This should not be hard-coded.
|
||||
for maptype in [
|
||||
stdmaps.HockeyStadium,
|
||||
stdmaps.FootballStadium,
|
||||
stdmaps.Bridgit,
|
||||
stdmaps.BigG,
|
||||
stdmaps.Roundabout,
|
||||
stdmaps.MonkeyFace,
|
||||
stdmaps.ZigZag,
|
||||
stdmaps.ThePad,
|
||||
stdmaps.DoomShroom,
|
||||
stdmaps.LakeFrigid,
|
||||
stdmaps.TipTop,
|
||||
stdmaps.CragCastle,
|
||||
stdmaps.TowerD,
|
||||
stdmaps.HappyThoughts,
|
||||
stdmaps.StepRightUp,
|
||||
stdmaps.Courtyard,
|
||||
stdmaps.Rampage,
|
||||
]:
|
||||
bascenev1.register_map(maptype)
|
||||
stdmaps.register_all_maps()
|
||||
|
||||
spazappearance.register_appearances()
|
||||
bascenev1.init_campaigns()
|
||||
|
|
@ -207,24 +198,6 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
cfg['launchCount'] = launch_count
|
||||
cfg.commit()
|
||||
|
||||
# Run a test in a few seconds to see if we should pop up an existing
|
||||
# pending special offer.
|
||||
def check_special_offer() -> None:
|
||||
assert plus is not None
|
||||
|
||||
from bauiv1lib.specialoffer import show_offer
|
||||
|
||||
if (
|
||||
'pendingSpecialOffer' in cfg
|
||||
and plus.get_v1_account_public_login_id()
|
||||
== cfg['pendingSpecialOffer']['a']
|
||||
):
|
||||
self.special_offer = cfg['pendingSpecialOffer']['o']
|
||||
show_offer()
|
||||
|
||||
if babase.app.env.gui:
|
||||
babase.apptimer(3.0, check_special_offer)
|
||||
|
||||
# If there's a leftover log file, attempt to upload it to the
|
||||
# master-server and/or get rid of it.
|
||||
babase.handle_leftover_v1_cloud_log_file()
|
||||
|
|
@ -261,8 +234,8 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
from babase import Lstr
|
||||
from bascenev1 import NodeActor
|
||||
|
||||
# FIXME: Shouldn't be touching scene stuff here;
|
||||
# should just pass the request on to the host-session.
|
||||
# FIXME: Shouldn't be touching scene stuff here; should just
|
||||
# pass the request on to the host-session.
|
||||
with activity.context:
|
||||
globs = activity.globalsnode
|
||||
if not globs.paused:
|
||||
|
|
@ -289,8 +262,8 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
to resume.
|
||||
"""
|
||||
|
||||
# FIXME: Shouldn't be touching scene stuff here;
|
||||
# should just pass the request on to the host-session.
|
||||
# FIXME: Shouldn't be touching scene stuff here; should just
|
||||
# pass the request on to the host-session.
|
||||
activity = bascenev1.get_foreground_host_activity()
|
||||
if activity is not None:
|
||||
with activity.context:
|
||||
|
|
@ -340,6 +313,9 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
)
|
||||
return False
|
||||
|
||||
# Save where we are in the UI to come back to when done.
|
||||
babase.app.classic.save_ui_state()
|
||||
|
||||
# Ok, we're good to go.
|
||||
self.coop_session_args = {
|
||||
'campaign': campaignname,
|
||||
|
|
@ -374,24 +350,24 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
assert plus is not None
|
||||
|
||||
if reset_ui:
|
||||
babase.app.ui_v1.clear_main_menu_window()
|
||||
babase.app.ui_v1.clear_main_window()
|
||||
|
||||
if isinstance(bascenev1.get_foreground_host_session(), MainMenuSession):
|
||||
# It may be possible we're on the main menu but the screen is faded
|
||||
# so fade back in.
|
||||
# It may be possible we're on the main menu but the screen
|
||||
# is faded so fade back in.
|
||||
babase.fade_screen(True)
|
||||
return
|
||||
|
||||
_benchmark.stop_stress_test() # Stop stress-test if in progress.
|
||||
|
||||
# If we're in a host-session, tell them to end.
|
||||
# This lets them tear themselves down gracefully.
|
||||
# If we're in a host-session, tell them to end. This lets them
|
||||
# tear themselves down gracefully.
|
||||
host_session: bascenev1.Session | None = (
|
||||
bascenev1.get_foreground_host_session()
|
||||
)
|
||||
if host_session is not None:
|
||||
# Kick off a little transaction so we'll hopefully have all the
|
||||
# latest account state when we get back to the menu.
|
||||
# Kick off a little transaction so we'll hopefully have all
|
||||
# the latest account state when we get back to the menu.
|
||||
plus.add_v1_account_transaction(
|
||||
{'type': 'END_SESSION', 'sType': str(type(host_session))}
|
||||
)
|
||||
|
|
@ -518,11 +494,36 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
request, 'post', data, callback, response_type
|
||||
).start()
|
||||
|
||||
def get_tournament_prize_strings(self, entry: dict[str, Any]) -> list[str]:
|
||||
def set_tournament_prize_image(
|
||||
self, entry: dict[str, Any], index: int, image: bauiv1.Widget
|
||||
) -> None:
|
||||
"""Given a tournament entry, return strings for its prize levels."""
|
||||
from baclassic import _tournament
|
||||
|
||||
return _tournament.get_tournament_prize_strings(entry)
|
||||
return _tournament.set_tournament_prize_chest_image(entry, index, image)
|
||||
|
||||
def create_in_game_tournament_prize_image(
|
||||
self,
|
||||
entry: dict[str, Any],
|
||||
index: int,
|
||||
position: tuple[float, float],
|
||||
) -> None:
|
||||
"""Given a tournament entry, return strings for its prize levels."""
|
||||
from baclassic import _tournament
|
||||
|
||||
_tournament.create_in_game_tournament_prize_image(
|
||||
entry, index, position
|
||||
)
|
||||
|
||||
def get_tournament_prize_strings(
|
||||
self, entry: dict[str, Any], include_tickets: bool
|
||||
) -> list[str]:
|
||||
"""Given a tournament entry, return strings for its prize levels."""
|
||||
from baclassic import _tournament
|
||||
|
||||
return _tournament.get_tournament_prize_strings(
|
||||
entry, include_tickets=include_tickets
|
||||
)
|
||||
|
||||
def getcampaign(self, name: str) -> bascenev1.Campaign:
|
||||
"""Return a campaign by name."""
|
||||
|
|
@ -536,26 +537,21 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
tip = self.tips.pop()
|
||||
return tip
|
||||
|
||||
def run_gpu_benchmark(self) -> None:
|
||||
"""Kick off a benchmark to test gpu speeds."""
|
||||
from baclassic._benchmark import run_gpu_benchmark as run
|
||||
|
||||
run()
|
||||
|
||||
def run_cpu_benchmark(self) -> None:
|
||||
"""Kick off a benchmark to test cpu speeds."""
|
||||
from baclassic._benchmark import run_cpu_benchmark as run
|
||||
from baclassic._benchmark import run_cpu_benchmark
|
||||
|
||||
run()
|
||||
run_cpu_benchmark()
|
||||
|
||||
def run_media_reload_benchmark(self) -> None:
|
||||
"""Kick off a benchmark to test media reloading speeds."""
|
||||
from baclassic._benchmark import run_media_reload_benchmark as run
|
||||
from baclassic._benchmark import run_media_reload_benchmark
|
||||
|
||||
run()
|
||||
run_media_reload_benchmark()
|
||||
|
||||
def run_stress_test(
|
||||
self,
|
||||
*,
|
||||
playlist_type: str = 'Random',
|
||||
playlist_name: str = '__default__',
|
||||
player_count: int = 8,
|
||||
|
|
@ -563,9 +559,9 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
attract_mode: bool = False,
|
||||
) -> None:
|
||||
"""Run a stress test."""
|
||||
from baclassic._benchmark import run_stress_test as run
|
||||
from baclassic._benchmark import run_stress_test
|
||||
|
||||
run(
|
||||
run_stress_test(
|
||||
playlist_type=playlist_type,
|
||||
playlist_name=playlist_name,
|
||||
player_count=player_count,
|
||||
|
|
@ -684,14 +680,6 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
babase.Call(ServerDialogWindow, sddata),
|
||||
)
|
||||
|
||||
def ticket_icon_press(self) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
|
||||
|
||||
ResourceTypeInfoWindow(
|
||||
origin_widget=bauiv1.get_special_widget('tickets_info_button')
|
||||
)
|
||||
|
||||
def show_url_window(self, address: str) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.url import ShowURLWindow
|
||||
|
|
@ -707,6 +695,7 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
def tournament_entry_window(
|
||||
self,
|
||||
tournament_id: str,
|
||||
*,
|
||||
tournament_activity: bascenev1.Activity | None = None,
|
||||
position: tuple[float, float] = (0.0, 0.0),
|
||||
delegate: Any = None,
|
||||
|
|
@ -733,30 +722,32 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
|
||||
return MainMenuSession
|
||||
|
||||
def continues_window(
|
||||
self,
|
||||
activity: bascenev1.Activity,
|
||||
cost: int,
|
||||
continue_call: Callable[[], Any],
|
||||
cancel_call: Callable[[], Any],
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.continues import ContinuesWindow
|
||||
|
||||
ContinuesWindow(activity, cost, continue_call, cancel_call)
|
||||
|
||||
def profile_browser_window(
|
||||
self,
|
||||
transition: str = 'in_right',
|
||||
in_main_menu: bool = True,
|
||||
selected_profile: str | None = None,
|
||||
origin_widget: bauiv1.Widget | None = None,
|
||||
selected_profile: str | None = None,
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.profile.browser import ProfileBrowserWindow
|
||||
|
||||
ProfileBrowserWindow(
|
||||
transition, in_main_menu, selected_profile, origin_widget
|
||||
main_window = babase.app.ui_v1.get_main_window()
|
||||
if main_window is not None:
|
||||
logging.warning(
|
||||
'profile_browser_window()'
|
||||
' called with existing main window; should not happen.'
|
||||
)
|
||||
return
|
||||
|
||||
babase.app.ui_v1.set_main_window(
|
||||
ProfileBrowserWindow(
|
||||
transition=transition,
|
||||
selected_profile=selected_profile,
|
||||
origin_widget=origin_widget,
|
||||
minimal_toolbar=True,
|
||||
),
|
||||
is_top_level=True,
|
||||
suppress_warning=True,
|
||||
)
|
||||
|
||||
def preload_map_preview_media(self) -> None:
|
||||
|
|
@ -781,6 +772,9 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
|
||||
assert app.env.gui
|
||||
|
||||
# Play explicit swish sound so it occurs due to keypresses/etc.
|
||||
# This means we have to disable it for any button or else we get
|
||||
# double.
|
||||
bauiv1.getsound('swish').play()
|
||||
|
||||
# If it exists, dismiss it; otherwise make a new one.
|
||||
|
|
@ -794,18 +788,182 @@ class ClassicSubsystem(babase.AppSubsystem):
|
|||
|
||||
def device_menu_press(self, device_id: int | None) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.mainmenu import MainMenuWindow
|
||||
from bauiv1lib.ingamemenu import InGameMenuWindow
|
||||
from bauiv1 import set_ui_input_device
|
||||
|
||||
assert babase.app is not None
|
||||
in_main_menu = babase.app.ui_v1.has_main_menu_window()
|
||||
in_main_menu = babase.app.ui_v1.has_main_window()
|
||||
if not in_main_menu:
|
||||
set_ui_input_device(device_id)
|
||||
|
||||
# Hack(ish). We play swish sound here so it happens for
|
||||
# device presses, but this means we need to disable default
|
||||
# swish sounds for any menu buttons or we'll get double.
|
||||
if babase.app.env.gui:
|
||||
bauiv1.getsound('swish').play()
|
||||
|
||||
babase.app.ui_v1.set_main_menu_window(
|
||||
MainMenuWindow().get_root_widget(),
|
||||
from_window=False, # Disable check here.
|
||||
babase.app.ui_v1.set_main_window(
|
||||
InGameMenuWindow(), is_top_level=True, suppress_warning=True
|
||||
)
|
||||
|
||||
def save_ui_state(self) -> None:
|
||||
"""Store our current place in the UI."""
|
||||
ui = babase.app.ui_v1
|
||||
mainwindow = ui.get_main_window()
|
||||
if mainwindow is not None:
|
||||
self.saved_ui_state = ui.save_main_window_state(mainwindow)
|
||||
else:
|
||||
self.saved_ui_state = None
|
||||
|
||||
def invoke_main_menu_ui(self) -> None:
|
||||
"""Bring up main menu ui."""
|
||||
|
||||
# Bring up the last place we were, or start at the main menu
|
||||
# otherwise.
|
||||
app = bauiv1.app
|
||||
env = app.env
|
||||
with bascenev1.ContextRef.empty():
|
||||
|
||||
assert app.classic is not None
|
||||
if app.env.headless:
|
||||
# UI stuff fails now in headless builds; avoid it.
|
||||
pass
|
||||
else:
|
||||
|
||||
# When coming back from a kiosk-mode game, jump to the
|
||||
# kiosk start screen.
|
||||
if env.demo or env.arcade:
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.kiosk import KioskWindow
|
||||
|
||||
app.ui_v1.set_main_window(
|
||||
KioskWindow(), is_top_level=True, suppress_warning=True
|
||||
)
|
||||
else:
|
||||
# If there's a saved ui state, restore that.
|
||||
if self.saved_ui_state is not None:
|
||||
app.ui_v1.restore_main_window_state(self.saved_ui_state)
|
||||
else:
|
||||
# Otherwise start fresh at the main menu.
|
||||
from bauiv1lib.mainmenu import MainMenuWindow
|
||||
|
||||
app.ui_v1.set_main_window(
|
||||
MainMenuWindow(transition=None),
|
||||
is_top_level=True,
|
||||
suppress_warning=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
|
||||
"""Run client effects sent from the master server."""
|
||||
from baclassic._clienteffect import run_bs_client_effects
|
||||
|
||||
run_bs_client_effects(effects)
|
||||
|
||||
@staticmethod
|
||||
def basic_client_ui_button_label_str(
|
||||
label: bacommon.bs.BasicClientUI.ButtonLabel,
|
||||
) -> babase.Lstr:
|
||||
"""Given a client-ui label, return an Lstr."""
|
||||
import bacommon.bs
|
||||
|
||||
cls = bacommon.bs.BasicClientUI.ButtonLabel
|
||||
if label is cls.UNKNOWN:
|
||||
# Server should not be sending us unknown stuff; make noise
|
||||
# if they do.
|
||||
logging.error(
|
||||
'Got BasicClientUI.ButtonLabel.UNKNOWN; should not happen.'
|
||||
)
|
||||
return babase.Lstr(value='<error>')
|
||||
|
||||
rsrc: str | None = None
|
||||
if label is cls.OK:
|
||||
rsrc = 'okText'
|
||||
elif label is cls.APPLY:
|
||||
rsrc = 'applyText'
|
||||
elif label is cls.CANCEL:
|
||||
rsrc = 'cancelText'
|
||||
elif label is cls.ACCEPT:
|
||||
rsrc = 'gatherWindow.partyInviteAcceptText'
|
||||
elif label is cls.DECLINE:
|
||||
rsrc = 'gatherWindow.partyInviteDeclineText'
|
||||
elif label is cls.IGNORE:
|
||||
rsrc = 'gatherWindow.partyInviteIgnoreText'
|
||||
elif label is cls.CLAIM:
|
||||
rsrc = 'claimText'
|
||||
elif label is cls.DISCARD:
|
||||
rsrc = 'discardText'
|
||||
else:
|
||||
assert_never(label)
|
||||
|
||||
return babase.Lstr(resource=rsrc)
|
||||
|
||||
def required_purchases_for_game(self, game: str) -> list[str]:
|
||||
"""Return which purchase (if any) is required for a game."""
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
if game in (
|
||||
'Challenges:Infinite Runaround',
|
||||
'Challenges:Tournament Infinite Runaround',
|
||||
):
|
||||
# Special case: Pro used to unlock this.
|
||||
return (
|
||||
[]
|
||||
if self.accounts.have_pro()
|
||||
else ['upgrades.infinite_runaround']
|
||||
)
|
||||
if game in (
|
||||
'Challenges:Infinite Onslaught',
|
||||
'Challenges:Tournament Infinite Onslaught',
|
||||
):
|
||||
# Special case: Pro used to unlock this.
|
||||
return (
|
||||
[]
|
||||
if self.accounts.have_pro()
|
||||
else ['upgrades.infinite_onslaught']
|
||||
)
|
||||
if game in (
|
||||
'Challenges:Meteor Shower',
|
||||
'Challenges:Epic Meteor Shower',
|
||||
):
|
||||
return ['games.meteor_shower']
|
||||
|
||||
if game in (
|
||||
'Challenges:Target Practice',
|
||||
'Challenges:Target Practice B',
|
||||
):
|
||||
return ['games.target_practice']
|
||||
|
||||
if game in (
|
||||
'Challenges:Ninja Fight',
|
||||
'Challenges:Pro Ninja Fight',
|
||||
):
|
||||
return ['games.ninja_fight']
|
||||
|
||||
if game in ('Challenges:Race', 'Challenges:Pro Race'):
|
||||
return ['games.race']
|
||||
|
||||
if game in ('Challenges:Lake Frigid Race',):
|
||||
return ['games.race', 'maps.lake_frigid']
|
||||
|
||||
if game in (
|
||||
'Challenges:Easter Egg Hunt',
|
||||
'Challenges:Pro Easter Egg Hunt',
|
||||
):
|
||||
return ['games.easter_egg_hunt']
|
||||
|
||||
return []
|
||||
|
||||
def is_game_unlocked(self, game: str) -> bool:
|
||||
"""Is a particular game unlocked?"""
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
purchases = self.required_purchases_for_game(game)
|
||||
if not purchases:
|
||||
return True
|
||||
|
||||
for purchase in purchases:
|
||||
if not plus.get_v1_account_product_purchased(purchase):
|
||||
return False
|
||||
return True
|
||||
27
dist/ba_data/python/baclassic/_benchmark.py
vendored
27
dist/ba_data/python/baclassic/_benchmark.py
vendored
|
|
@ -20,11 +20,14 @@ def run_cpu_benchmark() -> None:
|
|||
# pylint: disable=cyclic-import
|
||||
from bascenev1lib import tutorial
|
||||
|
||||
# Save our UI state that we'll return to when done.
|
||||
if babase.app.classic is not None:
|
||||
babase.app.classic.save_ui_state()
|
||||
|
||||
class BenchmarkSession(bascenev1.Session):
|
||||
"""Session type for cpu benchmark."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# print('FIXME: BENCHMARK SESSION WOULD CALC DEPS.')
|
||||
depsets: Sequence[bascenev1.DependencySet] = []
|
||||
|
||||
super().__init__(depsets)
|
||||
|
|
@ -99,7 +102,9 @@ def _start_stress_test(args: _StressTestArgs) -> None:
|
|||
"""(internal)"""
|
||||
from bascenev1 import DualTeamSession, FreeForAllSession
|
||||
|
||||
assert babase.app.classic is not None
|
||||
classic = babase.app.classic
|
||||
|
||||
assert classic is not None
|
||||
|
||||
appconfig = babase.app.config
|
||||
playlist_type = args.playlist_type
|
||||
|
|
@ -116,6 +121,10 @@ def _start_stress_test(args: _StressTestArgs) -> None:
|
|||
+ args.playlist_name
|
||||
+ '")...'
|
||||
)
|
||||
|
||||
# Save where we are in the UI so we'll return there when done.
|
||||
classic.save_ui_state()
|
||||
|
||||
if playlist_type == 'Teams':
|
||||
appconfig['Team Tournament Playlist Selection'] = args.playlist_name
|
||||
appconfig['Team Tournament Playlist Randomize'] = 1
|
||||
|
|
@ -137,11 +146,11 @@ def _start_stress_test(args: _StressTestArgs) -> None:
|
|||
),
|
||||
)
|
||||
_baclassic.set_stress_testing(True, args.player_count, args.attract_mode)
|
||||
babase.app.classic.stress_test_update_timer = babase.AppTimer(
|
||||
classic.stress_test_update_timer = babase.AppTimer(
|
||||
args.round_duration, babase.Call(_reset_stress_test, args)
|
||||
)
|
||||
if args.attract_mode:
|
||||
babase.app.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
|
||||
)
|
||||
|
||||
|
|
@ -170,12 +179,6 @@ def _reset_stress_test(args: _StressTestArgs) -> None:
|
|||
babase.apptimer(1.0, babase.Call(_start_stress_test, args))
|
||||
|
||||
|
||||
def run_gpu_benchmark() -> None:
|
||||
"""Kick off a benchmark to test gpu speeds."""
|
||||
# FIXME: Not wired up yet.
|
||||
babase.screenmessage('Not wired up yet.', color=(1, 0, 0))
|
||||
|
||||
|
||||
def run_media_reload_benchmark() -> None:
|
||||
"""Kick off a benchmark to test media reloading speeds."""
|
||||
babase.reload_media()
|
||||
|
|
@ -199,6 +202,6 @@ def run_media_reload_benchmark() -> None:
|
|||
|
||||
babase.add_clean_frame_callback(babase.Call(doit, start_time))
|
||||
|
||||
# The reload starts (should add a completion callback to the
|
||||
# reload func to fix this).
|
||||
# The reload starts (should add a completion callback to the reload
|
||||
# func to fix this).
|
||||
babase.apptimer(0.05, babase.Call(delay_add, babase.apptime()))
|
||||
|
|
|
|||
91
dist/ba_data/python/baclassic/_chest.py
vendored
Normal file
91
dist/ba_data/python/baclassic/_chest.py
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Chest related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bacommon.bs import ClassicChestAppearance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChestAppearanceDisplayInfo:
|
||||
"""Info about how to locally display chest appearances."""
|
||||
|
||||
# NOTE TO SELF: Don't rename these attrs; the C++ layer is hard
|
||||
# coded to look for them.
|
||||
|
||||
texclosed: str
|
||||
texclosedtint: str
|
||||
texopen: str
|
||||
texopentint: str
|
||||
color: tuple[float, float, float]
|
||||
tint: tuple[float, float, float]
|
||||
tint2: tuple[float, float, float]
|
||||
|
||||
|
||||
# Info for chest types we know how to draw. Anything not found in here
|
||||
# should fall back to the DEFAULT entry.
|
||||
CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT = ChestAppearanceDisplayInfo(
|
||||
texclosed='chestIcon',
|
||||
texclosedtint='chestIconTint',
|
||||
texopen='chestOpenIcon',
|
||||
texopentint='chestOpenIconTint',
|
||||
color=(1, 1, 1),
|
||||
tint=(1, 1, 1),
|
||||
tint2=(1, 1, 1),
|
||||
)
|
||||
|
||||
CHEST_APPEARANCE_DISPLAY_INFOS: dict[
|
||||
ClassicChestAppearance, ChestAppearanceDisplayInfo
|
||||
] = {
|
||||
ClassicChestAppearance.L2: ChestAppearanceDisplayInfo(
|
||||
texclosed='chestIcon',
|
||||
texclosedtint='chestIconTint',
|
||||
texopen='chestOpenIcon',
|
||||
texopentint='chestOpenIconTint',
|
||||
color=(0.8, 1.0, 0.93),
|
||||
tint=(0.65, 1.0, 0.8),
|
||||
tint2=(0.65, 1.0, 0.8),
|
||||
),
|
||||
ClassicChestAppearance.L3: ChestAppearanceDisplayInfo(
|
||||
texclosed='chestIcon',
|
||||
texclosedtint='chestIconTint',
|
||||
texopen='chestOpenIcon',
|
||||
texopentint='chestOpenIconTint',
|
||||
color=(0.75, 0.9, 1.3),
|
||||
tint=(0.7, 1, 1.9),
|
||||
tint2=(0.7, 1, 1.9),
|
||||
),
|
||||
ClassicChestAppearance.L4: ChestAppearanceDisplayInfo(
|
||||
texclosed='chestIcon',
|
||||
texclosedtint='chestIconTint',
|
||||
texopen='chestOpenIcon',
|
||||
texopentint='chestOpenIconTint',
|
||||
color=(0.7, 1.0, 1.4),
|
||||
tint=(1.4, 1.6, 2.0),
|
||||
tint2=(1.4, 1.6, 2.0),
|
||||
),
|
||||
ClassicChestAppearance.L5: ChestAppearanceDisplayInfo(
|
||||
texclosed='chestIcon',
|
||||
texclosedtint='chestIconTint',
|
||||
texopen='chestOpenIcon',
|
||||
texopentint='chestOpenIconTint',
|
||||
color=(0.75, 0.5, 2.4),
|
||||
tint=(1.0, 0.8, 0.0),
|
||||
tint2=(1.0, 0.8, 0.0),
|
||||
),
|
||||
ClassicChestAppearance.L6: ChestAppearanceDisplayInfo(
|
||||
texclosed='chestIcon',
|
||||
texclosedtint='chestIconTint',
|
||||
texopen='chestOpenIcon',
|
||||
texopentint='chestOpenIconTint',
|
||||
color=(1.1, 0.8, 0.0),
|
||||
tint=(2, 2, 2),
|
||||
tint2=(2, 2, 2),
|
||||
),
|
||||
}
|
||||
77
dist/ba_data/python/baclassic/_clienteffect.py
vendored
Normal file
77
dist/ba_data/python/baclassic/_clienteffect.py
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to running client-effects from the master server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, assert_never
|
||||
|
||||
from efro.util import strict_partial
|
||||
|
||||
import bacommon.bs
|
||||
import bauiv1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
|
||||
"""Run effects."""
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
delay = 0.0
|
||||
for effect in effects:
|
||||
if isinstance(effect, bacommon.bs.ClientEffectScreenMessage):
|
||||
textfin = bauiv1.Lstr(
|
||||
translate=('serverResponses', effect.message)
|
||||
).evaluate()
|
||||
if effect.subs is not None:
|
||||
# Should always be even.
|
||||
assert len(effect.subs) % 2 == 0
|
||||
for j in range(0, len(effect.subs) - 1, 2):
|
||||
textfin = textfin.replace(
|
||||
effect.subs[j],
|
||||
effect.subs[j + 1],
|
||||
)
|
||||
bauiv1.apptimer(
|
||||
delay,
|
||||
strict_partial(
|
||||
bauiv1.screenmessage, textfin, color=effect.color
|
||||
),
|
||||
)
|
||||
|
||||
elif isinstance(effect, bacommon.bs.ClientEffectSound):
|
||||
smcls = bacommon.bs.ClientEffectSound.Sound
|
||||
soundfile: str | None = None
|
||||
if effect.sound is smcls.UNKNOWN:
|
||||
# Server should avoid sending us sounds we don't
|
||||
# support. Make some noise if it happens.
|
||||
logging.error('Got unrecognized bacommon.bs.ClientEffectSound.')
|
||||
elif effect.sound is smcls.CASH_REGISTER:
|
||||
soundfile = 'cashRegister'
|
||||
elif effect.sound is smcls.ERROR:
|
||||
soundfile = 'error'
|
||||
elif effect.sound is smcls.POWER_DOWN:
|
||||
soundfile = 'powerdown01'
|
||||
elif effect.sound is smcls.GUN_COCKING:
|
||||
soundfile = 'gunCocking'
|
||||
else:
|
||||
assert_never(effect.sound)
|
||||
if soundfile is not None:
|
||||
bauiv1.apptimer(
|
||||
delay,
|
||||
strict_partial(
|
||||
bauiv1.getsound(soundfile).play, volume=effect.volume
|
||||
),
|
||||
)
|
||||
|
||||
elif isinstance(effect, bacommon.bs.ClientEffectDelay):
|
||||
delay += effect.seconds
|
||||
else:
|
||||
# Server should not send us stuff we can't digest. Make
|
||||
# some noise if it happens.
|
||||
logging.error(
|
||||
'Got unrecognized bacommon.bs.ClientEffect;'
|
||||
' should not happen.'
|
||||
)
|
||||
109
dist/ba_data/python/baclassic/_displayitem.py
vendored
Normal file
109
dist/ba_data/python/baclassic/_displayitem.py
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Display-item related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.util import pairs_from_flat
|
||||
import bacommon.bs
|
||||
import bauiv1
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
def show_display_item(
|
||||
itemwrapper: bacommon.bs.DisplayItemWrapper,
|
||||
parent: bauiv1.Widget,
|
||||
pos: tuple[float, float],
|
||||
width: float,
|
||||
) -> None:
|
||||
"""Create ui to depict a display-item."""
|
||||
|
||||
height = width * 0.666
|
||||
|
||||
# Silent no-op if our parent ui is dead.
|
||||
if not parent:
|
||||
return
|
||||
|
||||
img: str | None = None
|
||||
img_y_offs = 0.0
|
||||
text_y_offs = 0.0
|
||||
show_text = True
|
||||
|
||||
if isinstance(itemwrapper.item, bacommon.bs.TicketsDisplayItem):
|
||||
img = 'tickets'
|
||||
img_y_offs = width * 0.11
|
||||
text_y_offs = width * -0.15
|
||||
elif isinstance(itemwrapper.item, bacommon.bs.TokensDisplayItem):
|
||||
img = 'coin'
|
||||
img_y_offs = width * 0.11
|
||||
text_y_offs = width * -0.15
|
||||
elif isinstance(itemwrapper.item, bacommon.bs.ChestDisplayItem):
|
||||
from baclassic._chest import (
|
||||
CHEST_APPEARANCE_DISPLAY_INFOS,
|
||||
CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
|
||||
)
|
||||
|
||||
img = None
|
||||
show_text = False
|
||||
c_info = CHEST_APPEARANCE_DISPLAY_INFOS.get(
|
||||
itemwrapper.item.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
|
||||
)
|
||||
c_size = width * 0.85
|
||||
bauiv1.imagewidget(
|
||||
parent=parent,
|
||||
position=(pos[0] - c_size * 0.5, pos[1] - c_size * 0.5),
|
||||
color=c_info.color,
|
||||
size=(c_size, c_size),
|
||||
texture=bauiv1.gettexture(c_info.texclosed),
|
||||
tint_texture=bauiv1.gettexture(c_info.texclosedtint),
|
||||
tint_color=c_info.tint,
|
||||
tint2_color=c_info.tint2,
|
||||
)
|
||||
|
||||
# Enable this for testing spacing.
|
||||
if bool(False):
|
||||
bauiv1.imagewidget(
|
||||
parent=parent,
|
||||
position=(
|
||||
pos[0] - width * 0.5,
|
||||
pos[1] - height * 0.5,
|
||||
),
|
||||
size=(width, height),
|
||||
texture=bauiv1.gettexture('white'),
|
||||
color=(0, 1, 0),
|
||||
opacity=0.1,
|
||||
)
|
||||
|
||||
imgsize = width * 0.33
|
||||
if img is not None:
|
||||
bauiv1.imagewidget(
|
||||
parent=parent,
|
||||
position=(
|
||||
pos[0] - imgsize * 0.5,
|
||||
pos[1] + img_y_offs - imgsize * 0.5,
|
||||
),
|
||||
size=(imgsize, imgsize),
|
||||
texture=bauiv1.gettexture(img),
|
||||
)
|
||||
if show_text:
|
||||
subs = itemwrapper.description_subs
|
||||
if subs is None:
|
||||
subs = []
|
||||
bauiv1.textwidget(
|
||||
parent=parent,
|
||||
position=(pos[0], pos[1] + text_y_offs),
|
||||
scale=width * 0.006,
|
||||
size=(0, 0),
|
||||
text=bauiv1.Lstr(
|
||||
translate=('displayItemNames', itemwrapper.description),
|
||||
subs=pairs_from_flat(subs),
|
||||
),
|
||||
maxwidth=width * 0.9,
|
||||
color=(0.0, 1.0, 0.0),
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
7
dist/ba_data/python/baclassic/_music.py
vendored
7
dist/ba_data/python/baclassic/_music.py
vendored
|
|
@ -16,6 +16,8 @@ from bascenev1 import MusicType
|
|||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
||||
import bauiv1
|
||||
|
||||
|
||||
class MusicPlayMode(Enum):
|
||||
"""Influences behavior when playing music.
|
||||
|
|
@ -389,7 +391,7 @@ class MusicPlayer:
|
|||
callback: Callable[[Any], None],
|
||||
current_entry: Any,
|
||||
selection_target_name: str,
|
||||
) -> Any:
|
||||
) -> bauiv1.MainWindow:
|
||||
"""Summons a UI to select a new soundtrack entry."""
|
||||
return self.on_select_entry(
|
||||
callback, current_entry, selection_target_name
|
||||
|
|
@ -432,11 +434,12 @@ class MusicPlayer:
|
|||
callback: Callable[[Any], None],
|
||||
current_entry: Any,
|
||||
selection_target_name: str,
|
||||
) -> Any:
|
||||
) -> bauiv1.MainWindow:
|
||||
"""Present a GUI to select an entry.
|
||||
|
||||
The callback should be called with a valid entry or None to
|
||||
signify that the default soundtrack should be used.."""
|
||||
raise NotImplementedError()
|
||||
|
||||
# Subclasses should override the following:
|
||||
|
||||
|
|
|
|||
7
dist/ba_data/python/baclassic/_net.py
vendored
7
dist/ba_data/python/baclassic/_net.py
vendored
|
|
@ -35,6 +35,8 @@ class MasterServerV1CallThread(threading.Thread):
|
|||
callback: MasterServerCallback | None,
|
||||
response_type: MasterServerResponseType,
|
||||
):
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
|
||||
# Set daemon=True so long-running requests don't keep us from
|
||||
# quitting the app.
|
||||
super().__init__(daemon=True)
|
||||
|
|
@ -52,8 +54,9 @@ class MasterServerV1CallThread(threading.Thread):
|
|||
self._activity = weakref.ref(activity) if activity is not None else None
|
||||
|
||||
def _run_callback(self, arg: None | dict[str, Any]) -> None:
|
||||
# If we were created in an activity context and that activity has
|
||||
# since died, do nothing.
|
||||
# If we were created in an activity context and that activity
|
||||
# has since died, do nothing.
|
||||
|
||||
# FIXME: Should we just be using a ContextCall instead of doing
|
||||
# this check manually?
|
||||
if self._activity is not None:
|
||||
|
|
|
|||
60
dist/ba_data/python/baclassic/_store.py
vendored
60
dist/ba_data/python/baclassic/_store.py
vendored
|
|
@ -26,6 +26,7 @@ class StoreSubsystem:
|
|||
def get_store_item_name_translated(self, item_name: str) -> babase.Lstr:
|
||||
"""Return a babase.Lstr for a store item name."""
|
||||
# pylint: disable=cyclic-import
|
||||
# pylint: disable=too-many-return-statements
|
||||
item_info = self.get_store_item(item_name)
|
||||
if item_name.startswith('characters.'):
|
||||
return babase.Lstr(
|
||||
|
|
@ -46,6 +47,14 @@ class StoreSubsystem:
|
|||
return gametype.get_display_string()
|
||||
if item_name.startswith('icons.'):
|
||||
return babase.Lstr(resource='editProfileWindow.iconText')
|
||||
if item_name == 'upgrades.infinite_runaround':
|
||||
return babase.Lstr(
|
||||
translate=('coopLevelNames', 'Infinite Runaround')
|
||||
)
|
||||
if item_name == 'upgrades.infinite_onslaught':
|
||||
return babase.Lstr(
|
||||
translate=('coopLevelNames', 'Infinite Onslaught')
|
||||
)
|
||||
raise ValueError('unrecognized item: ' + item_name)
|
||||
|
||||
def get_store_item_display_size(
|
||||
|
|
@ -81,14 +90,17 @@ class StoreSubsystem:
|
|||
assert babase.app.classic is not None
|
||||
|
||||
if babase.app.classic.store_items is None:
|
||||
from bascenev1lib.game import ninjafight
|
||||
from bascenev1lib.game import meteorshower
|
||||
from bascenev1lib.game import targetpractice
|
||||
from bascenev1lib.game import easteregghunt
|
||||
from bascenev1lib.game.race import RaceGame
|
||||
from bascenev1lib.game.ninjafight import NinjaFightGame
|
||||
from bascenev1lib.game.meteorshower import MeteorShowerGame
|
||||
from bascenev1lib.game.targetpractice import TargetPracticeGame
|
||||
from bascenev1lib.game.easteregghunt import EasterEggHuntGame
|
||||
|
||||
# IMPORTANT - need to keep this synced with the master server.
|
||||
# (doing so manually for now)
|
||||
babase.app.classic.store_items = {
|
||||
'upgrades.infinite_runaround': {},
|
||||
'upgrades.infinite_onslaught': {},
|
||||
'characters.kronk': {'character': 'Kronk'},
|
||||
'characters.zoe': {'character': 'Zoe'},
|
||||
'characters.jackmorgan': {'character': 'Jack Morgan'},
|
||||
|
|
@ -111,20 +123,28 @@ class StoreSubsystem:
|
|||
'merch': {},
|
||||
'pro': {},
|
||||
'maps.lake_frigid': {'map_type': maps.LakeFrigid},
|
||||
'games.race': {
|
||||
'gametype': RaceGame,
|
||||
'previewTex': 'bigGPreview',
|
||||
},
|
||||
'games.ninja_fight': {
|
||||
'gametype': ninjafight.NinjaFightGame,
|
||||
'gametype': NinjaFightGame,
|
||||
'previewTex': 'courtyardPreview',
|
||||
},
|
||||
'games.meteor_shower': {
|
||||
'gametype': meteorshower.MeteorShowerGame,
|
||||
'gametype': MeteorShowerGame,
|
||||
'previewTex': 'rampagePreview',
|
||||
},
|
||||
'games.infinite_onslaught': {
|
||||
'gametype': MeteorShowerGame,
|
||||
'previewTex': 'rampagePreview',
|
||||
},
|
||||
'games.target_practice': {
|
||||
'gametype': targetpractice.TargetPracticeGame,
|
||||
'gametype': TargetPracticeGame,
|
||||
'previewTex': 'doomShroomPreview',
|
||||
},
|
||||
'games.easter_egg_hunt': {
|
||||
'gametype': easteregghunt.EasterEggHuntGame,
|
||||
'gametype': EasterEggHuntGame,
|
||||
'previewTex': 'towerDPreview',
|
||||
},
|
||||
'icons.flag_us': {
|
||||
|
|
@ -365,9 +385,12 @@ class StoreSubsystem:
|
|||
store_layout['minigames'] = [
|
||||
{
|
||||
'items': [
|
||||
'games.race',
|
||||
'games.ninja_fight',
|
||||
'games.meteor_shower',
|
||||
'games.target_practice',
|
||||
'upgrades.infinite_onslaught',
|
||||
'upgrades.infinite_runaround',
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -446,8 +469,9 @@ class StoreSubsystem:
|
|||
'price.' + item, None
|
||||
)
|
||||
if ticket_cost is not None:
|
||||
if our_tickets >= ticket_cost and not plus.get_purchased(
|
||||
item
|
||||
if (
|
||||
our_tickets >= ticket_cost
|
||||
and not plus.get_v1_account_product_purchased(item)
|
||||
):
|
||||
count += 1
|
||||
return count
|
||||
|
|
@ -522,7 +546,7 @@ class StoreSubsystem:
|
|||
for section in store_layout[tab]:
|
||||
for item in section['items']:
|
||||
if item in sales_raw:
|
||||
if not plus.get_purchased(item):
|
||||
if not plus.get_v1_account_product_purchased(item):
|
||||
to_end = (
|
||||
datetime.datetime.fromtimestamp(
|
||||
sales_raw[item]['e'], datetime.UTC
|
||||
|
|
@ -550,7 +574,10 @@ class StoreSubsystem:
|
|||
if babase.app.env.gui:
|
||||
for map_section in self.get_store_layout()['maps']:
|
||||
for mapitem in map_section['items']:
|
||||
if plus is None or not plus.get_purchased(mapitem):
|
||||
if (
|
||||
plus is None
|
||||
or not plus.get_v1_account_product_purchased(mapitem)
|
||||
):
|
||||
m_info = self.get_store_item(mapitem)
|
||||
unowned_maps.add(m_info['map_type'].name)
|
||||
return sorted(unowned_maps)
|
||||
|
|
@ -563,7 +590,14 @@ class StoreSubsystem:
|
|||
if babase.app.env.gui:
|
||||
for section in self.get_store_layout()['minigames']:
|
||||
for mname in section['items']:
|
||||
if plus is None or not plus.get_purchased(mname):
|
||||
if mname.startswith('upgrades.'):
|
||||
# Ignore things like infinite onslaught which
|
||||
# aren't actually game types.
|
||||
continue
|
||||
if (
|
||||
plus is None
|
||||
or not plus.get_v1_account_product_purchased(mname)
|
||||
):
|
||||
m_info = self.get_store_item(mname)
|
||||
unowned_games.add(m_info['gametype'])
|
||||
return unowned_games
|
||||
|
|
|
|||
108
dist/ba_data/python/baclassic/_tournament.py
vendored
108
dist/ba_data/python/baclassic/_tournament.py
vendored
|
|
@ -6,13 +6,23 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bacommon.bs import ClassicChestAppearance
|
||||
import babase
|
||||
import bauiv1
|
||||
import bascenev1
|
||||
|
||||
from baclassic._chest import (
|
||||
CHEST_APPEARANCE_DISPLAY_INFOS,
|
||||
CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
|
||||
def get_tournament_prize_strings(
|
||||
entry: dict[str, Any], include_tickets: bool
|
||||
) -> list[str]:
|
||||
"""Given a tournament entry, return strings for its prize levels."""
|
||||
# pylint: disable=too-many-locals
|
||||
from bascenev1 import get_trophy_string
|
||||
|
|
@ -27,7 +37,7 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
|
|||
trophy_type_2 = entry.get('prizeTrophy2')
|
||||
trophy_type_3 = entry.get('prizeTrophy3')
|
||||
out_vals = []
|
||||
for rng, prize, trophy_type in (
|
||||
for rng, ticket_prize, trophy_type in (
|
||||
(range1, prize1, trophy_type_1),
|
||||
(range2, prize2, trophy_type_2),
|
||||
(range3, prize3, trophy_type_3),
|
||||
|
|
@ -45,14 +55,100 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
|
|||
if trophy_type is not None:
|
||||
pvval += get_trophy_string(trophy_type)
|
||||
|
||||
# If we've got trophies but not for this entry, throw some space
|
||||
# in to compensate so the ticket counts line up.
|
||||
if prize is not None:
|
||||
if ticket_prize is not None and include_tickets:
|
||||
pvval = (
|
||||
babase.charstr(babase.SpecialChar.TICKET_BACKING)
|
||||
+ str(prize)
|
||||
+ str(ticket_prize)
|
||||
+ pvval
|
||||
)
|
||||
out_vals.append(prval)
|
||||
out_vals.append(pvval)
|
||||
return out_vals
|
||||
|
||||
|
||||
def set_tournament_prize_chest_image(
|
||||
entry: dict[str, Any], index: int, image: bauiv1.Widget
|
||||
) -> None:
|
||||
"""Set image attrs representing a tourney prize chest."""
|
||||
ranges = [
|
||||
entry.get('prizeRange1'),
|
||||
entry.get('prizeRange2'),
|
||||
entry.get('prizeRange3'),
|
||||
]
|
||||
chests = [
|
||||
entry.get('prizeChest1'),
|
||||
entry.get('prizeChest2'),
|
||||
entry.get('prizeChest3'),
|
||||
]
|
||||
|
||||
assert 0 <= index < 3
|
||||
|
||||
# If tourney doesn't include this prize, just hide the image.
|
||||
if ranges[index] is None:
|
||||
bauiv1.imagewidget(edit=image, opacity=0.0)
|
||||
return
|
||||
|
||||
try:
|
||||
appearance = ClassicChestAppearance(chests[index])
|
||||
except ValueError:
|
||||
appearance = ClassicChestAppearance.DEFAULT
|
||||
chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
|
||||
appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
|
||||
)
|
||||
bauiv1.imagewidget(
|
||||
edit=image,
|
||||
opacity=1.0,
|
||||
color=chestdisplayinfo.color,
|
||||
texture=bauiv1.gettexture(chestdisplayinfo.texclosed),
|
||||
tint_texture=bauiv1.gettexture(chestdisplayinfo.texclosedtint),
|
||||
tint_color=chestdisplayinfo.tint,
|
||||
tint2_color=chestdisplayinfo.tint2,
|
||||
)
|
||||
|
||||
|
||||
def create_in_game_tournament_prize_image(
|
||||
entry: dict[str, Any], index: int, position: tuple[float, float]
|
||||
) -> None:
|
||||
"""Create a display for the prize chest (if any) in-game."""
|
||||
from bascenev1lib.actor.image import Image
|
||||
|
||||
ranges = [
|
||||
entry.get('prizeRange1'),
|
||||
entry.get('prizeRange2'),
|
||||
entry.get('prizeRange3'),
|
||||
]
|
||||
chests = [
|
||||
entry.get('prizeChest1'),
|
||||
entry.get('prizeChest2'),
|
||||
entry.get('prizeChest3'),
|
||||
]
|
||||
|
||||
# If tourney doesn't include this prize, no-op.
|
||||
if ranges[index] is None:
|
||||
return
|
||||
|
||||
try:
|
||||
appearance = ClassicChestAppearance(chests[index])
|
||||
except ValueError:
|
||||
appearance = ClassicChestAppearance.DEFAULT
|
||||
chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
|
||||
appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
|
||||
)
|
||||
Image(
|
||||
# Provide magical extended dict version of texture that Image
|
||||
# actor supports.
|
||||
texture={
|
||||
'texture': bascenev1.gettexture(chestdisplayinfo.texclosed),
|
||||
'tint_texture': bascenev1.gettexture(
|
||||
chestdisplayinfo.texclosedtint
|
||||
),
|
||||
'tint_color': chestdisplayinfo.tint,
|
||||
'tint2_color': chestdisplayinfo.tint2,
|
||||
'mask_texture': None,
|
||||
},
|
||||
color=chestdisplayinfo.color + (1.0,),
|
||||
position=position,
|
||||
scale=(48.0, 48.0),
|
||||
transition=Image.Transition.FADE_IN,
|
||||
transition_delay=2.0,
|
||||
).autoretain()
|
||||
|
|
|
|||
4
dist/ba_data/python/baclassic/macmusicapp.py
vendored
4
dist/ba_data/python/baclassic/macmusicapp.py
vendored
|
|
@ -15,6 +15,8 @@ from baclassic._music import MusicPlayer
|
|||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
||||
import bauiv1
|
||||
|
||||
|
||||
class MacMusicAppMusicPlayer(MusicPlayer):
|
||||
"""A music-player that utilizes the macOS Music.app for playback.
|
||||
|
|
@ -33,7 +35,7 @@ class MacMusicAppMusicPlayer(MusicPlayer):
|
|||
callback: Callable[[Any], None],
|
||||
current_entry: Any,
|
||||
selection_target_name: str,
|
||||
) -> Any:
|
||||
) -> bauiv1.MainWindow:
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.soundtrack import entrytypeselect as etsel
|
||||
|
||||
|
|
|
|||
4
dist/ba_data/python/baclassic/osmusic.py
vendored
4
dist/ba_data/python/baclassic/osmusic.py
vendored
|
|
@ -16,6 +16,8 @@ from baclassic._music import MusicPlayer
|
|||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
||||
import bauiv1
|
||||
|
||||
|
||||
class OSMusicPlayer(MusicPlayer):
|
||||
"""Music player that talks to internal C++ layer for functionality.
|
||||
|
|
@ -39,7 +41,7 @@ class OSMusicPlayer(MusicPlayer):
|
|||
callback: Callable[[Any], None],
|
||||
current_entry: Any,
|
||||
selection_target_name: str,
|
||||
) -> Any:
|
||||
) -> bauiv1.MainWindow:
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.soundtrack.entrytypeselect import (
|
||||
SoundtrackEntryTypeSelectWindow,
|
||||
|
|
|
|||
20
dist/ba_data/python/bacommon/app.py
vendored
20
dist/ba_data/python/bacommon/app.py
vendored
|
|
@ -31,8 +31,8 @@ class AppInterfaceIdiom(Enum):
|
|||
class AppExperience(Enum):
|
||||
"""A particular experience that can be provided by a Ballistica app.
|
||||
|
||||
This is one metric used to isolate different playerbases from
|
||||
each other where there might be no technical barriers doing so. For
|
||||
This is one metric used to isolate different playerbases from each
|
||||
other where there might be no technical barriers doing so. For
|
||||
example, a casual one-hand-playable phone game and an augmented
|
||||
reality tabletop game may both use the same scene-versions and
|
||||
networking-protocols and whatnot, but it would make no sense to
|
||||
|
|
@ -75,10 +75,10 @@ class AppArchitecture(Enum):
|
|||
class AppPlatform(Enum):
|
||||
"""Overall platform a Ballistica build is targeting.
|
||||
|
||||
Each distinct flavor of an app has a unique combination
|
||||
of AppPlatform and AppVariant. Generally platform describes
|
||||
a set of hardware, while variant describes a destination or
|
||||
purpose for the build.
|
||||
Each distinct flavor of an app has a unique combination of
|
||||
AppPlatform and AppVariant. Generally platform describes a set of
|
||||
hardware, while variant describes a destination or purpose for the
|
||||
build.
|
||||
"""
|
||||
|
||||
MAC = 'mac'
|
||||
|
|
@ -92,10 +92,10 @@ class AppPlatform(Enum):
|
|||
class AppVariant(Enum):
|
||||
"""A unique Ballistica build type within a single platform.
|
||||
|
||||
Each distinct flavor of an app has a unique combination
|
||||
of AppPlatform and AppVariant. Generally platform describes
|
||||
a set of hardware, while variant describes a destination or
|
||||
purpose for the build.
|
||||
Each distinct flavor of an app has a unique combination of
|
||||
AppPlatform and AppVariant. Generally platform describes a set of
|
||||
hardware, while variant describes a destination or purpose for the
|
||||
build.
|
||||
"""
|
||||
|
||||
# Default builds.
|
||||
|
|
|
|||
887
dist/ba_data/python/bacommon/bs.py
vendored
Normal file
887
dist/ba_data/python/bacommon/bs.py
vendored
Normal file
|
|
@ -0,0 +1,887 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""BombSquad specific bits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Annotated, override, assert_never
|
||||
|
||||
from efro.util import pairs_to_flat
|
||||
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
|
||||
from efro.message import Message, Response
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class PrivatePartyMessage(Message):
|
||||
"""Message asking about info we need for private-party UI."""
|
||||
|
||||
need_datacode: Annotated[bool, IOAttrs('d')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [PrivatePartyResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class PrivatePartyResponse(Response):
|
||||
"""Here's that private party UI info you asked for, boss."""
|
||||
|
||||
success: Annotated[bool, IOAttrs('s')]
|
||||
tokens: Annotated[int, IOAttrs('t')]
|
||||
gold_pass: Annotated[bool, IOAttrs('g')]
|
||||
datacode: Annotated[str | None, IOAttrs('d')]
|
||||
|
||||
|
||||
class ClassicChestAppearance(Enum):
|
||||
"""Appearances bombsquad classic chests can have."""
|
||||
|
||||
UNKNOWN = 'u'
|
||||
DEFAULT = 'd'
|
||||
L1 = 'l1'
|
||||
L2 = 'l2'
|
||||
L3 = 'l3'
|
||||
L4 = 'l4'
|
||||
L5 = 'l5'
|
||||
L6 = 'l6'
|
||||
|
||||
@property
|
||||
def pretty_name(self) -> str:
|
||||
"""Pretty name for the chest in English."""
|
||||
# pylint: disable=too-many-return-statements
|
||||
cls = type(self)
|
||||
|
||||
if self is cls.UNKNOWN:
|
||||
return 'Unknown Chest'
|
||||
if self is cls.DEFAULT:
|
||||
return 'Chest'
|
||||
if self is cls.L1:
|
||||
return 'L1 Chest'
|
||||
if self is cls.L2:
|
||||
return 'L2 Chest'
|
||||
if self is cls.L3:
|
||||
return 'L3 Chest'
|
||||
if self is cls.L4:
|
||||
return 'L4 Chest'
|
||||
if self is cls.L5:
|
||||
return 'L5 Chest'
|
||||
if self is cls.L6:
|
||||
return 'L6 Chest'
|
||||
|
||||
assert_never(self)
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ClassicAccountLiveData:
|
||||
"""Live account data fed to the client in the bs classic app mode."""
|
||||
|
||||
@dataclass
|
||||
class Chest:
|
||||
"""A lovely chest."""
|
||||
|
||||
appearance: Annotated[
|
||||
ClassicChestAppearance,
|
||||
IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
|
||||
]
|
||||
unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
|
||||
ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]
|
||||
|
||||
class LeagueType(Enum):
|
||||
"""Type of league we are in."""
|
||||
|
||||
BRONZE = 'b'
|
||||
SILVER = 's'
|
||||
GOLD = 'g'
|
||||
DIAMOND = 'd'
|
||||
|
||||
tickets: Annotated[int, IOAttrs('ti')]
|
||||
|
||||
tokens: Annotated[int, IOAttrs('to')]
|
||||
gold_pass: Annotated[bool, IOAttrs('g')]
|
||||
remove_ads: Annotated[bool, IOAttrs('r')]
|
||||
|
||||
achievements: Annotated[int, IOAttrs('a')]
|
||||
achievements_total: Annotated[int, IOAttrs('at')]
|
||||
|
||||
league_type: Annotated[LeagueType | None, IOAttrs('lt')]
|
||||
league_num: Annotated[int | None, IOAttrs('ln')]
|
||||
league_rank: Annotated[int | None, IOAttrs('lr')]
|
||||
|
||||
level: Annotated[int, IOAttrs('lv')]
|
||||
xp: Annotated[int, IOAttrs('xp')]
|
||||
xpmax: Annotated[int, IOAttrs('xpm')]
|
||||
|
||||
inbox_count: Annotated[int, IOAttrs('ibc')]
|
||||
inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
|
||||
|
||||
chests: Annotated[dict[str, Chest], IOAttrs('c')]
|
||||
|
||||
|
||||
class DisplayItemTypeID(Enum):
|
||||
"""Type ID for each of our subclasses."""
|
||||
|
||||
UNKNOWN = 'u'
|
||||
TICKETS = 't'
|
||||
TOKENS = 'k'
|
||||
TEST = 's'
|
||||
CHEST = 'c'
|
||||
|
||||
|
||||
class DisplayItem(IOMultiType[DisplayItemTypeID]):
|
||||
"""Some amount of something that can be shown or described.
|
||||
|
||||
Used to depict chest contents or other rewards or prices.
|
||||
"""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> DisplayItemTypeID:
|
||||
# Require child classes to supply this themselves. If we did a
|
||||
# full type registry/lookup here it would require us to import
|
||||
# everything and would prevent lazy loading.
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
|
||||
"""Return the subclass for each of our type-ids."""
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
t = DisplayItemTypeID
|
||||
if type_id is t.UNKNOWN:
|
||||
return UnknownDisplayItem
|
||||
if type_id is t.TICKETS:
|
||||
return TicketsDisplayItem
|
||||
if type_id is t.TOKENS:
|
||||
return TokensDisplayItem
|
||||
if type_id is t.TEST:
|
||||
return TestDisplayItem
|
||||
if type_id is t.CHEST:
|
||||
return ChestDisplayItem
|
||||
|
||||
# Important to make sure we provide all types.
|
||||
assert_never(type_id)
|
||||
|
||||
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
|
||||
"""Return a string description and subs for the item.
|
||||
|
||||
These decriptions are baked into the DisplayItemWrapper and
|
||||
should be accessed from there when available. This allows
|
||||
clients to give descriptions even for newer display items they
|
||||
don't recognize.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
# Implement fallbacks so client can digest item lists even if they
|
||||
# contain unrecognized stuff. DisplayItemWrapper contains basic
|
||||
# baked down info that they can still use in such cases.
|
||||
@override
|
||||
@classmethod
|
||||
def get_unknown_type_fallback(cls) -> DisplayItem:
|
||||
return UnknownDisplayItem()
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class UnknownDisplayItem(DisplayItem):
|
||||
"""Something we don't know how to display."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> DisplayItemTypeID:
|
||||
return DisplayItemTypeID.UNKNOWN
|
||||
|
||||
@override
|
||||
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
|
||||
import logging
|
||||
|
||||
# Make noise but don't break.
|
||||
logging.exception(
|
||||
'UnknownDisplayItem.get_description() should never be called.'
|
||||
' Always access descriptions on the DisplayItemWrapper.'
|
||||
)
|
||||
return 'Unknown', []
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class TicketsDisplayItem(DisplayItem):
|
||||
"""Some amount of tickets."""
|
||||
|
||||
count: Annotated[int, IOAttrs('c')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> DisplayItemTypeID:
|
||||
return DisplayItemTypeID.TICKETS
|
||||
|
||||
@override
|
||||
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
|
||||
return '${C} Tickets', [('${C}', str(self.count))]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class TokensDisplayItem(DisplayItem):
|
||||
"""Some amount of tokens."""
|
||||
|
||||
count: Annotated[int, IOAttrs('c')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> DisplayItemTypeID:
|
||||
return DisplayItemTypeID.TOKENS
|
||||
|
||||
@override
|
||||
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
|
||||
return '${C} Tokens', [('${C}', str(self.count))]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class TestDisplayItem(DisplayItem):
|
||||
"""Fills usable space for a display-item - good for calibration."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> DisplayItemTypeID:
|
||||
return DisplayItemTypeID.TEST
|
||||
|
||||
@override
|
||||
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
|
||||
return 'Test Display Item Here', []
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ChestDisplayItem(DisplayItem):
|
||||
"""Display a chest."""
|
||||
|
||||
appearance: Annotated[ClassicChestAppearance, IOAttrs('a')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> DisplayItemTypeID:
|
||||
return DisplayItemTypeID.CHEST
|
||||
|
||||
@override
|
||||
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
|
||||
return self.appearance.pretty_name, []
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class DisplayItemWrapper:
|
||||
"""Wraps a DisplayItem and common info."""
|
||||
|
||||
item: Annotated[DisplayItem, IOAttrs('i')]
|
||||
description: Annotated[str, IOAttrs('d')]
|
||||
description_subs: Annotated[list[str] | None, IOAttrs('s')]
|
||||
|
||||
@classmethod
|
||||
def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
|
||||
"""Convenience method to wrap a DisplayItem."""
|
||||
desc, subs = item.get_description()
|
||||
return DisplayItemWrapper(item, desc, pairs_to_flat(subs))
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ChestInfoMessage(Message):
|
||||
"""Request info about a chest."""
|
||||
|
||||
chest_id: Annotated[str, IOAttrs('i')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [ChestInfoResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ChestInfoResponse(Response):
|
||||
"""Here's that chest info you asked for, boss."""
|
||||
|
||||
@dataclass
|
||||
class Chest:
|
||||
"""A lovely chest."""
|
||||
|
||||
@dataclass
|
||||
class PrizeSet:
|
||||
"""A possible set of prizes for this chest."""
|
||||
|
||||
weight: Annotated[float, IOAttrs('w')]
|
||||
contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
|
||||
|
||||
appearance: Annotated[
|
||||
ClassicChestAppearance,
|
||||
IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
|
||||
]
|
||||
|
||||
# How much it costs to unlock *now*.
|
||||
unlock_tokens: Annotated[int, IOAttrs('tk')]
|
||||
|
||||
# When it unlocks on its own.
|
||||
unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
|
||||
|
||||
# Possible prizes we contain.
|
||||
prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
|
||||
|
||||
# Are ads allowed now?
|
||||
ad_allow: Annotated[bool, IOAttrs('aa')]
|
||||
|
||||
chest: Annotated[Chest | None, IOAttrs('c')]
|
||||
user_tokens: Annotated[int | None, IOAttrs('t')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ChestActionMessage(Message):
|
||||
"""Request action about a chest."""
|
||||
|
||||
class Action(Enum):
|
||||
"""Types of actions we can request."""
|
||||
|
||||
# Unlocking (for free or with tokens).
|
||||
UNLOCK = 'u'
|
||||
|
||||
# Watched an ad to reduce wait.
|
||||
AD = 'ad'
|
||||
|
||||
action: Annotated[Action, IOAttrs('a')]
|
||||
|
||||
# Tokens we are paying (only applies to unlock).
|
||||
token_payment: Annotated[int, IOAttrs('t')]
|
||||
|
||||
chest_id: Annotated[str, IOAttrs('i')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [ChestActionResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ChestActionResponse(Response):
|
||||
"""Here's the results of that action you asked for, boss."""
|
||||
|
||||
# Tokens that were actually charged.
|
||||
tokens_charged: Annotated[int, IOAttrs('t')] = 0
|
||||
|
||||
# If present, signifies the chest has been opened and we should show
|
||||
# the user this stuff that was in it.
|
||||
contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None
|
||||
|
||||
# If contents are present, which of the chest's prize-sets they
|
||||
# represent.
|
||||
prizeindex: Annotated[int, IOAttrs('i')] = 0
|
||||
|
||||
# Printable error if something goes wrong.
|
||||
error: Annotated[str | None, IOAttrs('e')] = None
|
||||
|
||||
# Printable warning. Shown in orange with an error sound. Does not
|
||||
# mean the action failed; only that there's something to tell the
|
||||
# users such as 'It looks like you are faking ad views; stop it or
|
||||
# you won't have ad options anymore.'
|
||||
warning: Annotated[str | None, IOAttrs('w')] = None
|
||||
|
||||
# Printable success message. Shown in green with a cash-register
|
||||
# sound. Can be used for things like successful wait reductions via
|
||||
# ad views.
|
||||
success_msg: Annotated[str | None, IOAttrs('s')] = None
|
||||
|
||||
|
||||
class ClientUITypeID(Enum):
|
||||
"""Type ID for each of our subclasses."""
|
||||
|
||||
UNKNOWN = 'u'
|
||||
BASIC = 'b'
|
||||
|
||||
|
||||
class ClientUI(IOMultiType[ClientUITypeID]):
|
||||
"""Defines some user interface on the client."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> ClientUITypeID:
|
||||
# Require child classes to supply this themselves. If we did a
|
||||
# full type registry/lookup here it would require us to import
|
||||
# everything and would prevent lazy loading.
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type(cls, type_id: ClientUITypeID) -> type[ClientUI]:
|
||||
"""Return the subclass for each of our type-ids."""
|
||||
# pylint: disable=cyclic-import
|
||||
out: type[ClientUI]
|
||||
|
||||
t = ClientUITypeID
|
||||
if type_id is t.UNKNOWN:
|
||||
out = UnknownClientUI
|
||||
elif type_id is t.BASIC:
|
||||
out = BasicClientUI
|
||||
else:
|
||||
# Important to make sure we provide all types.
|
||||
assert_never(type_id)
|
||||
return out
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_unknown_type_fallback(cls) -> ClientUI:
|
||||
# If we encounter some future message type we don't know
|
||||
# anything about, drop in a placeholder.
|
||||
return UnknownClientUI()
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class UnknownClientUI(ClientUI):
|
||||
"""Fallback type for unrecognized entries."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> ClientUITypeID:
|
||||
return ClientUITypeID.UNKNOWN
|
||||
|
||||
|
||||
class BasicClientUIComponentTypeID(Enum):
|
||||
"""Type ID for each of our subclasses."""
|
||||
|
||||
UNKNOWN = 'u'
|
||||
TEXT = 't'
|
||||
LINK = 'l'
|
||||
BS_CLASSIC_TOURNEY_RESULT = 'ct'
|
||||
DISPLAY_ITEMS = 'di'
|
||||
EXPIRE_TIME = 'd'
|
||||
|
||||
|
||||
class BasicClientUIComponent(IOMultiType[BasicClientUIComponentTypeID]):
|
||||
"""Top level class for our multitype."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> BasicClientUIComponentTypeID:
|
||||
# Require child classes to supply this themselves. If we did a
|
||||
# full type registry/lookup here it would require us to import
|
||||
# everything and would prevent lazy loading.
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type(
|
||||
cls, type_id: BasicClientUIComponentTypeID
|
||||
) -> type[BasicClientUIComponent]:
|
||||
"""Return the subclass for each of our type-ids."""
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
t = BasicClientUIComponentTypeID
|
||||
if type_id is t.UNKNOWN:
|
||||
return BasicClientUIComponentUnknown
|
||||
if type_id is t.TEXT:
|
||||
return BasicClientUIComponentText
|
||||
if type_id is t.LINK:
|
||||
return BasicClientUIComponentLink
|
||||
if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
|
||||
return BasicClientUIBsClassicTourneyResult
|
||||
if type_id is t.DISPLAY_ITEMS:
|
||||
return BasicClientUIDisplayItems
|
||||
if type_id is t.EXPIRE_TIME:
|
||||
return BasicClientUIExpireTime
|
||||
|
||||
# Important to make sure we provide all types.
|
||||
assert_never(type_id)
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_unknown_type_fallback(cls) -> BasicClientUIComponent:
|
||||
# If we encounter some future message type we don't know
|
||||
# anything about, drop in a placeholder.
|
||||
return BasicClientUIComponentUnknown()
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class BasicClientUIComponentUnknown(BasicClientUIComponent):
|
||||
"""An unknown basic client component type.
|
||||
|
||||
In practice these should never show up since the master-server
|
||||
generates these on the fly for the client and so should not send
|
||||
clients one they can't digest.
|
||||
"""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> BasicClientUIComponentTypeID:
|
||||
return BasicClientUIComponentTypeID.UNKNOWN
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class BasicClientUIComponentText(BasicClientUIComponent):
|
||||
"""Show some text in the inbox message."""
|
||||
|
||||
text: Annotated[str, IOAttrs('t')]
|
||||
subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
|
||||
default_factory=list
|
||||
)
|
||||
scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0
|
||||
color: Annotated[
|
||||
tuple[float, float, float, float], IOAttrs('c', store_default=False)
|
||||
] = (1.0, 1.0, 1.0, 1.0)
|
||||
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
|
||||
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> BasicClientUIComponentTypeID:
|
||||
return BasicClientUIComponentTypeID.TEXT
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class BasicClientUIComponentLink(BasicClientUIComponent):
|
||||
"""Show a link in the inbox message."""
|
||||
|
||||
url: Annotated[str, IOAttrs('u')]
|
||||
label: Annotated[str, IOAttrs('l')]
|
||||
subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
|
||||
default_factory=list
|
||||
)
|
||||
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
|
||||
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> BasicClientUIComponentTypeID:
|
||||
return BasicClientUIComponentTypeID.LINK
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class BasicClientUIBsClassicTourneyResult(BasicClientUIComponent):
|
||||
"""Show info about a classic tourney."""
|
||||
|
||||
tournament_id: Annotated[str, IOAttrs('t')]
|
||||
game: Annotated[str, IOAttrs('g')]
|
||||
players: Annotated[int, IOAttrs('p')]
|
||||
rank: Annotated[int, IOAttrs('r')]
|
||||
trophy: Annotated[str | None, IOAttrs('tr')]
|
||||
prizes: Annotated[list[DisplayItemWrapper], IOAttrs('pr')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> BasicClientUIComponentTypeID:
|
||||
return BasicClientUIComponentTypeID.BS_CLASSIC_TOURNEY_RESULT
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class BasicClientUIDisplayItems(BasicClientUIComponent):
|
||||
"""Show some display-items."""
|
||||
|
||||
items: Annotated[list[DisplayItemWrapper], IOAttrs('d')]
|
||||
width: Annotated[float, IOAttrs('w')] = 100.0
|
||||
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
|
||||
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> BasicClientUIComponentTypeID:
|
||||
return BasicClientUIComponentTypeID.DISPLAY_ITEMS
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class BasicClientUIExpireTime(BasicClientUIComponent):
|
||||
"""Show expire-time."""
|
||||
|
||||
time: Annotated[datetime.datetime, IOAttrs('d')]
|
||||
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
|
||||
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> BasicClientUIComponentTypeID:
|
||||
return BasicClientUIComponentTypeID.EXPIRE_TIME
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class BasicClientUI(ClientUI):
|
||||
"""A basic UI for the client."""
|
||||
|
||||
class ButtonLabel(Enum):
|
||||
"""Distinct button labels we support."""
|
||||
|
||||
UNKNOWN = 'u'
|
||||
OK = 'o'
|
||||
APPLY = 'a'
|
||||
CANCEL = 'c'
|
||||
ACCEPT = 'ac'
|
||||
DECLINE = 'dn'
|
||||
IGNORE = 'ig'
|
||||
CLAIM = 'cl'
|
||||
DISCARD = 'd'
|
||||
|
||||
class InteractionStyle(Enum):
|
||||
"""Overall interaction styles we support."""
|
||||
|
||||
UNKNOWN = 'u'
|
||||
BUTTON_POSITIVE = 'p'
|
||||
BUTTON_POSITIVE_NEGATIVE = 'pn'
|
||||
|
||||
components: Annotated[list[BasicClientUIComponent], IOAttrs('s')]
|
||||
|
||||
interaction_style: Annotated[
|
||||
InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN)
|
||||
] = InteractionStyle.BUTTON_POSITIVE
|
||||
|
||||
button_label_positive: Annotated[
|
||||
ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN)
|
||||
] = ButtonLabel.OK
|
||||
|
||||
button_label_negative: Annotated[
|
||||
ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN)
|
||||
] = ButtonLabel.CANCEL
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> ClientUITypeID:
|
||||
return ClientUITypeID.BASIC
|
||||
|
||||
def contains_unknown_elements(self) -> bool:
|
||||
"""Whether something within us is an unknown type or enum."""
|
||||
return (
|
||||
self.interaction_style is self.InteractionStyle.UNKNOWN
|
||||
or self.button_label_positive is self.ButtonLabel.UNKNOWN
|
||||
or self.button_label_negative is self.ButtonLabel.UNKNOWN
|
||||
or any(
|
||||
c.get_type_id() is BasicClientUIComponentTypeID.UNKNOWN
|
||||
for c in self.components
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ClientUIWrapper:
|
||||
"""Wrapper for a ClientUI and its common data."""
|
||||
|
||||
id: Annotated[str, IOAttrs('i')]
|
||||
createtime: Annotated[datetime.datetime, IOAttrs('c')]
|
||||
ui: Annotated[ClientUI, IOAttrs('e')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class InboxRequestMessage(Message):
|
||||
"""Message requesting our inbox."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [InboxRequestResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class InboxRequestResponse(Response):
|
||||
"""Here's that inbox contents you asked for, boss."""
|
||||
|
||||
wrappers: Annotated[list[ClientUIWrapper], IOAttrs('w')]
|
||||
|
||||
# Printable error if something goes wrong.
|
||||
error: Annotated[str | None, IOAttrs('e')] = None
|
||||
|
||||
|
||||
class ClientUIAction(Enum):
|
||||
"""Types of actions we can run."""
|
||||
|
||||
BUTTON_PRESS_POSITIVE = 'p'
|
||||
BUTTON_PRESS_NEGATIVE = 'n'
|
||||
|
||||
|
||||
class ClientEffectTypeID(Enum):
|
||||
"""Type ID for each of our subclasses."""
|
||||
|
||||
UNKNOWN = 'u'
|
||||
SCREEN_MESSAGE = 'm'
|
||||
SOUND = 's'
|
||||
DELAY = 'd'
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# 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')]
|
||||
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 ClientEffectDelay(ClientEffect):
|
||||
"""Delay effect processing."""
|
||||
|
||||
seconds: Annotated[float, IOAttrs('s')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> ClientEffectTypeID:
|
||||
return ClientEffectTypeID.DELAY
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ClientUIActionMessage(Message):
|
||||
"""Do something to a client ui."""
|
||||
|
||||
id: Annotated[str, IOAttrs('i')]
|
||||
action: Annotated[ClientUIAction, IOAttrs('a')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [ClientUIActionResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ClientUIActionResponse(Response):
|
||||
"""Did something to that inbox entry, boss."""
|
||||
|
||||
class ErrorType(Enum):
|
||||
"""Types of errors that may have occurred."""
|
||||
|
||||
# Probably a future error type we don't recognize.
|
||||
UNKNOWN = 'u'
|
||||
|
||||
# Something went wrong on the server, but specifics are not
|
||||
# relevant.
|
||||
INTERNAL = 'i'
|
||||
|
||||
# The entry expired on the server. In various cases such as 'ok'
|
||||
# buttons this can generally be ignored.
|
||||
EXPIRED = 'e'
|
||||
|
||||
error_type: Annotated[
|
||||
ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN)
|
||||
]
|
||||
|
||||
# User facing error message in the case of errors.
|
||||
error_message: Annotated[str | None, IOAttrs('em')]
|
||||
|
||||
effects: Annotated[list[ClientEffect], IOAttrs('fx')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ScoreSubmitMessage(Message):
|
||||
"""Let the server know we got some score in something."""
|
||||
|
||||
score_token: Annotated[str, IOAttrs('t')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [ScoreSubmitResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ScoreSubmitResponse(Response):
|
||||
"""Did something to that inbox entry, boss."""
|
||||
|
||||
# Things we should show on our end.
|
||||
effects: Annotated[list[ClientEffect], IOAttrs('fx')]
|
||||
27
dist/ba_data/python/bacommon/cloud.py
vendored
27
dist/ba_data/python/bacommon/cloud.py
vendored
|
|
@ -3,9 +3,10 @@
|
|||
"""Functionality related to cloud functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Annotated, override
|
||||
from enum import Enum
|
||||
|
||||
from efro.message import Message, Response
|
||||
from efro.dataclassio import ioprepped, IOAttrs
|
||||
|
|
@ -299,27 +300,3 @@ class StoreQueryResponse(Response):
|
|||
|
||||
available_purchases: Annotated[list[Purchase], IOAttrs('p')]
|
||||
token_info_url: Annotated[str, IOAttrs('tiu')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class BSPrivatePartyMessage(Message):
|
||||
"""Message asking about info we need for private-party UI."""
|
||||
|
||||
need_datacode: Annotated[bool, IOAttrs('d')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [BSPrivatePartyResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class BSPrivatePartyResponse(Response):
|
||||
"""Here's that private party UI info you asked for, boss."""
|
||||
|
||||
success: Annotated[bool, IOAttrs('s')]
|
||||
tokens: Annotated[int, IOAttrs('t')]
|
||||
gold_pass: Annotated[bool, IOAttrs('g')]
|
||||
datacode: Annotated[str | None, IOAttrs('d')]
|
||||
|
|
|
|||
212
dist/ba_data/python/bacommon/loggercontrol.py
vendored
Normal file
212
dist/ba_data/python/bacommon/loggercontrol.py
vendored
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""System for managing loggers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from efro.dataclassio import ioprepped, IOAttrs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self, Sequence
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class LoggerControlConfig:
|
||||
"""A logging level configuration that applies to all loggers.
|
||||
|
||||
Any loggers not explicitly contained in the configuration will be
|
||||
set to NOTSET.
|
||||
"""
|
||||
|
||||
# Logger names mapped to log-level values (from system logging
|
||||
# module).
|
||||
levels: Annotated[dict[str, int], IOAttrs('l', store_default=False)] = (
|
||||
field(default_factory=dict)
|
||||
)
|
||||
|
||||
def apply(
|
||||
self,
|
||||
*,
|
||||
warn_unexpected_loggers: bool = False,
|
||||
warn_missing_loggers: bool = False,
|
||||
ignore_log_prefixes: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Apply the config to all Python loggers.
|
||||
|
||||
If 'warn_unexpected_loggers' is True, warnings will be issues for
|
||||
any loggers not explicitly covered by the config. This is useful
|
||||
to help ensure controls for all possible loggers are present in
|
||||
a UI/etc.
|
||||
|
||||
If 'warn_missing_loggers' is True, warnings will be issued for
|
||||
any loggers present in the config that are not found at apply time.
|
||||
This can be useful for pruning settings for no longer used loggers.
|
||||
|
||||
Warnings for any log names beginning with any strings in
|
||||
'ignore_log_prefixes' will be suppressed. This can allow
|
||||
ignoring loggers associated with submodules for a given package
|
||||
and instead presenting only a top level logger (or none at all).
|
||||
"""
|
||||
if ignore_log_prefixes is None:
|
||||
ignore_log_prefixes = []
|
||||
|
||||
existinglognames = (
|
||||
set(['root']) | logging.root.manager.loggerDict.keys()
|
||||
)
|
||||
|
||||
# First issue any warnings they want.
|
||||
if warn_unexpected_loggers:
|
||||
for logname in sorted(existinglognames):
|
||||
if logname not in self.levels and not any(
|
||||
logname.startswith(pre) for pre in ignore_log_prefixes
|
||||
):
|
||||
logging.warning(
|
||||
'Found a logger not covered by LoggerControlConfig:'
|
||||
" '%s'.",
|
||||
logname,
|
||||
)
|
||||
if warn_missing_loggers:
|
||||
for logname in sorted(self.levels.keys()):
|
||||
if logname not in existinglognames and not any(
|
||||
logname.startswith(pre) for pre in ignore_log_prefixes
|
||||
):
|
||||
logging.warning(
|
||||
'Logger covered by LoggerControlConfig does not exist:'
|
||||
' %s.',
|
||||
logname,
|
||||
)
|
||||
|
||||
# First, update levels for all existing loggers.
|
||||
for logname in existinglognames:
|
||||
logger = logging.getLogger(logname)
|
||||
level = self.levels.get(logname)
|
||||
if level is None:
|
||||
level = logging.NOTSET
|
||||
logger.setLevel(level)
|
||||
|
||||
# Next, assign levels to any loggers that don't exist.
|
||||
for logname, level in self.levels.items():
|
||||
if logname not in existinglognames:
|
||||
logging.getLogger(logname).setLevel(level)
|
||||
|
||||
def sanity_check_effective_levels(self) -> None:
|
||||
"""Checks existing loggers to make sure they line up with us.
|
||||
|
||||
This can be called periodically to ensure that a control-config
|
||||
is properly driving log levels and that nothing else is changing
|
||||
them behind our back.
|
||||
"""
|
||||
|
||||
existinglognames = (
|
||||
set(['root']) | logging.root.manager.loggerDict.keys()
|
||||
)
|
||||
for logname in existinglognames:
|
||||
logger = logging.getLogger(logname)
|
||||
if logger.getEffectiveLevel() != self.get_effective_level(logname):
|
||||
logging.error(
|
||||
'loggercontrol effective-level sanity check failed;'
|
||||
' expected logger %s to have effective level %s'
|
||||
' but it has %s.',
|
||||
logname,
|
||||
logging.getLevelName(self.get_effective_level(logname)),
|
||||
logging.getLevelName(logger.getEffectiveLevel()),
|
||||
)
|
||||
|
||||
def get_effective_level(self, logname: str) -> int:
|
||||
"""Given a log name, predict its level if this config is applied."""
|
||||
splits = logname.split('.')
|
||||
|
||||
splen = len(splits)
|
||||
for i in range(splen):
|
||||
subname = '.'.join(splits[: splen - i])
|
||||
thisval = self.levels.get(subname)
|
||||
if thisval is not None and thisval != logging.NOTSET:
|
||||
return thisval
|
||||
|
||||
# Haven't found anything; just return root value.
|
||||
thisval = self.levels.get('root')
|
||||
return (
|
||||
logging.DEBUG
|
||||
if thisval is None
|
||||
else logging.DEBUG if thisval == logging.NOTSET else thisval
|
||||
)
|
||||
|
||||
def would_make_changes(self) -> bool:
|
||||
"""Return whether calling apply would change anything."""
|
||||
|
||||
existinglognames = (
|
||||
set(['root']) | logging.root.manager.loggerDict.keys()
|
||||
)
|
||||
|
||||
# Return True if we contain any nonexistent loggers. Even if
|
||||
# we wouldn't change their level, the fact that we'd create
|
||||
# them still counts as a difference.
|
||||
if any(
|
||||
logname not in existinglognames for logname in self.levels.keys()
|
||||
):
|
||||
return True
|
||||
|
||||
# Now go through all existing loggers and return True if we
|
||||
# would change their level.
|
||||
for logname in existinglognames:
|
||||
logger = logging.getLogger(logname)
|
||||
level = self.levels.get(logname)
|
||||
if level is None:
|
||||
level = logging.NOTSET
|
||||
if logger.level != level:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def diff(self, baseconfig: LoggerControlConfig) -> LoggerControlConfig:
|
||||
"""Return a config containing only changes compared to a base config.
|
||||
|
||||
Note that this omits all NOTSET values that resolve to NOTSET in
|
||||
the base config.
|
||||
|
||||
This diffed config can later be used with apply_diff() against the
|
||||
base config to recreate the state represented by self.
|
||||
"""
|
||||
cls = type(self)
|
||||
config = cls()
|
||||
for loggername, level in self.levels.items():
|
||||
baselevel = baseconfig.levels.get(loggername, logging.NOTSET)
|
||||
if level != baselevel:
|
||||
config.levels[loggername] = level
|
||||
return config
|
||||
|
||||
def apply_diff(
|
||||
self, diffconfig: LoggerControlConfig
|
||||
) -> LoggerControlConfig:
|
||||
"""Apply a diff config to ourself.
|
||||
|
||||
Note that values that resolve to NOTSET are left intact in the
|
||||
output config. This is so all loggers expected by either the
|
||||
base or diff config to exist can be created if desired/etc.
|
||||
"""
|
||||
cls = type(self)
|
||||
|
||||
# Create a new config (with an indepenent levels dict copy).
|
||||
config = cls(levels=dict(self.levels))
|
||||
|
||||
# Overlay the diff levels dict onto our new one.
|
||||
config.levels.update(diffconfig.levels)
|
||||
|
||||
# Note: we do NOT prune NOTSET values here. This is so all
|
||||
# loggers mentioned in the base config get created if we are
|
||||
# applied, even if they are assigned a default level.
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def from_current_loggers(cls) -> Self:
|
||||
"""Build a config from the current set of loggers."""
|
||||
lognames = ['root'] + sorted(logging.root.manager.loggerDict)
|
||||
config = cls()
|
||||
for logname in lognames:
|
||||
config.levels[logname] = logging.getLogger(logname).level
|
||||
return config
|
||||
27
dist/ba_data/python/bacommon/logging.py
vendored
Normal file
27
dist/ba_data/python/bacommon/logging.py
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Logging functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from bacommon.loggercontrol import LoggerControlConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
def get_base_logger_control_config_client() -> LoggerControlConfig:
|
||||
"""Return the logger-control-config used by the ballistica client.
|
||||
|
||||
This should remain consistent since local logger configurations
|
||||
are stored relative to this.
|
||||
"""
|
||||
|
||||
# By default, go with WARNING on everything to keep things mostly
|
||||
# clean but show INFO for ba.app to get basic app startup messages
|
||||
# and whatnot.
|
||||
return LoggerControlConfig(
|
||||
levels={'root': logging.WARNING, 'ba.app': logging.INFO}
|
||||
)
|
||||
|
|
@ -186,6 +186,11 @@ class ServerConfig:
|
|||
# involving leaving and rejoining or switching teams rapidly.
|
||||
player_rejoin_cooldown: float = 10.0
|
||||
|
||||
# Log levels for particular loggers, overriding the engine's
|
||||
# defaults. Valid values are NOTSET, DEBUG, INFO, WARNING, ERROR, or
|
||||
# CRITICAL.
|
||||
log_levels: dict[str, str] | None = None
|
||||
|
||||
|
||||
# NOTE: as much as possible, communication from the server-manager to
|
||||
# the child-process should go through these and not ad-hoc Python string
|
||||
|
|
|
|||
129
dist/ba_data/python/baenv.py
vendored
129
dist/ba_data/python/baenv.py
vendored
|
|
@ -18,6 +18,7 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -27,7 +28,7 @@ import __main__
|
|||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from efro.log import LogHandler
|
||||
from efro.logging import LogHandler
|
||||
|
||||
# IMPORTANT - It is likely (and in some cases expected) that this
|
||||
# module's code will be exec'ed multiple times. This is because it is
|
||||
|
|
@ -52,7 +53,7 @@ if TYPE_CHECKING:
|
|||
|
||||
# Build number and version of the ballistica binary we expect to be
|
||||
# using.
|
||||
TARGET_BALLISTICA_BUILD = 21949
|
||||
TARGET_BALLISTICA_BUILD = 22278
|
||||
TARGET_BALLISTICA_VERSION = '1.7.37'
|
||||
|
||||
|
||||
|
|
@ -87,14 +88,13 @@ class EnvConfig:
|
|||
# stderr into the engine so they show up on in-app consoles, etc.
|
||||
log_handler: LogHandler | None
|
||||
|
||||
# Initial data from the ballisticakit-config.json file. This is
|
||||
# passed mostly as an optimization to avoid reading the same config
|
||||
# file twice, since config data is first needed in baenv and next in
|
||||
# the engine. It will be cleared after passing it to the app's
|
||||
# config management subsystem and should not be accessed by any
|
||||
# other code.
|
||||
# Initial data from the config.json file in the config dir. The
|
||||
# config file is parsed by
|
||||
initial_app_config: Any
|
||||
|
||||
# Timestamp when we first started doing stuff.
|
||||
launch_time: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class _EnvGlobals:
|
||||
|
|
@ -156,6 +156,7 @@ def get_config() -> EnvConfig:
|
|||
|
||||
|
||||
def configure(
|
||||
*,
|
||||
config_dir: str | None = None,
|
||||
data_dir: str | None = None,
|
||||
user_python_dir: str | None = None,
|
||||
|
|
@ -171,6 +172,11 @@ def configure(
|
|||
are imported; the environment is locked in as soon as that happens.
|
||||
"""
|
||||
|
||||
# Measure when we start doing this stuff. We plug this in to show
|
||||
# relative times in our log timestamp displays and also pass this to
|
||||
# the engine to do the same there.
|
||||
launch_time = time.time()
|
||||
|
||||
envglobals = _EnvGlobals.get()
|
||||
|
||||
# Keep track of whether we've been *called*, not whether a config
|
||||
|
|
@ -205,11 +211,19 @@ def configure(
|
|||
config_dir,
|
||||
)
|
||||
|
||||
# The second thing we do is set up our logging system and pipe
|
||||
# Python's stdout/stderr into it. At this point we can at least
|
||||
# debug problems on systems where native stdout/stderr is not easily
|
||||
# accessible such as Android.
|
||||
log_handler = _setup_logging() if setup_logging else None
|
||||
# Set up our log-handler and pipe Python's stdout/stderr into it.
|
||||
# Later, once the engine comes up, the handler will feed its logs
|
||||
# (including cached history) to the os-specific output location.
|
||||
# This means anything printed or logged at this point forward should
|
||||
# be visible on all platforms.
|
||||
log_handler = _create_log_handler(launch_time) if setup_logging else None
|
||||
|
||||
# Load the raw app-config dict.
|
||||
app_config = _read_app_config(os.path.join(config_dir, 'config.json'))
|
||||
|
||||
# Set logging levels to stored values or defaults.
|
||||
if setup_logging:
|
||||
_set_log_levels(app_config)
|
||||
|
||||
# We want to always be run in UTF-8 mode; complain if we're not.
|
||||
if sys.flags.utf8_mode != 1:
|
||||
|
|
@ -234,10 +248,46 @@ def configure(
|
|||
site_python_dir=site_python_dir,
|
||||
log_handler=log_handler,
|
||||
is_user_app_python_dir=is_user_app_python_dir,
|
||||
initial_app_config=None,
|
||||
initial_app_config=app_config,
|
||||
launch_time=launch_time,
|
||||
)
|
||||
|
||||
|
||||
def _read_app_config(config_file_path: str) -> dict:
|
||||
"""Read the app config."""
|
||||
import json
|
||||
|
||||
config: dict | Any
|
||||
config_contents = ''
|
||||
try:
|
||||
if os.path.exists(config_file_path):
|
||||
with open(config_file_path, encoding='utf-8') as infile:
|
||||
config_contents = infile.read()
|
||||
config = json.loads(config_contents)
|
||||
if not isinstance(config, dict):
|
||||
raise RuntimeError('Got non-dict for config root.')
|
||||
else:
|
||||
config = {}
|
||||
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"Error reading config file '%s'.\n"
|
||||
"Backing up broken config to'%s.broken'.",
|
||||
config_file_path,
|
||||
config_file_path,
|
||||
)
|
||||
|
||||
try:
|
||||
import shutil
|
||||
|
||||
shutil.copyfile(config_file_path, config_file_path + '.broken')
|
||||
except Exception:
|
||||
logging.exception('Error copying broken config.')
|
||||
config = {}
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _calc_data_dir(data_dir: str | None) -> str:
|
||||
if data_dir is None:
|
||||
# To calc default data_dir, we assume this module was imported
|
||||
|
|
@ -261,23 +311,58 @@ def _calc_data_dir(data_dir: str | None) -> str:
|
|||
return data_dir
|
||||
|
||||
|
||||
def _setup_logging() -> LogHandler:
|
||||
from efro.log import setup_logging, LogLevel
|
||||
def _create_log_handler(launch_time: float) -> LogHandler:
|
||||
from efro.logging import setup_logging, LogLevel
|
||||
|
||||
# TODO: should set this up with individual loggers under a top level
|
||||
# 'ba' logger, and at that point we can kill off the
|
||||
# suppress_non_root_debug option since we'll only ever need to set
|
||||
# 'ba' to DEBUG at most.
|
||||
log_handler = setup_logging(
|
||||
log_path=None,
|
||||
level=LogLevel.DEBUG,
|
||||
suppress_non_root_debug=True,
|
||||
level=LogLevel.INFO,
|
||||
log_stdout_stderr=True,
|
||||
cache_size_limit=1024 * 1024,
|
||||
launch_time=launch_time,
|
||||
)
|
||||
return log_handler
|
||||
|
||||
|
||||
def _set_log_levels(app_config: dict) -> None:
|
||||
|
||||
from bacommon.logging import get_base_logger_control_config_client
|
||||
from bacommon.loggercontrol import LoggerControlConfig
|
||||
|
||||
try:
|
||||
config = app_config.get('Log Levels', None)
|
||||
|
||||
if config is None:
|
||||
get_base_logger_control_config_client().apply()
|
||||
return
|
||||
|
||||
# Make sure data is expected types/values since this is user
|
||||
# editable.
|
||||
valid_levels = {
|
||||
logging.NOTSET,
|
||||
logging.DEBUG,
|
||||
logging.INFO,
|
||||
logging.WARNING,
|
||||
logging.ERROR,
|
||||
logging.CRITICAL,
|
||||
}
|
||||
for logname, loglevel in config.items():
|
||||
if (
|
||||
not isinstance(logname, str)
|
||||
or not logname
|
||||
or not isinstance(loglevel, int)
|
||||
or not loglevel in valid_levels
|
||||
):
|
||||
raise ValueError("Invalid 'Log Levels' data read from config.")
|
||||
|
||||
get_base_logger_control_config_client().apply_diff(
|
||||
LoggerControlConfig(levels=config)
|
||||
).apply()
|
||||
|
||||
except Exception:
|
||||
logging.exception('Error setting log levels.')
|
||||
|
||||
|
||||
def _setup_certs(contains_python_dist: bool) -> None:
|
||||
# In situations where we're bringing our own Python, let's also
|
||||
# provide our own root certs so ssl works. We can consider
|
||||
|
|
|
|||
12
dist/ba_data/python/baplus/__init__.py
vendored
12
dist/ba_data/python/baplus/__init__.py
vendored
|
|
@ -5,23 +5,23 @@
|
|||
This code concerns sensitive things like accounts and master-server
|
||||
communication so the native C++ parts of it remain closed. Native
|
||||
precompiled static libraries of this portion are provided for those who
|
||||
want to compile the rest of the engine, and a fully open-source engine
|
||||
can also be built by removing this 'plus' feature-set.
|
||||
want to compile the rest of the engine, or a fully open-source app
|
||||
can also be built by removing this feature-set.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Note: there's not much here.
|
||||
# All comms with this feature-set should go through app.plus.
|
||||
# Note: there's not much here. Most interaction with this feature-set
|
||||
# should go through ba*.app.plus.
|
||||
|
||||
import logging
|
||||
|
||||
from baplus._cloud import CloudSubsystem
|
||||
from baplus._subsystem import PlusSubsystem
|
||||
from baplus._appsubsystem import PlusAppSubsystem
|
||||
|
||||
__all__ = [
|
||||
'CloudSubsystem',
|
||||
'PlusSubsystem',
|
||||
'PlusAppSubsystem',
|
||||
]
|
||||
|
||||
# Sanity check: we want to keep ballistica's dependencies and
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@ import _baplus
|
|||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
||||
import bacommon.bs
|
||||
from babase import AccountV2Subsystem
|
||||
|
||||
from baplus._cloud import CloudSubsystem
|
||||
|
||||
|
||||
class PlusSubsystem(AppSubsystem):
|
||||
class PlusAppSubsystem(AppSubsystem):
|
||||
"""Subsystem for plus functionality in the app.
|
||||
|
||||
The single shared instance of this app can be accessed at
|
||||
|
|
@ -40,7 +41,6 @@ class PlusSubsystem(AppSubsystem):
|
|||
_baplus.on_app_loading()
|
||||
self.accounts.on_app_loading()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
@staticmethod
|
||||
def add_v1_account_transaction(
|
||||
transaction: dict, callback: Callable | None = None
|
||||
|
|
@ -66,9 +66,9 @@ class PlusSubsystem(AppSubsystem):
|
|||
return _baplus.get_master_server_address(source, version)
|
||||
|
||||
@staticmethod
|
||||
def get_news_show() -> str:
|
||||
def get_classic_news_show() -> str:
|
||||
"""(internal)"""
|
||||
return _baplus.get_news_show()
|
||||
return _baplus.get_classic_news_show()
|
||||
|
||||
@staticmethod
|
||||
def get_price(item: str) -> str | None:
|
||||
|
|
@ -76,14 +76,14 @@ class PlusSubsystem(AppSubsystem):
|
|||
return _baplus.get_price(item)
|
||||
|
||||
@staticmethod
|
||||
def get_purchased(item: str) -> bool:
|
||||
def get_v1_account_product_purchased(item: str) -> bool:
|
||||
"""(internal)"""
|
||||
return _baplus.get_purchased(item)
|
||||
return _baplus.get_v1_account_product_purchased(item)
|
||||
|
||||
@staticmethod
|
||||
def get_purchases_state() -> int:
|
||||
def get_v1_account_product_purchases_state() -> int:
|
||||
"""(internal)"""
|
||||
return _baplus.get_purchases_state()
|
||||
return _baplus.get_v1_account_product_purchases_state()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_display_string(full: bool = True) -> str:
|
||||
|
|
@ -129,7 +129,7 @@ class PlusSubsystem(AppSubsystem):
|
|||
def get_v1_account_ticket_count() -> int:
|
||||
"""(internal)
|
||||
|
||||
Returns the number of tickets for the current account.
|
||||
Return the number of tickets for the current account.
|
||||
"""
|
||||
return _baplus.get_v1_account_ticket_count()
|
||||
|
||||
|
|
@ -221,6 +221,7 @@ class PlusSubsystem(AppSubsystem):
|
|||
name: Any,
|
||||
score: int | None,
|
||||
callback: Callable,
|
||||
*,
|
||||
order: str = 'increasing',
|
||||
tournament_id: str | None = None,
|
||||
score_type: str = 'points',
|
||||
114
dist/ba_data/python/baplus/_cloud.py
vendored
114
dist/ba_data/python/baplus/_cloud.py
vendored
|
|
@ -7,6 +7,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, overload
|
||||
|
||||
from efro.call import CallbackSet
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -14,8 +15,8 @@ if TYPE_CHECKING:
|
|||
|
||||
from efro.message import Message, Response
|
||||
import bacommon.cloud
|
||||
import bacommon.bs
|
||||
|
||||
DEBUG_LOG = False
|
||||
|
||||
# TODO: Should make it possible to define a protocol in bacommon.cloud and
|
||||
# autogenerate this. That would give us type safety between this and
|
||||
|
|
@ -25,6 +26,12 @@ DEBUG_LOG = False
|
|||
class CloudSubsystem(babase.AppSubsystem):
|
||||
"""Manages communication with cloud components."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.on_connectivity_changed_callbacks: CallbackSet[
|
||||
Callable[[bool], None]
|
||||
] = CallbackSet()
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""Property equivalent of CloudSubsystem.is_connected()."""
|
||||
|
|
@ -40,15 +47,17 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
|
||||
def on_connectivity_changed(self, connected: bool) -> None:
|
||||
"""Called when cloud connectivity state changes."""
|
||||
if DEBUG_LOG:
|
||||
logging.debug('CloudSubsystem: Connectivity is now %s.', connected)
|
||||
babase.balog.debug('Connectivity is now %s.', connected)
|
||||
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
# Inform things that use this.
|
||||
# (TODO: should generalize this into some sort of registration system)
|
||||
plus.accounts.on_cloud_connectivity_changed(connected)
|
||||
# Fire any registered callbacks for this.
|
||||
for call in self.on_connectivity_changed_callbacks.getcalls():
|
||||
try:
|
||||
call(connected)
|
||||
except Exception:
|
||||
logging.exception('Error in connectivity-changed callback.')
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
|
|
@ -112,9 +121,54 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.BSPrivatePartyMessage,
|
||||
msg: bacommon.bs.PrivatePartyMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.cloud.BSPrivatePartyResponse | Exception], None
|
||||
[bacommon.bs.PrivatePartyResponse | Exception], None
|
||||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.bs.InboxRequestMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.bs.InboxRequestResponse | Exception], None
|
||||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.bs.ClientUIActionMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.bs.ClientUIActionResponse | Exception], None
|
||||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.bs.ChestInfoMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.bs.ChestInfoResponse | Exception], None
|
||||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.bs.ChestActionMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.bs.ChestActionResponse | Exception], None
|
||||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.bs.ScoreSubmitMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.bs.ScoreSubmitResponse | Exception], None
|
||||
],
|
||||
) -> None: ...
|
||||
|
||||
|
|
@ -128,14 +182,8 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
The provided on_response call will be run in the logic thread
|
||||
and passed either the response or the error that occurred.
|
||||
"""
|
||||
|
||||
del msg # Unused.
|
||||
|
||||
babase.pushcall(
|
||||
babase.Call(
|
||||
on_response,
|
||||
RuntimeError('Cloud functionality is not available.'),
|
||||
)
|
||||
raise NotImplementedError(
|
||||
'Cloud functionality is not present in this build.'
|
||||
)
|
||||
|
||||
@overload
|
||||
|
|
@ -158,7 +206,9 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
|
||||
Must be called from a background thread.
|
||||
"""
|
||||
raise RuntimeError('Cloud functionality is not available.')
|
||||
raise NotImplementedError(
|
||||
'Cloud functionality is not present in this build.'
|
||||
)
|
||||
|
||||
@overload
|
||||
async def send_message_async(
|
||||
|
|
@ -175,7 +225,35 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
|
||||
Must be called from the logic thread.
|
||||
"""
|
||||
raise RuntimeError('Cloud functionality is not available.')
|
||||
raise NotImplementedError(
|
||||
'Cloud functionality is not present in this build.'
|
||||
)
|
||||
|
||||
def subscribe_test(
|
||||
self, updatecall: Callable[[int | None], None]
|
||||
) -> babase.CloudSubscription:
|
||||
"""Subscribe to some test data."""
|
||||
raise NotImplementedError(
|
||||
'Cloud functionality is not present in this build.'
|
||||
)
|
||||
|
||||
def subscribe_classic_account_data(
|
||||
self,
|
||||
updatecall: Callable[[bacommon.bs.ClassicAccountLiveData], None],
|
||||
) -> babase.CloudSubscription:
|
||||
"""Subscribe to classic account data."""
|
||||
raise NotImplementedError(
|
||||
'Cloud functionality is not present in this build.'
|
||||
)
|
||||
|
||||
def unsubscribe(self, subscription_id: int) -> None:
|
||||
"""Unsubscribe from some subscription.
|
||||
|
||||
Do not call this manually; it is called by CloudSubscription.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'Cloud functionality is not present in this build.'
|
||||
)
|
||||
|
||||
|
||||
def cloud_console_exec(code: str) -> None:
|
||||
|
|
|
|||
8
dist/ba_data/python/bascenev1/__init__.py
vendored
8
dist/ba_data/python/bascenev1/__init__.py
vendored
|
|
@ -1,8 +1,8 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Ballistica scene api version 1. Basically all gameplay related code."""
|
||||
"""Gameplay-centric api for classic BombSquad."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
|
||||
# The stuff we expose here at the top level is our 'public' api for use
|
||||
# from other modules/packages. Code *within* this package should import
|
||||
|
|
@ -18,6 +18,7 @@ import logging
|
|||
|
||||
from efro.util import set_canonical_module_names
|
||||
from babase import (
|
||||
add_clean_frame_callback,
|
||||
app,
|
||||
AppIntent,
|
||||
AppIntentDefault,
|
||||
|
|
@ -149,7 +150,6 @@ from _bascenev1 import (
|
|||
from bascenev1._activity import Activity
|
||||
from bascenev1._activitytypes import JoinActivity, ScoreScreenActivity
|
||||
from bascenev1._actor import Actor
|
||||
from bascenev1._appmode import SceneV1AppMode
|
||||
from bascenev1._campaign import init_campaigns, Campaign
|
||||
from bascenev1._collision import Collision, getcollision
|
||||
from bascenev1._coopgame import CoopGameActivity
|
||||
|
|
@ -249,6 +249,7 @@ __all__ = [
|
|||
'Actor',
|
||||
'animate',
|
||||
'animate_array',
|
||||
'add_clean_frame_callback',
|
||||
'app',
|
||||
'AppIntent',
|
||||
'AppIntentDefault',
|
||||
|
|
@ -410,7 +411,6 @@ __all__ = [
|
|||
'seek_replay',
|
||||
'safecolor',
|
||||
'screenmessage',
|
||||
'SceneV1AppMode',
|
||||
'ScoreConfig',
|
||||
'ScoreScreenActivity',
|
||||
'ScoreType',
|
||||
|
|
|
|||
60
dist/ba_data/python/bascenev1/_appmode.py
vendored
60
dist/ba_data/python/bascenev1/_appmode.py
vendored
|
|
@ -1,60 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides AppMode functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from bacommon.app import AppExperience
|
||||
from babase import (
|
||||
app,
|
||||
AppMode,
|
||||
AppIntentExec,
|
||||
AppIntentDefault,
|
||||
invoke_main_menu,
|
||||
)
|
||||
|
||||
import _bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from babase import AppIntent
|
||||
|
||||
|
||||
class SceneV1AppMode(AppMode):
|
||||
"""Our app-mode."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_app_experience(cls) -> AppExperience:
|
||||
return AppExperience.MELEE
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def _supports_intent(cls, intent: AppIntent) -> bool:
|
||||
# We support default and exec intents currently.
|
||||
return isinstance(intent, AppIntentExec | AppIntentDefault)
|
||||
|
||||
@override
|
||||
def handle_intent(self, intent: AppIntent) -> None:
|
||||
if isinstance(intent, AppIntentExec):
|
||||
_bascenev1.handle_app_intent_exec(intent.code)
|
||||
return
|
||||
assert isinstance(intent, AppIntentDefault)
|
||||
_bascenev1.handle_app_intent_default()
|
||||
|
||||
@override
|
||||
def on_activate(self) -> None:
|
||||
# Let the native layer do its thing.
|
||||
_bascenev1.on_app_mode_activate()
|
||||
|
||||
@override
|
||||
def on_deactivate(self) -> None:
|
||||
# Let the native layer do its thing.
|
||||
_bascenev1.on_app_mode_deactivate()
|
||||
|
||||
@override
|
||||
def on_app_active_changed(self) -> None:
|
||||
# If we've gone inactive, bring up the main menu, which has the
|
||||
# side effect of pausing the action (when possible).
|
||||
if not app.active:
|
||||
invoke_main_menu()
|
||||
147
dist/ba_data/python/bascenev1/_gameactivity.py
vendored
147
dist/ba_data/python/bascenev1/_gameactivity.py
vendored
|
|
@ -64,36 +64,6 @@ class GameActivity(Activity[PlayerT, TeamT]):
|
|||
# (unless overridden by the map).
|
||||
default_music: bascenev1.MusicType | None = None
|
||||
|
||||
@classmethod
|
||||
def create_settings_ui(
|
||||
cls,
|
||||
sessiontype: type[bascenev1.Session],
|
||||
settings: dict | None,
|
||||
completion_call: Callable[[dict | None], None],
|
||||
) -> None:
|
||||
"""Launch an in-game UI to configure settings for a game type.
|
||||
|
||||
'sessiontype' should be the bascenev1.Session class the game will
|
||||
be used in.
|
||||
|
||||
'settings' should be an existing settings dict (implies 'edit'
|
||||
ui mode) or None (implies 'add' ui mode).
|
||||
|
||||
'completion_call' will be called with a filled-out settings dict on
|
||||
success or None on cancel.
|
||||
|
||||
Generally subclasses don't need to override this; if they override
|
||||
bascenev1.GameActivity.get_available_settings() and
|
||||
bascenev1.GameActivity.get_supported_maps() they can just rely on
|
||||
the default implementation here which calls those methods.
|
||||
"""
|
||||
assert babase.app.classic is not None
|
||||
delegate = babase.app.classic.delegate
|
||||
assert delegate is not None
|
||||
delegate.create_default_game_settings_ui(
|
||||
cls, sessiontype, settings, completion_call
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def getscoreconfig(cls) -> bascenev1.ScoreConfig:
|
||||
"""Return info about game scoring setup; can be overridden by games."""
|
||||
|
|
@ -238,8 +208,6 @@ class GameActivity(Activity[PlayerT, TeamT]):
|
|||
"""Instantiate the Activity."""
|
||||
super().__init__(settings)
|
||||
|
||||
plus = babase.app.plus
|
||||
|
||||
# Holds some flattened info about the player set at the point
|
||||
# when on_begin() is called.
|
||||
self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None
|
||||
|
|
@ -269,23 +237,6 @@ class GameActivity(Activity[PlayerT, TeamT]):
|
|||
None
|
||||
)
|
||||
self._zoom_message_times: dict[int, float] = {}
|
||||
self._is_waiting_for_continue = False
|
||||
|
||||
self._continue_cost = (
|
||||
25
|
||||
if plus is None
|
||||
else plus.get_v1_account_misc_read_val('continueStartCost', 25)
|
||||
)
|
||||
self._continue_cost_mult = (
|
||||
2
|
||||
if plus is None
|
||||
else plus.get_v1_account_misc_read_val('continuesMult', 2)
|
||||
)
|
||||
self._continue_cost_offset = (
|
||||
0
|
||||
if plus is None
|
||||
else plus.get_v1_account_misc_read_val('continuesOffset', 0)
|
||||
)
|
||||
|
||||
@property
|
||||
def map(self) -> _map.Map:
|
||||
|
|
@ -392,103 +343,6 @@ class GameActivity(Activity[PlayerT, TeamT]):
|
|||
if music is not None:
|
||||
_music.setmusic(music)
|
||||
|
||||
def on_continue(self) -> None:
|
||||
"""
|
||||
This is called if a game supports and offers a continue and the player
|
||||
accepts. In this case the player should be given an extra life or
|
||||
whatever is relevant to keep the game going.
|
||||
"""
|
||||
|
||||
def _continue_choice(self, do_continue: bool) -> None:
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
self._is_waiting_for_continue = False
|
||||
if self.has_ended():
|
||||
return
|
||||
with self.context:
|
||||
if do_continue:
|
||||
_bascenev1.getsound('shieldUp').play()
|
||||
_bascenev1.getsound('cashRegister').play()
|
||||
plus.add_v1_account_transaction(
|
||||
{'type': 'CONTINUE', 'cost': self._continue_cost}
|
||||
)
|
||||
if plus is not None:
|
||||
plus.run_v1_account_transactions()
|
||||
self._continue_cost = (
|
||||
self._continue_cost * self._continue_cost_mult
|
||||
+ self._continue_cost_offset
|
||||
)
|
||||
self.on_continue()
|
||||
else:
|
||||
self.end_game()
|
||||
|
||||
def is_waiting_for_continue(self) -> bool:
|
||||
"""Returns whether or not this activity is currently waiting for the
|
||||
player to continue (or timeout)"""
|
||||
return self._is_waiting_for_continue
|
||||
|
||||
def continue_or_end_game(self) -> None:
|
||||
"""If continues are allowed, prompts the player to purchase a continue
|
||||
and calls either end_game or continue_game depending on the result
|
||||
"""
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
# pylint: disable=cyclic-import
|
||||
from bascenev1._coopsession import CoopSession
|
||||
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
continues_window = classic.continues_window
|
||||
|
||||
# Turning these off. I want to migrate towards monetization that
|
||||
# feels less pay-to-win-ish.
|
||||
allow_continues = False
|
||||
|
||||
plus = babase.app.plus
|
||||
try:
|
||||
if (
|
||||
plus is not None
|
||||
and plus.get_v1_account_misc_read_val('enableContinues', False)
|
||||
and allow_continues
|
||||
):
|
||||
session = self.session
|
||||
|
||||
# We only support continuing in non-tournament games.
|
||||
tournament_id = session.tournament_id
|
||||
if tournament_id is None:
|
||||
# We currently only support continuing in sequential
|
||||
# co-op campaigns.
|
||||
if isinstance(session, CoopSession):
|
||||
assert session.campaign is not None
|
||||
if session.campaign.sequential:
|
||||
gnode = self.globalsnode
|
||||
|
||||
# Only attempt this if we're not currently paused
|
||||
# and there appears to be no UI.
|
||||
assert babase.app.classic is not None
|
||||
hmmw = babase.app.ui_v1.has_main_menu_window()
|
||||
if not gnode.paused and not hmmw:
|
||||
self._is_waiting_for_continue = True
|
||||
with babase.ContextRef.empty():
|
||||
babase.apptimer(
|
||||
0.5,
|
||||
lambda: continues_window(
|
||||
self,
|
||||
self._continue_cost,
|
||||
continue_call=babase.WeakCall(
|
||||
self._continue_choice, True
|
||||
),
|
||||
cancel_call=babase.WeakCall(
|
||||
self._continue_choice, False
|
||||
),
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
except Exception:
|
||||
logging.exception('Error handling continues.')
|
||||
|
||||
self.end_game()
|
||||
|
||||
@override
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
|
|
@ -1277,6 +1131,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
|
|||
def show_zoom_message(
|
||||
self,
|
||||
message: babase.Lstr,
|
||||
*,
|
||||
color: Sequence[float] = (0.9, 0.4, 0.0),
|
||||
scale: float = 0.8,
|
||||
duration: float = 2.0,
|
||||
|
|
|
|||
3
dist/ba_data/python/bascenev1/_gameutils.py
vendored
3
dist/ba_data/python/bascenev1/_gameutils.py
vendored
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, NewType
|
||||
|
||||
|
|
@ -111,6 +112,7 @@ def animate_array(
|
|||
attr: str,
|
||||
size: int,
|
||||
keys: dict[float, Sequence[float]],
|
||||
*,
|
||||
loop: bool = False,
|
||||
offset: float = 0,
|
||||
) -> None:
|
||||
|
|
@ -243,7 +245,6 @@ def cameraflash(duration: float = 999.0) -> None:
|
|||
Duration is in seconds.
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
import random
|
||||
from bascenev1._nodeactor import NodeActor
|
||||
|
||||
x_spread = 10
|
||||
|
|
|
|||
1
dist/ba_data/python/bascenev1/_level.py
vendored
1
dist/ba_data/python/bascenev1/_level.py
vendored
|
|
@ -27,6 +27,7 @@ class Level:
|
|||
gametype: type[bascenev1.GameActivity],
|
||||
settings: dict,
|
||||
preview_texture_name: str,
|
||||
*,
|
||||
displayname: str | None = None,
|
||||
):
|
||||
self._name = name
|
||||
|
|
|
|||
7
dist/ba_data/python/bascenev1/_lobby.py
vendored
7
dist/ba_data/python/bascenev1/_lobby.py
vendored
|
|
@ -588,10 +588,11 @@ class Chooser:
|
|||
# Handle '_edit' as a special case.
|
||||
if profilename == '_edit' and ready:
|
||||
with babase.ContextRef.empty():
|
||||
classic.profile_browser_window(in_main_menu=False)
|
||||
|
||||
# Give their input-device UI ownership too
|
||||
# (prevent someone else from snatching it in crowded games)
|
||||
classic.profile_browser_window()
|
||||
|
||||
# Give their input-device UI ownership too (prevent
|
||||
# someone else from snatching it in crowded games).
|
||||
babase.set_ui_input_device(self._sessionplayer.inputdevice.id)
|
||||
return
|
||||
|
||||
|
|
|
|||
1
dist/ba_data/python/bascenev1/_messages.py
vendored
1
dist/ba_data/python/bascenev1/_messages.py
vendored
|
|
@ -241,6 +241,7 @@ class HitMessage:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
srcnode: bascenev1.Node | None = None,
|
||||
pos: Sequence[float] | None = None,
|
||||
velocity: Sequence[float] | None = None,
|
||||
|
|
|
|||
1
dist/ba_data/python/bascenev1/_playlist.py
vendored
1
dist/ba_data/python/bascenev1/_playlist.py
vendored
|
|
@ -21,6 +21,7 @@ PlaylistType = list[dict[str, Any]]
|
|||
def filter_playlist(
|
||||
playlist: PlaylistType,
|
||||
sessiontype: type[Session],
|
||||
*,
|
||||
add_resolved_type: bool = False,
|
||||
remove_unowned: bool = True,
|
||||
mark_unowned: bool = False,
|
||||
|
|
|
|||
1
dist/ba_data/python/bascenev1/_session.py
vendored
1
dist/ba_data/python/bascenev1/_session.py
vendored
|
|
@ -96,6 +96,7 @@ class Session:
|
|||
def __init__(
|
||||
self,
|
||||
depsets: Sequence[bascenev1.DependencySet],
|
||||
*,
|
||||
team_names: Sequence[str] | None = None,
|
||||
team_colors: Sequence[Sequence[float]] | None = None,
|
||||
min_players: int = 1,
|
||||
|
|
|
|||
2
dist/ba_data/python/bascenev1/_stats.py
vendored
2
dist/ba_data/python/bascenev1/_stats.py
vendored
|
|
@ -196,6 +196,7 @@ class PlayerRecord:
|
|||
scale2: float,
|
||||
sound2: bascenev1.Sound | None,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
from bascenev1lib.actor.popuptext import PopupText
|
||||
|
||||
# Only award this if they're still alive and we can get
|
||||
|
|
@ -341,6 +342,7 @@ class Stats:
|
|||
self,
|
||||
player: bascenev1.Player,
|
||||
base_points: int = 1,
|
||||
*,
|
||||
target: Sequence[float] | None = None,
|
||||
kill: bool = False,
|
||||
victim_player: bascenev1.Player | None = None,
|
||||
|
|
|
|||
2
dist/ba_data/python/bascenev1lib/__init__.py
vendored
2
dist/ba_data/python/bascenev1lib/__init__.py
vendored
|
|
@ -2,4 +2,4 @@
|
|||
#
|
||||
"""Library of stuff using the bascenev1 api: games, actors, etc."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import random
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from efro.util import strict_partial
|
||||
import bacommon.bs
|
||||
from bacommon.login import LoginType
|
||||
import bascenev1 as bs
|
||||
import bauiv1 as bui
|
||||
|
|
@ -19,9 +21,6 @@ from bascenev1lib.actor.zoomtext import ZoomText
|
|||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
from bauiv1lib.store.button import StoreButton
|
||||
from bauiv1lib.league.rankbutton import LeagueRankButton
|
||||
|
||||
|
||||
class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
||||
"""Score screen showing the results of a cooperative game."""
|
||||
|
|
@ -105,10 +104,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
|
||||
# Ui bits.
|
||||
self._corner_button_offs: tuple[float, float] | None = None
|
||||
self._league_rank_button: LeagueRankButton | None = None
|
||||
self._store_button_instance: StoreButton | None = None
|
||||
self._restart_button: bui.Widget | None = None
|
||||
self._update_corner_button_positions_timer: bui.AppTimer | None = None
|
||||
self._next_level_error: bs.Actor | None = None
|
||||
|
||||
# Score/gameplay bits.
|
||||
|
|
@ -207,20 +203,12 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
)
|
||||
|
||||
def _ui_menu(self) -> None:
|
||||
from bauiv1lib import specialoffer
|
||||
|
||||
if specialoffer.show_offer():
|
||||
return
|
||||
bui.containerwidget(edit=self._root_ui, transition='out_left')
|
||||
with self.context:
|
||||
bs.timer(0.1, bs.Call(bs.WeakCall(self.session.end)))
|
||||
|
||||
def _ui_restart(self) -> None:
|
||||
from bauiv1lib.tournamententry import TournamentEntryWindow
|
||||
from bauiv1lib import specialoffer
|
||||
|
||||
if specialoffer.show_offer():
|
||||
return
|
||||
|
||||
# If we're in a tournament and it looks like there's no time left,
|
||||
# disallow.
|
||||
|
|
@ -268,10 +256,6 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
self.end({'outcome': 'restart'})
|
||||
|
||||
def _ui_next(self) -> None:
|
||||
from bauiv1lib.specialoffer import show_offer
|
||||
|
||||
if show_offer():
|
||||
return
|
||||
|
||||
# If we didn't just complete this level but are choosing to play the
|
||||
# next one, set it as current (this won't happen otherwise).
|
||||
|
|
@ -331,23 +315,30 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
)
|
||||
|
||||
def _should_show_worlds_best_button(self) -> bool:
|
||||
|
||||
# Old high score lists webpage for tourneys seems broken
|
||||
# (looking at meteor shower at least).
|
||||
if self.session.tournament_id is not None:
|
||||
return False
|
||||
|
||||
# Link is too complicated to display with no browser.
|
||||
return bui.is_browser_likely_available()
|
||||
|
||||
def request_ui(self) -> None:
|
||||
"""Set up a callback to show our UI at the next opportune time."""
|
||||
assert bui.app.classic is not None
|
||||
classic = bui.app.classic
|
||||
assert classic is not None
|
||||
# We don't want to just show our UI in case the user already has the
|
||||
# main menu up, so instead we add a callback for when the menu
|
||||
# closes; if we're still alive, we'll come up then.
|
||||
# If there's no main menu this gets called immediately.
|
||||
bui.app.ui_v1.add_main_menu_close_callback(bui.WeakCall(self.show_ui))
|
||||
classic.add_main_menu_close_callback(bui.WeakCall(self.show_ui))
|
||||
|
||||
def show_ui(self) -> None:
|
||||
"""Show the UI for restarting, playing the next Level, etc."""
|
||||
# pylint: disable=too-many-locals
|
||||
from bauiv1lib.store.button import StoreButton
|
||||
from bauiv1lib.league.rankbutton import LeagueRankButton
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
assert bui.app.classic is not None
|
||||
|
||||
|
|
@ -361,11 +352,14 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
return
|
||||
|
||||
rootc = self._root_ui = bui.containerwidget(
|
||||
size=(0, 0), transition='in_right'
|
||||
size=(0, 0),
|
||||
transition='in_right',
|
||||
toolbar_visibility='no_menu_minimal',
|
||||
)
|
||||
|
||||
h_offs = 7.0
|
||||
v_offs = -280.0
|
||||
v_offs2 = -236.0
|
||||
|
||||
# We wanna prevent controllers users from popping up browsers
|
||||
# or game-center widgets in cases where they can't easily get back
|
||||
|
|
@ -393,7 +387,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
bui.buttonwidget(
|
||||
parent=rootc,
|
||||
color=(0.45, 0.4, 0.5),
|
||||
position=(160, v_offs + 480),
|
||||
position=(240, v_offs2 + 439),
|
||||
size=(350, 62),
|
||||
label=(
|
||||
bui.Lstr(resource='tournamentStandingsText')
|
||||
|
|
@ -415,40 +409,85 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
show_next_button = self._is_more_levels and not (env.demo or env.arcade)
|
||||
|
||||
if not show_next_button:
|
||||
h_offs += 70
|
||||
h_offs += 60
|
||||
|
||||
menu_button = bui.buttonwidget(
|
||||
parent=rootc,
|
||||
autoselect=True,
|
||||
position=(h_offs - 130 - 60, v_offs),
|
||||
size=(110, 85),
|
||||
label='',
|
||||
on_activate_call=bui.WeakCall(self._ui_menu),
|
||||
)
|
||||
bui.imagewidget(
|
||||
parent=rootc,
|
||||
draw_controller=menu_button,
|
||||
position=(h_offs - 130 - 60 + 22, v_offs + 14),
|
||||
size=(60, 60),
|
||||
texture=self._menu_icon_texture,
|
||||
opacity=0.8,
|
||||
)
|
||||
self._restart_button = restart_button = bui.buttonwidget(
|
||||
parent=rootc,
|
||||
autoselect=True,
|
||||
position=(h_offs - 60, v_offs),
|
||||
size=(110, 85),
|
||||
label='',
|
||||
on_activate_call=bui.WeakCall(self._ui_restart),
|
||||
)
|
||||
bui.imagewidget(
|
||||
parent=rootc,
|
||||
draw_controller=restart_button,
|
||||
position=(h_offs - 60 + 19, v_offs + 7),
|
||||
size=(70, 70),
|
||||
texture=self._replay_icon_texture,
|
||||
opacity=0.8,
|
||||
)
|
||||
# Due to virtual-bounds changes, have to squish buttons a bit to
|
||||
# avoid overlapping with tips at bottom. Could look nicer to
|
||||
# rework things in the middle to get more space, but would
|
||||
# rather not touch this old code more than necessary.
|
||||
small_buttons = False
|
||||
|
||||
if small_buttons:
|
||||
menu_button = bui.buttonwidget(
|
||||
parent=rootc,
|
||||
autoselect=True,
|
||||
position=(h_offs - 130 - 45, v_offs + 40),
|
||||
size=(100, 50),
|
||||
label='',
|
||||
button_type='square',
|
||||
on_activate_call=bui.WeakCall(self._ui_menu),
|
||||
)
|
||||
bui.imagewidget(
|
||||
parent=rootc,
|
||||
draw_controller=menu_button,
|
||||
position=(h_offs - 130 - 60 + 43, v_offs + 43),
|
||||
size=(45, 45),
|
||||
texture=self._menu_icon_texture,
|
||||
opacity=0.8,
|
||||
)
|
||||
else:
|
||||
menu_button = bui.buttonwidget(
|
||||
parent=rootc,
|
||||
autoselect=True,
|
||||
position=(h_offs - 130 - 60, v_offs),
|
||||
size=(110, 85),
|
||||
label='',
|
||||
on_activate_call=bui.WeakCall(self._ui_menu),
|
||||
)
|
||||
bui.imagewidget(
|
||||
parent=rootc,
|
||||
draw_controller=menu_button,
|
||||
position=(h_offs - 130 - 60 + 22, v_offs + 14),
|
||||
size=(60, 60),
|
||||
texture=self._menu_icon_texture,
|
||||
opacity=0.8,
|
||||
)
|
||||
|
||||
if small_buttons:
|
||||
self._restart_button = restart_button = bui.buttonwidget(
|
||||
parent=rootc,
|
||||
autoselect=True,
|
||||
position=(h_offs - 60, v_offs + 40),
|
||||
size=(100, 50),
|
||||
label='',
|
||||
button_type='square',
|
||||
on_activate_call=bui.WeakCall(self._ui_restart),
|
||||
)
|
||||
bui.imagewidget(
|
||||
parent=rootc,
|
||||
draw_controller=restart_button,
|
||||
position=(h_offs - 60 + 25, v_offs + 42),
|
||||
size=(47, 47),
|
||||
texture=self._replay_icon_texture,
|
||||
opacity=0.8,
|
||||
)
|
||||
else:
|
||||
self._restart_button = restart_button = bui.buttonwidget(
|
||||
parent=rootc,
|
||||
autoselect=True,
|
||||
position=(h_offs - 60, v_offs),
|
||||
size=(110, 85),
|
||||
label='',
|
||||
on_activate_call=bui.WeakCall(self._ui_restart),
|
||||
)
|
||||
bui.imagewidget(
|
||||
parent=rootc,
|
||||
draw_controller=restart_button,
|
||||
position=(h_offs - 60 + 19, v_offs + 7),
|
||||
size=(70, 70),
|
||||
texture=self._replay_icon_texture,
|
||||
opacity=0.8,
|
||||
)
|
||||
|
||||
next_button: bui.Widget | None = None
|
||||
|
||||
|
|
@ -465,58 +504,53 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
button_sound = False
|
||||
image_opacity = 0.2
|
||||
color = (0.3, 0.3, 0.3)
|
||||
next_button = bui.buttonwidget(
|
||||
parent=rootc,
|
||||
autoselect=True,
|
||||
position=(h_offs + 130 - 60, v_offs),
|
||||
size=(110, 85),
|
||||
label='',
|
||||
on_activate_call=call,
|
||||
color=color,
|
||||
enable_sound=button_sound,
|
||||
)
|
||||
bui.imagewidget(
|
||||
parent=rootc,
|
||||
draw_controller=next_button,
|
||||
position=(h_offs + 130 - 60 + 12, v_offs + 5),
|
||||
size=(80, 80),
|
||||
texture=self._next_level_icon_texture,
|
||||
opacity=image_opacity,
|
||||
)
|
||||
|
||||
if small_buttons:
|
||||
next_button = bui.buttonwidget(
|
||||
parent=rootc,
|
||||
autoselect=True,
|
||||
position=(h_offs + 130 - 75, v_offs + 40),
|
||||
size=(100, 50),
|
||||
label='',
|
||||
button_type='square',
|
||||
on_activate_call=call,
|
||||
color=color,
|
||||
enable_sound=button_sound,
|
||||
)
|
||||
bui.imagewidget(
|
||||
parent=rootc,
|
||||
draw_controller=next_button,
|
||||
position=(h_offs + 130 - 60 + 12, v_offs + 40),
|
||||
size=(50, 50),
|
||||
texture=self._next_level_icon_texture,
|
||||
opacity=image_opacity,
|
||||
)
|
||||
else:
|
||||
next_button = bui.buttonwidget(
|
||||
parent=rootc,
|
||||
autoselect=True,
|
||||
position=(h_offs + 130 - 60, v_offs),
|
||||
size=(110, 85),
|
||||
label='',
|
||||
on_activate_call=call,
|
||||
color=color,
|
||||
enable_sound=button_sound,
|
||||
)
|
||||
bui.imagewidget(
|
||||
parent=rootc,
|
||||
draw_controller=next_button,
|
||||
position=(h_offs + 130 - 60 + 12, v_offs + 5),
|
||||
size=(80, 80),
|
||||
texture=self._next_level_icon_texture,
|
||||
opacity=image_opacity,
|
||||
)
|
||||
|
||||
x_offs_extra = 0 if show_next_button else -100
|
||||
self._corner_button_offs = (
|
||||
h_offs + 300.0 + 100.0 + x_offs_extra,
|
||||
v_offs + 560.0,
|
||||
h_offs + 300.0 + x_offs_extra,
|
||||
v_offs + 519.0,
|
||||
)
|
||||
|
||||
if env.demo or env.arcade:
|
||||
self._league_rank_button = None
|
||||
self._store_button_instance = None
|
||||
else:
|
||||
self._league_rank_button = LeagueRankButton(
|
||||
parent=rootc,
|
||||
position=(h_offs + 300 + 100 + x_offs_extra, v_offs + 560),
|
||||
size=(100, 60),
|
||||
scale=0.9,
|
||||
color=(0.4, 0.4, 0.9),
|
||||
textcolor=(0.9, 0.9, 2.0),
|
||||
transition_delay=0.0,
|
||||
smooth_update_delay=5.0,
|
||||
)
|
||||
self._store_button_instance = StoreButton(
|
||||
parent=rootc,
|
||||
position=(h_offs + 400 + 100 + x_offs_extra, v_offs + 560),
|
||||
show_tickets=True,
|
||||
sale_scale=0.85,
|
||||
size=(100, 60),
|
||||
scale=0.9,
|
||||
button_type='square',
|
||||
color=(0.35, 0.25, 0.45),
|
||||
textcolor=(0.9, 0.7, 1.0),
|
||||
transition_delay=0.0,
|
||||
)
|
||||
|
||||
bui.containerwidget(
|
||||
edit=rootc,
|
||||
selected_child=(
|
||||
|
|
@ -527,26 +561,12 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
on_cancel_call=menu_button.activate,
|
||||
)
|
||||
|
||||
self._update_corner_button_positions()
|
||||
self._update_corner_button_positions_timer = bui.AppTimer(
|
||||
1.0, bui.WeakCall(self._update_corner_button_positions), repeat=True
|
||||
)
|
||||
|
||||
def _update_corner_button_positions(self) -> None:
|
||||
offs = -55 if bui.is_party_icon_visible() else 0
|
||||
assert self._corner_button_offs is not None
|
||||
pos_x = self._corner_button_offs[0] + offs
|
||||
pos_y = self._corner_button_offs[1]
|
||||
if self._league_rank_button is not None:
|
||||
self._league_rank_button.set_position((pos_x, pos_y))
|
||||
if self._store_button_instance is not None:
|
||||
self._store_button_instance.set_position((pos_x + 100, pos_y))
|
||||
|
||||
def _player_press(self) -> None:
|
||||
# (Only for headless builds).
|
||||
|
||||
# If this activity is a good 'end point', ask server-mode just once if
|
||||
# it wants to do anything special like switch sessions or kill the app.
|
||||
# If this activity is a good 'end point', ask server-mode just
|
||||
# once if it wants to do anything special like switch sessions
|
||||
# or kill the app.
|
||||
if (
|
||||
self._allow_server_transition
|
||||
and bs.app.classic is not None
|
||||
|
|
@ -597,7 +617,6 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
|
||||
@override
|
||||
def on_begin(self) -> None:
|
||||
# FIXME: Clean this up.
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-locals
|
||||
|
|
@ -865,7 +884,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
# If we're not doing the world's-best button, just show a title
|
||||
# instead.
|
||||
ts_height = 300
|
||||
ts_h_offs = 210
|
||||
ts_h_offs = 290
|
||||
v_offs = 40
|
||||
txt = Text(
|
||||
(
|
||||
|
|
@ -939,7 +958,6 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
if display_scores[i][1] is None:
|
||||
name_str = '-'
|
||||
else:
|
||||
# noinspection PyUnresolvedReferences
|
||||
name_str = ', '.join(
|
||||
[p['name'] for p in display_scores[i][1]['players']]
|
||||
)
|
||||
|
|
@ -1008,9 +1026,8 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
ts_h_offs = -480
|
||||
v_offs = 40
|
||||
|
||||
# Only make this if we don't have the button
|
||||
# (never want clients to see it so no need for client-only
|
||||
# version, etc).
|
||||
# Only make this if we don't have the button (never want clients
|
||||
# to see it so no need for client-only version, etc).
|
||||
if self._have_achievements:
|
||||
if not self._account_has_achievements:
|
||||
Text(
|
||||
|
|
@ -1052,7 +1069,6 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
).autoretain()
|
||||
|
||||
def _got_friend_score_results(self, results: list[Any] | None) -> None:
|
||||
# FIXME: tidy this up
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
|
|
@ -1187,35 +1203,77 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
transition_delay=tdelay2,
|
||||
).autoretain()
|
||||
|
||||
def _on_v2_score_results(
|
||||
self, response: bacommon.bs.ScoreSubmitResponse | Exception
|
||||
) -> None:
|
||||
|
||||
if isinstance(response, Exception):
|
||||
logging.debug('Got error score-submit response: %s', response)
|
||||
return
|
||||
|
||||
assert isinstance(response, bacommon.bs.ScoreSubmitResponse)
|
||||
|
||||
# Aim to have these effects run shortly after the final rating
|
||||
# hit happens.
|
||||
with self.context:
|
||||
assert self._begin_time is not None
|
||||
delay = max(0, 5.5 - (bs.time() - self._begin_time))
|
||||
|
||||
assert bui.app.classic is not None
|
||||
bs.timer(
|
||||
delay,
|
||||
strict_partial(
|
||||
bui.app.classic.run_bs_client_effects, response.effects
|
||||
),
|
||||
)
|
||||
|
||||
def _got_score_results(self, results: dict[str, Any] | None) -> None:
|
||||
# FIXME: tidy this up
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
|
||||
plus = bs.app.plus
|
||||
assert plus is not None
|
||||
classic = bs.app.classic
|
||||
assert classic is not None
|
||||
|
||||
# We need to manually run this in the context of our activity
|
||||
# and only if we aren't shutting down.
|
||||
# (really should make the submit_score call handle that stuff itself)
|
||||
if self.expired:
|
||||
return
|
||||
|
||||
with self.context:
|
||||
# Delay a bit if results come in too fast.
|
||||
assert self._begin_time is not None
|
||||
base_delay = max(0, 2.7 - (bs.time() - self._begin_time))
|
||||
v_offs = 20
|
||||
# v_offs = 20
|
||||
v_offs = 64
|
||||
if results is None:
|
||||
self._score_loading_status = Text(
|
||||
bs.Lstr(resource='worldScoresUnavailableText'),
|
||||
position=(230, 150 + v_offs),
|
||||
position=(280, 130 + v_offs),
|
||||
color=(1, 1, 1, 0.4),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
transition_delay=base_delay + 0.3,
|
||||
scale=0.7,
|
||||
)
|
||||
else:
|
||||
|
||||
# If there's a score-uuid bundled, ship it along to the
|
||||
# v2 master server to ask about any rewards from that
|
||||
# end.
|
||||
score_token = results.get('token')
|
||||
if (
|
||||
isinstance(score_token, str)
|
||||
and plus.accounts.primary is not None
|
||||
):
|
||||
with plus.accounts.primary:
|
||||
plus.cloud.send_message_cb(
|
||||
bacommon.bs.ScoreSubmitMessage(score_token),
|
||||
on_response=bui.WeakCall(self._on_v2_score_results),
|
||||
)
|
||||
|
||||
self._score_link = results['link']
|
||||
assert self._score_link is not None
|
||||
# Prepend our master-server addr if its a relative addr.
|
||||
|
|
@ -1254,7 +1312,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
(1.5 + base_delay),
|
||||
bs.WeakCall(self._show_world_rank, offs_x),
|
||||
)
|
||||
ts_h_offs = 200
|
||||
ts_h_offs = 280
|
||||
ts_height = 300
|
||||
|
||||
# Show world tops.
|
||||
|
|
@ -1274,7 +1332,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
),
|
||||
position=(
|
||||
ts_h_offs - 35 + 95,
|
||||
ts_height / 2 + 6 + v_offs,
|
||||
ts_height / 2 + 6 + v_offs - 41,
|
||||
),
|
||||
color=(0.4, 0.4, 0.4, 1.0),
|
||||
scale=0.7,
|
||||
|
|
@ -1282,7 +1340,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
transition_delay=base_delay + 0.3,
|
||||
).autoretain()
|
||||
else:
|
||||
v_offs += 20
|
||||
v_offs += 40
|
||||
|
||||
h_offs_extra = 0
|
||||
v_offs_names = 0
|
||||
|
|
@ -1309,6 +1367,37 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
random.randrange(0, len(times) + 1),
|
||||
(base_delay + i * 0.05, base_delay + 0.4 + i * 0.05),
|
||||
)
|
||||
|
||||
# Conundrum: We want to place line numbers to the
|
||||
# left of our score column based on the largest
|
||||
# score width. However scores may use Lstrs and thus
|
||||
# may have different widths in different languages.
|
||||
# We don't want to bake down the Lstrs we display
|
||||
# because then clients can't view scores in their
|
||||
# own language. So as a compromise lets measure
|
||||
# max-width based on baked down Lstrs but then
|
||||
# display regular Lstrs with max-width set based on
|
||||
# that. Hopefully that'll look reasonable for most
|
||||
# languages.
|
||||
max_score_width = 10.0
|
||||
for tval in self._show_info['tops']:
|
||||
score = int(tval[0])
|
||||
name_str = tval[1]
|
||||
if name_str != '-':
|
||||
max_score_width = max(
|
||||
max_score_width,
|
||||
bui.get_string_width(
|
||||
(
|
||||
str(score)
|
||||
if self._score_type == 'points'
|
||||
else bs.timestring(
|
||||
(score * 10) / 1000.0
|
||||
).evaluate()
|
||||
),
|
||||
suppress_warning=True,
|
||||
),
|
||||
)
|
||||
|
||||
for i, tval in enumerate(self._show_info['tops']):
|
||||
score = int(tval[0])
|
||||
name_str = tval[1]
|
||||
|
|
@ -1330,19 +1419,45 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
tdelay2 = times[i][1]
|
||||
|
||||
if name_str != '-':
|
||||
sstr = (
|
||||
str(score)
|
||||
if self._score_type == 'points'
|
||||
else bs.timestring((score * 10) / 1000.0)
|
||||
)
|
||||
|
||||
# Line number.
|
||||
Text(
|
||||
(
|
||||
str(score)
|
||||
if self._score_type == 'points'
|
||||
else bs.timestring((score * 10) / 1000.0)
|
||||
str(i + 1),
|
||||
position=(
|
||||
ts_h_offs
|
||||
+ 20
|
||||
+ h_offs_extra
|
||||
- max_score_width
|
||||
- 8.0,
|
||||
ts_height / 2
|
||||
+ -ts_height * (i + 1) / 10
|
||||
+ v_offs
|
||||
- 30.0,
|
||||
),
|
||||
scale=0.5,
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=(0.3, 0.3, 0.3),
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay1,
|
||||
).autoretain()
|
||||
|
||||
# Score.
|
||||
Text(
|
||||
sstr,
|
||||
position=(
|
||||
ts_h_offs + 20 + h_offs_extra,
|
||||
ts_height / 2
|
||||
+ -ts_height * (i + 1) / 10
|
||||
+ v_offs
|
||||
+ 11.0,
|
||||
- 30.0,
|
||||
),
|
||||
maxwidth=max_score_width,
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=color0,
|
||||
|
|
@ -1350,6 +1465,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay1,
|
||||
).autoretain()
|
||||
# Player name.
|
||||
Text(
|
||||
bs.Lstr(value=name_str),
|
||||
position=(
|
||||
|
|
@ -1358,7 +1474,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
+ -ts_height * (i + 1) / 10
|
||||
+ v_offs_names
|
||||
+ v_offs
|
||||
+ 11.0,
|
||||
- 30.0,
|
||||
),
|
||||
maxwidth=80.0 + 100.0 * len(self._playerinfos),
|
||||
v_align=Text.VAlign.CENTER,
|
||||
|
|
@ -1453,16 +1569,12 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
]
|
||||
# pylint: disable=useless-suppression
|
||||
# pylint: disable=unbalanced-tuple-unpacking
|
||||
(
|
||||
pr1,
|
||||
pv1,
|
||||
pr2,
|
||||
pv2,
|
||||
pr3,
|
||||
pv3,
|
||||
) = bs.app.classic.get_tournament_prize_strings(
|
||||
tourney_info
|
||||
(pr1, pv1, pr2, pv2, pr3, pv3) = (
|
||||
bs.app.classic.get_tournament_prize_strings(
|
||||
tourney_info, include_tickets=False
|
||||
)
|
||||
)
|
||||
|
||||
# pylint: enable=unbalanced-tuple-unpacking
|
||||
# pylint: enable=useless-suppression
|
||||
|
||||
|
|
@ -1478,10 +1590,14 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
transition_delay=2.0,
|
||||
).autoretain()
|
||||
vval = -107 + 70
|
||||
for rng, val in ((pr1, pv1), (pr2, pv2), (pr3, pv3)):
|
||||
for i, rng, val in (
|
||||
(0, pr1, pv1),
|
||||
(1, pr2, pv2),
|
||||
(2, pr3, pv3),
|
||||
):
|
||||
Text(
|
||||
rng,
|
||||
position=(-410 + 10, vval),
|
||||
position=(-430 + 10, vval),
|
||||
color=(1, 1, 1, 0.7),
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
|
|
@ -1492,7 +1608,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
).autoretain()
|
||||
Text(
|
||||
val,
|
||||
position=(-390 + 10, vval),
|
||||
position=(-410 + 10, vval),
|
||||
color=(0.7, 0.7, 0.7, 1.0),
|
||||
h_align=Text.HAlign.LEFT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
|
|
@ -1501,6 +1617,9 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
maxwidth=300,
|
||||
transition_delay=2.0,
|
||||
).autoretain()
|
||||
bs.app.classic.create_in_game_tournament_prize_image(
|
||||
tourney_info, i, (-410 + 70, vval)
|
||||
)
|
||||
vval -= 35
|
||||
except Exception:
|
||||
logging.exception('Error showing prize ranges.')
|
||||
|
|
@ -1573,6 +1692,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
|
|||
transition_delay=1.0,
|
||||
).autoretain()
|
||||
else:
|
||||
assert rating is not None
|
||||
ZoomText(
|
||||
(
|
||||
f'{rating:.1f}'
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
kill_delay: float,
|
||||
shiftdelay: float,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
del kill_delay # Unused arg.
|
||||
ZoomText(
|
||||
str(sessionteam.customdata['score']),
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
extrascale: float,
|
||||
flash: bool = False,
|
||||
) -> Text:
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
return Text(
|
||||
text,
|
||||
position=(
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
|
|||
|
||||
def show_player_scores(
|
||||
self,
|
||||
*,
|
||||
delay: float = 2.5,
|
||||
results: bs.GameResults | None = None,
|
||||
scale: float = 1.0,
|
||||
|
|
@ -134,6 +135,7 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
|
|||
xoffs: float,
|
||||
yoffs: float,
|
||||
text: bs.Lstr,
|
||||
*,
|
||||
h_align: Text.HAlign = Text.HAlign.RIGHT,
|
||||
extrascale: float = 1.0,
|
||||
maxwidth: float | None = 120.0,
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import override
|
||||
from typing import override, TYPE_CHECKING
|
||||
|
||||
import bascenev1 as bs
|
||||
|
||||
from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
||||
"""Final score screen for a team series."""
|
||||
|
|
@ -24,6 +27,8 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
self._allow_server_transition = True
|
||||
self._tips_text = None
|
||||
self._default_show_tips = False
|
||||
self._ffa_top_player_info: list[Any] | None = None
|
||||
self._ffa_top_player_rec: bs.PlayerRecord | None = None
|
||||
|
||||
@override
|
||||
def on_begin(self) -> None:
|
||||
|
|
@ -70,6 +75,16 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
)
|
||||
)
|
||||
player_entries.sort(reverse=True, key=lambda x: x[0])
|
||||
if len(player_entries) > 0:
|
||||
# Store some info for the top ffa player so we can
|
||||
# show winner info even if they leave.
|
||||
self._ffa_top_player_info = list(player_entries[0])
|
||||
self._ffa_top_player_info[1] = self._ffa_top_player_info[
|
||||
2
|
||||
].getname()
|
||||
self._ffa_top_player_info[2] = self._ffa_top_player_info[
|
||||
2
|
||||
].get_icon()
|
||||
else:
|
||||
for _pkey, prec in self.stats.get_records().items():
|
||||
player_entries.append((prec.score, prec.name_full, prec))
|
||||
|
|
@ -308,7 +323,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
most_killed = entry[2].killed_count
|
||||
if mkp is not None:
|
||||
Text(
|
||||
bs.Lstr(resource='mostViolatedPlayerText'),
|
||||
bs.Lstr(resource='mostDestroyedPlayerText'),
|
||||
color=(0.5, 0.5, 0.5, 1.0),
|
||||
v_align=Text.VAlign.CENTER,
|
||||
maxwidth=300,
|
||||
|
|
@ -433,25 +448,42 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
maxwidth=250,
|
||||
).autoretain()
|
||||
else:
|
||||
offs_v = -80.0
|
||||
offs_v = -80
|
||||
assert isinstance(self.session, bs.MultiTeamSession)
|
||||
series_length = self.session.get_ffa_series_length()
|
||||
icon: dict | None
|
||||
# Pull live player info if they're still around.
|
||||
if len(team.players) == 1:
|
||||
icon = team.players[0].get_icon()
|
||||
player_name = team.players[0].getname(full=True, icon=False)
|
||||
# Otherwise use the special info we stored when we came in.
|
||||
elif (
|
||||
self._ffa_top_player_info is not None
|
||||
and self._ffa_top_player_info[0] >= series_length
|
||||
):
|
||||
icon = self._ffa_top_player_info[2]
|
||||
player_name = self._ffa_top_player_info[1]
|
||||
else:
|
||||
icon = None
|
||||
player_name = 'Player Not Found'
|
||||
|
||||
if icon is not None:
|
||||
i = Image(
|
||||
team.players[0].get_icon(),
|
||||
icon,
|
||||
position=(0, 143),
|
||||
scale=(100, 100),
|
||||
).autoretain()
|
||||
assert i.node
|
||||
bs.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0})
|
||||
ZoomText(
|
||||
bs.Lstr(
|
||||
value=team.players[0].getname(full=True, icon=False)
|
||||
),
|
||||
position=(0, 97 + offs_v),
|
||||
color=team.color,
|
||||
scale=1.15,
|
||||
jitter=1.0,
|
||||
maxwidth=250,
|
||||
).autoretain()
|
||||
|
||||
ZoomText(
|
||||
bs.Lstr(value=player_name),
|
||||
position=(0, 97 + offs_v + (0 if icon is not None else 60)),
|
||||
color=team.color,
|
||||
scale=1.15,
|
||||
jitter=1.0,
|
||||
maxwidth=250,
|
||||
).autoretain()
|
||||
|
||||
s_extra = 1.0 if self._is_ffa else 1.0
|
||||
|
||||
|
|
|
|||
|
|
@ -333,6 +333,7 @@ class Blast(bs.Actor):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
position: Sequence[float] = (0.0, 1.0, 0.0),
|
||||
velocity: Sequence[float] = (0.0, 0.0, 0.0),
|
||||
blast_radius: float = 2.0,
|
||||
|
|
@ -715,6 +716,7 @@ class Bomb(bs.Actor):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
position: Sequence[float] = (0.0, 1.0, 0.0),
|
||||
velocity: Sequence[float] = (0.0, 0.0, 0.0),
|
||||
bomb_type: str = 'normal',
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class ControlsGuide(bs.Actor):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
position: tuple[float, float] = (390.0, 120.0),
|
||||
scale: float = 1.0,
|
||||
delay: float = 0.0,
|
||||
|
|
|
|||
14
dist/ba_data/python/bascenev1lib/actor/flag.py
vendored
14
dist/ba_data/python/bascenev1lib/actor/flag.py
vendored
|
|
@ -144,6 +144,9 @@ class FlagDiedMessage:
|
|||
flag: Flag
|
||||
"""The `Flag` that died."""
|
||||
|
||||
self_kill: bool = False
|
||||
"""If the `Flag` killed itself or not."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlagDroppedMessage:
|
||||
|
|
@ -169,6 +172,7 @@ class Flag(bs.Actor):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
position: Sequence[float] = (0.0, 1.0, 0.0),
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0),
|
||||
materials: Sequence[bs.Material] | None = None,
|
||||
|
|
@ -282,7 +286,9 @@ class Flag(bs.Actor):
|
|||
)
|
||||
self._counter.text = str(self._count)
|
||||
if self._count < 1:
|
||||
self.handlemessage(bs.DieMessage())
|
||||
self.handlemessage(
|
||||
bs.DieMessage(how=bs.DeathType.LEFT_GAME)
|
||||
)
|
||||
else:
|
||||
assert self._counter
|
||||
self._counter.text = ''
|
||||
|
|
@ -336,7 +342,11 @@ class Flag(bs.Actor):
|
|||
if self.node:
|
||||
self.node.delete()
|
||||
if not msg.immediate:
|
||||
self.activity.handlemessage(FlagDiedMessage(self))
|
||||
self.activity.handlemessage(
|
||||
FlagDiedMessage(
|
||||
self, (msg.how is bs.DeathType.LEFT_GAME)
|
||||
)
|
||||
)
|
||||
elif isinstance(msg, bs.HitMessage):
|
||||
assert self.node
|
||||
assert msg.force_direction is not None
|
||||
|
|
|
|||
13
dist/ba_data/python/bascenev1lib/actor/image.py
vendored
13
dist/ba_data/python/bascenev1lib/actor/image.py
vendored
|
|
@ -37,6 +37,7 @@ class Image(bs.Actor):
|
|||
def __init__(
|
||||
self,
|
||||
texture: bs.Texture | dict[str, Any],
|
||||
*,
|
||||
position: tuple[float, float] = (0, 0),
|
||||
transition: Transition | None = None,
|
||||
transition_delay: float = 0.0,
|
||||
|
|
@ -55,15 +56,21 @@ class Image(bs.Actor):
|
|||
# pylint: disable=too-many-locals
|
||||
super().__init__()
|
||||
|
||||
# If they provided a dict as texture, assume its an icon.
|
||||
# otherwise its just a texture value itself.
|
||||
# If they provided a dict as texture, use it to wire up extended
|
||||
# stuff like tints and masks.
|
||||
mask_texture: bs.Texture | None
|
||||
if isinstance(texture, dict):
|
||||
tint_color = texture['tint_color']
|
||||
tint2_color = texture['tint2_color']
|
||||
tint_texture = texture['tint_texture']
|
||||
|
||||
# Assume we're dealing with a character icon but allow
|
||||
# overriding.
|
||||
mask_tex_name = texture.get('mask_texture', 'characterIconMask')
|
||||
mask_texture = (
|
||||
None if mask_tex_name is None else bs.gettexture(mask_tex_name)
|
||||
)
|
||||
texture = texture['texture']
|
||||
mask_texture = bs.gettexture('characterIconMask')
|
||||
else:
|
||||
tint_color = (1, 1, 1)
|
||||
tint2_color = None
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class PlayerSpaz(Spaz):
|
|||
def __init__(
|
||||
self,
|
||||
player: bs.Player,
|
||||
*,
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0),
|
||||
highlight: Sequence[float] = (0.5, 0.5, 0.5),
|
||||
character: str = 'Spaz',
|
||||
|
|
@ -102,6 +103,7 @@ class PlayerSpaz(Spaz):
|
|||
|
||||
def connect_controls_to_player(
|
||||
self,
|
||||
*,
|
||||
enable_jump: bool = True,
|
||||
enable_punch: bool = True,
|
||||
enable_pickup: bool = True,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class PopupText(bs.Actor):
|
|||
def __init__(
|
||||
self,
|
||||
text: str | bs.Lstr,
|
||||
*,
|
||||
position: Sequence[float] = (0.0, 0.0, 0.0),
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
|
||||
random_offset: float = 0.5,
|
||||
|
|
|
|||
|
|
@ -22,14 +22,18 @@ class _Entry:
|
|||
scale: float,
|
||||
label: bs.Lstr | None,
|
||||
flash_length: float,
|
||||
width: float | None = None,
|
||||
height: float | None = None,
|
||||
):
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
self._scoreboard = weakref.ref(scoreboard)
|
||||
self._do_cover = do_cover
|
||||
self._scale = scale
|
||||
self._flash_length = flash_length
|
||||
self._width = 140.0 * self._scale
|
||||
self._height = 32.0 * self._scale
|
||||
self._width = (140.0 if width is None else width) * self._scale
|
||||
self._height = (32.0 if height is None else height) * self._scale
|
||||
self._bar_width = 2.0 * self._scale
|
||||
self._bar_height = 32.0 * self._scale
|
||||
self._bar_tex = self._backing_tex = bs.gettexture('bar')
|
||||
|
|
@ -277,6 +281,7 @@ class _Entry:
|
|||
def set_value(
|
||||
self,
|
||||
score: float,
|
||||
*,
|
||||
max_score: float | None = None,
|
||||
countdown: bool = False,
|
||||
flash: bool = True,
|
||||
|
|
@ -368,16 +373,26 @@ class Scoreboard:
|
|||
|
||||
_ENTRYSTORENAME = bs.storagename('entry')
|
||||
|
||||
def __init__(self, label: bs.Lstr | None = None, score_split: float = 0.7):
|
||||
def __init__(
|
||||
self,
|
||||
label: bs.Lstr | None = None,
|
||||
score_split: float = 0.7,
|
||||
pos: Sequence[float] | None = None,
|
||||
width: float | None = None,
|
||||
height: float | None = None,
|
||||
):
|
||||
"""Instantiate a scoreboard.
|
||||
|
||||
Label can be something like 'points' and will
|
||||
show up on boards if provided.
|
||||
"""
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
self._flat_tex = bs.gettexture('null')
|
||||
self._entries: dict[int, _Entry] = {}
|
||||
self._label = label
|
||||
self.score_split = score_split
|
||||
self._width = width
|
||||
self._height = height
|
||||
|
||||
# For free-for-all we go simpler since we have one per player.
|
||||
self._pos: Sequence[float]
|
||||
|
|
@ -393,12 +408,14 @@ class Scoreboard:
|
|||
self._pos = (20.0, -70.0)
|
||||
self._scale = 1.0
|
||||
self._flash_length = 1.0
|
||||
self._pos = self._pos if pos is None else pos
|
||||
|
||||
def set_team_value(
|
||||
self,
|
||||
team: bs.Team,
|
||||
score: float,
|
||||
max_score: float | None = None,
|
||||
*,
|
||||
countdown: bool = False,
|
||||
flash: bool = True,
|
||||
show_value: bool = True,
|
||||
|
|
@ -430,6 +447,8 @@ class Scoreboard:
|
|||
do_cover=self._do_cover,
|
||||
scale=self._scale,
|
||||
label=self._label,
|
||||
width=self._width,
|
||||
height=self._height,
|
||||
flash_length=self._flash_length,
|
||||
)
|
||||
self._update_teams()
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class Spawner:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
data: Any = None,
|
||||
pt: Sequence[float] = (0, 0, 0),
|
||||
spawn_time: float = 1.0,
|
||||
|
|
|
|||
12
dist/ba_data/python/bascenev1lib/actor/spaz.py
vendored
12
dist/ba_data/python/bascenev1lib/actor/spaz.py
vendored
|
|
@ -71,6 +71,7 @@ class Spaz(bs.Actor):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0),
|
||||
highlight: Sequence[float] = (0.5, 0.5, 0.5),
|
||||
character: str = 'Spaz',
|
||||
|
|
@ -1195,11 +1196,12 @@ class Spaz(bs.Actor):
|
|||
if self.node:
|
||||
self.node.delete()
|
||||
elif self.node:
|
||||
self.node.hurt = 1.0
|
||||
if self.play_big_death_sound and not wasdead:
|
||||
SpazFactory.get().single_player_death_sound.play()
|
||||
self.node.dead = True
|
||||
bs.timer(2.0, self.node.delete)
|
||||
if not wasdead:
|
||||
self.node.hurt = 1.0
|
||||
if self.play_big_death_sound:
|
||||
SpazFactory.get().single_player_death_sound.play()
|
||||
self.node.dead = True
|
||||
bs.timer(2.0, self.node.delete)
|
||||
|
||||
elif isinstance(msg, bs.OutOfBoundsMessage):
|
||||
# By default we just die here.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ def get_appearances(include_locked: bool = False) -> list[str]:
|
|||
assert plus is not None
|
||||
|
||||
assert bs.app.classic is not None
|
||||
get_purchased = plus.get_purchased
|
||||
get_purchased = plus.get_v1_account_product_purchased
|
||||
disallowed = []
|
||||
if not include_locked:
|
||||
# Hmm yeah this'll be tough to hack...
|
||||
|
|
|
|||
|
|
@ -947,9 +947,9 @@ class SpazBotSet:
|
|||
on_spawn_call: Callable[[SpazBot], Any] | None = None,
|
||||
) -> None:
|
||||
"""Spawn a bot from this set."""
|
||||
from bascenev1lib.actor import spawner
|
||||
from bascenev1lib.actor.spawner import Spawner
|
||||
|
||||
spawner.Spawner(
|
||||
Spawner(
|
||||
pt=pos,
|
||||
spawn_time=spawn_time,
|
||||
send_spawn_message=False,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class Text(bs.Actor):
|
|||
def __init__(
|
||||
self,
|
||||
text: str | bs.Lstr,
|
||||
*,
|
||||
position: tuple[float, float] = (0.0, 0.0),
|
||||
h_align: HAlign = HAlign.LEFT,
|
||||
v_align: VAlign = VAlign.NONE,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class ZoomText(bs.Actor):
|
|||
self,
|
||||
text: str | bs.Lstr,
|
||||
position: tuple[float, float] = (0.0, 0.0),
|
||||
*,
|
||||
shiftposition: tuple[float, float] | None = None,
|
||||
shiftdelay: float | None = None,
|
||||
lifespan: float | None = None,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Defines assault minigame."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Defines a capture-the-flag game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -32,6 +32,7 @@ class CTFFlag(Flag):
|
|||
activity: CaptureTheFlagGame
|
||||
|
||||
def __init__(self, team: Team):
|
||||
|
||||
assert team.flagmaterial is not None
|
||||
super().__init__(
|
||||
materials=[team.flagmaterial],
|
||||
|
|
@ -73,6 +74,7 @@ class Team(bs.Team[Player]):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
base_pos: Sequence[float],
|
||||
base_region_material: bs.Material,
|
||||
base_region: bs.Node,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Provides the chosen-one mini-game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Provides the Conquest game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""DeathMatch game and support classes."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Provides an easter egg hunt game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Elimination mini-game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -27,6 +27,7 @@ class Icon(bs.Actor):
|
|||
player: Player,
|
||||
position: tuple[float, float],
|
||||
scale: float,
|
||||
*,
|
||||
show_lives: bool = True,
|
||||
show_death: bool = True,
|
||||
name_scale: float = 1.0,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
# pylint: disable=too-many-lines
|
||||
"""Implements football games (both co-op and teams varieties)."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -332,7 +331,10 @@ class FootballTeamGame(bs.TeamGameActivity[Player, Team]):
|
|||
# Respawn dead flags.
|
||||
elif isinstance(msg, FlagDiedMessage):
|
||||
if not self.has_ended():
|
||||
self._flag_respawn_timer = bs.Timer(3.0, self._spawn_flag)
|
||||
if msg.self_kill:
|
||||
self._spawn_flag()
|
||||
else:
|
||||
self._flag_respawn_timer = bs.Timer(3.0, self._spawn_flag)
|
||||
self._flag_respawn_light = bs.NodeActor(
|
||||
bs.newnode(
|
||||
'light',
|
||||
|
|
@ -655,11 +657,6 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
|
|||
for bot in bots:
|
||||
bot.target_flag = None
|
||||
|
||||
# If we're waiting on a continue, stop here so they don't keep scoring.
|
||||
if self.is_waiting_for_continue():
|
||||
self._bots.stop_moving()
|
||||
return
|
||||
|
||||
# If we've got a flag and no player are holding it, find the closest
|
||||
# bot to it, and make them the designated flag-bearer.
|
||||
assert self._flag is not None
|
||||
|
|
@ -816,14 +813,6 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
|
|||
self._bots.final_celebrate()
|
||||
bs.timer(0.001, bs.Call(self.do_end, 'defeat'))
|
||||
|
||||
@override
|
||||
def on_continue(self) -> None:
|
||||
# Subtract one touchdown from the bots and get them moving again.
|
||||
assert self._bot_team is not None
|
||||
self._bot_team.score -= 7
|
||||
self._bots.start_moving()
|
||||
self.update_scores()
|
||||
|
||||
def update_scores(self) -> None:
|
||||
"""update scoreboard and check for winners"""
|
||||
# FIXME: tidy this up
|
||||
|
|
@ -838,7 +827,7 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
|
|||
if not have_scoring_team:
|
||||
self._scoring_team = team
|
||||
if team is self._bot_team:
|
||||
self.continue_or_end_game()
|
||||
self.end_game()
|
||||
else:
|
||||
bs.setmusic(bs.MusicType.VICTORY)
|
||||
|
||||
|
|
@ -893,8 +882,7 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
|
|||
)
|
||||
self._time_text_input.node.timemax = self._final_time_ms
|
||||
|
||||
# FIXME: Does this still need to be deferred?
|
||||
bs.pushcall(bs.Call(self.do_end, 'victory'))
|
||||
self.do_end('victory')
|
||||
|
||||
def do_end(self, outcome: str) -> None:
|
||||
"""End the game with the specified outcome."""
|
||||
|
|
@ -945,7 +933,10 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
|
|||
# Respawn dead flags.
|
||||
elif isinstance(msg, FlagDiedMessage):
|
||||
assert isinstance(msg.flag, FootballFlag)
|
||||
msg.flag.respawn_timer = bs.Timer(3.0, self._spawn_flag)
|
||||
if msg.self_kill:
|
||||
self._spawn_flag()
|
||||
else:
|
||||
msg.flag.respawn_timer = bs.Timer(3.0, self._spawn_flag)
|
||||
self._flag_respawn_light = bs.NodeActor(
|
||||
bs.newnode(
|
||||
'light',
|
||||
|
|
@ -962,7 +953,7 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
|
|||
self._flag_respawn_light.node,
|
||||
'intensity',
|
||||
{0: 0, 0.25: 0.15, 0.5: 0},
|
||||
loop=True,
|
||||
loop=(not msg.self_kill),
|
||||
)
|
||||
bs.timer(3.0, self._flag_respawn_light.node.delete)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Hockey game and support classes."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Defines a keep-away game type."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Defines the King of the Hill game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Defines a bomb-dodging mini-game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Provides Ninja Fight mini-game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# Yes this is a long one..
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -805,6 +805,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]):
|
|||
max_level: int,
|
||||
) -> list[list[tuple[int, int]]]:
|
||||
"""Calculate a distribution of bad guys given some params."""
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
max_iterations = 10 + max_dudes * 2
|
||||
|
||||
groups: list[list[tuple[int, int]]] = []
|
||||
|
|
@ -1194,7 +1195,7 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]):
|
|||
|
||||
def _respawn_players_for_wave(self) -> None:
|
||||
# Respawn applicable players.
|
||||
if self._wavenum > 1 and not self.is_waiting_for_continue():
|
||||
if self._wavenum > 1:
|
||||
for player in self.players:
|
||||
if (
|
||||
not player.is_alive()
|
||||
|
|
@ -1641,19 +1642,9 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]):
|
|||
self.do_end('defeat', delay=2.0)
|
||||
bs.setmusic(None)
|
||||
|
||||
@override
|
||||
def on_continue(self) -> None:
|
||||
for player in self.players:
|
||||
if not player.is_alive():
|
||||
self.spawn_player(player)
|
||||
|
||||
def _checkroundover(self) -> None:
|
||||
"""Potentially end the round based on the state of the game."""
|
||||
if self.has_ended():
|
||||
return
|
||||
if not any(player.is_alive() for player in self.teams[0].players):
|
||||
# Allow continuing after wave 1.
|
||||
if self._wavenum > 1:
|
||||
self.continue_or_end_game()
|
||||
else:
|
||||
self.end_game()
|
||||
self.end_game()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Defines Race mini-game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -138,7 +138,9 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
|
|||
@override
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
return issubclass(sessiontype, bs.MultiTeamSession)
|
||||
return issubclass(sessiontype, bs.MultiTeamSession) or issubclass(
|
||||
sessiontype, bs.CoopSession
|
||||
)
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# We wear the cone of shame.
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -487,9 +487,9 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
|
|||
assert bs.app.classic is not None
|
||||
uiscale = bs.app.ui_v1.uiscale
|
||||
l_offs = (
|
||||
-80
|
||||
-120
|
||||
if uiscale is bs.UIScale.SMALL
|
||||
else -40 if uiscale is bs.UIScale.MEDIUM else 0
|
||||
else -60 if uiscale is bs.UIScale.MEDIUM else -30
|
||||
)
|
||||
|
||||
self._lives_bg = bs.NodeActor(
|
||||
|
|
@ -550,7 +550,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
|
|||
self._lives -= 1
|
||||
if self._lives == 0:
|
||||
self._bots.stop_moving()
|
||||
self.continue_or_end_game()
|
||||
self.end_game()
|
||||
|
||||
# Heartbeat behavior
|
||||
if self._lives < 5:
|
||||
|
|
@ -613,14 +613,6 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
|
|||
),
|
||||
)
|
||||
|
||||
@override
|
||||
def on_continue(self) -> None:
|
||||
self._lives = 3
|
||||
assert self._lives_text is not None
|
||||
assert self._lives_text.node
|
||||
self._lives_text.node.text = str(self._lives)
|
||||
self._bots.start_moving()
|
||||
|
||||
@override
|
||||
def spawn_player(self, player: Player) -> bs.Actor:
|
||||
pos = (
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
"""Implements Target Practice game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# ba_meta require api 9
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
376
dist/ba_data/python/bascenev1lib/mainmenu.py
vendored
376
dist/ba_data/python/bascenev1lib/mainmenu.py
vendored
|
|
@ -1,7 +1,6 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Session and Activity for displaying the main menu bg."""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -16,6 +15,8 @@ import bauiv1 as bui
|
|||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import bacommon.bs
|
||||
|
||||
|
||||
class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
|
||||
"""Activity showing the rotating main menu bg stuff."""
|
||||
|
|
@ -43,55 +44,23 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
|
|||
self._update_timer: bs.Timer | None = None
|
||||
self._news: NewsDisplay | None = None
|
||||
self._attract_mode_timer: bs.Timer | None = None
|
||||
self._logo_rotate_timer: bs.Timer | None = None
|
||||
|
||||
@override
|
||||
def on_transition_in(self) -> None:
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-branches
|
||||
super().on_transition_in()
|
||||
random.seed(123)
|
||||
app = bs.app
|
||||
env = app.env
|
||||
assert app.classic is not None
|
||||
|
||||
plus = bui.app.plus
|
||||
plus = bs.app.plus
|
||||
assert plus is not None
|
||||
|
||||
# FIXME: We shouldn't be doing things conditionally based on whether
|
||||
# the host is VR mode or not (clients may differ in that regard).
|
||||
# Any differences need to happen at the engine level so everyone
|
||||
# sees things in their own optimal way.
|
||||
vr_mode = bs.app.env.vr
|
||||
|
||||
if not bs.app.ui_v1.use_toolbars:
|
||||
color = (1.0, 1.0, 1.0, 1.0) if vr_mode else (0.5, 0.6, 0.5, 0.6)
|
||||
|
||||
# FIXME: Need a node attr for vr-specific-scale.
|
||||
scale = (
|
||||
0.9
|
||||
if (app.ui_v1.uiscale is bs.UIScale.SMALL or vr_mode)
|
||||
else 0.7
|
||||
)
|
||||
self.my_name = bs.NodeActor(
|
||||
bs.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'v_attach': 'bottom',
|
||||
'h_align': 'center',
|
||||
'color': color,
|
||||
'flatness': 1.0,
|
||||
'shadow': 1.0 if vr_mode else 0.5,
|
||||
'scale': scale,
|
||||
'position': (0, 10),
|
||||
'vr_depth': -10,
|
||||
'text': '\xa9 2011-2024 Eric Froemling',
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Throw up some text that only clients can see so they know that the
|
||||
# host is navigating menus while they're just staring at an
|
||||
# Throw up some text that only clients can see so they know that
|
||||
# the host is navigating menus while they're just staring at an
|
||||
# empty-ish screen.
|
||||
tval = bs.Lstr(
|
||||
resource='hostIsNavigatingMenusText',
|
||||
|
|
@ -109,73 +78,16 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
|
|||
},
|
||||
)
|
||||
)
|
||||
if not app.classic.main_menu_did_initial_transition and hasattr(
|
||||
self, 'my_name'
|
||||
if (
|
||||
not app.classic.main_menu_did_initial_transition
|
||||
and self.my_name is not None
|
||||
):
|
||||
assert self.my_name is not None
|
||||
assert self.my_name.node
|
||||
bs.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0})
|
||||
|
||||
# FIXME: We shouldn't be doing things conditionally based on whether
|
||||
# the host is vr mode or not (clients may not be or vice versa).
|
||||
# Any differences need to happen at the engine level so everyone sees
|
||||
# things in their own optimal way.
|
||||
vr_mode = app.env.vr
|
||||
uiscale = app.ui_v1.uiscale
|
||||
|
||||
# In cases where we're doing lots of dev work lets always show the
|
||||
# build number.
|
||||
force_show_build_number = False
|
||||
|
||||
if not bs.app.ui_v1.use_toolbars:
|
||||
if env.debug or env.test or force_show_build_number:
|
||||
if env.debug:
|
||||
text = bs.Lstr(
|
||||
value='${V} (${B}) (${D})',
|
||||
subs=[
|
||||
('${V}', app.env.engine_version),
|
||||
('${B}', str(app.env.engine_build_number)),
|
||||
('${D}', bs.Lstr(resource='debugText')),
|
||||
],
|
||||
)
|
||||
else:
|
||||
text = bs.Lstr(
|
||||
value='${V} (${B})',
|
||||
subs=[
|
||||
('${V}', app.env.engine_version),
|
||||
('${B}', str(app.env.engine_build_number)),
|
||||
],
|
||||
)
|
||||
else:
|
||||
text = bs.Lstr(
|
||||
value='${V}', subs=[('${V}', app.env.engine_version)]
|
||||
)
|
||||
scale = 0.9 if (uiscale is bs.UIScale.SMALL or vr_mode) else 0.7
|
||||
color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7)
|
||||
self.version = bs.NodeActor(
|
||||
bs.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'v_attach': 'bottom',
|
||||
'h_attach': 'right',
|
||||
'h_align': 'right',
|
||||
'flatness': 1.0,
|
||||
'vr_depth': -10,
|
||||
'shadow': 1.0 if vr_mode else 0.5,
|
||||
'color': color,
|
||||
'scale': scale,
|
||||
'position': (-260, 10) if vr_mode else (-10, 10),
|
||||
'text': text,
|
||||
},
|
||||
)
|
||||
)
|
||||
if not app.classic.main_menu_did_initial_transition:
|
||||
assert self.version.node
|
||||
bs.animate(self.version.node, 'opacity', {2.3: 0, 3.0: 1.0})
|
||||
|
||||
# Throw in test build info.
|
||||
self.beta_info = self.beta_info_2 = None
|
||||
if env.test and not (env.demo or env.arcade):
|
||||
if env.test:
|
||||
pos = (230, 35)
|
||||
self.beta_info = bs.NodeActor(
|
||||
bs.newnode(
|
||||
|
|
@ -292,125 +204,20 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
|
|||
self._update()
|
||||
|
||||
# Hopefully this won't hitch but lets space these out anyway.
|
||||
bui.add_clean_frame_callback(bs.WeakCall(self._start_preloads))
|
||||
bs.add_clean_frame_callback(bs.WeakCall(self._start_preloads))
|
||||
|
||||
random.seed()
|
||||
|
||||
if not (env.demo or env.arcade) and not app.ui_v1.use_toolbars:
|
||||
self._news = NewsDisplay(self)
|
||||
# Need to update this for toolbar mode; currenly doesn't fit.
|
||||
if bool(False):
|
||||
if not (env.demo or env.arcade):
|
||||
self._news = NewsDisplay(self)
|
||||
|
||||
self._attract_mode_timer = bs.Timer(
|
||||
3.12, self._update_attract_mode, repeat=True
|
||||
)
|
||||
|
||||
# Bring up the last place we were, or start at the main menu otherwise.
|
||||
with bs.ContextRef.empty():
|
||||
from bauiv1lib import specialoffer
|
||||
|
||||
assert bs.app.classic is not None
|
||||
if bui.app.env.headless:
|
||||
# UI stuff fails now in headless builds; avoid it.
|
||||
pass
|
||||
elif bool(False):
|
||||
uicontroller = bs.app.ui_v1.controller
|
||||
assert uicontroller is not None
|
||||
uicontroller.show_main_menu()
|
||||
else:
|
||||
main_menu_location = bs.app.ui_v1.get_main_menu_location()
|
||||
|
||||
# When coming back from a kiosk-mode game, jump to
|
||||
# the kiosk start screen.
|
||||
if env.demo or env.arcade:
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.kiosk import KioskWindow
|
||||
|
||||
bs.app.ui_v1.set_main_menu_window(
|
||||
KioskWindow().get_root_widget(),
|
||||
from_window=False, # Disable check here.
|
||||
)
|
||||
# ..or in normal cases go back to the main menu
|
||||
else:
|
||||
if main_menu_location == 'Gather':
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.gather import GatherWindow
|
||||
|
||||
bs.app.ui_v1.set_main_menu_window(
|
||||
GatherWindow(transition=None).get_root_widget(),
|
||||
from_window=False, # Disable check here.
|
||||
)
|
||||
elif main_menu_location == 'Watch':
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.watch import WatchWindow
|
||||
|
||||
bs.app.ui_v1.set_main_menu_window(
|
||||
WatchWindow(transition=None).get_root_widget(),
|
||||
from_window=False, # Disable check here.
|
||||
)
|
||||
elif main_menu_location == 'Team Game Select':
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.playlist.browser import (
|
||||
PlaylistBrowserWindow,
|
||||
)
|
||||
|
||||
bs.app.ui_v1.set_main_menu_window(
|
||||
PlaylistBrowserWindow(
|
||||
sessiontype=bs.DualTeamSession, transition=None
|
||||
).get_root_widget(),
|
||||
from_window=False, # Disable check here.
|
||||
)
|
||||
elif main_menu_location == 'Free-for-All Game Select':
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.playlist.browser import (
|
||||
PlaylistBrowserWindow,
|
||||
)
|
||||
|
||||
bs.app.ui_v1.set_main_menu_window(
|
||||
PlaylistBrowserWindow(
|
||||
sessiontype=bs.FreeForAllSession,
|
||||
transition=None,
|
||||
).get_root_widget(),
|
||||
from_window=False, # Disable check here.
|
||||
)
|
||||
elif main_menu_location == 'Coop Select':
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.coop.browser import CoopBrowserWindow
|
||||
|
||||
bs.app.ui_v1.set_main_menu_window(
|
||||
CoopBrowserWindow(
|
||||
transition=None
|
||||
).get_root_widget(),
|
||||
from_window=False, # Disable check here.
|
||||
)
|
||||
elif main_menu_location == 'Benchmarks & Stress Tests':
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.debug import DebugWindow
|
||||
|
||||
bs.app.ui_v1.set_main_menu_window(
|
||||
DebugWindow(transition=None).get_root_widget(),
|
||||
from_window=False, # Disable check here.
|
||||
)
|
||||
else:
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.mainmenu import MainMenuWindow
|
||||
|
||||
bs.app.ui_v1.set_main_menu_window(
|
||||
MainMenuWindow(transition=None).get_root_widget(),
|
||||
from_window=False, # Disable check here.
|
||||
)
|
||||
|
||||
# attempt to show any pending offers immediately.
|
||||
# If that doesn't work, try again in a few seconds
|
||||
# (we may not have heard back from the server)
|
||||
# ..if that doesn't work they'll just have to wait
|
||||
# until the next opportunity.
|
||||
if not specialoffer.show_offer():
|
||||
|
||||
def try_again() -> None:
|
||||
if not specialoffer.show_offer():
|
||||
# Try one last time..
|
||||
bui.apptimer(2.0, specialoffer.show_offer)
|
||||
|
||||
bui.apptimer(2.0, try_again)
|
||||
app.classic.invoke_main_menu_ui()
|
||||
|
||||
app.classic.main_menu_did_initial_transition = True
|
||||
|
||||
|
|
@ -445,7 +252,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
|
|||
y = 20
|
||||
base_scale = 1.1
|
||||
self._word_actors = []
|
||||
base_delay = 1.0
|
||||
base_delay = 0.8
|
||||
delay = base_delay
|
||||
delay_inc = 0.02
|
||||
|
||||
|
|
@ -628,6 +435,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
|
|||
word: str,
|
||||
x: float,
|
||||
y: float,
|
||||
*,
|
||||
scale: float = 1.0,
|
||||
delay: float = 0.0,
|
||||
vr_depth_offset: float = 0.0,
|
||||
|
|
@ -676,9 +484,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
|
|||
)
|
||||
self._word_actors.append(word_obj)
|
||||
|
||||
# Add a bit of stop-motion-y jitter to the logo
|
||||
# (unless we're in VR mode in which case its best to
|
||||
# leave things still).
|
||||
# Add a bit of stop-motion-y jitter to the logo (unless we're in
|
||||
# VR mode in which case its best to leave things still).
|
||||
if not bs.app.env.vr:
|
||||
cmb: bs.Node | None
|
||||
cmb2: bs.Node | None
|
||||
|
|
@ -757,13 +564,13 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
|
|||
y: float,
|
||||
scale: float,
|
||||
delay: float,
|
||||
*,
|
||||
custom_texture: str | None = None,
|
||||
jitter_scale: float = 1.0,
|
||||
rotate: float = 0.0,
|
||||
vr_depth_offset: float = 0.0,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-locals
|
||||
# Temp easter goodness.
|
||||
if custom_texture is None:
|
||||
custom_texture = self._get_custom_logo_tex_name()
|
||||
self._custom_logo_tex_name = custom_texture
|
||||
|
|
@ -776,60 +583,92 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
|
|||
if custom_texture is not None
|
||||
else bs.getmesh('logoTransparent')
|
||||
)
|
||||
logo = bs.NodeActor(
|
||||
bs.newnode(
|
||||
'image',
|
||||
attrs={
|
||||
'texture': ltex,
|
||||
'mesh_opaque': mopaque,
|
||||
'mesh_transparent': mtrans,
|
||||
'vr_depth': -10 + vr_depth_offset,
|
||||
'rotate': rotate,
|
||||
'attach': 'center',
|
||||
'tilt_translate': 0.21,
|
||||
'absolute_scale': True,
|
||||
},
|
||||
)
|
||||
)
|
||||
logo_attrs = {
|
||||
'position': (x, y),
|
||||
'texture': ltex,
|
||||
'mesh_opaque': mopaque,
|
||||
'mesh_transparent': mtrans,
|
||||
'vr_depth': -10 + vr_depth_offset,
|
||||
'rotate': rotate,
|
||||
'attach': 'center',
|
||||
'tilt_translate': 0.21,
|
||||
'absolute_scale': True,
|
||||
}
|
||||
if custom_texture is None:
|
||||
logo_attrs['scale'] = (2000.0, 2000.0)
|
||||
logo = bs.NodeActor(bs.newnode('image', attrs=logo_attrs))
|
||||
self._logo_node = logo.node
|
||||
self._word_actors.append(logo)
|
||||
|
||||
# Add a bit of stop-motion-y jitter to the logo
|
||||
# (unless we're in VR mode in which case its best to
|
||||
# leave things still).
|
||||
# Add a bit of stop-motion-y jitter to the logo (unless we're in
|
||||
# VR mode in which case its best to leave things still).
|
||||
assert logo.node
|
||||
if not bs.app.env.vr:
|
||||
|
||||
def jitter() -> None:
|
||||
if not bs.app.env.vr:
|
||||
cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2})
|
||||
cmb.connectattr('output', logo.node, 'position')
|
||||
keys = {}
|
||||
time_v = 0.0
|
||||
|
||||
# Gen some random keys for that stop-motion-y look
|
||||
for _i in range(10):
|
||||
keys[time_v] = (
|
||||
x + (random.random() - 0.5) * 0.7 * jitter_scale
|
||||
)
|
||||
time_v += random.random() * 0.1
|
||||
bs.animate(cmb, 'input0', keys, loop=True)
|
||||
keys = {}
|
||||
time_v = 0.0
|
||||
for _i in range(10):
|
||||
keys[time_v * self._ts] = (
|
||||
y + (random.random() - 0.5) * 0.7 * jitter_scale
|
||||
)
|
||||
time_v += random.random() * 0.1
|
||||
bs.animate(cmb, 'input1', keys, loop=True)
|
||||
|
||||
# Do a fun spinny animation on the logo the first time in.
|
||||
if (
|
||||
custom_texture is None
|
||||
and bs.app.classic is not None
|
||||
and not bs.app.classic.main_menu_did_initial_transition
|
||||
):
|
||||
jitter()
|
||||
cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2})
|
||||
cmb.connectattr('output', logo.node, 'position')
|
||||
keys = {}
|
||||
time_v = 0.0
|
||||
|
||||
# Gen some random keys for that stop-motion-y look
|
||||
for _i in range(10):
|
||||
keys[time_v] = x + (random.random() - 0.5) * 0.7 * jitter_scale
|
||||
time_v += random.random() * 0.1
|
||||
bs.animate(cmb, 'input0', keys, loop=True)
|
||||
keys = {}
|
||||
time_v = 0.0
|
||||
for _i in range(10):
|
||||
keys[time_v * self._ts] = (
|
||||
y + (random.random() - 0.5) * 0.7 * jitter_scale
|
||||
)
|
||||
time_v += random.random() * 0.1
|
||||
bs.animate(cmb, 'input1', keys, loop=True)
|
||||
delay = 0.0
|
||||
keys = {
|
||||
delay: 5000.0 * scale,
|
||||
delay + 0.4: 530.0 * scale,
|
||||
delay + 0.45: 620.0 * scale,
|
||||
delay + 0.5: 590.0 * scale,
|
||||
delay + 0.55: 605.0 * scale,
|
||||
delay + 0.6: 600.0 * scale,
|
||||
}
|
||||
bs.animate(cmb, 'input0', keys)
|
||||
bs.animate(cmb, 'input1', keys)
|
||||
cmb.connectattr('output', logo.node, 'scale')
|
||||
|
||||
keys = {
|
||||
delay: 100.0,
|
||||
delay + 0.4: 370.0,
|
||||
delay + 0.45: 357.0,
|
||||
delay + 0.5: 360.0,
|
||||
}
|
||||
bs.animate(logo.node, 'rotate', keys)
|
||||
else:
|
||||
logo.node.position = (x, y)
|
||||
# For all other cases do a simple scale up animation.
|
||||
jitter()
|
||||
cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2})
|
||||
|
||||
cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2})
|
||||
|
||||
keys = {
|
||||
delay: 0.0,
|
||||
delay + 0.1: 700.0 * scale,
|
||||
delay + 0.2: 600.0 * scale,
|
||||
}
|
||||
bs.animate(cmb, 'input0', keys)
|
||||
bs.animate(cmb, 'input1', keys)
|
||||
cmb.connectattr('output', logo.node, 'scale')
|
||||
keys = {
|
||||
delay: 0.0,
|
||||
delay + 0.1: 700.0 * scale,
|
||||
delay + 0.2: 600.0 * scale,
|
||||
}
|
||||
bs.animate(cmb, 'input0', keys)
|
||||
bs.animate(cmb, 'input1', keys)
|
||||
cmb.connectattr('output', logo.node, 'scale')
|
||||
|
||||
def _start_preloads(self) -> None:
|
||||
# FIXME: The func that calls us back doesn't save/restore state
|
||||
|
|
@ -879,8 +718,8 @@ class NewsDisplay:
|
|||
self._used_phrases: list[str] = []
|
||||
self._phrase_change_timer: bs.Timer | None = None
|
||||
|
||||
# If we're signed in, fetch news immediately.
|
||||
# Otherwise wait until we are signed in.
|
||||
# If we're signed in, fetch news immediately. Otherwise wait
|
||||
# until we are signed in.
|
||||
self._fetch_timer: bs.Timer | None = bs.Timer(
|
||||
1.0, bs.WeakCall(self._try_fetching_news), repeat=True
|
||||
)
|
||||
|
|
@ -913,8 +752,8 @@ class NewsDisplay:
|
|||
app = bs.app
|
||||
assert app.classic is not None
|
||||
|
||||
# If our news is way out of date, lets re-request it;
|
||||
# otherwise, rotate our phrase.
|
||||
# If our news is way out of date, lets re-request it; otherwise,
|
||||
# rotate our phrase.
|
||||
assert app.classic.main_menu_last_news_fetch_time is not None
|
||||
if time.time() - app.classic.main_menu_last_news_fetch_time > 600.0:
|
||||
self._fetch_news()
|
||||
|
|
@ -981,17 +820,16 @@ class NewsDisplay:
|
|||
self._text.node.text = val
|
||||
|
||||
def _got_news(self, news: str) -> None:
|
||||
# Run this stuff in the context of our activity since we
|
||||
# need to make nodes and stuff.. should fix the serverget
|
||||
# call so it.
|
||||
# Run this stuff in the context of our activity since we need to
|
||||
# make nodes and stuff.. should fix the serverget call so it.
|
||||
activity = self._activity()
|
||||
if activity is None or activity.expired:
|
||||
return
|
||||
with activity.context:
|
||||
self._phrases.clear()
|
||||
|
||||
# Show upcoming achievements in non-vr versions
|
||||
# (currently too hard to read in vr).
|
||||
# Show upcoming achievements in non-vr versions (currently
|
||||
# too hard to read in vr).
|
||||
self._used_phrases = (['__ACH__'] if not bs.app.env.vr else []) + [
|
||||
s for s in news.split('<br>\n') if s != ''
|
||||
]
|
||||
|
|
|
|||
24
dist/ba_data/python/bascenev1lib/maps.py
vendored
24
dist/ba_data/python/bascenev1lib/maps.py
vendored
|
|
@ -15,6 +15,30 @@ if TYPE_CHECKING:
|
|||
from typing import Any
|
||||
|
||||
|
||||
def register_all_maps() -> None:
|
||||
"""Registering all maps."""
|
||||
for maptype in [
|
||||
HockeyStadium,
|
||||
FootballStadium,
|
||||
Bridgit,
|
||||
BigG,
|
||||
Roundabout,
|
||||
MonkeyFace,
|
||||
ZigZag,
|
||||
ThePad,
|
||||
DoomShroom,
|
||||
LakeFrigid,
|
||||
TipTop,
|
||||
CragCastle,
|
||||
TowerD,
|
||||
HappyThoughts,
|
||||
StepRightUp,
|
||||
Courtyard,
|
||||
Rampage,
|
||||
]:
|
||||
bs.register_map(maptype)
|
||||
|
||||
|
||||
class HockeyStadium(bs.Map):
|
||||
"""Stadium map used for ice hockey games."""
|
||||
|
||||
|
|
|
|||
2
dist/ba_data/python/bascenev1lib/tutorial.py
vendored
2
dist/ba_data/python/bascenev1lib/tutorial.py
vendored
|
|
@ -515,6 +515,7 @@ class TutorialActivity(bs.Activity[Player, Team]):
|
|||
self,
|
||||
num: int,
|
||||
position: Sequence[float],
|
||||
*,
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0),
|
||||
make_current: bool = False,
|
||||
relative_to: int | None = None,
|
||||
|
|
@ -577,6 +578,7 @@ class TutorialActivity(bs.Activity[Player, Team]):
|
|||
self,
|
||||
num: int,
|
||||
position: Sequence[float],
|
||||
*,
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0),
|
||||
make_current: bool = False,
|
||||
relative_to: int | None = None,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue