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 {