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

View file

@ -64,7 +64,7 @@ class AccountV2Subsystem:
def set_primary_credentials(self, credentials: str | None) -> None:
"""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:
"""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'
account handle will be set.
"""
raise RuntimeError('This should be overridden.')
raise NotImplementedError('This should be overridden.')
@property
def primary(self) -> AccountV2Handle | None:
@ -128,7 +128,7 @@ class AccountV2Subsystem:
# Ok; no workspace to worry about; carry on.
if not self._initial_sign_in_completed:
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:
"""Should be called when logins for the active account change."""
@ -163,7 +163,7 @@ class AccountV2Subsystem:
"""
if not self._initial_sign_in_completed:
self._initial_sign_in_completed = True
_babase.app.on_initial_sign_in_completed()
_babase.app.on_initial_sign_in_complete()
@staticmethod
def _hashstr(val: str) -> str:
@ -409,7 +409,7 @@ class AccountV2Subsystem:
def _on_set_active_workspace_completed(self) -> None:
if not self._initial_sign_in_completed:
self._initial_sign_in_completed = True
_babase.app.on_initial_sign_in_completed()
_babase.app.on_initial_sign_in_complete()
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
# needed (would need to guard access to our implementations
# 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):
raise TypeError(
@ -73,7 +74,8 @@ class AppComponentSubsystem:
If no custom implementation has been set, the provided
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.
return cast(T, None)
@ -87,7 +89,9 @@ class AppComponentSubsystem:
loop. Note that any further setclass calls before the callback
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)
def _run_change_callbacks(self) -> None:

View file

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

View file

@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bacommon.app import AppExperience
from babase._appintent import AppIntent
@ -17,16 +18,33 @@ class AppMode:
"""
@classmethod
def supports_intent(cls, intent: AppIntent) -> bool:
"""Return whether our mode can handle the provided intent."""
del intent
def get_app_experience(cls) -> AppExperience:
"""Return the overall experience provided by this mode."""
raise NotImplementedError('AppMode subclasses must override this.')
# Say no to everything by default. Let's make mode explicitly
# lay out everything they *do* support.
return False
@classmethod
def can_handle_intent(cls, intent: AppIntent) -> bool:
"""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:
"""Handle an intent."""
raise NotImplementedError('AppMode subclasses must override this.')
def on_activate(self) -> None:
"""Called when the mode is being activated."""

View file

@ -1,6 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides AppMode functionality."""
"""Contains AppModeSelector base class."""
from __future__ import annotations
from typing import TYPE_CHECKING
@ -18,15 +18,15 @@ class AppModeSelector:
The app calls an instance of this class when passed an AppIntent to
determine which AppMode to use to handle the intent. Plugins 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.
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.
"""
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
can technically be any class and are not required to use this, but
building one out of this base class provides some conveniences such
as predefined callbacks during app state changes.
building one out of this base class provides conveniences such as
predefined callbacks during app state changes.
Subsystems must be registered with the app before it completes its
transition to the 'running' state.
@ -48,5 +48,8 @@ class AppSubsystem:
def on_app_shutdown(self) -> None:
"""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:
"""Called when the app config should be applied."""

View file

@ -48,7 +48,7 @@ def is_browser_likely_available() -> bool:
# assume no browser.
# FIXME: Might not be the case anymore; should make this definable
# 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
# Anywhere else assume we've got one.
@ -103,8 +103,8 @@ def handle_v1_cloud_log() -> None:
info = {
'log': _babase.get_v1_cloud_log(),
'version': app.version,
'build': app.build_number,
'version': app.env.version,
'build': app.env.build_number,
'userAgentString': classic.legacy_user_agent_string,
'session': sessionname,
'activity': activityname,
@ -222,8 +222,7 @@ def garbage_collect() -> None:
def print_corrupt_file_error() -> None:
"""Print an error if a corrupt file is found."""
# FIXME - filter this out for builds without bauiv1.
if not _babase.app.headless_mode:
if _babase.app.env.gui:
_babase.apptimer(
2.0,
lambda: _babase.screenmessage(
@ -279,7 +278,8 @@ def dump_app_state(
# the dump in that case.
try:
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:
outfile.write(
@ -297,7 +297,7 @@ def dump_app_state(
return
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')
@ -329,7 +329,8 @@ def log_dumped_app_state() -> None:
try:
out = ''
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):
# 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}'
)
tbpath = os.path.join(
os.path.dirname(_babase.app.config_file_path),
os.path.dirname(_babase.app.env.config_file_path),
'_appstate_dump_tb',
)
if os.path.exists(tbpath):
@ -378,6 +379,10 @@ class AppHealthMonitor(AppSubsystem):
self._response = False
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:
try:
self._monitor_app()

View file

@ -5,6 +5,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from bacommon.app import AppExperience
import _babase
from babase._appmode import AppMode
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."""
@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.
return isinstance(intent, AppIntentExec | AppIntentDefault)
@ -30,8 +36,8 @@ class EmptyAppMode(AppMode):
def on_activate(self) -> None:
# Let the native layer do its thing.
_babase.empty_app_mode_activate()
_babase.on_empty_app_mode_activate()
def on_deactivate(self) -> None:
# 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 signal
import logging
import warnings
from typing import TYPE_CHECKING
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.
_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
# explicit times to avoid random hitches and keep things more
# 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())
def on_app_launching() -> None:
"""Called when the app reaches the launching state."""
def on_app_state_initing() -> None:
"""Called when the app reaches the initing state."""
import _babase
import baenv
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()
if envconfig.is_user_app_python_dir:
_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
# 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
# logging will be delayed compared to code that uses native logging
# calls directly. Perhaps we should add some sort of 'immediate'
# callback option to better handle such cases (similar to the
# immediate echofile stderr print that already occurs).
# logging will be delayed relative to code that uses native logging
# calls directly. Ideally we should add some sort of 'immediate'
# callback option to better handle such cases (analogous to the
# immediate echofile stderr print that LogHandler already
# supports).
log_handler.add_callback(_on_log, feed_existing_logs=True)

View file

@ -6,12 +6,13 @@ from __future__ import annotations
import types
import weakref
import random
import logging
import inspect
from typing import TYPE_CHECKING, TypeVar, Protocol, NewType
from efro.terminal import Clr
import _babase
from babase._error import print_error, print_exception
if TYPE_CHECKING:
from typing import Any
@ -19,7 +20,8 @@ if TYPE_CHECKING:
# 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
# and pauses while the app is suspended.
@ -85,39 +87,6 @@ def getclass(name: str, subclassof: type[T]) -> type[T]:
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:
"""Convert any unicode data in provided sequence(s) to utf8 bytes."""
if isinstance(data, dict):
@ -136,7 +105,7 @@ def utf8_all(data: Any) -> Any:
def get_type_name(cls: type) -> str:
"""Return a full type name including module for a class."""
return cls.__module__ + '.' + cls.__name__
return f'{cls.__module__}.{cls.__name__}'
class _WeakCall:
@ -195,18 +164,12 @@ class _WeakCall:
else:
app = _babase.app
if not self._did_invalid_call_warning:
print(
(
'Warning: callable passed to babase.WeakCall() is not'
' weak-referencable ('
+ str(args[0])
+ '); use babase.Call() instead to avoid this '
'warning. Stack-trace:'
)
logging.warning(
'Warning: callable passed to babase.WeakCall() is not'
' weak-referencable (%s); use babase.Call() instead'
' to avoid this warning.',
stack_info=True,
)
import traceback
traceback.print_stack()
self._did_invalid_call_warning = True
self._call = args[0]
self._args = args[1:]
@ -320,7 +283,7 @@ def verify_object_death(obj: object) -> None:
try:
ref = weakref.ref(obj)
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
# 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
if TYPE_CHECKING:
pass
def on_app_bootstrapping_complete() -> None:
"""Called by C++ layer when bootstrapping finishes."""
_babase.app.on_app_bootstrapping_complete()
from babase._stringedit import StringEditAdapter
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))
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:
from babase._language import Lstr
@ -97,7 +84,7 @@ def connection_failed_message() -> None:
def temporarily_unavailable_message() -> None:
from babase._language import Lstr
if not _babase.app.headless_mode:
if _babase.app.env.gui:
_babase.getsimplesound('error').play()
_babase.screenmessage(
Lstr(resource='getTicketsWindow.unavailableTemporarilyText'),
@ -108,7 +95,7 @@ def temporarily_unavailable_message() -> None:
def in_progress_message() -> None:
from babase._language import Lstr
if not _babase.app.headless_mode:
if _babase.app.env.gui:
_babase.getsimplesound('error').play()
_babase.screenmessage(
Lstr(resource='getTicketsWindow.inProgressText'),
@ -119,7 +106,7 @@ def in_progress_message() -> None:
def error_message() -> None:
from babase._language import Lstr
if not _babase.app.headless_mode:
if _babase.app.env.gui:
_babase.getsimplesound('error').play()
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
@ -127,7 +114,7 @@ def error_message() -> None:
def purchase_not_valid_error() -> None:
from babase._language import Lstr
if not _babase.app.headless_mode:
if _babase.app.env.gui:
_babase.getsimplesound('error').play()
_babase.screenmessage(
Lstr(
@ -141,7 +128,7 @@ def purchase_not_valid_error() -> None:
def purchase_already_in_progress_error() -> None:
from babase._language import Lstr
if not _babase.app.headless_mode:
if _babase.app.env.gui:
_babase.getsimplesound('error').play()
_babase.screenmessage(
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:
assert _babase.app.classic is not None
_babase.app.classic.accounts.show_post_purchase_message()
@ -208,7 +187,7 @@ def award_dual_wielding_achievement() -> None:
def play_gong_sound() -> None:
if not _babase.app.headless_mode:
if _babase.app.env.gui:
_babase.getsimplesound('gong').play()
@ -272,15 +251,11 @@ def toggle_fullscreen() -> None:
cfg.apply_and_commit()
def read_config() -> None:
_babase.app.read_config()
def ui_remote_press() -> None:
"""Handle a press by a remote device that is only usable for nav."""
from babase._language import Lstr
if _babase.app.headless_mode:
if _babase.app.env.headless:
return
# Can be called without a context; need a context for getsound.
@ -300,10 +275,6 @@ def do_quit() -> None:
_babase.quit()
def shutdown() -> None:
_babase.app.on_app_shutdown()
def hash_strings(inputs: list[str]) -> str:
"""Hash provided strings into a short output string."""
import hashlib
@ -375,11 +346,11 @@ def show_client_too_old_error() -> None:
# a newer build.
if (
_babase.app.config.get('SuppressClientTooOldErrorForBuild')
== _babase.app.build_number
== _babase.app.env.build_number
):
return
if not _babase.app.headless_mode:
if _babase.app.env.gui:
_babase.getsimplesound('error').play()
_babase.screenmessage(
@ -393,3 +364,11 @@ def show_client_too_old_error() -> None:
),
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:
names = os.listdir(
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]
@ -121,7 +124,7 @@ class LanguageSubsystem(AppSubsystem):
with open(
os.path.join(
_babase.app.data_directory,
_babase.app.env.data_directory,
'ba_data',
'data',
'languages',
@ -139,7 +142,7 @@ class LanguageSubsystem(AppSubsystem):
lmodvalues = None
else:
lmodfile = os.path.join(
_babase.app.data_directory,
_babase.app.env.data_directory,
'ba_data',
'data',
'languages',

View file

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

View file

@ -18,11 +18,6 @@ import _babase
if TYPE_CHECKING:
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.
# 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 is None
env = _babase.app.env
self._scan_complete_cb = scan_complete_cb
self._scan = DirectoryScan(
[
path
for path in [
_babase.app.python_directory_app,
_babase.app.python_directory_user,
env.python_directory_app,
env.python_directory_user,
]
if path is not None
]
@ -212,7 +208,7 @@ class MetadataSubsystem:
'${NUM}',
str(len(results.incorrect_api_modules) - 1),
),
('${API}', str(CURRENT_API_VERSION)),
('${API}', str(_babase.app.env.api_version)),
],
)
else:
@ -220,7 +216,7 @@ class MetadataSubsystem:
resource='scanScriptsSingleModuleNeedsUpdatesText',
subs=[
('${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))
@ -344,13 +340,16 @@ class DirectoryScan:
# If we find a module requiring a different api version, warn
# 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(
'metascan: %s requires api %s but we are running'
' %s. Ignoring module.',
subpath,
required_api,
CURRENT_API_VERSION,
_babase.app.env.api_version,
)
self.results.incorrect_api_modules.append(
self._module_name_for_subpath(subpath)

View file

@ -81,7 +81,7 @@ class PluginSubsystem(AppSubsystem):
config_changed = 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.
if found_new and not auto_enable_new_plugins:
_babase.screenmessage(
@ -131,10 +131,10 @@ class PluginSubsystem(AppSubsystem):
disappeared_plugs.add(class_path)
continue
# If plugins disappeared, let the user know gently and remove them
# from the config so we'll again let the user know if they later
# reappear. This makes it much smoother to switch between users
# or workspaces.
# If plugins disappeared, let the user know gently and remove
# them from the config so we'll again let the user know if they
# later reappear. This makes it much smoother to switch between
# users or workspaces.
if disappeared_plugs:
_babase.getsimplesound('shieldDown').play()
_babase.screenmessage(
@ -197,6 +197,17 @@ class PluginSubsystem(AppSubsystem):
_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:
"""(internal)"""
@ -217,7 +228,7 @@ class PluginSpec:
key. Remember to commit the app-config after making any changes.
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
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,
@ -249,7 +260,7 @@ class PluginSpec:
plugstate['enabled'] = val
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._language import Lstr
@ -308,8 +319,8 @@ class Plugin:
Category: **App Classes**
Plugins are discoverable by the meta-tag system
and the user can select which ones they want to activate.
Active plugins are then called at specific times as the
and the user can select which ones they want to enable.
Enabled plugins are then called at specific times as the
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."""
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:
"""Called after the game continues."""
"""Called when the app is resuming from a paused state."""
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:
"""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)".
"""
app = _babase.app
path: str | None = app.python_directory_user
path: str | None = app.env.python_directory_user
if path is None:
return '<Not Available>'
@ -66,19 +66,20 @@ def _request_storage_permission() -> bool:
def show_user_scripts() -> None:
"""Open or nicely print the location of the user-scripts directory."""
app = _babase.app
env = app.env
# First off, if we need permission for this, ask for it.
if _request_storage_permission():
return
# 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>')
return
# Secondly, if the dir doesn't exist, attempt to make it.
if not os.path.exists(app.python_directory_user):
os.makedirs(app.python_directory_user)
if not os.path.exists(env.python_directory_user):
os.makedirs(env.python_directory_user)
# 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
@ -88,7 +89,7 @@ def show_user_scripts() -> None:
# they can see it.
if app.classic is not None and app.classic.platform == 'android':
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):
file_name = usd + '/about_this_folder.txt'
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.
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.
else:
@ -120,18 +121,19 @@ def create_user_system_scripts() -> None:
import shutil
app = _babase.app
env = app.env
# First off, if we need permission for this, ask for it.
if _request_storage_permission():
return
# 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')
if app.python_directory_app is None:
if env.python_directory_app is None:
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'
if os.path.exists(path):
shutil.rmtree(path)
@ -147,8 +149,8 @@ def create_user_system_scripts() -> None:
# /Knowledge-Nuggets#python-cache-files-gotcha
return ('__pycache__',)
print(f'COPYING "{app.python_directory_app}" -> "{pathtmp}".')
shutil.copytree(app.python_directory_app, pathtmp, ignore=_ignore_filter)
print(f'COPYING "{env.python_directory_app}" -> "{pathtmp}".')
shutil.copytree(env.python_directory_app, pathtmp, ignore=_ignore_filter)
print(f'MOVING "{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()."""
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')
path = app.python_directory_user + '/sys/' + app.version
path = f'{env.python_directory_user}/sys/{env.version}'
if os.path.exists(path):
shutil.rmtree(path)
print(
@ -185,6 +187,6 @@ def delete_user_system_scripts() -> None:
print(f"User system scripts not found at '{path}'.")
# 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):
os.rmdir(dpath)

View file

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

View file

@ -31,27 +31,6 @@ def get_input_device_mapped_value(
subplatform = app.classic.subplatform
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 'Controllers' in appconfig:
ccfgs = appconfig['Controllers']

View file

@ -4,13 +4,12 @@
from __future__ import annotations
import copy
import threading
import weakref
import threading
from enum import Enum
from typing import TYPE_CHECKING
import babase
from babase import DEFAULT_REQUEST_TIMEOUT_SECONDS
import bascenev1
if TYPE_CHECKING:
@ -36,7 +35,9 @@ class MasterServerV1CallThread(threading.Thread):
callback: MasterServerCallback | None,
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_type = request_type
if not isinstance(response_type, MasterServerResponseType):
@ -69,6 +70,7 @@ class MasterServerV1CallThread(threading.Thread):
def run(self) -> None:
# pylint: disable=consider-using-with
# pylint: disable=too-many-branches
import urllib.request
import urllib.parse
import urllib.error
@ -80,6 +82,13 @@ class MasterServerV1CallThread(threading.Thread):
assert plus is not None
response_data: Any = 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:
classic = babase.app.classic
assert classic is not None
@ -101,7 +110,7 @@ class MasterServerV1CallThread(threading.Thread):
{'User-Agent': classic.legacy_user_agent_string},
),
context=babase.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
elif self._request_type == 'post':
url = plus.get_master_server_address() + '/' + self._request
@ -113,7 +122,7 @@ class MasterServerV1CallThread(threading.Thread):
{'User-Agent': classic.legacy_user_agent_string},
),
context=babase.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
else:
raise TypeError('Invalid request_type: ' + self._request_type)
@ -147,6 +156,9 @@ class MasterServerV1CallThread(threading.Thread):
response_data = None
finally:
babase.shutdown_suppress_end()
if self._callback is not None:
babase.pushcall(
babase.Call(self._run_callback, response_data),

View file

@ -214,7 +214,10 @@ class ServerController:
babase.app.classic.master_server_v1_get(
'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,
)
@ -379,8 +382,8 @@ class ServerController:
if self._first_run:
curtimestr = time.strftime('%c')
startupmsg = (
f'{Clr.BLD}{Clr.BLU}{babase.appnameupper()} {app.version}'
f' ({app.build_number})'
f'{Clr.BLD}{Clr.BLU}{babase.appnameupper()} {app.env.version}'
f' ({app.env.build_number})'
f' entering server-mode {curtimestr}{Clr.RST}'
)
logging.info(startupmsg)

View file

@ -545,7 +545,7 @@ class StoreSubsystem:
"""
plus = babase.app.plus
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 mapitem in map_section['items']:
if plus is None or not plus.get_purchased(mapitem):
@ -558,7 +558,7 @@ class StoreSubsystem:
try:
plus = babase.app.plus
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 mname in section['items']:
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.special_offer: dict | None = None
self.ping_thread_count = 0
self.allow_ticket_purchases: bool = not babase.app.iircade_mode
self.allow_ticket_purchases: bool = True
# Main Menu.
self.main_menu_did_initial_transition = False
@ -128,6 +128,10 @@ class ClassicSubsystem(babase.AppSubsystem):
assert isinstance(self._env['platform'], str)
return self._env['platform']
def scene_v1_protocol_version(self) -> int:
"""(internal)"""
return bascenev1.protocol_version()
@property
def subplatform(self) -> str:
"""String for subplatform.
@ -153,6 +157,7 @@ class ClassicSubsystem(babase.AppSubsystem):
plus = babase.app.plus
assert plus is not None
env = babase.app.env
cfg = babase.app.config
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.
# (so I don't accidentally release a build that can't play tourneys)
if (
not babase.app.debug_build
and not babase.app.test_build
and not plus.is_blessed()
):
if not env.debug and not env.test and not plus.is_blessed():
babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
# FIXME: This should not be hard-coded.
@ -219,7 +220,7 @@ class ClassicSubsystem(babase.AppSubsystem):
self.special_offer = cfg['pendingSpecialOffer']['o']
show_offer()
if not babase.app.headless_mode:
if babase.app.env.gui:
babase.apptimer(3.0, check_special_offer)
# If there's a leftover log file, attempt to upload it to the
@ -465,6 +466,37 @@ class ClassicSubsystem(babase.AppSubsystem):
_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(
self,
request: str,
@ -750,7 +782,7 @@ class ClassicSubsystem(babase.AppSubsystem):
from bauiv1lib.party import PartyWindow
from babase import app
assert not app.headless_mode
assert app.env.gui
bauiv1.getsound('swish').play()
@ -773,7 +805,7 @@ class ClassicSubsystem(babase.AppSubsystem):
if not in_main_menu:
set_ui_input_device(device_id)
if not babase.app.headless_mode:
if babase.app.env.gui:
bauiv1.getsound('swish').play()
babase.app.ui_v1.set_main_menu_window(

View file

@ -106,16 +106,14 @@ def get_all_tips() -> list[str]:
),
]
app = babase.app
if not app.iircade_mode:
tips += [
'If your framerate is choppy, try turning down resolution\nor '
'visuals in the game\'s graphics settings.'
]
tips += [
'If your framerate is choppy, try turning down resolution\nor '
'visuals in the game\'s graphics settings.'
]
if (
app.classic is not None
and app.classic.platform in ('android', 'ios')
and not app.on_tv
and not app.iircade_mode
and not app.env.tv
):
tips += [
(
@ -124,11 +122,7 @@ def get_all_tips() -> list[str]:
'in Settings->Graphics'
),
]
if (
app.classic is not None
and app.classic.platform in ['mac', 'android']
and not app.iircade_mode
):
if app.classic is not None and app.classic.platform in ['mac', 'android']:
tips += [
'Tired of the soundtrack? Replace it with your own!'
'\nSee Settings->Audio->Soundtrack'
@ -136,11 +130,11 @@ def get_all_tips() -> list[str]:
# Hot-plugging is currently only on some platforms.
# FIXME: Should add a platform entry for this so don't forget to update it.
if (
app.classic is not None
and app.classic.platform in ['mac', 'android', 'windows']
and not app.iircade_mode
):
if app.classic is not None and app.classic.platform in [
'mac',
'android',
'windows',
]:
tips += [
'Players can join and leave in the middle of most games,\n'
'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
# using.
TARGET_BALLISTICA_BUILD = 21213
TARGET_BALLISTICA_VERSION = '1.7.26'
TARGET_BALLISTICA_BUILD = 21397
TARGET_BALLISTICA_VERSION = '1.7.28'
@dataclass
@ -461,11 +461,12 @@ def _modular_main() -> None:
# First baenv sets up things like Python paths the way 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
# some processing of command line args to allow overriding of paths
# or running explicit commands or whatever else. Our goal is that
# this modular form of the app should be basically indistinguishable
# from the monolithic form when used from the command line.
# Below we're doing a slightly fancier version of that. Namely, we
# do some processing of command line args to allow overriding of
# paths or running explicit commands or whatever else. Our goal is
# that this modular form of the app should be basically
# indistinguishable from the monolithic form when used from the
# command line.
try:
# 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
# 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()
# 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)
# We run configure() BEFORE importing babase. (part of its job
# is to wrangle paths to determine where babase and everything
# else gets loaded from).
# is to wrangle paths which can affect where babase and
# everything else gets loaded from).
configure(
config_dir=config_dir,
data_dir=data_dir,

View file

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

View file

@ -5,7 +5,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from bacommon.app import AppExperience
from babase import AppMode, AppIntentExec, AppIntentDefault
import _bascenev1
if TYPE_CHECKING:
@ -16,7 +18,11 @@ class SceneV1AppMode(AppMode):
"""Our app-mode."""
@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.
return isinstance(intent, AppIntentExec | AppIntentDefault)
@ -29,8 +35,8 @@ class SceneV1AppMode(AppMode):
def on_activate(self) -> None:
# Let the native layer do its thing.
_bascenev1.app_mode_activate()
_bascenev1.on_app_mode_activate()
def on_deactivate(self) -> None:
# 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()
# 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(
3.8, babase.WeakCall(self._show_remaining_achievements)
)
@ -108,7 +109,7 @@ class CoopGameActivity(GameActivity[PlayerT, TeamT]):
)
if not a.complete
]
vrmode = babase.app.vr_mode
vrmode = babase.app.env.vr
if achievements:
Text(
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
# running a GUI (or just the current game if we're running headless).
else:
if not babase.app.headless_mode:
if babase.app.env.gui:
self.end()
else:
if isinstance(activity, GameActivity):
@ -277,6 +277,7 @@ class CoopSession(Session):
from bascenev1._score import ScoreType
app = babase.app
env = app.env
classic = app.classic
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
# in-game players, quit out of the session (this can happen if
# 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]
if not active_players:
self.end()
@ -317,7 +318,7 @@ class CoopSession(Session):
if (
isinstance(activity, JoinActivity)
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:
raise RuntimeError('Tutorial not preloaded properly.')
@ -339,7 +340,7 @@ class CoopSession(Session):
# Now flip the current activity..
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:
self._custom_menu_ui = [
{

View file

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

View file

@ -185,7 +185,7 @@ def show_damage_count(
# (connected clients may have differing configs so they won't
# get the intended results).
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(
'text',
attrs={

View file

@ -33,15 +33,11 @@ class JoinInfo:
from bascenev1._nodeactor import NodeActor
self._state = 0
self._press_to_punch: str | bascenev1.Lstr = (
'C'
if babase.app.iircade_mode
else babase.charstr(babase.SpecialChar.LEFT_BUTTON)
self._press_to_punch: str | bascenev1.Lstr = babase.charstr(
babase.SpecialChar.LEFT_BUTTON
)
self._press_to_bomb: str | bascenev1.Lstr = (
'B'
if babase.app.iircade_mode
else babase.charstr(babase.SpecialChar.RIGHT_BUTTON)
self._press_to_bomb: str | bascenev1.Lstr = babase.charstr(
babase.SpecialChar.RIGHT_BUTTON
)
self._joinmsg = babase.Lstr(resource='pressAnyButtonToJoinText')
can_switch_teams = len(lobby.sessionteams) > 1
@ -53,7 +49,7 @@ class JoinInfo:
if keyboard is not None:
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(
_bascenev1.newnode(
'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]
else:
msg1 = babase.Lstr(
@ -438,6 +434,7 @@ class Chooser:
"""Reload all player profiles."""
app = babase.app
env = app.env
assert app.classic is not None
# Re-construct our profile index and other stuff since the profile
@ -465,7 +462,7 @@ class Chooser:
# (non-unicode/non-json) version.
# Make sure they conform to our standards
# (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.
for profile in list(self._profiles.items()):
@ -479,17 +476,13 @@ class Chooser:
self._profiles['_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:
del self._profiles['__account__']
# For local devices, add it an 'edit' option which will pop up
# the profile window.
if (
not is_remote
and not is_test_input
and not (app.demo_mode or app.arcade_mode)
):
if not is_remote and not is_test_input and not (env.demo or env.arcade):
self._profiles['_edit'] = {}
# 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."""
assert babase.app.classic is not None
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

View file

@ -69,7 +69,7 @@ def get_player_profile_colors(
# Special case: when being asked for a random color in kiosk mode,
# 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)
highlight = (0.4, 0.4, 0.5)
else:

View file

@ -45,6 +45,9 @@ class CoopJoinActivity(bs.JoinActivity):
def _show_remaining_achievements(self) -> None:
from bascenev1lib.actor.text import Text
app = bs.app
env = app.env
# We only show achievements and challenges for CoopGameActivities.
session = self.session
assert isinstance(session, bs.CoopSession)
@ -64,19 +67,15 @@ class CoopJoinActivity(bs.JoinActivity):
ts_h_offs = 60
# Show remaining achievements in some cases.
if bs.app.classic is not None and not (
bs.app.demo_mode or bs.app.arcade_mode
):
if app.classic is not None and not (env.demo or env.arcade):
achievements = [
a
for a in bs.app.classic.ach.achievements_for_coop_level(
levelname
)
for a in app.classic.ach.achievements_for_coop_level(levelname)
if not a.complete
]
have_achievements = bool(achievements)
achievements = [a for a in achievements if not a.complete]
vrmode = bs.app.vr_mode
vrmode = env.vr
if have_achievements:
Text(
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
env = bui.app.env
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
@ -406,9 +408,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
else:
pass
show_next_button = self._is_more_levels and not (
bui.app.demo_mode or bui.app.arcade_mode
)
show_next_button = self._is_more_levels and not (env.demo or env.arcade)
if not show_next_button:
h_offs += 70
@ -486,7 +486,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
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._store_button_instance = None
else:
@ -595,7 +595,9 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
# pylint: disable=too-many-locals
super().on_begin()
plus = bs.app.plus
app = bs.app
env = app.env
plus = app.plus
assert plus is not None
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
# as current.
if self._newly_complete:
cfg = bs.app.config
cfg = app.config
cfg['Selected Coop Game'] = (
self._campaign.name + ':' + self._next_level_name
)
@ -637,7 +639,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
self._is_complete
and self._victory
and self._is_more_levels
and not (bs.app.demo_mode or bs.app.arcade_mode)
and not (env.demo or env.arcade)
):
Text(
bs.Lstr(
@ -715,7 +717,7 @@ class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
position=(0, 230),
).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
# because only host can continue the game.
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':
# 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')
bs.pushcall(bs.WeakCall(self._got_score_results, None))
else:

View file

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

View file

@ -58,45 +58,11 @@ class ControlsGuide(bs.Actor):
self._update_timer: bs.Timer | None = None
self._title_text: bs.Node | None
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])
jump_pos = (position[0], position[1] - offs)
bomb_pos = (position[0] + offs * 1.1, position[1])
pickup_pos = (position[0], position[1] + offs)
extra_pos_1 = None
extra_pos_2 = None
self._force_hide_button_names = False
punch_pos = (position[0] - offs * 1.1, position[1])
jump_pos = (position[0], position[1] - offs)
bomb_pos = (position[0] + offs * 1.1, position[1])
pickup_pos = (position[0], position[1] + offs)
self._force_hide_button_names = False
if show_title:
self._title_text_pos_top = (
@ -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)
self._run_text_pos_top = (position[0], position[1] - 135.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(
'text',
attrs={
'scale': sval,
'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,
'maxwidth': 380,
'v_align': 'top',
@ -271,36 +237,8 @@ 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
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_1 = None
self._extra_image_2 = None
self._nodes = [
self._bomb_image,
@ -317,10 +255,6 @@ class ControlsGuide(bs.Actor):
if show_title:
assert 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.
for node in self._nodes:

View file

@ -209,7 +209,7 @@ class PlayerSpaz(Spaz):
picked_up_by = msg.node.source_player
if 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')
elif isinstance(msg, bs.StandMessage):
super().handlemessage(msg) # Augment standard behavior.
@ -247,7 +247,7 @@ class PlayerSpaz(Spaz):
# something like last_actor_attacked_by to fix that.
if (
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
else:
@ -278,7 +278,7 @@ class PlayerSpaz(Spaz):
source_player = msg.get_source_player(type(self._player))
if 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)
super().handlemessage(msg) # Augment standard behavior.
activity = self._activity()

View file

@ -45,7 +45,7 @@ class _Entry:
# FIXME: Should not do things conditionally for vr-mode, as there may
# 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 vrmode:

View file

@ -19,6 +19,9 @@ if TYPE_CHECKING:
from typing import Any, Sequence, Callable
POWERUP_WEAR_OFF_TIME = 20000
# Obsolete - just used for demo guy now.
BASE_PUNCH_POWER_SCALE = 1.2
BASE_PUNCH_COOLDOWN = 400
@ -95,7 +98,7 @@ class Spaz(bs.Actor):
self.source_player = source_player
self._dead = False
if self._demo_mode: # Preserve old behavior.
self._punch_power_scale = 1.2
self._punch_power_scale = BASE_PUNCH_POWER_SCALE
else:
self._punch_power_scale = factory.punch_power_scale
self.fly = bs.getactivity().globalsnode.happy_thoughts_mode
@ -189,7 +192,7 @@ class Spaz(bs.Actor):
self.land_mine_count = 0
self.blast_radius = 2.0
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
else:
self._punch_cooldown = factory.punch_cooldown
@ -482,12 +485,12 @@ class Spaz(bs.Actor):
Called to 'press bomb' on this spaz;
used for player or AI connections.
"""
if not self.node:
return
if self._dead or self.frozen:
return
if self.node.knockout > 0.0:
if (
not self.node
or self._dead
or self.frozen
or self.node.knockout > 0.0
):
return
t_ms = int(bs.time() * 1000.0)
assert isinstance(t_ms, int)
@ -514,15 +517,14 @@ class Spaz(bs.Actor):
"""
if not self.node:
return
t_ms = int(bs.time() * 1000.0)
assert isinstance(t_ms, int)
self.last_run_time_ms = t_ms
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
# 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:
self._turbo_filter_add_press('run')
@ -535,7 +537,7 @@ class Spaz(bs.Actor):
"""
if not self.node:
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
# killing a lot and making it hard to fly.. should look into this.
self.node.fly_pressed = True
@ -610,7 +612,7 @@ class Spaz(bs.Actor):
self.node, attr, materials + (factory.curse_material,)
)
# None specifies no time limit
# None specifies no time limit.
assert self.node
if self.curse_time is None:
self.node.curse_death_time = -1
@ -878,7 +880,7 @@ class Spaz(bs.Actor):
self.node.frozen = True
bs.timer(5.0, bs.WeakCall(self.handlemessage, bs.ThawMessage()))
# 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:
self.shatter()
@ -898,7 +900,7 @@ class Spaz(bs.Actor):
return True
# 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)
assert isinstance(local_time, int)
if (
@ -1133,11 +1135,11 @@ class Spaz(bs.Actor):
)
if self.hitpoints > 0:
# It's kinda crappy to die from impacts, so lets reduce
# impact damage by a reasonable amount *if* it'll keep us alive
# impact damage by a reasonable amount *if* it'll keep us alive.
if msg.hit_type == 'impact' and damage > self.hitpoints:
# Drop damage to whatever puts us at 10 hit points,
# or 200 less than it used to be whichever is greater
# (so it *can* still kill us if its high enough)
# (so it *can* still kill us if its high enough).
newdamage = max(damage - 200, self.hitpoints - 10)
damage = newdamage
self.node.handlemessage('flash')

View file

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

View file

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

View file

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

View file

@ -60,11 +60,11 @@ class Puck(bs.Actor):
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.DieMessage):
assert self.node
self.node.delete()
activity = self._activity()
if activity and not msg.immediate:
activity.handlemessage(PuckDiedMessage(self))
if self.node:
self.node.delete()
activity = self._activity()
if activity and not msg.immediate:
activity.handlemessage(PuckDiedMessage(self))
# If we go out of bounds, move back to where we started.
elif isinstance(msg, bs.OutOfBoundsMessage):

View file

@ -186,6 +186,7 @@ class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]):
'materials': flagmats,
},
)
self._update_scoreboard()
self._update_flag_state()
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}:
# Show controls help in demo/arcade modes.
if bs.app.demo_mode or bs.app.arcade_mode:
# Show controls help in demo or arcade modes.
env = bs.app.env
if env.demo or env.arcade:
ControlsGuide(
delay=3.0, lifespan=10.0, bright=True
).autoretain()

View file

@ -478,7 +478,7 @@ class RunaroundGame(bs.CoopGameActivity[Player, Team]):
)
# FIXME; should not set things based on vr mode.
# (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(
bs.newnode(
'text',

View file

@ -50,6 +50,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
super().on_transition_in()
random.seed(123)
app = bs.app
env = app.env
assert app.classic is not None
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).
# Any differences need to happen at the engine level so everyone
# 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)
# 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).
# Any differences need to happen at the engine level so everyone sees
# things in their own optimal way.
vr_mode = app.vr_mode
vr_mode = app.env.vr
uiscale = app.ui_v1.uiscale
# In cases where we're doing lots of dev work lets always show the
# build number.
force_show_build_number = False
if not bs.app.toolbar_test:
if app.debug_build or app.test_build or force_show_build_number:
if app.debug_build:
if not bs.app.ui_v1.use_toolbars:
if env.debug or env.test or force_show_build_number:
if env.debug:
text = bs.Lstr(
value='${V} (${B}) (${D})',
subs=[
('${V}', app.version),
('${B}', str(app.build_number)),
('${V}', app.env.version),
('${B}', str(app.env.build_number)),
('${D}', bs.Lstr(resource='debugText')),
],
)
@ -139,12 +140,12 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
text = bs.Lstr(
value='${V} (${B})',
subs=[
('${V}', app.version),
('${B}', str(app.build_number)),
('${V}', app.env.version),
('${B}', str(app.env.build_number)),
],
)
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
color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7)
self.version = bs.NodeActor(
@ -168,31 +169,9 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
assert self.version.node
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.
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)
self.beta_info = bs.NodeActor(
bs.newnode(
@ -313,7 +292,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
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)
# 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
# the kiosk start screen.
if bs.app.demo_mode or bs.app.arcade_mode:
if env.demo or env.arcade:
# pylint: disable=cyclic-import
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-statements
app = bs.app
env = app.env
assert app.classic is not None
# Update logo in case it changes.
@ -460,7 +440,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
base_x = -270.0
x = base_x - 20.0
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(
x - 110 + 50,
113 + y + 1.2 * y_extra,
@ -525,7 +505,7 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]):
base_x = -170
x = base_x - 20
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
delay1 = delay
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
# (unless we're in VR mode in which case its best to
# leave things still).
if not bs.app.vr_mode:
if not bs.app.env.vr:
cmb: bs.Node | None
cmb2: bs.Node | None
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
# leave things still).
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.connectattr('output', logo.node, 'position')
keys = {}
@ -904,7 +884,7 @@ class NewsDisplay:
self._phrases.insert(0, phr)
val = self._phrases.pop()
if val == '__ACH__':
vrmode = app.vr_mode
vrmode = app.env.vr
Text(
bs.Lstr(resource='nextAchievementsText'),
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
# (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 != ''
]
self._phrase_change_timer = bs.Timer(
@ -982,12 +962,12 @@ class NewsDisplay:
assert bs.app.classic is not None
scl = (
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
)
color2 = (1, 1, 1, 1) if bs.app.vr_mode else (0.7, 0.65, 0.75, 1.0)
shadow = 1.0 if bs.app.vr_mode else 0.4
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.env.vr else 0.4
self._text = bs.NodeActor(
bs.newnode(
'text',

View file

@ -4,8 +4,12 @@
# 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
__all__ = [
'TemplateFsSubsystem',
'hello_again_world',
]

View file

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

View file

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

View file

@ -55,7 +55,8 @@ class UIV1Subsystem(babase.AppSubsystem):
self.have_party_queue_window = False
self.cleanupchecks: list[UICleanupCheck] = []
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.heading_color = (0.72, 0.7, 0.75)
self.infotextcolor = (0.7, 0.9, 0.7)

View file

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

View file

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

View file

@ -11,7 +11,7 @@ class ConfigErrorWindow(bui.Window):
"""Window for dealing with a broken config."""
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
super().__init__(
bui.containerwidget(size=(width, 400), transition='in_right')

View file

@ -197,9 +197,19 @@ class QuitWindow:
time=0.2,
endcall=lambda: bui.quit(soft=True, back=self._back),
)
# Prevent the user from doing anything else while we're on our
# way out.
bui.lock_all_input()
# 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)
bui.apptimer(0.3, bui.unlock_all_input)
# On systems supporting soft-quit, unlock and fade back in shortly
# (soft-quit basically just backgrounds/hides the app).
if bui.app.env.supports_soft_quit:
# Unlock and fade back in shortly. Just in case something goes
# wrong (or on Android where quit just backs out of our activity
# and we may come back after).
def _come_back() -> None:
bui.unlock_all_input()
bui.fade_screen(True)
bui.apptimer(0.5, _come_back)

View file

@ -350,7 +350,7 @@ class CoopBrowserWindow(bui.Window):
# noinspection PyUnresolvedReferences
@staticmethod
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.coop.gamebutton as _unused2
import bauiv1lib.confirm as _unused3

View file

@ -212,7 +212,10 @@ class CreditsListWindow(bui.Window):
try:
with open(
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',
) as infile:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -196,214 +196,158 @@ class HelpWindow(bui.Window):
texture=logo_tex,
)
force_test = False
app = bui.app
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
v -= spacing * 50.0
txt = bui.Lstr(resource=self._r + '.someDaysText').evaluate()
bui.textwidget(
parent=self._subcontainer,
position=(h, v),
size=(0, 0),
scale=1.2,
maxwidth=self._sub_width * 0.9,
text=txt,
h_align='center',
color=paragraph,
v_align='center',
flatness=1.0,
)
v -= spacing * 25.0 + getres(self._r + '.someDaysExtraSpace')
txt_scale = 0.66
txt = bui.Lstr(resource=self._r + '.orPunchingSomethingText').evaluate()
bui.textwidget(
parent=self._subcontainer,
position=(h, v),
size=(0, 0),
scale=txt_scale,
maxwidth=self._sub_width * 0.9,
text=txt,
h_align='center',
color=paragraph,
v_align='center',
flatness=1.0,
)
v -= spacing * 27.0 + getres(self._r + '.orPunchingSomethingExtraSpace')
txt_scale = 1.0
txt = bui.Lstr(
resource=self._r + '.canHelpText',
subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
).evaluate()
bui.textwidget(
parent=self._subcontainer,
position=(h, v),
size=(0, 0),
scale=txt_scale,
flatness=1.0,
text=txt,
h_align='center',
color=paragraph,
v_align='center',
)
v -= spacing * 70.0
txt_scale = 1.0
txt = bui.Lstr(resource=self._r + '.toGetTheMostText').evaluate()
bui.textwidget(
parent=self._subcontainer,
position=(h, v),
size=(0, 0),
scale=txt_scale,
maxwidth=self._sub_width * 0.9,
text=txt,
h_align='center',
color=header,
v_align='center',
flatness=1.0,
)
v -= spacing * 40.0
txt_scale = 0.74
txt = bui.Lstr(resource=self._r + '.friendsText').evaluate()
hval2 = h - 220
bui.textwidget(
parent=self._subcontainer,
position=(hval2, v),
size=(0, 0),
scale=txt_scale,
maxwidth=100,
text=txt,
h_align='right',
color=header,
v_align='center',
flatness=1.0,
)
txt = bui.Lstr(
resource=self._r + '.friendsGoodText',
subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
).evaluate()
txt_scale = 0.7
bui.textwidget(
parent=self._subcontainer,
position=(hval2 + 10, v + 8),
size=(0, 0),
scale=txt_scale,
maxwidth=500,
text=txt,
h_align='left',
color=paragraph,
flatness=1.0,
)
app = bui.app
v -= spacing * 45.0
txt = (
bui.Lstr(resource=self._r + '.devicesText').evaluate()
if app.env.vr
else bui.Lstr(resource=self._r + '.controllersText').evaluate()
)
txt_scale = 0.74
hval2 = h - 220
bui.textwidget(
parent=self._subcontainer,
position=(hval2, v),
size=(0, 0),
scale=txt_scale,
maxwidth=100,
text=txt,
h_align='right',
v_align='center',
color=header,
flatness=1.0,
)
txt_scale = 0.7
if not app.env.vr:
infotxt = '.controllersInfoText'
txt = bui.Lstr(
resource=self._r + infotxt,
fallback_resource=self._r + '.controllersInfoText',
subs=[
('${APP_NAME}', bui.Lstr(resource='titleText')),
('${REMOTE_APP_NAME}', bui.get_remote_app_name()),
],
).evaluate()
else:
v -= spacing * 50.0
txt = bui.Lstr(resource=self._r + '.someDaysText').evaluate()
bui.textwidget(
parent=self._subcontainer,
position=(h, v),
size=(0, 0),
scale=1.2,
maxwidth=self._sub_width * 0.9,
text=txt,
h_align='center',
color=paragraph,
v_align='center',
flatness=1.0,
)
v -= spacing * 25.0 + getres(self._r + '.someDaysExtraSpace')
txt_scale = 0.66
txt = bui.Lstr(
resource=self._r + '.orPunchingSomethingText'
).evaluate()
bui.textwidget(
parent=self._subcontainer,
position=(h, v),
size=(0, 0),
scale=txt_scale,
maxwidth=self._sub_width * 0.9,
text=txt,
h_align='center',
color=paragraph,
v_align='center',
flatness=1.0,
)
v -= spacing * 27.0 + getres(
self._r + '.orPunchingSomethingExtraSpace'
)
txt_scale = 1.0
txt = bui.Lstr(
resource=self._r + '.canHelpText',
resource=self._r + '.devicesInfoText',
subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
).evaluate()
bui.textwidget(
parent=self._subcontainer,
position=(h, v),
size=(0, 0),
scale=txt_scale,
flatness=1.0,
text=txt,
h_align='center',
color=paragraph,
v_align='center',
)
v -= spacing * 70.0
txt_scale = 1.0
txt = bui.Lstr(resource=self._r + '.toGetTheMostText').evaluate()
bui.textwidget(
parent=self._subcontainer,
position=(h, v),
size=(0, 0),
scale=txt_scale,
maxwidth=self._sub_width * 0.9,
text=txt,
h_align='center',
color=header,
v_align='center',
flatness=1.0,
)
v -= spacing * 40.0
txt_scale = 0.74
txt = bui.Lstr(resource=self._r + '.friendsText').evaluate()
hval2 = h - 220
bui.textwidget(
parent=self._subcontainer,
position=(hval2, v),
size=(0, 0),
scale=txt_scale,
maxwidth=100,
text=txt,
h_align='right',
color=header,
v_align='center',
flatness=1.0,
)
txt = bui.Lstr(
resource=self._r + '.friendsGoodText',
subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
).evaluate()
txt_scale = 0.7
bui.textwidget(
parent=self._subcontainer,
position=(hval2 + 10, v + 8),
size=(0, 0),
scale=txt_scale,
maxwidth=500,
text=txt,
h_align='left',
color=paragraph,
flatness=1.0,
)
app = bui.app
v -= spacing * 45.0
txt = (
bui.Lstr(resource=self._r + '.devicesText').evaluate()
if app.vr_mode
else bui.Lstr(resource=self._r + '.controllersText').evaluate()
)
txt_scale = 0.74
hval2 = h - 220
bui.textwidget(
parent=self._subcontainer,
position=(hval2, v),
size=(0, 0),
scale=txt_scale,
maxwidth=100,
text=txt,
h_align='right',
v_align='center',
color=header,
flatness=1.0,
)
txt_scale = 0.7
if not app.vr_mode:
infotxt = (
'.controllersInfoTextRemoteOnly'
if app.iircade_mode
else '.controllersInfoText'
)
txt = bui.Lstr(
resource=self._r + infotxt,
fallback_resource=self._r + '.controllersInfoText',
subs=[
('${APP_NAME}', bui.Lstr(resource='titleText')),
('${REMOTE_APP_NAME}', bui.get_remote_app_name()),
],
).evaluate()
else:
txt = bui.Lstr(
resource=self._r + '.devicesInfoText',
subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
).evaluate()
bui.textwidget(
parent=self._subcontainer,
position=(hval2 + 10, v + 8),
size=(0, 0),
scale=txt_scale,
maxwidth=500,
max_height=105,
text=txt,
h_align='left',
color=paragraph,
flatness=1.0,
)
bui.textwidget(
parent=self._subcontainer,
position=(hval2 + 10, v + 8),
size=(0, 0),
scale=txt_scale,
maxwidth=500,
max_height=105,
text=txt,
h_align='left',
color=paragraph,
flatness=1.0,
)
v -= spacing * 150.0

View file

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

View file

@ -50,9 +50,8 @@ class MainMenuWindow(bui.Window):
)
# Grab this stuff in case it changes.
self._is_demo = bui.app.demo_mode
self._is_arcade = bui.app.arcade_mode
self._is_iircade = bui.app.iircade_mode
self._is_demo = bui.app.env.demo
self._is_arcade = bui.app.env.arcade
self._tdelay = 0.0
self._t_delay_inc = 0.02
@ -93,7 +92,7 @@ class MainMenuWindow(bui.Window):
# noinspection PyUnresolvedReferences
@staticmethod
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.confirm as _unused2
import bauiv1lib.store.button as _unused3
@ -118,7 +117,7 @@ class MainMenuWindow(bui.Window):
force_test = False
bs.get_local_active_input_devices_count()
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
) or force_test:
@ -220,8 +219,8 @@ class MainMenuWindow(bui.Window):
self._have_store_button = not self._in_game
self._have_settings_button = (
not self._in_game or not app.toolbar_test
) and not (self._is_demo or self._is_arcade or self._is_iircade)
not self._in_game or not app.ui_v1.use_toolbars
) and not (self._is_demo or self._is_arcade)
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.
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]
this_b_width = self._button_width * 0.4 * scale
demo_menu_delay = (
@ -635,7 +634,7 @@ class MainMenuWindow(bui.Window):
textcolor=(0.7, 0.8, 0.7),
label=bui.Lstr(
resource='modeArcadeText'
if bui.app.arcade_mode
if bui.app.env.arcade
else 'modeDemoText'
),
transition_delay=demo_menu_delay,

View file

@ -513,7 +513,7 @@ class PlayWindow(bui.Window):
# noinspection PyUnresolvedReferences
@staticmethod
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.account as _unused2
import bauiv1lib.coop.browser as _unused3

View file

@ -34,7 +34,7 @@ class PopupWindow:
focus_size = size
# 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_position = (0, 0)

View file

@ -718,11 +718,13 @@ class EditProfileWindow(bui.Window):
else '???'
)
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(
edit=self._clipped_name_text,
text=bui.Lstr(
resource='inGameClippedNameText',
subs=[('${NAME}', name[:10] + '...')],
subs=[('${NAME}', display_name)],
),
)
else:

View file

@ -155,7 +155,7 @@ class ProfileUpgradeWindow(bui.Window):
bui.app.classic.master_server_v1_get(
'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),
)
self._cost = plus.get_v1_account_misc_read_val(

View file

@ -16,7 +16,7 @@ if TYPE_CHECKING:
class AdvancedSettingsWindow(bui.Window):
"""Window for editing advanced game settings."""
"""Window for editing advanced app settings."""
def __init__(
self,
@ -61,6 +61,7 @@ class AdvancedSettingsWindow(bui.Window):
self._spacing = 32
self._menu_open = False
top_extra = 10 if uiscale is bui.UIScale.SMALL else 0
super().__init__(
root_widget=bui.containerwidget(
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,
# so no need to show this.
self._show_always_use_internal_keyboard = (
not app.vr_mode and not app.iircade_mode
)
self._show_always_use_internal_keyboard = not app.env.vr
self._scroll_width = self._width - (100 + 2 * x_inset)
self._scroll_height = self._height - 115.0
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:
self._sub_height += 62
@ -104,7 +103,7 @@ class AdvancedSettingsWindow(bui.Window):
if self._show_disable_gyro:
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._extra_button_spacing = self._spacing * 2.5
@ -180,14 +179,14 @@ class AdvancedSettingsWindow(bui.Window):
# Fetch the list of completed languages.
bui.app.classic.master_server_v1_get(
'bsLangGetCompleted',
{'b': app.build_number},
{'b': app.env.build_number},
callback=bui.WeakCall(self._completed_langs_cb),
)
# noinspection PyUnresolvedReferences
@staticmethod
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 bauiv1lib import config as _unused1
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
# language-list hasn't changed.
# NOTE - although we now support widgets updating their own
# translations, we still change the label formatting on the language
# menu based on the language so still need this. ...however we could
@ -324,7 +324,10 @@ class AdvancedSettingsWindow(bui.Window):
with open(
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',
) as infile:
@ -473,6 +476,19 @@ class AdvancedSettingsWindow(bui.Window):
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
self._disable_camera_shake_check_box = ConfigCheckBox(
parent=self._subcontainer,

View file

@ -224,7 +224,7 @@ class AllSettingsWindow(bui.Window):
# noinspection PyUnresolvedReferences
@staticmethod
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.settings.controls as _unused2
import bauiv1lib.settings.graphics as _unused3

View file

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

View file

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

View file

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

View file

@ -4,12 +4,15 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, cast
from bauiv1lib.popup import PopupMenu
from bauiv1lib.config import ConfigCheckBox, ConfigNumberEdit
from bauiv1lib.config import ConfigCheckBox
import bauiv1 as bui
if TYPE_CHECKING:
from typing import Any
class GraphicsSettingsWindow(bui.Window):
"""Window for graphics settings."""
@ -42,26 +45,26 @@ class GraphicsSettingsWindow(bui.Window):
uiscale = app.ui_v1.uiscale
width = 450.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
fullscreen_spacing_top = spacing * 0.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
height += fullscreen_spacing + fullscreen_spacing_top
show_gamma = False
gamma_spacing = spacing * 1.3
if bui.has_gamma_control():
show_gamma = True
height += gamma_spacing
show_vsync = bui.supports_vsync()
show_tv_mode = not bui.app.env.vr
show_vsync = False
if app.classic.platform == 'mac':
show_vsync = True
show_max_fps = bui.supports_max_fps()
if show_max_fps:
height += 50
show_resolution = True
if app.vr_mode:
if app.env.vr:
show_resolution = (
app.classic.platform == 'android'
and app.classic.subplatform == 'cardboard'
@ -70,7 +73,7 @@ class GraphicsSettingsWindow(bui.Window):
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
base_scale = (
2.4
2.0
if uiscale is bui.UIScale.SMALL
else 1.5
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,
position=(35, height - 50),
size=(120, 60),
# size=(120, 60),
size=(60, 60),
scale=0.8,
text_scale=1.2,
autoselect=True,
label=bui.Lstr(resource='backText'),
button_type='back',
label=bui.charstr(bui.SpecialChar.BACK),
button_type='backSmall',
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(
parent=self._root_widget,
@ -115,15 +119,7 @@ class GraphicsSettingsWindow(bui.Window):
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._gamma_controls: ConfigNumberEdit | None = None
if self._show_fullscreen:
v -= fullscreen_spacing_top
self._fullscreen_checkbox = ConfigCheckBox(
@ -149,34 +145,10 @@ class GraphicsSettingsWindow(bui.Window):
self._have_selected_child = True
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._unselected_color = (0.7, 0.7, 0.7, 1)
# quality
# Quality
bui.textwidget(
parent=self._root_widget,
position=(60, v),
@ -208,7 +180,7 @@ class GraphicsSettingsWindow(bui.Window):
on_value_change_call=self._set_quality,
)
# texture controls
# Texture controls
bui.textwidget(
parent=self._root_widget,
position=(230, v),
@ -244,8 +216,9 @@ class GraphicsSettingsWindow(bui.Window):
h_offs = 0
resolution_popup: PopupMenu | None = None
if show_resolution:
# resolution
bui.textwidget(
parent=self._root_widget,
position=(h_offs + 60, v),
@ -258,32 +231,17 @@ class GraphicsSettingsWindow(bui.Window):
v_align='center',
)
# on standard android we have 'Auto', 'Native', and a few
# HD standards
# On standard android we have 'Auto', 'Native', and a few
# HD standards.
if app.classic.platform == 'android':
# on cardboard/daydream android we have a few
# render-target-scale options
if app.classic.subplatform == 'cardboard':
rawval = bui.app.config.resolve('GVR Render Target Scale')
current_res_cardboard = (
str(
min(
100,
max(
10,
int(
round(
bui.app.config.resolve(
'GVR Render Target Scale'
)
* 100.0
)
),
),
)
)
+ '%'
str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
)
PopupMenu(
resolution_popup = PopupMenu(
parent=self._root_widget,
position=(h_offs + 60, v - 50),
width=120,
@ -301,16 +259,16 @@ class GraphicsSettingsWindow(bui.Window):
bui.Lstr(resource='nativeText'),
]
for res in [1440, 1080, 960, 720, 480]:
# nav bar is 72px so lets allow for that in what
# choices we show
# Nav bar is 72px so lets allow for that in what
# choices we show.
if native_res[1] >= res - 72:
res_str = str(res) + 'p'
res_str = f'{res}p'
choices.append(res_str)
choices_display.append(bui.Lstr(value=res_str))
current_res_android = bui.app.config.resolve(
'Resolution (Android)'
)
PopupMenu(
resolution_popup = PopupMenu(
parent=self._root_widget,
position=(h_offs + 60, v - 50),
width=120,
@ -325,26 +283,11 @@ class GraphicsSettingsWindow(bui.Window):
# set pixel-scale instead.
current_res = bui.get_display_resolution()
if current_res is None:
rawval = bui.app.config.resolve('Screen Pixel Scale')
current_res2 = (
str(
min(
100,
max(
10,
int(
round(
bui.app.config.resolve(
'Screen Pixel Scale'
)
* 100.0
)
),
),
)
)
+ '%'
str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
)
PopupMenu(
resolution_popup = PopupMenu(
parent=self._root_widget,
position=(h_offs + 60, v - 50),
width=120,
@ -355,11 +298,16 @@ class GraphicsSettingsWindow(bui.Window):
)
else:
raise RuntimeError(
'obsolete path; discrete resolutions'
'obsolete code path; discrete resolutions'
' 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:
bui.textwidget(
parent=self._root_widget,
@ -372,8 +320,7 @@ class GraphicsSettingsWindow(bui.Window):
h_align='center',
v_align='center',
)
PopupMenu(
vsync_popup = PopupMenu(
parent=self._root_widget,
position=(230, v - 50),
width=150,
@ -387,8 +334,59 @@ class GraphicsSettingsWindow(bui.Window):
current_choice=bui.app.config.resolve('Vertical Sync'),
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
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(
parent=self._root_widget,
position=(69, v - 6),
@ -398,9 +396,17 @@ class GraphicsSettingsWindow(bui.Window):
displayname=bui.Lstr(resource=self._r + '.showFPSText'),
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 not bui.app.vr_mode:
if show_tv_mode:
tvc = ConfigCheckBox(
parent=self._root_widget,
position=(240, v - 6),
@ -410,13 +416,8 @@ class GraphicsSettingsWindow(bui.Window):
displayname=bui.Lstr(resource=self._r + '.tvBorderText'),
maxwidth=130,
)
# grumble..
bui.widget(edit=fpsc.widget, right_widget=tvc.widget)
try:
pass
except Exception:
logging.exception('Exception wiring up graphics settings UI.')
bui.widget(edit=tvc.widget, left_widget=fpsc.widget)
v -= spacing
@ -429,6 +430,10 @@ class GraphicsSettingsWindow(bui.Window):
def _back(self) -> None:
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(
edit=self._root_widget, transition=self._transition_out
)
@ -469,7 +474,60 @@ class GraphicsSettingsWindow(bui.Window):
cfg['Vertical Sync'] = val
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:
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:
bui.checkboxwidget(
edit=self._fullscreen_checkbox,

View file

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

View file

@ -44,14 +44,14 @@ class SpecialOfferWindow(bui.Window):
real_price = plus.get_price(
'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.')
real_price = '$1.23'
zombie = real_price is None
elif isinstance(offer['price'], str):
# (a string price implies IAP id)
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.')
real_price = '$1.23'
zombie = real_price is None

View file

@ -566,8 +566,8 @@ class StoreBrowserWindow(bui.Window):
'item': item,
'platform': app.classic.platform,
'subplatform': app.classic.subplatform,
'version': app.version,
'buildNumber': app.build_number,
'version': app.env.version,
'buildNumber': app.env.build_number,
'purchaseType': 'ticket' if is_ticket_purchase else 'real',
},
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.
# Slight hack; start checking merch availability in the bg
# (but only if it looks like we're part of a running app; don't want to
# do this during docs generation/etc.)
# Slight hack; start checking merch availability in the bg (but only if
# it looks like we've been imported for use in a running app; don't want
# to do this during docs generation/etc.)
if (
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()

View file

@ -36,7 +36,9 @@ class HostConfig:
mosh_shell: str = 'sh'
workspaces_root: str = '/home/${USER}/cloudshell_workspaces'
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
idle_minutes: int = 5
can_sudo_reboot: bool = False

View file

@ -7,7 +7,9 @@ from typing import TYPE_CHECKING
import errno
if TYPE_CHECKING:
pass
from typing import Any
from efro.terminal import ClrBase
class CleanError(Exception):
@ -25,18 +27,29 @@ class CleanError(Exception):
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.
If the error has an empty message, prints nothing (not even a newline).
"""
from efro.terminal import Clr
if clr is None:
clr = Clr
if prefix:
prefix = f'{prefix}: '
errstr = str(self)
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):

View file

@ -39,6 +39,12 @@ class _EmptyObj:
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
if TYPE_CHECKING:
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
# for the static type checker.
del objtype # Unused.
# 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:

View file

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

Binary file not shown.

Binary file not shown.