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)