mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-21 11:27:38 +03:00
PLUGINS.md: s/EDMC/EDMarketConnector/ almost everywhere & reflow
The reflow is on *all* non-code/table text to be within 80 columns, excepting if a link just makes that impossible.
This commit is contained in:
parent
411bd0edf2
commit
6c34ceaba9
207
PLUGINS.md
207
PLUGINS.md
@ -1,6 +1,6 @@
|
||||
# Developer Plugin Documentation
|
||||
|
||||
Plugins allow you to customise and extend the behavior of EDMC.
|
||||
Plugins allow you to customise and extend the behavior of EDMarketConnector.
|
||||
|
||||
## Installing a Plugin
|
||||
|
||||
@ -12,28 +12,31 @@ wiki.
|
||||
Check [Releasing.md](docs/Releasing.md#environment) to be sure of the current
|
||||
version of Python that we've tested against.
|
||||
|
||||
Plugins are loaded when EDMC starts up.
|
||||
Plugins are loaded when EDMarketConnector starts up.
|
||||
|
||||
Each plugin has it's own folder in the `plugins` directory:
|
||||
|
||||
- Windows: `%LOCALAPPDATA%\EDMarketConnector\plugins`
|
||||
- Mac: `~/Library/Application Support/EDMarketConnector/plugins`
|
||||
- Linux: `$XDG_DATA_HOME/EDMarketConnector/plugins`, or `~/.local/share/EDMarketConnector/plugins` if `$XDG_DATA_HOME` is unset.
|
||||
- Linux: `$XDG_DATA_HOME/EDMarketConnector/plugins`, or
|
||||
`~/.local/share/EDMarketConnector/plugins` if `$XDG_DATA_HOME` is unset.
|
||||
|
||||
Plugins are python files. The plugin folder must have a file named `load.py`
|
||||
that must provide one module level function and optionally provide a few
|
||||
others.
|
||||
|
||||
If you're running from source (which allows for debugging with e.g. [PyCharm](https://www.jetbrains.com/pycharm/features/))
|
||||
If you're running from source (which allows for debugging with e.g.
|
||||
[PyCharm](https://www.jetbrains.com/pycharm/features/))
|
||||
then you'll need to be using an appropriate version of Python. The current
|
||||
version is listed in the [Environment section of Releasing.md](https://github.com/EDCD/EDMarketConnector/blob/main/docs/Releasing.md#environment).
|
||||
version is listed in the
|
||||
[Environment section of Releasing.md](https://github.com/EDCD/EDMarketConnector/blob/main/docs/Releasing.md#environment).
|
||||
If you're developing your plugin simply against an install of EDMarketConnector
|
||||
then you'll be relying on the bundled version of Python (it's baked
|
||||
into the .exe via the py2exe build process).
|
||||
|
||||
Please be sure to read the [Avoiding potential pitfalls](#avoiding-potential-pitfalls)
|
||||
section, else you might inadvertently cause issues for the core EDMC code
|
||||
including whole application crashes.
|
||||
section, else you might inadvertently cause issues for the core
|
||||
EDMarketConnector code including whole application crashes.
|
||||
|
||||
It is highly advisable to ensure you are aware of all EDMarketConnector
|
||||
releases, including the pre-releases. The -beta and -rc changelogs will
|
||||
@ -118,9 +121,9 @@ the packaged executeable all output is redirected to a log file. See
|
||||
[Reporting a problem](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#reporting-a-problem)
|
||||
for the location of this log file.
|
||||
|
||||
EDMC now implements proper logging using the Python `logging` module. Plugin
|
||||
developers should now use the following code instead of simple `print(...)`
|
||||
statements.
|
||||
EDMarketConnector now implements proper logging using the Python `logging`
|
||||
module. Plugin developers should now use the following code instead of simple
|
||||
`print(...)` statements.
|
||||
|
||||
Insert this at the top-level of your load.py file (so not inside
|
||||
`plugin_start3()`):
|
||||
@ -160,13 +163,14 @@ exactly the folder name. Our custom `qualname` and `class` formatters won't
|
||||
work with a 'bare' logger, and will cause your code to throw exceptions if
|
||||
you're not using our supplied logger.
|
||||
|
||||
If running with 4.1.0-beta1 or later of EDMC the logging setup happens in
|
||||
the core code and will include the extra logfile destinations. If your
|
||||
plugin is run under a pre-4.1.0 version of EDMC then the above will set up
|
||||
basic logging only to the console (and thus redirected to the log file).
|
||||
If running with 4.1.0-beta1 or later of EDMarketConnector the logging setup
|
||||
happens in the core code and will include the extra logfile destinations. If
|
||||
your plugin is run under a pre-4.1.0 version of EDMarketConnector then the
|
||||
above will set up basic logging only to the console (and thus redirected to
|
||||
the log file).
|
||||
|
||||
If you're certain your plugin will only be run under EDMC 4.1.0 or newer then
|
||||
you can remove the `if` clause.
|
||||
If you're certain your plugin will only be run under EDMarketConnector 4.1.0
|
||||
or newer then you can remove the `if` clause.
|
||||
|
||||
Replace all `print(...)` statements with one of the following:
|
||||
|
||||
@ -226,26 +230,26 @@ from config import appversion
|
||||
|
||||
# Yes, just blow up if config.appverison is neither str or callable
|
||||
|
||||
logger.info(f'Core EDMC version: {core_version}')
|
||||
logger.info(f'Core EDMarketConnector version: {core_version}')
|
||||
# And then compare like this
|
||||
if core_version < semantic_version.Version('5.0.0-beta1'):
|
||||
logger.info('EDMC core version is before 5.0.0-beta1')
|
||||
logger.info('EDMarketConnector core version is before 5.0.0-beta1')
|
||||
|
||||
else:
|
||||
logger.info('EDMC core version is at least 5.0.0-beta1')
|
||||
logger.info('EDMarketConnector core version is at least 5.0.0-beta1')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Startup
|
||||
|
||||
EDMC will import the `load.py` file as a module and then call the
|
||||
EDMarketConnector will import the `load.py` file as a module and then call the
|
||||
`plugin_start3()` function.
|
||||
|
||||
```python
|
||||
def plugin_start3(plugin_dir: str) -> str:
|
||||
"""
|
||||
Load this plugin into EDMC
|
||||
Load this plugin into EDMarketConnector
|
||||
"""
|
||||
print(f"I am loaded! My plugin folder is {plugin_dir}")
|
||||
return "Test"
|
||||
@ -267,8 +271,8 @@ Mac, and `$TMP/EDMarketConnector.log` on Linux.
|
||||
## Avoiding potential pitfalls
|
||||
|
||||
There are a number of things that your code should either do or avoiding
|
||||
doing so as to play nicely with the core EDMC code and not risk causing
|
||||
application crashes or hangs.
|
||||
doing so as to play nicely with the core EDMarketConnector code and not risk
|
||||
causing application crashes or hangs.
|
||||
|
||||
### Be careful about the name of your plugin directory
|
||||
|
||||
@ -344,15 +348,17 @@ So instead use:
|
||||
### Configuration
|
||||
|
||||
If you want your plugin to be configurable via the GUI you can define a frame
|
||||
(panel) to be displayed on its own tab in EDMC's settings dialog. The tab
|
||||
title will be the value that you returned from `plugin_start3`. Use widgets
|
||||
from EDMC's myNotebook.py for the correct look-and-feel. You can be notified
|
||||
when the settings dialog is closed so you can save your settings.
|
||||
(panel) to be displayed on its own tab in EDMarketConnector's settings dialog.
|
||||
The tab title will be the value that you returned from `plugin_start3`. Use
|
||||
widgets from EDMarketConnector's myNotebook.py for the correct look-and-feel.
|
||||
You can be notified when the settings dialog is closed, so you can save your
|
||||
settings.
|
||||
|
||||
You can use `set()` and `get_$type()` (where type is one of: `int`, `bool`,
|
||||
`str`, `list`) from EDMC's `config.config` object to retrieve your plugin's
|
||||
settings in a platform-independent way. Previously this was done with a single
|
||||
set and two get methods, the new methods provide better type safety.
|
||||
`str`, `list`) from EDMarketConnector's `config.config` object to retrieve
|
||||
your plugin's settings in a platform-independent way. Previously this was done
|
||||
with a single set and two get methods, the new methods provide better type
|
||||
safety.
|
||||
|
||||
If you want to maintain compatibility with pre-5.0.0 versions of this
|
||||
application (please encourage plugin users to update!) then you'll need to
|
||||
@ -377,12 +383,12 @@ if not hasattr(config, 'get_list'):
|
||||
```
|
||||
|
||||
**Be sure to use a unique prefix for any settings you save so as not to clash
|
||||
with core EDMC or other plugins.**
|
||||
with core EDMarketConnector or other plugins.**
|
||||
|
||||
Use `number_from_string()` from EDMC's `l10n.Locale` object to parse input
|
||||
numbers in a locale-independent way. NB: the old CamelCase versions of
|
||||
`number_from_string` and `string_from_number` do still exist, but are
|
||||
deprecated. They will continue to work, but will throw warnings.
|
||||
Use `number_from_string()` from EDMarketConnector's `l10n.Locale` object to
|
||||
parse input numbers in a locale-independent way. NB: the old CamelCase
|
||||
versions of `number_from_string` and `string_from_number` do still exist, but
|
||||
are deprecated. They will continue to work, but will throw warnings.
|
||||
|
||||
Note that in the following example the function signature defines that it
|
||||
returns `Optional[tk.Frame]` only because we need to allow for `None` if
|
||||
@ -401,7 +407,7 @@ my_setting: Optional[tk.IntVar] = None
|
||||
|
||||
def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]:
|
||||
"""
|
||||
Return a TK Frame for adding to the EDMC settings dialog.
|
||||
Return a TK Frame for adding to the EDMarketConnector settings dialog.
|
||||
"""
|
||||
global my_setting
|
||||
my_setting = tk.IntVar(value=config.get_int("MyPluginSetting")) # Retrieve saved value from config
|
||||
@ -438,15 +444,15 @@ def prefs_changed(cmdr: str, is_beta: bool) -> None:
|
||||
|
||||
### Display
|
||||
|
||||
You can also have your plugin add an item to the EDMC main window and update
|
||||
from your event hooks. This works in the same way as `plugin_prefs()`. For a
|
||||
simple one-line item return a `tk.Label` widget or a 2 tuple of widgets.
|
||||
For a more complicated item create a tk.Frame widget and populate it with other
|
||||
ttk widgets. Return `None` if you just want to use this as a callback after the
|
||||
main window and all other plugins are initialised.
|
||||
You can also have your plugin add an item to the EDMarketConnector main window
|
||||
and update from your event hooks. This works in the same way as
|
||||
`plugin_prefs()`. For a simple one-line item return a `tk.Label` widget or a 2
|
||||
tuple of widgets. For a more complicated item create a tk.Frame widget and
|
||||
populate it with other ttk widgets. Return `None` if you just want to use this
|
||||
as a callback after the main window and all other plugins are initialised.
|
||||
|
||||
You can use `string_from_number()` from EDMC's `l10n.Locale` object to format
|
||||
numbers in your widgets in a locale-independent way.
|
||||
You can use `string_from_number()` from EDMarketConnector's `l10n.Locale`
|
||||
object to format numbers in your widgets in a locale-independent way.
|
||||
|
||||
```python
|
||||
from typing import Optional, Tuple
|
||||
@ -457,7 +463,7 @@ status: Optional[tk.Label]
|
||||
|
||||
def plugin_app(parent: tk.Frame) -> Tuple[tk.Label, tk.Label]:
|
||||
"""
|
||||
Create a pair of TK widgets for the EDMC main window
|
||||
Create a pair of TK widgets for the EDMarketConnector main window
|
||||
"""
|
||||
global status
|
||||
label = tk.Label(parent, text="Status:") # By default widgets inherit the current theme's colors
|
||||
@ -473,7 +479,7 @@ def some_other_function() -> None:
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :---------------------------------------------: | :---------------------------------------------------------- |
|
||||
| `parent` | `tk.Frame` | The root EDMC window |
|
||||
| `parent` | `tk.Frame` | The root EDMarketConnector window |
|
||||
| `RETURN` | `Union[tk.Widget, Tuple[tk.Widget, tk.Widget]]` | A widget to add to the main window. See below for more info |
|
||||
|
||||
The return from `plugin_app()` can either be any widget (`Frame`, `Label`,
|
||||
@ -496,7 +502,7 @@ frame: Optional[tk.Frame] = None
|
||||
|
||||
def plugin_app(parent: tk.Frame) -> tk.Frame:
|
||||
"""
|
||||
Create a frame for the EDMC main window
|
||||
Create a frame for the EDMarketConnector main window
|
||||
"""
|
||||
global frame
|
||||
frame = tk.Frame(parent)
|
||||
@ -519,9 +525,10 @@ See [Avoiding potential pitfalls](#avoiding-potential-pitfalls).
|
||||
|
||||
### Events
|
||||
|
||||
Once you have created your plugin and EDMC has loaded it there are four other
|
||||
functions you can define to be notified by EDMC when something happens:
|
||||
`journal_entry()`, `journal_entry_cqc()`, `dashboard_entry()` and `cmdr_data()`.
|
||||
Once you have created your plugin and EDMarketConnector has loaded it there
|
||||
are four other functions you can define to be notified by EDMarketConnector
|
||||
when something happens: `journal_entry()`, `journal_entry_cqc()`,
|
||||
`dashboard_entry()` and `cmdr_data()`.
|
||||
|
||||
Your events all get called on the main Tkinter loop so be sure not to block for
|
||||
very long or the app will appear to freeze. If you have a long running
|
||||
@ -551,7 +558,7 @@ def journal_entry(
|
||||
logger.info(f'Arrived at {entry["StarSystem"]}')
|
||||
```
|
||||
|
||||
This gets called when EDMC sees a new entry in the game's journal.
|
||||
This gets called when EDMarketConnector sees a new entry in the game's journal.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :--------------: | :--------------------------------------------------------------------- |
|
||||
@ -682,10 +689,10 @@ ___
|
||||
|
||||
##### Synthetic Events
|
||||
|
||||
A special "StartUp" entry is sent if EDMC is started while the game is already
|
||||
running. In this case you won't receive initial events such as "LoadGame",
|
||||
"Rank", "Location", etc. However the `state` dictionary will reflect the
|
||||
cumulative effect of these missed events.
|
||||
A special "StartUp" entry is sent if EDMarketConnector is started while the
|
||||
game is already running. In this case you won't receive initial events such as
|
||||
"LoadGame", "Rank", "Location", etc. However the `state` dictionary will
|
||||
reflect the cumulative effect of these missed events.
|
||||
|
||||
**NB: Any of the values in this might be `None` if the Cmdr has loaded into
|
||||
Arena (CQC) from the Main Menu.**
|
||||
@ -702,7 +709,7 @@ between the two scenarios.
|
||||
**NB: Any of these events are passing to `journal_entry_cqc` rather than to
|
||||
`journal_entry` if player has loaded into Arena (CQC).**
|
||||
|
||||
This event is not sent when EDMC is running on a different
|
||||
This event is not sent when EDMarketConnector is running on a different
|
||||
machine so you should not *rely* on receiving this event.
|
||||
|
||||
---
|
||||
@ -782,7 +789,7 @@ This gets called when the user closes the program:
|
||||
```python
|
||||
def plugin_stop() -> None:
|
||||
"""
|
||||
EDMC is closing
|
||||
EDMarketConnector is closing
|
||||
"""
|
||||
print("Farewell cruel world!")
|
||||
```
|
||||
@ -926,23 +933,23 @@ time after the corresponding `journal_entry()` event.
|
||||
|
||||
## Error messages
|
||||
|
||||
You can display an error in EDMC's status area by returning a string from your
|
||||
`journal_entry()`, `dashboard_entry()` or `cmdr_data()` function, or
|
||||
asynchronously (e.g. from a "worker" thread that is performing a long running
|
||||
operation) by calling `plug.show_error()`. Either method will cause the "bad"
|
||||
sound to be played (unless the user has muted sound).
|
||||
You can display an error in EDMarketConnector's status area by returning a
|
||||
string from your `journal_entry()`, `dashboard_entry()` or `cmdr_data()`
|
||||
function, or asynchronously (e.g. from a "worker" thread that is performing a
|
||||
long-running operation) by calling `plug.show_error()`. Either method will
|
||||
cause the "bad" sound to be played (unless the user has muted sound).
|
||||
|
||||
The status area is shared between EDMC itself and all other plugins, so your
|
||||
message won't be displayed for very long. Create a dedicated widget if you need
|
||||
to display routine status information.
|
||||
The status area is shared between EDMarketConnector itself and all other
|
||||
plugins, so your message won't be displayed for very long. Create a dedicated
|
||||
widget if you need to display routine status information.
|
||||
|
||||
---
|
||||
|
||||
## Localisation
|
||||
|
||||
You can localise your plugin to one of the languages that EDMC itself supports.
|
||||
Add the following boilerplate near the top of each source file that contains
|
||||
strings that needs translating:
|
||||
You can localise your plugin to one of the languages that EDMarketConnector
|
||||
itself supports. Add the following boilerplate near the top of each source
|
||||
file that contains strings that needs translating:
|
||||
|
||||
```python
|
||||
import l10n
|
||||
@ -956,9 +963,9 @@ Wrap each string that needs translating with the `_()` function, e.g.:
|
||||
status["text"] = _('Happy!') # Main window status
|
||||
```
|
||||
|
||||
If you display localized strings in EDMC's main window you should refresh them
|
||||
in your `prefs_changed` function in case the user has changed their preferred
|
||||
language.
|
||||
If you display localized strings in EDMarketConnector's main window you should
|
||||
refresh them in your `prefs_changed` function in case the user has changed
|
||||
their preferred language.
|
||||
|
||||
Translation files should reside in folder named `L10n` inside your plugin's
|
||||
folder. Files must be in macOS/iOS ".strings" format, encoded as UTF-8. You can
|
||||
@ -967,7 +974,7 @@ in your plugin's folder. This extracts all the translatable strings from Python
|
||||
files in your plugin's folder and places them in a file named `en.template` in
|
||||
the `L10n` folder. Rename this file as `<language_code>.strings` and edit it.
|
||||
|
||||
See EDMC's own [`L10n`](https://github.com/EDCD/EDMarketConnector/tree/main/L10n)
|
||||
See EDMarketConnector's own [`L10n`](https://github.com/EDCD/EDMarketConnector/tree/main/L10n)
|
||||
folder for the list of supported language codes and for example translation
|
||||
files.
|
||||
|
||||
@ -976,9 +983,9 @@ files.
|
||||
## Python Package Plugins
|
||||
|
||||
A _Package Plugin_ is both a standard Python package (i.e. contains an
|
||||
`__init__.py` file) and an EDMC plugin (i.e. contains a `load.py` file
|
||||
providing at minimum a `plugin_start3()` function). These plugins are loaded
|
||||
before any non-Package plugins.
|
||||
`__init__.py` file) and an EDMarketConnector plugin (i.e. contains a `load.py`
|
||||
file providing at minimum a `plugin_start3()` function). These plugins are
|
||||
loaded before any non-Package plugins.
|
||||
|
||||
Other plugins can access features in a Package Plugin by `import`ing the
|
||||
package by name in the usual way.
|
||||
@ -1085,16 +1092,20 @@ step for the extra module(s) until it works.
|
||||
|
||||
You can debug your http post requests using the builtin debug webserver.
|
||||
|
||||
To add support for said debug webserver to your plugin, you need to check `config.debug_senders` (`list[str]`) for
|
||||
some indicator string for your plugin. `debug_senders` is generated from args to `--debug-sender` on the invocation
|
||||
command line.
|
||||
To add support for said debug webserver to your plugin, you need to check
|
||||
`config.debug_senders` (`list[str]`) for some indicator string for your
|
||||
plugin. `debug_senders` is generated from args to `--debug-sender` on the
|
||||
invocation command line.
|
||||
|
||||
If said string exists, `DEBUG_WEBSERVER_HOST` and `DEBUG_WEBSERVER_PORT` in
|
||||
`edmc_data` will contain the host and port for the currently running local webserver. Simply redirect your requests
|
||||
there, and your requests will be logged to disk. For organisation, rewrite your request path to simply be `/pluginname`.
|
||||
`edmc_data` will contain the host and port for the currently running local
|
||||
webserver. Simply redirect your requests there, and your requests will be
|
||||
logged to disk. For organisation, rewrite your request path to simply be
|
||||
`/pluginname`.
|
||||
|
||||
Logs exist in `$TEMP/EDMarketConnector/http_debug/$path.log`. If somehow you manage to cause a directory traversal, your
|
||||
data will not be saved to disk at all. You will see this in EDMCs log.
|
||||
Logs exist in `$TEMP/EDMarketConnector/http_debug/$path.log`. If somehow you
|
||||
manage to cause a directory traversal, your data will not be saved to disk at
|
||||
all. You will see this in EDMarketConnectors log.
|
||||
|
||||
The simplest way to go about adding support is:
|
||||
|
||||
@ -1110,15 +1121,16 @@ if 'my_plugin' in debug_senders:
|
||||
|
||||
```
|
||||
|
||||
For returned data, you can modify `debug_webserver.DEFAULT_RESPONSES` (`dict[str, Union[Callable[[str], str]], str])`
|
||||
with either a function that accepts a single string (the raw post data) and returns a single string
|
||||
(the response to send), or with a string if your required response is simple.
|
||||
For returned data, you can modify `debug_webserver.DEFAULT_RESPONSES`
|
||||
(`dict[str, Union[Callable[[str], str]], str])` with either a function that
|
||||
accepts a single string (the raw post data) and returns a single string (the
|
||||
response to send), or with a string if your required response is simple.
|
||||
|
||||
## Disable a plugin
|
||||
|
||||
EDMC now lets you disable a plugin without deleting it, simply rename the
|
||||
plugin folder to append ".disabled". Eg,
|
||||
"SuperSpaceHelper" -> "SuperSpaceHelper.disabled"
|
||||
EDMarketConnector now lets you disable a plugin without deleting it, simply
|
||||
rename the plugin folder to append ".disabled". Eg, "SuperSpaceHelper" ->
|
||||
"SuperSpaceHelper.disabled"
|
||||
|
||||
Disabled and enabled plugins are listed on the "Plugins" Settings tab
|
||||
|
||||
@ -1126,18 +1138,19 @@ Disabled and enabled plugins are listed on the "Plugins" Settings tab
|
||||
|
||||
## Migration from Python 2.7
|
||||
|
||||
Starting with pre-release 3.5 EDMC used Python 3.7. The first full
|
||||
release under Python 3.7 was 4.0.0.0. The 4.2.x series was the last to use
|
||||
Python 3.7, with releases moving on to the latest Python 3.9.x after that.
|
||||
Starting with pre-release 3.5 EDMarketConnector used Python 3.7. The first
|
||||
full release under Python 3.7 was 4.0.0.0. The 4.2.x series was the last to
|
||||
use Python 3.7, with releases moving on to the latest Python 3.9.x after that.
|
||||
|
||||
This is a brief outline of the steps required to migrate a plugin from earlier
|
||||
versions of EDMC:
|
||||
versions of EDMarketConnector:
|
||||
|
||||
- Rename the function `plugin_start` to `plugin_start3(plugin_dir)`.
|
||||
Plugins without a `plugin_start3` function are listed as disabled on EDMC's
|
||||
"Plugins" tab and a message like "plugin SuperSpaceHelper needs migrating"
|
||||
appears in the log. Such plugins are also listed in a section "Plugins
|
||||
Without Python 3.x Support:" on the Settings > Plugins tab.
|
||||
Plugins without a `plugin_start3` function are listed as disabled on
|
||||
EDMarketConnector's "Plugins" tab and a message like "plugin
|
||||
SuperSpaceHelper needs migrating" appears in the log. Such plugins are
|
||||
also listed in a section "Plugins Without Python 3.x Support:" on the
|
||||
`Settings` > `Plugins` tab.
|
||||
|
||||
- Check that callback functions `plugin_prefs`, `prefs_changed`,
|
||||
`journal_entry`, `dashboard_entry` and `cmdr_data`, if used, are declared
|
||||
@ -1150,5 +1163,5 @@ versions of EDMC:
|
||||
|
||||
We advise *against* making any attempt to have a plugin's code work under
|
||||
both Python 2.7 and 3.x. We no longer maintain the Python 2.7-based
|
||||
versions of this application and you shouldn't support use of them with
|
||||
versions of this application, and you shouldn't support use of them with
|
||||
your plugin.
|
||||
|
Loading…
x
Reference in New Issue
Block a user