mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-11-07 17:36:15 +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
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]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue