update from origin

This commit is contained in:
Ayush Saini 2023-09-30 17:21:33 +05:30
parent 8beb334d64
commit bf2f252ee5
91 changed files with 1839 additions and 1281 deletions

View file

@ -27,6 +27,7 @@ from _babase import (
apptime, apptime,
apptimer, apptimer,
AppTimer, AppTimer,
can_toggle_fullscreen,
charstr, charstr,
clipboard_get_text, clipboard_get_text,
clipboard_has_text, clipboard_has_text,
@ -39,6 +40,7 @@ from _babase import (
DisplayTimer, DisplayTimer,
do_once, do_once,
env, env,
Env,
fade_screen, fade_screen,
fatal_error, fatal_error,
get_display_resolution, get_display_resolution,
@ -48,8 +50,9 @@ from _babase import (
get_replays_dir, get_replays_dir,
get_string_height, get_string_height,
get_string_width, get_string_width,
get_v1_cloud_log_file_path,
getsimplesound, getsimplesound,
has_gamma_control, has_user_run_commands,
have_chars, have_chars,
have_permission, have_permission,
in_logic_thread, in_logic_thread,
@ -83,7 +86,12 @@ from _babase import (
set_thread_name, set_thread_name,
set_ui_input_device, set_ui_input_device,
show_progress_bar, show_progress_bar,
shutdown_suppress_begin,
shutdown_suppress_end,
shutdown_suppress_count,
SimpleSound, SimpleSound,
supports_max_fps,
supports_vsync,
unlock_all_input, unlock_all_input,
user_agent_string, user_agent_string,
Vec3, Vec3,
@ -96,12 +104,14 @@ from babase._appconfig import commit_app_config
from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec
from babase._appmode import AppMode from babase._appmode import AppMode
from babase._appsubsystem import AppSubsystem from babase._appsubsystem import AppSubsystem
from babase._appmodeselector import AppModeSelector
from babase._appconfig import AppConfig from babase._appconfig import AppConfig
from babase._apputils import ( from babase._apputils import (
handle_leftover_v1_cloud_log_file, handle_leftover_v1_cloud_log_file,
is_browser_likely_available, is_browser_likely_available,
garbage_collect, garbage_collect,
get_remote_app_name, get_remote_app_name,
AppHealthMonitor,
) )
from babase._cloud import CloudSubsystem from babase._cloud import CloudSubsystem
from babase._emptyappmode import EmptyAppMode from babase._emptyappmode import EmptyAppMode
@ -135,7 +145,6 @@ from babase._general import (
storagename, storagename,
getclass, getclass,
get_type_name, get_type_name,
json_prep,
) )
from babase._keyboard import Keyboard from babase._keyboard import Keyboard
from babase._language import Lstr, LanguageSubsystem from babase._language import Lstr, LanguageSubsystem
@ -153,6 +162,7 @@ from babase._math import normalized_color, is_point_in_box, vec3validate
from babase._meta import MetadataSubsystem from babase._meta import MetadataSubsystem
from babase._net import get_ip_address_type, DEFAULT_REQUEST_TIMEOUT_SECONDS from babase._net import get_ip_address_type, DEFAULT_REQUEST_TIMEOUT_SECONDS
from babase._plugin import PluginSpec, Plugin, PluginSubsystem from babase._plugin import PluginSpec, Plugin, PluginSubsystem
from babase._stringedit import StringEditAdapter, StringEditSubsystem
from babase._text import timestring from babase._text import timestring
_babase.app = app = App() _babase.app = app = App()
@ -169,12 +179,14 @@ __all__ = [
'app', 'app',
'App', 'App',
'AppConfig', 'AppConfig',
'AppHealthMonitor',
'AppIntent', 'AppIntent',
'AppIntentDefault', 'AppIntentDefault',
'AppIntentExec', 'AppIntentExec',
'AppMode', 'AppMode',
'appname', 'appname',
'appnameupper', 'appnameupper',
'AppModeSelector',
'AppSubsystem', 'AppSubsystem',
'apptime', 'apptime',
'AppTime', 'AppTime',
@ -182,6 +194,7 @@ __all__ = [
'apptimer', 'apptimer',
'AppTimer', 'AppTimer',
'Call', 'Call',
'can_toggle_fullscreen',
'charstr', 'charstr',
'clipboard_get_text', 'clipboard_get_text',
'clipboard_has_text', 'clipboard_has_text',
@ -200,6 +213,7 @@ __all__ = [
'do_once', 'do_once',
'EmptyAppMode', 'EmptyAppMode',
'env', 'env',
'Env',
'Existable', 'Existable',
'existing', 'existing',
'fade_screen', 'fade_screen',
@ -214,11 +228,12 @@ __all__ = [
'get_replays_dir', 'get_replays_dir',
'get_string_height', 'get_string_height',
'get_string_width', 'get_string_width',
'get_v1_cloud_log_file_path',
'get_type_name', 'get_type_name',
'getclass', 'getclass',
'getsimplesound', 'getsimplesound',
'handle_leftover_v1_cloud_log_file', 'handle_leftover_v1_cloud_log_file',
'has_gamma_control', 'has_user_run_commands',
'have_chars', 'have_chars',
'have_permission', 'have_permission',
'in_logic_thread', 'in_logic_thread',
@ -231,7 +246,6 @@ __all__ = [
'is_point_in_box', 'is_point_in_box',
'is_running_on_fire_tv', 'is_running_on_fire_tv',
'is_xcode_build', 'is_xcode_build',
'json_prep',
'Keyboard', 'Keyboard',
'LanguageSubsystem', 'LanguageSubsystem',
'lock_all_input', 'lock_all_input',
@ -277,9 +291,16 @@ __all__ = [
'set_thread_name', 'set_thread_name',
'set_ui_input_device', 'set_ui_input_device',
'show_progress_bar', 'show_progress_bar',
'shutdown_suppress_begin',
'shutdown_suppress_end',
'shutdown_suppress_count',
'SimpleSound', 'SimpleSound',
'SpecialChar', 'SpecialChar',
'storagename', 'storagename',
'StringEditAdapter',
'StringEditSubsystem',
'supports_max_fps',
'supports_vsync',
'TeamNotFoundError', 'TeamNotFoundError',
'timestring', 'timestring',
'UIScale', 'UIScale',

View file

@ -64,7 +64,7 @@ class AccountV2Subsystem:
def set_primary_credentials(self, credentials: str | None) -> None: def set_primary_credentials(self, credentials: str | None) -> None:
"""Set credentials for the primary app account.""" """Set credentials for the primary app account."""
raise RuntimeError('This should be overridden.') raise NotImplementedError('This should be overridden.')
def have_primary_credentials(self) -> bool: def have_primary_credentials(self) -> bool:
"""Are credentials currently set for the primary app account? """Are credentials currently set for the primary app account?
@ -73,7 +73,7 @@ class AccountV2Subsystem:
only that they exist. If/when credentials are validated, the 'primary' only that they exist. If/when credentials are validated, the 'primary'
account handle will be set. account handle will be set.
""" """
raise RuntimeError('This should be overridden.') raise NotImplementedError('This should be overridden.')
@property @property
def primary(self) -> AccountV2Handle | None: def primary(self) -> AccountV2Handle | None:
@ -128,7 +128,7 @@ class AccountV2Subsystem:
# Ok; no workspace to worry about; carry on. # Ok; no workspace to worry about; carry on.
if not self._initial_sign_in_completed: if not self._initial_sign_in_completed:
self._initial_sign_in_completed = True self._initial_sign_in_completed = True
_babase.app.on_initial_sign_in_completed() _babase.app.on_initial_sign_in_complete()
def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None: def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
"""Should be called when logins for the active account change.""" """Should be called when logins for the active account change."""
@ -163,7 +163,7 @@ class AccountV2Subsystem:
""" """
if not self._initial_sign_in_completed: if not self._initial_sign_in_completed:
self._initial_sign_in_completed = True self._initial_sign_in_completed = True
_babase.app.on_initial_sign_in_completed() _babase.app.on_initial_sign_in_complete()
@staticmethod @staticmethod
def _hashstr(val: str) -> str: def _hashstr(val: str) -> str:
@ -409,7 +409,7 @@ class AccountV2Subsystem:
def _on_set_active_workspace_completed(self) -> None: def _on_set_active_workspace_completed(self) -> None:
if not self._initial_sign_in_completed: if not self._initial_sign_in_completed:
self._initial_sign_in_completed = True self._initial_sign_in_completed = True
_babase.app.on_initial_sign_in_completed() _babase.app.on_initial_sign_in_complete()
class AccountV2Handle: class AccountV2Handle:

File diff suppressed because it is too large Load diff

View file

@ -50,7 +50,8 @@ class AppComponentSubsystem:
# Currently limiting this to logic-thread use; can revisit if # Currently limiting this to logic-thread use; can revisit if
# needed (would need to guard access to our implementations # needed (would need to guard access to our implementations
# dict). # dict).
assert _babase.in_logic_thread() if not _babase.in_logic_thread():
raise RuntimeError('this must be called from the logic thread.')
if not issubclass(implementation, baseclass): if not issubclass(implementation, baseclass):
raise TypeError( raise TypeError(
@ -73,7 +74,8 @@ class AppComponentSubsystem:
If no custom implementation has been set, the provided If no custom implementation has been set, the provided
base-class is returned. base-class is returned.
""" """
assert _babase.in_logic_thread() if not _babase.in_logic_thread():
raise RuntimeError('this must be called from the logic thread.')
del baseclass # Unused. del baseclass # Unused.
return cast(T, None) return cast(T, None)
@ -87,7 +89,9 @@ class AppComponentSubsystem:
loop. Note that any further setclass calls before the callback loop. Note that any further setclass calls before the callback
runs will not result in additional callbacks. runs will not result in additional callbacks.
""" """
assert _babase.in_logic_thread() if not _babase.in_logic_thread():
raise RuntimeError('this must be called from the logic thread.')
self._change_callbacks.setdefault(baseclass, []).append(callback) self._change_callbacks.setdefault(baseclass, []).append(callback)
def _run_change_callbacks(self) -> None: def _run_change_callbacks(self) -> None:

View file

@ -3,6 +3,7 @@
"""Provides the AppConfig class.""" """Provides the AppConfig class."""
from __future__ import annotations from __future__ import annotations
import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _babase import _babase
@ -109,7 +110,7 @@ def read_app_config() -> tuple[AppConfig, bool]:
# NOTE: it is assumed that this only gets called once and the # NOTE: it is assumed that this only gets called once and the
# config object will not change from here on out # config object will not change from here on out
config_file_path = _babase.app.config_file_path config_file_path = _babase.app.env.config_file_path
config_contents = '' config_contents = ''
try: try:
if os.path.exists(config_file_path): if os.path.exists(config_file_path):
@ -120,33 +121,24 @@ def read_app_config() -> tuple[AppConfig, bool]:
config = AppConfig() config = AppConfig()
config_file_healthy = True config_file_healthy = True
except Exception as exc: except Exception:
print( logging.exception(
( "Error reading config file at time %.3f: '%s'.",
'error reading config file at time ' _babase.apptime(),
+ str(_babase.apptime()) config_file_path,
+ ': \''
+ config_file_path
+ '\':\n'
),
exc,
) )
# Whenever this happens lets back up the broken one just in case it # Whenever this happens lets back up the broken one just in case it
# gets overwritten accidentally. # gets overwritten accidentally.
print( logging.info(
( "Backing up current config file to '%s.broken'", config_file_path
'backing up current config file to \''
+ config_file_path
+ ".broken\'"
)
) )
try: try:
import shutil import shutil
shutil.copyfile(config_file_path, config_file_path + '.broken') shutil.copyfile(config_file_path, config_file_path + '.broken')
except Exception as exc2: except Exception:
print('EXC copying broken config:', exc2) logging.exception('Error copying broken config.')
config = AppConfig() config = AppConfig()
# Now attempt to read one of our 'prev' backup copies. # Now attempt to read one of our 'prev' backup copies.
@ -159,9 +151,9 @@ def read_app_config() -> tuple[AppConfig, bool]:
else: else:
config = AppConfig() config = AppConfig()
config_file_healthy = True config_file_healthy = True
print('successfully read backup config.') logging.info('Successfully read backup config.')
except Exception as exc2: except Exception:
print('EXC reading prev backup config:', exc2) logging.exception('Error reading prev backup config.')
return config, config_file_healthy return config, config_file_healthy
@ -176,7 +168,7 @@ def commit_app_config(force: bool = False) -> None:
assert plus is not None assert plus is not None
if not _babase.app.config_file_healthy and not force: if not _babase.app.config_file_healthy and not force:
print( logging.warning(
'Current config file is broken; ' 'Current config file is broken; '
'skipping write to avoid losing settings.' 'skipping write to avoid losing settings.'
) )

View file

@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from bacommon.app import AppExperience
from babase._appintent import AppIntent from babase._appintent import AppIntent
@ -17,16 +18,33 @@ class AppMode:
""" """
@classmethod @classmethod
def supports_intent(cls, intent: AppIntent) -> bool: def get_app_experience(cls) -> AppExperience:
"""Return whether our mode can handle the provided intent.""" """Return the overall experience provided by this mode."""
del intent raise NotImplementedError('AppMode subclasses must override this.')
# Say no to everything by default. Let's make mode explicitly @classmethod
# lay out everything they *do* support. def can_handle_intent(cls, intent: AppIntent) -> bool:
return False """Return whether this mode can handle the provided intent.
For this to return True, the AppMode must claim to support the
provided intent (via its _supports_intent() method) AND the
AppExperience associated with the AppMode must be supported by
the current app and runtime environment.
"""
return cls._supports_intent(intent)
@classmethod
def _supports_intent(cls, intent: AppIntent) -> bool:
"""Return whether our mode can handle the provided intent.
AppModes should override this to define what they can handle.
Note that AppExperience does not have to be considered here; that
is handled automatically by the can_handle_intent() call."""
raise NotImplementedError('AppMode subclasses must override this.')
def handle_intent(self, intent: AppIntent) -> None: def handle_intent(self, intent: AppIntent) -> None:
"""Handle an intent.""" """Handle an intent."""
raise NotImplementedError('AppMode subclasses must override this.')
def on_activate(self) -> None: def on_activate(self) -> None:
"""Called when the mode is being activated.""" """Called when the mode is being activated."""

View file

@ -1,6 +1,6 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Provides AppMode functionality.""" """Contains AppModeSelector base class."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -18,15 +18,15 @@ class AppModeSelector:
The app calls an instance of this class when passed an AppIntent to The app calls an instance of this class when passed an AppIntent to
determine which AppMode to use to handle the intent. Plugins or determine which AppMode to use to handle the intent. Plugins or
spinoff projects can modify high level app behavior by replacing or spinoff projects can modify high level app behavior by replacing or
modifying this. modifying the app's mode-selector.
""" """
def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]: def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode] | None:
"""Given an AppIntent, return the AppMode that should handle it. """Given an AppIntent, return the AppMode that should handle it.
If None is returned, the AppIntent will be ignored. If None is returned, the AppIntent will be ignored.
This is called in a background thread, so avoid any calls This may be called in a background thread, so avoid any calls
limited to logic thread use/etc. limited to logic thread use/etc.
""" """
raise RuntimeError('app_mode_for_intent() should be overridden.') raise NotImplementedError('app_mode_for_intent() should be overridden.')

View file

@ -18,8 +18,8 @@ class AppSubsystem:
An app 'subsystem' is a bit of a vague term, as pieces of the app An app 'subsystem' is a bit of a vague term, as pieces of the app
can technically be any class and are not required to use this, but can technically be any class and are not required to use this, but
building one out of this base class provides some conveniences such building one out of this base class provides conveniences such as
as predefined callbacks during app state changes. predefined callbacks during app state changes.
Subsystems must be registered with the app before it completes its Subsystems must be registered with the app before it completes its
transition to the 'running' state. transition to the 'running' state.
@ -48,5 +48,8 @@ class AppSubsystem:
def on_app_shutdown(self) -> None: def on_app_shutdown(self) -> None:
"""Called when the app is shutting down.""" """Called when the app is shutting down."""
def on_app_shutdown_complete(self) -> None:
"""Called when the app is done shutting down."""
def do_apply_app_config(self) -> None: def do_apply_app_config(self) -> None:
"""Called when the app config should be applied.""" """Called when the app config should be applied."""

View file

@ -48,7 +48,7 @@ def is_browser_likely_available() -> bool:
# assume no browser. # assume no browser.
# FIXME: Might not be the case anymore; should make this definable # FIXME: Might not be the case anymore; should make this definable
# at the platform level. # at the platform level.
if app.vr_mode or (platform == 'android' and not hastouchscreen): if app.env.vr or (platform == 'android' and not hastouchscreen):
return False return False
# Anywhere else assume we've got one. # Anywhere else assume we've got one.
@ -103,8 +103,8 @@ def handle_v1_cloud_log() -> None:
info = { info = {
'log': _babase.get_v1_cloud_log(), 'log': _babase.get_v1_cloud_log(),
'version': app.version, 'version': app.env.version,
'build': app.build_number, 'build': app.env.build_number,
'userAgentString': classic.legacy_user_agent_string, 'userAgentString': classic.legacy_user_agent_string,
'session': sessionname, 'session': sessionname,
'activity': activityname, 'activity': activityname,
@ -222,8 +222,7 @@ def garbage_collect() -> None:
def print_corrupt_file_error() -> None: def print_corrupt_file_error() -> None:
"""Print an error if a corrupt file is found.""" """Print an error if a corrupt file is found."""
# FIXME - filter this out for builds without bauiv1. if _babase.app.env.gui:
if not _babase.app.headless_mode:
_babase.apptimer( _babase.apptimer(
2.0, 2.0,
lambda: _babase.screenmessage( lambda: _babase.screenmessage(
@ -279,7 +278,8 @@ def dump_app_state(
# the dump in that case. # the dump in that case.
try: try:
mdpath = os.path.join( mdpath = os.path.join(
os.path.dirname(_babase.app.config_file_path), '_appstate_dump_md' os.path.dirname(_babase.app.env.config_file_path),
'_appstate_dump_md',
) )
with open(mdpath, 'w', encoding='utf-8') as outfile: with open(mdpath, 'w', encoding='utf-8') as outfile:
outfile.write( outfile.write(
@ -297,7 +297,7 @@ def dump_app_state(
return return
tbpath = os.path.join( tbpath = os.path.join(
os.path.dirname(_babase.app.config_file_path), '_appstate_dump_tb' os.path.dirname(_babase.app.env.config_file_path), '_appstate_dump_tb'
) )
tbfile = open(tbpath, 'w', encoding='utf-8') tbfile = open(tbpath, 'w', encoding='utf-8')
@ -329,7 +329,8 @@ def log_dumped_app_state() -> None:
try: try:
out = '' out = ''
mdpath = os.path.join( mdpath = os.path.join(
os.path.dirname(_babase.app.config_file_path), '_appstate_dump_md' os.path.dirname(_babase.app.env.config_file_path),
'_appstate_dump_md',
) )
if os.path.exists(mdpath): if os.path.exists(mdpath):
# We may be hanging on to open file descriptors for use by # We may be hanging on to open file descriptors for use by
@ -354,7 +355,7 @@ def log_dumped_app_state() -> None:
f'Time: {metadata.app_time:.2f}' f'Time: {metadata.app_time:.2f}'
) )
tbpath = os.path.join( tbpath = os.path.join(
os.path.dirname(_babase.app.config_file_path), os.path.dirname(_babase.app.env.config_file_path),
'_appstate_dump_tb', '_appstate_dump_tb',
) )
if os.path.exists(tbpath): if os.path.exists(tbpath):
@ -378,6 +379,10 @@ class AppHealthMonitor(AppSubsystem):
self._response = False self._response = False
self._first_check = True self._first_check = True
def on_app_loading(self) -> None:
# If any traceback dumps happened last run, log and clear them.
log_dumped_app_state()
def _app_monitor_thread_main(self) -> None: def _app_monitor_thread_main(self) -> None:
try: try:
self._monitor_app() self._monitor_app()

View file

@ -5,6 +5,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from bacommon.app import AppExperience
import _babase import _babase
from babase._appmode import AppMode from babase._appmode import AppMode
from babase._appintent import AppIntentExec, AppIntentDefault from babase._appintent import AppIntentExec, AppIntentDefault
@ -17,7 +19,11 @@ class EmptyAppMode(AppMode):
"""An empty app mode that can be used as a fallback/etc.""" """An empty app mode that can be used as a fallback/etc."""
@classmethod @classmethod
def supports_intent(cls, intent: AppIntent) -> bool: def get_app_experience(cls) -> AppExperience:
return AppExperience.EMPTY
@classmethod
def _supports_intent(cls, intent: AppIntent) -> bool:
# We support default and exec intents currently. # We support default and exec intents currently.
return isinstance(intent, AppIntentExec | AppIntentDefault) return isinstance(intent, AppIntentExec | AppIntentDefault)
@ -30,8 +36,8 @@ class EmptyAppMode(AppMode):
def on_activate(self) -> None: def on_activate(self) -> None:
# Let the native layer do its thing. # Let the native layer do its thing.
_babase.empty_app_mode_activate() _babase.on_empty_app_mode_activate()
def on_deactivate(self) -> None: def on_deactivate(self) -> None:
# Let the native layer do its thing. # Let the native layer do its thing.
_babase.empty_app_mode_deactivate() _babase.on_empty_app_mode_deactivate()

View file

@ -6,6 +6,7 @@ from __future__ import annotations
import sys import sys
import signal import signal
import logging import logging
import warnings
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from efro.log import LogLevel from efro.log import LogLevel
@ -103,6 +104,12 @@ def on_main_thread_start_app() -> None:
signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling. signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling.
_babase.setup_sigint() _babase.setup_sigint()
# Turn on deprecation warnings. By default these are off for release
# builds except for in __main__. However this is a key way to
# communicate api changes to modders and most modders are running
# release builds so its good to have this on everywhere.
warnings.simplefilter('default', DeprecationWarning)
# Turn off fancy-pants cyclic garbage-collection. We run it only at # Turn off fancy-pants cyclic garbage-collection. We run it only at
# explicit times to avoid random hitches and keep things more # explicit times to avoid random hitches and keep things more
# deterministic. Non-reference-looped objects will still get cleaned # deterministic. Non-reference-looped objects will still get cleaned
@ -149,14 +156,15 @@ def on_main_thread_start_app() -> None:
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
def on_app_launching() -> None: def on_app_state_initing() -> None:
"""Called when the app reaches the launching state.""" """Called when the app reaches the initing state."""
import _babase import _babase
import baenv import baenv
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
# Let the user know if the app Python dir is a 'user' one. # Let the user know if the app Python dir is a 'user' one. This is a
# risky thing to be doing so don't let them forget they're doing it.
envconfig = baenv.get_config() envconfig = baenv.get_config()
if envconfig.is_user_app_python_dir: if envconfig.is_user_app_python_dir:
_babase.screenmessage( _babase.screenmessage(
@ -192,12 +200,13 @@ def _feed_logs_to_babase(log_handler: LogHandler) -> None:
# cache. This will feed the engine any logs that happened between # cache. This will feed the engine any logs that happened between
# baenv.configure() and now. # baenv.configure() and now.
# FIXME: while this works for now, the downside is that these # FIXME: while this setup works for now, the downside is that these
# callbacks fire in a bg thread so certain things like android # callbacks fire in a bg thread so certain things like android
# logging will be delayed compared to code that uses native logging # logging will be delayed relative to code that uses native logging
# calls directly. Perhaps we should add some sort of 'immediate' # calls directly. Ideally we should add some sort of 'immediate'
# callback option to better handle such cases (similar to the # callback option to better handle such cases (analogous to the
# immediate echofile stderr print that already occurs). # immediate echofile stderr print that LogHandler already
# supports).
log_handler.add_callback(_on_log, feed_existing_logs=True) log_handler.add_callback(_on_log, feed_existing_logs=True)

View file

@ -6,12 +6,13 @@ from __future__ import annotations
import types import types
import weakref import weakref
import random import random
import logging
import inspect import inspect
from typing import TYPE_CHECKING, TypeVar, Protocol, NewType from typing import TYPE_CHECKING, TypeVar, Protocol, NewType
from efro.terminal import Clr from efro.terminal import Clr
import _babase import _babase
from babase._error import print_error, print_exception
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any
@ -19,7 +20,8 @@ if TYPE_CHECKING:
# Declare distinct types for different time measurements we use so the # Declare distinct types for different time measurements we use so the
# type-checker can help prevent us from mixing and matching accidentally. # type-checker can help prevent us from mixing and matching accidentally,
# even if the *actual* types being used are the same.
# Our monotonic time measurement that starts at 0 when the app launches # Our monotonic time measurement that starts at 0 when the app launches
# and pauses while the app is suspended. # and pauses while the app is suspended.
@ -85,39 +87,6 @@ def getclass(name: str, subclassof: type[T]) -> type[T]:
return cls return cls
def json_prep(data: Any) -> Any:
"""Return a json-friendly version of the provided data.
This converts any tuples to lists and any bytes to strings
(interpreted as utf-8, ignoring errors). Logs errors (just once)
if any data is modified/discarded/unsupported.
"""
if isinstance(data, dict):
return dict(
(json_prep(key), json_prep(value))
for key, value in list(data.items())
)
if isinstance(data, list):
return [json_prep(element) for element in data]
if isinstance(data, tuple):
print_error('json_prep encountered tuple', once=True)
return [json_prep(element) for element in data]
if isinstance(data, bytes):
try:
return data.decode(errors='ignore')
except Exception:
from babase import _error
print_error('json_prep encountered utf-8 decode error', once=True)
return data.decode(errors='ignore')
if not isinstance(data, (str, float, bool, type(None), int)):
print_error(
'got unsupported type in json_prep:' + str(type(data)), once=True
)
return data
def utf8_all(data: Any) -> Any: def utf8_all(data: Any) -> Any:
"""Convert any unicode data in provided sequence(s) to utf8 bytes.""" """Convert any unicode data in provided sequence(s) to utf8 bytes."""
if isinstance(data, dict): if isinstance(data, dict):
@ -136,7 +105,7 @@ def utf8_all(data: Any) -> Any:
def get_type_name(cls: type) -> str: def get_type_name(cls: type) -> str:
"""Return a full type name including module for a class.""" """Return a full type name including module for a class."""
return cls.__module__ + '.' + cls.__name__ return f'{cls.__module__}.{cls.__name__}'
class _WeakCall: class _WeakCall:
@ -195,18 +164,12 @@ class _WeakCall:
else: else:
app = _babase.app app = _babase.app
if not self._did_invalid_call_warning: if not self._did_invalid_call_warning:
print( logging.warning(
(
'Warning: callable passed to babase.WeakCall() is not' 'Warning: callable passed to babase.WeakCall() is not'
' weak-referencable (' ' weak-referencable (%s); use babase.Call() instead'
+ str(args[0]) ' to avoid this warning.',
+ '); use babase.Call() instead to avoid this ' stack_info=True,
'warning. Stack-trace:'
) )
)
import traceback
traceback.print_stack()
self._did_invalid_call_warning = True self._did_invalid_call_warning = True
self._call = args[0] self._call = args[0]
self._args = args[1:] self._args = args[1:]
@ -320,7 +283,7 @@ def verify_object_death(obj: object) -> None:
try: try:
ref = weakref.ref(obj) ref = weakref.ref(obj)
except Exception: except Exception:
print_exception('Unable to create weak-ref in verify_object_death') logging.exception('Unable to create weak-ref in verify_object_death')
return return
# Use a slight range for our checks so they don't all land at once # Use a slight range for our checks so they don't all land at once

View file

@ -20,12 +20,7 @@ from typing import TYPE_CHECKING
import _babase import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
pass from babase._stringedit import StringEditAdapter
def on_app_bootstrapping_complete() -> None:
"""Called by C++ layer when bootstrapping finishes."""
_babase.app.on_app_bootstrapping_complete()
def reset_to_main_menu() -> None: def reset_to_main_menu() -> None:
@ -69,14 +64,6 @@ def open_url_with_webbrowser_module(url: str) -> None:
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
def connecting_to_party_message() -> None:
from babase._language import Lstr
_babase.screenmessage(
Lstr(resource='internal.connectingToPartyText'), color=(1, 1, 1)
)
def rejecting_invite_already_in_party_message() -> None: def rejecting_invite_already_in_party_message() -> None:
from babase._language import Lstr from babase._language import Lstr
@ -97,7 +84,7 @@ def connection_failed_message() -> None:
def temporarily_unavailable_message() -> None: def temporarily_unavailable_message() -> None:
from babase._language import Lstr from babase._language import Lstr
if not _babase.app.headless_mode: if _babase.app.env.gui:
_babase.getsimplesound('error').play() _babase.getsimplesound('error').play()
_babase.screenmessage( _babase.screenmessage(
Lstr(resource='getTicketsWindow.unavailableTemporarilyText'), Lstr(resource='getTicketsWindow.unavailableTemporarilyText'),
@ -108,7 +95,7 @@ def temporarily_unavailable_message() -> None:
def in_progress_message() -> None: def in_progress_message() -> None:
from babase._language import Lstr from babase._language import Lstr
if not _babase.app.headless_mode: if _babase.app.env.gui:
_babase.getsimplesound('error').play() _babase.getsimplesound('error').play()
_babase.screenmessage( _babase.screenmessage(
Lstr(resource='getTicketsWindow.inProgressText'), Lstr(resource='getTicketsWindow.inProgressText'),
@ -119,7 +106,7 @@ def in_progress_message() -> None:
def error_message() -> None: def error_message() -> None:
from babase._language import Lstr from babase._language import Lstr
if not _babase.app.headless_mode: if _babase.app.env.gui:
_babase.getsimplesound('error').play() _babase.getsimplesound('error').play()
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
@ -127,7 +114,7 @@ def error_message() -> None:
def purchase_not_valid_error() -> None: def purchase_not_valid_error() -> None:
from babase._language import Lstr from babase._language import Lstr
if not _babase.app.headless_mode: if _babase.app.env.gui:
_babase.getsimplesound('error').play() _babase.getsimplesound('error').play()
_babase.screenmessage( _babase.screenmessage(
Lstr( Lstr(
@ -141,7 +128,7 @@ def purchase_not_valid_error() -> None:
def purchase_already_in_progress_error() -> None: def purchase_already_in_progress_error() -> None:
from babase._language import Lstr from babase._language import Lstr
if not _babase.app.headless_mode: if _babase.app.env.gui:
_babase.getsimplesound('error').play() _babase.getsimplesound('error').play()
_babase.screenmessage( _babase.screenmessage(
Lstr(resource='store.purchaseAlreadyInProgressText'), Lstr(resource='store.purchaseAlreadyInProgressText'),
@ -172,14 +159,6 @@ def orientation_reset_message() -> None:
) )
def on_app_pause() -> None:
_babase.app.pause()
def on_app_resume() -> None:
_babase.app.resume()
def show_post_purchase_message() -> None: def show_post_purchase_message() -> None:
assert _babase.app.classic is not None assert _babase.app.classic is not None
_babase.app.classic.accounts.show_post_purchase_message() _babase.app.classic.accounts.show_post_purchase_message()
@ -208,7 +187,7 @@ def award_dual_wielding_achievement() -> None:
def play_gong_sound() -> None: def play_gong_sound() -> None:
if not _babase.app.headless_mode: if _babase.app.env.gui:
_babase.getsimplesound('gong').play() _babase.getsimplesound('gong').play()
@ -272,15 +251,11 @@ def toggle_fullscreen() -> None:
cfg.apply_and_commit() cfg.apply_and_commit()
def read_config() -> None:
_babase.app.read_config()
def ui_remote_press() -> None: def ui_remote_press() -> None:
"""Handle a press by a remote device that is only usable for nav.""" """Handle a press by a remote device that is only usable for nav."""
from babase._language import Lstr from babase._language import Lstr
if _babase.app.headless_mode: if _babase.app.env.headless:
return return
# Can be called without a context; need a context for getsound. # Can be called without a context; need a context for getsound.
@ -300,10 +275,6 @@ def do_quit() -> None:
_babase.quit() _babase.quit()
def shutdown() -> None:
_babase.app.on_app_shutdown()
def hash_strings(inputs: list[str]) -> str: def hash_strings(inputs: list[str]) -> str:
"""Hash provided strings into a short output string.""" """Hash provided strings into a short output string."""
import hashlib import hashlib
@ -375,11 +346,11 @@ def show_client_too_old_error() -> None:
# a newer build. # a newer build.
if ( if (
_babase.app.config.get('SuppressClientTooOldErrorForBuild') _babase.app.config.get('SuppressClientTooOldErrorForBuild')
== _babase.app.build_number == _babase.app.env.build_number
): ):
return return
if not _babase.app.headless_mode: if _babase.app.env.gui:
_babase.getsimplesound('error').play() _babase.getsimplesound('error').play()
_babase.screenmessage( _babase.screenmessage(
@ -393,3 +364,11 @@ def show_client_too_old_error() -> None:
), ),
color=(1, 0, 0), color=(1, 0, 0),
) )
def string_edit_adapter_can_be_replaced(adapter: StringEditAdapter) -> bool:
"""Return whether a StringEditAdapter can be replaced."""
from babase._stringedit import StringEditAdapter
assert isinstance(adapter, StringEditAdapter)
return adapter.can_be_replaced()

View file

@ -68,7 +68,10 @@ class LanguageSubsystem(AppSubsystem):
try: try:
names = os.listdir( names = os.listdir(
os.path.join( os.path.join(
_babase.app.data_directory, 'ba_data', 'data', 'languages' _babase.app.env.data_directory,
'ba_data',
'data',
'languages',
) )
) )
names = [n.replace('.json', '').capitalize() for n in names] names = [n.replace('.json', '').capitalize() for n in names]
@ -121,7 +124,7 @@ class LanguageSubsystem(AppSubsystem):
with open( with open(
os.path.join( os.path.join(
_babase.app.data_directory, _babase.app.env.data_directory,
'ba_data', 'ba_data',
'data', 'data',
'languages', 'languages',
@ -139,7 +142,7 @@ class LanguageSubsystem(AppSubsystem):
lmodvalues = None lmodvalues = None
else: else:
lmodfile = os.path.join( lmodfile = os.path.join(
_babase.app.data_directory, _babase.app.env.data_directory,
'ba_data', 'ba_data',
'data', 'data',
'languages', 'languages',

View file

@ -10,6 +10,7 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING, final from typing import TYPE_CHECKING, final
from bacommon.login import LoginType from bacommon.login import LoginType
import _babase import _babase
if TYPE_CHECKING: if TYPE_CHECKING:

View file

@ -18,11 +18,6 @@ import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable from typing import Callable
# The meta api version of this build of the game.
# Only packages and modules requiring this exact api version
# will be considered when scanning directories.
# See: https://ballistica.net/wiki/Meta-Tag-System
CURRENT_API_VERSION = 8
# Meta export lines can use these names to represent these classes. # Meta export lines can use these names to represent these classes.
# This is purely a convenience; it is possible to use full class paths # This is purely a convenience; it is possible to use full class paths
@ -76,14 +71,15 @@ class MetadataSubsystem:
""" """
assert self._scan_complete_cb is None assert self._scan_complete_cb is None
assert self._scan is None assert self._scan is None
env = _babase.app.env
self._scan_complete_cb = scan_complete_cb self._scan_complete_cb = scan_complete_cb
self._scan = DirectoryScan( self._scan = DirectoryScan(
[ [
path path
for path in [ for path in [
_babase.app.python_directory_app, env.python_directory_app,
_babase.app.python_directory_user, env.python_directory_user,
] ]
if path is not None if path is not None
] ]
@ -212,7 +208,7 @@ class MetadataSubsystem:
'${NUM}', '${NUM}',
str(len(results.incorrect_api_modules) - 1), str(len(results.incorrect_api_modules) - 1),
), ),
('${API}', str(CURRENT_API_VERSION)), ('${API}', str(_babase.app.env.api_version)),
], ],
) )
else: else:
@ -220,7 +216,7 @@ class MetadataSubsystem:
resource='scanScriptsSingleModuleNeedsUpdatesText', resource='scanScriptsSingleModuleNeedsUpdatesText',
subs=[ subs=[
('${PATH}', results.incorrect_api_modules[0]), ('${PATH}', results.incorrect_api_modules[0]),
('${API}', str(CURRENT_API_VERSION)), ('${API}', str(_babase.app.env.api_version)),
], ],
) )
_babase.screenmessage(msg, color=(1, 0, 0)) _babase.screenmessage(msg, color=(1, 0, 0))
@ -344,13 +340,16 @@ class DirectoryScan:
# If we find a module requiring a different api version, warn # If we find a module requiring a different api version, warn
# and ignore. # and ignore.
if required_api is not None and required_api != CURRENT_API_VERSION: if (
required_api is not None
and required_api != _babase.app.env.api_version
):
logging.warning( logging.warning(
'metascan: %s requires api %s but we are running' 'metascan: %s requires api %s but we are running'
' %s. Ignoring module.', ' %s. Ignoring module.',
subpath, subpath,
required_api, required_api,
CURRENT_API_VERSION, _babase.app.env.api_version,
) )
self.results.incorrect_api_modules.append( self.results.incorrect_api_modules.append(
self._module_name_for_subpath(subpath) self._module_name_for_subpath(subpath)

View file

@ -81,7 +81,7 @@ class PluginSubsystem(AppSubsystem):
config_changed = True config_changed = True
found_new = True found_new = True
# If we're *not* auto-enabling, just let the user know if we # If we're *not* auto-enabling, simply let the user know if we
# found new ones. # found new ones.
if found_new and not auto_enable_new_plugins: if found_new and not auto_enable_new_plugins:
_babase.screenmessage( _babase.screenmessage(
@ -131,10 +131,10 @@ class PluginSubsystem(AppSubsystem):
disappeared_plugs.add(class_path) disappeared_plugs.add(class_path)
continue continue
# If plugins disappeared, let the user know gently and remove them # If plugins disappeared, let the user know gently and remove
# from the config so we'll again let the user know if they later # them from the config so we'll again let the user know if they
# reappear. This makes it much smoother to switch between users # later reappear. This makes it much smoother to switch between
# or workspaces. # users or workspaces.
if disappeared_plugs: if disappeared_plugs:
_babase.getsimplesound('shieldDown').play() _babase.getsimplesound('shieldDown').play()
_babase.screenmessage( _babase.screenmessage(
@ -197,6 +197,17 @@ class PluginSubsystem(AppSubsystem):
_error.print_exception('Error in plugin on_app_shutdown()') _error.print_exception('Error in plugin on_app_shutdown()')
def on_app_shutdown_complete(self) -> None:
for plugin in self.active_plugins:
try:
plugin.on_app_shutdown_complete()
except Exception:
from babase import _error
_error.print_exception(
'Error in plugin on_app_shutdown_complete()'
)
def load_plugins(self) -> None: def load_plugins(self) -> None:
"""(internal)""" """(internal)"""
@ -217,7 +228,7 @@ class PluginSpec:
key. Remember to commit the app-config after making any changes. key. Remember to commit the app-config after making any changes.
The 'attempted_load' attr will be True if the engine has attempted The 'attempted_load' attr will be True if the engine has attempted
to load the plugin. If 'attempted_load' is True for a plugin-spec to load the plugin. If 'attempted_load' is True for a PluginSpec
but the 'plugin' attr is None, it means there was an error loading but the 'plugin' attr is None, it means there was an error loading
the plugin. If a plugin's api-version does not match the running the plugin. If a plugin's api-version does not match the running
app, if a new plugin is detected with auto-enable-plugins disabled, app, if a new plugin is detected with auto-enable-plugins disabled,
@ -249,7 +260,7 @@ class PluginSpec:
plugstate['enabled'] = val plugstate['enabled'] = val
def attempt_load_if_enabled(self) -> Plugin | None: def attempt_load_if_enabled(self) -> Plugin | None:
"""Possibly load the plugin and report errors.""" """Possibly load the plugin and log any errors."""
from babase._general import getclass from babase._general import getclass
from babase._language import Lstr from babase._language import Lstr
@ -308,8 +319,8 @@ class Plugin:
Category: **App Classes** Category: **App Classes**
Plugins are discoverable by the meta-tag system Plugins are discoverable by the meta-tag system
and the user can select which ones they want to activate. and the user can select which ones they want to enable.
Active plugins are then called at specific times as the Enabled plugins are then called at specific times as the
app is running in order to modify its behavior in some way. app is running in order to modify its behavior in some way.
""" """
@ -317,13 +328,16 @@ class Plugin:
"""Called when the app reaches the running state.""" """Called when the app reaches the running state."""
def on_app_pause(self) -> None: def on_app_pause(self) -> None:
"""Called after pausing game activity.""" """Called when the app is switching to a paused state."""
def on_app_resume(self) -> None: def on_app_resume(self) -> None:
"""Called after the game continues.""" """Called when the app is resuming from a paused state."""
def on_app_shutdown(self) -> None: def on_app_shutdown(self) -> None:
"""Called before closing the application.""" """Called when the app is beginning the shutdown process."""
def on_app_shutdown_complete(self) -> None:
"""Called when the app has completed the shutdown process."""
def has_settings_ui(self) -> bool: def has_settings_ui(self) -> bool:
"""Called to ask if we have settings UI we can show.""" """Called to ask if we have settings UI we can show."""

View file

@ -0,0 +1,146 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality for editing text strings.
This abstracts native edit dialogs as well as ones implemented via our
own ui toolkits.
"""
from __future__ import annotations
import time
import logging
import weakref
from typing import TYPE_CHECKING, final
from efro.util import empty_weakref
import _babase
if TYPE_CHECKING:
pass
class StringEditSubsystem:
"""Full string-edit state for the app."""
def __init__(self) -> None:
self.active_adapter = empty_weakref(StringEditAdapter)
class StringEditAdapter:
"""Represents a string editing operation on some object.
Editable objects such as text widgets or in-app-consoles can
subclass this to make their contents editable on all platforms.
There can only be one string-edit at a time for the app. New
StringEdits will attempt to register themselves as the globally
active one in their constructor, but this may not succeed. When
creating a StringEditAdapter, always check its 'is_valid()' value after
creating it. If this is False, it was not able to set itself as
the global active one and should be discarded.
"""
def __init__(
self,
description: str,
initial_text: str,
max_length: int | None,
screen_space_center: tuple[float, float] | None,
) -> None:
if not _babase.in_logic_thread():
raise RuntimeError('This must be called from the logic thread.')
self.create_time = time.monotonic()
# Note: these attr names are hard-coded in C++ code so don't
# change them willy-nilly.
self.description = description
self.initial_text = initial_text
self.max_length = max_length
self.screen_space_center = screen_space_center
# Attempt to register ourself as the active edit.
subsys = _babase.app.stringedit
current_edit = subsys.active_adapter()
if current_edit is None or current_edit.can_be_replaced():
subsys.active_adapter = weakref.ref(self)
@final
def can_be_replaced(self) -> bool:
"""Return whether this adapter can be replaced by a new one.
This is mainly a safeguard to allow adapters whose drivers have
gone away without calling apply or cancel to time out and be
replaced with new ones.
"""
if not _babase.in_logic_thread():
raise RuntimeError('This must be called from the logic thread.')
# Allow ourself to be replaced after a bit.
if time.monotonic() - self.create_time > 5.0:
if _babase.do_once():
logging.warning(
'StringEditAdapter can_be_replaced() check for %s'
' yielding True due to timeout; ideally this should'
' not be possible as the StringEditAdapter driver'
' should be blocking anything else from kicking off'
' new edits.',
self,
)
return True
# We also are always considered replaceable if we're not the
# active global adapter.
current_edit = _babase.app.stringedit.active_adapter()
if current_edit is not self:
return True
return False
@final
def apply(self, new_text: str) -> None:
"""Should be called by the owner when editing is complete.
Note that in some cases this call may be a no-op (such as if
this StringEditAdapter is no longer the globally active one).
"""
if not _babase.in_logic_thread():
raise RuntimeError('This must be called from the logic thread.')
# Make sure whoever is feeding this adapter is honoring max-length.
if self.max_length is not None and len(new_text) > self.max_length:
logging.warning(
'apply() on %s was passed a string of length %d,'
' but adapter max_length is %d; this should not happen'
' (will truncate).',
self,
len(new_text),
self.max_length,
stack_info=True,
)
new_text = new_text[: self.max_length]
self._do_apply(new_text)
@final
def cancel(self) -> None:
"""Should be called by the owner when editing is cancelled."""
if not _babase.in_logic_thread():
raise RuntimeError('This must be called from the logic thread.')
self._do_cancel()
def _do_apply(self, new_text: str) -> None:
"""Should be overridden by subclasses to handle apply.
Will always be called in the logic thread.
"""
raise NotImplementedError('Subclasses must override this.')
def _do_cancel(self) -> None:
"""Should be overridden by subclasses to handle cancel.
Will always be called in the logic thread.
"""
raise NotImplementedError('Subclasses must override this.')

32
dist/ba_data/python/babase/_ui.py vendored Normal file
View file

@ -0,0 +1,32 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI related bits of babase."""
from __future__ import annotations
from typing import TYPE_CHECKING
from babase._stringedit import StringEditAdapter
import _babase
if TYPE_CHECKING:
pass
class DevConsoleStringEditAdapter(StringEditAdapter):
"""Allows editing dev-console text."""
def __init__(self) -> None:
description = 'Dev Console Input'
initial_text = _babase.get_dev_console_input_text()
max_length = None
screen_space_center = None
super().__init__(
description, initial_text, max_length, screen_space_center
)
def _do_apply(self, new_text: str) -> None:
_babase.set_dev_console_input_text(new_text)
_babase.dev_console_input_adapter_finish()
def _do_cancel(self) -> None:
_babase.dev_console_input_adapter_finish()

View file

@ -18,7 +18,7 @@ def get_human_readable_user_scripts_path() -> str:
This is NOT a valid filesystem path; may be something like "(SD Card)". This is NOT a valid filesystem path; may be something like "(SD Card)".
""" """
app = _babase.app app = _babase.app
path: str | None = app.python_directory_user path: str | None = app.env.python_directory_user
if path is None: if path is None:
return '<Not Available>' return '<Not Available>'
@ -66,19 +66,20 @@ def _request_storage_permission() -> bool:
def show_user_scripts() -> None: def show_user_scripts() -> None:
"""Open or nicely print the location of the user-scripts directory.""" """Open or nicely print the location of the user-scripts directory."""
app = _babase.app app = _babase.app
env = app.env
# First off, if we need permission for this, ask for it. # First off, if we need permission for this, ask for it.
if _request_storage_permission(): if _request_storage_permission():
return return
# If we're running in a nonstandard environment its possible this is unset. # If we're running in a nonstandard environment its possible this is unset.
if app.python_directory_user is None: if env.python_directory_user is None:
_babase.screenmessage('<unset>') _babase.screenmessage('<unset>')
return return
# Secondly, if the dir doesn't exist, attempt to make it. # Secondly, if the dir doesn't exist, attempt to make it.
if not os.path.exists(app.python_directory_user): if not os.path.exists(env.python_directory_user):
os.makedirs(app.python_directory_user) os.makedirs(env.python_directory_user)
# On android, attempt to write a file in their user-scripts dir telling # On android, attempt to write a file in their user-scripts dir telling
# them about modding. This also has the side-effect of allowing us to # them about modding. This also has the side-effect of allowing us to
@ -88,7 +89,7 @@ def show_user_scripts() -> None:
# they can see it. # they can see it.
if app.classic is not None and app.classic.platform == 'android': if app.classic is not None and app.classic.platform == 'android':
try: try:
usd: str | None = app.python_directory_user usd: str | None = env.python_directory_user
if usd is not None and os.path.isdir(usd): if usd is not None and os.path.isdir(usd):
file_name = usd + '/about_this_folder.txt' file_name = usd + '/about_this_folder.txt'
with open(file_name, 'w', encoding='utf-8') as outfile: with open(file_name, 'w', encoding='utf-8') as outfile:
@ -105,7 +106,7 @@ def show_user_scripts() -> None:
# On a few platforms we try to open the dir in the UI. # 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']: if app.classic is not None and app.classic.platform in ['mac', 'windows']:
_babase.open_dir_externally(app.python_directory_user) _babase.open_dir_externally(env.python_directory_user)
# Otherwise we just print a pretty version of it. # Otherwise we just print a pretty version of it.
else: else:
@ -120,18 +121,19 @@ def create_user_system_scripts() -> None:
import shutil import shutil
app = _babase.app app = _babase.app
env = app.env
# First off, if we need permission for this, ask for it. # First off, if we need permission for this, ask for it.
if _request_storage_permission(): if _request_storage_permission():
return return
# Its possible these are unset in non-standard environments. # Its possible these are unset in non-standard environments.
if app.python_directory_user is None: if env.python_directory_user is None:
raise RuntimeError('user python dir unset') raise RuntimeError('user python dir unset')
if app.python_directory_app is None: if env.python_directory_app is None:
raise RuntimeError('app python dir unset') raise RuntimeError('app python dir unset')
path = app.python_directory_user + '/sys/' + app.version path = f'{env.python_directory_user}/sys/{env.version}'
pathtmp = path + '_tmp' pathtmp = path + '_tmp'
if os.path.exists(path): if os.path.exists(path):
shutil.rmtree(path) shutil.rmtree(path)
@ -147,8 +149,8 @@ def create_user_system_scripts() -> None:
# /Knowledge-Nuggets#python-cache-files-gotcha # /Knowledge-Nuggets#python-cache-files-gotcha
return ('__pycache__',) return ('__pycache__',)
print(f'COPYING "{app.python_directory_app}" -> "{pathtmp}".') print(f'COPYING "{env.python_directory_app}" -> "{pathtmp}".')
shutil.copytree(app.python_directory_app, pathtmp, ignore=_ignore_filter) shutil.copytree(env.python_directory_app, pathtmp, ignore=_ignore_filter)
print(f'MOVING "{pathtmp}" -> "{path}".') print(f'MOVING "{pathtmp}" -> "{path}".')
shutil.move(pathtmp, path) shutil.move(pathtmp, path)
@ -168,12 +170,12 @@ def delete_user_system_scripts() -> None:
"""Clean out the scripts created by create_user_system_scripts().""" """Clean out the scripts created by create_user_system_scripts()."""
import shutil import shutil
app = _babase.app env = _babase.app.env
if app.python_directory_user is None: if env.python_directory_user is None:
raise RuntimeError('user python dir unset') raise RuntimeError('user python dir unset')
path = app.python_directory_user + '/sys/' + app.version path = f'{env.python_directory_user}/sys/{env.version}'
if os.path.exists(path): if os.path.exists(path):
shutil.rmtree(path) shutil.rmtree(path)
print( print(
@ -185,6 +187,6 @@ def delete_user_system_scripts() -> None:
print(f"User system scripts not found at '{path}'.") print(f"User system scripts not found at '{path}'.")
# If the sys path is empty, kill it. # If the sys path is empty, kill it.
dpath = app.python_directory_user + '/sys' dpath = env.python_directory_user + '/sys'
if os.path.isdir(dpath) and not os.listdir(dpath): if os.path.isdir(dpath) and not os.listdir(dpath):
os.rmdir(dpath) os.rmdir(dpath)

View file

@ -42,7 +42,7 @@ class AccountV1Subsystem:
if babase.app.plus is None: if babase.app.plus is None:
return return
if ( if (
babase.app.headless_mode babase.app.env.headless
or babase.app.config.get('Auto Account State') == 'Local' or babase.app.config.get('Auto Account State') == 'Local'
): ):
babase.app.plus.sign_in_v1('Local') babase.app.plus.sign_in_v1('Local')

View file

@ -31,27 +31,6 @@ def get_input_device_mapped_value(
subplatform = app.classic.subplatform subplatform = app.classic.subplatform
appconfig = babase.app.config appconfig = babase.app.config
# iiRcade: hard-code for a/b/c/x for now...
if babase.app.iircade_mode:
return {
'triggerRun2': 19,
'unassignedButtonsRun': False,
'buttonPickUp': 100,
'buttonBomb': 98,
'buttonJump': 97,
'buttonStart': 83,
'buttonStart2': 109,
'buttonPunch': 99,
'buttonRun2': 102,
'buttonRun1': 101,
'triggerRun1': 18,
'buttonLeft': 22,
'buttonRight': 23,
'buttonUp': 20,
'buttonDown': 21,
'buttonVRReorient': 110,
}.get(name, -1)
# If there's an entry in our config for this controller, use it. # If there's an entry in our config for this controller, use it.
if 'Controllers' in appconfig: if 'Controllers' in appconfig:
ccfgs = appconfig['Controllers'] ccfgs = appconfig['Controllers']

View file

@ -4,13 +4,12 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
import threading
import weakref import weakref
import threading
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import babase import babase
from babase import DEFAULT_REQUEST_TIMEOUT_SECONDS
import bascenev1 import bascenev1
if TYPE_CHECKING: if TYPE_CHECKING:
@ -36,7 +35,9 @@ class MasterServerV1CallThread(threading.Thread):
callback: MasterServerCallback | None, callback: MasterServerCallback | None,
response_type: MasterServerResponseType, response_type: MasterServerResponseType,
): ):
super().__init__() # Set daemon=True so long-running requests don't keep us from
# quitting the app.
super().__init__(daemon=True)
self._request = request self._request = request
self._request_type = request_type self._request_type = request_type
if not isinstance(response_type, MasterServerResponseType): if not isinstance(response_type, MasterServerResponseType):
@ -69,6 +70,7 @@ class MasterServerV1CallThread(threading.Thread):
def run(self) -> None: def run(self) -> None:
# pylint: disable=consider-using-with # pylint: disable=consider-using-with
# pylint: disable=too-many-branches
import urllib.request import urllib.request
import urllib.parse import urllib.parse
import urllib.error import urllib.error
@ -80,6 +82,13 @@ class MasterServerV1CallThread(threading.Thread):
assert plus is not None assert plus is not None
response_data: Any = None response_data: Any = None
url: str | None = None url: str | None = None
# Tearing the app down while this is running can lead to
# rare crashes in LibSSL, so avoid that if at all possible.
if not babase.shutdown_suppress_begin():
# App is already shutting down, so we're a no-op.
return
try: try:
classic = babase.app.classic classic = babase.app.classic
assert classic is not None assert classic is not None
@ -101,7 +110,7 @@ class MasterServerV1CallThread(threading.Thread):
{'User-Agent': classic.legacy_user_agent_string}, {'User-Agent': classic.legacy_user_agent_string},
), ),
context=babase.app.net.sslcontext, context=babase.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS, timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
) )
elif self._request_type == 'post': elif self._request_type == 'post':
url = plus.get_master_server_address() + '/' + self._request url = plus.get_master_server_address() + '/' + self._request
@ -113,7 +122,7 @@ class MasterServerV1CallThread(threading.Thread):
{'User-Agent': classic.legacy_user_agent_string}, {'User-Agent': classic.legacy_user_agent_string},
), ),
context=babase.app.net.sslcontext, context=babase.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS, timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
) )
else: else:
raise TypeError('Invalid request_type: ' + self._request_type) raise TypeError('Invalid request_type: ' + self._request_type)
@ -147,6 +156,9 @@ class MasterServerV1CallThread(threading.Thread):
response_data = None response_data = None
finally:
babase.shutdown_suppress_end()
if self._callback is not None: if self._callback is not None:
babase.pushcall( babase.pushcall(
babase.Call(self._run_callback, response_data), babase.Call(self._run_callback, response_data),

View file

@ -214,7 +214,10 @@ class ServerController:
babase.app.classic.master_server_v1_get( babase.app.classic.master_server_v1_get(
'bsAccessCheck', 'bsAccessCheck',
{'port': bascenev1.get_game_port(), 'b': babase.app.build_number}, {
'port': bascenev1.get_game_port(),
'b': babase.app.env.build_number,
},
callback=self._access_check_response, callback=self._access_check_response,
) )
@ -379,8 +382,8 @@ class ServerController:
if self._first_run: if self._first_run:
curtimestr = time.strftime('%c') curtimestr = time.strftime('%c')
startupmsg = ( startupmsg = (
f'{Clr.BLD}{Clr.BLU}{babase.appnameupper()} {app.version}' f'{Clr.BLD}{Clr.BLU}{babase.appnameupper()} {app.env.version}'
f' ({app.build_number})' f' ({app.env.build_number})'
f' entering server-mode {curtimestr}{Clr.RST}' f' entering server-mode {curtimestr}{Clr.RST}'
) )
logging.info(startupmsg) logging.info(startupmsg)

View file

@ -545,7 +545,7 @@ class StoreSubsystem:
""" """
plus = babase.app.plus plus = babase.app.plus
unowned_maps: set[str] = set() unowned_maps: set[str] = set()
if not babase.app.headless_mode: if babase.app.env.gui:
for map_section in self.get_store_layout()['maps']: for map_section in self.get_store_layout()['maps']:
for mapitem in map_section['items']: for mapitem in map_section['items']:
if plus is None or not plus.get_purchased(mapitem): if plus is None or not plus.get_purchased(mapitem):
@ -558,7 +558,7 @@ class StoreSubsystem:
try: try:
plus = babase.app.plus plus = babase.app.plus
unowned_games: set[type[bascenev1.GameActivity]] = set() unowned_games: set[type[bascenev1.GameActivity]] = set()
if not babase.app.headless_mode: if babase.app.env.gui:
for section in self.get_store_layout()['minigames']: for section in self.get_store_layout()['minigames']:
for mname in section['items']: for mname in section['items']:
if plus is None or not plus.get_purchased(mname): if plus is None or not plus.get_purchased(mname):

View file

@ -73,7 +73,7 @@ class ClassicSubsystem(babase.AppSubsystem):
self.value_test_defaults: dict = {} self.value_test_defaults: dict = {}
self.special_offer: dict | None = None self.special_offer: dict | None = None
self.ping_thread_count = 0 self.ping_thread_count = 0
self.allow_ticket_purchases: bool = not babase.app.iircade_mode self.allow_ticket_purchases: bool = True
# Main Menu. # Main Menu.
self.main_menu_did_initial_transition = False self.main_menu_did_initial_transition = False
@ -128,6 +128,10 @@ class ClassicSubsystem(babase.AppSubsystem):
assert isinstance(self._env['platform'], str) assert isinstance(self._env['platform'], str)
return self._env['platform'] return self._env['platform']
def scene_v1_protocol_version(self) -> int:
"""(internal)"""
return bascenev1.protocol_version()
@property @property
def subplatform(self) -> str: def subplatform(self) -> str:
"""String for subplatform. """String for subplatform.
@ -153,6 +157,7 @@ class ClassicSubsystem(babase.AppSubsystem):
plus = babase.app.plus plus = babase.app.plus
assert plus is not None assert plus is not None
env = babase.app.env
cfg = babase.app.config cfg = babase.app.config
self.music.on_app_loading() self.music.on_app_loading()
@ -161,11 +166,7 @@ class ClassicSubsystem(babase.AppSubsystem):
# Non-test, non-debug builds should generally be blessed; warn if not. # Non-test, non-debug builds should generally be blessed; warn if not.
# (so I don't accidentally release a build that can't play tourneys) # (so I don't accidentally release a build that can't play tourneys)
if ( if not env.debug and not env.test and not plus.is_blessed():
not babase.app.debug_build
and not babase.app.test_build
and not plus.is_blessed()
):
babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
# FIXME: This should not be hard-coded. # FIXME: This should not be hard-coded.
@ -219,7 +220,7 @@ class ClassicSubsystem(babase.AppSubsystem):
self.special_offer = cfg['pendingSpecialOffer']['o'] self.special_offer = cfg['pendingSpecialOffer']['o']
show_offer() show_offer()
if not babase.app.headless_mode: if babase.app.env.gui:
babase.apptimer(3.0, check_special_offer) babase.apptimer(3.0, check_special_offer)
# If there's a leftover log file, attempt to upload it to the # If there's a leftover log file, attempt to upload it to the
@ -465,6 +466,37 @@ class ClassicSubsystem(babase.AppSubsystem):
_analytics.game_begin_analytics() _analytics.game_begin_analytics()
@classmethod
def json_prep(cls, data: Any) -> Any:
"""Return a json-friendly version of the provided data.
This converts any tuples to lists and any bytes to strings
(interpreted as utf-8, ignoring errors). Logs errors (just once)
if any data is modified/discarded/unsupported.
"""
if isinstance(data, dict):
return dict(
(cls.json_prep(key), cls.json_prep(value))
for key, value in list(data.items())
)
if isinstance(data, list):
return [cls.json_prep(element) for element in data]
if isinstance(data, tuple):
logging.exception('json_prep encountered tuple')
return [cls.json_prep(element) for element in data]
if isinstance(data, bytes):
try:
return data.decode(errors='ignore')
except Exception:
logging.exception('json_prep encountered utf-8 decode error')
return data.decode(errors='ignore')
if not isinstance(data, (str, float, bool, type(None), int)):
logging.exception(
'got unsupported type in json_prep: %s', type(data)
)
return data
def master_server_v1_get( def master_server_v1_get(
self, self,
request: str, request: str,
@ -750,7 +782,7 @@ class ClassicSubsystem(babase.AppSubsystem):
from bauiv1lib.party import PartyWindow from bauiv1lib.party import PartyWindow
from babase import app from babase import app
assert not app.headless_mode assert app.env.gui
bauiv1.getsound('swish').play() bauiv1.getsound('swish').play()
@ -773,7 +805,7 @@ class ClassicSubsystem(babase.AppSubsystem):
if not in_main_menu: if not in_main_menu:
set_ui_input_device(device_id) set_ui_input_device(device_id)
if not babase.app.headless_mode: if babase.app.env.gui:
bauiv1.getsound('swish').play() bauiv1.getsound('swish').play()
babase.app.ui_v1.set_main_menu_window( babase.app.ui_v1.set_main_menu_window(

View file

@ -106,7 +106,6 @@ def get_all_tips() -> list[str]:
), ),
] ]
app = babase.app app = babase.app
if not app.iircade_mode:
tips += [ tips += [
'If your framerate is choppy, try turning down resolution\nor ' 'If your framerate is choppy, try turning down resolution\nor '
'visuals in the game\'s graphics settings.' 'visuals in the game\'s graphics settings.'
@ -114,8 +113,7 @@ def get_all_tips() -> list[str]:
if ( if (
app.classic is not None app.classic is not None
and app.classic.platform in ('android', 'ios') and app.classic.platform in ('android', 'ios')
and not app.on_tv and not app.env.tv
and not app.iircade_mode
): ):
tips += [ tips += [
( (
@ -124,11 +122,7 @@ def get_all_tips() -> list[str]:
'in Settings->Graphics' 'in Settings->Graphics'
), ),
] ]
if ( if app.classic is not None and app.classic.platform in ['mac', 'android']:
app.classic is not None
and app.classic.platform in ['mac', 'android']
and not app.iircade_mode
):
tips += [ tips += [
'Tired of the soundtrack? Replace it with your own!' 'Tired of the soundtrack? Replace it with your own!'
'\nSee Settings->Audio->Soundtrack' '\nSee Settings->Audio->Soundtrack'
@ -136,11 +130,11 @@ def get_all_tips() -> list[str]:
# Hot-plugging is currently only on some platforms. # Hot-plugging is currently only on some platforms.
# FIXME: Should add a platform entry for this so don't forget to update it. # FIXME: Should add a platform entry for this so don't forget to update it.
if ( if app.classic is not None and app.classic.platform in [
app.classic is not None 'mac',
and app.classic.platform in ['mac', 'android', 'windows'] 'android',
and not app.iircade_mode 'windows',
): ]:
tips += [ tips += [
'Players can join and leave in the middle of most games,\n' 'Players can join and leave in the middle of most games,\n'
'and you can also plug and unplug controllers on the fly.', 'and you can also plug and unplug controllers on the fly.',

35
dist/ba_data/python/bacommon/app.py vendored Normal file
View file

@ -0,0 +1,35 @@
# Released under the MIT License. See LICENSE for details.
#
"""Common high level values/functionality related to apps."""
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
if TYPE_CHECKING:
pass
class AppExperience(Enum):
"""Overall experience that can be provided by 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.
"""
# A special experience category that is supported everywhere. Used
# for the default empty AppMode when starting the app, etc.
EMPTY = 'empty'
# The traditional BombSquad experience: multiple players using
# controllers in a single arena small enough for all action to be
# viewed on a single screen.
MELEE = 'melee'
# The traditional BombSquad Remote experience; buttons on a
# touch-screen allowing a mobile device to be used as a game
# controller.
REMOTE = 'remote'

View file

@ -52,8 +52,8 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be # Build number and version of the ballistica binary we expect to be
# using. # using.
TARGET_BALLISTICA_BUILD = 21213 TARGET_BALLISTICA_BUILD = 21397
TARGET_BALLISTICA_VERSION = '1.7.26' TARGET_BALLISTICA_VERSION = '1.7.28'
@dataclass @dataclass
@ -461,11 +461,12 @@ def _modular_main() -> None:
# First baenv sets up things like Python paths the way the engine # First baenv sets up things like Python paths the way the engine
# needs them, and then we import and run the engine. # needs them, and then we import and run the engine.
# #
# Below we're doing a slightly fancier version of that. Namely we do # Below we're doing a slightly fancier version of that. Namely, we
# some processing of command line args to allow overriding of paths # do some processing of command line args to allow overriding of
# or running explicit commands or whatever else. Our goal is that # paths or running explicit commands or whatever else. Our goal is
# this modular form of the app should be basically indistinguishable # that this modular form of the app should be basically
# from the monolithic form when used from the command line. # indistinguishable from the monolithic form when used from the
# command line.
try: try:
# Take note that we're running via modular-main. The native # Take note that we're running via modular-main. The native
@ -476,7 +477,8 @@ def _modular_main() -> None:
# Deal with a few key things here ourself before even running # Deal with a few key things here ourself before even running
# configure. # configure.
# Extract stuff below modifies this so work with a copy. # The extract_arg stuff below modifies this so we work with a
# copy.
args = sys.argv.copy() args = sys.argv.copy()
# NOTE: We need to keep these arg long/short arg versions synced # NOTE: We need to keep these arg long/short arg versions synced
@ -496,8 +498,8 @@ def _modular_main() -> None:
mods_dir = extract_arg(args, ['--mods-dir', '-m'], is_dir=True) mods_dir = extract_arg(args, ['--mods-dir', '-m'], is_dir=True)
# We run configure() BEFORE importing babase. (part of its job # We run configure() BEFORE importing babase. (part of its job
# is to wrangle paths to determine where babase and everything # is to wrangle paths which can affect where babase and
# else gets loaded from). # everything else gets loaded from).
configure( configure(
config_dir=config_dir, config_dir=config_dir,
data_dir=data_dir, data_dir=data_dir,

View file

@ -112,6 +112,7 @@ from _bascenev1 import (
newnode, newnode,
Node, Node,
printnodes, printnodes,
protocol_version,
release_gamepad_input, release_gamepad_input,
release_keyboard_input, release_keyboard_input,
reset_random_player_names, reset_random_player_names,
@ -383,6 +384,7 @@ __all__ = [
'PowerupMessage', 'PowerupMessage',
'print_live_object_warnings', 'print_live_object_warnings',
'printnodes', 'printnodes',
'protocol_version',
'pushcall', 'pushcall',
'register_map', 'register_map',
'release_gamepad_input', 'release_gamepad_input',

View file

@ -5,7 +5,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from bacommon.app import AppExperience
from babase import AppMode, AppIntentExec, AppIntentDefault from babase import AppMode, AppIntentExec, AppIntentDefault
import _bascenev1 import _bascenev1
if TYPE_CHECKING: if TYPE_CHECKING:
@ -16,7 +18,11 @@ class SceneV1AppMode(AppMode):
"""Our app-mode.""" """Our app-mode."""
@classmethod @classmethod
def supports_intent(cls, intent: AppIntent) -> bool: def get_app_experience(cls) -> AppExperience:
return AppExperience.MELEE
@classmethod
def _supports_intent(cls, intent: AppIntent) -> bool:
# We support default and exec intents currently. # We support default and exec intents currently.
return isinstance(intent, AppIntentExec | AppIntentDefault) return isinstance(intent, AppIntentExec | AppIntentDefault)
@ -29,8 +35,8 @@ class SceneV1AppMode(AppMode):
def on_activate(self) -> None: def on_activate(self) -> None:
# Let the native layer do its thing. # Let the native layer do its thing.
_bascenev1.app_mode_activate() _bascenev1.on_app_mode_activate()
def on_deactivate(self) -> None: def on_deactivate(self) -> None:
# Let the native layer do its thing. # Let the native layer do its thing.
_bascenev1.app_mode_deactivate() _bascenev1.on_app_mode_deactivate()

View file

@ -53,7 +53,8 @@ class CoopGameActivity(GameActivity[PlayerT, TeamT]):
super().on_begin() super().on_begin()
# Show achievements remaining. # Show achievements remaining.
if not (babase.app.demo_mode or babase.app.arcade_mode): env = babase.app.env
if not (env.demo or env.arcade):
_bascenev1.timer( _bascenev1.timer(
3.8, babase.WeakCall(self._show_remaining_achievements) 3.8, babase.WeakCall(self._show_remaining_achievements)
) )
@ -108,7 +109,7 @@ class CoopGameActivity(GameActivity[PlayerT, TeamT]):
) )
if not a.complete if not a.complete
] ]
vrmode = babase.app.vr_mode vrmode = babase.app.env.vr
if achievements: if achievements:
Text( Text(
babase.Lstr(resource='achievementsRemainingText'), babase.Lstr(resource='achievementsRemainingText'),

View file

@ -211,7 +211,7 @@ class CoopSession(Session):
# Hmm; no players anywhere. Let's end the entire session if we're # Hmm; no players anywhere. Let's end the entire session if we're
# running a GUI (or just the current game if we're running headless). # running a GUI (or just the current game if we're running headless).
else: else:
if not babase.app.headless_mode: if babase.app.env.gui:
self.end() self.end()
else: else:
if isinstance(activity, GameActivity): if isinstance(activity, GameActivity):
@ -277,6 +277,7 @@ class CoopSession(Session):
from bascenev1._score import ScoreType from bascenev1._score import ScoreType
app = babase.app app = babase.app
env = app.env
classic = app.classic classic = app.classic
assert classic is not None assert classic is not None
@ -291,7 +292,7 @@ class CoopSession(Session):
# If we're running with a gui and at any point we have no # If we're running with a gui and at any point we have no
# in-game players, quit out of the session (this can happen if # in-game players, quit out of the session (this can happen if
# someone leaves in the tutorial for instance). # someone leaves in the tutorial for instance).
if not babase.app.headless_mode: if env.gui:
active_players = [p for p in self.sessionplayers if p.in_game] active_players = [p for p in self.sessionplayers if p.in_game]
if not active_players: if not active_players:
self.end() self.end()
@ -317,7 +318,7 @@ class CoopSession(Session):
if ( if (
isinstance(activity, JoinActivity) isinstance(activity, JoinActivity)
and self.campaign_level_name == 'Onslaught Training' and self.campaign_level_name == 'Onslaught Training'
and not (app.demo_mode or app.arcade_mode) and not (env.demo or env.arcade)
): ):
if self._tutorial_activity is None: if self._tutorial_activity is None:
raise RuntimeError('Tutorial not preloaded properly.') raise RuntimeError('Tutorial not preloaded properly.')
@ -339,7 +340,7 @@ class CoopSession(Session):
# Now flip the current activity.. # Now flip the current activity..
self.setactivity(next_game) self.setactivity(next_game)
if not (app.demo_mode or app.arcade_mode): if not (env.demo or env.arcade):
if self.tournament_id is not None: if self.tournament_id is not None:
self._custom_menu_ui = [ self._custom_menu_ui = [
{ {

View file

@ -600,7 +600,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
translate=('gameDescriptions', sb_desc_l[0]), subs=subs translate=('gameDescriptions', sb_desc_l[0]), subs=subs
) )
sb_desc = translation sb_desc = translation
vrmode = babase.app.vr_mode vrmode = babase.app.env.vr
yval = -34 if is_empty else -20 yval = -34 if is_empty else -20
yval -= 16 yval -= 16
sbpos = ( sbpos = (
@ -706,7 +706,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
resource='epicDescriptionFilterText', resource='epicDescriptionFilterText',
subs=[('${DESCRIPTION}', translation)], subs=[('${DESCRIPTION}', translation)],
) )
vrmode = babase.app.vr_mode vrmode = babase.app.env.vr
dnode = _bascenev1.newnode( dnode = _bascenev1.newnode(
'text', 'text',
attrs={ attrs={
@ -761,7 +761,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
base_position = (75, 50) base_position = (75, 50)
tip_scale = 0.8 tip_scale = 0.8
tip_title_scale = 1.2 tip_title_scale = 1.2
vrmode = babase.app.vr_mode vrmode = babase.app.env.vr
t_offs = -350.0 t_offs = -350.0
tnode = _bascenev1.newnode( tnode = _bascenev1.newnode(

View file

@ -185,7 +185,7 @@ def show_damage_count(
# (connected clients may have differing configs so they won't # (connected clients may have differing configs so they won't
# get the intended results). # get the intended results).
assert app.classic is not None assert app.classic is not None
do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.vr_mode do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.env.vr
txtnode = _bascenev1.newnode( txtnode = _bascenev1.newnode(
'text', 'text',
attrs={ attrs={

View file

@ -33,15 +33,11 @@ class JoinInfo:
from bascenev1._nodeactor import NodeActor from bascenev1._nodeactor import NodeActor
self._state = 0 self._state = 0
self._press_to_punch: str | bascenev1.Lstr = ( self._press_to_punch: str | bascenev1.Lstr = babase.charstr(
'C' babase.SpecialChar.LEFT_BUTTON
if babase.app.iircade_mode
else babase.charstr(babase.SpecialChar.LEFT_BUTTON)
) )
self._press_to_bomb: str | bascenev1.Lstr = ( self._press_to_bomb: str | bascenev1.Lstr = babase.charstr(
'B' babase.SpecialChar.RIGHT_BUTTON
if babase.app.iircade_mode
else babase.charstr(babase.SpecialChar.RIGHT_BUTTON)
) )
self._joinmsg = babase.Lstr(resource='pressAnyButtonToJoinText') self._joinmsg = babase.Lstr(resource='pressAnyButtonToJoinText')
can_switch_teams = len(lobby.sessionteams) > 1 can_switch_teams = len(lobby.sessionteams) > 1
@ -53,7 +49,7 @@ class JoinInfo:
if keyboard is not None: if keyboard is not None:
self._update_for_keyboard(keyboard) self._update_for_keyboard(keyboard)
flatness = 1.0 if babase.app.vr_mode else 0.0 flatness = 1.0 if babase.app.env.vr else 0.0
self._text = NodeActor( self._text = NodeActor(
_bascenev1.newnode( _bascenev1.newnode(
'text', 'text',
@ -69,7 +65,7 @@ class JoinInfo:
) )
) )
if babase.app.demo_mode or babase.app.arcade_mode: if babase.app.env.demo or babase.app.env.arcade:
self._messages = [self._joinmsg] self._messages = [self._joinmsg]
else: else:
msg1 = babase.Lstr( msg1 = babase.Lstr(
@ -438,6 +434,7 @@ class Chooser:
"""Reload all player profiles.""" """Reload all player profiles."""
app = babase.app app = babase.app
env = app.env
assert app.classic is not None assert app.classic is not None
# Re-construct our profile index and other stuff since the profile # Re-construct our profile index and other stuff since the profile
@ -465,7 +462,7 @@ class Chooser:
# (non-unicode/non-json) version. # (non-unicode/non-json) version.
# Make sure they conform to our standards # Make sure they conform to our standards
# (unicode strings, no tuples, etc) # (unicode strings, no tuples, etc)
self._profiles = babase.json_prep(self._profiles) self._profiles = app.classic.json_prep(self._profiles)
# Filter out any characters we're unaware of. # Filter out any characters we're unaware of.
for profile in list(self._profiles.items()): for profile in list(self._profiles.items()):
@ -479,17 +476,13 @@ class Chooser:
self._profiles['_random'] = {} self._profiles['_random'] = {}
# In kiosk mode we disable account profiles to force random. # In kiosk mode we disable account profiles to force random.
if app.demo_mode or app.arcade_mode: if env.demo or env.arcade:
if '__account__' in self._profiles: if '__account__' in self._profiles:
del self._profiles['__account__'] del self._profiles['__account__']
# For local devices, add it an 'edit' option which will pop up # For local devices, add it an 'edit' option which will pop up
# the profile window. # the profile window.
if ( if not is_remote and not is_test_input and not (env.demo or env.arcade):
not is_remote
and not is_test_input
and not (app.demo_mode or app.arcade_mode)
):
self._profiles['_edit'] = {} self._profiles['_edit'] = {}
# Build a sorted name list we can iterate through. # Build a sorted name list we can iterate through.

View file

@ -371,5 +371,5 @@ def register_map(maptype: type[Map]) -> None:
"""Register a map class with the game.""" """Register a map class with the game."""
assert babase.app.classic is not None assert babase.app.classic is not None
if maptype.name in babase.app.classic.maps: if maptype.name in babase.app.classic.maps:
raise RuntimeError('map "' + maptype.name + '" already registered') raise RuntimeError(f'Map "{maptype.name}" is already registered.')
babase.app.classic.maps[maptype.name] = maptype babase.app.classic.maps[maptype.name] = maptype

View file

@ -69,7 +69,7 @@ def get_player_profile_colors(
# Special case: when being asked for a random color in kiosk mode, # Special case: when being asked for a random color in kiosk mode,
# always return default purple. # always return default purple.
if (babase.app.demo_mode or babase.app.arcade_mode) and profilename is None: if (babase.app.env.demo or babase.app.env.arcade) and profilename is None:
color = (0.5, 0.4, 1.0) color = (0.5, 0.4, 1.0)
highlight = (0.4, 0.4, 0.5) highlight = (0.4, 0.4, 0.5)
else: else:

View file

@ -45,6 +45,9 @@ class CoopJoinActivity(bs.JoinActivity):
def _show_remaining_achievements(self) -> None: def _show_remaining_achievements(self) -> None:
from bascenev1lib.actor.text import Text from bascenev1lib.actor.text import Text
app = bs.app
env = app.env
# We only show achievements and challenges for CoopGameActivities. # We only show achievements and challenges for CoopGameActivities.
session = self.session session = self.session
assert isinstance(session, bs.CoopSession) assert isinstance(session, bs.CoopSession)
@ -64,19 +67,15 @@ class CoopJoinActivity(bs.JoinActivity):
ts_h_offs = 60 ts_h_offs = 60
# Show remaining achievements in some cases. # Show remaining achievements in some cases.
if bs.app.classic is not None and not ( if app.classic is not None and not (env.demo or env.arcade):
bs.app.demo_mode or bs.app.arcade_mode
):
achievements = [ achievements = [
a a
for a in bs.app.classic.ach.achievements_for_coop_level( for a in app.classic.ach.achievements_for_coop_level(levelname)
levelname
)
if not a.complete if not a.complete
] ]
have_achievements = bool(achievements) have_achievements = bool(achievements)
achievements = [a for a in achievements if not a.complete] achievements = [a for a in achievements if not a.complete]
vrmode = bs.app.vr_mode vrmode = env.vr
if have_achievements: if have_achievements:
Text( Text(
bs.Lstr(resource='achievementsRemainingText'), bs.Lstr(resource='achievementsRemainingText'),

View file

@ -351,6 +351,8 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
assert bui.app.classic is not None assert bui.app.classic is not None
env = bui.app.env
delay = 0.7 if (self._score is not None) else 0.0 delay = 0.7 if (self._score is not None) else 0.0
# If there's no players left in the game, lets not show the UI # If there's no players left in the game, lets not show the UI
@ -406,9 +408,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
else: else:
pass pass
show_next_button = self._is_more_levels and not ( show_next_button = self._is_more_levels and not (env.demo or env.arcade)
bui.app.demo_mode or bui.app.arcade_mode
)
if not show_next_button: if not show_next_button:
h_offs += 70 h_offs += 70
@ -486,7 +486,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
v_offs + 560.0, v_offs + 560.0,
) )
if bui.app.demo_mode or bui.app.arcade_mode: if env.demo or env.arcade:
self._league_rank_button = None self._league_rank_button = None
self._store_button_instance = None self._store_button_instance = None
else: else:
@ -595,7 +595,9 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
super().on_begin() super().on_begin()
plus = bs.app.plus app = bs.app
env = app.env
plus = app.plus
assert plus is not None assert plus is not None
self._begin_time = bs.time() self._begin_time = bs.time()
@ -624,7 +626,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
# If this is the first time we completed it, set the next one # If this is the first time we completed it, set the next one
# as current. # as current.
if self._newly_complete: if self._newly_complete:
cfg = bs.app.config cfg = app.config
cfg['Selected Coop Game'] = ( cfg['Selected Coop Game'] = (
self._campaign.name + ':' + self._next_level_name self._campaign.name + ':' + self._next_level_name
) )
@ -637,7 +639,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
self._is_complete self._is_complete
and self._victory and self._victory
and self._is_more_levels and self._is_more_levels
and not (bs.app.demo_mode or bs.app.arcade_mode) and not (env.demo or env.arcade)
): ):
Text( Text(
bs.Lstr( bs.Lstr(
@ -715,7 +717,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
position=(0, 230), position=(0, 230),
).autoretain() ).autoretain()
if bs.app.classic is not None and bs.app.classic.server is None: if app.classic is not None and app.classic.server is None:
# If we're running in normal non-headless build, show this text # If we're running in normal non-headless build, show this text
# because only host can continue the game. # because only host can continue the game.
adisp = plus.get_v1_account_display_string() adisp = plus.get_v1_account_display_string()
@ -828,7 +830,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
) )
if plus.get_v1_account_state() != 'signed_in': if plus.get_v1_account_state() != 'signed_in':
# We expect this only in kiosk mode; complain otherwise. # We expect this only in kiosk mode; complain otherwise.
if not (bs.app.demo_mode or bs.app.arcade_mode): if not (env.demo or env.arcade):
logging.error('got not-signed-in at score-submit; unexpected') logging.error('got not-signed-in at score-submit; unexpected')
bs.pushcall(bs.WeakCall(self._got_score_results, None)) bs.pushcall(bs.WeakCall(self._got_score_results, None))
else: else:

View file

@ -74,7 +74,7 @@ class Background(bs.Actor):
self.node.connectattr('opacity', self.logo, 'opacity') self.node.connectattr('opacity', self.logo, 'opacity')
# add jitter/pulse for a stop-motion-y look unless we're in VR # add jitter/pulse for a stop-motion-y look unless we're in VR
# in which case stillness is better # in which case stillness is better
if not bs.app.vr_mode: if not bs.app.env.vr:
self.cmb = bs.newnode( self.cmb = bs.newnode(
'combine', owner=self.node, attrs={'size': 2} 'combine', owner=self.node, attrs={'size': 2}
) )

View file

@ -58,44 +58,10 @@ class ControlsGuide(bs.Actor):
self._update_timer: bs.Timer | None = None self._update_timer: bs.Timer | None = None
self._title_text: bs.Node | None self._title_text: bs.Node | None
clr: Sequence[float] clr: Sequence[float]
extra_pos_1: tuple[float, float] | None
extra_pos_2: tuple[float, float] | None
if bs.app.iircade_mode:
xtweak = 0.2
ytweak = 0.2
jump_pos = (
position[0] + offs * (-1.2 + xtweak),
position[1] + offs * (0.1 + ytweak),
)
bomb_pos = (
position[0] + offs * (0.0 + xtweak),
position[1] + offs * (0.5 + ytweak),
)
punch_pos = (
position[0] + offs * (1.2 + xtweak),
position[1] + offs * (0.5 + ytweak),
)
pickup_pos = (
position[0] + offs * (-1.4 + xtweak),
position[1] + offs * (-1.2 + ytweak),
)
extra_pos_1 = (
position[0] + offs * (-0.2 + xtweak),
position[1] + offs * (-0.8 + ytweak),
)
extra_pos_2 = (
position[0] + offs * (1.0 + xtweak),
position[1] + offs * (-0.8 + ytweak),
)
self._force_hide_button_names = True
else:
punch_pos = (position[0] - offs * 1.1, position[1]) punch_pos = (position[0] - offs * 1.1, position[1])
jump_pos = (position[0], position[1] - offs) jump_pos = (position[0], position[1] - offs)
bomb_pos = (position[0] + offs * 1.1, position[1]) bomb_pos = (position[0] + offs * 1.1, position[1])
pickup_pos = (position[0], position[1] + offs) pickup_pos = (position[0], position[1] + offs)
extra_pos_1 = None
extra_pos_2 = None
self._force_hide_button_names = False self._force_hide_button_names = False
if show_title: if show_title:
@ -242,13 +208,13 @@ class ControlsGuide(bs.Actor):
clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0) clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0)
self._run_text_pos_top = (position[0], position[1] - 135.0 * scale) self._run_text_pos_top = (position[0], position[1] - 135.0 * scale)
self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale) self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale)
sval = 1.0 * scale if bs.app.vr_mode else 0.8 * scale sval = 1.0 * scale if bs.app.env.vr else 0.8 * scale
self._run_text = bs.newnode( self._run_text = bs.newnode(
'text', 'text',
attrs={ attrs={
'scale': sval, 'scale': sval,
'host_only': True, 'host_only': True,
'shadow': 1.0 if bs.app.vr_mode else 0.5, 'shadow': 1.0 if bs.app.env.vr else 0.5,
'flatness': 1.0, 'flatness': 1.0,
'maxwidth': 380, 'maxwidth': 380,
'v_align': 'top', 'v_align': 'top',
@ -271,35 +237,7 @@ class ControlsGuide(bs.Actor):
}, },
) )
if extra_pos_1 is not None:
self._extra_image_1: bs.Node | None = bs.newnode(
'image',
attrs={
'texture': bs.gettexture('nub'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': extra_pos_1,
'scale': (image_size, image_size),
'color': (0.5, 0.5, 0.5),
},
)
else:
self._extra_image_1 = None self._extra_image_1 = None
if extra_pos_2 is not None:
self._extra_image_2: bs.Node | None = bs.newnode(
'image',
attrs={
'texture': bs.gettexture('nub'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': extra_pos_2,
'scale': (image_size, image_size),
'color': (0.5, 0.5, 0.5),
},
)
else:
self._extra_image_2 = None self._extra_image_2 = None
self._nodes = [ self._nodes = [
@ -317,10 +255,6 @@ class ControlsGuide(bs.Actor):
if show_title: if show_title:
assert self._title_text assert self._title_text
self._nodes.append(self._title_text) self._nodes.append(self._title_text)
if self._extra_image_1 is not None:
self._nodes.append(self._extra_image_1)
if self._extra_image_2 is not None:
self._nodes.append(self._extra_image_2)
# Start everything invisible. # Start everything invisible.
for node in self._nodes: for node in self._nodes:

View file

@ -209,7 +209,7 @@ class PlayerSpaz(Spaz):
picked_up_by = msg.node.source_player picked_up_by = msg.node.source_player
if picked_up_by: if picked_up_by:
self.last_player_attacked_by = picked_up_by self.last_player_attacked_by = picked_up_by
self.last_attacked_time = bs.apptime() self.last_attacked_time = bs.time()
self.last_attacked_type = ('picked_up', 'default') self.last_attacked_type = ('picked_up', 'default')
elif isinstance(msg, bs.StandMessage): elif isinstance(msg, bs.StandMessage):
super().handlemessage(msg) # Augment standard behavior. super().handlemessage(msg) # Augment standard behavior.
@ -247,7 +247,7 @@ class PlayerSpaz(Spaz):
# something like last_actor_attacked_by to fix that. # something like last_actor_attacked_by to fix that.
if ( if (
self.last_player_attacked_by self.last_player_attacked_by
and bs.apptime() - self.last_attacked_time < 4.0 and bs.time() - self.last_attacked_time < 4.0
): ):
killerplayer = self.last_player_attacked_by killerplayer = self.last_player_attacked_by
else: else:
@ -278,7 +278,7 @@ class PlayerSpaz(Spaz):
source_player = msg.get_source_player(type(self._player)) source_player = msg.get_source_player(type(self._player))
if source_player: if source_player:
self.last_player_attacked_by = source_player self.last_player_attacked_by = source_player
self.last_attacked_time = bs.apptime() self.last_attacked_time = bs.time()
self.last_attacked_type = (msg.hit_type, msg.hit_subtype) self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
super().handlemessage(msg) # Augment standard behavior. super().handlemessage(msg) # Augment standard behavior.
activity = self._activity() activity = self._activity()

View file

@ -45,7 +45,7 @@ class _Entry:
# FIXME: Should not do things conditionally for vr-mode, as there may # FIXME: Should not do things conditionally for vr-mode, as there may
# be non-vr clients connected which will also get these value. # be non-vr clients connected which will also get these value.
vrmode = bs.app.vr_mode vrmode = bs.app.env.vr
if self._do_cover: if self._do_cover:
if vrmode: if vrmode:

View file

@ -19,6 +19,9 @@ if TYPE_CHECKING:
from typing import Any, Sequence, Callable from typing import Any, Sequence, Callable
POWERUP_WEAR_OFF_TIME = 20000 POWERUP_WEAR_OFF_TIME = 20000
# Obsolete - just used for demo guy now.
BASE_PUNCH_POWER_SCALE = 1.2
BASE_PUNCH_COOLDOWN = 400 BASE_PUNCH_COOLDOWN = 400
@ -95,7 +98,7 @@ class Spaz(bs.Actor):
self.source_player = source_player self.source_player = source_player
self._dead = False self._dead = False
if self._demo_mode: # Preserve old behavior. if self._demo_mode: # Preserve old behavior.
self._punch_power_scale = 1.2 self._punch_power_scale = BASE_PUNCH_POWER_SCALE
else: else:
self._punch_power_scale = factory.punch_power_scale self._punch_power_scale = factory.punch_power_scale
self.fly = bs.getactivity().globalsnode.happy_thoughts_mode self.fly = bs.getactivity().globalsnode.happy_thoughts_mode
@ -189,7 +192,7 @@ class Spaz(bs.Actor):
self.land_mine_count = 0 self.land_mine_count = 0
self.blast_radius = 2.0 self.blast_radius = 2.0
self.powerups_expire = powerups_expire self.powerups_expire = powerups_expire
if self._demo_mode: # preserve old behavior if self._demo_mode: # Preserve old behavior.
self._punch_cooldown = BASE_PUNCH_COOLDOWN self._punch_cooldown = BASE_PUNCH_COOLDOWN
else: else:
self._punch_cooldown = factory.punch_cooldown self._punch_cooldown = factory.punch_cooldown
@ -482,12 +485,12 @@ class Spaz(bs.Actor):
Called to 'press bomb' on this spaz; Called to 'press bomb' on this spaz;
used for player or AI connections. used for player or AI connections.
""" """
if not self.node: if (
return not self.node
or self._dead
if self._dead or self.frozen: or self.frozen
return or self.node.knockout > 0.0
if self.node.knockout > 0.0: ):
return return
t_ms = int(bs.time() * 1000.0) t_ms = int(bs.time() * 1000.0)
assert isinstance(t_ms, int) assert isinstance(t_ms, int)
@ -514,15 +517,14 @@ class Spaz(bs.Actor):
""" """
if not self.node: if not self.node:
return return
t_ms = int(bs.time() * 1000.0) t_ms = int(bs.time() * 1000.0)
assert isinstance(t_ms, int) assert isinstance(t_ms, int)
self.last_run_time_ms = t_ms self.last_run_time_ms = t_ms
self.node.run = value self.node.run = value
# filtering these events would be tough since its an analog # Filtering these events would be tough since its an analog
# value, but lets still pass full 0-to-1 presses along to # value, but lets still pass full 0-to-1 presses along to
# the turbo filter to punish players if it looks like they're turbo-ing # the turbo filter to punish players if it looks like they're turbo-ing.
if self._last_run_value < 0.01 and value > 0.99: if self._last_run_value < 0.01 and value > 0.99:
self._turbo_filter_add_press('run') self._turbo_filter_add_press('run')
@ -535,7 +537,7 @@ class Spaz(bs.Actor):
""" """
if not self.node: if not self.node:
return return
# not adding a cooldown time here for now; slightly worried # Not adding a cooldown time here for now; slightly worried
# input events get clustered up during net-games and we'd wind up # input events get clustered up during net-games and we'd wind up
# killing a lot and making it hard to fly.. should look into this. # killing a lot and making it hard to fly.. should look into this.
self.node.fly_pressed = True self.node.fly_pressed = True
@ -610,7 +612,7 @@ class Spaz(bs.Actor):
self.node, attr, materials + (factory.curse_material,) self.node, attr, materials + (factory.curse_material,)
) )
# None specifies no time limit # None specifies no time limit.
assert self.node assert self.node
if self.curse_time is None: if self.curse_time is None:
self.node.curse_death_time = -1 self.node.curse_death_time = -1
@ -878,7 +880,7 @@ class Spaz(bs.Actor):
self.node.frozen = True self.node.frozen = True
bs.timer(5.0, bs.WeakCall(self.handlemessage, bs.ThawMessage())) bs.timer(5.0, bs.WeakCall(self.handlemessage, bs.ThawMessage()))
# Instantly shatter if we're already dead. # Instantly shatter if we're already dead.
# (otherwise its hard to tell we're dead) # (otherwise its hard to tell we're dead).
if self.hitpoints <= 0: if self.hitpoints <= 0:
self.shatter() self.shatter()
@ -898,7 +900,7 @@ class Spaz(bs.Actor):
return True return True
# If we were recently hit, don't count this as another. # If we were recently hit, don't count this as another.
# (so punch flurries and bomb pileups essentially count as 1 hit) # (so punch flurries and bomb pileups essentially count as 1 hit).
local_time = int(bs.time() * 1000.0) local_time = int(bs.time() * 1000.0)
assert isinstance(local_time, int) assert isinstance(local_time, int)
if ( if (
@ -1133,11 +1135,11 @@ class Spaz(bs.Actor):
) )
if self.hitpoints > 0: if self.hitpoints > 0:
# It's kinda crappy to die from impacts, so lets reduce # It's kinda crappy to die from impacts, so lets reduce
# impact damage by a reasonable amount *if* it'll keep us alive # 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, # Drop damage to whatever puts us at 10 hit points,
# or 200 less than it used to be whichever is greater # or 200 less than it used to be whichever is greater
# (so it *can* still kill us if its high enough) # (so it *can* still kill us if its high enough).
newdamage = max(damage - 200, self.hitpoints - 10) newdamage = max(damage - 200, self.hitpoints - 10)
damage = newdamage damage = newdamage
self.node.handlemessage('flash') self.node.handlemessage('flash')

View file

@ -69,7 +69,7 @@ class ZoomText(bs.Actor):
) )
# we never jitter in vr mode.. # we never jitter in vr mode..
if bs.app.vr_mode: if bs.app.env.vr:
jitter = 0.0 jitter = 0.0
# if they want jitter, animate its position slightly... # if they want jitter, animate its position slightly...

View file

@ -483,9 +483,6 @@ class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]):
except bs.NotFoundError: except bs.NotFoundError:
return return
if not spaz.is_alive():
return
player = spaz.getplayer(Player, True) player = spaz.getplayer(Player, True)
if player: if player:

View file

@ -487,8 +487,8 @@ class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
super().on_begin() super().on_begin()
# Show controls help in kiosk mode. # Show controls help in demo or arcade mode.
if bs.app.demo_mode or bs.app.arcade_mode: if bs.app.env.demo or bs.app.env.arcade:
controlsguide.ControlsGuide( controlsguide.ControlsGuide(
delay=3.0, lifespan=10.0, bright=True delay=3.0, lifespan=10.0, bright=True
).autoretain() ).autoretain()

View file

@ -60,7 +60,7 @@ class Puck(bs.Actor):
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.DieMessage): if isinstance(msg, bs.DieMessage):
assert self.node if self.node:
self.node.delete() self.node.delete()
activity = self._activity() activity = self._activity()
if activity and not msg.immediate: if activity and not msg.immediate:

View file

@ -186,6 +186,7 @@ class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]):
'materials': flagmats, 'materials': flagmats,
}, },
) )
self._update_scoreboard()
self._update_flag_state() self._update_flag_state()
def _tick(self) -> None: def _tick(self) -> None:

View file

@ -550,8 +550,9 @@ class OnslaughtGame(bs.CoopGameActivity[Player, Team]):
] ]
elif self._preset in {Preset.UBER, Preset.UBER_EASY}: elif self._preset in {Preset.UBER, Preset.UBER_EASY}:
# Show controls help in demo/arcade modes. # Show controls help in demo or arcade modes.
if bs.app.demo_mode or bs.app.arcade_mode: env = bs.app.env
if env.demo or env.arcade:
ControlsGuide( ControlsGuide(
delay=3.0, lifespan=10.0, bright=True delay=3.0, lifespan=10.0, bright=True
).autoretain() ).autoretain()

View file

@ -478,7 +478,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
) )
# FIXME; should not set things based on vr mode. # FIXME; should not set things based on vr mode.
# (won't look right to non-vr connected clients, etc) # (won't look right to non-vr connected clients, etc)
vrmode = bs.app.vr_mode vrmode = bs.app.env.vr
self._lives_text = bs.NodeActor( self._lives_text = bs.NodeActor(
bs.newnode( bs.newnode(
'text', 'text',

View file

@ -50,6 +50,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
super().on_transition_in() super().on_transition_in()
random.seed(123) random.seed(123)
app = bs.app app = bs.app
env = app.env
assert app.classic is not None assert app.classic is not None
plus = bui.app.plus plus = bui.app.plus
@ -59,9 +60,9 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
# the host is VR mode or not (clients may differ in that regard). # the host is VR mode or not (clients may differ in that regard).
# Any differences need to happen at the engine level so everyone # Any differences need to happen at the engine level so everyone
# sees things in their own optimal way. # sees things in their own optimal way.
vr_mode = bs.app.vr_mode vr_mode = bs.app.env.vr
if not bs.app.toolbar_test: if not bs.app.ui_v1.use_toolbars:
color = (1.0, 1.0, 1.0, 1.0) if vr_mode else (0.5, 0.6, 0.5, 0.6) color = (1.0, 1.0, 1.0, 1.0) if vr_mode else (0.5, 0.6, 0.5, 0.6)
# FIXME: Need a node attr for vr-specific-scale. # FIXME: Need a node attr for vr-specific-scale.
@ -117,21 +118,21 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
# the host is vr mode or not (clients may not be or vice versa). # the host is vr mode or not (clients may not be or vice versa).
# Any differences need to happen at the engine level so everyone sees # Any differences need to happen at the engine level so everyone sees
# things in their own optimal way. # things in their own optimal way.
vr_mode = app.vr_mode vr_mode = app.env.vr
uiscale = app.ui_v1.uiscale uiscale = app.ui_v1.uiscale
# In cases where we're doing lots of dev work lets always show the # In cases where we're doing lots of dev work lets always show the
# build number. # build number.
force_show_build_number = False force_show_build_number = False
if not bs.app.toolbar_test: if not bs.app.ui_v1.use_toolbars:
if app.debug_build or app.test_build or force_show_build_number: if env.debug or env.test or force_show_build_number:
if app.debug_build: if env.debug:
text = bs.Lstr( text = bs.Lstr(
value='${V} (${B}) (${D})', value='${V} (${B}) (${D})',
subs=[ subs=[
('${V}', app.version), ('${V}', app.env.version),
('${B}', str(app.build_number)), ('${B}', str(app.env.build_number)),
('${D}', bs.Lstr(resource='debugText')), ('${D}', bs.Lstr(resource='debugText')),
], ],
) )
@ -139,12 +140,12 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
text = bs.Lstr( text = bs.Lstr(
value='${V} (${B})', value='${V} (${B})',
subs=[ subs=[
('${V}', app.version), ('${V}', app.env.version),
('${B}', str(app.build_number)), ('${B}', str(app.env.build_number)),
], ],
) )
else: else:
text = bs.Lstr(value='${V}', subs=[('${V}', app.version)]) text = bs.Lstr(value='${V}', subs=[('${V}', app.env.version)])
scale = 0.9 if (uiscale is bs.UIScale.SMALL or vr_mode) else 0.7 scale = 0.9 if (uiscale is bs.UIScale.SMALL or vr_mode) else 0.7
color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7) color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7)
self.version = bs.NodeActor( self.version = bs.NodeActor(
@ -168,31 +169,9 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
assert self.version.node assert self.version.node
bs.animate(self.version.node, 'opacity', {2.3: 0, 3.0: 1.0}) bs.animate(self.version.node, 'opacity', {2.3: 0, 3.0: 1.0})
# Show the iircade logo on our iircade build.
if app.iircade_mode:
img = bs.NodeActor(
bs.newnode(
'image',
attrs={
'texture': bs.gettexture('iircadeLogo'),
'attach': 'center',
'scale': (250, 250),
'position': (0, 0),
'tilt_translate': 0.21,
'absolute_scale': True,
},
)
).autoretain()
imgdelay = (
0.0 if app.classic.main_menu_did_initial_transition else 1.0
)
bs.animate(
img.node, 'opacity', {imgdelay + 1.5: 0.0, imgdelay + 2.5: 1.0}
)
# Throw in test build info. # Throw in test build info.
self.beta_info = self.beta_info_2 = None self.beta_info = self.beta_info_2 = None
if app.test_build and not (app.demo_mode or app.arcade_mode): if env.test and not (env.demo or env.arcade):
pos = (230, 35) pos = (230, 35)
self.beta_info = bs.NodeActor( self.beta_info = bs.NodeActor(
bs.newnode( bs.newnode(
@ -313,7 +292,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
random.seed() random.seed()
if not (app.demo_mode or app.arcade_mode) and not app.toolbar_test: if not (env.demo or env.arcade) and not app.ui_v1.use_toolbars:
self._news = NewsDisplay(self) self._news = NewsDisplay(self)
# Bring up the last place we were, or start at the main menu otherwise. # Bring up the last place we were, or start at the main menu otherwise.
@ -330,7 +309,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
# When coming back from a kiosk-mode game, jump to # When coming back from a kiosk-mode game, jump to
# the kiosk start screen. # the kiosk start screen.
if bs.app.demo_mode or bs.app.arcade_mode: if env.demo or env.arcade:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bauiv1lib.kiosk import KioskWindow from bauiv1lib.kiosk import KioskWindow
@ -417,6 +396,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
app = bs.app app = bs.app
env = app.env
assert app.classic is not None assert app.classic is not None
# Update logo in case it changes. # Update logo in case it changes.
@ -460,7 +440,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
base_x = -270.0 base_x = -270.0
x = base_x - 20.0 x = base_x - 20.0
spacing = 85.0 * base_scale spacing = 85.0 * base_scale
y_extra = 0.0 if (app.demo_mode or app.arcade_mode) else 0.0 y_extra = 0.0 if (env.demo or env.arcade) else 0.0
self._make_logo( self._make_logo(
x - 110 + 50, x - 110 + 50,
113 + y + 1.2 * y_extra, 113 + y + 1.2 * y_extra,
@ -525,7 +505,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
base_x = -170 base_x = -170
x = base_x - 20 x = base_x - 20
spacing = 55 * base_scale spacing = 55 * base_scale
y_extra = 0 if (app.demo_mode or app.arcade_mode) else 0 y_extra = 0 if (env.demo or env.arcade) else 0
xv1 = x xv1 = x
delay1 = delay delay1 = delay
for shadow in (True, False): for shadow in (True, False):
@ -677,7 +657,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
# Add a bit of stop-motion-y jitter to the logo # Add a bit of stop-motion-y jitter to the logo
# (unless we're in VR mode in which case its best to # (unless we're in VR mode in which case its best to
# leave things still). # leave things still).
if not bs.app.vr_mode: if not bs.app.env.vr:
cmb: bs.Node | None cmb: bs.Node | None
cmb2: bs.Node | None cmb2: bs.Node | None
if not shadow: if not shadow:
@ -796,7 +776,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
# (unless we're in VR mode in which case its best to # (unless we're in VR mode in which case its best to
# leave things still). # leave things still).
assert logo.node assert logo.node
if not bs.app.vr_mode: if not bs.app.env.vr:
cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2})
cmb.connectattr('output', logo.node, 'position') cmb.connectattr('output', logo.node, 'position')
keys = {} keys = {}
@ -904,7 +884,7 @@ class NewsDisplay:
self._phrases.insert(0, phr) self._phrases.insert(0, phr)
val = self._phrases.pop() val = self._phrases.pop()
if val == '__ACH__': if val == '__ACH__':
vrmode = app.vr_mode vrmode = app.env.vr
Text( Text(
bs.Lstr(resource='nextAchievementsText'), bs.Lstr(resource='nextAchievementsText'),
color=((1, 1, 1, 1) if vrmode else (0.95, 0.9, 1, 0.4)), color=((1, 1, 1, 1) if vrmode else (0.95, 0.9, 1, 0.4)),
@ -970,7 +950,7 @@ class NewsDisplay:
# Show upcoming achievements in non-vr versions # Show upcoming achievements in non-vr versions
# (currently too hard to read in vr). # (currently too hard to read in vr).
self._used_phrases = (['__ACH__'] if not bs.app.vr_mode else []) + [ self._used_phrases = (['__ACH__'] if not bs.app.env.vr else []) + [
s for s in news.split('<br>\n') if s != '' s for s in news.split('<br>\n') if s != ''
] ]
self._phrase_change_timer = bs.Timer( self._phrase_change_timer = bs.Timer(
@ -982,12 +962,12 @@ class NewsDisplay:
assert bs.app.classic is not None assert bs.app.classic is not None
scl = ( scl = (
1.2 1.2
if (bs.app.ui_v1.uiscale is bs.UIScale.SMALL or bs.app.vr_mode) if (bs.app.ui_v1.uiscale is bs.UIScale.SMALL or bs.app.env.vr)
else 0.8 else 0.8
) )
color2 = (1, 1, 1, 1) if bs.app.vr_mode else (0.7, 0.65, 0.75, 1.0) color2 = (1, 1, 1, 1) if bs.app.env.vr else (0.7, 0.65, 0.75, 1.0)
shadow = 1.0 if bs.app.vr_mode else 0.4 shadow = 1.0 if bs.app.env.vr else 0.4
self._text = bs.NodeActor( self._text = bs.NodeActor(
bs.newnode( bs.newnode(
'text', 'text',

View file

@ -4,8 +4,12 @@
# ba_meta require api 8 # ba_meta require api 8
# Package up various private bits (including stuff from our native
# module) into a nice clean public API.
from _batemplatefs import hello_again_world
from batemplatefs._subsystem import TemplateFsSubsystem from batemplatefs._subsystem import TemplateFsSubsystem
__all__ = [ __all__ = [
'TemplateFsSubsystem', 'TemplateFsSubsystem',
'hello_again_world',
] ]

View file

@ -1,12 +1,10 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Snippets of code for use by the c++ layer.""" """Snippets of code for use by the native layer."""
# (most of these are self-explanatory)
# pylint: disable=missing-function-docstring
from __future__ import annotations from __future__ import annotations
def hello_world() -> None: def hello_world() -> None:
"""The usual example."""
print('HELLO WORLD FROM TemplateFs!') print('HELLO WORLD FROM TemplateFs!')

View file

@ -31,6 +31,7 @@ from babase import (
apptimer, apptimer,
AppTimer, AppTimer,
Call, Call,
can_toggle_fullscreen,
charstr, charstr,
clipboard_is_supported, clipboard_is_supported,
clipboard_set_text, clipboard_set_text,
@ -52,7 +53,6 @@ from babase import (
get_string_width, get_string_width,
get_type_name, get_type_name,
getclass, getclass,
has_gamma_control,
have_permission, have_permission,
in_logic_thread, in_logic_thread,
increment_analytics_count, increment_analytics_count,
@ -76,6 +76,8 @@ from babase import (
set_low_level_config_value, set_low_level_config_value,
set_ui_input_device, set_ui_input_device,
SpecialChar, SpecialChar,
supports_max_fps,
supports_vsync,
timestring, timestring,
UIScale, UIScale,
unlock_all_input, unlock_all_input,
@ -136,6 +138,7 @@ __all__ = [
'buttonwidget', 'buttonwidget',
'Call', 'Call',
'can_show_ad', 'can_show_ad',
'can_toggle_fullscreen',
'charstr', 'charstr',
'checkboxwidget', 'checkboxwidget',
'clipboard_is_supported', 'clipboard_is_supported',
@ -165,7 +168,6 @@ __all__ = [
'getmesh', 'getmesh',
'getsound', 'getsound',
'gettexture', 'gettexture',
'has_gamma_control',
'has_video_ads', 'has_video_ads',
'have_incentivized_ad', 'have_incentivized_ad',
'have_permission', 'have_permission',
@ -205,6 +207,8 @@ __all__ = [
'show_online_score_ui', 'show_online_score_ui',
'Sound', 'Sound',
'SpecialChar', 'SpecialChar',
'supports_max_fps',
'supports_vsync',
'Texture', 'Texture',
'textwidget', 'textwidget',
'timestring', 'timestring',

View file

@ -55,7 +55,8 @@ class UIV1Subsystem(babase.AppSubsystem):
self.have_party_queue_window = False self.have_party_queue_window = False
self.cleanupchecks: list[UICleanupCheck] = [] self.cleanupchecks: list[UICleanupCheck] = []
self.upkeeptimer: babase.AppTimer | None = None self.upkeeptimer: babase.AppTimer | None = None
self.use_toolbars = env.get('toolbar_test', True) self.use_toolbars = _bauiv1.toolbar_test()
self.title_color = (0.72, 0.7, 0.75) self.title_color = (0.72, 0.7, 0.75)
self.heading_color = (0.72, 0.7, 0.75) self.heading_color = (0.72, 0.7, 0.75)
self.infotextcolor = (0.7, 0.9, 0.7) self.infotextcolor = (0.7, 0.9, 0.7)

View file

@ -241,3 +241,35 @@ def ui_upkeep() -> None:
else: else:
remainingchecks.append(check) remainingchecks.append(check)
ui.cleanupchecks = remainingchecks ui.cleanupchecks = remainingchecks
class TextWidgetStringEditAdapter(babase.StringEditAdapter):
"""A StringEditAdapter subclass for editing our text widgets."""
def __init__(self, text_widget: bauiv1.Widget) -> None:
self.widget = text_widget
# Ugly hacks to pull values from widgets. Really need to clean
# up that api.
description: Any = _bauiv1.textwidget(query_description=text_widget)
assert isinstance(description, str)
initial_text: Any = _bauiv1.textwidget(query=text_widget)
assert isinstance(initial_text, str)
max_length: Any = _bauiv1.textwidget(query_max_chars=text_widget)
assert isinstance(max_length, int)
screen_space_center = text_widget.get_screen_space_center()
super().__init__(
description, initial_text, max_length, screen_space_center
)
def _do_apply(self, new_text: str) -> None:
if self.widget:
_bauiv1.textwidget(
edit=self.widget, text=new_text, adapter_finished=True
)
def _do_cancel(self) -> None:
if self.widget:
_bauiv1.textwidget(edit=self.widget, adapter_finished=True)

View file

@ -15,27 +15,28 @@ import _bauiv1
from bauiv1._uitypes import Window from bauiv1._uitypes import Window
if TYPE_CHECKING: if TYPE_CHECKING:
from babase import StringEditAdapter
import bauiv1 as bui import bauiv1 as bui
class OnScreenKeyboardWindow(Window): class OnScreenKeyboardWindow(Window):
"""Simple built-in on-screen keyboard.""" """Simple built-in on-screen keyboard."""
def __init__(self, textwidget: bui.Widget, label: str, max_chars: int): def __init__(self, adapter: StringEditAdapter):
self._target_text = textwidget self._adapter = adapter
self._width = 700 self._width = 700
self._height = 400 self._height = 400
assert babase.app.classic is not None assert babase.app.classic is not None
uiscale = babase.app.ui_v1.uiscale uiscale = babase.app.ui_v1.uiscale
top_extra = 20 if uiscale is babase.UIScale.SMALL else 0 top_extra = 20 if uiscale is babase.UIScale.SMALL else 0
super().__init__( super().__init__(
root_widget=_bauiv1.containerwidget( root_widget=_bauiv1.containerwidget(
parent=_bauiv1.get_special_widget('overlay_stack'), parent=_bauiv1.get_special_widget('overlay_stack'),
size=(self._width, self._height + top_extra), size=(self._width, self._height + top_extra),
transition='in_scale', transition='in_scale',
scale_origin_stack_offset=( scale_origin_stack_offset=adapter.screen_space_center,
self._target_text.get_screen_space_center()
),
scale=( scale=(
2.0 2.0
if uiscale is babase.UIScale.SMALL if uiscale is babase.UIScale.SMALL
@ -69,7 +70,7 @@ class OnScreenKeyboardWindow(Window):
position=(self._width * 0.5, self._height - 41), position=(self._width * 0.5, self._height - 41),
size=(0, 0), size=(0, 0),
scale=0.95, scale=0.95,
text=label, text=adapter.description,
maxwidth=self._width - 140, maxwidth=self._width - 140,
color=babase.app.ui_v1.title_color, color=babase.app.ui_v1.title_color,
h_align='center', h_align='center',
@ -79,8 +80,8 @@ class OnScreenKeyboardWindow(Window):
self._text_field = _bauiv1.textwidget( self._text_field = _bauiv1.textwidget(
parent=self._root_widget, parent=self._root_widget,
position=(70, self._height - 116), position=(70, self._height - 116),
max_chars=max_chars, max_chars=adapter.max_length,
text=cast(str, _bauiv1.textwidget(query=self._target_text)), text=adapter.initial_text,
on_return_press_call=self._done, on_return_press_call=self._done,
autoselect=True, autoselect=True,
size=(self._width - 140, 55), size=(self._width - 140, 55),
@ -436,13 +437,12 @@ class OnScreenKeyboardWindow(Window):
self._refresh() self._refresh()
def _cancel(self) -> None: def _cancel(self) -> None:
self._adapter.cancel()
_bauiv1.getsound('swish').play() _bauiv1.getsound('swish').play()
_bauiv1.containerwidget(edit=self._root_widget, transition='out_scale') _bauiv1.containerwidget(edit=self._root_widget, transition='out_scale')
def _done(self) -> None: def _done(self) -> None:
_bauiv1.containerwidget(edit=self._root_widget, transition='out_scale') _bauiv1.containerwidget(edit=self._root_widget, transition='out_scale')
if self._target_text: self._adapter.apply(
_bauiv1.textwidget( cast(str, _bauiv1.textwidget(query=self._text_field))
edit=self._target_text,
text=cast(str, _bauiv1.textwidget(query=self._text_field)),
) )

View file

@ -141,7 +141,7 @@ class AccountViewerWindow(PopupWindow):
bui.app.classic.master_server_v1_get( bui.app.classic.master_server_v1_get(
'bsAccountInfo', 'bsAccountInfo',
{ {
'buildNumber': bui.app.build_number, 'buildNumber': bui.app.env.build_number,
'accountID': self._account_id, 'accountID': self._account_id,
'profileID': self._profile_id, 'profileID': self._profile_id,
}, },

View file

@ -11,7 +11,7 @@ class ConfigErrorWindow(bui.Window):
"""Window for dealing with a broken config.""" """Window for dealing with a broken config."""
def __init__(self) -> None: def __init__(self) -> None:
self._config_file_path = bui.app.config_file_path self._config_file_path = bui.app.env.config_file_path
width = 800 width = 800
super().__init__( super().__init__(
bui.containerwidget(size=(width, 400), transition='in_right') bui.containerwidget(size=(width, 400), transition='in_right')

View file

@ -197,9 +197,19 @@ class QuitWindow:
time=0.2, time=0.2,
endcall=lambda: bui.quit(soft=True, back=self._back), 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() bui.lock_all_input()
# Unlock and fade back in shortly.. just in case something goes wrong # On systems supporting soft-quit, unlock and fade back in shortly
# (or on android where quit just backs out of our activity and # (soft-quit basically just backgrounds/hides the app).
# we may come back) if bui.app.env.supports_soft_quit:
bui.apptimer(0.3, bui.unlock_all_input) # 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

@ -350,7 +350,7 @@ class CoopBrowserWindow(bui.Window):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@staticmethod @staticmethod
def _preload_modules() -> None: def _preload_modules() -> None:
"""Preload modules we use (called in bg thread).""" """Preload modules we use; avoids hitches (called in bg thread)."""
import bauiv1lib.purchase as _unused1 import bauiv1lib.purchase as _unused1
import bauiv1lib.coop.gamebutton as _unused2 import bauiv1lib.coop.gamebutton as _unused2
import bauiv1lib.confirm as _unused3 import bauiv1lib.confirm as _unused3

View file

@ -212,7 +212,10 @@ class CreditsListWindow(bui.Window):
try: try:
with open( with open(
os.path.join( os.path.join(
bui.app.data_directory, 'ba_data', 'data', 'langdata.json' bui.app.env.data_directory,
'ba_data',
'data',
'langdata.json',
), ),
encoding='utf-8', encoding='utf-8',
) as infile: ) as infile:

View file

@ -15,7 +15,7 @@ def ask_for_rating() -> bui.Widget | None:
subplatform = app.classic.subplatform subplatform = app.classic.subplatform
# FIXME: should whitelist platforms we *do* want this for. # FIXME: should whitelist platforms we *do* want this for.
if bui.app.test_build: if bui.app.env.test:
return None return None
if not ( if not (
platform == 'mac' platform == 'mac'

View file

@ -32,11 +32,7 @@ class AboutGatherTab(GatherTab):
plus = bui.app.plus plus = bui.app.plus
assert plus is not None assert plus is not None
party_button_label = ( party_button_label = bui.charstr(bui.SpecialChar.TOP_BUTTON)
'X'
if bui.app.iircade_mode
else bui.charstr(bui.SpecialChar.TOP_BUTTON)
)
message = bui.Lstr( message = bui.Lstr(
resource='gatherWindow.aboutDescriptionText', resource='gatherWindow.aboutDescriptionText',
subs=[ subs=[
@ -47,7 +43,7 @@ class AboutGatherTab(GatherTab):
# Let's not talk about sharing in vr-mode; its tricky to fit more # Let's not talk about sharing in vr-mode; its tricky to fit more
# than one head in a VR-headset ;-) # than one head in a VR-headset ;-)
if not bui.app.vr_mode: if not bui.app.env.vr:
message = bui.Lstr( message = bui.Lstr(
value='${A}\n\n${B}', value='${A}\n\n${B}',
subs=[ subs=[

View file

@ -1010,7 +1010,7 @@ class ManualGatherTab(GatherTab):
self._t_accessible_extra = t_accessible_extra self._t_accessible_extra = t_accessible_extra
bui.app.classic.master_server_v1_get( bui.app.classic.master_server_v1_get(
'bsAccessCheck', 'bsAccessCheck',
{'b': bui.app.build_number}, {'b': bui.app.env.build_number},
callback=bui.WeakCall(self._on_accessible_response), callback=bui.WeakCall(self._on_accessible_response),
) )

View file

@ -1169,7 +1169,7 @@ class PublicGatherTab(GatherTab):
plus.add_v1_account_transaction( plus.add_v1_account_transaction(
{ {
'type': 'PUBLIC_PARTY_QUERY', 'type': 'PUBLIC_PARTY_QUERY',
'proto': bui.app.protocol_version, 'proto': bs.protocol_version(),
'lang': bui.app.lang.language, 'lang': bui.app.lang.language,
}, },
callback=bui.WeakCall(self._on_public_party_query_result), callback=bui.WeakCall(self._on_public_party_query_result),
@ -1327,7 +1327,7 @@ class PublicGatherTab(GatherTab):
) )
bui.app.classic.master_server_v1_get( bui.app.classic.master_server_v1_get(
'bsAccessCheck', 'bsAccessCheck',
{'b': bui.app.build_number}, {'b': bui.app.env.build_number},
callback=bui.WeakCall(self._on_public_party_accessible_response), callback=bui.WeakCall(self._on_public_party_accessible_response),
) )

View file

@ -621,7 +621,7 @@ class GetCurrencyWindow(bui.Window):
app = bui.app app = bui.app
assert app.classic is not None assert app.classic is not None
if ( if (
app.test_build app.env.test
or ( or (
app.classic.platform == 'android' app.classic.platform == 'android'
and app.classic.subplatform in ['oculus', 'cardboard'] and app.classic.subplatform in ['oculus', 'cardboard']
@ -664,8 +664,8 @@ class GetCurrencyWindow(bui.Window):
'item': item, 'item': item,
'platform': app.classic.platform, 'platform': app.classic.platform,
'subplatform': app.classic.subplatform, 'subplatform': app.classic.subplatform,
'version': app.version, 'version': app.env.version,
'buildNumber': app.build_number, 'buildNumber': app.env.build_number,
}, },
callback=bui.WeakCall(self._purchase_check_result, item), callback=bui.WeakCall(self._purchase_check_result, item),
) )

View file

@ -196,57 +196,9 @@ class HelpWindow(bui.Window):
texture=logo_tex, texture=logo_tex,
) )
force_test = False
app = bui.app app = bui.app
assert app.classic is not None assert app.classic is not None
if (
app.classic.platform == 'android'
and app.classic.subplatform == 'alibaba'
) or force_test:
v -= 120.0
txtv = (
'\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\x80\xe4\xb8\xaa\xe5\x8f\xaf'
'\xe4\xbb\xa5\xe5\x92\x8c\xe5\xae\xb6\xe4\xba\xba\xe6\x9c\x8b'
'\xe5\x8f\x8b\xe4\xb8\x80\xe8\xb5\xb7\xe7\x8e\xa9\xe7\x9a\x84'
'\xe6\xb8\xb8\xe6\x88\x8f,\xe5\x90\x8c\xe6\x97\xb6\xe6\x94\xaf'
'\xe6\x8c\x81\xe8\x81\x94 \xe2\x80\xa8\xe7\xbd\x91\xe5\xaf\xb9'
'\xe6\x88\x98\xe3\x80\x82\n'
'\xe5\xa6\x82\xe6\xb2\xa1\xe6\x9c\x89\xe6\xb8\xb8\xe6\x88\x8f'
'\xe6\x89\x8b\xe6\x9f\x84,\xe5\x8f\xaf\xe4\xbb\xa5\xe4\xbd\xbf'
'\xe7\x94\xa8\xe7\xa7\xbb\xe5\x8a\xa8\xe8\xae\xbe\xe5\xa4\x87'
'\xe6\x89\xab\xe7\xa0\x81\xe4\xb8\x8b\xe8\xbd\xbd\xe2\x80\x9c'
'\xe9\x98\xbf\xe9\x87\x8c\xc2'
'\xa0TV\xc2\xa0\xe5\x8a\xa9\xe6\x89'
'\x8b\xe2\x80\x9d\xe7\x94\xa8 \xe6\x9d\xa5\xe4\xbb\xa3\xe6\x9b'
'\xbf\xe5\xa4\x96\xe8\xae\xbe\xe3\x80\x82\n'
'\xe6\x9c\x80\xe5\xa4\x9a\xe6\x94\xaf\xe6\x8c\x81\xe6\x8e\xa5'
'\xe5\x85\xa5\xc2\xa08\xc2\xa0\xe4\xb8\xaa\xe5\xa4\x96\xe8'
'\xae\xbe'
)
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=self._sub_width * 0.9,
position=(self._sub_width * 0.5, v - 180),
text=txtv,
)
bui.imagewidget(
parent=self._subcontainer,
position=(self._sub_width - 320, v - 120),
size=(200, 200),
texture=bui.gettexture('aliControllerQR'),
)
bui.imagewidget(
parent=self._subcontainer,
position=(90, v - 130),
size=(210, 210),
texture=bui.gettexture('multiplayerExamples'),
)
v -= 120.0
else:
v -= spacing * 50.0 v -= spacing * 50.0
txt = bui.Lstr(resource=self._r + '.someDaysText').evaluate() txt = bui.Lstr(resource=self._r + '.someDaysText').evaluate()
bui.textwidget( bui.textwidget(
@ -263,9 +215,7 @@ class HelpWindow(bui.Window):
) )
v -= spacing * 25.0 + getres(self._r + '.someDaysExtraSpace') v -= spacing * 25.0 + getres(self._r + '.someDaysExtraSpace')
txt_scale = 0.66 txt_scale = 0.66
txt = bui.Lstr( txt = bui.Lstr(resource=self._r + '.orPunchingSomethingText').evaluate()
resource=self._r + '.orPunchingSomethingText'
).evaluate()
bui.textwidget( bui.textwidget(
parent=self._subcontainer, parent=self._subcontainer,
position=(h, v), position=(h, v),
@ -278,9 +228,7 @@ class HelpWindow(bui.Window):
v_align='center', v_align='center',
flatness=1.0, flatness=1.0,
) )
v -= spacing * 27.0 + getres( v -= spacing * 27.0 + getres(self._r + '.orPunchingSomethingExtraSpace')
self._r + '.orPunchingSomethingExtraSpace'
)
txt_scale = 1.0 txt_scale = 1.0
txt = bui.Lstr( txt = bui.Lstr(
resource=self._r + '.canHelpText', resource=self._r + '.canHelpText',
@ -353,7 +301,7 @@ class HelpWindow(bui.Window):
v -= spacing * 45.0 v -= spacing * 45.0
txt = ( txt = (
bui.Lstr(resource=self._r + '.devicesText').evaluate() bui.Lstr(resource=self._r + '.devicesText').evaluate()
if app.vr_mode if app.env.vr
else bui.Lstr(resource=self._r + '.controllersText').evaluate() else bui.Lstr(resource=self._r + '.controllersText').evaluate()
) )
txt_scale = 0.74 txt_scale = 0.74
@ -372,12 +320,8 @@ class HelpWindow(bui.Window):
) )
txt_scale = 0.7 txt_scale = 0.7
if not app.vr_mode: if not app.env.vr:
infotxt = ( infotxt = '.controllersInfoText'
'.controllersInfoTextRemoteOnly'
if app.iircade_mode
else '.controllersInfoText'
)
txt = bui.Lstr( txt = bui.Lstr(
resource=self._r + infotxt, resource=self._r + infotxt,
fallback_resource=self._r + '.controllersInfoText', fallback_resource=self._r + '.controllersInfoText',

View file

@ -88,7 +88,7 @@ class KioskWindow(bui.Window):
resource='demoText', resource='demoText',
fallback_resource='mainMenu.demoMenuText', fallback_resource='mainMenu.demoMenuText',
) )
if bui.app.demo_mode if bui.app.env.demo
else 'ARCADE' else 'ARCADE'
), ),
flatness=1.0, flatness=1.0,
@ -332,7 +332,7 @@ class KioskWindow(bui.Window):
self._b4 = self._b5 = self._b6 = None self._b4 = self._b5 = self._b6 = None
self._b7: bui.Widget | None self._b7: bui.Widget | None
if bui.app.arcade_mode: if bui.app.env.arcade:
self._b7 = bui.buttonwidget( self._b7 = bui.buttonwidget(
parent=self._root_widget, parent=self._root_widget,
autoselect=True, autoselect=True,

View file

@ -50,9 +50,8 @@ class MainMenuWindow(bui.Window):
) )
# Grab this stuff in case it changes. # Grab this stuff in case it changes.
self._is_demo = bui.app.demo_mode self._is_demo = bui.app.env.demo
self._is_arcade = bui.app.arcade_mode self._is_arcade = bui.app.env.arcade
self._is_iircade = bui.app.iircade_mode
self._tdelay = 0.0 self._tdelay = 0.0
self._t_delay_inc = 0.02 self._t_delay_inc = 0.02
@ -93,7 +92,7 @@ class MainMenuWindow(bui.Window):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@staticmethod @staticmethod
def _preload_modules() -> None: def _preload_modules() -> None:
"""Preload modules we use (called in bg thread).""" """Preload modules we use; avoids hitches (called in bg thread)."""
import bauiv1lib.getremote as _unused import bauiv1lib.getremote as _unused
import bauiv1lib.confirm as _unused2 import bauiv1lib.confirm as _unused2
import bauiv1lib.store.button as _unused3 import bauiv1lib.store.button as _unused3
@ -118,7 +117,7 @@ class MainMenuWindow(bui.Window):
force_test = False force_test = False
bs.get_local_active_input_devices_count() bs.get_local_active_input_devices_count()
if ( if (
(app.on_tv or app.classic.platform == 'mac') (app.env.tv or app.classic.platform == 'mac')
and bui.app.config.get('launchCount', 0) <= 1 and bui.app.config.get('launchCount', 0) <= 1
) or force_test: ) or force_test:
@ -220,8 +219,8 @@ class MainMenuWindow(bui.Window):
self._have_store_button = not self._in_game self._have_store_button = not self._in_game
self._have_settings_button = ( self._have_settings_button = (
not self._in_game or not app.toolbar_test not self._in_game or not app.ui_v1.use_toolbars
) and not (self._is_demo or self._is_arcade or self._is_iircade) ) and not (self._is_demo or self._is_arcade)
self._input_device = input_device = bs.get_ui_input_device() self._input_device = input_device = bs.get_ui_input_device()
@ -618,7 +617,7 @@ class MainMenuWindow(bui.Window):
) )
) )
# In kiosk mode, provide a button to get back to the kiosk menu. # In kiosk mode, provide a button to get back to the kiosk menu.
if bui.app.demo_mode or bui.app.arcade_mode: if bui.app.env.demo or bui.app.env.arcade:
h, v, scale = positions[self._p_index] h, v, scale = positions[self._p_index]
this_b_width = self._button_width * 0.4 * scale this_b_width = self._button_width * 0.4 * scale
demo_menu_delay = ( demo_menu_delay = (
@ -635,7 +634,7 @@ class MainMenuWindow(bui.Window):
textcolor=(0.7, 0.8, 0.7), textcolor=(0.7, 0.8, 0.7),
label=bui.Lstr( label=bui.Lstr(
resource='modeArcadeText' resource='modeArcadeText'
if bui.app.arcade_mode if bui.app.env.arcade
else 'modeDemoText' else 'modeDemoText'
), ),
transition_delay=demo_menu_delay, transition_delay=demo_menu_delay,

View file

@ -513,7 +513,7 @@ class PlayWindow(bui.Window):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@staticmethod @staticmethod
def _preload_modules() -> None: def _preload_modules() -> None:
"""Preload modules we use (called in bg thread).""" """Preload modules we use; avoids hitches (called in bg thread)."""
import bauiv1lib.mainmenu as _unused1 import bauiv1lib.mainmenu as _unused1
import bauiv1lib.account as _unused2 import bauiv1lib.account as _unused2
import bauiv1lib.coop.browser as _unused3 import bauiv1lib.coop.browser as _unused3

View file

@ -34,7 +34,7 @@ class PopupWindow:
focus_size = size focus_size = size
# In vr mode we can't have windows going outside the screen. # In vr mode we can't have windows going outside the screen.
if bui.app.vr_mode: if bui.app.env.vr:
focus_size = size focus_size = size
focus_position = (0, 0) focus_position = (0, 0)

View file

@ -718,11 +718,13 @@ class EditProfileWindow(bui.Window):
else '???' else '???'
) )
if len(name) > 10 and not (self._global or self._is_account_profile): if len(name) > 10 and not (self._global or self._is_account_profile):
name = name.strip()
display_name = (name[:10] + '...') if len(name) > 10 else name
bui.textwidget( bui.textwidget(
edit=self._clipped_name_text, edit=self._clipped_name_text,
text=bui.Lstr( text=bui.Lstr(
resource='inGameClippedNameText', resource='inGameClippedNameText',
subs=[('${NAME}', name[:10] + '...')], subs=[('${NAME}', display_name)],
), ),
) )
else: else:

View file

@ -155,7 +155,7 @@ class ProfileUpgradeWindow(bui.Window):
bui.app.classic.master_server_v1_get( bui.app.classic.master_server_v1_get(
'bsGlobalProfileCheck', 'bsGlobalProfileCheck',
{'name': self._name, 'b': bui.app.build_number}, {'name': self._name, 'b': bui.app.env.build_number},
callback=bui.WeakCall(self._profile_check_result), callback=bui.WeakCall(self._profile_check_result),
) )
self._cost = plus.get_v1_account_misc_read_val( self._cost = plus.get_v1_account_misc_read_val(

View file

@ -16,7 +16,7 @@ if TYPE_CHECKING:
class AdvancedSettingsWindow(bui.Window): class AdvancedSettingsWindow(bui.Window):
"""Window for editing advanced game settings.""" """Window for editing advanced app settings."""
def __init__( def __init__(
self, self,
@ -61,6 +61,7 @@ class AdvancedSettingsWindow(bui.Window):
self._spacing = 32 self._spacing = 32
self._menu_open = False self._menu_open = False
top_extra = 10 if uiscale is bui.UIScale.SMALL else 0 top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
super().__init__( super().__init__(
root_widget=bui.containerwidget( root_widget=bui.containerwidget(
size=(self._width, self._height + top_extra), size=(self._width, self._height + top_extra),
@ -88,14 +89,12 @@ class AdvancedSettingsWindow(bui.Window):
# In vr-mode, the internal keyboard is currently the *only* option, # In vr-mode, the internal keyboard is currently the *only* option,
# so no need to show this. # so no need to show this.
self._show_always_use_internal_keyboard = ( self._show_always_use_internal_keyboard = not app.env.vr
not app.vr_mode and not app.iircade_mode
)
self._scroll_width = self._width - (100 + 2 * x_inset) self._scroll_width = self._width - (100 + 2 * x_inset)
self._scroll_height = self._height - 115.0 self._scroll_height = self._height - 115.0
self._sub_width = self._scroll_width * 0.95 self._sub_width = self._scroll_width * 0.95
self._sub_height = 724.0 self._sub_height = 766.0
if self._show_always_use_internal_keyboard: if self._show_always_use_internal_keyboard:
self._sub_height += 62 self._sub_height += 62
@ -104,7 +103,7 @@ class AdvancedSettingsWindow(bui.Window):
if self._show_disable_gyro: if self._show_disable_gyro:
self._sub_height += 42 self._sub_height += 42
self._do_vr_test_button = app.vr_mode self._do_vr_test_button = app.env.vr
self._do_net_test_button = True self._do_net_test_button = True
self._extra_button_spacing = self._spacing * 2.5 self._extra_button_spacing = self._spacing * 2.5
@ -180,14 +179,14 @@ class AdvancedSettingsWindow(bui.Window):
# Fetch the list of completed languages. # Fetch the list of completed languages.
bui.app.classic.master_server_v1_get( bui.app.classic.master_server_v1_get(
'bsLangGetCompleted', 'bsLangGetCompleted',
{'b': app.build_number}, {'b': app.env.build_number},
callback=bui.WeakCall(self._completed_langs_cb), callback=bui.WeakCall(self._completed_langs_cb),
) )
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@staticmethod @staticmethod
def _preload_modules() -> None: def _preload_modules() -> None:
"""Preload modules we use (called in bg thread).""" """Preload modules we use; avoids hitches (called in bg thread)."""
from babase import modutils as _unused2 from babase import modutils as _unused2
from bauiv1lib import config as _unused1 from bauiv1lib import config as _unused1
from bauiv1lib.settings import vrtesting as _unused3 from bauiv1lib.settings import vrtesting as _unused3
@ -244,6 +243,7 @@ class AdvancedSettingsWindow(bui.Window):
# Don't rebuild if the menu is open or if our language and # Don't rebuild if the menu is open or if our language and
# language-list hasn't changed. # language-list hasn't changed.
# NOTE - although we now support widgets updating their own # NOTE - although we now support widgets updating their own
# translations, we still change the label formatting on the language # translations, we still change the label formatting on the language
# menu based on the language so still need this. ...however we could # menu based on the language so still need this. ...however we could
@ -324,7 +324,10 @@ class AdvancedSettingsWindow(bui.Window):
with open( with open(
os.path.join( os.path.join(
bui.app.data_directory, 'ba_data', 'data', 'langdata.json' bui.app.env.data_directory,
'ba_data',
'data',
'langdata.json',
), ),
encoding='utf-8', encoding='utf-8',
) as infile: ) as infile:
@ -473,6 +476,19 @@ class AdvancedSettingsWindow(bui.Window):
maxwidth=430, maxwidth=430,
) )
v -= 42
self._show_dev_console_button_check_box = ConfigCheckBox(
parent=self._subcontainer,
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Show Dev Console Button',
displayname=bui.Lstr(
resource=f'{self._r}.showDevConsoleButtonText'
),
scale=1.0,
maxwidth=430,
)
v -= 42 v -= 42
self._disable_camera_shake_check_box = ConfigCheckBox( self._disable_camera_shake_check_box = ConfigCheckBox(
parent=self._subcontainer, parent=self._subcontainer,

View file

@ -224,7 +224,7 @@ class AllSettingsWindow(bui.Window):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@staticmethod @staticmethod
def _preload_modules() -> None: def _preload_modules() -> None:
"""Preload modules we use (called in bg thread).""" """Preload modules we use; avoids hitches (called in bg thread)."""
import bauiv1lib.mainmenu as _unused1 import bauiv1lib.mainmenu as _unused1
import bauiv1lib.settings.controls as _unused2 import bauiv1lib.settings.controls as _unused2
import bauiv1lib.settings.graphics as _unused3 import bauiv1lib.settings.graphics as _unused3

View file

@ -47,14 +47,14 @@ class ControlsSettingsWindow(bui.Window):
space_height = spacing * 0.3 space_height = spacing * 0.3
# FIXME: should create vis settings in platform for these, # FIXME: should create vis settings under platform or app-adapter
# not hard code them here. # to determine whether to show this stuff; not hard code it.
show_gamepads = False show_gamepads = False
platform = app.classic.platform platform = app.classic.platform
subplatform = app.classic.subplatform subplatform = app.classic.subplatform
non_vr_windows = platform == 'windows' and ( non_vr_windows = platform == 'windows' and (
subplatform != 'oculus' or not app.vr_mode subplatform != 'oculus' or not app.env.vr
) )
if platform in ('linux', 'android', 'mac') or non_vr_windows: if platform in ('linux', 'android', 'mac') or non_vr_windows:
show_gamepads = True show_gamepads = True
@ -74,7 +74,7 @@ class ControlsSettingsWindow(bui.Window):
if bs.getinputdevice('Keyboard', '#1', doraise=False) is not None: if bs.getinputdevice('Keyboard', '#1', doraise=False) is not None:
show_keyboard = True show_keyboard = True
height += spacing height += spacing
show_keyboard_p2 = False if app.vr_mode else show_keyboard show_keyboard_p2 = False if app.env.vr else show_keyboard
if show_keyboard_p2: if show_keyboard_p2:
height += spacing height += spacing
@ -91,7 +91,7 @@ class ControlsSettingsWindow(bui.Window):
# On windows (outside of oculus/vr), show an option to disable xinput. # On windows (outside of oculus/vr), show an option to disable xinput.
show_xinput_toggle = False show_xinput_toggle = False
if platform == 'windows' and not app.vr_mode: if platform == 'windows' and not app.env.vr:
show_xinput_toggle = True show_xinput_toggle = True
# On mac builds, show an option to switch between generic and # On mac builds, show an option to switch between generic and
@ -352,6 +352,7 @@ class ControlsSettingsWindow(bui.Window):
maxwidth=width * 0.8, maxwidth=width * 0.8,
) )
v -= spacing * 1.5 v -= spacing * 1.5
self._restore_state() self._restore_state()
def _set_mac_controller_subsystem(self, val: str) -> None: def _set_mac_controller_subsystem(self, val: str) -> None:

View file

@ -829,7 +829,7 @@ class GamepadSettingsWindow(bui.Window):
'controllerConfig', 'controllerConfig',
{ {
'ua': classic.legacy_user_agent_string, 'ua': classic.legacy_user_agent_string,
'b': bui.app.build_number, 'b': bui.app.env.build_number,
'name': self._name, 'name': self._name,
'inputMapHash': inputhash, 'inputMapHash': inputhash,
'config': dst2, 'config': dst2,

View file

@ -91,7 +91,7 @@ class GamepadAdvancedSettingsWindow(bui.Window):
self._sub_height = ( self._sub_height = (
940 if self._parent_window.get_is_secondary() else 1040 940 if self._parent_window.get_is_secondary() else 1040
) )
if app.vr_mode: if app.env.vr:
self._sub_height += 50 self._sub_height += 50
self._scrollwidget = bui.scrollwidget( self._scrollwidget = bui.scrollwidget(
parent=self._root_widget, parent=self._root_widget,
@ -183,7 +183,7 @@ class GamepadAdvancedSettingsWindow(bui.Window):
) )
# in vr mode, allow assigning a reset-view button # in vr mode, allow assigning a reset-view button
if app.vr_mode: if app.env.vr:
v -= 50 v -= 50
self._capture_button( self._capture_button(
pos=(h2, v), pos=(h2, v),

View file

@ -4,12 +4,15 @@
from __future__ import annotations from __future__ import annotations
import logging from typing import TYPE_CHECKING, cast
from bauiv1lib.popup import PopupMenu from bauiv1lib.popup import PopupMenu
from bauiv1lib.config import ConfigCheckBox, ConfigNumberEdit from bauiv1lib.config import ConfigCheckBox
import bauiv1 as bui import bauiv1 as bui
if TYPE_CHECKING:
from typing import Any
class GraphicsSettingsWindow(bui.Window): class GraphicsSettingsWindow(bui.Window):
"""Window for graphics settings.""" """Window for graphics settings."""
@ -42,26 +45,26 @@ class GraphicsSettingsWindow(bui.Window):
uiscale = app.ui_v1.uiscale uiscale = app.ui_v1.uiscale
width = 450.0 width = 450.0
height = 302.0 height = 302.0
self._max_fps_dirty = False
self._last_max_fps_set_time = bui.apptime()
self._last_max_fps_str = ''
self._show_fullscreen = False self._show_fullscreen = False
fullscreen_spacing_top = spacing * 0.2 fullscreen_spacing_top = spacing * 0.2
fullscreen_spacing = spacing * 1.2 fullscreen_spacing = spacing * 1.2
if uiscale == bui.UIScale.LARGE and app.classic.platform != 'android': if bui.can_toggle_fullscreen():
self._show_fullscreen = True self._show_fullscreen = True
height += fullscreen_spacing + fullscreen_spacing_top height += fullscreen_spacing + fullscreen_spacing_top
show_gamma = False show_vsync = bui.supports_vsync()
gamma_spacing = spacing * 1.3 show_tv_mode = not bui.app.env.vr
if bui.has_gamma_control():
show_gamma = True
height += gamma_spacing
show_vsync = False show_max_fps = bui.supports_max_fps()
if app.classic.platform == 'mac': if show_max_fps:
show_vsync = True height += 50
show_resolution = True show_resolution = True
if app.vr_mode: if app.env.vr:
show_resolution = ( show_resolution = (
app.classic.platform == 'android' app.classic.platform == 'android'
and app.classic.subplatform == 'cardboard' and app.classic.subplatform == 'cardboard'
@ -70,7 +73,7 @@ class GraphicsSettingsWindow(bui.Window):
assert bui.app.classic is not None assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale uiscale = bui.app.ui_v1.uiscale
base_scale = ( base_scale = (
2.4 2.0
if uiscale is bui.UIScale.SMALL if uiscale is bui.UIScale.SMALL
else 1.5 else 1.5
if uiscale is bui.UIScale.MEDIUM if uiscale is bui.UIScale.MEDIUM
@ -91,19 +94,20 @@ class GraphicsSettingsWindow(bui.Window):
) )
) )
btn = bui.buttonwidget( back_button = bui.buttonwidget(
parent=self._root_widget, parent=self._root_widget,
position=(35, height - 50), position=(35, height - 50),
size=(120, 60), # size=(120, 60),
size=(60, 60),
scale=0.8, scale=0.8,
text_scale=1.2, text_scale=1.2,
autoselect=True, autoselect=True,
label=bui.Lstr(resource='backText'), label=bui.charstr(bui.SpecialChar.BACK),
button_type='back', button_type='backSmall',
on_activate_call=self._back, on_activate_call=self._back,
) )
bui.containerwidget(edit=self._root_widget, cancel_button=btn) bui.containerwidget(edit=self._root_widget, cancel_button=back_button)
bui.textwidget( bui.textwidget(
parent=self._root_widget, parent=self._root_widget,
@ -115,15 +119,7 @@ class GraphicsSettingsWindow(bui.Window):
v_align='top', v_align='top',
) )
bui.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 60),
label=bui.charstr(bui.SpecialChar.BACK),
)
self._fullscreen_checkbox: bui.Widget | None = None self._fullscreen_checkbox: bui.Widget | None = None
self._gamma_controls: ConfigNumberEdit | None = None
if self._show_fullscreen: if self._show_fullscreen:
v -= fullscreen_spacing_top v -= fullscreen_spacing_top
self._fullscreen_checkbox = ConfigCheckBox( self._fullscreen_checkbox = ConfigCheckBox(
@ -149,34 +145,10 @@ class GraphicsSettingsWindow(bui.Window):
self._have_selected_child = True self._have_selected_child = True
v -= fullscreen_spacing v -= fullscreen_spacing
if show_gamma:
self._gamma_controls = gmc = ConfigNumberEdit(
parent=self._root_widget,
position=(90, v),
configkey='Screen Gamma',
displayname=bui.Lstr(resource=self._r + '.gammaText'),
minval=0.1,
maxval=2.0,
increment=0.1,
xoffset=-70,
textscale=0.85,
)
if bui.app.ui_v1.use_toolbars:
bui.widget(
edit=gmc.plusbutton,
right_widget=bui.get_special_widget('party_button'),
)
if not self._have_selected_child:
bui.containerwidget(
edit=self._root_widget, selected_child=gmc.minusbutton
)
self._have_selected_child = True
v -= gamma_spacing
self._selected_color = (0.5, 1, 0.5, 1) self._selected_color = (0.5, 1, 0.5, 1)
self._unselected_color = (0.7, 0.7, 0.7, 1) self._unselected_color = (0.7, 0.7, 0.7, 1)
# quality # Quality
bui.textwidget( bui.textwidget(
parent=self._root_widget, parent=self._root_widget,
position=(60, v), position=(60, v),
@ -208,7 +180,7 @@ class GraphicsSettingsWindow(bui.Window):
on_value_change_call=self._set_quality, on_value_change_call=self._set_quality,
) )
# texture controls # Texture controls
bui.textwidget( bui.textwidget(
parent=self._root_widget, parent=self._root_widget,
position=(230, v), position=(230, v),
@ -244,8 +216,9 @@ class GraphicsSettingsWindow(bui.Window):
h_offs = 0 h_offs = 0
resolution_popup: PopupMenu | None = None
if show_resolution: if show_resolution:
# resolution
bui.textwidget( bui.textwidget(
parent=self._root_widget, parent=self._root_widget,
position=(h_offs + 60, v), position=(h_offs + 60, v),
@ -258,32 +231,17 @@ class GraphicsSettingsWindow(bui.Window):
v_align='center', v_align='center',
) )
# on standard android we have 'Auto', 'Native', and a few # On standard android we have 'Auto', 'Native', and a few
# HD standards # HD standards.
if app.classic.platform == 'android': if app.classic.platform == 'android':
# on cardboard/daydream android we have a few # on cardboard/daydream android we have a few
# render-target-scale options # render-target-scale options
if app.classic.subplatform == 'cardboard': if app.classic.subplatform == 'cardboard':
rawval = bui.app.config.resolve('GVR Render Target Scale')
current_res_cardboard = ( current_res_cardboard = (
str( str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
min(
100,
max(
10,
int(
round(
bui.app.config.resolve(
'GVR Render Target Scale'
) )
* 100.0 resolution_popup = PopupMenu(
)
),
),
)
)
+ '%'
)
PopupMenu(
parent=self._root_widget, parent=self._root_widget,
position=(h_offs + 60, v - 50), position=(h_offs + 60, v - 50),
width=120, width=120,
@ -301,16 +259,16 @@ class GraphicsSettingsWindow(bui.Window):
bui.Lstr(resource='nativeText'), bui.Lstr(resource='nativeText'),
] ]
for res in [1440, 1080, 960, 720, 480]: for res in [1440, 1080, 960, 720, 480]:
# nav bar is 72px so lets allow for that in what # Nav bar is 72px so lets allow for that in what
# choices we show # choices we show.
if native_res[1] >= res - 72: if native_res[1] >= res - 72:
res_str = str(res) + 'p' res_str = f'{res}p'
choices.append(res_str) choices.append(res_str)
choices_display.append(bui.Lstr(value=res_str)) choices_display.append(bui.Lstr(value=res_str))
current_res_android = bui.app.config.resolve( current_res_android = bui.app.config.resolve(
'Resolution (Android)' 'Resolution (Android)'
) )
PopupMenu( resolution_popup = PopupMenu(
parent=self._root_widget, parent=self._root_widget,
position=(h_offs + 60, v - 50), position=(h_offs + 60, v - 50),
width=120, width=120,
@ -325,26 +283,11 @@ class GraphicsSettingsWindow(bui.Window):
# set pixel-scale instead. # set pixel-scale instead.
current_res = bui.get_display_resolution() current_res = bui.get_display_resolution()
if current_res is None: if current_res is None:
rawval = bui.app.config.resolve('Screen Pixel Scale')
current_res2 = ( current_res2 = (
str( str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
min(
100,
max(
10,
int(
round(
bui.app.config.resolve(
'Screen Pixel Scale'
) )
* 100.0 resolution_popup = PopupMenu(
)
),
),
)
)
+ '%'
)
PopupMenu(
parent=self._root_widget, parent=self._root_widget,
position=(h_offs + 60, v - 50), position=(h_offs + 60, v - 50),
width=120, width=120,
@ -355,11 +298,16 @@ class GraphicsSettingsWindow(bui.Window):
) )
else: else:
raise RuntimeError( raise RuntimeError(
'obsolete path; discrete resolutions' 'obsolete code path; discrete resolutions'
' no longer supported' ' no longer supported'
) )
if resolution_popup is not None:
bui.widget(
edit=resolution_popup.get_button(),
left_widget=back_button,
)
# vsync vsync_popup: PopupMenu | None = None
if show_vsync: if show_vsync:
bui.textwidget( bui.textwidget(
parent=self._root_widget, parent=self._root_widget,
@ -372,8 +320,7 @@ class GraphicsSettingsWindow(bui.Window):
h_align='center', h_align='center',
v_align='center', v_align='center',
) )
vsync_popup = PopupMenu(
PopupMenu(
parent=self._root_widget, parent=self._root_widget,
position=(230, v - 50), position=(230, v - 50),
width=150, width=150,
@ -387,8 +334,59 @@ class GraphicsSettingsWindow(bui.Window):
current_choice=bui.app.config.resolve('Vertical Sync'), current_choice=bui.app.config.resolve('Vertical Sync'),
on_value_change_call=self._set_vsync, on_value_change_call=self._set_vsync,
) )
if resolution_popup is not None:
bui.widget(
edit=vsync_popup.get_button(),
left_widget=resolution_popup.get_button(),
)
if resolution_popup is not None and vsync_popup is not None:
bui.widget(
edit=resolution_popup.get_button(),
right_widget=vsync_popup.get_button(),
)
v -= 90 v -= 90
self._max_fps_text: bui.Widget | None = None
if show_max_fps:
v -= 5
bui.textwidget(
parent=self._root_widget,
position=(155, v + 10),
size=(0, 0),
text=bui.Lstr(resource=self._r + '.maxFPSText'),
color=bui.app.ui_v1.heading_color,
scale=0.9,
maxwidth=90,
h_align='right',
v_align='center',
)
max_fps_str = str(bui.app.config.resolve('Max FPS'))
self._last_max_fps_str = max_fps_str
self._max_fps_text = bui.textwidget(
parent=self._root_widget,
position=(170, v - 5),
size=(105, 30),
text=max_fps_str,
max_chars=5,
editable=True,
h_align='left',
v_align='center',
on_return_press_call=self._on_max_fps_return_press,
)
v -= 45
if self._max_fps_text is not None and resolution_popup is not None:
bui.widget(
edit=resolution_popup.get_button(),
down_widget=self._max_fps_text,
)
bui.widget(
edit=self._max_fps_text,
up_widget=resolution_popup.get_button(),
)
fpsc = ConfigCheckBox( fpsc = ConfigCheckBox(
parent=self._root_widget, parent=self._root_widget,
position=(69, v - 6), position=(69, v - 6),
@ -398,9 +396,17 @@ class GraphicsSettingsWindow(bui.Window):
displayname=bui.Lstr(resource=self._r + '.showFPSText'), displayname=bui.Lstr(resource=self._r + '.showFPSText'),
maxwidth=130, maxwidth=130,
) )
if self._max_fps_text is not None:
bui.widget(
edit=self._max_fps_text,
down_widget=fpsc.widget,
)
bui.widget(
edit=fpsc.widget,
up_widget=self._max_fps_text,
)
# (tv mode doesnt apply to vr) if show_tv_mode:
if not bui.app.vr_mode:
tvc = ConfigCheckBox( tvc = ConfigCheckBox(
parent=self._root_widget, parent=self._root_widget,
position=(240, v - 6), position=(240, v - 6),
@ -410,13 +416,8 @@ class GraphicsSettingsWindow(bui.Window):
displayname=bui.Lstr(resource=self._r + '.tvBorderText'), displayname=bui.Lstr(resource=self._r + '.tvBorderText'),
maxwidth=130, maxwidth=130,
) )
# grumble..
bui.widget(edit=fpsc.widget, right_widget=tvc.widget) bui.widget(edit=fpsc.widget, right_widget=tvc.widget)
try: bui.widget(edit=tvc.widget, left_widget=fpsc.widget)
pass
except Exception:
logging.exception('Exception wiring up graphics settings UI.')
v -= spacing v -= spacing
@ -429,6 +430,10 @@ class GraphicsSettingsWindow(bui.Window):
def _back(self) -> None: def _back(self) -> None:
from bauiv1lib.settings import allsettings from bauiv1lib.settings import allsettings
# Applying max-fps takes a few moments. Apply if it hasn't been
# yet.
self._apply_max_fps()
bui.containerwidget( bui.containerwidget(
edit=self._root_widget, transition=self._transition_out edit=self._root_widget, transition=self._transition_out
) )
@ -469,7 +474,60 @@ class GraphicsSettingsWindow(bui.Window):
cfg['Vertical Sync'] = val cfg['Vertical Sync'] = val
cfg.apply_and_commit() cfg.apply_and_commit()
def _on_max_fps_return_press(self) -> None:
self._apply_max_fps()
bui.containerwidget(
edit=self._root_widget, selected_child=cast(bui.Widget, 0)
)
def _apply_max_fps(self) -> None:
if not self._max_fps_dirty or not self._max_fps_text:
return
val: Any = bui.textwidget(query=self._max_fps_text)
assert isinstance(val, str)
# If there's a broken value, replace it with the default.
try:
ival = int(val)
except ValueError:
ival = bui.app.config.default_value('Max FPS')
assert isinstance(ival, int)
# Clamp to reasonable limits (allow -1 to mean no max).
if ival != -1:
ival = max(10, ival)
ival = min(99999, ival)
# Store it to the config.
cfg = bui.app.config
cfg['Max FPS'] = ival
cfg.apply_and_commit()
# Update the display if we changed the value.
if str(ival) != val:
bui.textwidget(edit=self._max_fps_text, text=str(ival))
self._max_fps_dirty = False
def _update_controls(self) -> None: def _update_controls(self) -> None:
if self._max_fps_text is not None:
# Keep track of when the max-fps value changes. Once it
# remains stable for a few moments, apply it.
val: Any = bui.textwidget(query=self._max_fps_text)
assert isinstance(val, str)
if val != self._last_max_fps_str:
# Oop; it changed. Note the time and the fact that we'll
# need to apply it at some point.
self._max_fps_dirty = True
self._last_max_fps_str = val
self._last_max_fps_set_time = bui.apptime()
else:
# If its been stable long enough, apply it.
if (
self._max_fps_dirty
and bui.apptime() - self._last_max_fps_set_time > 1.0
):
self._apply_max_fps()
if self._show_fullscreen: if self._show_fullscreen:
bui.checkboxwidget( bui.checkboxwidget(
edit=self._fullscreen_checkbox, edit=self._fullscreen_checkbox,

View file

@ -301,7 +301,7 @@ class ConfigKeyboardWindow(bui.Window):
{ {
'ua': bui.app.classic.legacy_user_agent_string, 'ua': bui.app.classic.legacy_user_agent_string,
'name': self._name, 'name': self._name,
'b': bui.app.build_number, 'b': bui.app.env.build_number,
'config': dst2, 'config': dst2,
'v': 2, 'v': 2,
}, },

View file

@ -44,14 +44,14 @@ class SpecialOfferWindow(bui.Window):
real_price = plus.get_price( real_price = plus.get_price(
'pro' if offer['item'] == 'pro_fullprice' else 'pro_sale' 'pro' if offer['item'] == 'pro_fullprice' else 'pro_sale'
) )
if real_price is None and bui.app.debug_build: if real_price is None and bui.app.env.debug:
print('NOTE: Faking prices for debug build.') print('NOTE: Faking prices for debug build.')
real_price = '$1.23' real_price = '$1.23'
zombie = real_price is None zombie = real_price is None
elif isinstance(offer['price'], str): elif isinstance(offer['price'], str):
# (a string price implies IAP id) # (a string price implies IAP id)
real_price = plus.get_price(offer['price']) real_price = plus.get_price(offer['price'])
if real_price is None and bui.app.debug_build: if real_price is None and bui.app.env.debug:
print('NOTE: Faking price for debug build.') print('NOTE: Faking price for debug build.')
real_price = '$1.23' real_price = '$1.23'
zombie = real_price is None zombie = real_price is None

View file

@ -566,8 +566,8 @@ class StoreBrowserWindow(bui.Window):
'item': item, 'item': item,
'platform': app.classic.platform, 'platform': app.classic.platform,
'subplatform': app.classic.subplatform, 'subplatform': app.classic.subplatform,
'version': app.version, 'version': app.env.version,
'buildNumber': app.build_number, 'buildNumber': app.env.build_number,
'purchaseType': 'ticket' if is_ticket_purchase else 'real', 'purchaseType': 'ticket' if is_ticket_purchase else 'real',
}, },
callback=bui.WeakCall( callback=bui.WeakCall(
@ -1406,11 +1406,11 @@ def _check_merch_availability_in_bg_thread() -> None:
time.sleep(1.1934) # A bit randomized to avoid aliasing. time.sleep(1.1934) # A bit randomized to avoid aliasing.
# Slight hack; start checking merch availability in the bg # Slight hack; start checking merch availability in the bg (but only if
# (but only if it looks like we're part of a running app; don't want to # it looks like we've been imported for use in a running app; don't want
# do this during docs generation/etc.) # to do this during docs generation/etc.)
if ( if (
os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') != '1' os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') != '1'
and bui.app.state is not bui.app.State.INITIAL and bui.app.state is not bui.app.State.NOT_RUNNING
): ):
Thread(target=_check_merch_availability_in_bg_thread, daemon=True).start() Thread(target=_check_merch_availability_in_bg_thread, daemon=True).start()

View file

@ -36,7 +36,9 @@ class HostConfig:
mosh_shell: str = 'sh' mosh_shell: str = 'sh'
workspaces_root: str = '/home/${USER}/cloudshell_workspaces' workspaces_root: str = '/home/${USER}/cloudshell_workspaces'
sync_perms: bool = True sync_perms: bool = True
precommand: str | None = None precommand: str | None = None # KILL THIS
precommand_noninteractive: str | None = None
precommand_interactive: str | None = None
managed: bool = False managed: bool = False
idle_minutes: int = 5 idle_minutes: int = 5
can_sudo_reboot: bool = False can_sudo_reboot: bool = False

View file

@ -7,7 +7,9 @@ from typing import TYPE_CHECKING
import errno import errno
if TYPE_CHECKING: if TYPE_CHECKING:
pass from typing import Any
from efro.terminal import ClrBase
class CleanError(Exception): class CleanError(Exception):
@ -25,18 +27,29 @@ class CleanError(Exception):
more descriptive exception types. more descriptive exception types.
""" """
def pretty_print(self, flush: bool = True, prefix: str = 'Error') -> None: def pretty_print(
self,
flush: bool = True,
prefix: str = 'Error',
file: Any = None,
clr: type[ClrBase] | None = None,
) -> None:
"""Print the error to stdout, using red colored output if available. """Print the error to stdout, using red colored output if available.
If the error has an empty message, prints nothing (not even a newline). If the error has an empty message, prints nothing (not even a newline).
""" """
from efro.terminal import Clr from efro.terminal import Clr
if clr is None:
clr = Clr
if prefix: if prefix:
prefix = f'{prefix}: ' prefix = f'{prefix}: '
errstr = str(self) errstr = str(self)
if errstr: if errstr:
print(f'{Clr.SRED}{prefix}{errstr}{Clr.RST}', flush=flush) print(
f'{clr.SRED}{prefix}{errstr}{clr.RST}', flush=flush, file=file
)
class CommunicationError(Exception): class CommunicationError(Exception):

View file

@ -39,6 +39,12 @@ class _EmptyObj:
pass pass
# A dead weak-ref should be immutable, right? So we can create exactly
# one and return it for all cases that need an empty weak-ref.
_g_empty_weak_ref = weakref.ref(_EmptyObj())
assert _g_empty_weak_ref() is None
# TODO: kill this and just use efro.call.tpartial # TODO: kill this and just use efro.call.tpartial
if TYPE_CHECKING: if TYPE_CHECKING:
Call = Call Call = Call
@ -148,8 +154,11 @@ def empty_weakref(objtype: type[T]) -> weakref.ref[T]:
# At runtime, all weakrefs are the same; our type arg is just # At runtime, all weakrefs are the same; our type arg is just
# for the static type checker. # for the static type checker.
del objtype # Unused. del objtype # Unused.
# Just create an object and let it die. Is there a cleaner way to do this? # Just create an object and let it die. Is there a cleaner way to do this?
return weakref.ref(_EmptyObj()) # type: ignore # return weakref.ref(_EmptyObj()) # type: ignore
return _g_empty_weak_ref # type: ignore
def data_size_str(bytecount: int) -> str: def data_size_str(bytecount: int) -> str:

View file

@ -64,7 +64,7 @@ class modSetup(babase.Plugin):
bootstraping() bootstraping()
servercheck.checkserver().start() servercheck.checkserver().start()
ServerUpdate.check() ServerUpdate.check()
bs.apptimer(5, account.updateOwnerIps) # bs.apptimer(5, account.updateOwnerIps)
if settings["afk_remover"]['enable']: if settings["afk_remover"]['enable']:
afk_check.checkIdle().start() afk_check.checkIdle().start()
if (settings["useV2Account"]): if (settings["useV2Account"]):

Binary file not shown.

Binary file not shown.