various plugin subsystem improvements

This commit is contained in:
Eric 2023-06-26 22:37:44 -07:00
parent 3a6a35749a
commit 8ef31acc81
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
12 changed files with 376 additions and 259 deletions

44
.efrocachemap generated
View File

@ -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/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/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/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/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/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/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/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/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/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", "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/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/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/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/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/85/7b/fa206153e23235bd97eb3abfb45a", "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/c8/97/a251315351949cbe40914f3f02a3", "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/7f/07/1ba1983b2ad7b1061f0c7624ee87", "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/14/3b/984737685ea813a3a8d587fd9083", "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/03/ee/59971d68ce248f89175441bc232c", "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/69/0a/1a067c77c85e2a57fccdd553fe44", "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/d8/af/beea487d4cd9b68e323af8190bdb", "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/7f/aa/f77f3330bea584a9710f13485ac0", "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/fa/1e/f0c6f0a6edf00bd4975c30b44957", "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/eb/ca/e8aa9629a9da31c838e8d7dd0f9d", "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/d0/13/08ed924a1032c25b5404f505398b", "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/b6/10/dd86496c0e5eac41803e43c552d3", "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/89/d2/087efa3672aa053f6456cf5d263f", "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/aa/a5/7a4703567c6f754e2248c115fb09", "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/71/c1/e52972ac5b26273ad841da7d664b", "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/b3/2f/1af856ae52297f7952329efc7617", "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/84/5c/b58affdbdae8e334f7dcff14c684", "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/4b/e7/5964489b7d22e27a413c9edb9eb3", "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/1e/c9/770a9e209a63de714aff7da7e8ee", "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/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_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", "build/prefab/lib/linux_arm64_server/debug/libballistica_plus.a": "https://files.ballistica.net/cache/ba1/be/19/b5458933dfc7371d91ecfcd2e06f",

View File

@ -2133,6 +2133,8 @@
<w>plugkeys</w> <w>plugkeys</w>
<w>pluglist</w> <w>pluglist</w>
<w>plugnames</w> <w>plugnames</w>
<w>plugspec</w>
<w>plugspecs</w>
<w>plugstate</w> <w>plugstate</w>
<w>plugstates</w> <w>plugstates</w>
<w>plusbutton</w> <w>plusbutton</w>

View File

@ -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. - Fixed an issue where server builds would not always include collision meshes.
- Upgraded Python to 3.11.4 on Android builds. - 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 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 avoids a crash that could occur when more data was provided than could
physically fit in the qr code. 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) ### 1.7.20 (build 21140, api 8, 2023-06-22)

View File

@ -1246,6 +1246,8 @@
<w>plen</w> <w>plen</w>
<w>pluginsettings</w> <w>pluginsettings</w>
<w>plugnames</w> <w>plugnames</w>
<w>plugspec</w>
<w>plugspecs</w>
<w>plusnet</w> <w>plusnet</w>
<w>pname</w> <w>pname</w>
<w>pnamel</w> <w>pnamel</w>

View File

