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/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",

View File

@ -2133,6 +2133,8 @@
<w>plugkeys</w>
<w>pluglist</w>
<w>plugnames</w>
<w>plugspec</w>
<w>plugspecs</w>
<w>plugstate</w>
<w>plugstates</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.
- 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)

View File

@ -1246,6 +1246,8 @@
<w>plen</w>
<w>pluginsettings</w>
<w>plugnames</w>
<w>plugspec</w>
<w>plugspecs</w>
<w>plusnet</w>
<w>pname</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._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',

View File

@ -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 <NUM>" lines found;'
' ignoring module.'
logging.warning(
"metascan: %s: multiple '# ba_meta require api <NUM>'"
' 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 <NUM>" 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 <NUM>"
' line found; ignoring module.',
subpath,
)
self.results.announce_errors_occurred = True
return None

View File

@ -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:

View File

@ -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

View File

@ -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',

View File

@ -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,

View File

@ -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() {

View File

@ -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 {