diff --git a/.efrocachemap b/.efrocachemap index eb9f5425..e3a69f13 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -430,12 +430,12 @@ "build/assets/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/54/a2/91da0eec3c0820602d779ef24d10", "build/assets/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/3f/d6/9080783d5c9dcc0af737f02b6f1e", "build/assets/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/22/b4/4a33bf81142ba2befad14eb5746e", - "build/assets/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/0f/a5/14ed3ec7d80bf0a8751c0d764f64", + "build/assets/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/d1/6e/8899211693c20d3b00fc198f58c6", "build/assets/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/0e/39/7cfa5f3fb8cef5f4a64f21cda880", "build/assets/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/cb/49/1739273c68c82cebca0aee16d6c9", "build/assets/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/51/89/e01389f8153497b56fbf0fa069c2", "build/assets/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/22/a4/452043a401252ca66b703ce5d4aa", - "build/assets/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/42/75/f30546475d6b7aa6599a9251973a", + "build/assets/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/23/6f/8547ba09722b7c7f5b8333986984", "build/assets/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/a6/5d/78f912e9a89f98de004405167a6a", "build/assets/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/88/ee/0cda537bab9ac827def5e236fe1a", "build/assets/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/00/ba/cf1b8bb9f7914f64647d4665b0a8", @@ -4068,26 +4068,26 @@ "build/assets/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/2d/ef/5335207d41b21b9823f6805997f1", "build/assets/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/b0/8a/55e2e77623fe657bea24f223a3ae", "build/assets/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/86/5b/2af4d1e26a1a8073c89acb06e599", - "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/d1/5f/aa35974d118ac4a45de5d105edd5", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/85/7b/fa206153e23235bd97eb3abfb45a", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/c8/97/a251315351949cbe40914f3f02a3", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/7f/07/1ba1983b2ad7b1061f0c7624ee87", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/14/3b/984737685ea813a3a8d587fd9083", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/03/ee/59971d68ce248f89175441bc232c", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/69/0a/1a067c77c85e2a57fccdd553fe44", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/d8/af/beea487d4cd9b68e323af8190bdb", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/7f/aa/f77f3330bea584a9710f13485ac0", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/fa/1e/f0c6f0a6edf00bd4975c30b44957", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/eb/ca/e8aa9629a9da31c838e8d7dd0f9d", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/d0/13/08ed924a1032c25b5404f505398b", - "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/b6/10/dd86496c0e5eac41803e43c552d3", - "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/89/d2/087efa3672aa053f6456cf5d263f", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/aa/a5/7a4703567c6f754e2248c115fb09", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/71/c1/e52972ac5b26273ad841da7d664b", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/b3/2f/1af856ae52297f7952329efc7617", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/84/5c/b58affdbdae8e334f7dcff14c684", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/4b/e7/5964489b7d22e27a413c9edb9eb3", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/1e/c9/770a9e209a63de714aff7da7e8ee", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/63/44/27519c2a85ba72d2a4e338ed4650", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/3a/33/047dbfc05746818a9dfbc7668314", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/0b/64/e2da499eeba056e64bfce5dab20a", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/76/74/619b268bc7f8609a6f477fc9e6f6", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/8d/36/fbde0a1a43724a97e6478e27b3ab", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/b0/82/021314e49a35124a1d52f8f81c09", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/75/3b/0024994a18df5b10272161903d5f", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/f6/ca/a4db280a528841bccb7aaf3baa63", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/cb/64/d43244656d4310fe0145bef54627", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/44/22/737933ac2d7caaba5cd5fd13d88f", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/6c/77/715843f46cc2c3c1bdc0e21b7b55", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/a4/48/33d74871b0c39e60064d0ce267cf", + "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/a9/61/26f4c31c7b537e2fb557c4d87c0c", + "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/19/a2/888b33bcdc7f9d87dd2d1ce4c2fd", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/67/e0/be7233baecdbe2acc133a7e4b596", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/ec/9f/ba7a1e3d7c34d7a3392f31a9fabb", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/22/d7/105197eb744fb4ee35bc99af236e", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/1c/8c/727910c0c52e8f30c6262e57bd61", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/ab/79/6a873cc823621d63f79c2a0256ed", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/1f/5a/7a8a2804f0b49076e01081075a49", "build/prefab/lib/linux_arm64_gui/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/be/19/b5458933dfc7371d91ecfcd2e06f", "build/prefab/lib/linux_arm64_gui/release/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/4e/48/123b806cbe6ddb3d9a8368bbb4f8", "build/prefab/lib/linux_arm64_server/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/be/19/b5458933dfc7371d91ecfcd2e06f", diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 92b54a80..e4b43ccb 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -2133,6 +2133,8 @@ plugkeys pluglist plugnames + plugspec + plugspecs plugstate plugstates plusbutton diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bb7bdcb..ce48b7d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.21 (build 21148, api 8, 2023-06-26) +### 1.7.21 (build 21150, api 8, 2023-06-26) - Fixed an issue where server builds would not always include collision meshes. - Upgraded Python to 3.11.4 on Android builds. @@ -9,6 +9,27 @@ code creation will fail. This should keep the images reasonably readable and avoids a crash that could occur when more data was provided than could physically fit in the qr code. +- `PotentialPlugin` has been renamed to `PluginSpec` and the list of them + renamed from `babase.app.plugins.potential_plugins` to + `babase.app.plugins.plugin_specs`. +- Added a simpler warning message when plugins are found that need to be updated + for the new api version. +- Previously, the app would only check api version on plugins when initially + registering them. This meant that once a plugin was enabled, the app would + always try to load it even if api version stopped matching. This has been + corrected; now if the api version doesn't match it will never be loaded. +- Fixed an error where plugins nested more than one level such as + `mypackage.myplugin.MyPlugin` would fail to load. +- Removed the `display_name` attr from the `PluginSpec` class, as it was simply + set to `class_path`. It seems that referring to plugins simply by their + class-paths is a reasonable system for now. +- Added `enabled`, `loadable`, `attempted_load` and `plugin` attrs to the + `PluginSpec` class. This should make it easier to interact with the overall + app plugin situation without having to do hacky raw config wrangling. +- Plugins should now show up more sensibly in the plugins UI in some cases. For + example, a plugin which was previously loading but no longer is after an + api-version change will still show up in the list as red instead of not + showing up at all. ### 1.7.20 (build 21140, api 8, 2023-06-22) diff --git a/ballisticakit-cmake/.idea/dictionaries/ericf.xml b/ballisticakit-cmake/.idea/dictionaries/ericf.xml index f8c809c9..42a1ee62 100644 --- a/ballisticakit-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticakit-cmake/.idea/dictionaries/ericf.xml @@ -1246,6 +1246,8 @@ plen pluginsettings plugnames + plugspec + plugspecs plusnet pname pnamel diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py index 6a4dcc94..c5e0e436 100644 --- a/src/assets/ba_data/python/babase/__init__.py +++ b/src/assets/ba_data/python/babase/__init__.py @@ -148,7 +148,7 @@ from babase._mgen.enums import ( 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 PotentialPlugin, Plugin, PluginSubsystem +from babase._plugin import PluginSpec, Plugin, PluginSubsystem from babase._text import timestring _babase.app = app = App() @@ -252,7 +252,7 @@ __all__ = [ 'PlayerNotFoundError', 'Plugin', 'PluginSubsystem', - 'PotentialPlugin', + 'PluginSpec', 'print_error', 'print_exception', 'print_load_info', diff --git a/src/assets/ba_data/python/babase/_meta.py b/src/assets/ba_data/python/babase/_meta.py index 1da0321e..9c67f5f1 100644 --- a/src/assets/ba_data/python/babase/_meta.py +++ b/src/assets/ba_data/python/babase/_meta.py @@ -40,8 +40,8 @@ class ScanResults: """Final results from a meta-scan.""" exports: dict[str, list[str]] = field(default_factory=dict) - errors: list[str] = field(default_factory=list) - warnings: list[str] = field(default_factory=list) + incorrect_api_modules: list[str] = field(default_factory=list) + announce_errors_occurred: bool = False def exports_of_class(self, cls: type) -> list[str]: """Return exports of a given class.""" @@ -181,8 +181,9 @@ class MetadataSubsystem: self._scan.run() results = self._scan.results self._scan = None - except Exception as exc: - results = ScanResults(errors=[f'Scan exception: {exc}']) + except Exception: + logging.exception('metascan: Error running scan in bg.') + results = ScanResults(announce_errors_occurred=True) # Place results and tell the logic thread they're ready. self.scanresults = results @@ -197,28 +198,44 @@ class MetadataSubsystem: results = self.scanresults assert results is not None - # Spit out any warnings/errors that happened. - # 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. - if results.warnings or results.errors: - import textwrap + do_play_error_sound = False + # If we found modules needing to be updated to the newer api version, + # mention that specifically. + if results.incorrect_api_modules: + if len(results.incorrect_api_modules) > 1: + msg = Lstr( + resource='scanScriptsMultipleModulesNeedUpdatesText', + subs=[ + ('${PATH}', results.incorrect_api_modules[0]), + ( + '${NUM}', + str(len(results.incorrect_api_modules) - 1), + ), + ('${API}', str(CURRENT_API_VERSION)), + ], + ) + else: + msg = Lstr( + resource='scanScriptsSingleModuleNeedsUpdatesText', + subs=[ + ('${PATH}', results.incorrect_api_modules[0]), + ('${API}', str(CURRENT_API_VERSION)), + ], + ) + _babase.screenmessage(msg, color=(1, 0, 0)) + do_play_error_sound = True + + # Let the user know if there's warning/errors in their log + # they may want to look at. + if results.announce_errors_occurred: _babase.screenmessage( Lstr(resource='scanScriptsErrorText'), color=(1, 0, 0) ) - _babase.getsimplesound('error').play() + do_play_error_sound = True - if results.warnings: - allwarnings = textwrap.indent( - '\n'.join(results.warnings), 'Warning (meta-scan): ' - ) - logging.warning(allwarnings) - if results.errors: - allerrors = textwrap.indent( - '\n'.join(results.errors), 'Error (meta-scan): ' - ) - logging.error(allerrors) + if do_play_error_sound: + _babase.getsimplesound('error').play() # Let the game know we're done. assert self._scan_complete_cb is not None @@ -262,11 +279,7 @@ class DirectoryScan: try: self._scan_module(moduledir, subpath) except Exception: - import traceback - - self.results.warnings.append( - f"Error scanning '{subpath}': " + traceback.format_exc() - ) + logging.exception("metascan: Error scanning '%s'.", subpath) # Sort our results for exportlist in self.results.exports.values(): @@ -289,9 +302,10 @@ class DirectoryScan: except PermissionError: # Expected sometimes. entries = [] - except Exception as exc: + except Exception: # Unexpected; report this. - self.results.errors.append(str(exc)) + logging.exception('metascan: Error in _get_path_module_entries.') + self.results.announce_errors_occurred = True entries = [] # Now identify python packages/modules out of what we found. @@ -331,9 +345,15 @@ 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: - self.results.warnings.append( - f'{subpath} requires api {required_api} but' - f' we are running {CURRENT_API_VERSION}. Ignoring module.' + logging.warning( + 'metascan: %s requires api %s but we are running' + ' %s. Ignoring module.', + subpath, + required_api, + CURRENT_API_VERSION, + ) + self.results.incorrect_api_modules.append( + self._module_name_for_subpath(subpath) ) return @@ -349,11 +369,13 @@ class DirectoryScan: if submodule[1].name != '__init__.py': self._scan_module(submodule[0], submodule[1]) except Exception: - import traceback + logging.exception('metascan: Error scanning %s.', subpath) - self.results.warnings.append( - f"Error scanning '{subpath}': {traceback.format_exc()}" - ) + def _module_name_for_subpath(self, subpath: Path) -> str: + # (should not be getting these) + assert '__init__.py' not in str(subpath) + + return '.'.join(subpath.parts).removesuffix('.py') def _process_module_meta_tags( self, subpath: Path, flines: list[str], meta_lines: dict[int, list[str]] @@ -363,10 +385,12 @@ class DirectoryScan: # meta_lines is just anything containing '# ba_meta '; make sure # the ba_meta is in the right place. if mline[0] != 'ba_meta': - self.results.warnings.append( - f'Warning: {subpath}:' - f' malformed ba_meta statement on line {lindex + 1}.' + logging.warning( + 'metascan: %s:%d: malformed ba_meta statement.', + subpath, + lindex + 1, ) + self.results.announce_errors_occurred = True elif ( len(mline) == 4 and mline[1] == 'require' and mline[2] == 'api' ): @@ -375,15 +399,15 @@ class DirectoryScan: elif len(mline) != 3 or mline[1] != 'export': # Currently we only support 'ba_meta export FOO'; # complain for anything else we see. - self.results.warnings.append( - f'Warning: {subpath}' - f': unrecognized ba_meta statement on line {lindex + 1}.' + logging.warning( + 'metascan: %s:%d: unrecognized ba_meta statement.', + subpath, + lindex + 1, ) + self.results.announce_errors_occurred = True else: # Looks like we've got a valid export line! - modulename = '.'.join(subpath.parts) - if subpath.name.endswith('.py'): - modulename = modulename[:-3] + modulename = self._module_name_for_subpath(subpath) exporttypestr = mline[2] export_class_name = self._get_export_class_name( subpath, flines, lindex @@ -395,11 +419,14 @@ class DirectoryScan: # classes we need to migrate people to using base # class names for them. if exporttypestr == 'game': - self.results.warnings.append( - f'{subpath}:' - " '# ba_meta export game' tag should be replaced by" - f" '# ba_meta export bascenev1.GameActivity'." + logging.warning( + "metascan: %s:%d: '# ba_meta export" + " game' tag should be replaced by '# ba_meta" + " export bascenev1.GameActivity'.", + subpath, + lindex + 1, ) + self.results.announce_errors_occurred = True else: # If export type is one of our shortcuts, sub in the # actual class path. Otherwise assume its a classpath @@ -434,10 +461,13 @@ class DirectoryScan: classname = cbits[0] break # Success! if classname is None: - self.results.warnings.append( - f'Warning: {subpath}: class definition not found below' - f' "ba_meta export" statement on line {lindexorig + 1}.' + logging.warning( + 'metascan: %s:%d: class definition not found below' + " 'ba_meta export' statement.", + subpath, + lindexorig + 1, ) + self.results.announce_errors_occurred = True return classname def _get_api_requirement( @@ -460,23 +490,26 @@ class DirectoryScan: and l[3].isdigit() ] - # We're successful if we find exactly one properly formatted line. + # We're successful if we find exactly one properly formatted + # line. if len(lines) == 1: return int(lines[0][3]) # Ok; not successful. lets issue warnings for a few error cases. if len(lines) > 1: - self.results.warnings.append( - f'Warning: {subpath}: multiple' - ' "# ba_meta require api " lines found;' - ' ignoring module.' + logging.warning( + "metascan: %s: multiple '# ba_meta require api '" + ' lines found; ignoring module.', + subpath, ) + self.results.announce_errors_occurred = True elif not lines and toplevel and meta_lines: - # If we're a top-level module containing meta lines but - # no valid "require api" line found, complain. - self.results.warnings.append( - f'Warning: {subpath}:' - ' no valid "# ba_meta require api " line found;' - ' ignoring module.' + # If we're a top-level module containing meta lines but no + # valid "require api" line found, complain. + logging.warning( + "metascan: %s: no valid '# ba_meta require api " + ' line found; ignoring module.', + subpath, ) + self.results.announce_errors_occurred = True return None diff --git a/src/assets/ba_data/python/babase/_plugin.py b/src/assets/ba_data/python/babase/_plugin.py index e030075f..ba315698 100644 --- a/src/assets/ba_data/python/babase/_plugin.py +++ b/src/assets/ba_data/python/babase/_plugin.py @@ -7,7 +7,6 @@ from __future__ import annotations import logging import importlib.util from typing import TYPE_CHECKING -from dataclasses import dataclass import _babase from babase._appsubsystem import AppSubsystem @@ -31,14 +30,21 @@ class PluginSubsystem(AppSubsystem): def __init__(self) -> None: super().__init__() - self.potential_plugins: list[babase.PotentialPlugin] = [] - self.active_plugins: dict[str, babase.Plugin] = {} + + # Info about plugins that we are aware of. This may include + # plugins discovered through meta-scanning as well as plugins + # registered in the app-config. This may include plugins that + # cannot be loaded for various reasons or that have been + # intentionally disabled. + self.plugin_specs: dict[str, babase.PluginSpec] = {} + + # The set of live active plugin objects. + self.active_plugins: list[babase.Plugin] = [] def on_meta_scan_complete(self) -> None: - """Should be called when meta-scanning is complete.""" + """Called when meta-scanning is complete.""" from babase._language import Lstr - plugs = _babase.app.plugins config_changed = False found_new = False plugstates: dict[str, dict] = _babase.app.config.setdefault( @@ -56,146 +62,75 @@ class PluginSubsystem(AppSubsystem): ) is True ) - # Create a potential-plugin for each class we found in the scan. + + assert not self.plugin_specs + assert not self.active_plugins + + # Create a plugin-spec for each plugin class we found in the + # meta-scan. for class_path in results.exports_of_class(Plugin): - plugs.potential_plugins.append( - PotentialPlugin( - display_name=Lstr(value=class_path), - class_path=class_path, - available=True, - ) + assert class_path not in self.plugin_specs + plugspec = self.plugin_specs[class_path] = PluginSpec( + class_path=class_path, loadable=True ) + + # Auto-enable new ones if desired. if auto_enable_new_plugins: if class_path not in plugstates: - # Go ahead and enable new plugins by default, but we'll - # inform the user that they need to restart to pick them up. - # they can also disable them in settings so they never load. - plugstates[class_path] = {'enabled': True} + plugspec.enabled = True config_changed = True found_new = True - plugs.potential_plugins.sort(key=lambda p: p.class_path) - - # If we're *not* auto-enabling new plugins, at least let the - # user know we found something new. + # If we're *not* auto-enabling, just let the user know if we + # found new ones. if found_new and not auto_enable_new_plugins: _babase.screenmessage( Lstr(resource='pluginsDetectedText'), color=(0, 1, 0) ) _babase.getsimplesound('ding').play() - if config_changed: - _babase.app.config.commit() - - def on_app_running(self) -> None: - # Load up our plugins and go ahead and call their on_app_running calls. - self.load_plugins() - for plugin in self.active_plugins.values(): - try: - plugin.on_app_running() - except Exception: - from babase import _error - - _error.print_exception('Error in plugin on_app_running()') - - def on_app_pause(self) -> None: - for plugin in self.active_plugins.values(): - try: - plugin.on_app_pause() - except Exception: - from babase import _error - - _error.print_exception('Error in plugin on_app_pause()') - - def on_app_resume(self) -> None: - for plugin in self.active_plugins.values(): - try: - plugin.on_app_resume() - except Exception: - from babase import _error - - _error.print_exception('Error in plugin on_app_resume()') - - def on_app_shutdown(self) -> None: - for plugin in self.active_plugins.values(): - try: - plugin.on_app_shutdown() - except Exception: - from babase import _error - - _error.print_exception('Error in plugin on_app_shutdown()') - - def load_plugins(self) -> None: - """(internal)""" - from babase._general import getclass - from babase._language import Lstr - - # Note: the plugins we load is purely based on what's enabled - # in the app config. Its not our job to look at meta stuff here. - plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {}) + # Ok, now go through all plugins registered in the app-config + # that weren't covered by the meta stuff above, either creating + # plugin-specs for them or clearing them out. This covers + # plugins with api versions not matching ours, plugins without + # ba_meta tags, and plugins that have since disappeared. assert isinstance(plugstates, dict) - plugkeys: list[str] = sorted( - key for key, val in plugstates.items() if val.get('enabled', False) - ) + wrong_api_prefixes = [f'{m}.' for m in results.incorrect_api_modules] + disappeared_plugs: set[str] = set() - for plugkey in plugkeys: - # Originally I was just catching ModuleNotFoundError on the - # getclass() call to detect plugins disappearing. However - # this breaks if the module *does* exist but itself imports - # something that does not exist; in that case we would - # incorrectly show that the plugin had disappeared. - # - # So now we're first explicitly asking Python if it can - # locate the module, and if it can then we treat any further - # errors including ModuleNotFound as problems with the - # module's code; not ours. + + for class_path in sorted(plugstates.keys()): + # Already have a spec for it; nothing to be done. + if class_path in self.plugin_specs: + continue + + # If this plugin corresponds to any modules that we've + # identified as having incorrect api versions, we'll take + # note of its existence but we won't try to load it. + if any( + class_path.startswith(prefix) for prefix in wrong_api_prefixes + ): + plugspec = self.plugin_specs[class_path] = PluginSpec( + class_path=class_path, loadable=False + ) + continue + + # Ok, it seems to be a class we have no metadata for. Look + # to see if it appears to be an actual class we could + # theoretically load. If so, we'll try. If not, we consider + # the plugin to have disappeared and inform the user as + # such. try: - spec = importlib.util.find_spec(plugkey.split('.')[0]) + spec = importlib.util.find_spec( + '.'.join(class_path.split('.')[:-1]) + ) except Exception: spec = None if spec is None: - disappeared_plugs.add(plugkey) + disappeared_plugs.add(class_path) continue - # Ok; it seems that there's *something* there. Now try to load - # it and treat any further errors as the module's fault. - try: - cls = getclass(plugkey, Plugin) - except Exception as exc: - _babase.getsimplesound('error').play() - _babase.screenmessage( - Lstr( - resource='pluginClassLoadErrorText', - subs=[ - ('${PLUGIN}', plugkey), - ('${ERROR}', str(exc)), - ], - ), - color=(1, 0, 0), - ) - logging.exception("Error loading plugin class '%s'.", plugkey) - continue - try: - plugin = cls() - assert plugkey not in self.active_plugins - self.active_plugins[plugkey] = plugin - except Exception as exc: - from babase import _error - - _babase.getsimplesound('error').play() - _babase.screenmessage( - Lstr( - resource='pluginInitErrorText', - subs=[ - ('${PLUGIN}', plugkey), - ('${ERROR}', str(exc)), - ], - ), - color=(1, 0, 0), - ) - _error.print_exception(f"Error initing plugin: '{plugkey}'.") - # 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 @@ -220,22 +155,151 @@ class PluginSubsystem(AppSubsystem): del _babase.app.config['Plugins'][goneplug] _babase.app.config.commit() + if config_changed: + _babase.app.config.commit() -@dataclass -class PotentialPlugin: - """Represents a babase.Plugin which can potentially be loaded. + def on_app_running(self) -> None: + # Load up our plugins and go ahead and call their on_app_running + # calls. + self.load_plugins() + for plugin in self.active_plugins: + try: + plugin.on_app_running() + except Exception: + from babase import _error + + _error.print_exception('Error in plugin on_app_running()') + + def on_app_pause(self) -> None: + for plugin in self.active_plugins: + try: + plugin.on_app_pause() + except Exception: + from babase import _error + + _error.print_exception('Error in plugin on_app_pause()') + + def on_app_resume(self) -> None: + for plugin in self.active_plugins: + try: + plugin.on_app_resume() + except Exception: + from babase import _error + + _error.print_exception('Error in plugin on_app_resume()') + + def on_app_shutdown(self) -> None: + for plugin in self.active_plugins: + try: + plugin.on_app_shutdown() + except Exception: + from babase import _error + + _error.print_exception('Error in plugin on_app_shutdown()') + + def load_plugins(self) -> None: + """(internal)""" + + # Load plugins from any specs that are enabled & able to. + for _class_path, plug_spec in sorted(self.plugin_specs.items()): + plugin = plug_spec.attempt_load_if_enabled() + if plugin is not None: + self.active_plugins.append(plugin) + + +class PluginSpec: + """Represents a plugin the engine knows about. Category: **App Classes** - These generally represent plugins which were detected by the - meta-tag scan. However they may also represent plugins which - were previously set to be loaded but which were unable to be - for some reason. In that case, 'available' will be set to False. + The 'enabled' attr represents whether this plugin is set to load. + Getting or setting that attr affects the corresponding app-config + 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 + 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, + or if the user has explicitly disabled a plugin, the engine will not + even attempt to load it. """ - display_name: babase.Lstr - class_path: str - available: bool + def __init__(self, class_path: str, loadable: bool): + self.class_path = class_path + self.loadable = loadable + self.attempted_load = False + self.plugin: Plugin | None = None + + @property + def enabled(self) -> bool: + """Whether the user wants this plugin to load.""" + plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {}) + assert isinstance(plugstates, dict) + val = plugstates.get(self.class_path, {}).get('enabled', False) is True + return val + + @enabled.setter + def enabled(self, val: bool) -> None: + plugstates: dict[str, dict] = _babase.app.config.setdefault( + 'Plugins', {} + ) + assert isinstance(plugstates, dict) + plugstate = plugstates.setdefault(self.class_path, {}) + plugstate['enabled'] = val + + def attempt_load_if_enabled(self) -> Plugin | None: + """Possibly load the plugin and report errors.""" + from babase._general import getclass + from babase._language import Lstr + + assert not self.attempted_load + assert self.plugin is None + + if not self.enabled: + return None + self.attempted_load = True + if not self.loadable: + return None + try: + cls = getclass(self.class_path, Plugin) + except Exception as exc: + _babase.getsimplesound('error').play() + _babase.screenmessage( + Lstr( + resource='pluginClassLoadErrorText', + subs=[ + ('${PLUGIN}', self.class_path), + ('${ERROR}', str(exc)), + ], + ), + color=(1, 0, 0), + ) + logging.exception( + "Error loading plugin class '%s'.", self.class_path + ) + return None + try: + self.plugin = cls() + return self.plugin + except Exception as exc: + from babase import _error + + _babase.getsimplesound('error').play() + _babase.screenmessage( + Lstr( + resource='pluginInitErrorText', + subs=[ + ('${PLUGIN}', self.class_path), + ('${ERROR}', str(exc)), + ], + ), + color=(1, 0, 0), + ) + logging.exception( + "Error initing plugin class: '%s'.", self.class_path + ) + return None class Plugin: diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index 3c48c583..ca32a9e2 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 21148 +TARGET_BALLISTICA_BUILD = 21150 TARGET_BALLISTICA_VERSION = '1.7.21' _g_env_config: EnvConfig | None = None diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py index 418e1d2c..6dde577d 100644 --- a/src/assets/ba_data/python/bauiv1/__init__.py +++ b/src/assets/ba_data/python/bauiv1/__init__.py @@ -66,7 +66,7 @@ from babase import ( NotFoundError, Permission, Plugin, - PotentialPlugin, + PluginSpec, pushcall, quit, request_permission, @@ -187,7 +187,7 @@ __all__ = [ 'open_url', 'Permission', 'Plugin', - 'PotentialPlugin', + 'PluginSpec', 'pushcall', 'quit', 'request_permission', diff --git a/src/assets/ba_data/python/bauiv1lib/settings/plugins.py b/src/assets/ba_data/python/bauiv1lib/settings/plugins.py index 635f80b0..0c7c58ce 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/plugins.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/plugins.py @@ -179,13 +179,13 @@ class PluginWindow(bui.Window): 'Still scanning plugins; please try again.', color=(1, 0, 0) ) bui.getsound('error').play() - pluglist = bui.app.plugins.potential_plugins - plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {}) + plugspecs = bui.app.plugins.plugin_specs + plugstates: dict[str, dict] = bui.app.config.get('Plugins', {}) assert isinstance(plugstates, dict) plug_line_height = 50 sub_width = self._scroll_width - sub_height = len(pluglist) * plug_line_height + sub_height = len(plugspecs) * plug_line_height self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(sub_width, sub_height), @@ -197,9 +197,7 @@ class PluginWindow(bui.Window): ) self._restore_state() - def _check_value_changed( - self, plug: bui.PotentialPlugin, value: bool - ) -> None: + def _check_value_changed(self, plug: bui.PluginSpec, value: bool) -> None: bui.screenmessage( bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'), color=(1.0, 0.5, 0.0), @@ -266,7 +264,7 @@ class PluginWindow(bui.Window): # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements - pluglist = bui.app.plugins.potential_plugins + plugspecs = bui.app.plugins.plugin_specs plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {}) assert isinstance(plugstates, dict) @@ -275,17 +273,18 @@ class PluginWindow(bui.Window): num_enabled = 0 num_disabled = 0 - for i, availplug in enumerate(pluglist): + plugspecs_sorted = sorted(plugspecs.items()) + + for _classpath, plugspec in plugspecs_sorted: # counting number of enabled and disabled plugins - plugstate = plugstates.setdefault(availplug.class_path, {}) - enabled = plugstate.get('enabled', False) - if enabled: + # plugstate = plugstates.setdefault(plugspec[0], {}) + if plugspec.enabled: num_enabled += 1 - elif availplug.available and not enabled: + else: num_disabled += 1 if self._category is Category.ALL: - sub_height = len(pluglist) * plug_line_height + sub_height = len(plugspecs) * plug_line_height bui.containerwidget( edit=self._subcontainer, size=(self._scroll_width, sub_height) ) @@ -305,20 +304,16 @@ class PluginWindow(bui.Window): sub_height = 0 num_shown = 0 - for i, availplug in enumerate(pluglist): - plugin = bui.app.plugins.active_plugins.get(availplug.class_path) - active = plugin is not None - - plugstate = plugstates.setdefault(availplug.class_path, {}) - checked = plugstate.get('enabled', False) - assert isinstance(checked, bool) + for classpath, plugspec in plugspecs_sorted: + plugin = plugspec.plugin + enabled = plugspec.enabled if self._category is Category.ALL: show = True elif self._category is Category.ENABLED: - show = checked + show = enabled elif self._category is Category.DISABLED: - show = availplug.available and not checked + show = not enabled else: assert_never(self._category) show = False @@ -329,25 +324,24 @@ class PluginWindow(bui.Window): item_y = sub_height - (num_shown + 1) * plug_line_height check = bui.checkboxwidget( parent=self._subcontainer, - text=availplug.display_name, + text=bui.Lstr(value=classpath), autoselect=True, - value=checked, + value=enabled, maxwidth=self._scroll_width - 200, position=(10, item_y), size=(self._scroll_width - 40, 50), on_value_change_call=bui.Call( - self._check_value_changed, availplug + self._check_value_changed, plugspec ), textcolor=( (0.8, 0.3, 0.3) - if not availplug.available - else (0, 1, 0) - if active and checked - else (0.8, 0.3, 0.3) - if checked + if (plugspec.attempted_load and plugspec.plugin is None) else (0.6, 0.6, 0.6) + if plugspec.plugin is None + else (0, 1, 0) ), ) + # noinspection PyUnresolvedReferences if plugin is not None and plugin.has_settings_ui(): button = bui.buttonwidget( parent=self._subcontainer, @@ -356,6 +350,7 @@ class PluginWindow(bui.Window): size=(100, 40), position=(sub_width - 130, item_y + 6), ) + # noinspection PyUnresolvedReferences bui.buttonwidget( edit=button, on_activate_call=bui.Call(plugin.show_settings_ui, button), @@ -364,7 +359,7 @@ class PluginWindow(bui.Window): button = None # Allow getting back to back button. - if i == 0: + if num_shown == 0: bui.widget( edit=check, up_widget=self._back_button, diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc index 3ab8c87b..1e20ba1f 100644 --- a/src/ballistica/core/platform/core_platform.cc +++ b/src/ballistica/core/platform/core_platform.cc @@ -663,19 +663,19 @@ auto CorePlatform::GetTextTextureData(void* tex) -> uint8_t* { void CorePlatform::OnMainThreadStartApp() {} void CorePlatform::OnAppStart() { - assert(g_base_soft && g_base_soft->InLogicThread()); + // assert(g_base_soft && g_base_soft->InLogicThread()); } void CorePlatform::OnAppPause() { - assert(g_base_soft && g_base_soft->InLogicThread()); + // assert(g_base_soft && g_base_soft->InLogicThread()); } void CorePlatform::OnAppResume() { - assert(g_base_soft && g_base_soft->InLogicThread()); + // assert(g_base_soft && g_base_soft->InLogicThread()); } void CorePlatform::OnAppShutdown() { - assert(g_base_soft && g_base_soft->InLogicThread()); + // assert(g_base_soft && g_base_soft->InLogicThread()); } void CorePlatform::OnScreenSizeChange() { diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index 31089a65..1d85cf6b 100644 --- a/src/ballistica/shared/ballistica.cc +++ b/src/ballistica/shared/ballistica.cc @@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int { namespace ballistica { // These are set automatically via script; don't modify them here. -const int kEngineBuildNumber = 21148; +const int kEngineBuildNumber = 21150; const char* kEngineVersion = "1.7.21"; auto MonolithicMain(const core::CoreConfig& core_config) -> int {