@ -148,7 +148,7 @@ from babase._mgen.enums import (
from babase._math import normalized_color, is_point_in_box, vec3validate from babase._math import normalized_color, is_point_in_box, vec3validate
from babase._meta import MetadataSubsystem from babase._meta import MetadataSubsystem
from babase._net import get_ip_address_type, DEFAULT_REQUEST_TIMEOUT_SECONDS 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 from babase._text import timestring
_babase.app = app = App() _babase.app = app = App()
@ -252,7 +252,7 @@ __all__ = [
'PlayerNotFoundError', 'PlayerNotFoundError',
'Plugin', 'Plugin',
'PluginSubsystem', 'PluginSubsystem',
'PotentialPlugin', 'PluginSpec',
'print_error', 'print_error',
'print_exception', 'print_exception',
'print_load_info', 'print_load_info',

View File

@ -40,8 +40,8 @@ class ScanResults:
"""Final results from a meta-scan.""" """Final results from a meta-scan."""
exports: dict[str, list[str]] = field(default_factory=dict) exports: dict[str, list[str]] = field(default_factory=dict)
errors: list[str] = field(default_factory=list) incorrect_api_modules: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list) announce_errors_occurred: bool = False
def exports_of_class(self, cls: type) -> list[str]: def exports_of_class(self, cls: type) -> list[str]:
"""Return exports of a given class.""" """Return exports of a given class."""
@ -181,8 +181,9 @@ class MetadataSubsystem:
self._scan.run() self._scan.run()
results = self._scan.results results = self._scan.results
self._scan = None self._scan = None
except Exception as exc: except Exception:
results = ScanResults(errors=[f'Scan exception: {exc}']) logging.exception('metascan: Error running scan in bg.')
results = ScanResults(announce_errors_occurred=True)
# Place results and tell the logic thread they're ready. # Place results and tell the logic thread they're ready.
self.scanresults = results self.scanresults = results
@ -197,28 +198,44 @@ class MetadataSubsystem:
results = self.scanresults results = self.scanresults
assert results is not None assert results is not None
# Spit out any warnings/errors that happened. do_play_error_sound = False
# 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
# 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( _babase.screenmessage(
Lstr(resource='scanScriptsErrorText'), color=(1, 0, 0) Lstr(resource='scanScriptsErrorText'), color=(1, 0, 0)
) )
_babase.getsimplesound('error').play() do_play_error_sound = True
if results.warnings: if do_play_error_sound:
allwarnings = textwrap.indent( _babase.getsimplesound('error').play()
'\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)
# Let the game know we're done. # Let the game know we're done.
assert self._scan_complete_cb is not None assert self._scan_complete_cb is not None
@ -262,11 +279,7 @@ class DirectoryScan:
try: try:
self._scan_module(moduledir, subpath) self._scan_module(moduledir, subpath)
except Exception: except Exception:
import traceback logging.exception("metascan: Error scanning '%s'.", subpath)
self.results.warnings.append(
f"Error scanning '{subpath}': " + traceback.format_exc()
)
# Sort our results # Sort our results
for exportlist in self.results.exports.values(): for exportlist in self.results.exports.values():
@ -289,9 +302,10 @@ class DirectoryScan:
except PermissionError: except PermissionError:
# Expected sometimes. # Expected sometimes.
entries = [] entries = []
except Exception as exc: except Exception:
# Unexpected; report this. # 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 = [] entries = []
# Now identify python packages/modules out of what we found. # 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 # If we find a module requiring a different api version, warn
# and ignore. # and ignore.
if required_api is not None and required_api != CURRENT_API_VERSION: if required_api is not None and required_api != CURRENT_API_VERSION:
self.results.warnings.append( logging.warning(
f'{subpath} requires api {required_api} but' 'metascan: %s requires api %s but we are running'
f' we are running {CURRENT_API_VERSION}. Ignoring module.' ' %s. Ignoring module.',
subpath,
required_api,
CURRENT_API_VERSION,
)
self.results.incorrect_api_modules.append(
self._module_name_for_subpath(subpath)
) )
return return
@ -349,11 +369,13 @@ class DirectoryScan:
if submodule[1].name != '__init__.py': if submodule[1].name != '__init__.py':
self._scan_module(submodule[0], submodule[1]) self._scan_module(submodule[0], submodule[1])
except Exception: except Exception:
import traceback logging.exception('metascan: Error scanning %s.', subpath)
self.results.warnings.append( def _module_name_for_subpath(self, subpath: Path) -> str:
f"Error scanning '{subpath}': {traceback.format_exc()}" # (should not be getting these)
) assert '__init__.py' not in str(subpath)
return '.'.join(subpath.parts).removesuffix('.py')
def _process_module_meta_tags( def _process_module_meta_tags(
self, subpath: Path, flines: list[str], meta_lines: dict[int, list[str]] 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 # meta_lines is just anything containing '# ba_meta '; make sure
# the ba_meta is in the right place. # the ba_meta is in the right place.
if mline[0] != 'ba_meta': if mline[0] != 'ba_meta':
self.results.warnings.append( logging.warning(
f'Warning: {subpath}:' 'metascan: %s:%d: malformed ba_meta statement.',
f' malformed ba_meta statement on line {lindex + 1}.' subpath,
lindex + 1,
) )
self.results.announce_errors_occurred = True
elif ( elif (
len(mline) == 4 and mline[1] == 'require' and mline[2] == 'api' 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': elif len(mline) != 3 or mline[1] != 'export':
# Currently we only support 'ba_meta export FOO'; # Currently we only support 'ba_meta export FOO';
# complain for anything else we see. # complain for anything else we see.
self.results.warnings.append( logging.warning(
f'Warning: {subpath}' 'metascan: %s:%d: unrecognized ba_meta statement.',
f': unrecognized ba_meta statement on line {lindex + 1}.' subpath,
lindex + 1,
) )
self.results.announce_errors_occurred = True
else: else:
# Looks like we've got a valid export line! # Looks like we've got a valid export line!
modulename = '.'.join(subpath.parts) modulename = self._module_name_for_subpath(subpath)
if subpath.name.endswith('.py'):
modulename = modulename[:-3]
exporttypestr = mline[2] exporttypestr = mline[2]
export_class_name = self._get_export_class_name( export_class_name = self._get_export_class_name(
subpath, flines, lindex subpath, flines, lindex
@ -395,11 +419,14 @@ class DirectoryScan:
# classes we need to migrate people to using base # classes we need to migrate people to using base
# class names for them. # class names for them.
if exporttypestr == 'game': if exporttypestr == 'game':
self.results.warnings.append( logging.warning(
f'{subpath}:' "metascan: %s:%d: '# ba_meta export"
" '# ba_meta export game' tag should be replaced by" " game' tag should be replaced by '# ba_meta"
f" '# ba_meta export bascenev1.GameActivity'." " export bascenev1.GameActivity'.",
subpath,
lindex + 1,
) )
self.results.announce_errors_occurred = True
else: else:
# If export type is one of our shortcuts, sub in the # If export type is one of our shortcuts, sub in the
# actual class path. Otherwise assume its a classpath # actual class path. Otherwise assume its a classpath
@ -434,10 +461,13 @@ class DirectoryScan:
classname = cbits[0] classname = cbits[0]
break # Success! break # Success!
if classname is None: if classname is None:
self.results.warnings.append( logging.warning(
f'Warning: {subpath}: class definition not found below' 'metascan: %s:%d: class definition not found below'
f' "ba_meta export" statement on line {lindexorig + 1}.' " 'ba_meta export' statement.",
subpath,
lindexorig + 1,
) )
self.results.announce_errors_occurred = True
return classname return classname
def _get_api_requirement( def _get_api_requirement(
@ -460,23 +490,26 @@ class DirectoryScan:
and l[3].isdigit() 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: if len(lines) == 1:
return int(lines[0][3]) return int(lines[0][3])
# Ok; not successful. lets issue warnings for a few error cases. # Ok; not successful. lets issue warnings for a few error cases.
if len(lines) > 1: if len(lines) > 1:
self.results.warnings.append( logging.warning(
f'Warning: {subpath}: multiple' "metascan: %s: multiple '# ba_meta require api <NUM>'"
' "# ba_meta require api <NUM>" lines found;' ' lines found; ignoring module.',
' ignoring module.' subpath,
) )
self.results.announce_errors_occurred = True
elif not lines and toplevel and meta_lines: elif not lines and toplevel and meta_lines:
# If we're a top-level module containing meta lines but # If we're a top-level module containing meta lines but no
# no valid "require api" line found, complain. # valid "require api" line found, complain.
self.results.warnings.append( logging.warning(
f'Warning: {subpath}:' "metascan: %s: no valid '# ba_meta require api <NUM>"
' no valid "# ba_meta require api <NUM>" line found;' ' line found; ignoring module.',
' ignoring module.' subpath,
) )
self.results.announce_errors_occurred = True
return None return None

View File

@ -7,7 +7,6 @@ from __future__ import annotations
import logging import logging
import importlib.util import importlib.util
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dataclasses import dataclass
import _babase import _babase
from babase._appsubsystem import AppSubsystem from babase._appsubsystem import AppSubsystem
@ -31,14 +30,21 @@ class PluginSubsystem(AppSubsystem):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() 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: 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 from babase._language import Lstr
plugs = _babase.app.plugins
config_changed = False config_changed = False
found_new = False found_new = False
plugstates: dict[str, dict] = _babase.app.config.setdefault( plugstates: dict[str, dict] = _babase.app.config.setdefault(
@ -56,146 +62,75 @@ class PluginSubsystem(AppSubsystem):
) )
is True 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): for class_path in results.exports_of_class(Plugin):
plugs.potential_plugins.append( assert class_path not in self.plugin_specs
PotentialPlugin( plugspec = self.plugin_specs[class_path] = PluginSpec(
display_name=Lstr(value=class_path), class_path=class_path, loadable=True
class_path=class_path,
available=True,
)
) )
# Auto-enable new ones if desired.
if auto_enable_new_plugins: if auto_enable_new_plugins:
if class_path not in plugstates: if class_path not in plugstates:
# Go ahead and enable new plugins by default, but we'll plugspec.enabled = True
# 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}
config_changed = True config_changed = True
found_new = True found_new = True
plugs.potential_plugins.sort(key=lambda p: p.class_path) # If we're *not* auto-enabling, just let the user know if we
# found new ones.
# If we're *not* auto-enabling new plugins, at least let the
# user know we found something new.
if found_new and not auto_enable_new_plugins: if found_new and not auto_enable_new_plugins:
_babase.screenmessage( _babase.screenmessage(
Lstr(resource='pluginsDetectedText'), color=(0, 1, 0) Lstr(resource='pluginsDetectedText'), color=(0, 1, 0)
) )
_babase.getsimplesound('ding').play() _babase.getsimplesound('ding').play()
if config_changed: # Ok, now go through all plugins registered in the app-config
_babase.app.config.commit() # that weren't covered by the meta stuff above, either creating
# plugin-specs for them or clearing them out. This covers
def on_app_running(self) -> None: # plugins with api versions not matching ours, plugins without
# Load up our plugins and go ahead and call their on_app_running calls. # ba_meta tags, and plugins that have since disappeared.
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', {})
assert isinstance(plugstates, dict) assert isinstance(plugstates, dict)
plugkeys: list[str] = sorted( wrong_api_prefixes = [f'{m}.' for m in results.incorrect_api_modules]
key for key, val in plugstates.items() if val.get('enabled', False)
)
disappeared_plugs: set[str] = set() disappeared_plugs: set[str] = set()
for plugkey in plugkeys:
# Originally I was just catching ModuleNotFoundError on the for class_path in sorted(plugstates.keys()):
# getclass() call to detect plugins disappearing. However # Already have a spec for it; nothing to be done.
# this breaks if the module *does* exist but itself imports if class_path in self.plugin_specs:
# something that does not exist; in that case we would continue
# incorrectly show that the plugin had disappeared.
# # If this plugin corresponds to any modules that we've
# So now we're first explicitly asking Python if it can # identified as having incorrect api versions, we'll take
# locate the module, and if it can then we treat any further # note of its existence but we won't try to load it.
# errors including ModuleNotFound as problems with the if any(
# module's code; not ours. 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: try:
spec = importlib.util.find_spec(plugkey.split('.')[0]) spec = importlib.util.find_spec(
'.'.join(class_path.split('.')[:-1])
)
except Exception: except Exception:
spec = None spec = None
if spec is None: if spec is None:
disappeared_plugs.add(plugkey) disappeared_plugs.add(class_path)
continue 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 # 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 # from the config so we'll again let the user know if they later
# reappear. This makes it much smoother to switch between users # reappear. This makes it much smoother to switch between users
@ -220,22 +155,151 @@ class PluginSubsystem(AppSubsystem):
del _babase.app.config['Plugins'][goneplug] del _babase.app.config['Plugins'][goneplug]
_babase.app.config.commit() _babase.app.config.commit()
if config_changed:
_babase.app.config.commit()
@dataclass def on_app_running(self) -> None:
class PotentialPlugin: # Load up our plugins and go ahead and call their on_app_running
"""Represents a babase.Plugin which can potentially be loaded. # 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** Category: **App Classes**
These generally represent plugins which were detected by the The 'enabled' attr represents whether this plugin is set to load.
meta-tag scan. However they may also represent plugins which Getting or setting that attr affects the corresponding app-config
were previously set to be loaded but which were unable to be key. Remember to commit the app-config after making any changes.
for some reason. In that case, 'available' will be set to False.
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 def __init__(self, class_path: str, loadable: bool):
class_path: str self.class_path = class_path
available: bool 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: class Plugin:

View File

@ -28,7 +28,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be # Build number and version of the ballistica binary we expect to be
# using. # using.
TARGET_BALLISTICA_BUILD = 21148 TARGET_BALLISTICA_BUILD = 21150
TARGET_BALLISTICA_VERSION = '1.7.21' TARGET_BALLISTICA_VERSION = '1.7.21'
_g_env_config: EnvConfig | None = None _g_env_config: EnvConfig | None = None

View File

@ -66,7 +66,7 @@ from babase import (
NotFoundError, NotFoundError,
Permission, Permission,
Plugin, Plugin,
PotentialPlugin, PluginSpec,
pushcall, pushcall,
quit, quit,
request_permission, request_permission,
@ -187,7 +187,7 @@ __all__ = [
'open_url', 'open_url',
'Permission', 'Permission',
'Plugin', 'Plugin',
'PotentialPlugin', 'PluginSpec',
'pushcall', 'pushcall',
'quit', 'quit',
'request_permission', 'request_permission',

View File

@ -179,13 +179,13 @@ class PluginWindow(bui.Window):
'Still scanning plugins; please try again.', color=(1, 0, 0) 'Still scanning plugins; please try again.', color=(1, 0, 0)
) )
bui.getsound('error').play() bui.getsound('error').play()
pluglist = bui.app.plugins.potential_plugins plugspecs = bui.app.plugins.plugin_specs
plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {}) plugstates: dict[str, dict] = bui.app.config.get('Plugins', {})
assert isinstance(plugstates, dict) assert isinstance(plugstates, dict)
plug_line_height = 50 plug_line_height = 50
sub_width = self._scroll_width sub_width = self._scroll_width
sub_height = len(pluglist) * plug_line_height sub_height = len(plugspecs) * plug_line_height
self._subcontainer = bui.containerwidget( self._subcontainer = bui.containerwidget(
parent=self._scrollwidget, parent=self._scrollwidget,
size=(sub_width, sub_height), size=(sub_width, sub_height),
@ -197,9 +197,7 @@ class PluginWindow(bui.Window):
) )
self._restore_state() self._restore_state()
def _check_value_changed( def _check_value_changed(self, plug: bui.PluginSpec, value: bool) -> None:
self, plug: bui.PotentialPlugin, value: bool
) -> None:
bui.screenmessage( bui.screenmessage(
bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'), bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'),
color=(1.0, 0.5, 0.0), color=(1.0, 0.5, 0.0),
@ -266,7 +264,7 @@ class PluginWindow(bui.Window):
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-statements # 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', {}) plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {})
assert isinstance(plugstates, dict) assert isinstance(plugstates, dict)
@ -275,17 +273,18 @@ class PluginWindow(bui.Window):
num_enabled = 0 num_enabled = 0
num_disabled = 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 # counting number of enabled and disabled plugins
plugstate = plugstates.setdefault(availplug.class_path, {}) # plugstate = plugstates.setdefault(plugspec[0], {})
enabled = plugstate.get('enabled', False) if plugspec.enabled:
if enabled:
num_enabled += 1 num_enabled += 1
elif availplug.available and not enabled: else:
num_disabled += 1 num_disabled += 1
if self._category is Category.ALL: if self._category is Category.ALL:
sub_height = len(pluglist) * plug_line_height sub_height = len(plugspecs) * plug_line_height
bui.containerwidget( bui.containerwidget(
edit=self._subcontainer, size=(self._scroll_width, sub_height) edit=self._subcontainer, size=(self._scroll_width, sub_height)
) )
@ -305,20 +304,16 @@ class PluginWindow(bui.Window):
sub_height = 0 sub_height = 0
num_shown = 0 num_shown = 0
for i, availplug in enumerate(pluglist): for classpath, plugspec in plugspecs_sorted:
plugin = bui.app.plugins.active_plugins.get(availplug.class_path) plugin = plugspec.plugin
active = plugin is not None enabled = plugspec.enabled
plugstate = plugstates.setdefault(availplug.class_path, {})
checked = plugstate.get('enabled', False)
assert isinstance(checked, bool)
if self._category is Category.ALL: if self._category is Category.ALL:
show = True show = True
elif self._category is Category.ENABLED: elif self._category is Category.ENABLED:
show = checked show = enabled
elif self._category is Category.DISABLED: elif self._category is Category.DISABLED:
show = availplug.available and not checked show = not enabled
else: else:
assert_never(self._category) assert_never(self._category)
show = False show = False
@ -329,25 +324,24 @@ class PluginWindow(bui.Window):
item_y = sub_height - (num_shown + 1) * plug_line_height item_y = sub_height - (num_shown + 1) * plug_line_height
check = bui.checkboxwidget( check = bui.checkboxwidget(
parent=self._subcontainer, parent=self._subcontainer,
text=availplug.display_name, text=bui.Lstr(value=classpath),
autoselect=True, autoselect=True,
value=checked, value=enabled,
maxwidth=self._scroll_width - 200, maxwidth=self._scroll_width - 200,
position=(10, item_y), position=(10, item_y),
size=(self._scroll_width - 40, 50), size=(self._scroll_width - 40, 50),
on_value_change_call=bui.Call( on_value_change_call=bui.Call(
self._check_value_changed, availplug self._check_value_changed, plugspec
), ),
textcolor=( textcolor=(
(0.8, 0.3, 0.3) (0.8, 0.3, 0.3)
if not availplug.available if (plugspec.attempted_load and plugspec.plugin is None)
else (0, 1, 0)
if active and checked
else (0.8, 0.3, 0.3)
if checked
else (0.6, 0.6, 0.6) 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(): if plugin is not None and plugin.has_settings_ui():
button = bui.buttonwidget( button = bui.buttonwidget(
parent=self._subcontainer, parent=self._subcontainer,
@ -356,6 +350,7 @@ class PluginWindow(bui.Window):
size=(100, 40), size=(100, 40),
position=(sub_width - 130, item_y + 6), position=(sub_width - 130, item_y + 6),
) )
# noinspection PyUnresolvedReferences
bui.buttonwidget( bui.buttonwidget(
edit=button, edit=button,
on_activate_call=bui.Call(plugin.show_settings_ui, button), on_activate_call=bui.Call(plugin.show_settings_ui, button),
@ -364,7 +359,7 @@ class PluginWindow(bui.Window):
button = None button = None
# Allow getting back to back button. # Allow getting back to back button.
if i == 0: if num_shown == 0:
bui.widget( bui.widget(
edit=check, edit=check,
up_widget=self._back_button, up_widget=self._back_button,

View File

@ -663,19 +663,19 @@ auto CorePlatform::GetTextTextureData(void* tex) -> uint8_t* {
void CorePlatform::OnMainThreadStartApp() {} void CorePlatform::OnMainThreadStartApp() {}
void CorePlatform::OnAppStart() { void CorePlatform::OnAppStart() {
assert(g_base_soft && g_base_soft->InLogicThread()); // assert(g_base_soft && g_base_soft->InLogicThread());
} }
void CorePlatform::OnAppPause() { void CorePlatform::OnAppPause() {
assert(g_base_soft && g_base_soft->InLogicThread()); // assert(g_base_soft && g_base_soft->InLogicThread());
} }
void CorePlatform::OnAppResume() { void CorePlatform::OnAppResume() {
assert(g_base_soft && g_base_soft->InLogicThread()); // assert(g_base_soft && g_base_soft->InLogicThread());
} }
void CorePlatform::OnAppShutdown() { void CorePlatform::OnAppShutdown() {
assert(g_base_soft && g_base_soft->InLogicThread()); // assert(g_base_soft && g_base_soft->InLogicThread());
} }
void CorePlatform::OnScreenSizeChange() { void CorePlatform::OnScreenSizeChange() {

View File

@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
namespace ballistica { namespace ballistica {
// These are set automatically via script; don't modify them here. // 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"; const char* kEngineVersion = "1.7.21";
auto MonolithicMain(const core::CoreConfig& core_config) -> int { auto MonolithicMain(const core::CoreConfig& core_config) -> int {