1.7.32 ba_data update

This commit is contained in:
Ayush Saini 2023-12-21 15:55:50 +05:30
parent bf2f252ee5
commit 15393d5461
144 changed files with 4296 additions and 2411 deletions

View file

@ -27,7 +27,10 @@ from _babase import (
apptime,
apptimer,
AppTimer,
can_toggle_fullscreen,
fullscreen_control_available,
fullscreen_control_get,
fullscreen_control_key_shortcut,
fullscreen_control_set,
charstr,
clipboard_get_text,
clipboard_has_text,
@ -58,10 +61,8 @@ from _babase import (
in_logic_thread,
increment_analytics_count,
is_os_playing_music,
is_running_on_fire_tv,
is_xcode_build,
lock_all_input,
mac_music_app_get_library_source,
mac_music_app_get_playlists,
mac_music_app_get_volume,
mac_music_app_init,
@ -72,7 +73,10 @@ from _babase import (
music_player_set_volume,
music_player_shutdown,
music_player_stop,
native_review_request,
native_review_request_supported,
native_stack_trace,
open_file_externally,
print_load_info,
pushcall,
quit,
@ -82,7 +86,6 @@ from _babase import (
screenmessage,
set_analytics_screen,
set_low_level_config_value,
set_stress_testing,
set_thread_name,
set_ui_input_device,
show_progress_bar,
@ -114,6 +117,11 @@ from babase._apputils import (
AppHealthMonitor,
)
from babase._cloud import CloudSubsystem
from babase._devconsole import (
DevConsoleTab,
DevConsoleTabEntry,
DevConsoleSubsystem,
)
from babase._emptyappmode import EmptyAppMode
from babase._error import (
print_exception,
@ -146,9 +154,8 @@ from babase._general import (
getclass,
get_type_name,
)
from babase._keyboard import Keyboard
from babase._language import Lstr, LanguageSubsystem
from babase._login import LoginAdapter
from babase._login import LoginAdapter, LoginInfo
# noinspection PyProtectedMember
# (PyCharm inspection bug?)
@ -157,6 +164,7 @@ from babase._mgen.enums import (
SpecialChar,
InputType,
UIScale,
QuitType,
)
from babase._math import normalized_color, is_point_in_box, vec3validate
from babase._meta import MetadataSubsystem
@ -194,7 +202,10 @@ __all__ = [
'apptimer',
'AppTimer',
'Call',
'can_toggle_fullscreen',
'fullscreen_control_available',
'fullscreen_control_get',
'fullscreen_control_key_shortcut',
'fullscreen_control_set',
'charstr',
'clipboard_get_text',
'clipboard_has_text',
@ -206,6 +217,9 @@ __all__ = [
'ContextError',
'ContextRef',
'DelegateNotFoundError',
'DevConsoleTab',
'DevConsoleTabEntry',
'DevConsoleSubsystem',
'DisplayTime',
'displaytime',
'displaytimer',
@ -244,14 +258,12 @@ __all__ = [
'is_browser_likely_available',
'is_os_playing_music',
'is_point_in_box',
'is_running_on_fire_tv',
'is_xcode_build',
'Keyboard',
'LanguageSubsystem',
'lock_all_input',
'LoginAdapter',
'LoginInfo',
'Lstr',
'mac_music_app_get_library_source',
'mac_music_app_get_playlists',
'mac_music_app_get_volume',
'mac_music_app_init',
@ -264,10 +276,13 @@ __all__ = [
'music_player_set_volume',
'music_player_shutdown',
'music_player_stop',
'native_review_request',
'native_review_request_supported',
'native_stack_trace',
'NodeNotFoundError',
'normalized_color',
'NotFoundError',
'open_file_externally',
'Permission',
'PlayerNotFoundError',
'Plugin',
@ -278,6 +293,7 @@ __all__ = [
'print_load_info',
'pushcall',
'quit',
'QuitType',
'reload_media',
'request_permission',
'safecolor',
@ -287,7 +303,6 @@ __all__ = [
'SessionTeamNotFoundError',
'set_analytics_screen',
'set_low_level_config_value',
'set_stress_testing',
'set_thread_name',
'set_ui_input_device',
'show_progress_bar',

View file

@ -6,7 +6,7 @@ from __future__ import annotations
import hashlib
import logging
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, assert_never
from efro.call import tpartial
from efro.error import CommunicationError
@ -16,7 +16,7 @@ import _babase
if TYPE_CHECKING:
from typing import Any
from babase._login import LoginAdapter
from babase._login import LoginAdapter, LoginInfo
DEBUG_LOG = False
@ -27,10 +27,12 @@ class AccountV2Subsystem:
Category: **App Classes**
Access the single shared instance of this class at 'ba.app.accounts'.
Access the single shared instance of this class at 'ba.app.plus.accounts'.
"""
def __init__(self) -> None:
from babase._login import LoginAdapterGPGS, LoginAdapterGameCenter
# Whether or not everything related to an initial login
# (or lack thereof) has completed. This includes things like
# workspace syncing. Completion of this is what flips the app
@ -45,16 +47,13 @@ class AccountV2Subsystem:
self._implicit_state_changed = False
self._can_do_auto_sign_in = True
if _babase.app.classic is None:
raise RuntimeError('Needs updating for no-classic case.')
if (
_babase.app.classic.platform == 'android'
and _babase.app.classic.subplatform == 'google'
):
from babase._login import LoginAdapterGPGS
self.login_adapters[LoginType.GPGS] = LoginAdapterGPGS()
adapter: LoginAdapter
if _babase.using_google_play_game_services():
adapter = LoginAdapterGPGS()
self.login_adapters[adapter.login_type] = adapter
if _babase.using_game_center():
adapter = LoginAdapterGameCenter()
self.login_adapters[adapter.login_type] = adapter
def on_app_loading(self) -> None:
"""Should be called at standard on_app_loading time."""
@ -62,10 +61,6 @@ class AccountV2Subsystem:
for adapter in self.login_adapters.values():
adapter.on_app_loading()
def set_primary_credentials(self, credentials: str | None) -> None:
"""Set credentials for the primary app account."""
raise NotImplementedError('This should be overridden.')
def have_primary_credentials(self) -> bool:
"""Are credentials currently set for the primary app account?
@ -80,10 +75,6 @@ class AccountV2Subsystem:
"""The primary account for the app, or None if not logged in."""
return self.do_get_primary()
def do_get_primary(self) -> AccountV2Handle | None:
"""Internal - should be overridden by subclass."""
return None
def on_primary_account_changed(
self, account: AccountV2Handle | None
) -> None:
@ -142,6 +133,8 @@ class AccountV2Subsystem:
"""An implicit sign-in happened (called by native layer)."""
from babase._login import LoginAdapter
assert _babase.in_logic_thread()
with _babase.ContextRef.empty():
self.login_adapters[login_type].set_implicit_login_state(
LoginAdapter.ImplicitLoginState(
@ -151,6 +144,7 @@ class AccountV2Subsystem:
def on_implicit_sign_out(self, login_type: LoginType) -> None:
"""An implicit sign-out happened (called by native layer)."""
assert _babase.in_logic_thread()
with _babase.ContextRef.empty():
self.login_adapters[login_type].set_implicit_login_state(None)
@ -192,9 +186,10 @@ class AccountV2Subsystem:
cfgkey = 'ImplicitLoginStates'
cfgdict = _babase.app.config.setdefault(cfgkey, {})
# Store which (if any) adapter is currently implicitly signed in.
# Making the assumption there will only ever be one implicit
# adapter at a time; may need to update this if that changes.
# Store which (if any) adapter is currently implicitly signed
# in. Making the assumption there will only ever be one implicit
# adapter at a time; may need to revisit this logic if that
# changes.
prev_state = cfgdict.get(login_type.value)
if state is None:
self._implicit_signed_in_adapter = None
@ -205,18 +200,26 @@ class AccountV2Subsystem:
state.login_id
)
# Special case: if the user is already signed in but not with
# this implicit login, we may want to let them know that the
# 'Welcome back FOO' they likely just saw is not actually
# accurate.
# Special case: if the user is already signed in but not
# with this implicit login, let them know that the 'Welcome
# back FOO' they likely just saw is not actually accurate.
if (
self.primary is not None
and not self.login_adapters[login_type].is_back_end_active()
):
service_str: Lstr | None
if login_type is LoginType.GPGS:
service_str = Lstr(resource='googlePlayText')
else:
elif login_type is LoginType.GAME_CENTER:
# Note: Apparently Game Center is just called 'Game
# Center' in all languages. Can revisit if not true.
# https://developer.apple.com/forums/thread/725779
service_str = Lstr(value='Game Center')
elif login_type is LoginType.EMAIL:
# Not possible; just here for exhaustive coverage.
service_str = None
else:
assert_never(login_type)
if service_str is not None:
_babase.apptimer(
2.0,
@ -259,6 +262,14 @@ class AccountV2Subsystem:
# We may want to auto-sign-in based on this new state.
self._update_auto_sign_in()
def do_get_primary(self) -> AccountV2Handle | None:
"""Internal - should be overridden by subclass."""
raise NotImplementedError('This should be overridden.')
def set_primary_credentials(self, credentials: str | None) -> None:
"""Set credentials for the primary app account."""
raise NotImplementedError('This should be overridden.')
def _update_auto_sign_in(self) -> None:
plus = _babase.app.plus
assert plus is not None
@ -266,7 +277,7 @@ class AccountV2Subsystem:
# If implicit state has changed, try to respond.
if self._implicit_state_changed:
if self._implicit_signed_in_adapter is None:
# If implicit back-end is signed out, follow suit
# If implicit back-end has signed out, we follow suit
# immediately; no need to wait for network connectivity.
if DEBUG_LOG:
logging.debug(
@ -286,9 +297,8 @@ class AccountV2Subsystem:
# Consider this an 'explicit' sign in because the
# implicit-login state change presumably was triggered
# by some user action (signing in, signing out, or
# switching accounts via the back-end).
# NOTE: should test case where we don't have
# connectivity here.
# 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(
@ -419,14 +429,11 @@ class AccountV2Handle:
used with some operations such as cloud messaging.
"""
def __init__(self) -> None:
self.tag = '?'
self.workspacename: str | None = None
self.workspaceid: str | None = None
# Login types and their display-names associated with this account.
self.logins: dict[LoginType, str] = {}
accountid: str
tag: str
workspacename: str | None
workspaceid: str | None
logins: dict[LoginType, LoginInfo]
def __enter__(self) -> None:
"""Support for "with" statement.

View file

@ -1,12 +1,10 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the high level state of the app."""
# pylint: disable=too-many-lines
from __future__ import annotations
import os
import logging
import warnings
from enum import Enum
from typing import TYPE_CHECKING
from concurrent.futures import ThreadPoolExecutor
@ -24,6 +22,7 @@ from babase._appcomponent import AppComponentSubsystem
from babase._appmodeselector import AppModeSelector
from babase._appintent import AppIntentDefault, AppIntentExec
from babase._stringedit import StringEditSubsystem
from babase._devconsole import DevConsoleSubsystem
if TYPE_CHECKING:
import asyncio
@ -57,6 +56,8 @@ class App:
# pylint: disable=too-many-public-methods
# A few things defined as non-optional values but not actually
# available until the app starts.
plugins: PluginSubsystem
lang: LanguageSubsystem
health_monitor: AppHealthMonitor
@ -71,7 +72,7 @@ class App:
# The app has not yet begun starting and should not be used in
# any way.
NOT_RUNNING = 0
NOT_STARTED = 0
# The native layer is spinning up its machinery (screens,
# renderers, etc.). Nothing should happen in the Python layer
@ -91,13 +92,23 @@ class App:
# All pieces are in place and the app is now doing its thing.
RUNNING = 4
# The app is backgrounded or otherwise suspended.
PAUSED = 5
# Used on platforms such as mobile where the app basically needs
# to shut down while backgrounded. In this state, all event
# loops are suspended and all graphics and audio must cease
# completely. Be aware that the suspended state can be entered
# from any other state including NATIVE_BOOTSTRAPPING and
# SHUTTING_DOWN.
SUSPENDED = 5
# The app is shutting down.
# The app is shutting down. This process may involve sending
# network messages or other things that can take up to a few
# seconds, so ideally graphics and audio should remain
# functional (with fades or spinners or whatever to show
# something is happening).
SHUTTING_DOWN = 6
# The app has completed shutdown.
# The app has completed shutdown. Any code running here should
# be basically immediate.
SHUTDOWN_COMPLETE = 7
class DefaultAppModeSelector(AppModeSelector):
@ -140,9 +151,9 @@ class App:
def __init__(self) -> None:
"""(internal)
Do not instantiate this class; access the single shared instance
of it as 'app' which is available in various Ballistica
feature-set modules such as babase.
Do not instantiate this class. You can access the single shared
instance of it through various high level packages: 'babase.app',
'bascenev1.app', 'bauiv1.app', etc.
"""
# Hack for docs-generation: we can be imported with dummy modules
@ -151,32 +162,35 @@ class App:
return
self.env: babase.Env = _babase.Env()
self.state = self.State.NOT_RUNNING
self.state = self.State.NOT_STARTED
# Default executor which can be used for misc background
# 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(thread_name_prefix='baworker')
self.threadpool = ThreadPoolExecutor(
thread_name_prefix='baworker',
initializer=self._thread_pool_thread_init,
)
self.meta = MetadataSubsystem()
self.net = NetworkSubsystem()
self.workspaces = WorkspaceSubsystem()
self.components = AppComponentSubsystem()
self.stringedit = StringEditSubsystem()
self.devconsole = DevConsoleSubsystem()
# This is incremented any time the app is backgrounded or
# foregrounded; can be a simple way to determine if network data
# should be refreshed/etc.
self.fg_state = 0
self.config_file_healthy: bool = False
self._subsystems: list[AppSubsystem] = []
self._native_bootstrapping_completed = False
self._init_completed = False
self._meta_scan_completed = False
self._native_start_called = False
self._native_paused = False
self._native_suspended = False
self._native_shutdown_called = False
self._native_shutdown_complete_called = False
self._initial_sign_in_completed = False
@ -194,8 +208,11 @@ class App:
self._mode_selector: babase.AppModeSelector | None = None
self._shutdown_task: asyncio.Task[None] | None = None
self._shutdown_tasks: list[Coroutine[None, None, None]] = [
self._wait_for_shutdown_suppressions()
self._wait_for_shutdown_suppressions(),
self._fade_and_shutdown_graphics(),
self._fade_and_shutdown_audio(),
]
self._pool_thread_count = 0
def postinit(self) -> None:
"""Called after we've been inited and assigned to babase.app.
@ -212,6 +229,15 @@ class App:
self.lang = LanguageSubsystem()
self.plugins = PluginSubsystem()
@property
def active(self) -> bool:
"""Whether the app is currently front and center.
This will be False when the app is hidden, other activities
are covering it, etc. (depending on the platform).
"""
return _babase.app_is_active()
@property
def aioloop(self) -> asyncio.AbstractEventLoop:
"""The logic thread's asyncio event loop.
@ -311,7 +337,7 @@ class App:
def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
"""Add a task to be run on app shutdown.
Note that tasks will be killed after
Note that shutdown tasks will be canceled after
App.SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.
"""
if (
@ -385,18 +411,18 @@ class App:
self._native_bootstrapping_completed = True
self._update_state()
def on_native_pause(self) -> None:
"""Called by the native layer when the app pauses."""
def on_native_suspend(self) -> None:
"""Called by the native layer when the app is suspended."""
assert _babase.in_logic_thread()
assert not self._native_paused # Should avoid redundant calls.
self._native_paused = True
assert not self._native_suspended # Should avoid redundant calls.
self._native_suspended = True
self._update_state()
def on_native_resume(self) -> None:
"""Called by the native layer when the app resumes."""
def on_native_unsuspend(self) -> None:
"""Called by the native layer when the app suspension ends."""
assert _babase.in_logic_thread()
assert self._native_paused # Should avoid redundant calls.
self._native_paused = False
assert self._native_suspended # Should avoid redundant calls.
self._native_suspended = False
self._update_state()
def on_native_shutdown(self) -> None:
@ -415,7 +441,7 @@ class App:
"""(internal)"""
from babase._appconfig import read_app_config
self._config, self.config_file_healthy = read_app_config()
self._config = read_app_config()
def handle_deep_link(self, url: str) -> None:
"""Handle a deep link URL."""
@ -493,7 +519,7 @@ class App:
except Exception:
logging.exception('Error setting app intent to %s.', intent)
_babase.pushcall(
tpartial(self._apply_intent_error, intent),
tpartial(self._display_set_intent_error, intent),
from_other_thread=True,
)
@ -538,10 +564,11 @@ class App:
'Error handling intent %s in app-mode %s.', intent, mode
)
def _apply_intent_error(self, intent: AppIntent) -> None:
def _display_set_intent_error(self, intent: AppIntent) -> None:
"""Show the *user* something went wrong setting an intent."""
from babase._language import Lstr
del intent # Unused.
del intent
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
_babase.getsimplesound('error').play()
@ -564,19 +591,6 @@ class App:
self._aioloop = _asyncio.setup_asyncio()
self.health_monitor = AppHealthMonitor()
# Only proceed if our config file is healthy so we don't
# overwrite a broken one or whatnot and wipe out data.
if not self.config_file_healthy:
if self.classic is not None:
handled = self.classic.show_config_error_window()
if handled:
return
# For now on other systems we just overwrite the bum config.
# At this point settings are already set; lets just commit
# them to disk.
_appconfig.commit_app_config(force=True)
# __FEATURESET_APP_SUBSYSTEM_CREATE_BEGIN__
# This section generated by batools.appmodule; do not edit.
@ -726,15 +740,15 @@ class App:
_babase.lifecyclelog('app state shutting down')
self._on_shutting_down()
elif self._native_paused:
# Entering paused state:
if self.state is not self.State.PAUSED:
self.state = self.State.PAUSED
self._on_pause()
elif self._native_suspended:
# Entering suspended state:
if self.state is not self.State.SUSPENDED:
self.state = self.State.SUSPENDED
self._on_suspend()
else:
# Leaving paused state:
if self.state is self.State.PAUSED:
self._on_resume()
# Leaving suspended state:
if self.state is self.State.SUSPENDED:
self._on_unsuspend()
# Entering or returning to running state
if self._initial_sign_in_completed and self._meta_scan_completed:
@ -768,7 +782,7 @@ class App:
self.state = self.State.NATIVE_BOOTSTRAPPING
_babase.lifecyclelog('app state native bootstrapping')
else:
# Only logical possibility left is NOT_RUNNING, in which
# Only logical possibility left is NOT_STARTED, in which
# case we should not be getting called.
logging.warning(
'App._update_state called while in %s state;'
@ -780,6 +794,7 @@ class App:
async def _shutdown(self) -> None:
import asyncio
_babase.lock_all_input()
try:
async with asyncio.TaskGroup() as task_group:
for task_coro in self._shutdown_tasks:
@ -809,33 +824,33 @@ class App:
try:
await asyncio.wait_for(task, self.SHUTDOWN_TASK_TIMEOUT_SECONDS)
except Exception:
logging.exception('Error in shutdown task.')
logging.exception('Error in shutdown task (%s).', coro)
def _on_pause(self) -> None:
"""Called when the app goes to a paused state."""
def _on_suspend(self) -> None:
"""Called when the app goes to a suspended state."""
assert _babase.in_logic_thread()
# Pause all app subsystems in the opposite order they were inited.
# Suspend all app subsystems in the opposite order they were inited.
for subsystem in reversed(self._subsystems):
try:
subsystem.on_app_pause()
subsystem.on_app_suspend()
except Exception:
logging.exception(
'Error in on_app_pause for subsystem %s.', subsystem
'Error in on_app_suspend for subsystem %s.', subsystem
)
def _on_resume(self) -> None:
"""Called when resuming."""
def _on_unsuspend(self) -> None:
"""Called when unsuspending."""
assert _babase.in_logic_thread()
self.fg_state += 1
# Resume all app subsystems in the same order they were inited.
# Unsuspend all app subsystems in the same order they were inited.
for subsystem in self._subsystems:
try:
subsystem.on_app_resume()
subsystem.on_app_unsuspend()
except Exception:
logging.exception(
'Error in on_app_resume for subsystem %s.', subsystem
'Error in on_app_unsuspend for subsystem %s.', subsystem
)
def _on_shutting_down(self) -> None:
@ -875,10 +890,45 @@ class App:
import asyncio
# Spin and wait for anything blocking shutdown to complete.
starttime = _babase.apptime()
_babase.lifecyclelog('shutdown-suppress wait begin')
while _babase.shutdown_suppress_count() > 0:
await asyncio.sleep(0.001)
_babase.lifecyclelog('shutdown-suppress wait end')
duration = _babase.apptime() - starttime
if duration > 1.0:
logging.warning(
'Shutdown-suppressions lasted longer than ideal '
'(%.2f seconds).',
duration,
)
async def _fade_and_shutdown_graphics(self) -> None:
import asyncio
# Kick off a short fade and give it time to complete.
_babase.lifecyclelog('fade-and-shutdown-graphics begin')
_babase.fade_screen(False, time=0.15)
await asyncio.sleep(0.15)
# Now tell the graphics system to go down and wait until
# it has done so.
_babase.graphics_shutdown_begin()
while not _babase.graphics_shutdown_is_complete():
await asyncio.sleep(0.01)
_babase.lifecyclelog('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')
_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:
@ -888,243 +938,7 @@ class App:
'Error in work submitted via threadpool_submit_no_wait()'
)
# --------------------------------------------------------------------
# THE FOLLOWING ARE DEPRECATED AND WILL BE REMOVED IN A FUTURE UPDATE.
# --------------------------------------------------------------------
@property
def build_number(self) -> int:
"""Integer build number.
This value increases by at least 1 with each release of the engine.
It is independent of the human readable babase.App.version string.
"""
warnings.warn(
'app.build_number is deprecated; use app.env.build_number',
DeprecationWarning,
stacklevel=2,
)
return self.env.build_number
@property
def device_name(self) -> str:
"""Name of the device running the app."""
warnings.warn(
'app.device_name is deprecated; use app.env.device_name',
DeprecationWarning,
stacklevel=2,
)
return self.env.device_name
@property
def config_file_path(self) -> str:
"""Where the app's config file is stored on disk."""
warnings.warn(
'app.config_file_path is deprecated;'
' use app.env.config_file_path',
DeprecationWarning,
stacklevel=2,
)
return self.env.config_file_path
@property
def version(self) -> str:
"""Human-readable engine version string; something like '1.3.24'.
This should not be interpreted as a number; it may contain
string elements such as 'alpha', 'beta', 'test', etc.
If a numeric version is needed, use `build_number`.
"""
warnings.warn(
'app.version is deprecated; use app.env.version',
DeprecationWarning,
stacklevel=2,
)
return self.env.version
@property
def debug_build(self) -> bool:
"""Whether the app was compiled in debug mode.
Debug builds generally run substantially slower than non-debug
builds due to compiler optimizations being disabled and extra
checks being run.
"""
warnings.warn(
'app.debug_build is deprecated; use app.env.debug',
DeprecationWarning,
stacklevel=2,
)
return self.env.debug
@property
def test_build(self) -> bool:
"""Whether the app was compiled in test mode.
Test mode enables extra checks and features that are useful for
release testing but which do not slow the game down significantly.
"""
warnings.warn(
'app.test_build is deprecated; use app.env.test',
DeprecationWarning,
stacklevel=2,
)
return self.env.test
@property
def data_directory(self) -> str:
"""Path where static app data lives."""
warnings.warn(
'app.data_directory is deprecated; use app.env.data_directory',
DeprecationWarning,
stacklevel=2,
)
return self.env.data_directory
@property
def python_directory_user(self) -> str | None:
"""Path where the app expects its user scripts (mods) to live.
Be aware that this value may be None if ballistica is running in
a non-standard environment, and that python-path modifications may
cause modules to be loaded from other locations.
"""
warnings.warn(
'app.python_directory_user is deprecated;'
' use app.env.python_directory_user',
DeprecationWarning,
stacklevel=2,
)
return self.env.python_directory_user
@property
def python_directory_app(self) -> str | None:
"""Path where the app expects its bundled modules to live.
Be aware that this value may be None if Ballistica is running in
a non-standard environment, and that python-path modifications may
cause modules to be loaded from other locations.
"""
warnings.warn(
'app.python_directory_app is deprecated;'
' use app.env.python_directory_app',
DeprecationWarning,
stacklevel=2,
)
return self.env.python_directory_app
@property
def python_directory_app_site(self) -> str | None:
"""Path where the app expects its bundled pip modules to live.
Be aware that this value may be None if Ballistica is running in
a non-standard environment, and that python-path modifications may
cause modules to be loaded from other locations.
"""
warnings.warn(
'app.python_directory_app_site is deprecated;'
' use app.env.python_directory_app_site',
DeprecationWarning,
stacklevel=2,
)
return self.env.python_directory_app_site
@property
def api_version(self) -> int:
"""The app's api version.
Only Python modules and packages associated with the current API
version number will be detected by the game (see the ba_meta tag).
This value will change whenever substantial backward-incompatible
changes are introduced to ballistica APIs. When that happens,
modules/packages should be updated accordingly and set to target
the newer API version number.
"""
warnings.warn(
'app.api_version is deprecated; use app.env.api_version',
DeprecationWarning,
stacklevel=2,
)
return self.env.api_version
@property
def on_tv(self) -> bool:
"""Whether the app is currently running on a TV."""
warnings.warn(
'app.on_tv is deprecated; use app.env.tv',
DeprecationWarning,
stacklevel=2,
)
return self.env.tv
@property
def vr_mode(self) -> bool:
"""Whether the app is currently running in VR."""
warnings.warn(
'app.vr_mode is deprecated; use app.env.vr',
DeprecationWarning,
stacklevel=2,
)
return self.env.vr
# __SPINOFF_REQUIRE_UI_V1_BEGIN__
@property
def toolbar_test(self) -> bool:
"""(internal)."""
warnings.warn(
'app.toolbar_test is deprecated; use app.ui_v1.use_toolbars',
DeprecationWarning,
stacklevel=2,
)
return self.ui_v1.use_toolbars
# __SPINOFF_REQUIRE_UI_V1_END__
@property
def arcade_mode(self) -> bool:
"""Whether the app is currently running on arcade hardware."""
warnings.warn(
'app.arcade_mode is deprecated; use app.env.arcade',
DeprecationWarning,
stacklevel=2,
)
return self.env.arcade
@property
def headless_mode(self) -> bool:
"""Whether the app is running headlessly."""
warnings.warn(
'app.headless_mode is deprecated; use app.env.headless',
DeprecationWarning,
stacklevel=2,
)
return self.env.headless
@property
def demo_mode(self) -> bool:
"""Whether the app is targeting a demo experience."""
warnings.warn(
'app.demo_mode is deprecated; use app.env.demo',
DeprecationWarning,
stacklevel=2,
)
return self.env.demo
# __SPINOFF_REQUIRE_SCENE_V1_BEGIN__
@property
def protocol_version(self) -> int:
"""(internal)."""
# pylint: disable=cyclic-import
import bascenev1
warnings.warn(
'app.protocol_version is deprecated;'
' use bascenev1.protocol_version()',
DeprecationWarning,
stacklevel=2,
)
return bascenev1.protocol_version()
# __SPINOFF_REQUIRE_SCENE_V1_END__
def _thread_pool_thread_init(self) -> None:
# Help keep things clear in profiling tools/etc.
self._pool_thread_count += 1
_babase.set_thread_name(f'ballistica worker-{self._pool_thread_count}')

View file

@ -101,15 +101,13 @@ class AppConfig(dict):
self.commit()
def read_app_config() -> tuple[AppConfig, bool]:
def read_app_config() -> AppConfig:
"""Read the app config."""
import os
import json
config_file_healthy = False
# NOTE: it is assumed that this only gets called once and the
# config object will not change from here on out
# 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:
@ -119,20 +117,16 @@ def read_app_config() -> tuple[AppConfig, bool]:
config = AppConfig(json.loads(config_contents))
else:
config = AppConfig()
config_file_healthy = True
except Exception:
logging.exception(
"Error reading config file at time %.3f: '%s'.",
"Error reading config file '%s' at time %.3f.\n"
"Backing up broken config to'%s.broken'.",
config_file_path,
_babase.apptime(),
config_file_path,
)
# Whenever this happens lets back up the broken one just in case it
# gets overwritten accidentally.
logging.info(
"Backing up current config file to '%s.broken'", config_file_path
)
try:
import shutil
@ -141,23 +135,10 @@ def read_app_config() -> tuple[AppConfig, bool]:
logging.exception('Error copying broken config.')
config = AppConfig()
# Now attempt to read one of our 'prev' backup copies.
prev_path = config_file_path + '.prev'
try:
if os.path.exists(prev_path):
with open(prev_path, encoding='utf-8') as infile:
config_contents = infile.read()
config = AppConfig(json.loads(config_contents))
else:
config = AppConfig()
config_file_healthy = True
logging.info('Successfully read backup config.')
except Exception:
logging.exception('Error reading prev backup config.')
return config, config_file_healthy
return config
def commit_app_config(force: bool = False) -> None:
def commit_app_config() -> None:
"""Commit the config to persistent storage.
Category: **General Utility Functions**
@ -167,10 +148,4 @@ def commit_app_config(force: bool = False) -> None:
plus = _babase.app.plus
assert plus is not None
if not _babase.app.config_file_healthy and not force:
logging.warning(
'Current config file is broken; '
'skipping write to avoid losing settings.'
)
return
plus.mark_config_dirty()

View file

@ -31,6 +31,7 @@ class AppMode:
AppExperience associated with the AppMode must be supported by
the current app and runtime environment.
"""
# FIXME: check AppExperience.
return cls._supports_intent(intent)
@classmethod

View file

@ -39,10 +39,10 @@ class AppSubsystem:
def on_app_running(self) -> None:
"""Called when the app reaches the running state."""
def on_app_pause(self) -> None:
def on_app_suspend(self) -> None:
"""Called when the app enters the paused state."""
def on_app_resume(self) -> None:
def on_app_unsuspend(self) -> None:
"""Called when the app exits the paused state."""
def on_app_shutdown(self) -> None:

View file

@ -64,7 +64,9 @@ def get_remote_app_name() -> babase.Lstr:
def should_submit_debug_info() -> bool:
"""(internal)"""
return _babase.app.config.get('Submit Debug Info', True)
val = _babase.app.config.get('Submit Debug Info', True)
assert isinstance(val, bool)
return val
def handle_v1_cloud_log() -> None:
@ -323,7 +325,7 @@ def dump_app_state(
)
def log_dumped_app_state() -> None:
def log_dumped_app_state(from_previous_run: bool = False) -> None:
"""If an app-state dump exists, log it and clear it. No-op otherwise."""
try:
@ -350,8 +352,13 @@ def log_dumped_app_state() -> None:
metadata = dataclass_from_json(DumpedAppStateMetadata, appstatedata)
header = (
'Found app state dump from previous app run'
if from_previous_run
else 'App state dump'
)
out += (
f'App state dump:\nReason: {metadata.reason}\n'
f'{header}:\nReason: {metadata.reason}\n'
f'Time: {metadata.app_time:.2f}'
)
tbpath = os.path.join(
@ -381,9 +388,10 @@ class AppHealthMonitor(AppSubsystem):
def on_app_loading(self) -> None:
# If any traceback dumps happened last run, log and clear them.
log_dumped_app_state()
log_dumped_app_state(from_previous_run=True)
def _app_monitor_thread_main(self) -> None:
_babase.set_thread_name('ballistica app-monitor')
try:
self._monitor_app()
except Exception:
@ -441,10 +449,10 @@ class AppHealthMonitor(AppSubsystem):
self._first_check = False
def on_app_pause(self) -> None:
def on_app_suspend(self) -> None:
assert _babase.in_logic_thread()
self._running = False
def on_app_resume(self) -> None:
def on_app_unsuspend(self) -> None:
assert _babase.in_logic_thread()
self._running = True

View file

@ -26,6 +26,11 @@ DEBUG_LOG = False
class CloudSubsystem(AppSubsystem):
"""Manages communication with cloud components."""
@property
def connected(self) -> bool:
"""Property equivalent of CloudSubsystem.is_connected()."""
return self.is_connected()
def is_connected(self) -> bool:
"""Return whether a connection to the cloud is present.

View file

@ -0,0 +1,188 @@
# Released under the MIT License. See LICENSE for details.
#
"""Dev-Console functionality."""
from __future__ import annotations
import os
from typing import TYPE_CHECKING
from dataclasses import dataclass
import logging
import _babase
if TYPE_CHECKING:
from typing import Callable, Any, Literal
class DevConsoleTab:
"""Defines behavior for a tab in the dev-console."""
def refresh(self) -> None:
"""Called when the tab should refresh itself."""
def request_refresh(self) -> None:
"""The tab can call this to request that it be refreshed."""
_babase.dev_console_request_refresh()
def button(
self,
label: str,
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',
) -> None:
"""Add a button to the tab being refreshed."""
assert _babase.app.devconsole.is_refreshing
_babase.dev_console_add_button(
label,
pos[0],
pos[1],
size[0],
size[1],
call,
h_anchor,
label_scale,
corner_radius,
style,
)
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',
scale: float = 1.0,
) -> None:
"""Add a button to the tab being refreshed."""
assert _babase.app.devconsole.is_refreshing
_babase.dev_console_add_text(
text, pos[0], pos[1], h_anchor, h_align, v_align, scale
)
def python_terminal(self) -> None:
"""Add a Python Terminal to the tab being refreshed."""
assert _babase.app.devconsole.is_refreshing
_babase.dev_console_add_python_terminal()
@property
def width(self) -> float:
"""Return the current tab width. Only call during refreshes."""
assert _babase.app.devconsole.is_refreshing
return _babase.dev_console_tab_width()
@property
def height(self) -> float:
"""Return the current tab height. Only call during refreshes."""
assert _babase.app.devconsole.is_refreshing
return _babase.dev_console_tab_height()
@property
def base_scale(self) -> float:
"""A scale value set depending on the app's UI scale.
Dev-console tabs can incorporate this into their UI sizes and
positions if they desire. This must be done manually however.
"""
assert _babase.app.devconsole.is_refreshing
return _babase.dev_console_base_scale()
class DevConsoleTabPython(DevConsoleTab):
"""The Python dev-console tab."""
def refresh(self) -> None:
self.python_terminal()
class DevConsoleTabTest(DevConsoleTab):
"""Test dev-console tab."""
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."""
name: str
factory: Callable[[], DevConsoleTab]
class DevConsoleSubsystem:
"""Subsystem for wrangling the dev console.
The single instance of this class can be found at
babase.app.devconsole. The dev-console is a simple always-available
UI intended for use by developers; not end users. Traditionally it
is available by typing a backtick (`) key on a keyboard, but now can
be accessed via an on-screen button (see settings/advanced to enable
said button).
"""
def __init__(self) -> None:
# All tabs in the dev-console. Add your own stuff here via
# plugins or whatnot.
self.tabs: list[DevConsoleTabEntry] = [
DevConsoleTabEntry('Python', DevConsoleTabPython)
]
if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1':
self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest))
self.is_refreshing = False
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
if tab is None:
logging.error(
'DevConsole got refresh request for tab'
" '%s' which does not exist.",
tabname,
)
return
self.is_refreshing = True
try:
tab.refresh()
finally:
self.is_refreshing = False

View file

@ -40,6 +40,11 @@ def on_native_module_import() -> None:
if envconfig.log_handler is not None:
_feed_logs_to_babase(envconfig.log_handler)
# Also let's name the log-handler thread to help in profiling.
envconfig.log_handler.call_in_thread(
lambda: _babase.set_thread_name('ballistica logging')
)
env = _babase.pre_env()
# Give a soft warning if we're being used with a different binary
@ -180,10 +185,8 @@ def _feed_logs_to_babase(log_handler: LogHandler) -> None:
def _on_log(entry: LogEntry) -> None:
# Forward this along to the engine to display in the in-app
# console, in the Android log, etc.
_babase.display_log(
name=entry.name,
level=entry.level.name,
message=entry.message,
_babase.emit_log(
name=entry.name, level=entry.level.name, message=entry.message
)
# We also want to feed some logs to the old v1-cloud-log system.

View file

@ -33,18 +33,47 @@ def reset_to_main_menu() -> None:
logging.warning('reset_to_main_menu: no-op due to classic not present.')
def set_config_fullscreen_on() -> None:
def get_v2_account_id() -> str | None:
"""Return the current V2 account id if signed in, or None if not."""
try:
plus = _babase.app.plus
if plus is not None:
account = plus.accounts.primary
if account is not None:
accountid = account.accountid
# (Avoids mypy complaints when plus is not present)
assert isinstance(accountid, (str, type(None)))
return accountid
return None
except Exception:
logging.exception('Error fetching v2 account id.')
return None
def store_config_fullscreen_on() -> None:
"""The OS has changed our fullscreen state and we should take note."""
_babase.app.config['Fullscreen'] = True
_babase.app.config.commit()
def set_config_fullscreen_off() -> None:
def store_config_fullscreen_off() -> None:
"""The OS has changed our fullscreen state and we should take note."""
_babase.app.config['Fullscreen'] = False
_babase.app.config.commit()
def set_config_fullscreen_on() -> None:
"""Set and store fullscreen state"""
_babase.app.config['Fullscreen'] = True
_babase.app.config.apply_and_commit()
def set_config_fullscreen_off() -> None:
"""The OS has changed our fullscreen state and we should take note."""
_babase.app.config['Fullscreen'] = False
_babase.app.config.apply_and_commit()
def not_signed_in_screen_message() -> None:
from babase._language import Lstr
@ -111,6 +140,14 @@ def error_message() -> None:
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
def success_message() -> None:
from babase._language import Lstr
if _babase.app.env.gui:
_babase.getsimplesound('dingSmall').play()
_babase.screenmessage(Lstr(resource='successText'), color=(0, 1, 0))
def purchase_not_valid_error() -> None:
from babase._language import Lstr
@ -300,6 +337,7 @@ def implicit_sign_in(
from bacommon.login import LoginType
assert _babase.app.plus is not None
_babase.app.plus.accounts.on_implicit_sign_in(
login_type=LoginType(login_type_str),
login_id=login_id,
@ -372,3 +410,22 @@ def string_edit_adapter_can_be_replaced(adapter: StringEditAdapter) -> bool:
assert isinstance(adapter, StringEditAdapter)
return adapter.can_be_replaced()
def get_dev_console_tab_names() -> list[str]:
"""Return the current set of dev-console tab names."""
return [t.name for t in _babase.app.devconsole.tabs]
def unsupported_controller_message(name: str) -> None:
"""Print a message when an unsupported controller is connected."""
from babase._language import Lstr
# Ick; this can get called early in the bootstrapping process
# before we're allowed to load assets. Guard against that.
if _babase.asset_loads_allowed():
_babase.getsimplesound('error').play()
_babase.screenmessage(
Lstr(resource='unsupportedControllerText', subs=[('${NAME}', name)]),
color=(1, 0, 0),
)

View file

@ -20,6 +20,13 @@ if TYPE_CHECKING:
DEBUG_LOG = False
@dataclass
class LoginInfo:
"""Basic info about a login available in the app.plus.accounts section."""
name: str
class LoginAdapter:
"""Allows using implicit login types in an explicit way.
@ -138,7 +145,7 @@ class LoginAdapter:
is actually being used by the app. It should therefore register
unlocked achievements, leaderboard scores, allow viewing native
UIs, etc. When not active it should ignore everything and behave
as if logged out, even if it technically is still logged in.
as if signed out, even if it technically is still signed in.
"""
assert _babase.in_logic_thread()
del active # Unused.
@ -149,7 +156,7 @@ class LoginAdapter:
result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
description: str,
) -> None:
"""Attempt an explicit sign in via this adapter.
"""Attempt to sign in via this adapter.
This can be called even if the back-end is not implicitly signed in;
the adapter will attempt to sign in if possible. An exception will
@ -161,7 +168,7 @@ class LoginAdapter:
# Have been seeing multiple sign-in attempts come through
# nearly simultaneously which can be problematic server-side.
# Let's error if a sign-in attempt is made within a few seconds
# of the last one to address this.
# of the last one to try and address this.
now = time.monotonic()
appnow = _babase.apptime()
if self._last_sign_in_time is not None:
@ -229,6 +236,7 @@ class LoginAdapter:
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(
@ -239,20 +247,18 @@ class LoginAdapter:
)
_babase.pushcall(Call(result_cb, self, response))
else:
if DEBUG_LOG:
logging.debug(
'LoginAdapter: %s adapter got successful'
' sign-in response',
self.login_type.name,
)
# This means our credentials were explicitly rejected.
if response.credentials is None:
result2: LoginAdapter.SignInResult | Exception = (
RuntimeError(
'No credentials returned after'
' submitting sign-in-token.'
)
RuntimeError('Sign-in-token was rejected.')
)
else:
if DEBUG_LOG:
logging.debug(
'LoginAdapter: %s adapter got successful'
' sign-in response',
self.login_type.name,
)
result2 = self.SignInResult(
credentials=response.credentials
)
@ -269,7 +275,7 @@ class LoginAdapter:
on_response=_got_sign_in_response,
)
# Kick off the process by fetching a sign-in token.
# Kick off the sign-in process by fetching a sign-in token.
self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
def is_back_end_active(self) -> bool:
@ -282,11 +288,10 @@ class LoginAdapter:
"""Get a sign-in token from the adapter back end.
This token is then passed to the master-server to complete the
login process.
The adapter can use this opportunity to bring up account creation
UI, call its internal sign_in function, etc. as needed.
The provided completion_cb should then be called with either a token
or None if sign in failed or was cancelled.
sign-in process. The adapter can use this opportunity to bring
up account creation UI, call its internal sign_in function, etc.
as needed. The provided completion_cb should then be called with
either a token or None if sign in failed or was cancelled.
"""
from babase._general import Call
@ -295,7 +300,7 @@ class LoginAdapter:
def _update_implicit_login_state(self) -> None:
# If we've received an implicit login state, schedule it to be
# sent along to the app. We wait until on-app-launch has been
# sent along to the app. We wait until on-app-loading has been
# called so that account-client-v2 has had a chance to load
# any existing state so it can properly respond to this.
if self._implicit_login_state_dirty and self._on_app_loading_called:
@ -340,8 +345,8 @@ class LoginAdapter:
class LoginAdapterNative(LoginAdapter):
"""A login adapter that does its work in the native layer."""
def __init__(self) -> None:
super().__init__(LoginType.GPGS)
def __init__(self, login_type: LoginType) -> None:
super().__init__(login_type)
# Store int ids for in-flight attempts since they may go through
# various platform layers and back.
@ -375,3 +380,13 @@ class LoginAdapterNative(LoginAdapter):
class LoginAdapterGPGS(LoginAdapterNative):
"""Google Play Game Services adapter."""
def __init__(self) -> None:
super().__init__(LoginType.GPGS)
class LoginAdapterGameCenter(LoginAdapterNative):
"""Apple Game Center adapter."""
def __init__(self) -> None:
super().__init__(LoginType.GAME_CENTER)

View file

@ -24,6 +24,8 @@ if TYPE_CHECKING:
# instead of these or to make the meta system aware of arbitrary classes.
EXPORT_CLASS_NAME_SHORTCUTS: dict[str, str] = {
'plugin': 'babase.Plugin',
# DEPRECATED as of 12/2023. Currently am warning if finding these
# but should take this out eventually.
'keyboard': 'babase.Keyboard',
}
@ -414,30 +416,27 @@ class DirectoryScan:
if export_class_name is not None:
classname = modulename + '.' + export_class_name
# Since we'll soon have multiple versions of 'game'
# classes we need to migrate people to using base
# class names for them.
if exporttypestr == 'game':
# Migrating away from the 'keyboard' name shortcut
# since it's specific to bauiv1; warn if we find it.
if exporttypestr == 'keyboard':
logging.warning(
"metascan: %s:%d: '# ba_meta export"
" game' tag should be replaced by '# ba_meta"
" export bascenev1.GameActivity'.",
" keyboard' tag should be replaced by '# ba_meta"
" export bauiv1.Keyboard'.",
subpath,
lindex + 1,
)
self.results.announce_errors_occurred = True
else:
# If export type is one of our shortcuts, sub in the
# actual class path. Otherwise assume its a classpath
# itself.
exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(
exporttypestr
)
if exporttype is None:
exporttype = exporttypestr
self.results.exports.setdefault(exporttype, []).append(
classname
)
# If export type is one of our shortcuts, sub in the
# actual class path. Otherwise assume its a classpath
# itself.
exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(exporttypestr)
if exporttype is None:
exporttype = exporttypestr
self.results.exports.setdefault(exporttype, []).append(
classname
)
def _get_export_class_name(
self, subpath: Path, lines: list[str], lindex: int

View file

@ -38,6 +38,27 @@ class InputType(Enum):
DOWN_RELEASE = 26
class QuitType(Enum):
"""Types of input a controller can send to the game.
Category: Enums
'soft' may hide/reset the app but keep the process running, depending
on the platform.
'back' is a variant of 'soft' which may give 'back-button-pressed'
behavior depending on the platform. (returning to some previous
activity instead of dumping to the home screen, etc.)
'hard' leads to the process exiting. This generally should be avoided
on platforms such as mobile.
"""
SOFT = 0
BACK = 1
HARD = 2
class UIScale(Enum):
"""The overall scale the UI is being rendered for. Note that this is
independent of pixel resolution. For example, a phone and a desktop PC

View file

@ -170,23 +170,23 @@ class PluginSubsystem(AppSubsystem):
_error.print_exception('Error in plugin on_app_running()')
def on_app_pause(self) -> None:
def on_app_suspend(self) -> None:
for plugin in self.active_plugins:
try:
plugin.on_app_pause()
plugin.on_app_suspend()
except Exception:
from babase import _error
_error.print_exception('Error in plugin on_app_pause()')
_error.print_exception('Error in plugin on_app_suspend()')
def on_app_resume(self) -> None:
def on_app_unsuspend(self) -> None:
for plugin in self.active_plugins:
try:
plugin.on_app_resume()
plugin.on_app_unsuspend()
except Exception:
from babase import _error
_error.print_exception('Error in plugin on_app_resume()')
_error.print_exception('Error in plugin on_app_unsuspend()')
def on_app_shutdown(self) -> None:
for plugin in self.active_plugins:
@ -327,11 +327,11 @@ class Plugin:
def on_app_running(self) -> None:
"""Called when the app reaches the running state."""
def on_app_pause(self) -> None:
"""Called when the app is switching to a paused state."""
def on_app_suspend(self) -> None:
"""Called when the app enters the suspended state."""
def on_app_resume(self) -> None:
"""Called when the app is resuming from a paused state."""
def on_app_unsuspend(self) -> None:
"""Called when the app exits the suspended state."""
def on_app_shutdown(self) -> None:
"""Called when the app is beginning the shutdown process."""

View file

@ -104,8 +104,8 @@ def show_user_scripts() -> None:
_error.print_exception('error writing about_this_folder stuff')
# On a few platforms we try to open the dir in the UI.
if app.classic is not None and app.classic.platform in ['mac', 'windows']:
# On platforms that support it, open the dir in the UI.
if _babase.supports_open_dir_externally():
_babase.open_dir_externally(env.python_directory_user)
# Otherwise we just print a pretty version of it.

View file

@ -49,10 +49,10 @@ class AccountV1Subsystem:
babase.pushcall(do_auto_sign_in)
def on_app_pause(self) -> None:
def on_app_suspend(self) -> None:
"""Should be called when app is pausing."""
def on_app_resume(self) -> None:
def on_app_unsuspend(self) -> None:
"""Should be called when the app is resumed."""
# Mark our cached tourneys as invalid so anyone using them knows
@ -302,6 +302,11 @@ class AccountV1Subsystem:
"""(internal)"""
plus = babase.app.plus
if plus is None:
import logging
logging.warning(
'Error adding pending promo code; plus not present.'
)
babase.screenmessage(
babase.Lstr(resource='errorText'), color=(1, 0, 0)
)

View file

@ -4,10 +4,11 @@
from __future__ import annotations
import time
import asyncio
import logging
from typing import TYPE_CHECKING
import babase
import bauiv1
import bascenev1
if TYPE_CHECKING:
@ -31,6 +32,7 @@ class AdsSubsystem:
self.last_in_game_ad_remove_message_show_time: float | None = None
self.last_ad_completion_time: float | None = None
self.last_ad_was_short = False
self._fallback_task: asyncio.Task | None = None
def do_remove_in_game_ads_message(self) -> None:
"""(internal)"""
@ -69,7 +71,8 @@ class AdsSubsystem:
) -> None:
"""(internal)"""
self.last_ad_purpose = purpose
bauiv1.show_ad(purpose, on_completion_call)
assert babase.app.plus is not None
babase.app.plus.show_ad(purpose, on_completion_call)
def show_ad_2(
self,
@ -78,7 +81,8 @@ class AdsSubsystem:
) -> None:
"""(internal)"""
self.last_ad_purpose = purpose
bauiv1.show_ad_2(purpose, on_completion_call)
assert babase.app.plus is not None
babase.app.plus.show_ad_2(purpose, on_completion_call)
def call_after_ad(self, call: Callable[[], Any]) -> None:
"""Run a call after potentially showing an ad."""
@ -94,7 +98,7 @@ class AdsSubsystem:
show = True
# No ads without net-connections, etc.
if not bauiv1.can_show_ad():
if not plus.can_show_ad():
show = False
if classic.accounts.have_pro():
show = False # Pro disables interstitials.
@ -132,7 +136,7 @@ class AdsSubsystem:
# ad-show-threshold and see if we should *actually* show
# (we reach our threshold faster the longer we've been
# playing).
base = 'ads' if bauiv1.has_video_ads() else 'ads2'
base = 'ads' if plus.has_video_ads() else 'ads2'
min_lc = plus.get_v1_account_misc_read_val(base + '.minLC', 0.0)
max_lc = plus.get_v1_account_misc_read_val(base + '.maxLC', 5.0)
min_lc_scale = plus.get_v1_account_misc_read_val(
@ -181,36 +185,53 @@ class AdsSubsystem:
# If we're *still* cleared to show, actually tell the system to show.
if show:
# As a safety-check, set up an object that will run
# the completion callback if we've returned and sat for 10 seconds
# (in case some random ad network doesn't properly deliver its
# completion callback).
# As a safety-check, we set up an object that will run the
# completion callback if we've returned and sat for several
# seconds (in case some random ad network doesn't properly
# deliver its completion callback).
class _Payload:
def __init__(self, pcall: Callable[[], Any]):
self._call = pcall
self._ran = False
def run(self, fallback: bool = False) -> None:
"""Run fallback call (and issue a warning about it)."""
"""Run the payload."""
assert app.classic is not None
if not self._ran:
if fallback:
lanst = app.classic.ads.last_ad_network_set_time
print(
'ERROR: relying on fallback ad-callback! '
'last network: '
+ app.classic.ads.last_ad_network
+ ' (set '
+ str(int(time.time() - lanst))
+ 's ago); purpose='
+ app.classic.ads.last_ad_purpose
logging.error(
'Relying on fallback ad-callback! '
'last network: %s (set %s seconds ago);'
' purpose=%s.',
app.classic.ads.last_ad_network,
time.time() - lanst,
app.classic.ads.last_ad_purpose,
)
babase.pushcall(self._call)
self._ran = True
payload = _Payload(call)
# Set up our backup.
with babase.ContextRef.empty():
babase.apptimer(5.0, lambda: payload.run(fallback=True))
# Note to self: Previously this was a simple 5 second
# timer because the app got totally suspended while ads
# were showing (which delayed the timer), but these days
# the app may continue to run, so we need to be more
# careful and only fire the fallback after we see that
# the app has been front-and-center for several seconds.
async def add_fallback_task() -> None:
activesecs = 5
while activesecs > 0:
if babase.app.active:
activesecs -= 1
await asyncio.sleep(1.0)
payload.run(fallback=True)
_fallback_task = babase.app.aioloop.create_task(
add_fallback_task()
)
self.show_ad('between_game', on_completion_call=payload.run)
else:
babase.pushcall(call) # Just run the callback without the ad.

View file

@ -41,5 +41,6 @@ class AppDelegate:
sessiontype,
settings,
completion_call=completion_call,
).get_root_widget()
).get_root_widget(),
from_window=False, # Disable check since we don't know.
)

View file

@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
import babase
import bascenev1
import _baclassic
if TYPE_CHECKING:
from typing import Any, Sequence
@ -54,7 +55,6 @@ def run_stress_test(
round_duration: int = 30,
) -> None:
"""Run a stress test."""
from babase import modutils
babase.screenmessage(
"Beginning stress test.. use 'End Test' to stop testing.",
@ -69,22 +69,12 @@ def run_stress_test(
'round_duration': round_duration,
}
)
babase.apptimer(
7.0,
babase.Call(
babase.screenmessage,
(
'stats will be written to '
+ modutils.get_human_readable_user_scripts_path()
+ '/stress_test_stats.csv'
),
),
)
def stop_stress_test() -> None:
"""End a running stress test."""
babase.set_stress_testing(False, 0)
_baclassic.set_stress_testing(False, 0)
assert babase.app.classic is not None
try:
if babase.app.classic.stress_test_reset_timer is not None:
@ -134,14 +124,14 @@ def start_stress_test(args: dict[str, Any]) -> None:
babase.Call(bascenev1.new_host_session, FreeForAllSession),
),
)
babase.set_stress_testing(True, args['player_count'])
_baclassic.set_stress_testing(True, args['player_count'])
babase.app.classic.stress_test_reset_timer = babase.AppTimer(
args['round_duration'], babase.Call(_reset_stress_test, args)
)
def _reset_stress_test(args: dict[str, Any]) -> None:
babase.set_stress_testing(False, args['player_count'])
_baclassic.set_stress_testing(False, args['player_count'])
babase.screenmessage('Resetting stress test...')
session = bascenev1.get_foreground_host_session()
assert session is not None

View file

@ -20,7 +20,6 @@ def get_input_device_mapped_value(
This checks the user config and falls back to default values
where available.
"""
# pylint: disable=too-many-statements
# pylint: disable=too-many-return-statements
# pylint: disable=too-many-branches
@ -40,7 +39,14 @@ def get_input_device_mapped_value(
mapping = ccfgs[devicename][unique_id]
elif 'default' in ccfgs[devicename]:
mapping = ccfgs[devicename]['default']
if mapping is not None:
# We now use the config mapping *only* if it is not empty.
# There have been cases of config writing code messing up
# and leaving empty dicts in the app config, which currently
# leaves the device unusable. Alternatively, we'd perhaps
# want to fall back to defaults for individual missing
# values, but that is a bigger change we can make later.
if isinstance(mapping, dict) and mapping:
return mapping.get(name, -1)
if platform == 'windows':
@ -76,91 +82,6 @@ def get_input_device_mapped_value(
'triggerRun1': 5,
}.get(name, -1)
# Look for some exact types.
if babase.is_running_on_fire_tv():
if devicename in ['Thunder', 'Amazon Fire Game Controller']:
return {
'triggerRun2': 23,
'unassignedButtonsRun': False,
'buttonPickUp': 101,
'buttonBomb': 98,
'buttonJump': 97,
'analogStickDeadZone': 0.0,
'startButtonActivatesDefaultWidget': False,
'buttonStart': 83,
'buttonPunch': 100,
'buttonRun2': 103,
'buttonRun1': 104,
'triggerRun1': 24,
}.get(name, -1)
if devicename == 'NYKO PLAYPAD PRO':
return {
'triggerRun2': 23,
'triggerRun1': 24,
'buttonPickUp': 101,
'buttonBomb': 98,
'buttonJump': 97,
'buttonUp': 20,
'buttonLeft': 22,
'buttonRight': 23,
'buttonStart': 83,
'buttonPunch': 100,
'buttonDown': 21,
}.get(name, -1)
if devicename == 'Logitech Dual Action':
return {
'triggerRun2': 23,
'triggerRun1': 24,
'buttonPickUp': 98,
'buttonBomb': 101,
'buttonJump': 100,
'buttonStart': 109,
'buttonPunch': 97,
}.get(name, -1)
if devicename == 'Xbox 360 Wireless Receiver':
return {
'triggerRun2': 23,
'triggerRun1': 24,
'buttonPickUp': 101,
'buttonBomb': 98,
'buttonJump': 97,
'buttonUp': 20,
'buttonLeft': 22,
'buttonRight': 23,
'buttonStart': 83,
'buttonPunch': 100,
'buttonDown': 21,
}.get(name, -1)
if devicename == 'Microsoft X-Box 360 pad':
return {
'triggerRun2': 23,
'triggerRun1': 24,
'buttonPickUp': 101,
'buttonBomb': 98,
'buttonJump': 97,
'buttonStart': 83,
'buttonPunch': 100,
}.get(name, -1)
if devicename in [
'Amazon Remote',
'Amazon Bluetooth Dev',
'Amazon Fire TV Remote',
]:
return {
'triggerRun2': 23,
'triggerRun1': 24,
'buttonPickUp': 24,
'buttonBomb': 91,
'buttonJump': 86,
'buttonUp': 20,
'buttonLeft': 22,
'startButtonActivatesDefaultWidget': False,
'buttonRight': 23,
'buttonStart': 83,
'buttonPunch': 90,
'buttonDown': 21,
}.get(name, -1)
elif 'NVIDIA SHIELD;' in useragentstring:
if 'NVIDIA Controller' in devicename:
return {
@ -175,112 +96,6 @@ def get_input_device_mapped_value(
'buttonIgnored': 184,
'buttonIgnored2': 86,
}.get(name, -1)
elif platform == 'mac':
if devicename == 'PLAYSTATION(R)3 Controller':
return {
'buttonLeft': 8,
'buttonUp': 5,
'buttonRight': 6,
'buttonDown': 7,
'buttonJump': 15,
'buttonPunch': 16,
'buttonBomb': 14,
'buttonPickUp': 13,
'buttonStart': 4,
'buttonIgnored': 17,
}.get(name, -1)
if devicename in ['Wireless 360 Controller', 'Controller']:
# Xbox360 gamepads
return {
'analogStickDeadZone': 1.2,
'buttonBomb': 13,
'buttonDown': 2,
'buttonJump': 12,
'buttonLeft': 3,
'buttonPickUp': 15,
'buttonPunch': 14,
'buttonRight': 4,
'buttonStart': 5,
'buttonUp': 1,
'triggerRun1': 5,
'triggerRun2': 6,
'buttonIgnored': 11,
}.get(name, -1)
if devicename in [
'Logitech Dual Action',
'Logitech Cordless RumblePad 2',
]:
return {
'buttonJump': 2,
'buttonPunch': 1,
'buttonBomb': 3,
'buttonPickUp': 4,
'buttonStart': 10,
}.get(name, -1)
# Old gravis gamepad.
if devicename == 'GamePad Pro USB ':
return {
'buttonJump': 2,
'buttonPunch': 1,
'buttonBomb': 3,
'buttonPickUp': 4,
'buttonStart': 10,
}.get(name, -1)
if devicename == 'Microsoft SideWinder Plug & Play Game Pad':
return {
'buttonJump': 1,
'buttonPunch': 3,
'buttonBomb': 2,
'buttonPickUp': 4,
'buttonStart': 6,
}.get(name, -1)
# Saitek P2500 Rumble Force Pad.. (hopefully works for others too?..)
if devicename == 'Saitek P2500 Rumble Force Pad':
return {
'buttonJump': 3,
'buttonPunch': 1,
'buttonBomb': 4,
'buttonPickUp': 2,
'buttonStart': 11,
}.get(name, -1)
# Some crazy 'Senze' dual gamepad.
if devicename == 'Twin USB Joystick':
return {
'analogStickLR': 3,
'analogStickLR_B': 7,
'analogStickUD': 4,
'analogStickUD_B': 8,
'buttonBomb': 2,
'buttonBomb_B': 14,
'buttonJump': 3,
'buttonJump_B': 15,
'buttonPickUp': 1,
'buttonPickUp_B': 13,
'buttonPunch': 4,
'buttonPunch_B': 16,
'buttonRun1': 7,
'buttonRun1_B': 19,
'buttonRun2': 8,
'buttonRun2_B': 20,
'buttonStart': 10,
'buttonStart_B': 22,
'enableSecondary': 1,
'unassignedButtonsRun': False,
}.get(name, -1)
if devicename == 'USB Gamepad ': # some weird 'JITE' gamepad
return {
'analogStickLR': 4,
'analogStickUD': 5,
'buttonJump': 3,
'buttonPunch': 4,
'buttonBomb': 2,
'buttonPickUp': 1,
'buttonStart': 10,
}.get(name, -1)
default_android_mapping = {
'triggerRun2': 19,
@ -303,6 +118,41 @@ def get_input_device_mapped_value(
# Generic android...
if platform == 'android':
if devicename in ['Amazon Fire Game Controller']:
return {
'triggerRun2': 23,
'unassignedButtonsRun': False,
'buttonPickUp': 101,
'buttonBomb': 98,
'buttonJump': 97,
'analogStickDeadZone': 0.0,
'startButtonActivatesDefaultWidget': False,
'buttonStart': 83,
'buttonPunch': 100,
'buttonRun2': 103,
'buttonRun1': 104,
'triggerRun1': 24,
}.get(name, -1)
if devicename in [
'Amazon Remote',
'Amazon Bluetooth Dev',
'Amazon Fire TV Remote',
]:
return {
'triggerRun2': 23,
'triggerRun1': 24,
'buttonPickUp': 24,
'buttonBomb': 91,
'buttonJump': 86,
'buttonUp': 20,
'buttonLeft': 22,
'startButtonActivatesDefaultWidget': False,
'buttonRight': 23,
'buttonStart': 83,
'buttonPunch': 90,
'buttonDown': 21,
}.get(name, -1)
# Steelseries stratus xl.
if devicename == 'SteelSeries Stratus XL':
return {
@ -380,14 +230,6 @@ def get_input_device_mapped_value(
'uiOnly': True,
}.get(name, -1)
# flag particular gamepads to use exact android defaults..
# (so they don't even ask to configure themselves)
if devicename in [
'Samsung Game Pad EI-GP20',
'ASUS Gamepad',
] or devicename.startswith('Freefly VR Glide'):
return default_android_mapping.get(name, -1)
# Nvidia controller is default, but gets some strange
# keypresses we want to ignore.. touching the touchpad,
# so lets ignore those.
@ -445,76 +287,11 @@ def get_input_device_mapped_value(
'buttonRight': 100,
}.get(name, -1)
# Ok, this gamepad's not in our specific preset list;
# fall back to some (hopefully) reasonable defaults.
# Leaving these in here for now but not gonna add any more now that we have
# fancy-pants config sharing across the internet.
if platform == 'mac':
if 'PLAYSTATION' in devicename: # ps3 gamepad?..
return {
'buttonLeft': 8,
'buttonUp': 5,
'buttonRight': 6,
'buttonDown': 7,
'buttonJump': 15,
'buttonPunch': 16,
'buttonBomb': 14,
'buttonPickUp': 13,
'buttonStart': 4,
}.get(name, -1)
# Dual Action Config - hopefully applies to more...
if 'Logitech' in devicename:
return {
'buttonJump': 2,
'buttonPunch': 1,
'buttonBomb': 3,
'buttonPickUp': 4,
'buttonStart': 10,
}.get(name, -1)
# Saitek P2500 Rumble Force Pad.. (hopefully works for others too?..)
if 'Saitek' in devicename:
return {
'buttonJump': 3,
'buttonPunch': 1,
'buttonBomb': 4,
'buttonPickUp': 2,
'buttonStart': 11,
}.get(name, -1)
# Gravis stuff?...
if 'GamePad' in devicename:
return {
'buttonJump': 2,
'buttonPunch': 1,
'buttonBomb': 3,
'buttonPickUp': 4,
'buttonStart': 10,
}.get(name, -1)
# Ok, this gamepad's not in our specific preset list; fall back to
# some (hopefully) reasonable defaults.
# Reasonable defaults.
if platform == 'android':
if babase.is_running_on_fire_tv():
# Mostly same as default firetv controller.
return {
'triggerRun2': 23,
'triggerRun1': 24,
'buttonPickUp': 101,
'buttonBomb': 98,
'buttonJump': 97,
'buttonStart': 83,
'buttonPunch': 100,
'buttonDown': 21,
'buttonUp': 20,
'buttonLeft': 22,
'buttonRight': 23,
'startButtonActivatesDefaultWidget': False,
}.get(name, -1)
# Mostly same as 'Gamepad' except with 'menu' for default start
# button instead of 'mode'.
return default_android_mapping.get(name, -1)
# Is there a point to any sort of fallbacks here?.. should check.
@ -533,9 +310,9 @@ def _gen_android_input_hash() -> str:
md5 = hashlib.md5()
# Currently we just do a single hash of *all* inputs on android
# and that's it.. good enough.
# (grabbing mappings for a specific device looks to be non-trivial)
# Currently we just do a single hash of *all* inputs on android and
# that's it. Good enough. (grabbing mappings for a specific device
# looks to be non-trivial)
for dirname in [
'/system/usr/keylayout',
'/data/usr/keylayout',
@ -544,9 +321,9 @@ def _gen_android_input_hash() -> str:
try:
if os.path.isdir(dirname):
for f_name in os.listdir(dirname):
# This is usually volume keys and stuff;
# assume we can skip it?..
# (since it'll vary a lot across devices)
# This is usually volume keys and stuff; assume we
# can skip it?.. (since it'll vary a lot across
# devices)
if f_name == 'gpio-keys.kl':
continue
try:
@ -569,8 +346,8 @@ def get_input_device_map_hash() -> str:
"""
app = babase.app
# Currently only using this when classic is present.
# Need to replace with a modern equivalent.
# Currently only using this when classic is present. Need to replace
# with a modern equivalent.
if app.classic is not None:
try:
if app.classic.input_map_hash is None:

View file

@ -165,15 +165,16 @@ class MusicSubsystem:
def supports_soundtrack_entry_type(self, entry_type: str) -> bool:
"""Return whether provided soundtrack entry type is supported here."""
uas = babase.env()['legacy_user_agent_string']
assert isinstance(uas, str)
# FIXME: Generalize this.
# Note to self; can't access babase.app.classic here because
# we are called during its construction.
env = babase.env()
platform = env.get('platform')
assert isinstance(platform, str)
if entry_type == 'iTunesPlaylist':
return 'Mac' in uas
return platform == 'mac' and babase.is_xcode_build()
if entry_type in ('musicFile', 'musicFolder'):
return (
'android' in uas
platform == 'android'
and babase.android_get_external_files_dir() is not None
)
if entry_type == 'default':
@ -239,7 +240,7 @@ class MusicSubsystem:
logging.exception('Error in get_soundtrack_entry_name.')
return 'default'
def on_app_resume(self) -> None:
def on_app_unsuspend(self) -> None:
"""Should be run when the app resumes from a suspended state."""
if babase.is_os_playing_music():
self.do_play_music(None)

View file

@ -423,6 +423,10 @@ class ServerController:
bascenev1.set_public_party_stats_url(self._config.stats_url)
bascenev1.set_public_party_enabled(self._config.party_is_public)
bascenev1.set_player_rejoin_cooldown(
self._config.player_rejoin_cooldown
)
# And here.. we.. go.
if self._config.stress_test_players is not None:
# Special case: run a stress test.

View file

@ -229,12 +229,12 @@ class ClassicSubsystem(babase.AppSubsystem):
self.accounts.on_app_loading()
def on_app_pause(self) -> None:
self.accounts.on_app_pause()
def on_app_suspend(self) -> None:
self.accounts.on_app_suspend()
def on_app_resume(self) -> None:
self.accounts.on_app_resume()
self.music.on_app_resume()
def on_app_unsuspend(self) -> None:
self.accounts.on_app_unsuspend()
self.music.on_app_unsuspend()
def on_app_shutdown(self) -> None:
self.music.on_app_shutdown()
@ -451,15 +451,6 @@ class ClassicSubsystem(babase.AppSubsystem):
if playtype in val.get_play_types()
)
def show_online_score_ui(
self,
show: str = 'general',
game: str | None = None,
game_version: str | None = None,
) -> None:
"""(internal)"""
bauiv1.show_online_score_ui(show, game, game_version)
def game_begin_analytics(self) -> None:
"""(internal)"""
from baclassic import _analytics
@ -627,15 +618,6 @@ class ClassicSubsystem(babase.AppSubsystem):
"""(internal)"""
return bascenev1.get_foreground_host_activity()
def show_config_error_window(self) -> bool:
"""(internal)"""
if self.platform in ('mac', 'linux', 'windows'):
from bauiv1lib.configerror import ConfigErrorWindow
babase.pushcall(ConfigErrorWindow)
return True
return False
def value_test(
self,
arg: str,
@ -701,11 +683,11 @@ class ClassicSubsystem(babase.AppSubsystem):
ShowURLWindow(address)
def quit_window(self) -> None:
def quit_window(self, quit_type: babase.QuitType) -> None:
"""(internal)"""
from bauiv1lib.confirm import QuitWindow
QuitWindow()
QuitWindow(quit_type)
def tournament_entry_window(
self,
@ -809,5 +791,6 @@ class ClassicSubsystem(babase.AppSubsystem):
bauiv1.getsound('swish').play()
babase.app.ui_v1.set_main_menu_window(
MainMenuWindow().get_root_widget()
MainMenuWindow().get_root_widget(),
from_window=False, # Disable check here.
)

View file

@ -80,14 +80,13 @@ class _MacMusicAppThread(threading.Thread):
def run(self) -> None:
"""Run the Music.app thread."""
babase.set_thread_name('BA_MacMusicAppThread')
babase.mac_music_app_init()
# Let's mention to the user we're launching Music.app in case
# it causes any funny business (this used to background the app
# sometimes, though I think that is fixed now)
def do_print() -> None:
babase.apptimer(
1.0,
0.5,
babase.Call(
babase.screenmessage,
babase.Lstr(resource='usingItunesText'),
@ -97,9 +96,8 @@ class _MacMusicAppThread(threading.Thread):
babase.pushcall(do_print, from_other_thread=True)
# Here we grab this to force the actual launch.
babase.mac_music_app_get_volume()
babase.mac_music_app_get_library_source()
babase.mac_music_app_init()
done = False
while not done:
self._commands_available.wait()

View file

@ -5,22 +5,49 @@
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated
from efro.dataclassio import ioprepped, IOAttrs
if TYPE_CHECKING:
pass
class AppExperience(Enum):
"""Overall experience that can be provided by a Ballistica app.
class AppInterfaceIdiom(Enum):
"""A general form-factor or way of experiencing a Ballistica app.
This corresponds generally, but not exactly, to distinct apps built
with Ballistica. However, a single app may support multiple experiences,
or there may be multiple apps targeting one experience. Cloud components
such as leagues are generally associated with an AppExperience.
Note that it is possible for a running app to switch idioms (for
instance if a mobile device or computer is connected to a TV).
"""
# A special experience category that is supported everywhere. Used
PHONE = 'phone'
TABLET = 'tablet'
DESKTOP = 'desktop'
TV = 'tv'
XR = 'xr'
class AppExperience(Enum):
"""A particular experience that can be provided by a Ballistica app.
This is one metric used to isolate different playerbases from
eachother where there might be no technical barriers doing so.
For example, a casual one-hand-playable phone game and an augmented
reality tabletop game may both use the same scene-versions and
networking-protocols and whatnot, but it would make no sense to
allow players of one join servers for the other. AppExperience can
be used to keep these player bases separate.
Generally a single Ballistica app targets a single AppExperience.
This is not a technical requirement, however. A single app may
support multiple experiences, or there may be multiple apps
targeting one experience. Cloud components such as leagues are
generally associated with an AppExperience so that they are only
visible to client apps designed for that play style.
"""
# An experience that is supported everywhere. Used
# for the default empty AppMode when starting the app, etc.
EMPTY = 'empty'
@ -33,3 +60,79 @@ class AppExperience(Enum):
# touch-screen allowing a mobile device to be used as a game
# controller.
REMOTE = 'remote'
class AppArchitecture(Enum):
"""Processor architecture the App is running on."""
ARM = 'arm'
ARM64 = 'arm64'
X86 = 'x86'
X86_64 = 'x86_64'
class AppPlatform(Enum):
"""Overall platform a Ballistica build can be targeting.
Each distinct flavor of an app has a unique combination
of AppPlatform and AppVariant. Generally platform describes
a set of hardware, while variant describes a destination or
purpose for the build.
"""
MAC = 'mac'
WINDOWS = 'windows'
LINUX = 'linux'
ANDROID = 'android'
IOS = 'ios'
TVOS = 'tvos'
class AppVariant(Enum):
"""A unique Ballistica build type within a single platform.
Each distinct flavor of an app has a unique combination
of AppPlatform and AppVariant. Generally platform describes
a set of hardware, while variant describes a destination or
purpose for the build.
"""
# Default builds.
GENERIC = 'generic'
# Builds intended for public testing (may have some extra checks
# or logging enabled).
TEST = 'test'
# Various stores.
AMAZON_APPSTORE = 'amazon_appstore'
GOOGLE_PLAY = 'google_play'
APP_STORE = 'app_store'
WINDOWS_STORE = 'windows_store'
STEAM = 'steam'
META = 'meta'
EPIC_GAMES_STORE = 'epic_games_store'
# Other.
ARCADE = 'arcade'
DEMO = 'demo'
@ioprepped
@dataclass
class AppInstanceInfo:
"""General info about an individual running app."""
name = Annotated[str, IOAttrs('n')]
version = Annotated[str, IOAttrs('v')]
build = Annotated[int, IOAttrs('b')]
platform = Annotated[AppPlatform, IOAttrs('p')]
variant = Annotated[AppVariant, IOAttrs('va')]
architecture = Annotated[AppArchitecture, IOAttrs('a')]
os_version = Annotated[str | None, IOAttrs('o')]
interface_idiom: Annotated[AppInterfaceIdiom, IOAttrs('i')]
locale: Annotated[str, IOAttrs('l')]
device: Annotated[str | None, IOAttrs('d')]

View file

@ -11,6 +11,12 @@ if TYPE_CHECKING:
pass
# NOTE TO SELF:
# Whenever adding login types here, make sure to update all
# basn nodes before trying to send values through to bamaster,
# as they need to be extractable by basn en route.
class LoginType(Enum):
"""Types of logins available."""
@ -20,6 +26,9 @@ class LoginType(Enum):
# Google Play Game Services
GPGS = 'gpgs'
# Apple's Game Center
GAME_CENTER = 'game_center'
@property
def displayname(self) -> str:
"""Human readable name for this value."""
@ -29,3 +38,5 @@ class LoginType(Enum):
return 'Email/Password'
case cls.GPGS:
return 'Google Play Games'
case cls.GAME_CENTER:
return 'Game Center'

View file

@ -143,9 +143,20 @@ class ServerConfig:
# queue spamming attacks.
enable_queue: bool = True
# Protocol version we host with. Currently the default is 33 which
# still allows older 1.4 game clients to connect. Explicitly setting
# to 35 no longer allows those clients but adds/fixes a few things
# such as making camera shake properly work in net games.
protocol_version: int | None = None
# (internal) stress-testing mode.
stress_test_players: int | None = None
# How many seconds individual players from a given account must wait
# before rejoining the game. This can help suppress exploits
# involving leaving and rejoining or switching teams rapidly.
player_rejoin_cooldown: float = 10.0
# NOTE: as much as possible, communication from the server-manager to the
# child-process should go through these and not ad-hoc Python string commands

View file

@ -40,7 +40,7 @@ if TYPE_CHECKING:
# the last load. Either way, however, multiple execs will happen in some
# form.
#
# So we need to do a few things to handle that situation gracefully.
# To handle that situation gracefully, we need to do a few things:
#
# - First, we need to store any mutable global state in the __main__
# module; not in ourself. This way, alternate versions of ourself will
@ -48,12 +48,12 @@ if TYPE_CHECKING:
#
# - Second, we should avoid the use of isinstance and similar calls for
# our types. An EnvConfig we create would technically be a different
# type than that created by an alternate baenv.
# type than an EnvConfig created by an alternate baenv.
# Build number and version of the ballistica binary we expect to be
# using.
TARGET_BALLISTICA_BUILD = 21397
TARGET_BALLISTICA_VERSION = '1.7.28'
TARGET_BALLISTICA_BUILD = 21739
TARGET_BALLISTICA_VERSION = '1.7.32'
@dataclass

View file

@ -1,6 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides classic app subsystem."""
"""Provides plus app subsystem."""
from __future__ import annotations
from typing import TYPE_CHECKING
@ -249,3 +249,41 @@ class PlusSubsystem(AppSubsystem):
) -> None:
"""(internal)"""
return _baplus.tournament_query(callback, args)
@staticmethod
def have_incentivized_ad() -> bool:
"""Is an incentivized ad available?"""
return _baplus.have_incentivized_ad()
@staticmethod
def has_video_ads() -> bool:
"""Are video ads available?"""
return _baplus.has_video_ads()
@staticmethod
def can_show_ad() -> bool:
"""Can we show an ad?"""
return _baplus.can_show_ad()
@staticmethod
def show_ad(
purpose: str, on_completion_call: Callable[[], None] | None = None
) -> None:
"""Show an ad."""
_baplus.show_ad(purpose, on_completion_call)
@staticmethod
def show_ad_2(
purpose: str, on_completion_call: Callable[[bool], None] | None = None
) -> None:
"""Show an ad."""
_baplus.show_ad_2(purpose, on_completion_call)
@staticmethod
def show_game_service_ui(
show: str = 'general',
game: str | None = None,
game_version: str | None = None,
) -> None:
"""Show game-service provided UI."""
_baplus.show_game_service_ui(show, game, game_version)

View file

@ -78,6 +78,7 @@ from _bascenev1 import (
end_host_scanning,
get_chat_messages,
get_connection_to_host_info,
get_connection_to_host_info_2,
get_foreground_host_activity,
get_foreground_host_session,
get_game_port,
@ -202,6 +203,7 @@ from bascenev1._multiteamsession import (
DEFAULT_TEAM_NAMES,
)
from bascenev1._music import MusicType, setmusic
from bascenev1._net import HostInfo
from bascenev1._nodeactor import NodeActor
from bascenev1._powerup import get_default_powerup_distribution
from bascenev1._profile import (
@ -226,7 +228,7 @@ from bascenev1._settings import (
IntSetting,
Setting,
)
from bascenev1._session import Session
from bascenev1._session import Session, set_player_rejoin_cooldown
from bascenev1._stats import PlayerScoredMessage, PlayerRecord, Stats
from bascenev1._team import SessionTeam, Team, EmptyTeam
from bascenev1._teamgame import TeamGameActivity
@ -303,6 +305,7 @@ __all__ = [
'GameTip',
'get_chat_messages',
'get_connection_to_host_info',
'get_connection_to_host_info_2',
'get_default_free_for_all_playlist',
'get_default_teams_playlist',
'get_default_powerup_distribution',
@ -338,6 +341,7 @@ __all__ = [
'have_connected_clients',
'have_touchscreen_input',
'HitMessage',
'HostInfo',
'host_scan_cycle',
'ImpactDamageMessage',
'increment_analytics_count',
@ -415,6 +419,7 @@ __all__ = [
'set_public_party_name',
'set_public_party_queue_enabled',
'set_public_party_stats_url',
'set_player_rejoin_cooldown',
'set_replay_speed_exponent',
'set_touchscreen_editing',
'setmusic',

View file

@ -87,7 +87,9 @@ class Campaign:
def get_selected_level(self) -> str:
"""Return the name of the Level currently selected in the UI."""
return self.configdict.get('Selection', self._levels[0].name)
val = self.configdict.get('Selection', self._levels[0].name)
assert isinstance(val, str)
return val
@property
def configdict(self) -> dict[str, Any]:

View file

@ -438,10 +438,16 @@ class GameActivity(Activity[PlayerT, TeamT]):
assert classic is not None
continues_window = classic.continues_window
# Turning these off. I want to migrate towards monetization that
# feels less pay-to-win-ish.
allow_continues = False
plus = babase.app.plus
try:
if plus is not None and plus.get_v1_account_misc_read_val(
'enableContinues', False
if (
plus is not None
and plus.get_v1_account_misc_read_val('enableContinues', False)
and allow_continues
):
session = self.session

View file

@ -105,7 +105,9 @@ class Level:
def complete(self) -> bool:
"""Whether this Level has been completed."""
config = self._get_config_dict()
return config.get('Complete', False)
val = config.get('Complete', False)
assert isinstance(val, bool)
return val
def set_complete(self, val: bool) -> None:
"""Set whether or not this level is complete."""
@ -147,7 +149,9 @@ class Level:
@property
def rating(self) -> float:
"""The current rating for this Level."""
return self._get_config_dict().get('Rating', 0.0)
val = self._get_config_dict().get('Rating', 0.0)
assert isinstance(val, float)
return val
def set_rating(self, rating: float) -> None:
"""Set a rating for this Level, replacing the old ONLY IF higher."""

View file

@ -170,8 +170,11 @@ class MultiTeamSession(Session):
def get_max_players(self) -> int:
"""Return max number of Players allowed to join the game at once."""
if self.use_teams:
return babase.app.config.get('Team Game Max Players', 8)
return babase.app.config.get('Free-for-All Max Players', 8)
val = babase.app.config.get('Team Game Max Players', 8)
else:
val = babase.app.config.get('Free-for-All Max Players', 8)
assert isinstance(val, int)
return val
def _instantiate_next_game(self) -> None:
self._next_game_instance = _bascenev1.newactivity(

24
dist/ba_data/python/bascenev1/_net.py vendored Normal file
View file

@ -0,0 +1,24 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to net play."""
from __future__ import annotations
from typing import TYPE_CHECKING
from dataclasses import dataclass
if TYPE_CHECKING:
pass
@dataclass
class HostInfo:
"""Info about a host."""
name: str
build_number: int
# Note this can be None for non-ip hosts such as bluetooth.
address: str | None
# Note this can be None for non-ip hosts such as bluetooth.
port: int | None

View file

@ -3,6 +3,7 @@
"""Defines base session class."""
from __future__ import annotations
import math
import weakref
import logging
from typing import TYPE_CHECKING
@ -17,6 +18,17 @@ if TYPE_CHECKING:
import bascenev1
# How many seconds someone who left the session (but not the party) must
# wait to rejoin the session again. Intended to prevent game exploits
# such as skipping respawn waits.
_g_player_rejoin_cooldown: float = 0.0
def set_player_rejoin_cooldown(cooldown: float) -> None:
"""Set the cooldown for individual players rejoining after leaving."""
global _g_player_rejoin_cooldown # pylint: disable=global-statement
_g_player_rejoin_cooldown = max(0.0, cooldown)
class Session:
"""Defines a high level series of bascenev1.Activity-es.
@ -203,6 +215,11 @@ class Session:
# Instantiate our session globals node which will apply its settings.
self._sessionglobalsnode = _bascenev1.newnode('sessionglobals')
# Rejoin cooldown stuff.
self._players_on_wait: dict = {}
self._player_requested_identifiers: dict = {}
self._waitlist_timers: dict = {}
@property
def context(self) -> bascenev1.ContextRef:
"""A context-ref pointing at this activity."""
@ -253,6 +270,33 @@ class Session:
)
return False
# Rejoin cooldown.
identifier = player.get_v1_account_id()
if identifier:
leave_time = self._players_on_wait.get(identifier)
if leave_time:
diff = str(
math.ceil(
_g_player_rejoin_cooldown
- babase.apptime()
+ leave_time
)
)
_bascenev1.broadcastmessage(
babase.Lstr(
translate=(
'serverResponses',
'You can join in ${COUNT} seconds.',
),
subs=[('${COUNT}', diff)],
),
color=(1, 1, 0),
clients=[player.inputdevice.client_id],
transient=True,
)
return False
self._player_requested_identifiers[player.id] = identifier
_bascenev1.getsound('dripity').play()
return True
@ -270,6 +314,16 @@ class Session:
activity = self._activity_weak()
# Rejoin cooldown.
identifier = self._player_requested_identifiers.get(sessionplayer.id)
if identifier:
self._players_on_wait[identifier] = babase.apptime()
with babase.ContextRef.empty():
self._waitlist_timers[identifier] = babase.AppTimer(
_g_player_rejoin_cooldown,
babase.Call(self._remove_player_from_waitlist, identifier),
)
if not sessionplayer.in_game:
# Ok, the player is still in the lobby; simply remove them.
with self.context:
@ -770,3 +824,9 @@ class Session:
if pass_to_activity:
activity.add_player(sessionplayer)
return sessionplayer
def _remove_player_from_waitlist(self, identifier: str) -> None:
try:
self._players_on_wait.pop(identifier)
except KeyError:
pass

View file

@ -9,6 +9,7 @@ import random
import logging
from typing import TYPE_CHECKING
from bacommon.login import LoginType
import bascenev1 as bs
import bauiv1 as bui
@ -59,29 +60,25 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
)
)
self._account_type = (
plus.get_v1_account_type()
if plus.get_v1_account_state() == 'signed_in'
else None
)
self._game_service_icon_color: Sequence[float] | None
self._game_service_achievements_texture: bui.Texture | None
self._game_service_leaderboards_texture: bui.Texture | None
if self._account_type == 'Game Center':
# Tie in to specific game services if they are active.
adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
gpgs_active = adapter is not None and adapter.is_back_end_active()
adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
game_center_active = (
adapter is not None and adapter.is_back_end_active()
)
if game_center_active:
self._game_service_icon_color = (1.0, 1.0, 1.0)
icon = bui.gettexture('gameCenterIcon')
self._game_service_achievements_texture = icon
self._game_service_leaderboards_texture = icon
self._account_has_achievements = True
elif self._account_type == 'Game Circle':
icon = bui.gettexture('gameCircleIcon')
self._game_service_icon_color = (1, 1, 1)
self._game_service_achievements_texture = icon
self._game_service_leaderboards_texture = icon
self._account_has_achievements = True
elif self._account_type == 'Google Play':
elif gpgs_active:
self._game_service_icon_color = (0.8, 1.0, 0.6)
self._game_service_achievements_texture = bui.gettexture(
'googlePlayAchievementsIcon'
@ -193,7 +190,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
super().__del__()
# If our UI is still up, kill it.
if self._root_ui:
if self._root_ui and not self._root_ui.transitioning_out:
with bui.ContextRef.empty():
bui.containerwidget(edit=self._root_ui, transition='out_left')
@ -287,20 +284,20 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
self.end({'outcome': 'next_level'})
def _ui_gc(self) -> None:
if bs.app.classic is not None:
bs.app.classic.show_online_score_ui(
if bs.app.plus is not None:
bs.app.plus.show_game_service_ui(
'leaderboard',
game=self._game_name_str,
game_version=self._game_config_str,
)
else:
logging.warning('show_online_score_ui requires classic')
logging.warning('show_game_service_ui requires plus feature-set')
def _ui_show_achievements(self) -> None:
if bs.app.classic is not None:
bs.app.classic.show_online_score_ui('achievements')
if bs.app.plus is not None:
bs.app.plus.show_game_service_ui('achievements')
else:
logging.warning('show_online_score_ui requires classic')
logging.warning('show_game_service_ui requires plus feature-set')
def _ui_worlds_best(self) -> None:
if self._score_link is None:

View file

@ -35,7 +35,7 @@ class ControlsGuide(bs.Actor):
delay: is the time in seconds before the overlay fades in.
lifespan: if not None, the overlay will fade back out and die after
that long (in milliseconds).
that long (in seconds).
bright: if True, brighter colors will be used; handy when showing
over gameplay but may be too bright for join-screens, etc.
@ -50,6 +50,7 @@ class ControlsGuide(bs.Actor):
offs5 = 43.0 * scale
ouya = False
maxw = 50
xtweak = -2.8 * scale
self._lifespan = lifespan
self._dead = False
self._bright = bright
@ -117,7 +118,7 @@ class ControlsGuide(bs.Actor):
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'position': (pos[0] + xtweak, pos[1] - offs5),
'color': clr,
},
)
@ -145,7 +146,7 @@ class ControlsGuide(bs.Actor):
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'position': (pos[0] + xtweak, pos[1] - offs5),
'color': clr,
},
)
@ -173,7 +174,7 @@ class ControlsGuide(bs.Actor):
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'position': (pos[0] + xtweak, pos[1] - offs5),
'color': clr,
},
)
@ -201,7 +202,7 @@ class ControlsGuide(bs.Actor):
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'position': (pos[0] + xtweak, pos[1] - offs5),
'color': clr,
},
)
@ -264,10 +265,19 @@ class ControlsGuide(bs.Actor):
bs.timer(delay, bs.WeakCall(self._start_updating))
@staticmethod
def _meaningful_button_name(device: bs.InputDevice, button: int) -> str:
def _meaningful_button_name(
device: bs.InputDevice, button_name: str
) -> str:
"""Return a flattened string button name; empty for non-meaningful."""
if not device.has_meaningful_button_names:
return ''
assert bs.app.classic is not None
button = bs.app.classic.get_input_device_mapped_value(
device, button_name
)
# -1 means unset; let's show that.
if button == -1:
return bs.Lstr(resource='configGamepadWindow.unsetText').evaluate()
return device.get_button_name(button).evaluate()
def _start_updating(self) -> None:
@ -289,10 +299,10 @@ class ControlsGuide(bs.Actor):
def _check_fade_in(self) -> None:
assert bs.app.classic is not None
# If we have a touchscreen, we only fade in if we have a player with
# an input device that is *not* the touchscreen.
# (otherwise it is confusing to see the touchscreen buttons right
# next to our display buttons)
# If we have a touchscreen, we only fade in if we have a player
# with an input device that is *not* the touchscreen. Otherwise
# it is confusing to see the touchscreen buttons right next to
# our display buttons.
touchscreen: bs.InputDevice | None = bs.getinputdevice(
'TouchScreen', '#1', doraise=False
)
@ -318,15 +328,7 @@ class ControlsGuide(bs.Actor):
'buttonBomb',
'buttonPickUp',
):
if (
self._meaningful_button_name(
device,
bs.app.classic.get_input_device_mapped_value(
device, name
),
)
!= ''
):
if self._meaningful_button_name(device, name) != '':
fade_in = True
break
if fade_in:
@ -401,58 +403,30 @@ class ControlsGuide(bs.Actor):
# We only care about movement buttons in the case of keyboards.
if all_keyboards:
right_button_names.add(
device.get_button_name(
classic.get_input_device_mapped_value(
device, 'buttonRight'
)
)
self._meaningful_button_name(device, 'buttonRight')
)
left_button_names.add(
device.get_button_name(
classic.get_input_device_mapped_value(
device, 'buttonLeft'
)
)
self._meaningful_button_name(device, 'buttonLeft')
)
down_button_names.add(
device.get_button_name(
classic.get_input_device_mapped_value(
device, 'buttonDown'
)
)
self._meaningful_button_name(device, 'buttonDown')
)
up_button_names.add(
device.get_button_name(
classic.get_input_device_mapped_value(
device, 'buttonUp'
)
)
self._meaningful_button_name(device, 'buttonUp')
)
# Ignore empty values; things like the remote app or
# wiimotes can return these.
bname = self._meaningful_button_name(
device,
classic.get_input_device_mapped_value(device, 'buttonPunch'),
)
bname = self._meaningful_button_name(device, 'buttonPunch')
if bname != '':
punch_button_names.add(bname)
bname = self._meaningful_button_name(
device,
classic.get_input_device_mapped_value(device, 'buttonJump'),
)
bname = self._meaningful_button_name(device, 'buttonJump')
if bname != '':
jump_button_names.add(bname)
bname = self._meaningful_button_name(
device,
classic.get_input_device_mapped_value(device, 'buttonBomb'),
)
bname = self._meaningful_button_name(device, 'buttonBomb')
if bname != '':
bomb_button_names.add(bname)
bname = self._meaningful_button_name(
device,
classic.get_input_device_mapped_value(device, 'buttonPickUp'),
)
bname = self._meaningful_button_name(device, 'buttonPickUp')
if bname != '':
pickup_button_names.add(bname)
@ -582,8 +556,8 @@ class ControlsGuide(bs.Actor):
if msg.immediate:
self._die()
else:
# If they don't need immediate,
# fade out our nodes and die later.
# If they don't need immediate, fade out our nodes and
# die later.
for node in self._nodes:
bs.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0})
bs.timer(3.1, bs.WeakCall(self._die))

View file

@ -41,17 +41,17 @@ class Spawner:
self,
spawner: Spawner,
data: Any,
pt: Sequence[float], # pylint: disable=invalid-name
pt: Sequence[float],
):
"""Instantiate with the given values."""
self.spawner = spawner
self.data = data
self.pt = pt # pylint: disable=invalid-name
self.pt = pt
def __init__(
self,
data: Any = None,
pt: Sequence[float] = (0, 0, 0), # pylint: disable=invalid-name
pt: Sequence[float] = (0, 0, 0),
spawn_time: float = 1.0,
send_spawn_message: bool = True,
spawn_callback: Callable[[], Any] | None = None,

View file

@ -624,7 +624,7 @@ class Spaz(bs.Actor):
1000.0 * (tval + self.curse_time)
)
self._curse_timer = bs.Timer(
5.0, bs.WeakCall(self.curse_explode)
5.0, bs.WeakCall(self.handlemessage, CurseExplodeMessage())
)
def equip_boxing_gloves(self) -> None:
@ -1136,7 +1136,7 @@ class Spaz(bs.Actor):
if self.hitpoints > 0:
# It's kinda crappy to die from impacts, so lets reduce
# impact damage by a reasonable amount *if* it'll keep us alive.
if msg.hit_type == 'impact' and damage > self.hitpoints:
if msg.hit_type == 'impact' and damage >= self.hitpoints:
# Drop damage to whatever puts us at 10 hit points,
# or 200 less than it used to be whichever is greater
# (so it *can* still kill us if its high enough).

View file

@ -122,7 +122,6 @@ def register_appearances() -> None:
"""Register our builtin spaz appearances."""
# This is quite ugly but will be going away so not worth cleaning up.
# pylint: disable=invalid-name
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements

View file

@ -14,13 +14,12 @@ from bascenev1lib.actor.flag import Flag
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.gameutils import SharedObjects
from bascenev1lib.actor.respawnicon import RespawnIcon
import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, Sequence
from bascenev1lib.actor.respawnicon import RespawnIcon
class ConquestFlag(Flag):
"""A custom flag for use with Conquest games."""
@ -49,7 +48,9 @@ class Player(bs.Player['Team']):
@property
def respawn_timer(self) -> bs.Timer | None:
"""Type safe access to standard respawn timer."""
return self.customdata.get('respawn_timer', None)
val = self.customdata.get('respawn_timer', None)
assert isinstance(val, (bs.Timer, type(None)))
return val
@respawn_timer.setter
def respawn_timer(self, value: bs.Timer | None) -> None:
@ -58,7 +59,9 @@ class Player(bs.Player['Team']):
@property
def respawn_icon(self) -> RespawnIcon | None:
"""Type safe access to standard respawn icon."""
return self.customdata.get('respawn_icon', None)
val = self.customdata.get('respawn_icon', None)
assert isinstance(val, (RespawnIcon, type(None)))
return val
@respawn_icon.setter
def respawn_icon(self, value: RespawnIcon | None) -> None:

View file

@ -300,7 +300,10 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
from bauiv1lib import specialoffer
assert bs.app.classic is not None
if bool(False):
if bui.app.env.headless:
# UI stuff fails now in headless builds; avoid it.
pass
elif bool(False):
uicontroller = bs.app.ui_v1.controller
assert uicontroller is not None
uicontroller.show_main_menu()
@ -314,7 +317,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
from bauiv1lib.kiosk import KioskWindow
bs.app.ui_v1.set_main_menu_window(
KioskWindow().get_root_widget()
KioskWindow().get_root_widget(),
from_window=False, # Disable check here.
)
# ..or in normal cases go back to the main menu
else:
@ -323,14 +327,16 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
from bauiv1lib.gather import GatherWindow
bs.app.ui_v1.set_main_menu_window(
GatherWindow(transition=None).get_root_widget()
GatherWindow(transition=None).get_root_widget(),
from_window=False, # Disable check here.
)
elif main_menu_location == 'Watch':
# pylint: disable=cyclic-import
from bauiv1lib.watch import WatchWindow
bs.app.ui_v1.set_main_menu_window(
WatchWindow(transition=None).get_root_widget()
WatchWindow(transition=None).get_root_widget(),
from_window=False, # Disable check here.
)
elif main_menu_location == 'Team Game Select':
# pylint: disable=cyclic-import
@ -341,7 +347,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
bs.app.ui_v1.set_main_menu_window(
PlaylistBrowserWindow(
sessiontype=bs.DualTeamSession, transition=None
).get_root_widget()
).get_root_widget(),
from_window=False, # Disable check here.
)
elif main_menu_location == 'Free-for-All Game Select':
# pylint: disable=cyclic-import
@ -353,28 +360,34 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
PlaylistBrowserWindow(
sessiontype=bs.FreeForAllSession,
transition=None,
).get_root_widget()
).get_root_widget(),
from_window=False, # Disable check here.
)
elif main_menu_location == 'Coop Select':
# pylint: disable=cyclic-import
from bauiv1lib.coop.browser import CoopBrowserWindow
bs.app.ui_v1.set_main_menu_window(
CoopBrowserWindow(transition=None).get_root_widget()
CoopBrowserWindow(
transition=None
).get_root_widget(),
from_window=False, # Disable check here.
)
elif main_menu_location == 'Benchmarks & Stress Tests':
# pylint: disable=cyclic-import
from bauiv1lib.debug import DebugWindow
bs.app.ui_v1.set_main_menu_window(
DebugWindow(transition=None).get_root_widget()
DebugWindow(transition=None).get_root_widget(),
from_window=False, # Disable check here.
)
else:
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
bs.app.ui_v1.set_main_menu_window(
MainMenuWindow(transition=None).get_root_widget()
MainMenuWindow(transition=None).get_root_widget(),
from_window=None,
)
# attempt to show any pending offers immediately.

View file

@ -8,7 +8,6 @@
# pylint: disable=too-many-statements
# pylint: disable=too-many-lines
# pylint: disable=missing-function-docstring, missing-class-docstring
# pylint: disable=invalid-name
# pylint: disable=too-many-locals
# pylint: disable=unused-argument
# pylint: disable=unused-variable

View file

@ -31,7 +31,10 @@ from babase import (
apptimer,
AppTimer,
Call,
can_toggle_fullscreen,
fullscreen_control_available,
fullscreen_control_get,
fullscreen_control_key_shortcut,
fullscreen_control_set,
charstr,
clipboard_is_supported,
clipboard_set_text,
@ -57,18 +60,21 @@ from babase import (
in_logic_thread,
increment_analytics_count,
is_browser_likely_available,
is_running_on_fire_tv,
is_xcode_build,
Keyboard,
lock_all_input,
LoginAdapter,
LoginInfo,
Lstr,
native_review_request,
native_review_request_supported,
NotFoundError,
open_file_externally,
Permission,
Plugin,
PluginSpec,
pushcall,
quit,
QuitType,
request_permission,
safecolor,
screenmessage,
@ -87,7 +93,6 @@ from babase import (
from _bauiv1 import (
buttonwidget,
can_show_ad,
checkboxwidget,
columnwidget,
containerwidget,
@ -96,21 +101,15 @@ from _bauiv1 import (
getmesh,
getsound,
gettexture,
has_video_ads,
have_incentivized_ad,
hscrollwidget,
imagewidget,
is_party_icon_visible,
Mesh,
open_file_externally,
open_url,
rowwidget,
scrollwidget,
set_party_icon_always_visible,
set_party_window_open,
show_ad,
show_ad_2,
show_online_score_ui,
Sound,
Texture,
textwidget,
@ -118,6 +117,7 @@ from _bauiv1 import (
Widget,
widget,
)
from bauiv1._keyboard import Keyboard
from bauiv1._uitypes import Window, uicleanupcheck
from bauiv1._subsystem import UIV1Subsystem
@ -137,8 +137,10 @@ __all__ = [
'AppTimer',
'buttonwidget',
'Call',
'can_show_ad',
'can_toggle_fullscreen',
'fullscreen_control_available',
'fullscreen_control_get',
'fullscreen_control_key_shortcut',
'fullscreen_control_set',
'charstr',
'checkboxwidget',
'clipboard_is_supported',
@ -168,8 +170,6 @@ __all__ = [
'getmesh',
'getsound',
'gettexture',
'has_video_ads',
'have_incentivized_ad',
'have_permission',
'hscrollwidget',
'imagewidget',
@ -177,13 +177,15 @@ __all__ = [
'increment_analytics_count',
'is_browser_likely_available',
'is_party_icon_visible',
'is_running_on_fire_tv',
'is_xcode_build',
'Keyboard',
'lock_all_input',
'LoginAdapter',
'LoginInfo',
'Lstr',
'Mesh',
'native_review_request',
'native_review_request_supported',
'NotFoundError',
'open_file_externally',
'open_url',
@ -192,6 +194,7 @@ __all__ = [
'PluginSpec',
'pushcall',
'quit',
'QuitType',
'request_permission',
'rowwidget',
'safecolor',
@ -202,9 +205,6 @@ __all__ = [
'set_party_icon_always_visible',
'set_party_window_open',
'set_ui_input_device',
'show_ad',
'show_ad_2',
'show_online_score_ui',
'Sound',
'SpecialChar',
'supports_max_fps',

View file

@ -6,6 +6,7 @@
from __future__ import annotations
import logging
import inspect
from typing import TYPE_CHECKING
import _bauiv1
@ -13,6 +14,8 @@ import _bauiv1
if TYPE_CHECKING:
from typing import Sequence
import babase
def ticket_icon_press() -> None:
from babase import app
@ -57,14 +60,14 @@ def party_icon_activate(origin: Sequence[float]) -> None:
logging.warning('party_icon_activate: no classic.')
def quit_window() -> None:
def quit_window(quit_type: babase.QuitType) -> None:
from babase import app
if app.classic is None:
logging.exception('Classic not present.')
return
app.classic.quit_window()
app.classic.quit_window(quit_type)
def device_menu_press(device_id: int | None) -> None:
@ -85,3 +88,19 @@ def show_url_window(address: str) -> None:
return
app.classic.show_url_window(address)
def double_transition_out_warning() -> None:
"""Called if a widget is set to transition out twice."""
caller_frame = inspect.stack()[1]
caller_filename = caller_frame.filename
caller_line_number = caller_frame.lineno
logging.warning(
'ContainerWidget was set to transition out twice;'
' this often implies buggy code (%s line %s).\n'
' Generally you should check the value of'
' _root_widget.transitioning_out and perform no actions if that'
' is True.',
caller_filename,
caller_line_number,
)

33
dist/ba_data/python/bauiv1/_keyboard.py vendored Normal file
View file

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

@ -5,6 +5,7 @@
from __future__ import annotations
import logging
import inspect
from typing import TYPE_CHECKING
import babase
@ -66,6 +67,16 @@ class UIV1Subsystem(babase.AppSubsystem):
# a more elegant way once we revamp high level UI stuff a bit.
self.selecting_private_party_playlist: bool = False
@property
def available(self) -> bool:
"""Can uiv1 currently be used?
Code that may run in headless mode, before the UI has been spun up,
while other ui systems are active, etc. can check this to avoid
likely erroring.
"""
return _bauiv1.is_available()
@property
def uiscale(self) -> babase.UIScale:
"""Current ui scale for the app."""
@ -106,21 +117,69 @@ class UIV1Subsystem(babase.AppSubsystem):
# FIXME: Can probably kill this if we do immediate UI death checks.
self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True)
def set_main_menu_window(self, window: bauiv1.Widget) -> None:
"""Set the current 'main' window, replacing any existing."""
def set_main_menu_window(
self,
window: bauiv1.Widget,
from_window: bauiv1.Widget | None | bool = True,
) -> None:
"""Set the current 'main' window, replacing any existing.
If 'from_window' is passed as a bauiv1.Widget or None, a warning
will be issued if it that value does not match the current main
window. This can help clean up flawed code that can lead to bad
UI states. A value of False will disable the check.
"""
existing = self._main_menu_window
from inspect import currentframe, getframeinfo
try:
if isinstance(from_window, bool):
# For default val True we warn that the arg wasn't
# passed. False can be explicitly passed to disable this
# check.
if from_window is True:
caller_frame = inspect.stack()[1]
caller_filename = caller_frame.filename
caller_line_number = caller_frame.lineno
logging.warning(
'set_main_menu_window() should be passed a'
" 'from_window' value to help ensure proper UI behavior"
' (%s line %i).',
caller_filename,
caller_line_number,
)
else:
# For everything else, warn if what they passed wasn't
# the previous main menu widget.
if from_window is not existing:
caller_frame = inspect.stack()[1]
caller_filename = caller_frame.filename
caller_line_number = caller_frame.lineno
logging.warning(
"set_main_menu_window() was passed 'from_window' %s"
' but existing main-menu-window is %s. (%s line %i).',
from_window,
existing,
caller_filename,
caller_line_number,
)
except Exception:
# Prevent any bugs in these checks from causing problems.
logging.exception('Error checking from_window')
# Once the above code leads to us fixing all leftover window bugs
# at the source, we can kill the code below.
# Let's grab the location where we were called from to report
# if we have to force-kill the existing window (which normally
# should not happen).
frameline = None
try:
frame = currentframe()
frame = inspect.currentframe()
if frame is not None:
frame = frame.f_back
if frame is not None:
frameinfo = getframeinfo(frame)
frameinfo = inspect.getframeinfo(frame)
frameline = f'{frameinfo.filename} {frameinfo.lineno}'
except Exception:
logging.exception('Error calcing line for set_main_menu_window')
@ -150,13 +209,18 @@ class UIV1Subsystem(babase.AppSubsystem):
def clear_main_menu_window(self, transition: str | None = None) -> None:
"""Clear any existing 'main' window with the provided transition."""
assert transition is None or not transition.endswith('_in')
if self._main_menu_window:
if transition is not None:
if (
transition is not None
and not self._main_menu_window.transitioning_out
):
_bauiv1.containerwidget(
edit=self._main_menu_window, transition=transition
)
else:
self._main_menu_window.delete()
self._main_menu_window = None
def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None:
"""(internal)"""

View file

@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
import babase
import _bauiv1
from bauiv1._keyboard import Keyboard
from bauiv1._uitypes import Window
if TYPE_CHECKING:
@ -51,6 +52,19 @@ class OnScreenKeyboardWindow(Window):
else (0, 0),
)
)
self._cancel_button = _bauiv1.buttonwidget(
parent=self._root_widget,
scale=0.5,
position=(30, self._height - 55),
size=(60, 60),
label='',
enable_sound=False,
on_activate_call=self._cancel,
autoselect=True,
color=(0.55, 0.5, 0.6),
icon=_bauiv1.gettexture('crossOut'),
iconscale=1.2,
)
self._done_button = _bauiv1.buttonwidget(
parent=self._root_widget,
position=(self._width - 200, 44),
@ -240,9 +254,7 @@ class OnScreenKeyboardWindow(Window):
# Show change instructions only if we have more than one
# keyboard option.
keyboards = (
babase.app.meta.scanresults.exports_of_class(
babase.Keyboard
)
babase.app.meta.scanresults.exports_of_class(Keyboard)
if babase.app.meta.scanresults is not None
else []
)
@ -274,10 +286,10 @@ class OnScreenKeyboardWindow(Window):
def _get_keyboard(self) -> bui.Keyboard:
assert babase.app.meta.scanresults is not None
classname = babase.app.meta.scanresults.exports_of_class(
babase.Keyboard
)[self._keyboard_index]
kbclass = babase.getclass(classname, babase.Keyboard)
classname = babase.app.meta.scanresults.exports_of_class(Keyboard)[
self._keyboard_index
]
kbclass = babase.getclass(classname, Keyboard)
return kbclass()
def _refresh(self) -> None:
@ -372,9 +384,7 @@ class OnScreenKeyboardWindow(Window):
def _next_keyboard(self) -> None:
assert babase.app.meta.scanresults is not None
kbexports = babase.app.meta.scanresults.exports_of_class(
babase.Keyboard
)
kbexports = babase.app.meta.scanresults.exports_of_class(Keyboard)
self._keyboard_index = (self._keyboard_index + 1) % len(kbexports)
self._load_keyboard()

View file

@ -63,20 +63,14 @@ class AccountSettingsWindow(bui.Window):
1.0, bui.WeakCall(self._update), repeat=True
)
# Currently we can only reset achievements on game-center.
v1_account_type: str | None
if self._v1_signed_in:
v1_account_type = plus.get_v1_account_type()
else:
v1_account_type = None
self._can_reset_achievements = v1_account_type == 'Game Center'
self._can_reset_achievements = False
app = bui.app
assert app.classic is not None
uiscale = app.ui_v1.uiscale
self._width = 760 if uiscale is bui.UIScale.SMALL else 660
x_offs = 50 if uiscale is bui.UIScale.SMALL else 0
self._width = 860 if uiscale is bui.UIScale.SMALL else 660
x_offs = 100 if uiscale is bui.UIScale.SMALL else 0
self._height = (
390
if uiscale is bui.UIScale.SMALL
@ -98,6 +92,9 @@ class AccountSettingsWindow(bui.Window):
if LoginType.GPGS in plus.accounts.login_adapters:
self._show_sign_in_buttons.append('Google Play')
if LoginType.GAME_CENTER in plus.accounts.login_adapters:
self._show_sign_in_buttons.append('Game Center')
# Always want to show our web-based v2 login option.
self._show_sign_in_buttons.append('V2Proxy')
@ -227,6 +224,8 @@ class AccountSettingsWindow(bui.Window):
plus = bui.app.plus
assert plus is not None
via_lines: list[str] = []
primary_v2_account = plus.accounts.primary
v1_state = plus.get_v1_account_state()
@ -237,14 +236,55 @@ class AccountSettingsWindow(bui.Window):
# We expose GPGS-specific functionality only if it is 'active'
# (meaning the current GPGS player matches one of our account's
# logins).
gpgs_adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
is_gpgs = (
False if gpgs_adapter is None else gpgs_adapter.is_back_end_active()
adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
gpgs_active = adapter is not None and adapter.is_back_end_active()
# Ditto for Game Center.
adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
game_center_active = (
adapter is not None and adapter.is_back_end_active()
)
show_signed_in_as = self._v1_signed_in
signed_in_as_space = 95.0
# To reduce confusion about the whole V2 account situation for
# people used to seeing their Google Play Games or Game Center
# account name and icon and whatnot, let's show those underneath
# the V2 tag to help communicate that they are in fact logged in
# through that account.
via_space = 25.0
if show_signed_in_as and bui.app.plus is not None:
accounts = bui.app.plus.accounts
if accounts.primary is not None:
# For these login types, we show 'via' IF there is a
# login of that type attached to our account AND it is
# currently active (We don't want to show 'via Game
# Center' if we're signed out of Game Center or
# currently running on Steam, even if there is a Game
# Center login attached to our account).
for ltype, lchar in [
(LoginType.GPGS, bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
(LoginType.GAME_CENTER, bui.SpecialChar.GAME_CENTER_LOGO),
]:
linfo = accounts.primary.logins.get(ltype)
ladapter = accounts.login_adapters.get(ltype)
if (
linfo is not None
and ladapter is not None
and ladapter.is_back_end_active()
):
via_lines.append(f'{bui.charstr(lchar)}{linfo.name}')
# TEMP TESTING
if bool(False):
icontxt = bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
via_lines.append(f'{icontxt}FloofDibble')
icontxt = bui.charstr(
bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO
)
via_lines.append(f'{icontxt}StinkBobble')
show_sign_in_benefits = not self._v1_signed_in
sign_in_benefits_space = 80.0
@ -258,6 +298,11 @@ class AccountSettingsWindow(bui.Window):
and self._signing_in_adapter is None
and 'Google Play' in self._show_sign_in_buttons
)
show_game_center_sign_in_button = (
v1_state == 'signed_out'
and self._signing_in_adapter is None
and 'Game Center' in self._show_sign_in_buttons
)
show_v2_proxy_sign_in_button = (
v1_state == 'signed_out'
and self._signing_in_adapter is None
@ -271,9 +316,8 @@ class AccountSettingsWindow(bui.Window):
sign_in_button_space = 70.0
deprecated_space = 60
show_game_service_button = self._v1_signed_in and v1_account_type in [
'Game Center'
]
# Game Center currently has a single UI for everything.
show_game_service_button = game_center_active
game_service_button_space = 60.0
show_what_is_v2 = self._v1_signed_in and v1_account_type == 'V2'
@ -281,11 +325,9 @@ class AccountSettingsWindow(bui.Window):
show_linked_accounts_text = self._v1_signed_in
linked_accounts_text_space = 60.0
show_achievements_button = self._v1_signed_in and v1_account_type in (
'Google Play',
'Local',
'V2',
)
# Always show achievements except in the game-center case where
# its unified UI covers them.
show_achievements_button = self._v1_signed_in and not game_center_active
achievements_button_space = 60.0
show_achievements_text = (
@ -293,7 +335,7 @@ class AccountSettingsWindow(bui.Window):
)
achievements_text_space = 27.0
show_leaderboards_button = self._v1_signed_in and is_gpgs
show_leaderboards_button = self._v1_signed_in and gpgs_active
leaderboards_button_space = 60.0
show_campaign_progress = self._v1_signed_in
@ -330,7 +372,6 @@ class AccountSettingsWindow(bui.Window):
show_sign_out_button = self._v1_signed_in and v1_account_type in [
'Local',
'Google Play',
'V2',
]
sign_out_button_space = 70.0
@ -349,10 +390,13 @@ class AccountSettingsWindow(bui.Window):
self._sub_height = 60.0
if show_signed_in_as:
self._sub_height += signed_in_as_space
self._sub_height += via_space * len(via_lines)
if show_signing_in_text:
self._sub_height += signing_in_text_space
if show_google_play_sign_in_button:
self._sub_height += sign_in_button_space
if show_game_center_sign_in_button:
self._sub_height += sign_in_button_space
if show_v2_proxy_sign_in_button:
self._sub_height += sign_in_button_space
if show_device_sign_in_button:
@ -442,20 +486,21 @@ class AccountSettingsWindow(bui.Window):
self._account_name_what_is_text = bui.textwidget(
parent=self._subcontainer,
position=(0.0, self._account_name_what_is_y),
size=(200.0, 60),
size=(220.0, 60),
text=bui.Lstr(
value='${WHAT} -->',
subs=[('${WHAT}', bui.Lstr(resource='whatIsThisText'))],
),
scale=0.6,
color=(0.3, 0.7, 0.05),
maxwidth=200.0,
maxwidth=130.0,
h_align='right',
v_align='center',
autoselect=True,
selectable=True,
on_activate_call=show_what_is_v2_page,
click_activate=True,
glow_type='uniform',
)
if first_selectable is None:
first_selectable = self._account_name_what_is_text
@ -466,6 +511,54 @@ class AccountSettingsWindow(bui.Window):
v -= signed_in_as_space * 0.4
for via in via_lines:
v -= via_space * 0.1
sscale = 0.7
swidth = (
bui.get_string_width(via, suppress_warning=True) * sscale
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v),
size=(0, 0),
text=via,
scale=sscale,
color=(0.6, 0.6, 0.6),
flatness=1.0,
shadow=0.0,
h_align='center',
v_align='center',
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5 - swidth * 0.5 - 5, v),
size=(0, 0),
text=bui.Lstr(
value='(${VIA}',
subs=[('${VIA}', bui.Lstr(resource='viaText'))],
),
scale=0.5,
color=(0.4, 0.6, 0.4, 0.5),
flatness=1.0,
shadow=0.0,
h_align='right',
v_align='center',
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5 + swidth * 0.5 + 10, v),
size=(0, 0),
text=')',
scale=0.5,
color=(0.4, 0.6, 0.4, 0.5),
flatness=1.0,
shadow=0.0,
h_align='right',
v_align='center',
)
v -= via_space * 0.9
else:
self._account_name_text = None
self._account_name_what_is_text = None
@ -477,22 +570,6 @@ class AccountSettingsWindow(bui.Window):
if show_sign_in_benefits:
v -= sign_in_benefits_space
app = bui.app
assert app.classic is not None
extra: str | bui.Lstr | None
if (
app.classic.platform in ['mac', 'ios']
and app.classic.subplatform == 'appstore'
):
extra = bui.Lstr(
value='\n${S}',
subs=[
('${S}', bui.Lstr(resource='signInWithGameCenterText'))
],
)
else:
extra = ''
bui.textwidget(
parent=self._subcontainer,
position=(
@ -500,16 +577,7 @@ class AccountSettingsWindow(bui.Window):
v + sign_in_benefits_space * 0.4,
),
size=(0, 0),
text=bui.Lstr(
value='${A}${B}',
subs=[
(
'${A}',
bui.Lstr(resource=self._r + '.signInInfoText'),
),
('${B}', extra),
],
),
text=bui.Lstr(resource=self._r + '.signInInfoText'),
max_height=sign_in_benefits_space * 0.9,
scale=0.9,
color=(0.75, 0.7, 0.8),
@ -554,7 +622,13 @@ class AccountSettingsWindow(bui.Window):
(
'${B}',
bui.Lstr(
resource=self._r + '.signInWithGooglePlayText'
resource=self._r + '.signInWithText',
subs=[
(
'${SERVICE}',
bui.Lstr(resource='googlePlayText'),
)
],
),
),
],
@ -572,6 +646,48 @@ class AccountSettingsWindow(bui.Window):
bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None
if show_game_center_sign_in_button:
button_width = 350
v -= sign_in_button_space
self._sign_in_google_play_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v - 20),
autoselect=True,
size=(button_width, 60),
# Note: Apparently Game Center is just called 'Game Center'
# in all languages. Can revisit if not true.
# https://developer.apple.com/forums/thread/725779
label=bui.Lstr(
value='${A}${B}',
subs=[
(
'${A}',
bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO),
),
(
'${B}',
bui.Lstr(
resource=self._r + '.signInWithText',
subs=[('${SERVICE}', 'Game Center')],
),
),
],
),
on_activate_call=lambda: self._sign_in_press(
LoginType.GAME_CENTER
),
)
if first_selectable is None:
first_selectable = btn
if bui.app.ui_v1.use_toolbars:
bui.widget(
edit=btn,
right_widget=bui.get_special_widget('party_button'),
)
bui.widget(edit=btn, left_widget=bbtn)
bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None
if show_v2_proxy_sign_in_button:
button_width = 350
v -= sign_in_button_space
@ -704,7 +820,7 @@ class AccountSettingsWindow(bui.Window):
position=((self._sub_width - button_width) * 0.5, v + 30),
autoselect=True,
size=(button_width, 60),
label=bui.Lstr(resource=self._r + '.manageAccountText'),
label=bui.Lstr(resource=f'{self._r}.manageAccountText'),
color=(0.55, 0.5, 0.6),
icon=bui.gettexture('settingsIcon'),
textcolor=(0.75, 0.7, 0.8),
@ -745,10 +861,15 @@ class AccountSettingsWindow(bui.Window):
# the button to go to OS-Specific leaderboards/high-score-lists/etc.
if show_game_service_button:
button_width = 300
v -= game_service_button_space * 0.85
v1_account_type = plus.get_v1_account_type()
if v1_account_type == 'Game Center':
v1_account_type_name = bui.Lstr(resource='gameCenterText')
v -= game_service_button_space * 0.6
if game_center_active:
# Note: Apparently Game Center is just called 'Game Center'
# in all languages. Can revisit if not true.
# https://developer.apple.com/forums/thread/725779
game_service_button_label = bui.Lstr(
value=bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
+ 'Game Center'
)
else:
raise ValueError(
"unknown account type: '" + str(v1_account_type) + "'"
@ -761,7 +882,7 @@ class AccountSettingsWindow(bui.Window):
autoselect=True,
on_activate_call=self._on_game_service_button_press,
size=(button_width, 50),
label=v1_account_type_name,
label=game_service_button_label,
)
if first_selectable is None:
first_selectable = btn
@ -771,7 +892,7 @@ class AccountSettingsWindow(bui.Window):
right_widget=bui.get_special_widget('party_button'),
)
bui.widget(edit=btn, left_widget=bbtn)
v -= game_service_button_space * 0.15
v -= game_service_button_space * 0.4
else:
self.game_service_button = None
@ -804,13 +925,15 @@ class AccountSettingsWindow(bui.Window):
autoselect=True,
icon=bui.gettexture(
'googlePlayAchievementsIcon'
if is_gpgs
if gpgs_active
else 'achievementsIcon'
),
icon_color=(0.8, 0.95, 0.7) if is_gpgs else (0.85, 0.8, 0.9),
icon_color=(0.8, 0.95, 0.7)
if gpgs_active
else (0.85, 0.8, 0.9),
on_activate_call=(
self._on_custom_achievements_press
if is_gpgs
if gpgs_active
else self._on_achievements_press
),
size=(button_width, 50),
@ -1135,19 +1258,21 @@ class AccountSettingsWindow(bui.Window):
self._needs_refresh = False
def _on_game_service_button_press(self) -> None:
if bui.app.classic is not None:
bui.app.classic.show_online_score_ui()
if bui.app.plus is not None:
bui.app.plus.show_game_service_ui()
else:
logging.warning('game service ui not available without classic.')
logging.warning(
'game-service-ui not available without plus feature-set.'
)
def _on_custom_achievements_press(self) -> None:
if bui.app.classic is not None:
if bui.app.plus is not None:
bui.apptimer(
0.15,
bui.Call(bui.app.classic.show_online_score_ui, 'achievements'),
bui.Call(bui.app.plus.show_game_service_ui, 'achievements'),
)
else:
logging.warning('show_online_score_ui requires classic')
logging.warning('show_game_service_ui requires plus feature-set.')
def _on_achievements_press(self) -> None:
# pylint: disable=cyclic-import
@ -1162,11 +1287,21 @@ class AccountSettingsWindow(bui.Window):
show_what_is_v2_page()
def _on_manage_account_press(self) -> None:
bui.screenmessage(bui.Lstr(resource='oneMomentText'))
plus = bui.app.plus
assert plus is not None
# Preemptively fail if it looks like we won't be able to talk to
# the server anyway.
if not plus.cloud.connected:
bui.screenmessage(
bui.Lstr(resource='internal.unavailableNoConnectionText'),
color=(1, 0, 0),
)
bui.getsound('error').play()
return
bui.screenmessage(bui.Lstr(resource='oneMomentText'))
# We expect to have a v2 account signed in if we get here.
if plus.accounts.primary is None:
logging.exception(
@ -1184,6 +1319,9 @@ class AccountSettingsWindow(bui.Window):
self, response: bacommon.cloud.ManageAccountResponse | Exception
) -> None:
if isinstance(response, Exception) or response.url is None:
logging.warning(
'Got error in manage-account-response: %s.', response
)
bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
bui.getsound('error').play()
return
@ -1191,13 +1329,13 @@ class AccountSettingsWindow(bui.Window):
bui.open_url(response.url)
def _on_leaderboards_press(self) -> None:
if bui.app.classic is not None:
if bui.app.plus is not None:
bui.apptimer(
0.15,
bui.Call(bui.app.classic.show_online_score_ui, 'leaderboards'),
bui.Call(bui.app.plus.show_game_service_ui, 'leaderboards'),
)
else:
logging.warning('show_online_score_ui requires classic')
logging.warning('show_game_service_ui requires classic')
def _have_unlinkable_v1_accounts(self) -> bool:
plus = bui.app.plus
@ -1323,7 +1461,7 @@ class AccountSettingsWindow(bui.Window):
swidth = bui.get_string_width(name_str, suppress_warning=True)
# Eww; number-fudging. Need to recalibrate this if
# account name scaling changes.
x = self._sub_width * 0.5 - swidth * 0.75 - 170
x = self._sub_width * 0.5 - swidth * 0.75 - 190
bui.textwidget(
edit=self._account_name_what_is_text,
@ -1371,9 +1509,18 @@ class AccountSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.profile.browser import ProfileBrowserWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
ProfileBrowserWindow(origin_widget=self._player_profiles_button)
bui.app.ui_v1.set_main_menu_window(
ProfileBrowserWindow(
origin_widget=self._player_profiles_button
).get_root_widget(),
from_window=self._root_widget,
)
def _cancel_sign_in_press(self) -> None:
# If we're waiting on an adapter to give us credentials, abort.
@ -1466,7 +1613,11 @@ class AccountSettingsWindow(bui.Window):
if isinstance(result, Exception):
# For now just make a bit of noise if anything went wrong;
# can get more specific as needed later.
bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
logging.warning('Got error in v2 sign-in result: %s', result)
bui.screenmessage(
bui.Lstr(resource='internal.signInNoConnectionText'),
color=(1, 0, 0),
)
bui.getsound('error').play()
else:
# Success! Plug in these credentials which will begin
@ -1530,6 +1681,10 @@ class AccountSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@ -1538,7 +1693,8 @@ class AccountSettingsWindow(bui.Window):
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
MainMenuWindow(transition='in_left').get_root_widget()
MainMenuWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
def _save_state(self) -> None:

View file

@ -62,14 +62,11 @@ class V2ProxySignInWindow(bui.Window):
label=bui.Lstr(resource='cancelText'),
on_activate_call=self._done,
autoselect=True,
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
)
if bool(False):
bui.containerwidget(
edit=self._root_widget, cancel_button=self._cancel_button
)
bui.containerwidget(
edit=self._root_widget, cancel_button=self._cancel_button
)
self._update_timer: bui.AppTimer | None = None
@ -131,14 +128,17 @@ class V2ProxySignInWindow(bui.Window):
else:
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 145),
size=(0, 0),
position=(self._width * 0.5 - 200, self._height - 180),
size=(button_width - 50, 50),
text=bui.Lstr(value=address_pretty),
flatness=1.0,
maxwidth=self._width,
scale=0.75,
h_align='center',
v_align='center',
autoselect=True,
on_activate_call=bui.Call(self._copy_link, address_pretty),
selectable=True,
)
qroffs = 20.0
@ -231,5 +231,15 @@ class V2ProxySignInWindow(bui.Window):
# We could do something smart like retry on exceptions here, but
# this isn't critical so we'll just let anything slide.
def _copy_link(self, link: str) -> None:
if bui.clipboard_is_supported():
bui.clipboard_set_text(link)
bui.screenmessage(
bui.Lstr(resource='copyConfirmText'), color=(0, 1, 0)
)
def _done(self) -> None:
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_scale')

View file

@ -93,6 +93,7 @@ class ConfigNumberEdit:
displayname: str | bui.Lstr | None = None,
changesound: bool = True,
textscale: float = 1.0,
as_percent: bool = False,
):
if displayname is None:
displayname = configkey
@ -103,6 +104,7 @@ class ConfigNumberEdit:
self._increment = increment
self._callback = callback
self._value = bui.app.config.resolve(configkey)
self._as_percent = as_percent
self.nametext = bui.textwidget(
parent=parent,
@ -166,4 +168,8 @@ class ConfigNumberEdit:
bui.app.config.apply_and_commit()
def _update_display(self) -> None:
bui.textwidget(edit=self.valuetext, text=f'{self._value:.1f}')
if self._as_percent:
val = f'{round(self._value*100.0)}%'
else:
val = f'{self._value:.1f}'
bui.textwidget(edit=self.valuetext, text=val)

View file

@ -153,15 +153,15 @@ class QuitWindow:
def __init__(
self,
quit_type: bui.QuitType | None = None,
swish: bool = False,
back: bool = False,
origin_widget: bui.Widget | None = None,
):
classic = bui.app.classic
assert classic is not None
ui = bui.app.ui_v1
app = bui.app
self._back = back
self._quit_type = quit_type
# If there's already one of us up somewhere, kill it.
if ui.quit_window is not None:
@ -187,29 +187,8 @@ class QuitWindow:
resource=quit_resource,
subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
),
self._fade_and_quit,
lambda: bui.quit(confirm=False, quit_type=self._quit_type)
if self._quit_type is not None
else bui.quit(confirm=False),
origin_widget=origin_widget,
).root_widget
def _fade_and_quit(self) -> None:
bui.fade_screen(
False,
time=0.2,
endcall=lambda: bui.quit(soft=True, back=self._back),
)
# Prevent the user from doing anything else while we're on our
# way out.
bui.lock_all_input()
# On systems supporting soft-quit, unlock and fade back in shortly
# (soft-quit basically just backgrounds/hides the app).
if bui.app.env.supports_soft_quit:
# Unlock and fade back in shortly. Just in case something goes
# wrong (or on Android where quit just backs out of our activity
# and we may come back after).
def _come_back() -> None:
bui.unlock_all_input()
bui.fade_screen(True)
bui.apptimer(0.5, _come_back)

View file

@ -85,8 +85,8 @@ class CoopBrowserWindow(bui.Window):
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._width = 1320 if uiscale is bui.UIScale.SMALL else 1120
self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
self._width = 1520 if uiscale is bui.UIScale.SMALL else 1120
self._x_inset = x_inset = 200 if uiscale is bui.UIScale.SMALL else 0
self._height = (
657
if uiscale is bui.UIScale.SMALL
@ -415,7 +415,7 @@ class CoopBrowserWindow(bui.Window):
)
# Decrement time on our tournament buttons.
ads_enabled = bui.have_incentivized_ad()
ads_enabled = plus.have_incentivized_ad()
for tbtn in self._tournament_buttons:
tbtn.time_remaining = max(0, tbtn.time_remaining - 1)
if tbtn.time_remaining_value_text is not None:
@ -430,7 +430,7 @@ class CoopBrowserWindow(bui.Window):
)
# Also adjust the ad icon visibility.
if tbtn.allow_ads and bui.has_video_ads():
if tbtn.allow_ads and plus.has_video_ads():
bui.imagewidget(
edit=tbtn.entry_fee_ad_image,
opacity=1.0 if ads_enabled else 0.25,
@ -1019,6 +1019,10 @@ class CoopBrowserWindow(bui.Window):
from bauiv1lib.account import show_sign_in_prompt
from bauiv1lib.league.rankwindow import LeagueRankWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
plus = bui.app.plus
assert plus is not None
@ -1032,7 +1036,8 @@ class CoopBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
LeagueRankWindow(
origin_widget=self._league_rank_button.get_button()
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _switch_to_score(
@ -1043,6 +1048,10 @@ class CoopBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.account import show_sign_in_prompt
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
plus = bui.app.plus
assert plus is not None
@ -1058,7 +1067,8 @@ class CoopBrowserWindow(bui.Window):
origin_widget=self._store_button.get_button(),
show_tab=show_tab,
back_location='CoopBrowserWindow',
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def is_tourney_data_up_to_date(self) -> bool:
@ -1218,6 +1228,10 @@ class CoopBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
# If something is selected, store it.
self._save_state()
bui.containerwidget(
@ -1225,7 +1239,8 @@ class CoopBrowserWindow(bui.Window):
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PlayWindow(transition='in_left').get_root_widget()
PlayWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
def _save_state(self) -> None:

View file

@ -638,8 +638,8 @@ class TournamentButton:
# Now, if this fee allows ads and we support video ads, show
# the 'or ad' version.
if allow_ads and bui.has_video_ads():
ads_enabled = bui.have_incentivized_ad()
if allow_ads and plus.has_video_ads():
ads_enabled = plus.have_incentivized_ad()
bui.imagewidget(
edit=self.entry_fee_ad_image,
opacity=1.0 if ads_enabled else 0.25,

View file

@ -359,10 +359,15 @@ class CreditsListWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
MainMenuWindow(transition='in_left').get_root_widget()
MainMenuWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

View file

@ -379,8 +379,13 @@ class DebugWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AdvancedSettingsWindow(transition='in_left').get_root_widget()
AdvancedSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

133
dist/ba_data/python/bauiv1lib/discord.py vendored Normal file
View file

@ -0,0 +1,133 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality for the Discord window."""
from __future__ import annotations
import bauiv1 as bui
class DiscordWindow(bui.Window):
"""Window for joining the Discord."""
def __init__(
self,
transition: str = 'in_right',
origin_widget: bui.Widget | None = None,
):
if bui.app.classic is None:
raise RuntimeError('This requires classic support.')
app = bui.app
assert app.classic is not None
# If they provided an origin-widget, scale up from that.
scale_origin: tuple[float, float] | None
if origin_widget is not None:
self._transition_out = 'out_scale'
scale_origin = origin_widget.get_screen_space_center()
transition = 'in_scale'
else:
self._transition_out = 'out_right'
scale_origin = None
uiscale = bui.app.ui_v1.uiscale
self._width = 800
x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
self._height = 320
top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height + top_extra),
transition=transition,
toolbar_visibility='menu_minimal',
scale_origin_stack_offset=scale_origin,
scale=(
1.6
if uiscale is bui.UIScale.SMALL
else 1.3
if uiscale is bui.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, 5) if uiscale is bui.UIScale.SMALL else (0, 0),
)
)
if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
bui.containerwidget(
edit=self._root_widget, on_cancel_call=self._do_back
)
self._back_button = None
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
position=(53 + x_inset, self._height - 60),
size=(140, 60),
scale=0.8,
autoselect=True,
label=bui.Lstr(resource='backText'),
button_type='back',
on_activate_call=self._do_back,
)
bui.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
)
# Do we need to translate 'Discord'? Or is that always the name?
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(0, self._height - 52),
size=(self._width, 25),
text='Discord',
color=app.ui_v1.title_color,
h_align='center',
v_align='top',
)
min_size = min(self._width - 25, self._height - 25)
bui.imagewidget(
parent=self._root_widget,
position=(40, -15),
size=(min_size, min_size),
texture=bui.gettexture('discordServer'),
)
# Hmm should we translate this? The discord server is mostly
# English so being able to read this might be a good screening
# process?..
bui.textwidget(
parent=self._root_widget,
position=(self._width / 2 - 60, self._height - 100),
text='We have our own Discord server where you can:\n- Find new'
' friends and people to play with\n- Participate in Office'
' Hours/Coffee with Eric\n- Share mods, plugins, art, and'
' memes\n- Report bugs and make feature suggestions\n'
'- Troubleshoot issues',
maxwidth=(self._width - 10) / 2,
color=(1, 1, 1, 1),
h_align='left',
v_align='top',
)
bui.buttonwidget(
parent=self._root_widget,
position=(self._width / 2 - 30, 20),
size=(self._width / 2 - 60, 60),
autoselect=True,
label=bui.Lstr(resource='discordJoinText'),
text_scale=1.0,
on_activate_call=bui.Call(
bui.open_url, 'https://ballistica.net/discord'
),
)
if self._back_button is not None:
bui.buttonwidget(
edit=self._back_button,
button_type='backSmall',
size=(60, 60),
label=bui.charstr(bui.SpecialChar.BACK),
)
def _do_back(self) -> None:
bui.containerwidget(edit=self._root_widget, transition='out_scale')

View file

@ -94,8 +94,8 @@ class GatherWindow(bui.Window):
bui.app.ui_v1.set_main_menu_location('Gather')
bui.set_party_icon_always_visible(True)
uiscale = bui.app.ui_v1.uiscale
self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040
x_offs = 100 if uiscale is bui.UIScale.SMALL else 0
self._width = 1440 if uiscale is bui.UIScale.SMALL else 1040
x_offs = 200 if uiscale is bui.UIScale.SMALL else 0
self._height = (
582
if uiscale is bui.UIScale.SMALL
@ -270,12 +270,17 @@ class GatherWindow(bui.Window):
"""Called by the private-hosting tab to select a playlist."""
from bauiv1lib.play import PlayWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.selecting_private_party_playlist = True
bui.app.ui_v1.set_main_menu_window(
PlayWindow(origin_widget=origin_widget).get_root_widget()
PlayWindow(origin_widget=origin_widget).get_root_widget(),
from_window=self._root_widget,
)
def _set_tab(self, tab_id: TabID) -> None:
@ -383,11 +388,16 @@ class GatherWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
MainMenuWindow(transition='in_left').get_root_widget()
MainMenuWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

View file

@ -16,10 +16,6 @@ if TYPE_CHECKING:
class AboutGatherTab(GatherTab):
"""The about tab in the gather UI"""
def __init__(self, window: GatherWindow) -> None:
super().__init__(window)
self._container: bui.Widget | None = None
def on_activate(
self,
parent_widget: bui.Widget,
@ -29,9 +25,45 @@ class AboutGatherTab(GatherTab):
region_left: float,
region_bottom: float,
) -> bui.Widget:
# pylint: disable=too-many-locals
plus = bui.app.plus
assert plus is not None
try_tickets = plus.get_v1_account_misc_read_val(
'friendTryTickets', None
)
show_message = True
# Squish message as needed to get things to fit nicely at
# various scales.
uiscale = bui.app.ui_v1.uiscale
message_height = (
210
if uiscale is bui.UIScale.SMALL
else 305
if uiscale is bui.UIScale.MEDIUM
else 370
)
# Let's not talk about sharing in vr-mode; its tricky to fit more
# than one head in a VR-headset.
show_message_extra = not bui.app.env.vr
message_extra_height = 60
show_invite = try_tickets is not None
invite_height = 80
show_discord = True
discord_height = 80
c_height = 0
if show_message:
c_height += message_height
if show_message_extra:
c_height += message_extra_height
if show_invite:
c_height += invite_height
if show_discord:
c_height += discord_height
party_button_label = bui.charstr(bui.SpecialChar.TOP_BUTTON)
message = bui.Lstr(
resource='gatherWindow.aboutDescriptionText',
@ -41,9 +73,7 @@ class AboutGatherTab(GatherTab):
],
)
# Let's not talk about sharing in vr-mode; its tricky to fit more
# than one head in a VR-headset ;-)
if not bui.app.env.vr:
if show_message_extra:
message = bui.Lstr(
value='${A}\n\n${B}',
subs=[
@ -57,47 +87,52 @@ class AboutGatherTab(GatherTab):
),
],
)
string_height = 400
include_invite = True
msc_scale = 1.1
c_height_2 = min(region_height, string_height * msc_scale + 100)
try_tickets = plus.get_v1_account_misc_read_val(
'friendTryTickets', None
)
if try_tickets is None:
include_invite = False
self._container = bui.containerwidget(
scroll_widget = bui.scrollwidget(
parent=parent_widget,
position=(region_left, region_bottom),
size=(region_width, region_height),
highlight=False,
border_opacity=0,
)
msc_scale = 1.1
container = bui.containerwidget(
parent=scroll_widget,
position=(
region_left,
region_bottom + (region_height - c_height_2) * 0.5,
region_bottom + (region_height - c_height) * 0.5,
),
size=(region_width, c_height_2),
size=(region_width, c_height),
background=False,
selectable=include_invite,
selectable=show_invite or show_discord,
)
bui.widget(edit=self._container, up_widget=tab_button)
# Allows escaping if we select the container somehow (though
# shouldn't be possible when buttons are present).
bui.widget(edit=container, up_widget=tab_button)
bui.textwidget(
parent=self._container,
position=(
region_width * 0.5,
c_height_2 * (0.58 if include_invite else 0.5),
),
color=(0.6, 1.0, 0.6),
scale=msc_scale,
size=(0, 0),
maxwidth=region_width * 0.9,
max_height=c_height_2 * (0.7 if include_invite else 0.9),
h_align='center',
v_align='center',
text=message,
)
if include_invite:
y = c_height - 30
if show_message:
bui.textwidget(
parent=self._container,
position=(region_width * 0.57, 35),
parent=container,
position=(region_width * 0.5, y),
color=(0.6, 1.0, 0.6),
scale=msc_scale,
size=(0, 0),
maxwidth=region_width * 0.9,
max_height=message_height,
h_align='center',
v_align='top',
text=message,
)
y -= message_height
if show_message_extra:
y -= message_extra_height
if show_invite:
bui.textwidget(
parent=container,
position=(region_width * 0.57, y),
color=(0, 1, 0),
scale=0.6,
size=(0, 0),
@ -110,9 +145,9 @@ class AboutGatherTab(GatherTab):
subs=[('${COUNT}', str(try_tickets))],
),
)
bui.buttonwidget(
parent=self._container,
position=(region_width * 0.59, 10),
invite_button = bui.buttonwidget(
parent=container,
position=(region_width * 0.59, y - 25),
size=(230, 50),
color=(0.54, 0.42, 0.56),
textcolor=(0, 1, 0),
@ -124,7 +159,44 @@ class AboutGatherTab(GatherTab):
on_activate_call=bui.WeakCall(self._invite_to_try_press),
up_widget=tab_button,
)
return self._container
y -= invite_height
else:
invite_button = None
if show_discord:
bui.textwidget(
parent=container,
position=(region_width * 0.57, y),
color=(0.6, 0.6, 1),
scale=0.6,
size=(0, 0),
maxwidth=region_width * 0.5,
h_align='right',
v_align='center',
flatness=1.0,
text=bui.Lstr(resource='discordFriendsText'),
)
discord_button = bui.buttonwidget(
parent=container,
position=(region_width * 0.59, y - 25),
size=(230, 50),
color=(0.54, 0.42, 0.56),
textcolor=(0.6, 0.6, 1),
label=bui.Lstr(resource='discordJoinText'),
autoselect=True,
on_activate_call=bui.WeakCall(self._join_the_discord_press),
up_widget=(
invite_button if invite_button is not None else tab_button
),
)
y -= discord_height
else:
discord_button = None
if discord_button is not None:
pass
return scroll_widget
def _invite_to_try_press(self) -> None:
from bauiv1lib.account import show_sign_in_prompt
@ -137,3 +209,10 @@ class AboutGatherTab(GatherTab):
show_sign_in_prompt()
return
handle_app_invites_press()
def _join_the_discord_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.discord import DiscordWindow
assert bui.app.classic is not None
DiscordWindow().get_root_widget()

View file

@ -99,6 +99,7 @@ class ManualGatherTab(GatherTab):
self._party_edit_name_text: bui.Widget | None = None
self._party_edit_addr_text: bui.Widget | None = None
self._party_edit_port_text: bui.Widget | None = None
self._no_parties_added_text: bui.Widget | None = None
def on_activate(
self,
@ -142,6 +143,7 @@ class ManualGatherTab(GatherTab):
playsound=True,
),
text=bui.Lstr(resource='gatherWindow.manualJoinSectionText'),
glow_type='uniform',
)
self._favorites_text = bui.textwidget(
parent=self._container,
@ -162,6 +164,7 @@ class ManualGatherTab(GatherTab):
playsound=True,
),
text=bui.Lstr(resource='gatherWindow.favoritesText'),
glow_type='uniform',
)
bui.widget(edit=self._join_by_address_text, up_widget=tab_button)
bui.widget(
@ -316,7 +319,7 @@ class ManualGatherTab(GatherTab):
self._check_button = bui.textwidget(
parent=self._container,
size=(250, 60),
text=bui.Lstr(resource='gatherWindow.' 'showMyAddressText'),
text=bui.Lstr(resource='gatherWindow.showMyAddressText'),
v_align='center',
h_align='center',
click_activate=True,
@ -331,6 +334,7 @@ class ManualGatherTab(GatherTab):
self._container,
c_width,
),
glow_type='uniform',
)
bui.widget(edit=self._check_button, up_widget=btn)
@ -453,6 +457,24 @@ class ManualGatherTab(GatherTab):
claims_left_right=True,
)
self._no_parties_added_text = bui.textwidget(
parent=self._container,
size=(0, 0),
h_align='center',
v_align='center',
text='',
color=(0.6, 0.6, 0.6),
scale=1.2,
position=(
(
(190 if uiscale is bui.UIScale.SMALL else 225)
+ sub_scroll_width * 0.5
),
v + sub_scroll_height * 0.5,
),
glow_type='uniform',
)
self._favorite_selected = None
self._refresh_favorites()
@ -695,6 +717,12 @@ class ManualGatherTab(GatherTab):
assert self._favorites_scroll_width is not None
assert self._favorites_connect_button is not None
bui.textwidget(
edit=self._no_parties_added_text,
text='',
)
num_of_fav = 0
for i, server in enumerate(servers):
txt = bui.textwidget(
parent=self._columnwidget,
@ -718,11 +746,13 @@ class ManualGatherTab(GatherTab):
)
if i == 0:
bui.widget(edit=txt, up_widget=self._favorites_text)
self._favorite_selected = server
bui.widget(
edit=txt,
left_widget=self._favorites_connect_button,
right_widget=txt,
)
num_of_fav = num_of_fav + 1
# If there's no servers, allow selecting out of the scroll area
bui.containerwidget(
@ -735,6 +765,11 @@ class ManualGatherTab(GatherTab):
up_widget=self._favorites_text,
left_widget=self._favorites_connect_button,
)
if num_of_fav == 0:
bui.textwidget(
edit=self._no_parties_added_text,
text=bui.Lstr(resource='gatherWindow.noPartiesAddedText'),
)
def on_deactivate(self) -> None:
self._access_check_timer = None
@ -800,8 +835,17 @@ class ManualGatherTab(GatherTab):
}
config.commit()
bui.getsound('gunCocking').play()
bui.screenmessage(
bui.Lstr(
resource='addedToFavoritesText', subs=[('${NAME}', addr)]
),
color=(0, 1, 0),
)
else:
bui.screenmessage('Invalid Address', color=(1, 0, 0))
bui.screenmessage(
bui.Lstr(resource='internal.invalidAddressErrorText'),
color=(1, 0, 0),
)
bui.getsound('error').play()
def _host_lookup_result(

View file

@ -120,6 +120,7 @@ class PrivateGatherTab(GatherTab):
playsound=True,
),
text=bui.Lstr(resource='gatherWindow.privatePartyJoinText'),
glow_type='uniform',
)
self._host_sub_tab_text = bui.textwidget(
parent=self._container,
@ -138,6 +139,7 @@ class PrivateGatherTab(GatherTab):
playsound=True,
),
text=bui.Lstr(resource='gatherWindow.privatePartyHostText'),
glow_type='uniform',
)
bui.widget(edit=self._join_sub_tab_text, up_widget=tab_button)
bui.widget(
@ -458,9 +460,9 @@ class PrivateGatherTab(GatherTab):
scale=1.5,
size=(300, 50),
editable=True,
max_chars=20,
description=bui.Lstr(resource='gatherWindow.partyCodeText'),
autoselect=True,
maxwidth=250,
h_align='left',
v_align='center',
text='',
@ -962,7 +964,7 @@ class PrivateGatherTab(GatherTab):
code = cast(str, bui.textwidget(query=self._join_party_code_text))
if not code:
bui.screenmessage(
bui.Lstr(resource='internal.invalidAddressErrorText'),
bui.Lstr(translate=('serverResponses', 'Invalid code.')),
color=(1, 0, 0),
)
bui.getsound('error').play()

View file

@ -114,7 +114,7 @@ class UIRow:
self._name_widget = bui.textwidget(
text=bui.Lstr(value=party.name),
parent=columnwidget,
size=(sub_scroll_width * 0.63, 20),
size=(sub_scroll_width * 0.46, 20),
position=(0 + hpos, 4 + vpos),
selectable=True,
on_select_call=bui.WeakCall(
@ -248,6 +248,7 @@ class AddrFetchThread(Thread):
self._call = call
def run(self) -> None:
sock: socket.socket | None = None
try:
# FIXME: Update this to work with IPv6 at some point.
import socket
@ -255,7 +256,6 @@ class AddrFetchThread(Thread):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(('8.8.8.8', 80))
val = sock.getsockname()[0]
sock.close()
bui.pushcall(bui.Call(self._call, val), from_other_thread=True)
except Exception as exc:
from efro.error import is_udp_communication_error
@ -265,6 +265,9 @@ class AddrFetchThread(Thread):
pass
else:
logging.exception('Error in addr-fetch-thread')
finally:
if sock is not None:
sock.close()
class PingThread(Thread):
@ -361,6 +364,7 @@ class PublicGatherTab(GatherTab):
self._last_server_list_query_time: float | None = None
self._join_list_column: bui.Widget | None = None
self._join_status_text: bui.Widget | None = None
self._no_servers_found_text: bui.Widget | None = None
self._host_max_party_size_value: bui.Widget | None = None
self._host_max_party_size_minus_button: (bui.Widget | None) = None
self._host_max_party_size_plus_button: (bui.Widget | None) = None
@ -431,6 +435,7 @@ class PublicGatherTab(GatherTab):
text=bui.Lstr(
resource='gatherWindow.' 'joinPublicPartyDescriptionText'
),
glow_type='uniform',
)
self._host_text = bui.textwidget(
parent=self._container,
@ -453,6 +458,7 @@ class PublicGatherTab(GatherTab):
text=bui.Lstr(
resource='gatherWindow.' 'hostPublicPartyDescriptionText'
),
glow_type='uniform',
)
bui.widget(edit=self._join_text, up_widget=tab_button)
bui.widget(
@ -658,6 +664,18 @@ class PublicGatherTab(GatherTab):
color=(0.6, 0.6, 0.6),
position=(c_width * 0.5, c_height * 0.5),
)
self._no_servers_found_text = bui.textwidget(
parent=self._container,
text='',
size=(0, 0),
scale=0.9,
flatness=1.0,
shadow=0.0,
h_align='center',
v_align='top',
color=(0.6, 0.6, 0.6),
position=(c_width * 0.5, c_height * 0.5),
)
def _build_host_tab(
self, region_width: float, region_height: float
@ -950,6 +968,9 @@ class PublicGatherTab(GatherTab):
self._update_party_rows()
def _update_party_rows(self) -> None:
plus = bui.app.plus
assert plus is not None
columnwidget = self._join_list_column
if not columnwidget:
return
@ -963,6 +984,7 @@ class PublicGatherTab(GatherTab):
edit=self._host_scrollwidget,
claims_up_down=(len(self._parties_displayed) > 0),
)
bui.textwidget(edit=self._no_servers_found_text, text='')
# Clip if we have more UI rows than parties to show.
clipcount = len(self._ui_rows) - len(self._parties_displayed)
@ -972,6 +994,15 @@ class PublicGatherTab(GatherTab):
# If we have no parties to show, we're done.
if not self._parties_displayed:
text = self._join_status_text
if (
plus.get_v1_account_state() == 'signed_in'
and cast(str, bui.textwidget(query=text)) == ''
):
bui.textwidget(
edit=self._no_servers_found_text,
text=bui.Lstr(resource='noServersFoundText'),
)
return
sub_scroll_width = 830

View file

@ -334,7 +334,7 @@ class GetCurrencyWindow(bui.Window):
tex_scale=1.2,
) # 19.99-ish
self._enable_ad_button = bui.has_video_ads()
self._enable_ad_button = plus.has_video_ads()
h = self._width * 0.5 + 110.0
v = self._height - b_size[1] - 115.0
@ -561,7 +561,7 @@ class GetCurrencyWindow(bui.Window):
next_reward_ad_time
)
now = datetime.datetime.utcnow()
if bui.have_incentivized_ad() and (
if plus.have_incentivized_ad() and (
next_reward_ad_time is None or next_reward_ad_time <= now
):
self._ad_button_greyed = False
@ -732,8 +732,13 @@ class GetCurrencyWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.store import browser
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
if self._transitioning_out:
return
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
@ -745,7 +750,9 @@ class GetCurrencyWindow(bui.Window):
).get_root_widget()
if not self._from_modal_store:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(window)
bui.app.ui_v1.set_main_menu_window(
window, from_window=self._root_widget
)
self._transitioning_out = True

View file

@ -36,8 +36,8 @@ class HelpWindow(bui.Window):
self._main_menu = main_menu
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
width = 950 if uiscale is bui.UIScale.SMALL else 750
x_offs = 100 if uiscale is bui.UIScale.SMALL else 0
width = 1050 if uiscale is bui.UIScale.SMALL else 750
x_offs = 150 if uiscale is bui.UIScale.SMALL else 0
height = (
460
if uiscale is bui.UIScale.SMALL
@ -645,11 +645,16 @@ class HelpWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if self._main_menu:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
MainMenuWindow(transition='in_left').get_root_widget()
MainMenuWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

View file

@ -9,7 +9,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import babase
import bauiv1 as bui
if TYPE_CHECKING:
from typing import Iterable
@ -33,15 +33,15 @@ def split(chars: Iterable[str], maxlen: int) -> list[list[str]]:
def generate_emojis(maxlen: int) -> list[list[str]]:
"""Generates a lot of UTF8 emojis prepared for babase.Keyboard pages"""
"""Generates a lot of UTF8 emojis prepared for bui.Keyboard pages"""
all_emojis = split([chr(i) for i in range(0x1F601, 0x1F650)], maxlen)
all_emojis += split([chr(i) for i in range(0x2702, 0x27B1)], maxlen)
all_emojis += split([chr(i) for i in range(0x1F680, 0x1F6C1)], maxlen)
return all_emojis
# ba_meta export keyboard
class EnglishKeyboard(babase.Keyboard):
# ba_meta export bauiv1.Keyboard
class EnglishKeyboard(bui.Keyboard):
"""Default English keyboard."""
name = 'English'

View file

@ -21,7 +21,7 @@ class KioskWindow(bui.Window):
self._height = 340.0
def _do_cancel() -> None:
QuitWindow(swish=True, back=True)
QuitWindow(swish=True, quit_type=bui.QuitType.BACK)
super().__init__(
root_widget=bui.containerwidget(
@ -501,9 +501,15 @@ class KioskWindow(bui.Window):
def _do_full_menu(self) -> None:
from bauiv1lib.mainmenu import MainMenuWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
assert bui.app.classic is not None
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
bui.app.classic.did_menu_intro = True # prevent delayed transition-in
bui.app.ui_v1.set_main_menu_window(MainMenuWindow().get_root_widget())
bui.app.ui_v1.set_main_menu_window(
MainMenuWindow().get_root_widget(), from_window=self._root_widget
)

View file

@ -1142,6 +1142,10 @@ class LeagueRankWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.coop.browser import CoopBrowserWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@ -1149,5 +1153,6 @@ class LeagueRankWindow(bui.Window):
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
CoopBrowserWindow(transition='in_left').get_root_widget()
CoopBrowserWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

View file

@ -190,7 +190,6 @@ class MainMenuWindow(bui.Window):
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from bauiv1lib.confirm import QuitWindow
from bauiv1lib.store.button import StoreButton
plus = bui.app.plus
@ -312,8 +311,8 @@ class MainMenuWindow(bui.Window):
else self._confirm_end_game
),
)
# Assume we're in a client-session.
else:
# Assume we're in a client-session.
bui.buttonwidget(
parent=self._root_widget,
position=(h - self._button_width * 0.5 * scale, v),
@ -361,7 +360,6 @@ class MainMenuWindow(bui.Window):
tilt_scale=0.0,
draw_controller=store_button,
)
self._tdelay += self._t_delay_inc
else:
self._store_button = None
@ -422,7 +420,7 @@ class MainMenuWindow(bui.Window):
):
def _do_quit() -> None:
QuitWindow(swish=True, back=True)
bui.quit(confirm=True, quit_type=bui.QuitType.BACK)
bui.containerwidget(
edit=self._root_widget, on_cancel_call=_do_quit
@ -1040,30 +1038,47 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.confirm import QuitWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
# Note: Normally we should go through bui.quit(confirm=True) but
# invoking the window directly lets us scale it up from the
# button.
QuitWindow(origin_widget=self._quit_button)
def _demo_menu_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.kiosk import KioskWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
KioskWindow(transition='in_left').get_root_widget()
KioskWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
def _show_account_window(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.account.settings import AccountSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AccountSettingsWindow(
origin_widget=self._account_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _on_store_pressed(self) -> None:
@ -1071,6 +1086,10 @@ class MainMenuWindow(bui.Window):
from bauiv1lib.store.browser import StoreBrowserWindow
from bauiv1lib.account import show_sign_in_prompt
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
plus = bui.app.plus
assert plus is not None
@ -1083,7 +1102,8 @@ class MainMenuWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
StoreBrowserWindow(
origin_widget=self._store_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _is_benchmark(self) -> bool:
@ -1148,8 +1168,11 @@ class MainMenuWindow(bui.Window):
def _end_game(self) -> None:
assert bui.app.classic is not None
if not self._root_widget:
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_left')
bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False)
@ -1165,39 +1188,54 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.creditslist import CreditsListWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
CreditsListWindow(
origin_widget=self._credits_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _howtoplay(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.helpui import HelpWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
HelpWindow(
main_menu=True, origin_widget=self._how_to_play_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _settings(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.allsettings import AllSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AllSettingsWindow(
origin_widget=self._settings_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _resume_and_call(self, call: Callable[[], Any]) -> None:
@ -1206,10 +1244,12 @@ class MainMenuWindow(bui.Window):
def _do_game_service_press(self) -> None:
self._save_state()
if bui.app.classic is not None:
bui.app.classic.show_online_score_ui()
if bui.app.plus is not None:
bui.app.plus.show_game_service_ui()
else:
logging.warning('classic is required to show game service ui')
logging.warning(
'plus feature-set is required to show game service ui'
)
def _save_state(self) -> None:
# Don't do this for the in-game menu.
@ -1280,35 +1320,50 @@ class MainMenuWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.gather import GatherWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
GatherWindow(origin_widget=self._gather_button).get_root_widget()
GatherWindow(origin_widget=self._gather_button).get_root_widget(),
from_window=self._root_widget,
)
def _watch_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.watch import WatchWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
WatchWindow(origin_widget=self._watch_button).get_root_widget()
WatchWindow(origin_widget=self._watch_button).get_root_widget(),
from_window=self._root_widget,
)
def _play_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.selecting_private_party_playlist = False
bui.app.ui_v1.set_main_menu_window(
PlayWindow(origin_widget=self._start_button).get_root_widget()
PlayWindow(origin_widget=self._start_button).get_root_widget(),
from_window=self._root_widget,
)
def _resume(self) -> None:
@ -1316,7 +1371,7 @@ class MainMenuWindow(bui.Window):
bui.app.classic.resume()
if self._root_widget:
bui.containerwidget(edit=self._root_widget, transition='out_right')
bui.app.ui_v1.clear_main_menu_window()
bui.app.ui_v1.clear_main_menu_window(transition='out_right')
# If there's callbacks waiting for this window to go away, call them.
for call in bui.app.ui_v1.main_menu_resume_callbacks:

View file

@ -40,6 +40,7 @@ class PartyWindow(bui.Window):
if uiscale is bui.UIScale.MEDIUM
else 600
)
self._display_old_msgs = True
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
@ -92,9 +93,10 @@ class PartyWindow(bui.Window):
iconscale=1.2,
)
info = bs.get_connection_to_host_info()
if info.get('name', '') != '':
title = bui.Lstr(value=info['name'])
info = bs.get_connection_to_host_info_2()
if info is not None and info.name != '':
title = bui.Lstr(value=info.name)
else:
title = bui.Lstr(resource=self._r + '.titleText')
@ -142,12 +144,6 @@ class PartyWindow(bui.Window):
)
self._chat_texts: list[bui.Widget] = []
# add all existing messages if chat is not muted
if not bui.app.config.resolve('Chat Muted'):
msgs = bs.get_chat_messages()
for msg in msgs:
self._add_msg(msg)
self._text_field = txt = bui.textwidget(
parent=self._root_widget,
editable=True,
@ -233,6 +229,23 @@ class PartyWindow(bui.Window):
is_muted = bui.app.config.resolve('Chat Muted')
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
choices: list[str] = ['unmute' if is_muted else 'mute']
choices_display: list[bui.Lstr] = [
bui.Lstr(resource='chatUnMuteText' if is_muted else 'chatMuteText')
]
# Allow the 'Add to Favorites' option only if we're actually
# connected to a party and if it doesn't seem to be a private
# party (those are dynamically assigned addresses and ports so
# it makes no sense to save them).
server_info = bs.get_connection_to_host_info_2()
if server_info is not None and not server_info.name.startswith(
'Private Party '
):
choices.append('add_to_favorites')
choices_display.append(bui.Lstr(resource='addToFavoritesText'))
PopupMenuWindow(
position=self._menu_button.get_screen_space_center(),
scale=(
@ -242,12 +255,8 @@ class PartyWindow(bui.Window):
if uiscale is bui.UIScale.MEDIUM
else 1.23
),
choices=['unmute' if is_muted else 'mute'],
choices_display=[
bui.Lstr(
resource='chatUnMuteText' if is_muted else 'chatMuteText'
)
],
choices=choices,
choices_display=choices_display,
current_choice='unmute' if is_muted else 'mute',
delegate=self,
)
@ -269,6 +278,12 @@ class PartyWindow(bui.Window):
first.delete()
else:
bui.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0))
# add all existing messages if chat is not muted
if self._display_old_msgs:
msgs = bs.get_chat_messages()
for msg in msgs:
self._add_msg(msg)
self._display_old_msgs = False
# update roster section
roster = bs.get_game_roster()
@ -466,10 +481,75 @@ class PartyWindow(bui.Window):
cfg = bui.app.config
cfg['Chat Muted'] = choice == 'mute'
cfg.apply_and_commit()
self._display_old_msgs = True
self._update()
if choice == 'add_to_favorites':
info = bs.get_connection_to_host_info_2()
if info is not None:
self._add_to_favorites(
name=info.name,
address=info.address,
port_num=info.port,
)
else:
# We should not allow the user to see this option
# if they aren't in a server; this is our bad.
bui.screenmessage(
bui.Lstr(resource='errorText'), color=(1, 0, 0)
)
bui.getsound('error').play()
else:
print(f'unhandled popup type: {self._popup_type}')
def _add_to_favorites(
self, name: str, address: str | None, port_num: int | None
) -> None:
addr = address
if addr == '':
bui.screenmessage(
bui.Lstr(resource='internal.invalidAddressErrorText'),
color=(1, 0, 0),
)
bui.getsound('error').play()
return
port = port_num if port_num is not None else -1
if port > 65535 or port < 0:
bui.screenmessage(
bui.Lstr(resource='internal.invalidPortErrorText'),
color=(1, 0, 0),
)
bui.getsound('error').play()
return
# Avoid empty names.
if not name:
name = f'{addr}@{port}'
config = bui.app.config
if addr:
if not isinstance(config.get('Saved Servers'), dict):
config['Saved Servers'] = {}
config['Saved Servers'][f'{addr}@{port}'] = {
'addr': addr,
'port': port,
'name': name,
}
config.commit()
bui.getsound('gunCocking').play()
bui.screenmessage(
bui.Lstr(
resource='addedToFavoritesText', subs=[('${NAME}', name)]
),
color=(0, 1, 0),
)
else:
bui.screenmessage(
bui.Lstr(resource='internal.invalidAddressErrorText'),
color=(1, 0, 0),
)
bui.getsound('error').play()
def popup_menu_closing(self, popup_window: PopupWindow) -> None:
"""Called when the popup is closing."""
@ -481,7 +561,8 @@ class PartyWindow(bui.Window):
kick_str = bui.Lstr(resource='kickText')
else:
# kick-votes appeared in build 14248
if bs.get_connection_to_host_info().get('build_number', 0) < 14248:
info = bs.get_connection_to_host_info_2()
if info is None or info.build_number < 14248:
return
kick_str = bui.Lstr(resource='kickVoteText')
assert bui.app.classic is not None
@ -510,9 +591,17 @@ class PartyWindow(bui.Window):
def close(self) -> None:
"""Close the window."""
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_scale')
def close_with_sound(self) -> None:
"""Close the window and make a lovely sound."""
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.getsound('swish').play()
self.close()

View file

@ -20,7 +20,6 @@ class PartyQueueWindow(bui.Window):
"""Window showing players waiting to join a server."""
# Ewww this needs quite a bit of de-linting if/when i revisit it..
# pylint: disable=invalid-name
# pylint: disable=consider-using-dict-comprehension
class Dude:
"""Represents a single dude waiting in a server line."""

View file

@ -32,8 +32,8 @@ class PlayWindow(bui.Window):
self._is_main_menu = not bui.app.ui_v1.selecting_private_party_playlist
uiscale = bui.app.ui_v1.uiscale
width = 1000 if uiscale is bui.UIScale.SMALL else 800
x_offs = 100 if uiscale is bui.UIScale.SMALL else 0
width = 1100 if uiscale is bui.UIScale.SMALL else 800
x_offs = 150 if uiscale is bui.UIScale.SMALL else 0
height = 550
button_width = 400
@ -521,13 +521,19 @@ class PlayWindow(bui.Window):
def _back(self) -> None:
# pylint: disable=cyclic-import
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
if self._is_main_menu:
from bauiv1lib.mainmenu import MainMenuWindow
self._save_state()
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
MainMenuWindow(transition='in_left').get_root_widget()
MainMenuWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@ -538,7 +544,8 @@ class PlayWindow(bui.Window):
self._save_state()
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
GatherWindow(transition='in_left').get_root_widget()
GatherWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@ -549,6 +556,10 @@ class PlayWindow(bui.Window):
from bauiv1lib.account import show_sign_in_prompt
from bauiv1lib.coop.browser import CoopBrowserWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
plus = bui.app.plus
assert plus is not None
@ -559,26 +570,38 @@ class PlayWindow(bui.Window):
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
CoopBrowserWindow(origin_widget=self._coop_button).get_root_widget()
CoopBrowserWindow(
origin_widget=self._coop_button
).get_root_widget(),
from_window=self._root_widget,
)
def _team_tourney(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.playlist.browser import PlaylistBrowserWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PlaylistBrowserWindow(
origin_widget=self._teams_button, sessiontype=bs.DualTeamSession
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _free_for_all(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.playlist.browser import PlaylistBrowserWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
@ -586,7 +609,8 @@ class PlayWindow(bui.Window):
PlaylistBrowserWindow(
origin_widget=self._free_for_all_button,
sessiontype=bs.FreeForAllSession,
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _draw_dude(

View file

@ -198,7 +198,7 @@ class PlaylistAddGameWindow(bui.Window):
txt = bui.textwidget(
parent=self._column,
position=(0, 0),
size=(self._width - 88, 24),
size=(self._scroll_width * 1.1, 24),
text=gametype.get_display_string(),
h_align='left',
v_align='center',

View file

@ -62,8 +62,8 @@ class PlaylistBrowserWindow(bui.Window):
)
uiscale = bui.app.ui_v1.uiscale
self._width = 900.0 if uiscale is bui.UIScale.SMALL else 800.0
x_inset = 50 if uiscale is bui.UIScale.SMALL else 0
self._width = 1100.0 if uiscale is bui.UIScale.SMALL else 800.0
x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
self._height = (
480
if uiscale is bui.UIScale.SMALL
@ -684,6 +684,10 @@ class PlaylistBrowserWindow(bui.Window):
PlaylistCustomizeBrowserWindow,
)
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
@ -691,13 +695,18 @@ class PlaylistBrowserWindow(bui.Window):
PlaylistCustomizeBrowserWindow(
origin_widget=self._customize_button,
sessiontype=self._sessiontype,
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _on_back_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
# Store our selected playlist if that's changed.
if self._selected_playlist is not None:
prev_sel = bui.app.config.get(
@ -716,7 +725,8 @@ class PlaylistBrowserWindow(bui.Window):
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PlayWindow(transition='in_left').get_root_widget()
PlayWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
def _save_state(self) -> None:

View file

@ -47,8 +47,8 @@ class PlaylistCustomizeBrowserWindow(bui.Window):
self._r = 'gameListWindow'
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._width = 750.0 if uiscale is bui.UIScale.SMALL else 650.0
x_inset = 50.0 if uiscale is bui.UIScale.SMALL else 0.0
self._width = 850.0 if uiscale is bui.UIScale.SMALL else 650.0
x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
self._height = (
380.0
if uiscale is bui.UIScale.SMALL
@ -323,6 +323,10 @@ class PlaylistCustomizeBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.playlist import browser
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
if self._selected_playlist_name is not None:
cfg = bui.app.config
cfg[
@ -337,7 +341,8 @@ class PlaylistCustomizeBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
browser.PlaylistBrowserWindow(
transition='in_left', sessiontype=self._sessiontype
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _select(self, name: str, index: int) -> None:

View file

@ -31,8 +31,8 @@ class PlaylistEditWindow(bui.Window):
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._width = 770 if uiscale is bui.UIScale.SMALL else 670
x_inset = 50 if uiscale is bui.UIScale.SMALL else 0
self._width = 870 if uiscale is bui.UIScale.SMALL else 670
x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
self._height = (
400
if uiscale is bui.UIScale.SMALL
@ -283,6 +283,10 @@ class PlaylistEditWindow(bui.Window):
PlaylistCustomizeBrowserWindow,
)
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.getsound('powerdown01').play()
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
@ -293,7 +297,8 @@ class PlaylistEditWindow(bui.Window):
select_playlist=(
self._editcontroller.get_existing_playlist_name()
),
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _add(self) -> None:
@ -315,6 +320,10 @@ class PlaylistEditWindow(bui.Window):
PlaylistCustomizeBrowserWindow,
)
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
plus = bui.app.plus
assert plus is not None
@ -380,7 +389,8 @@ class PlaylistEditWindow(bui.Window):
transition='in_left',
sessiontype=self._editcontroller.get_session_type(),
select_playlist=new_name,
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _save_press_with_sound(self) -> None:

View file

@ -92,7 +92,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition=transition
).get_root_widget()
).get_root_widget(),
from_window=False, # Disable this check.
)
def get_config_name(self) -> str:
@ -150,7 +151,8 @@ class PlaylistEditController:
assert bui.app.classic is not None
bui.app.ui_v1.clear_main_menu_window(transition='out_left')
bui.app.ui_v1.set_main_menu_window(
PlaylistAddGameWindow(editcontroller=self).get_root_widget()
PlaylistAddGameWindow(editcontroller=self).get_root_widget(),
from_window=None,
)
def edit_game_pressed(self) -> None:
@ -175,7 +177,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
).get_root_widget()
).get_root_widget(),
from_window=None,
)
def _show_edit_ui(
@ -205,7 +208,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
).get_root_widget()
).get_root_widget(),
from_window=None,
)
# Otherwise we were adding; go back to the add type choice list.
@ -214,7 +218,8 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistAddGameWindow(
editcontroller=self, transition='in_left'
).get_root_widget()
).get_root_widget(),
from_window=None,
)
else:
# Make sure type is in there.
@ -236,5 +241,6 @@ class PlaylistEditController:
bui.app.ui_v1.set_main_menu_window(
PlaylistEditWindow(
editcontroller=self, transition='in_left'
).get_root_widget()
).get_root_widget(),
from_window=None,
)

View file

@ -103,8 +103,8 @@ class PlaylistEditGameWindow(bui.Window):
self._choice_selections: dict[str, int] = {}
uiscale = bui.app.ui_v1.uiscale
width = 720 if uiscale is bui.UIScale.SMALL else 620
x_inset = 50 if uiscale is bui.UIScale.SMALL else 0
width = 820 if uiscale is bui.UIScale.SMALL else 620
x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
height = (
365
if uiscale is bui.UIScale.SMALL
@ -514,6 +514,10 @@ class PlaylistEditGameWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
# Replace ourself with the map-select UI.
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
@ -524,7 +528,8 @@ class PlaylistEditGameWindow(bui.Window):
copy.deepcopy(self._getconfig()),
self._edit_info,
self._completion_call,
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _choice_inc(

View file

@ -44,8 +44,8 @@ class PlaylistMapSelectWindow(bui.Window):
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
width = 715 if uiscale is bui.UIScale.SMALL else 615
x_inset = 50 if uiscale is bui.UIScale.SMALL else 0
width = 815 if uiscale is bui.UIScale.SMALL else 615
x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
height = (
400
if uiscale is bui.UIScale.SMALL
@ -273,6 +273,10 @@ class PlaylistMapSelectWindow(bui.Window):
def _select(self, map_name: str) -> None:
from bauiv1lib.playlist.editgame import PlaylistEditGameWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._config['settings']['map'] = map_name
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
@ -285,7 +289,8 @@ class PlaylistMapSelectWindow(bui.Window):
default_selection='map',
transition='in_left',
edit_info=self._edit_info,
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _select_with_delay(self, map_name: str) -> None:
@ -296,6 +301,10 @@ class PlaylistMapSelectWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.playlist.editgame import PlaylistEditGameWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
@ -307,5 +316,6 @@ class PlaylistMapSelectWindow(bui.Window):
default_selection='map',
transition='in_left',
edit_info=self._edit_info,
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)

View file

@ -140,7 +140,6 @@ class PlayOptionsWindow(PopupWindow):
if show_shuffle_check_box:
self._height += 40
# Creates our _root_widget.
uiscale = bui.app.ui_v1.uiscale
scale = (
1.69
@ -149,6 +148,7 @@ class PlayOptionsWindow(PopupWindow):
if uiscale is bui.UIScale.MEDIUM
else 0.85
)
# Creates our _root_widget.
super().__init__(
position=scale_origin, size=(self._width, self._height), scale=scale
)
@ -448,6 +448,10 @@ class PlayOptionsWindow(PopupWindow):
self._transition_out()
def _on_ok_press(self) -> None:
# no-op if our underlying widget is dead or on its way out.
if not self.root_widget or self.root_widget.transitioning_out:
return
# Disallow if our playlist has disappeared.
if not self._does_target_playlist_exist():
return
@ -478,8 +482,12 @@ class PlayOptionsWindow(PopupWindow):
cfg['Private Party Host Session Type'] = typename
bui.getsound('gunCocking').play()
assert bui.app.classic is not None
# Note: this is a wonky situation where we aren't actually
# the main window but we set it on behalf of the main window
# that popped us up.
bui.app.ui_v1.set_main_menu_window(
GatherWindow(transition='in_right').get_root_widget()
GatherWindow(transition='in_right').get_root_widget(),
from_window=False, # Disable this test.
)
self._transition_out(transition='out_left')
if self._delegate is not None:

View file

@ -33,8 +33,8 @@ class ProfileBrowserWindow(bui.Window):
back_label = bui.Lstr(resource='doneText')
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._width = 700.0 if uiscale is bui.UIScale.SMALL else 600.0
x_inset = 50.0 if uiscale is bui.UIScale.SMALL else 0.0
self._width = 800.0 if uiscale is bui.UIScale.SMALL else 600.0
x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
self._height = (
360.0
if uiscale is bui.UIScale.SMALL
@ -197,8 +197,10 @@ class ProfileBrowserWindow(bui.Window):
bui.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
self._columnwidget = bui.columnwidget(
parent=self._scrollwidget, border=2, margin=0
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
size=(self._scroll_width, 32),
background=False,
)
v -= 255
self._profiles: dict[str, dict[str, Any]] | None = None
@ -212,6 +214,10 @@ class ProfileBrowserWindow(bui.Window):
from bauiv1lib.profile.edit import EditProfileWindow
from bauiv1lib.purchase import PurchaseWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
plus = bui.app.plus
assert plus is not None
@ -252,7 +258,8 @@ class ProfileBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
EditProfileWindow(
existing_profile=None, in_main_menu=self._in_main_menu
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget if self._in_main_menu else False,
)
def _delete_profile(self) -> None:
@ -301,6 +308,10 @@ class ProfileBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.profile.edit import EditProfileWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
if self._selected_profile is None:
bui.getsound('error').play()
bui.screenmessage(
@ -313,7 +324,8 @@ class ProfileBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
EditProfileWindow(
self._selected_profile, in_main_menu=self._in_main_menu
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget if self._in_main_menu else False,
)
def _select(self, name: str, index: int) -> None:
@ -324,6 +336,10 @@ class ProfileBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.account.settings import AccountSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
assert bui.app.classic is not None
self._save_state()
@ -333,7 +349,8 @@ class ProfileBrowserWindow(bui.Window):
if self._in_main_menu:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AccountSettingsWindow(transition='in_left').get_root_widget()
AccountSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
# If we're being called up standalone, handle pause/resume ourself.
@ -342,8 +359,10 @@ class ProfileBrowserWindow(bui.Window):
def _refresh(self) -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from efro.util import asserttype
from bascenev1 import PlayerProfilesChangedMessage
from bascenev1lib.actor import spazappearance
assert bui.app.classic is not None
@ -359,14 +378,27 @@ class ProfileBrowserWindow(bui.Window):
assert self._profiles is not None
items = list(self._profiles.items())
items.sort(key=lambda x: asserttype(x[0], str).lower())
spazzes = spazappearance.get_appearances()
spazzes.sort()
icon_textures = [
bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture)
for s in spazzes
]
icon_tint_textures = [
bui.gettexture(
bui.app.classic.spaz_appearances[s].icon_mask_texture
)
for s in spazzes
]
index = 0
y_val = 35 * (len(self._profiles) - 1)
account_name: str | None
if plus.get_v1_account_state() == 'signed_in':
account_name = plus.get_v1_account_display_string()
else:
account_name = None
widget_to_select = None
for p_name, _ in items:
for p_name, p_info in items:
if p_name == '__account__' and account_name is None:
continue
color, _highlight = bui.app.classic.get_player_profile_colors(
@ -378,16 +410,35 @@ class ProfileBrowserWindow(bui.Window):
if p_name == '__account__'
else bui.app.classic.get_player_profile_icon(p_name) + p_name
)
try:
char_index = spazzes.index(p_info['character'])
except Exception:
char_index = spazzes.index('Spaz')
assert isinstance(tval, str)
character = bui.buttonwidget(
parent=self._subcontainer,
position=(0, y_val),
size=(28, 28),
label='',
color=(1, 1, 1),
mask_texture=bui.gettexture('characterIconMask'),
tint_color=color,
tint2_color=_highlight,
texture=icon_textures[char_index],
tint_texture=icon_tint_textures[char_index],
selectable=False,
)
txtw = bui.textwidget(
parent=self._columnwidget,
position=(0, 32),
size=((self._width - 40) / scl, 28),
parent=self._subcontainer,
position=(35, y_val),
size=((self._width - 210) / scl, 28),
text=bui.Lstr(value=tval),
h_align='left',
v_align='center',
on_select_call=bui.WeakCall(self._select, p_name, index),
maxwidth=self._scroll_width * 0.92,
maxwidth=self._scroll_width * 0.86,
corner_scale=scl,
color=bui.safecolor(color, 0.4),
always_highlight=True,
@ -396,8 +447,11 @@ class ProfileBrowserWindow(bui.Window):
)
if index == 0:
bui.widget(edit=txtw, up_widget=self._back_button)
if self._selected_profile is None:
self._selected_profile = p_name
bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40)
self._profile_widgets.append(txtw)
self._profile_widgets.append(character)
# Select/show this one if it was previously selected
# (but defer till after this loop since our height is
@ -406,10 +460,15 @@ class ProfileBrowserWindow(bui.Window):
widget_to_select = txtw
index += 1
y_val -= 35
bui.containerwidget(
edit=self._subcontainer,
size=(self._scroll_width, index * 35),
)
if widget_to_select is not None:
bui.columnwidget(
edit=self._columnwidget,
bui.containerwidget(
edit=self._subcontainer,
selected_child=widget_to_select,
visible_child=widget_to_select,
)

View file

@ -18,12 +18,18 @@ class EditProfileWindow(bui.Window):
# FIXME: WILL NEED TO CHANGE THIS FOR UILOCATION.
def reload_window(self) -> None:
"""Transitions out and recreates ourself."""
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
EditProfileWindow(
self.getname(), self._in_main_menu
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def __init__(
@ -54,8 +60,8 @@ class EditProfileWindow(bui.Window):
self._highlight,
) = bui.app.classic.get_player_profile_colors(existing_profile)
uiscale = bui.app.ui_v1.uiscale
self._width = width = 780.0 if uiscale is bui.UIScale.SMALL else 680.0
self._x_inset = x_inset = 50.0 if uiscale is bui.UIScale.SMALL else 0.0
self._width = width = 880.0 if uiscale is bui.UIScale.SMALL else 680.0
self._x_inset = x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
self._height = height = (
350.0
if uiscale is bui.UIScale.SMALL
@ -184,7 +190,7 @@ class EditProfileWindow(bui.Window):
self._clipped_name_text = bui.textwidget(
parent=self._root_widget,
text='',
position=(540 + x_inset, v - 8),
position=(580 + x_inset, v - 8),
flatness=1.0,
shadow=0.0,
scale=0.55,
@ -390,6 +396,16 @@ class EditProfileWindow(bui.Window):
autoselect=True,
on_activate_call=self.upgrade_profile,
)
self._random_name_button = bui.buttonwidget(
parent=self._root_widget,
label=bui.Lstr(resource='randomText'),
size=(30, 20),
position=(495 + x_inset, v - 20),
button_type='square',
color=(0.6, 0.5, 0.65),
autoselect=True,
on_activate_call=self.assign_random_name,
)
self._update_clipped_name()
self._clipped_name_timer = bui.AppTimer(
@ -498,8 +514,17 @@ class EditProfileWindow(bui.Window):
)
self._update_character()
def assign_random_name(self) -> None:
"""Assigning a random name to the player."""
names = bs.get_random_names()
name = names[random.randrange(len(names))]
bui.textwidget(
edit=self._text_field,
text=name,
)
def upgrade_profile(self) -> None:
"""Attempt to ugrade the profile to global."""
"""Attempt to upgrade the profile to global."""
from bauiv1lib import account
from bauiv1lib.profile import upgrade as pupgrade
@ -653,6 +678,10 @@ class EditProfileWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.profile.browser import ProfileBrowserWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
@ -660,7 +689,8 @@ class EditProfileWindow(bui.Window):
'in_left',
selected_profile=self._existing_profile,
in_main_menu=self._in_main_menu,
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _set_color(self, color: tuple[float, float, float]) -> None:
@ -759,6 +789,10 @@ class EditProfileWindow(bui.Window):
"""Save has been selected."""
from bauiv1lib.profile.browser import ProfileBrowserWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return False
plus = bui.app.plus
assert plus is not None
@ -808,6 +842,7 @@ class EditProfileWindow(bui.Window):
'in_left',
selected_profile=new_name,
in_main_menu=self._in_main_menu,
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
return True

View file

@ -26,7 +26,7 @@ class PromoCodeWindow(bui.Window):
transition = 'in_right'
width = 450
height = 230
height = 330
self._modal = modal
self._r = 'promoCodeWindow'
@ -62,17 +62,50 @@ class PromoCodeWindow(bui.Window):
iconscale=1.2,
)
v = height - 74
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource='codesExplainText'),
maxwidth=width * 0.9,
position=(width * 0.5, v),
color=(0.7, 0.7, 0.7, 1.0),
size=(0, 0),
scale=0.8,
h_align='center',
v_align='center',
)
v -= 60
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(
resource='supportEmailText',
subs=[('${EMAIL}', 'support@froemling.net')],
),
maxwidth=width * 0.9,
position=(width * 0.5, v),
color=(0.7, 0.7, 0.7, 1.0),
size=(0, 0),
scale=0.65,
h_align='center',
v_align='center',
)
v -= 80
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource=self._r + '.codeText'),
position=(22, height - 113),
position=(22, v),
color=(0.8, 0.8, 0.8, 1.0),
size=(90, 30),
h_align='right',
)
v -= 8
self._text_field = bui.textwidget(
parent=self._root_widget,
position=(125, height - 121),
position=(125, v),
size=(280, 46),
text='',
h_align='left',
@ -86,10 +119,11 @@ class PromoCodeWindow(bui.Window):
)
bui.widget(edit=btn, down_widget=self._text_field)
v -= 79
b_width = 200
self._enter_button = btn2 = bui.buttonwidget(
parent=self._root_widget,
position=(width * 0.5 - b_width * 0.5, height - 200),
position=(width * 0.5 - b_width * 0.5, v),
size=(b_width, 60),
scale=1.0,
label=bui.Lstr(
@ -108,13 +142,18 @@ class PromoCodeWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AdvancedSettingsWindow(transition='in_left').get_root_widget()
AdvancedSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
def _activate_enter_button(self) -> None:
@ -124,6 +163,10 @@ class PromoCodeWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
plus = bui.app.plus
assert plus is not None
@ -133,7 +176,8 @@ class PromoCodeWindow(bui.Window):
if not self._modal:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AdvancedSettingsWindow(transition='in_left').get_root_widget()
AdvancedSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
plus.add_v1_account_transaction(
{

View file

@ -47,8 +47,8 @@ class AdvancedSettingsWindow(bui.Window):
scale_origin = None
uiscale = bui.app.ui_v1.uiscale
self._width = 870.0 if uiscale is bui.UIScale.SMALL else 670.0
x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
self._width = 970.0 if uiscale is bui.UIScale.SMALL else 670.0
x_inset = 150 if uiscale is bui.UIScale.SMALL else 0
self._height = (
390.0
if uiscale is bui.UIScale.SMALL
@ -682,11 +682,16 @@ class AdvancedSettingsWindow(bui.Window):
def _on_vr_test_press(self) -> None:
from bauiv1lib.settings.vrtesting import VRTestingWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
VRTestingWindow(transition='in_right').get_root_widget()
VRTestingWindow(transition='in_right').get_root_widget(),
from_window=self._root_widget,
)
def _on_net_test_press(self) -> None:
@ -694,6 +699,10 @@ class AdvancedSettingsWindow(bui.Window):
assert plus is not None
from bauiv1lib.settings.nettesting import NetTestingWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
# Net-testing requires a signed in v1 account.
if plus.get_v1_account_state() != 'signed_in':
bui.screenmessage(
@ -706,7 +715,8 @@ class AdvancedSettingsWindow(bui.Window):
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
NetTestingWindow(transition='in_right').get_root_widget()
NetTestingWindow(transition='in_right').get_root_widget(),
from_window=self._root_widget,
)
def _on_friend_promo_code_press(self) -> None:
@ -724,17 +734,26 @@ class AdvancedSettingsWindow(bui.Window):
def _on_plugins_button_press(self) -> None:
from bauiv1lib.settings.plugins import PluginWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PluginWindow(origin_widget=self._plugins_button).get_root_widget()
PluginWindow(origin_widget=self._plugins_button).get_root_widget(),
from_window=self._root_widget,
)
def _on_promo_code_press(self) -> None:
from bauiv1lib.promocode import PromoCodeWindow
from bauiv1lib.account import show_sign_in_prompt
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
plus = bui.app.plus
assert plus is not None
@ -742,23 +761,30 @@ class AdvancedSettingsWindow(bui.Window):
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PromoCodeWindow(
origin_widget=self._promo_code_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _on_benchmark_press(self) -> None:
from bauiv1lib.debug import DebugWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
DebugWindow(transition='in_right').get_root_widget()
DebugWindow(transition='in_right').get_root_widget(),
from_window=self._root_widget,
)
def _save_state(self) -> None:
@ -807,6 +833,8 @@ class AdvancedSettingsWindow(bui.Window):
sel_name = 'ModdingGuide'
elif sel == self._language_inform_checkbox:
sel_name = 'LangInform'
elif sel == self._show_dev_console_button_check_box.widget:
sel_name = 'ShowDevConsole'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
elif sel == self._back_button:
@ -870,6 +898,8 @@ class AdvancedSettingsWindow(bui.Window):
sel = self._modding_guide_button
elif sel_name == 'LangInform':
sel = self._language_inform_checkbox
elif sel_name == 'ShowDevConsole':
sel = self._show_dev_console_button_check_box.widget
else:
sel = None
if sel is not None:
@ -904,11 +934,16 @@ class AdvancedSettingsWindow(bui.Window):
def _do_back(self) -> None:
from bauiv1lib.settings.allsettings import AllSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AllSettingsWindow(transition='in_left').get_root_widget()
AllSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

View file

@ -40,8 +40,8 @@ class AllSettingsWindow(bui.Window):
scale_origin = None
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
width = 900 if uiscale is bui.UIScale.SMALL else 580
x_inset = 75 if uiscale is bui.UIScale.SMALL else 0
width = 1000 if uiscale is bui.UIScale.SMALL else 580
x_inset = 125 if uiscale is bui.UIScale.SMALL else 0
height = 435
self._r = 'settingsWindow'
top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
@ -235,65 +235,90 @@ class AllSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
MainMenuWindow(transition='in_left').get_root_widget()
MainMenuWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
def _do_controllers(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.controls import ControlsSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ControlsSettingsWindow(
origin_widget=self._controllers_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _do_graphics(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.graphics import GraphicsSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
GraphicsSettingsWindow(
origin_widget=self._graphics_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _do_audio(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.audio import AudioSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AudioSettingsWindow(
origin_widget=self._audio_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _do_advanced(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AdvancedSettingsWindow(
origin_widget=self._advanced_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _save_state(self) -> None:

View file

@ -121,7 +121,8 @@ class AudioSettingsWindow(bui.Window):
displayname=bui.Lstr(resource=self._r + '.soundVolumeText'),
minval=0.0,
maxval=1.0,
increment=0.1,
increment=0.05,
as_percent=True,
)
if bui.app.ui_v1.use_toolbars:
bui.widget(
@ -137,9 +138,10 @@ class AudioSettingsWindow(bui.Window):
displayname=bui.Lstr(resource=self._r + '.musicVolumeText'),
minval=0.0,
maxval=1.0,
increment=0.1,
increment=0.05,
callback=music.music_volume_changed,
changesound=False,
as_percent=True,
)
v -= 0.5 * spacing
@ -235,6 +237,10 @@ class AudioSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.soundtrack import browser as stb
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
# We require disk access for soundtracks;
# if we don't have it, request it.
if not bui.have_permission(bui.Permission.STORAGE):
@ -254,13 +260,18 @@ class AudioSettingsWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
stb.SoundtrackBrowserWindow(
origin_widget=self._soundtrack_button
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _back(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings import allsettings
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
@ -269,7 +280,8 @@ class AudioSettingsWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
allsettings.AllSettingsWindow(
transition='in_left'
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _save_state(self) -> None:

View file

@ -98,9 +98,11 @@ class ControlsSettingsWindow(bui.Window):
# made-for-iOS/Mac systems
# (we can run into problems where devices register as one of each
# type otherwise)..
# UPDATE: We always use the apple system these days (which should
# support older controllers). So no need for a switch.
show_mac_controller_subsystem = False
if platform == 'mac' and bui.is_xcode_build():
show_mac_controller_subsystem = True
# if platform == 'mac' and bui.is_xcode_build():
# show_mac_controller_subsystem = True
if show_mac_controller_subsystem:
height += spacing * 1.5
@ -311,6 +313,7 @@ class ControlsSettingsWindow(bui.Window):
maxwidth=width * 0.8,
)
v -= spacing
if show_mac_controller_subsystem:
PopupMenu(
parent=self._root_widget,
@ -364,59 +367,84 @@ class ControlsSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.keyboard import ConfigKeyboardWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ConfigKeyboardWindow(
bs.getinputdevice('Keyboard', '#1')
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _config_keyboard2(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.keyboard import ConfigKeyboardWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ConfigKeyboardWindow(
bs.getinputdevice('Keyboard', '#2')
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _do_mobile_devices(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.remoteapp import RemoteAppSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
RemoteAppSettingsWindow().get_root_widget()
RemoteAppSettingsWindow().get_root_widget(),
from_window=self._root_widget,
)
def _do_gamepads(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.gamepadselect import GamepadSelectWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
GamepadSelectWindow().get_root_widget()
GamepadSelectWindow().get_root_widget(),
from_window=self._root_widget,
)
def _do_touchscreen(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.touchscreen import TouchscreenSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
TouchscreenSettingsWindow().get_root_widget()
TouchscreenSettingsWindow().get_root_widget(),
from_window=self._root_widget,
)
def _save_state(self) -> None:
@ -463,11 +491,16 @@ class ControlsSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.allsettings import AllSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AllSettingsWindow(transition='in_left').get_root_widget()
AllSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

View file

@ -431,7 +431,9 @@ class GamepadSettingsWindow(bui.Window):
def get_unassigned_buttons_run_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
return self._settings.get('unassignedButtonsRun', True)
val = self._settings.get('unassignedButtonsRun', True)
assert isinstance(val, bool)
return val
def set_unassigned_buttons_run_value(self, value: bool) -> None:
"""(internal)"""
@ -446,7 +448,9 @@ class GamepadSettingsWindow(bui.Window):
def get_start_button_activates_default_widget_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
return self._settings.get('startButtonActivatesDefaultWidget', True)
val = self._settings.get('startButtonActivatesDefaultWidget', True)
assert isinstance(val, bool)
return val
def set_start_button_activates_default_widget_value(
self, value: bool
@ -463,7 +467,9 @@ class GamepadSettingsWindow(bui.Window):
def get_ui_only_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
return self._settings.get('uiOnly', False)
val = self._settings.get('uiOnly', False)
assert isinstance(val, bool)
return val
def set_ui_only_value(self, value: bool) -> None:
"""(internal)"""
@ -478,7 +484,9 @@ class GamepadSettingsWindow(bui.Window):
def get_ignore_completely_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
return self._settings.get('ignoreCompletely', False)
val = self._settings.get('ignoreCompletely', False)
assert isinstance(val, bool)
return val
def set_ignore_completely_value(self, value: bool) -> None:
"""(internal)"""
@ -493,7 +501,9 @@ class GamepadSettingsWindow(bui.Window):
def get_auto_recalibrate_analog_stick_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
return self._settings.get('autoRecalibrateAnalogStick', False)
val = self._settings.get('autoRecalibrateAnalogStick', False)
assert isinstance(val, bool)
return val
def set_auto_recalibrate_analog_stick_value(self, value: bool) -> None:
"""(internal)"""
@ -510,7 +520,9 @@ class GamepadSettingsWindow(bui.Window):
assert self._settings is not None
if not self._is_secondary:
raise RuntimeError('Enable value only applies to secondary editor.')
return self._settings.get('enableSecondary', False)
val = self._settings.get('enableSecondary', False)
assert isinstance(val, bool)
return val
def show_secondary_editor(self) -> None:
"""(internal)"""
@ -533,20 +545,24 @@ class GamepadSettingsWindow(bui.Window):
if 'analogStickLR' + self._ext in self._settings
else 5
if self._is_secondary
else 1
else None
)
sval2 = (
self._settings['analogStickUD' + self._ext]
if 'analogStickUD' + self._ext in self._settings
else 6
if self._is_secondary
else 2
)
return (
self._input.get_axis_name(sval1)
+ ' / '
+ self._input.get_axis_name(sval2)
else None
)
assert isinstance(sval1, (int, type(None)))
assert isinstance(sval2, (int, type(None)))
if sval1 is not None and sval2 is not None:
return (
self._input.get_axis_name(sval1)
+ ' / '
+ self._input.get_axis_name(sval2)
)
return bui.Lstr(resource=self._r + '.unsetText')
# If they're looking for triggers.
if control in ['triggerRun1' + self._ext, 'triggerRun2' + self._ext]:
@ -561,7 +577,7 @@ class GamepadSettingsWindow(bui.Window):
return str(1.0)
# For dpad buttons: show individual buttons if any are set.
# Otherwise show whichever dpad is set (defaulting to 1).
# Otherwise show whichever dpad is set.
dpad_buttons = [
'buttonLeft' + self._ext,
'buttonRight' + self._ext,
@ -576,24 +592,28 @@ class GamepadSettingsWindow(bui.Window):
return bui.Lstr(resource=self._r + '.unsetText')
# No dpad buttons - show the dpad number for all 4.
return bui.Lstr(
value='${A} ${B}',
subs=[
('${A}', bui.Lstr(resource=self._r + '.dpadText')),
(
'${B}',
str(
self._settings['dpad' + self._ext]
if 'dpad' + self._ext in self._settings
else 2
if self._is_secondary
else 1
),
),
],
dpadnum = (
self._settings['dpad' + self._ext]
if 'dpad' + self._ext in self._settings
else 2
if self._is_secondary
else None
)
assert isinstance(dpadnum, (int, type(None)))
if dpadnum is not None:
return bui.Lstr(
value='${A} ${B}',
subs=[
('${A}', bui.Lstr(resource=self._r + '.dpadText')),
(
'${B}',
str(dpadnum),
),
],
)
return bui.Lstr(resource=self._r + '.unsetText')
# other buttons..
# Other buttons.
if control in self._settings:
return self._input.get_button_name(self._settings[control])
return bui.Lstr(resource=self._r + '.unsetText')
@ -604,9 +624,7 @@ class GamepadSettingsWindow(bui.Window):
event: dict[str, Any],
dialog: AwaitGamepadInputWindow,
) -> None:
# pylint: disable=too-many-nested-blocks
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
assert self._settings is not None
ext = self._ext
@ -636,10 +654,6 @@ class GamepadSettingsWindow(bui.Window):
if btn in self._settings:
del self._settings[btn]
if event['hat'] == (2 if self._is_secondary else 1):
# Exclude value in default case.
if 'dpad' + ext in self._settings:
del self._settings['dpad' + ext]
else:
self._settings['dpad' + ext] = event['hat']
# Update the 4 dpad button txt widgets.
@ -668,10 +682,6 @@ class GamepadSettingsWindow(bui.Window):
if abs(event['value']) > 0.5:
axis = event['axis']
if axis == (5 if self._is_secondary else 1):
# Exclude value in default case.
if 'analogStickLR' + ext in self._settings:
del self._settings['analogStickLR' + ext]
else:
self._settings['analogStickLR' + ext] = axis
bui.textwidget(
edit=self._textwidgets['analogStickLR' + ext],
@ -701,10 +711,6 @@ class GamepadSettingsWindow(bui.Window):
lr_axis = 5 if self._is_secondary else 1
if axis != lr_axis:
if axis == (6 if self._is_secondary else 2):
# Exclude value in default case.
if 'analogStickUD' + ext in self._settings:
del self._settings['analogStickUD' + ext]
else:
self._settings['analogStickUD' + ext] = axis
bui.textwidget(
edit=self._textwidgets['analogStickLR' + ext],
@ -783,25 +789,34 @@ class GamepadSettingsWindow(bui.Window):
),
)
bui.apptimer(0, doit)
bui.pushcall(doit)
return btn
def _cancel(self) -> None:
from bauiv1lib.settings.controls import ControlsSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if self._is_main_menu:
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ControlsSettingsWindow(transition='in_left').get_root_widget()
ControlsSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
def _save(self) -> None:
classic = bui.app.classic
assert classic is not None
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
@ -846,7 +861,8 @@ class GamepadSettingsWindow(bui.Window):
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ControlsSettingsWindow(transition='in_left').get_root_widget()
ControlsSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

View file

@ -452,7 +452,7 @@ class GamepadAdvancedSettingsWindow(bui.Window):
),
)
bui.apptimer(0, doit)
bui.pushcall(doit)
return btn, btn2
def _inc(

View file

@ -29,16 +29,17 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
logging.exception('Error transitioning out main_menu_window.')
bui.getsound('activateBeep').play()
bui.getsound('swish').play()
inputdevice = event['input_device']
assert isinstance(inputdevice, bs.InputDevice)
if inputdevice.allows_configuring:
device = event['input_device']
assert isinstance(device, bs.InputDevice)
if device.allows_configuring:
bui.app.ui_v1.set_main_menu_window(
gamepad.GamepadSettingsWindow(inputdevice).get_root_widget()
gamepad.GamepadSettingsWindow(device).get_root_widget(),
from_window=None,
)
else:
width = 700
height = 200
button_width = 100
button_width = 80
uiscale = bui.app.ui_v1.uiscale
dlg = bui.containerwidget(
scale=(
@ -51,9 +52,14 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
size=(width, height),
transition='in_right',
)
bui.app.ui_v1.set_main_menu_window(dlg)
device_name = inputdevice.name
if device_name == 'iDevice':
bui.app.ui_v1.set_main_menu_window(dlg, from_window=None)
if device.allows_configuring_in_system_settings:
msg = bui.Lstr(
resource='configureDeviceInSystemSettingsText',
subs=[('${DEVICE}', device.name)],
)
elif device.is_controller_app:
msg = bui.Lstr(
resource='bsRemoteConfigureInAppText',
subs=[('${REMOTE_APP_NAME}', bui.get_remote_app_name())],
@ -61,7 +67,7 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
else:
msg = bui.Lstr(
resource='cantConfigureDeviceText',
subs=[('${DEVICE}', device_name)],
subs=[('${DEVICE}', device.name)],
)
bui.textwidget(
parent=dlg,
@ -76,12 +82,17 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
def _ok() -> None:
from bauiv1lib.settings import controls
# no-op if our underlying widget is dead or on its way out.
if not dlg or dlg.transitioning_out:
return
bui.containerwidget(edit=dlg, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
).get_root_widget()
).get_root_widget(),
from_window=dlg,
)
bui.buttonwidget(
@ -186,11 +197,16 @@ class GamepadSelectWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import controls
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bs.release_gamepad_input()
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)

View file

@ -52,7 +52,7 @@ class GraphicsSettingsWindow(bui.Window):
self._show_fullscreen = False
fullscreen_spacing_top = spacing * 0.2
fullscreen_spacing = spacing * 1.2
if bui.can_toggle_fullscreen():
if bui.fullscreen_control_available():
self._show_fullscreen = True
height += fullscreen_spacing + fullscreen_spacing_top
@ -122,21 +122,29 @@ class GraphicsSettingsWindow(bui.Window):
self._fullscreen_checkbox: bui.Widget | None = None
if self._show_fullscreen:
v -= fullscreen_spacing_top
self._fullscreen_checkbox = ConfigCheckBox(
# Fullscreen control does not necessarily talk to the
# app config so we have to wrangle it manually instead of
# using a config-checkbox.
label = bui.Lstr(resource=f'{self._r}.fullScreenText')
# Show keyboard shortcut alongside the control if they
# provide one.
shortcut = bui.fullscreen_control_key_shortcut()
if shortcut is not None:
label = bui.Lstr(
value='$(NAME) [$(SHORTCUT)]',
subs=[('$(NAME)', label), ('$(SHORTCUT)', shortcut)],
)
self._fullscreen_checkbox = bui.checkboxwidget(
parent=self._root_widget,
position=(100, v),
maxwidth=200,
value=bui.fullscreen_control_get(),
on_value_change_call=bui.fullscreen_control_set,
maxwidth=250,
size=(300, 30),
configkey='Fullscreen',
displayname=bui.Lstr(
resource=self._r
+ (
'.fullScreenCmdText'
if app.classic.platform == 'mac'
else '.fullScreenCtrlText'
)
),
).widget
text=label,
)
if not self._have_selected_child:
bui.containerwidget(
edit=self._root_widget,
@ -259,9 +267,7 @@ class GraphicsSettingsWindow(bui.Window):
bui.Lstr(resource='nativeText'),
]
for res in [1440, 1080, 960, 720, 480]:
# Nav bar is 72px so lets allow for that in what
# choices we show.
if native_res[1] >= res - 72:
if native_res[1] >= res:
res_str = f'{res}p'
choices.append(res_str)
choices_display.append(bui.Lstr(value=res_str))
@ -430,6 +436,10 @@ class GraphicsSettingsWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import allsettings
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
# Applying max-fps takes a few moments. Apply if it hasn't been
# yet.
self._apply_max_fps()
@ -441,7 +451,8 @@ class GraphicsSettingsWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
allsettings.AllSettingsWindow(
transition='in_left'
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _set_quality(self, quality: str) -> None:
@ -528,8 +539,10 @@ class GraphicsSettingsWindow(bui.Window):
and bui.apptime() - self._last_max_fps_set_time > 1.0
):
self._apply_max_fps()
if self._show_fullscreen:
# Keep the fullscreen checkbox up to date with the current value.
bui.checkboxwidget(
edit=self._fullscreen_checkbox,
value=bui.app.config.resolve('Fullscreen'),
value=bui.fullscreen_control_get(),
)

View file

@ -213,6 +213,12 @@ class ConfigKeyboardWindow(bui.Window):
scale=1.0,
)
def _pretty_button_name(self, button_name: str) -> bui.Lstr:
button_id = self._settings[button_name]
if button_id == -1:
return bs.Lstr(resource='configGamepadWindow.unsetText')
return self._input.get_button_name(button_id)
def _capture_button(
self,
pos: tuple[float, float],
@ -250,7 +256,7 @@ class ConfigKeyboardWindow(bui.Window):
v_align='top',
scale=uiscale,
maxwidth=maxwidth,
text=self._input.get_button_name(self._settings[button]),
text=self._pretty_button_name(button),
)
bui.buttonwidget(
edit=btn,
@ -265,15 +271,24 @@ class ConfigKeyboardWindow(bui.Window):
def _cancel(self) -> None:
from bauiv1lib.settings.controls import ControlsSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
ControlsSettingsWindow(transition='in_left').get_root_widget()
ControlsSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
def _save(self) -> None:
from bauiv1lib.settings.controls import ControlsSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
assert bui.app.classic is not None
bui.containerwidget(edit=self._root_widget, transition='out_right')
bui.getsound('gunCocking').play()
@ -308,7 +323,8 @@ class ConfigKeyboardWindow(bui.Window):
)
bui.app.config.apply_and_commit()
bui.app.ui_v1.set_main_menu_window(
ControlsSettingsWindow(transition='in_left').get_root_widget()
ControlsSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

View file

@ -135,8 +135,14 @@ class NetTestingWindow(bui.Window):
def _show_val_testing(self) -> None:
assert bui.app.classic is not None
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.app.ui_v1.set_main_menu_window(
NetValTestingWindow().get_root_widget()
NetValTestingWindow().get_root_widget(),
from_window=self._root_widget,
)
bui.containerwidget(edit=self._root_widget, transition='out_left')
@ -144,9 +150,14 @@ class NetTestingWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AdvancedSettingsWindow(transition='in_left').get_root_widget()
AdvancedSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
bui.containerwidget(edit=self._root_widget, transition='out_right')

View file

@ -110,11 +110,11 @@ class PluginWindow(bui.Window):
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 38),
position=(self._width * 0.5, self._height - 41),
size=(0, 0),
text=bui.Lstr(resource='pluginsText'),
color=app.ui_v1.title_color,
maxwidth=200,
maxwidth=170,
h_align='center',
v_align='center',
)
@ -129,6 +129,15 @@ class PluginWindow(bui.Window):
settings_button_x = 670 if uiscale is bui.UIScale.SMALL else 570
self._num_plugins_text = bui.textwidget(
parent=self._root_widget,
position=(settings_button_x - 130, self._height - 41),
size=(0, 0),
text='',
h_align='center',
v_align='center',
)
self._category_button = bui.buttonwidget(
parent=self._root_widget,
scale=0.7,
@ -174,6 +183,17 @@ class PluginWindow(bui.Window):
)
bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
self._no_plugins_installed_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.5),
size=(0, 0),
text='',
color=(0.6, 0.6, 0.6),
scale=0.8,
h_align='center',
v_align='center',
)
if bui.app.meta.scanresults is None:
bui.screenmessage(
'Still scanning plugins; please try again.', color=(1, 0, 0)
@ -212,11 +232,16 @@ class PluginWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.pluginsettings import PluginSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(edit=self._root_widget, transition='out_left')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PluginSettingsWindow(transition='in_right').get_root_widget()
PluginSettingsWindow(transition='in_right').get_root_widget(),
from_window=self._root_widget,
)
def _show_category_options(self) -> None:
@ -263,6 +288,7 @@ class PluginWindow(bui.Window):
def _show_plugins(self) -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
plugspecs = bui.app.plugins.plugin_specs
plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {})
assert isinstance(plugstates, dict)
@ -274,6 +300,11 @@ class PluginWindow(bui.Window):
plugspecs_sorted = sorted(plugspecs.items())
bui.textwidget(
edit=self._no_plugins_installed_text,
text='',
)
for _classpath, plugspec in plugspecs_sorted:
# counting number of enabled and disabled plugins
# plugstate = plugstates.setdefault(plugspec[0], {})
@ -372,6 +403,17 @@ class PluginWindow(bui.Window):
bui.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40)
num_shown += 1
bui.textwidget(
edit=self._num_plugins_text,
text=str(num_shown),
)
if num_shown == 0:
bui.textwidget(
edit=self._no_plugins_installed_text,
text=bui.Lstr(resource='noPluginsInstalledText'),
)
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
@ -412,11 +454,16 @@ class PluginWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
AdvancedSettingsWindow(transition='in_left').get_root_widget()
AdvancedSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

View file

@ -161,10 +161,15 @@ class PluginSettingsWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.plugins import PluginWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
PluginWindow(transition='in_left').get_root_widget()
PluginWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)

View file

@ -138,10 +138,15 @@ class RemoteAppSettingsWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import controls
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)

View file

@ -217,6 +217,10 @@ class TestingWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_right')
backwin = (
self._back_call()
@ -224,4 +228,6 @@ class TestingWindow(bui.Window):
else AdvancedSettingsWindow(transition='in_left')
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(backwin.get_root_widget())
bui.app.ui_v1.set_main_menu_window(
backwin.get_root_widget(), from_window=self._root_widget
)

View file

@ -276,11 +276,16 @@ class TouchscreenSettingsWindow(bui.Window):
def _back(self) -> None:
from bauiv1lib.settings import controls
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.containerwidget(edit=self._root_widget, transition='out_right')
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
bs.set_touchscreen_editing(False)

View file

@ -394,13 +394,18 @@ class SoundtrackBrowserWindow(bui.Window):
# pylint: disable=cyclic-import
from bauiv1lib.settings import audio
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self._save_state()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
assert bui.app.classic is not None
bui.app.ui_v1.set_main_menu_window(
audio.AudioSettingsWindow(transition='in_left').get_root_widget()
audio.AudioSettingsWindow(transition='in_left').get_root_widget(),
from_window=self._root_widget,
)
def _edit_soundtrack_with_sound(self) -> None:
@ -421,6 +426,10 @@ class SoundtrackBrowserWindow(bui.Window):
from bauiv1lib.purchase import PurchaseWindow
from bauiv1lib.soundtrack.edit import SoundtrackEditWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
if (
bui.app.classic is not None
and not bui.app.classic.accounts.have_pro_options()
@ -443,7 +452,8 @@ class SoundtrackBrowserWindow(bui.Window):
bui.app.ui_v1.set_main_menu_window(
SoundtrackEditWindow(
existing_soundtrack=self._selected_soundtrack
).get_root_widget()
).get_root_widget(),
from_window=self._root_widget,
)
def _get_soundtrack_display_name(self, soundtrack: str) -> bui.Lstr:

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