From 7c7f89385e02c72d9815b5dd104c9d88879ce9bf Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Fri, 16 Oct 2020 17:45:24 -0700 Subject: [PATCH] Packaged up MetadataSubsystem into ba.app.meta --- CHANGELOG.md | 1 + assets/src/ba_data/python/ba/__init__.py | 1 + assets/src/ba_data/python/ba/_app.py | 14 +- assets/src/ba_data/python/ba/_meta.py | 244 +++++++++--------- assets/src/ba_data/python/ba/_playlist.py | 4 +- assets/src/ba_data/python/ba/internal.py | 1 - .../python/bastd/ui/onscreenkeyboard.py | 14 +- .../python/bastd/ui/playlist/addgame.py | 5 +- .../python/bastd/ui/settings/plugins.py | 2 +- docs/ba_module.md | 58 +++++ 10 files changed, 205 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d7ee6f..eb436088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Achievement functionality has been consolidated into an AchievementSubsystem object at ba.app.ach - Plugin functionality has been consolidated into a PluginSubsystem obj at ba.app.plugins - Ditto with AccountSubsystem and ba.app.accounts +- Ditto with MetadataSubsystem and ba.app.meta ### 1.5.26 (20217) - Simplified licensing header on python scripts. diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index 79c80daf..b4a0c97b 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -65,6 +65,7 @@ from ba._keyboard import Keyboard from ba._level import Level from ba._lobby import Lobby, Chooser from ba._math import normalized_color, is_point_in_box, vec3validate +from ba._meta import MetadataSubsystem from ba._messages import (UNHANDLED, OutOfBoundsMessage, DeathType, DieMessage, PlayerDiedMessage, StandMessage, PickUpMessage, DropMessage, PickedUpMessage, DroppedMessage, diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py index d2191d11..9d52c9c8 100644 --- a/assets/src/ba_data/python/ba/_app.py +++ b/assets/src/ba_data/python/ba/_app.py @@ -11,7 +11,7 @@ import _ba if TYPE_CHECKING: import ba - from ba import _language, _meta + from ba import _language from bastd.actor import spazappearance from typing import Optional, Dict, Set, Any, Type, Tuple, Callable, List @@ -28,11 +28,6 @@ class App: """ # pylint: disable=too-many-public-methods - # Note: many values here are simple method attrs and thus don't show - # up in docs. If there's any that'd be useful to expose publicly, they - # should be converted to properties so its possible to validate values - # and provide docs. - @property def build_number(self) -> int: """Integer build number. @@ -177,6 +172,7 @@ class App: from ba._achievement import AchievementSubsystem from ba._plugin import PluginSubsystem from ba._account import AccountSubsystem + from ba._meta import MetadataSubsystem # Config. self.config_file_healthy = False @@ -201,7 +197,6 @@ class App: assert isinstance(self.headless_mode, bool) # Misc. - self.metascan: Optional[_meta.ScanResults] = None self.tips: List[str] = [] self.stress_test_reset_timer: Optional[ba.Timer] = None self.last_ad_completion_time: Optional[float] = None @@ -235,6 +230,7 @@ class App: self.last_ad_purpose = 'invalid' self.attempted_first_ad = False + self.meta = MetadataSubsystem() self.accounts = AccountSubsystem() self.plugins = PluginSubsystem() self.music = MusicSubsystem() @@ -288,7 +284,6 @@ class App: from ba import _appconfig from ba import _achievement from ba import _map - from ba import _meta from ba import _campaign from bastd import appdelegate from bastd import maps as stdmaps @@ -382,8 +377,7 @@ class App: if not self.headless_mode: _ba.timer(3.0, check_special_offer, timetype=TimeType.REAL) - # Start scanning for things exposed via ba_meta. - _meta.start_scan() + self.meta.on_app_launch() self.accounts.on_app_launch() self.plugins.on_app_launch() diff --git a/assets/src/ba_data/python/ba/_meta.py b/assets/src/ba_data/python/ba/_meta.py index 28b88aff..5ccda02b 100644 --- a/assets/src/ba_data/python/ba/_meta.py +++ b/assets/src/ba_data/python/ba/_meta.py @@ -5,6 +5,7 @@ from __future__ import annotations import os +import time import pathlib import threading from typing import TYPE_CHECKING @@ -33,78 +34,146 @@ class ScanResults: warnings: str = '' -def start_scan() -> None: - """Begin scanning script directories for scripts containing metadata. +class MetadataSubsystem: + """Subsystem for working with script metadata in the app. - Should be called only once at launch.""" - app = _ba.app - if app.metascan is not None: - print('WARNING: meta scan run more than once.') - pythondirs = [app.python_directory_app, app.python_directory_user] - thread = ScanThread(pythondirs) - thread.start() + Category: App Classes + Access the single shared instance of this class at 'ba.app.meta'. + """ -def handle_scan_results(results: ScanResults) -> None: - """Called in the game thread with results of a completed scan.""" + def __init__(self) -> None: + self.metascan: Optional[ScanResults] = None - from ba._language import Lstr - from ba._plugin import PotentialPlugin + def on_app_launch(self) -> None: + """Should be called when the app is done bootstrapping.""" - # Warnings generally only get printed locally for users' benefit - # (things like out-of-date scripts being ignored, etc.) - # Errors are more serious and will get included in the regular log - # warnings = results.get('warnings', '') - # errors = results.get('errors', '') - if results.warnings != '' or results.errors != '': - import textwrap - _ba.screenmessage(Lstr(resource='scanScriptsErrorText'), - color=(1, 0, 0)) - _ba.playsound(_ba.getsound('error')) - if results.warnings != '': - _ba.log(textwrap.indent(results.warnings, 'Warning (meta-scan): '), - to_server=False) - if results.errors != '': - _ba.log(textwrap.indent(results.errors, 'Error (meta-scan): ')) + # Start scanning for things exposed via ba_meta. + self.start_scan() - # Handle plugins. - plugs = _ba.app.plugins - config_changed = False - found_new = False - plugstates: Dict[str, Dict] = _ba.app.config.setdefault('Plugins', {}) - assert isinstance(plugstates, dict) + def start_scan(self) -> None: + """Begin scanning script directories for scripts containing metadata. - # Create a potential-plugin for each class we found in the scan. - for class_path in results.plugins: - plugs.potential_plugins.append( - PotentialPlugin(display_name=Lstr(value=class_path), - class_path=class_path, - available=True)) - if class_path not in plugstates: - plugstates[class_path] = {'enabled': False} - config_changed = True - found_new = True + Should be called only once at launch.""" + app = _ba.app + if self.metascan is not None: + print('WARNING: meta scan run more than once.') + pythondirs = [app.python_directory_app, app.python_directory_user] + thread = ScanThread(pythondirs) + thread.start() - # Also add a special one for any plugins set to load but *not* found - # in the scan (this way they will show up in the UI so we can disable them) - for class_path, plugstate in plugstates.items(): - enabled = plugstate.get('enabled', False) - assert isinstance(enabled, bool) - if enabled and class_path not in results.plugins: + def handle_scan_results(self, results: ScanResults) -> None: + """Called in the game thread with results of a completed scan.""" + + from ba._language import Lstr + from ba._plugin import PotentialPlugin + + # Warnings generally only get printed locally for users' benefit + # (things like out-of-date scripts being ignored, etc.) + # Errors are more serious and will get included in the regular log + # warnings = results.get('warnings', '') + # errors = results.get('errors', '') + if results.warnings != '' or results.errors != '': + import textwrap + _ba.screenmessage(Lstr(resource='scanScriptsErrorText'), + color=(1, 0, 0)) + _ba.playsound(_ba.getsound('error')) + if results.warnings != '': + _ba.log(textwrap.indent(results.warnings, + 'Warning (meta-scan): '), + to_server=False) + if results.errors != '': + _ba.log(textwrap.indent(results.errors, 'Error (meta-scan): ')) + + # Handle plugins. + plugs = _ba.app.plugins + config_changed = False + found_new = False + plugstates: Dict[str, Dict] = _ba.app.config.setdefault('Plugins', {}) + assert isinstance(plugstates, dict) + + # Create a potential-plugin for each class we found in the scan. + for class_path in results.plugins: plugs.potential_plugins.append( PotentialPlugin(display_name=Lstr(value=class_path), class_path=class_path, - available=False)) + available=True)) + if class_path not in plugstates: + plugstates[class_path] = {'enabled': False} + config_changed = True + found_new = True - plugs.potential_plugins.sort(key=lambda p: p.class_path) + # Also add a special one for any plugins set to load but *not* found + # in the scan (this way they will show up in the UI so we can disable + # them) + for class_path, plugstate in plugstates.items(): + enabled = plugstate.get('enabled', False) + assert isinstance(enabled, bool) + if enabled and class_path not in results.plugins: + plugs.potential_plugins.append( + PotentialPlugin(display_name=Lstr(value=class_path), + class_path=class_path, + available=False)) - if found_new: - _ba.screenmessage(Lstr(resource='pluginsDetectedText'), - color=(0, 1, 0)) - _ba.playsound(_ba.getsound('ding')) + plugs.potential_plugins.sort(key=lambda p: p.class_path) - if config_changed: - _ba.app.config.commit() + if found_new: + _ba.screenmessage(Lstr(resource='pluginsDetectedText'), + color=(0, 1, 0)) + _ba.playsound(_ba.getsound('ding')) + + if config_changed: + _ba.app.config.commit() + + def get_scan_results(self) -> ScanResults: + """Return meta scan results; block if the scan is not yet complete.""" + if self.metascan is None: + print('WARNING: ba.meta.get_scan_results()' + ' called before scan completed.' + ' This can cause hitches.') + + # Now wait a bit for the scan to complete. + # Eventually error though if it doesn't. + starttime = time.time() + while self.metascan is None: + time.sleep(0.05) + if time.time() - starttime > 10.0: + raise TimeoutError( + 'timeout waiting for meta scan to complete.') + return self.metascan + + def get_game_types(self) -> List[Type[ba.GameActivity]]: + """Return available game types.""" + from ba._general import getclass + from ba._gameactivity import GameActivity + gameclassnames = self.get_scan_results().games + gameclasses = [] + for gameclassname in gameclassnames: + try: + cls = getclass(gameclassname, GameActivity) + gameclasses.append(cls) + except Exception: + from ba import _error + _error.print_exception('error importing ' + str(gameclassname)) + unowned = self.get_unowned_game_types() + return [cls for cls in gameclasses if cls not in unowned] + + def get_unowned_game_types(self) -> Set[Type[ba.GameActivity]]: + """Return present game types not owned by the current account.""" + try: + from ba import _store + unowned_games: Set[Type[ba.GameActivity]] = set() + if not _ba.app.headless_mode: + for section in _store.get_store_layout()['minigames']: + for mname in section['items']: + if not _ba.get_purchased(mname): + m_info = _store.get_store_item(mname) + unowned_games.add(m_info['gametype']) + return unowned_games + except Exception: + from ba import _error + _error.print_exception('error calcing un-owned games') + return set() class ScanThread(threading.Thread): @@ -125,13 +194,13 @@ class ScanThread(threading.Thread): # Push a call to the game thread to print warnings/errors # or otherwise deal with scan results. - _ba.pushcall(Call(handle_scan_results, results), + _ba.pushcall(Call(_ba.app.meta.handle_scan_results, results), from_other_thread=True) # We also, however, immediately make results available. # This is because the game thread may be blocked waiting # for them so we can't push a call or we'd get deadlock. - _ba.app.metascan = results + _ba.app.meta.metascan = results class DirectoryScan: @@ -338,58 +407,3 @@ class DirectoryScan: ': no valid "# ba_meta api require " line found;' ' ignoring module.\n') return None - - -def get_scan_results() -> ScanResults: - """Return meta scan results; blocking if the scan is not yet complete.""" - import time - app = _ba.app - if app.metascan is None: - print( - 'WARNING: ba.meta.get_scan_results() called before scan completed.' - ' This can cause hitches.') - - # Now wait a bit for the scan to complete. - # Eventually error though if it doesn't. - starttime = time.time() - while app.metascan is None: - time.sleep(0.05) - if time.time() - starttime > 10.0: - raise TimeoutError( - 'timeout waiting for meta scan to complete.') - return app.metascan - - -def get_game_types() -> List[Type[ba.GameActivity]]: - """Return available game types.""" - from ba._general import getclass - from ba._gameactivity import GameActivity - gameclassnames = get_scan_results().games - gameclasses = [] - for gameclassname in gameclassnames: - try: - cls = getclass(gameclassname, GameActivity) - gameclasses.append(cls) - except Exception: - from ba import _error - _error.print_exception('error importing ' + str(gameclassname)) - unowned = get_unowned_game_types() - return [cls for cls in gameclasses if cls not in unowned] - - -def get_unowned_game_types() -> Set[Type[ba.GameActivity]]: - """Return present game types not owned by the current account.""" - try: - from ba import _store - unowned_games: Set[Type[ba.GameActivity]] = set() - if not _ba.app.headless_mode: - for section in _store.get_store_layout()['minigames']: - for mname in section['items']: - if not _ba.get_purchased(mname): - m_info = _store.get_store_item(mname) - unowned_games.add(m_info['gametype']) - return unowned_games - except Exception: - from ba import _error - _error.print_exception('error calcing un-owned games') - return set() diff --git a/assets/src/ba_data/python/ba/_playlist.py b/assets/src/ba_data/python/ba/_playlist.py index 58883a3e..1f31ee6d 100644 --- a/assets/src/ba_data/python/ba/_playlist.py +++ b/assets/src/ba_data/python/ba/_playlist.py @@ -28,7 +28,7 @@ def filter_playlist(playlist: PlaylistType, # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements - from ba import _meta + import _ba from ba import _map from ba import _general from ba import _gameactivity @@ -36,7 +36,7 @@ def filter_playlist(playlist: PlaylistType, unowned_maps: Sequence[str] if remove_unowned or mark_unowned: unowned_maps = _map.get_unowned_maps() - unowned_game_types = _meta.get_unowned_game_types() + unowned_game_types = _ba.app.meta.get_unowned_game_types() else: unowned_maps = [] unowned_game_types = set() diff --git a/assets/src/ba_data/python/ba/internal.py b/assets/src/ba_data/python/ba/internal.py index 0b1092d5..4e7289f8 100644 --- a/assets/src/ba_data/python/ba/internal.py +++ b/assets/src/ba_data/python/ba/internal.py @@ -23,7 +23,6 @@ from ba._benchmark import (run_gpu_benchmark, run_cpu_benchmark, run_media_reload_benchmark, run_stress_test) from ba._campaign import getcampaign from ba._messages import PlayerProfilesChangedMessage -from ba._meta import get_game_types from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES from ba._music import do_play_music from ba._netutils import (master_server_get, master_server_post, diff --git a/assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py b/assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py index 1df4aff6..abe95cd5 100644 --- a/assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py +++ b/assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py @@ -213,8 +213,8 @@ class OnScreenKeyboardWindow(ba.Window): # Show change instructions only if we have more than one # keyboard option. - if (ba.app.metascan is not None - and len(ba.app.metascan.keyboards) > 1): + if (ba.app.meta.metascan is not None + and len(ba.app.meta.metascan.keyboards) > 1): ba.textwidget( parent=self._root_widget, h_align='center', @@ -238,8 +238,8 @@ class OnScreenKeyboardWindow(ba.Window): self._refresh() def _get_keyboard(self) -> ba.Keyboard: - assert ba.app.metascan is not None - classname = ba.app.metascan.keyboards[self._keyboard_index] + assert ba.app.meta.metascan is not None + classname = ba.app.meta.metascan.keyboards[self._keyboard_index] kbclass = ba.getclass(classname, ba.Keyboard) return kbclass() @@ -305,11 +305,11 @@ class OnScreenKeyboardWindow(ba.Window): self._refresh() def _next_keyboard(self) -> None: - assert ba.app.metascan is not None + assert ba.app.meta.metascan is not None self._keyboard_index = (self._keyboard_index + 1) % len( - ba.app.metascan.keyboards) + ba.app.meta.metascan.keyboards) self._load_keyboard() - if len(ba.app.metascan.keyboards) < 2: + if len(ba.app.meta.metascan.keyboards) < 2: ba.playsound(ba.getsound('error')) ba.screenmessage(ba.Lstr(resource='keyboardNoOthersAvailableText'), color=(1, 0, 0)) diff --git a/assets/src/ba_data/python/bastd/ui/playlist/addgame.py b/assets/src/ba_data/python/bastd/ui/playlist/addgame.py index 46cb3d70..bcc183d7 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/addgame.py +++ b/assets/src/ba_data/python/bastd/ui/playlist/addgame.py @@ -120,7 +120,6 @@ class PlaylistAddGameWindow(ba.Window): self._refresh() def _refresh(self, select_get_more_games_button: bool = False) -> None: - from ba.internal import get_game_types if self._column is not None: self._column.delete() @@ -130,8 +129,8 @@ class PlaylistAddGameWindow(ba.Window): margin=0) gametypes = [ - gt for gt in get_game_types() if gt.supports_session_type( - self._editcontroller.get_session_type()) + gt for gt in ba.app.meta.get_game_types() if + gt.supports_session_type(self._editcontroller.get_session_type()) ] # Sort in the current language. diff --git a/assets/src/ba_data/python/bastd/ui/settings/plugins.py b/assets/src/ba_data/python/bastd/ui/settings/plugins.py index c9d347eb..72e44805 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/plugins.py +++ b/assets/src/ba_data/python/bastd/ui/settings/plugins.py @@ -93,7 +93,7 @@ class PluginSettingsWindow(ba.Window): self._subcontainer = ba.columnwidget(parent=self._scrollwidget, selection_loops_to_parent=True) - if ba.app.metascan is None: + if ba.app.meta.metascan is None: ba.screenmessage('Still scanning plugins; please try again.', color=(1, 0, 0)) ba.playsound(ba.getsound('error')) diff --git a/docs/ba_module.md b/docs/ba_module.md index f504997b..249c2d76 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -155,6 +155,7 @@
  • ba.Campaign
  • ba.Keyboard
  • ba.LanguageSubsystem
  • +
  • ba.MetadataSubsystem
  • ba.MusicPlayer
  • ba.MusicSubsystem
  • ba.Plugin
  • @@ -3807,6 +3808,63 @@ m.add_actions(conditions=('they_have_material', actions=(('impact_sound', ba.getsound('metalHit'), 2, 5), ('skid_sound', ba.getsound('metalSkid'), 2, 5))) + + +
    +

    ba.MetadataSubsystem

    +

    <top level class> +

    +

    Subsystem for working with script metadata in the app.

    + +

    Category: App Classes

    + +

    Access the single shared instance of this class at 'ba.app.meta'. +

    + +

    Methods:

    +
    <constructor>, get_game_types(), get_scan_results(), get_unowned_game_types(), handle_scan_results(), on_app_launch(), start_scan()
    +
    +

    <constructor>

    +

    ba.MetadataSubsystem()

    + +
    +

    get_game_types()

    +

    get_game_types(self) -> List[Type[ba.GameActivity]]

    + +

    Return available game types.

    + +
    +

    get_scan_results()

    +

    get_scan_results(self) -> ScanResults

    + +

    Return meta scan results; block if the scan is not yet complete.

    + +
    +

    get_unowned_game_types()

    +

    get_unowned_game_types(self) -> Set[Type[ba.GameActivity]]

    + +

    Return present game types not owned by the current account.

    + +
    +

    handle_scan_results()

    +

    handle_scan_results(self, results: ScanResults) -> None

    + +

    Called in the game thread with results of a completed scan.

    + +
    +

    on_app_launch()

    +

    on_app_launch(self) -> None

    + +

    Should be called when the app is done bootstrapping.

    + +
    +

    start_scan()

    +

    start_scan(self) -> None

    + +

    Begin scanning script directories for scripts containing metadata.

    + +

    Should be called only once at launch.

    +