diff --git a/.efrocachemap b/.efrocachemap
index 23462ceb..ae607fc4 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -4135,16 +4135,16 @@
"assets/build/windows/x64/vc_redist.x64.exe": "https://files.ballistica.net/cache/ba1/ea/19/8b8787d81abcdce158ba608cd24f",
"assets/build/windows/x64/vcruntime140_1d.dll": "https://files.ballistica.net/cache/ba1/11/d8/ff6344b429b00c24d9a1930d4338",
"assets/build/windows/x64/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/20/33/0825e11e6518f87ece3009309933",
- "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/26/e9/3cc27e1957894e2926093efaf419",
- "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cc/6d/523557326436bb1f2c79b40db30f",
- "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/59/b5/cd299564c2795a47f19253121289",
- "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/90/15/400a238c241f4baaf593338c2f0e",
- "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4a/17/9c657df88fcf1aa529a6d57c1c63",
- "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2c/14/4af6ef9f9452c10d3dbc7b864045",
- "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/11/05/2d5de66e04958be2440d10c96694",
- "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/89/c8/2e9218badd4259e480c3b3315bf6",
- "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/7e/7b/173c424cd6548670163304241161",
- "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/bc/d9/52268ba89bf9e6821fd95d4b6852",
- "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e3/49/f6b920cf41705a40f7c00ad2cd26",
- "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d4/9f/365187c2ebdeffdfba74a35cf4f7"
+ "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b2/16/0414b009ce117f73bb2bc8991d3b",
+ "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/66/07/d82a3ec80ff3243321e373439210",
+ "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/12/5f/f87158c3281a2616897fdd2165cb",
+ "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/69/23/a2d029f81db60cf89ad5f6510ddb",
+ "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c4/e3/5aba369252ed1977be8950e5a1ce",
+ "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f7/ef/c28554f863485e9cd54f0d8d4e6f",
+ "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/68/af/f060b3463bc31c202a830fb0b20a",
+ "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/96/46/a239821d00e782303077192817c5",
+ "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/dc/15/fdd9195afe2bf299405ec49ed238",
+ "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/68/f9/c42b1f9b5af34cb371f80250e16b",
+ "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/5d/9b/213c602a9aa9cbc28872d2753f51",
+ "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/2f/a4/e6a6a6e0e2b64c5232cccddffdd3"
}
\ No newline at end of file
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index c1bf311e..9bcbca70 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -570,6 +570,7 @@
elim
emitfx
emoji
+ emojis
enablexinput
ename
encerr
@@ -1453,7 +1454,10 @@
plistname
plpt
plst
+ plugkey
+ plugkeys
pluglist
+ plugstate
plugstates
plusbutton
plvel
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63563696..e7a1696f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
- Fixed the shebang line in `bombsquad_server` file by using `-S` flag for `/usr/bin/env`.
- Fixed a bug with hardware keyboards emitting extra characters in the in-game console (~ or F2)
- Added support for 'plugin' mods and user controls to configure them in settings->advanced->plugins.
-- renamed selection_loop_to_parent to selection_loops_to_parent in widget calls.
+- Renamed selection_loop_to_parent to selection_loops_to_parent in widget calls.
- Added 'selection_loops_to_parent', 'border', 'margin', 'claims_left_right', and 'claims_tab' args to ba.columnwidget().
- Column-widget now has a default 'border' of 0 (explicitly pass 2 to get the old look).
- Column-widget now has a default 'margin' of 10 (explicitly pass 0 to get the old look).
@@ -10,7 +10,7 @@
- Added 'selection_loops_to_parent', 'claims_left_right', and 'claims_tab' args to ba.rowwidget.
- Added 'claims_left_right' and 'claims_tab' to ba.hscrollwidget().
- Default widget 'show_buffer' is now 20 instead of 0 (causes scrolling to stay slightly ahead of widget selection). This can be overridden with the ba.widget() call if anything breaks.
-- Relocated ba.app.uiscale to ba.app.ui.uiscale
+- Relocated ba.app.uiscale to ba.app.ui.uiscale.
- Top level settings window now properly saves/restores its state again.
- Added Emojis to the Internal Game Keyboard.
- Added continuous CAPITAL letters typing feature in the Internal Game Keyboard.
diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py
index 88e53a3e..6fa61a4c 100644
--- a/assets/src/ba_data/python/ba/__init__.py
+++ b/assets/src/ba_data/python/ba/__init__.py
@@ -38,7 +38,7 @@ from _ba import (CollideModel, Context, ContextCall, Data, InputDevice,
set_analytics_screen, charstr, textwidget, time, timer,
open_url, widget)
from ba._activity import Activity
-from ba._plugin import AvailablePlugin, Plugin
+from ba._plugin import PotentialPlugin, Plugin
from ba._actor import Actor
from ba._player import PlayerInfo, Player, EmptyPlayer, StandLocation
from ba._nodeactor import NodeActor
diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py
index e06e3903..d3265d40 100644
--- a/assets/src/ba_data/python/ba/_app.py
+++ b/assets/src/ba_data/python/ba/_app.py
@@ -306,8 +306,8 @@ class App:
assert isinstance(self.headless_build, bool)
# Plugins.
- self.loaded_plugins: List[ba.Plugin] = []
- self.available_plugins: List[ba.AvailablePlugin] = []
+ self.potential_plugins: List[ba.PotentialPlugin] = []
+ self.active_plugins: Dict[str, ba.Plugin] = {}
# Misc.
self.default_language = self._get_default_language()
@@ -411,6 +411,7 @@ class App:
(internal)"""
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
+ # pylint: disable=too-many-statements
from ba import _apputils
from ba import _appconfig
from ba import _achievement
@@ -520,11 +521,49 @@ class App:
_ba.pushcall(do_auto_sign_in)
+ # Load up our plugins and go ahead and call their on_app_launch calls.
+ self.load_plugins()
+ for plugin in self.active_plugins.values():
+ try:
+ plugin.on_app_launch()
+ except Exception:
+ from ba import _error
+ _error.print_exception('Error in plugin on_app_launch()')
+
self.ran_on_app_launch = True
# from ba._dependency import test_depset
# test_depset()
+ def load_plugins(self) -> None:
+ """(internal)"""
+ from ba._general import getclass
+ from ba._plugin import Plugin
+
+ # Note: the plugins we load is purely based on what's enabled
+ # in the app config. Our meta-scan gives us a list of available
+ # plugins, but that is only used to give the user a list of plugins
+ # that they can enable. (we wouldn't want to look at meta-scan here
+ # anyway because it may not be done yet at this point in the launch)
+ plugstates: Dict[str, Dict] = self.config.get('Plugins', {})
+ assert isinstance(plugstates, dict)
+ plugkeys: List[str] = sorted(key for key, val in plugstates.items()
+ if val.get('enabled', False))
+ for plugkey in plugkeys:
+ try:
+ cls = getclass(plugkey, Plugin)
+ except Exception as exc:
+ _ba.log(f"Error loading plugin class '{plugkey}': {exc}",
+ to_server=False)
+ continue
+ try:
+ plugin = cls()
+ assert plugkey not in self.active_plugins
+ self.active_plugins[plugkey] = plugin
+ except Exception:
+ from ba import _error
+ _error.print_exception(f'Error loading plugin: {plugkey}')
+
def read_config(self) -> None:
"""(internal)"""
from ba import _appconfig
diff --git a/assets/src/ba_data/python/ba/_meta.py b/assets/src/ba_data/python/ba/_meta.py
index c67bcca9..ecc5d727 100644
--- a/assets/src/ba_data/python/ba/_meta.py
+++ b/assets/src/ba_data/python/ba/_meta.py
@@ -66,6 +66,7 @@ def handle_scan_results(results: ScanResults) -> None:
"""Called in the game thread with results of a completed scan."""
from ba._lang import Lstr
+ from ba._plugin import PotentialPlugin
# Warnings generally only get printed locally for users' benefit
# (things like out-of-date scripts being ignored, etc.)
@@ -87,18 +88,32 @@ def handle_scan_results(results: ScanResults) -> None:
config_changed = False
found_new = False
plugstates: Dict[str, Dict] = _ba.app.config.setdefault('Plugins', {})
- if not isinstance(plugstates, dict):
- print('Warning; found non-dict for "Plugins" in config.')
- plugstates = {}
- config_changed = True
+ assert isinstance(plugstates, dict)
- for plug in results.plugins:
- if plug not in plugstates:
- print('found new plugin:', plug)
- plugstates[plug] = {'enabled': False}
+ # Create a potential-plugin for each class we found in the scan.
+ for class_path in results.plugins:
+ _ba.app.potential_plugins.append(
+ PotentialPlugin(display_name=Lstr(value=class_path),
+ class_path=class_path,
+ available=True))
+ if class_path not in plugstates:
+ plugstates[class_path] = {'enabled': False}
config_changed = True
found_new = True
+ # Also add a special one for any plugins set to load but *not* found
+ # in the scan (this way they will show up in the UI so we can disable them)
+ for class_path, plugstate in plugstates.items():
+ enabled = plugstate.get('enabled', False)
+ assert isinstance(enabled, bool)
+ if enabled and class_path not in results.plugins:
+ _ba.app.potential_plugins.append(
+ PotentialPlugin(display_name=Lstr(value=class_path),
+ class_path=class_path,
+ available=False))
+
+ _ba.app.potential_plugins.sort(key=lambda p: p.class_path)
+
if found_new:
_ba.screenmessage(Lstr(resource='pluginsDetectedText'),
color=(0, 1, 0))
@@ -106,7 +121,6 @@ def handle_scan_results(results: ScanResults) -> None:
if config_changed:
_ba.app.config.commit()
- # print(f'would check {len(results.plugins)} plugs')
class ScanThread(threading.Thread):
@@ -189,6 +203,9 @@ class DirectoryScan:
self.results.warnings += ("Error scanning '" + str(subpath) +
"': " + traceback.format_exc() +
'\n')
+ # Sort our results
+ self.results.games.sort()
+ self.results.plugins.sort()
def scan_module(self, moduledir: pathlib.Path,
subpath: pathlib.Path) -> None:
diff --git a/assets/src/ba_data/python/ba/_plugin.py b/assets/src/ba_data/python/ba/_plugin.py
index 146d7a7f..51b9b077 100644
--- a/assets/src/ba_data/python/ba/_plugin.py
+++ b/assets/src/ba_data/python/ba/_plugin.py
@@ -26,23 +26,35 @@ from typing import TYPE_CHECKING
from dataclasses import dataclass
if TYPE_CHECKING:
- pass
+ import ba
@dataclass
-class AvailablePlugin:
- """Defines a plugin which can potentially be loaded.
+class PotentialPlugin:
+ """Represents a ba.Plugin which can potentially be loaded.
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.
"""
- display_name: str
+ display_name: ba.Lstr
class_path: str
+ available: bool
class Plugin:
"""A plugin to alter app behavior in some way.
Category: App Classes
+
+ Plugins are discoverable by the meta-tag system
+ and the user can select which ones they want to activate.
+ Active plugins are then called at specific times as the
+ app is running in order to modify its behavior in some way.
"""
- name: str
+ def on_app_launch(self) -> None:
+ """Called when the app is being launched."""
diff --git a/assets/src/ba_data/python/bastd/gameutils.py b/assets/src/ba_data/python/bastd/gameutils.py
index d756995a..de34b2bd 100644
--- a/assets/src/ba_data/python/bastd/gameutils.py
+++ b/assets/src/ba_data/python/bastd/gameutils.py
@@ -161,13 +161,3 @@ class SharedObjects:
),
)
return self._railing_material
-
-
-# ba _meta export plugin
-class TestPlug1(ba.Plugin):
- """Just Testing."""
-
-
-# ba _meta export plugin
-class TestPlug2(ba.Plugin):
- """Just Testing 2."""
diff --git a/assets/src/ba_data/python/bastd/ui/settings/plugins.py b/assets/src/ba_data/python/bastd/ui/settings/plugins.py
index 6e73646a..7b1993b1 100644
--- a/assets/src/ba_data/python/bastd/ui/settings/plugins.py
+++ b/assets/src/ba_data/python/bastd/ui/settings/plugins.py
@@ -27,20 +27,16 @@ from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
- from typing import Tuple, Optional
+ from typing import Tuple, Optional, Dict
class PluginSettingsWindow(ba.Window):
"""Window for configuring plugins."""
- def __del__(self) -> None:
- print('~PluginSettingsWindow()')
-
def __init__(self,
transition: str = 'in_right',
origin_widget: ba.Widget = None):
- print('PluginSettingsWindow()')
-
+ # pylint: disable=too-many-locals
app = ba.app
# If they provided an origin-widget, scale up from that.
@@ -115,22 +111,29 @@ class PluginSettingsWindow(ba.Window):
self._subcontainer = ba.columnwidget(parent=self._scrollwidget,
selection_loops_to_parent=True)
- pluglist = [
- ba.AvailablePlugin(display_name=f'Test {i}',
- class_path='fakemodule') for i in range(10)
- ]
+ if ba.app.metascan is None:
+ ba.screenmessage('Still scanning plugins; please try again.',
+ color=(1, 0, 0))
+ ba.playsound(ba.getsound('error'))
+ pluglist = ba.app.potential_plugins
+ plugstates: Dict[str, Dict] = ba.app.config.setdefault('Plugins', {})
+ assert isinstance(plugstates, dict)
for i, availplug in enumerate(pluglist):
- active = i % 3 < 2
- check = ba.checkboxwidget(parent=self._subcontainer,
- text=availplug.display_name,
- value=active,
- maxwidth=self._scroll_width - 100,
- size=(self._scroll_width - 40, 50),
- on_value_change_call=ba.Call(
- self._check_value_changed,
- availplug),
- textcolor=((0, 1, 0) if active else
- (0.6, 0.6, 0.6)))
+ active = availplug.class_path in ba.app.active_plugins
+
+ plugstate = plugstates.setdefault(availplug.class_path, {})
+ checked = plugstate.get('enabled', False)
+ assert isinstance(checked, bool)
+ check = ba.checkboxwidget(
+ parent=self._subcontainer,
+ text=availplug.display_name,
+ value=checked,
+ maxwidth=self._scroll_width - 100,
+ size=(self._scroll_width - 40, 50),
+ on_value_change_call=ba.Call(self._check_value_changed,
+ availplug),
+ textcolor=((0.8, 0.3, 0.3) if not availplug.available else
+ (0, 1, 0) if active else (0.6, 0.6, 0.6)))
# Make sure we scroll all the way to the end when using
# keyboard/button nav.
@@ -144,12 +147,16 @@ class PluginSettingsWindow(ba.Window):
self._restore_state()
- def _check_value_changed(self, plug: ba.AvailablePlugin,
+ def _check_value_changed(self, plug: ba.PotentialPlugin,
value: bool) -> None:
ba.screenmessage(
ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'),
color=(1.0, 0.5, 0.0))
- print(f'check value changed for {plug} to {value}')
+ plugstates: Dict[str, Dict] = ba.app.config.setdefault('Plugins', {})
+ assert isinstance(plugstates, dict)
+ plugstate = plugstates.setdefault(plug.class_path, {})
+ plugstate['enabled'] = value
+ ba.app.config.commit()
def _save_state(self) -> None:
pass
diff --git a/docs/ba_module.md b/docs/ba_module.md
index 6ff42acd..2c401f48 100644
--- a/docs/ba_module.md
+++ b/docs/ba_module.md
@@ -1,5 +1,5 @@
-
last updated on 2020-07-21 for Ballistica version 1.5.23 build 20151
+last updated on 2020-07-22 for Ballistica version 1.5.23 build 20152
This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!
@@ -150,10 +150,10 @@
ba.App
ba.AppConfig
ba.AppDelegate
- ba.AvailablePlugin
ba.Campaign
ba.MusicPlayer
ba.Plugin
+ ba.PotentialPlugin
ba.ServerController
@@ -1166,22 +1166,6 @@ when done.
Behavior is similar to ba.gettexture()
-
-
-
-<top level class>
-
-Defines a plugin which can potentially be loaded.
-
-Category: App Classes
-
-
-Methods:
-
--
-
ba.AvailablePlugin(display_name: str, class_path: str)
-
@@ -4509,9 +4493,44 @@ the type-checker properly identifies the returned value as one.
A plugin to alter app behavior in some way.
-Category: App Classes
+
Category: App Classes
+
+ Plugins are discoverable by the meta-tag system
+ and the user can select which ones they want to activate.
+ Active plugins are then called at specific times as the
+ app is running in order to modify its behavior in some way.
+Methods:
+
+-
+
on_app_launch(self) -> None
+
+Called when the app is being launched.
+
+
+
+
+<top level class>
+
+Represents a ba.Plugin which can potentially be loaded.
+
+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.
+
+
+Methods:
+
+-
+
ba.PotentialPlugin(display_name: ba.Lstr, class_path: str, available: bool)
+
+
+
<top level class>