syncing changes from ballistica/master

This commit is contained in:
Ayush Saini 2025-02-09 00:17:58 +05:30
parent 2a07c0c840
commit 8913080562
227 changed files with 15756 additions and 12772 deletions

View file

@ -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',

View file

@ -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'

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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()

View file

@ -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.
"""

View file

@ -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:

View file

@ -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)

View file

@ -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(

View 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',
)

View file

@ -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()

View file

@ -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.

View file

@ -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__

View file

@ -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()

View file

@ -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."""

View file

@ -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
View 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')

View file

@ -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

View file

@ -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'
):

View file

@ -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):

View file

@ -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'

View file

@ -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]