4393697753
Document new fields: authors maintainers license/{designation,file,url} url/{home-page,download,support,code-repository} tags Add precisions concerning how fields are parsed, a bit more structure, etc. Mailing-list discussion: around https://sourceforge.net/p/flightgear/mailman/message/36155660/
543 lines
20 KiB
Text
543 lines
20 KiB
Text
-*- coding: utf-8; fill-column: 72; -*-
|
|
|
|
Add-ons in FlightGear
|
|
=====================
|
|
|
|
This document explains how add-ons work in FlightGear. The add-on
|
|
feature was first added in FlightGear 2017.3. This document describes an
|
|
evolution of the framework that appeared in FlightGear 2017.4.
|
|
|
|
|
|
Contents
|
|
--------
|
|
|
|
1. Terminology
|
|
2. The addon-metadata.xml file
|
|
3. Add-on metadata in the Property Tree
|
|
4. How to run code after an add-on is loaded
|
|
5. Overview of the C++ API
|
|
6. Nasal API
|
|
|
|
|
|
Introduction
|
|
------------
|
|
|
|
fgfs can be passed the --addon=<path> option, where <path> indicates an
|
|
add-on directory. Such a directory, when used as the argument of
|
|
--addon, receives special treatment :
|
|
|
|
1) The add-on directory is added to the list of aircraft paths.
|
|
|
|
2) The add-on directory must contain a PropertyList file called
|
|
addon-metadata.xml that gives the name of the add-on, its
|
|
identifier (id), its version and possibly a few other things (see
|
|
details below).
|
|
|
|
3) The add-on directory may contain a PropertyList file called
|
|
config.xml, in which case it will be loaded into the Property Tree
|
|
at FlightGear startup, as if it were passed to the --config fgfs
|
|
option.
|
|
|
|
4) The add-on directory must contain a Nasal file called main.nas.
|
|
This file will be loaded at startup too, and its main() function
|
|
run in the namespace __addon[ADDON_ID]__, where ADDON_ID is the
|
|
add-on identifier specified in the addon-metadata.xml file. This
|
|
operation is done by $FG_ROOT/Nasal/addons.nas at the time of this
|
|
writing.
|
|
|
|
Also, the Property Tree is populated (under /addons) with information
|
|
about registered add-ons. More details will be given below.
|
|
|
|
The --addon option can be specified zero or more times; each of the
|
|
operations indicated above is carried out for every specified add-on in
|
|
the order given by the --addon options used: that's what we call add-on
|
|
registration order, or add-on load order. In other words, add-ons are
|
|
registered and loaded in the order specified by the --addon options
|
|
used.
|
|
|
|
|
|
1. Terminology
|
|
~~~~~~~~~~~
|
|
|
|
add-on base path
|
|
|
|
Path to a directory containing all of the add-on files. This is the
|
|
path passed to the --addon fgfs option, when one wants to load the
|
|
add-on in question.
|
|
|
|
add-on identifier (id)
|
|
|
|
A string such as org.flightgear.addons.ATCChatter or
|
|
user.joe.MyGreatAddon, used to uniquely identify an add-on. The add-on
|
|
identifier is declared in <path>/addon-metadata.xml, where <path> is
|
|
the add-on base path.
|
|
|
|
add-on registration
|
|
|
|
When a --addon option is processed, FlightGear ensures that the add-on
|
|
identifier found in the corresponding addon-metadata.xml file isn't
|
|
already used by an add-on from a previous --addon option on the same
|
|
command line, and stores the add-on metadata inside dedicated C++
|
|
objects. This process is called add-on registration.
|
|
|
|
add-on loading
|
|
|
|
The following sequence of actions:
|
|
|
|
a) loading an add-on's main.nas file in the namespace
|
|
__addon[ADDON_ID]__
|
|
b) calling its main() function
|
|
|
|
is performed later (see $FG_ROOT/Nasal/addons.nas) and called add-on
|
|
loading.
|
|
|
|
|
|
2. The addon-metadata.xml file
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Every add-on must have in its base directory a file called
|
|
'addon-metadata.xml'. This section explains how to write this file.
|
|
|
|
Sample addon-metadata.xml file
|
|
==============================
|
|
|
|
Here is an example of an addon-metadata.xml file, for a hypothetical
|
|
add-on called “Flying Turtle” distributed by Joe User:
|
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
<PropertyList>
|
|
<meta>
|
|
<file-type type="string">FlightGear add-on metadata</file-type>
|
|
<format-version type="int">1</format-version>
|
|
</meta>
|
|
|
|
<addon>
|
|
<identifier type="string">user.joe.FlyingTurtle</identifier>
|
|
<name type="string">Flying Turtle</name>
|
|
<version type="string">1.0.0rc2</version>
|
|
|
|
<authors type="string">
|
|
Joe user <optional_address@example.com>
|
|
Jane Maintainer <jane@example.com>
|
|
</authors>
|
|
<maintainers type="string">
|
|
Jane Maintainer <jane@example.com>
|
|
</maintainers>
|
|
|
|
<short-description type="string">
|
|
Allow flying with new foobar powers.
|
|
</short-description>
|
|
|
|
<long-description type="string">
|
|
This add-on enables something really great involving turtles...
|
|
</long-description>
|
|
|
|
<license>
|
|
<designation type="string">
|
|
GNU GPL version 2 or later
|
|
</designation>
|
|
|
|
<file type="string">
|
|
COPYING
|
|
</file>
|
|
|
|
<url type="string">
|
|
https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
|
|
</url>
|
|
</license>
|
|
|
|
<min-FG-version type="string">2017.4.0</min-FG-version>
|
|
<max-FG-version type="string">none</max-FG-version>
|
|
|
|
<urls>
|
|
<home-page type="string">
|
|
https://example.com/quux
|
|
</home-page>
|
|
|
|
<download type="string">
|
|
https://example.com/quux/download
|
|
</download>
|
|
|
|
<support type="string">
|
|
https://example.com/quux/support
|
|
</support>
|
|
|
|
<code-repository type="string">
|
|
https://example.com/quux/code-repository
|
|
</code-repository>
|
|
</urls>
|
|
|
|
<tags>
|
|
<tag type="string">first tag</tag>
|
|
<tag type="string">second tag</tag>
|
|
<tag type="string">etc.</tag>
|
|
</tags>
|
|
</addon>
|
|
</PropertyList>
|
|
|
|
General rules
|
|
=============
|
|
|
|
We use the terms “field” or “node” interchangeably here to refer to
|
|
nodes of the addon-metadata.xml PropertyList file (technically, a field
|
|
always has a value, possibly empty, therefore fields are all leaf
|
|
nodes).
|
|
|
|
Leading and trailing whitespace in each field of addon-metadata.xml is
|
|
removed. All other whitespace is a priori preserved (this could depend
|
|
on the particular field, though).
|
|
|
|
Most fields are optional. In most cases, omitting a field is the same as
|
|
leaving it empty. But don't write empty tag fields, it is really too
|
|
ugly. ;-)
|
|
|
|
Name and id
|
|
===========
|
|
|
|
Nodes: /addon/name and /addon/identifier
|
|
|
|
The add-on name is the pretty form. It should not be overly long, but
|
|
otherwise isn't constrained. On the other hand, the add-on identifier
|
|
(id), which serves to uniquely identify an add-on:
|
|
- must contain only ASCII letters (A-Z, a-z) and dots ('.');
|
|
- must be in reverse DNS style (even if the domain doesn't exist),
|
|
e.g., org.flightgear.addons.ATCChatter for an add-on distributed in
|
|
FGAddon, or user.joe.FlyingTurtle for Joe User's “Flying Turtle”
|
|
add-on. Of course, if Joe User owns a domain name and uses it to
|
|
distribute his add-on, he should put it here.
|
|
|
|
Authors and maintainers
|
|
=======================
|
|
|
|
Nodes: /addon/authors and /addon/maintainers
|
|
|
|
Authors are people who contributed significantly to the add-on.
|
|
Maintainers are people currently in charge of maintaining the add-on.
|
|
These fields are free-form, therefore contents such as “Joe User and
|
|
others” is allowed. However, please stick to the format used in the
|
|
above example if possible.
|
|
|
|
Short and long descriptions
|
|
===========================
|
|
|
|
Nodes: /addon/short-description and /addon/long-description
|
|
|
|
The short description should fit on one line (try not to exceed, say, 78
|
|
characters), and in general consist of only one sentence.
|
|
|
|
The long description is essentially free-form, but only break lines when
|
|
you do want a line break at this point. In other words, don't wrap lines
|
|
manually in the XML file: this will be automatically done by the
|
|
software displaying the add-on description, according to the particular
|
|
line width it uses (which can depend on the user's screen or
|
|
configuration, etc.). A single \n inside a paragraph (see footnote [1])
|
|
means a hard line break. Two \n in a row (i.e., a blank line) should be
|
|
used to separate paragraphs. Example:
|
|
|
|
This is a paragraph.
|
|
This is the second line of the same paragraph. It can be very, very, very long and contain several sentences.
|
|
|
|
This is a different paragraph. Again, don't break lines (i.e., don't press Enter) unless a particular formatting reason makes it necessary. For instance, it is okay to break lines in order to present a list of items, but not for line wrapping.
|
|
|
|
Licensing terms
|
|
===============
|
|
|
|
Nodes: /addon/license/designation
|
|
/addon/license/file
|
|
/addon/license/url
|
|
|
|
The /add-on/license/designation node should describe the add-on
|
|
licensing terms in a short but accurate way, if possible. If this is not
|
|
practically doable, use the value “Custom”. If the add-on is distributed
|
|
under several licenses, use the value “Multiple”. In all cases, make
|
|
sure the licensing terms are clearly specified in other files of the
|
|
add-on (typically, at least README.txt or COPYING). Values for
|
|
/add-on/license/designation could be “GNU GPL version 2 or later”, “CC0
|
|
1.0 Universal”, “3-clause BSD”, etc.
|
|
|
|
In most cases, the add-on should contain a file containing the full
|
|
license text. Use the /add-on/license/file node to point to this file:
|
|
it should contain a file path that is relative to the add-on base
|
|
directory. This path must use slash separators ('/'), even if you use
|
|
Windows.
|
|
|
|
The /add-on/license/url node should contain a single URL if there is an
|
|
official, stable URL for the license under which the add-on is
|
|
distributed. The term “official” here is to be interpreted in the
|
|
context of the particular license. For instance, for a GNU license
|
|
(GPL2, LGPL2.1, etc.), the URL domain must be gnu.org; for a CC license
|
|
(CC0 1.0 Universal, CC-BY-SA 4.0...), it must be creativecommons.org,
|
|
etc.
|
|
|
|
Minimum and maximum FlightGear versions
|
|
=======================================
|
|
|
|
Nodes: /addon/min-FG-version and /addon/max-FG-version
|
|
|
|
These two nodes are optional and may be omitted unless the add-on is
|
|
known not to work with particular FlightGear versions.
|
|
/addon/min-FG-version defaults to 2017.4.0 and /addon/max-FG-version to
|
|
the special value 'none' (only allowed for /addon/max-FG-version). Apart
|
|
from this special case, every non-empty value present in one of these
|
|
two fields must be a proper FlightGear version number usable with
|
|
simgear::strutils::compare_versions(), for instance '2017.4.1'.
|
|
|
|
Add-on version
|
|
==============
|
|
|
|
Node: /addon/version
|
|
|
|
The /addon/version node gives the version of the add-on and must obey a
|
|
strict syntax[2], which is a subset of what is described in PEP 440:
|
|
|
|
https://www.python.org/dev/peps/pep-0440/
|
|
|
|
Valid examples are, in increasing sort order:
|
|
|
|
1.2.5.dev1 # first development release of 1.2.5
|
|
1.2.5.dev4 # fourth development release of 1.2.5
|
|
1.2.5
|
|
1.2.9
|
|
1.2.10a1.dev2 # second dev release of the first alpha release of 1.2.10
|
|
1.2.10a1 # first alpha release of 1.2.10
|
|
1.2.10b5 # fifth beta release of 1.2.10
|
|
1.2.10rc12 # twelfth release candidate for 1.2.10
|
|
1.2.10
|
|
1.3.0
|
|
2017.4.12a2
|
|
2017.4.12b1
|
|
2017.4.12rc1
|
|
2017.4.12
|
|
|
|
.devN suffixes can of course be used on beta and release candidates too,
|
|
just as with the 1.2.10a1.dev2 example given above for an alpha release.
|
|
Note that a development release always sorts before the corresponding
|
|
non-development release (e.g., 2017.2.1b5.dev4 comes before 2017.2.1b5).
|
|
|
|
Other fields
|
|
============
|
|
|
|
The other nodes of 'addon-metadata.xml' should be self-explanatory. :-)
|
|
|
|
|
|
3. Add-on metadata in the Property Tree
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The most important metadata for each registered add-on is made
|
|
accessible in the Property Tree under /addons/by-id/ADDON_ID and the
|
|
property /addons/by-id/ADDON_ID/loaded can be checked or listened to, in
|
|
order to determine when a particular add-on is loaded. There is also a
|
|
Nasal interface to access add-on metadata in a convenient way (see
|
|
below).
|
|
|
|
More precisely, when an add-on is registered, its name, id, base path,
|
|
version (converted to a string), loaded status (boolean) and load
|
|
sequence number (int) become available in the Property Tree as
|
|
/addons/by-id/ADDON_ID/{name,id,path,version,loaded,load-seq-num}. The
|
|
loaded status is initially false, and set to true when the add-on
|
|
loading phase is complete.
|
|
|
|
There are also /addons/addon[i]/path nodes where i is 0 for the first
|
|
registered add-on, 1 for the second one, etc.
|
|
|
|
|
|
4. How to run code after an add-on is loaded
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
You may want to set up Nasal code to be run after an add-on is loaded;
|
|
here is how to do that:
|
|
|
|
var addonId = "user.joe.FlyingTurtle";
|
|
var loadedFlagNode = props.globals.getNode("/addons")
|
|
.getChild("by-id", 0, 1)
|
|
.getChild(addonId, 0, 1)
|
|
.getChild("loaded", 0, 1);
|
|
|
|
if (loadedFlagNode.getBoolValue()) {
|
|
logprint(5, addonId ~ " is already loaded");
|
|
} else {
|
|
# Define a function to be called after the add-on is loaded
|
|
var id = setlistener(
|
|
loadedFlagNode,
|
|
func(changedNode, listenedNode) {
|
|
if (listenedNode.getBoolValue()) {
|
|
removelistener(id);
|
|
logprint(5, addonId ~ " is loaded");
|
|
};
|
|
},
|
|
0, 0);
|
|
}
|
|
|
|
|
|
5. Overview of the C++ API
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The add-on C++ infrastructure mainly relies on the following classes:
|
|
AddonManager, Addon and AddonVersion. AddonManager is used to register
|
|
add-ons, which later leads to their loading. AddonManager relies on an
|
|
std::map<std::string, AddonRef>, where keys are add-on identifiers and
|
|
AddonRef is SGSharedPtr<Addon> at the time of this writing (changing it
|
|
to another kind of smart pointer should be a mere one-line change). This
|
|
map holds the metadata of each registered add-on. Accessor methods are
|
|
available for:
|
|
|
|
- retrieving the lists of registered and loaded add-ons;
|
|
|
|
- checking if a particular add-on has already been registered or
|
|
loaded;
|
|
|
|
- for each add-on, obtaining an Addon instance which can be queried
|
|
for its identifier, its name, identifier, version, base path, the
|
|
minimum and maximum FlightGear versions it requires, its base node
|
|
in the Property Tree, its order in the load sequence...
|
|
|
|
The AddonVersion class handles everything about add-on version numbers:
|
|
- initialization from the individual components or from a string;
|
|
- conversion to a string and output to an std::ostream;
|
|
- access to every component;
|
|
- comparisons using the standard operators: ==, !=, <, <=, >, >=.
|
|
|
|
Registering an add-on using AddonManager::registerAddon() ensures
|
|
uniqueness of the add-on identifier and makes its name, identifier, base
|
|
path, version (converted to a string), loaded status (boolean) and load
|
|
sequence number (int) available in the Property Tree as
|
|
/addons/by-id/ADDON_ID/{name,id,path,version,loaded,load-seq-num}.
|
|
|
|
Note: if C++ code needs to use the add-on base path, better use
|
|
AddonManager::addonBasePath() or Addon::getBasePath(), whose
|
|
return values can't be tampered with by Nasal code.
|
|
|
|
AddonManager::registerAddon() fails with a specific exception if the
|
|
running FlightGear instance doesn't match the min-FG-version and
|
|
max-FG-version requirements declared in the addon-metadata.xml file, as
|
|
well as in the obvious other cases (config.xml or addon-metadata.xml not
|
|
found, invalid syntax in any of these files, etc.). The code in
|
|
options.cxx (fgOptAddon()) catches such exceptions and displays the
|
|
appropriate error message with SG_LOG() and fatalMessageBoxThenExit().
|
|
|
|
|
|
6. Nasal API
|
|
~~~~~~~~~
|
|
|
|
The Nasal add-on API all lives in the 'addons' namespace. It gives Nasal
|
|
code easy access to add-on metadata, for instance like this:
|
|
|
|
var myAddon = addons.getAddon("user.joe.FlyingTurtle");
|
|
print(myAddon.id);
|
|
print(myAddon.name);
|
|
print(myAddon.version.str());
|
|
print(myAddon.authors);
|
|
print(myAddon.maintainers);
|
|
print(myAddon.shortDescription);
|
|
print(myAddon.longDescription);
|
|
print(myAddon.licenseDesignation);
|
|
print(myAddon.licenseFile);
|
|
print(myAddon.licenseUrl);
|
|
print(myAddon.basePath);
|
|
print(myAddon.minFGVersionRequired);
|
|
print(myAddon.maxFGVersionRequired);
|
|
print(myAddon.homePage);
|
|
print(myAddon.downloadUrl);
|
|
print(myAddon.supportUrl);
|
|
print(myAddon.codeRepositoryUrl);
|
|
|
|
foreach (var tag; myAddon.tags) {
|
|
print(tag);
|
|
}
|
|
|
|
print(myAddon.loadSequenceNumber);
|
|
# myAddon.node is a props.Node object for /addons/by-id/ADDON_ID
|
|
print(myAddon.node.getPath());
|
|
|
|
Among other things, the Nasal add-on API allows one to get the version
|
|
of any registered add-on as a ghost and reliably compare it to another
|
|
instance of addons.AddonVersion:
|
|
|
|
var myAddon = addons.getAddon("user.joe.FlyingTurtle");
|
|
var firstVersionOK = addons.AddonVersion.new("2.12.5rc1");
|
|
# Or alternatively:
|
|
# var firstVersionOK = addons.AddonVersion.new(2, 12, 5, "rc1");
|
|
|
|
if (myAddon.version.lowerThan(firstVersionOK)) {
|
|
...
|
|
|
|
Here follows the complete Nasal add-on API, at the time of this writing.
|
|
All strings are encoded in UTF-8.
|
|
|
|
Queries to the AddonManager:
|
|
|
|
addons.isAddonRegistered(string addonId) -> bool (1 or 0)
|
|
addons.registeredAddons() -> vector<addons.Addon>
|
|
(in registration/load order)
|
|
addons.isAddonLoaded(string addonId) -> bool (1 or 0)
|
|
addons.loadedAddons() -> vector<addons.Addon>
|
|
(in lexicographic order)
|
|
addons.getAddon(string addonId) -> addons.Addon instance (ghost)
|
|
|
|
Read-only data members (attributes) of addons.Addon objects:
|
|
|
|
id the add-on identifier, in reverse DNS style (string)
|
|
name the add-on “pretty name” (string)
|
|
version the add-on version (instance of addons.AddonVersion,
|
|
ghost)
|
|
authors the add-on authors (string)
|
|
maintainers the add-on maintainers (string)
|
|
shortDescription the add-on short description (string)
|
|
longDescription the add-on long description (string)
|
|
licenseDesignation licensing terms: "GNU GPL version 2 or later",
|
|
"CC0 1.0 Universal", etc. (string)
|
|
licenseFile relative, slash-separated path to a file under
|
|
the add-on base directory containing the license
|
|
text (string)
|
|
licenseUrl stable, official URL for the add-on license text
|
|
(string)
|
|
basePath path to the add-on base directory (string)
|
|
minFGVersionRequired minimum required FG version for the add-on (string)
|
|
maxFGVersionRequired max. required FG version... or "none" (string)
|
|
homePage add-on home page (string)
|
|
downloadUrl add-on download URL (string)
|
|
supportUrl add-on support URL (string)
|
|
codeRepositoryUrl URL pointing to the development repository of
|
|
the add-on (Git, Subversion, etc.; string)
|
|
tags vector containing the add-on tags used to help
|
|
users find add-ons (vector of strings)
|
|
node base node for the add-on in the Property Tree:
|
|
/addons/by-id/ADDON_ID (props.Node object)
|
|
loadSequenceNumber 0 for the first registered add-on, 1 for the
|
|
second one, etc. (integer)
|
|
|
|
Read-only data members (attributes) of addons.AddonVersion objects:
|
|
|
|
majorNumber non-negative integer
|
|
minorNumber non-negative integer
|
|
patchLevel non-negative integer
|
|
suffix string such as "", "a1", "b2.dev45", "rc12"...
|
|
|
|
Member functions (methods) of addons.AddonVersion objects:
|
|
|
|
new(string version) | construct from string
|
|
|
|
new(int major, int minor=0, int patchLevel=0, | construct
|
|
string suffix="") | from components
|
|
|
|
str() | string representation
|
|
|
|
equal(addons.AddonVersion other) |
|
|
nonEqual(addons.AddonVersion other) | compare to another
|
|
lowerThan(addons.AddonVersion other) | addons.AddonVersion
|
|
lowerThanOrEqual(addons.AddonVersion other) | instance
|
|
greaterThan(addons.AddonVersion other) |
|
|
greaterThanOrEqual(addons.AddonVersion other) |
|
|
|
|
|
|
Footnotes
|
|
---------
|
|
|
|
[1] \n represents end-of-line in string literals of languages such as C,
|
|
C++, Python and many others. We use this convention here to
|
|
represent the end-of-line character sequence in the XML data.
|
|
|
|
[2] MAJOR.MINOR.PATCHLEVEL[{a|b|rc}N1][.devN2] where MAJOR, MINOR and
|
|
PATCHLEVEL are non-negative integers, and N1 and N2 are positive
|
|
integers.
|