1-*- coding: utf-8; fill-column: 72; -*-
2
3Add-ons in FlightGear
4=====================
5
6This document explains how add-ons work in FlightGear. The add-on
7feature was first added in FlightGear 2017.3. This document describes an
8evolution of the framework that appeared in FlightGear 2017.4.
9
10
11Contents
12--------
13
141. Terminology
15
162. The addon-metadata.xml file
17
183. Add-ons and the Property Tree
19
20   a) Add-on metadata
21   b) Subtree reserved for add-on developers
22
234. Resources under the add-on directory
24
255. Persistent storage location for add-ons
26
276. Add-on-specific menus and dialogs
28
29   a) Add-on-specific menus
30   b) Add-on-specific dialogs
31
327. How to run code after an add-on is loaded
33
348. Overview of the C++ API
35
369. Nasal API
37
3810. Add-on development; in-sim reload of Nasal code
39
40
41Introduction
42------------
43
44fgfs can be passed the --addon=<path> option, where <path> indicates an
45add-on directory. Such a directory, when used as the argument of
46--addon, receives special treatment :
47
48  1) The add-on directory is added to the list of aircraft paths.
49
50  2) The add-on directory must contain a PropertyList file called
51     addon-metadata.xml that gives the name of the add-on, its
52     identifier (id), its version and possibly a few other things (see
53     details below).
54
55  3) The add-on directory may contain a PropertyList file called
56     addon-config.xml, in which case it will be loaded into the Property
57     Tree at FlightGear startup, as if it were passed to the --config
58     fgfs option.
59
60  4) The add-on directory must contain a Nasal file called
61     addon-main.nas. This file will be loaded at startup too, and its
62     main() function run in the namespace __addon[ADDON_ID]__, where
63     ADDON_ID is the add-on identifier specified in the
64     addon-metadata.xml file. The main() function is passed one
65     argument: the addons.Addon object (a Nasal ghost, see below)
66     corresponding to the add-on being loaded. This operation is done by
67     $FG_ROOT/Nasal/addons.nas at the time of this writing.
68
69Also, the Property Tree is populated (under /addons) with information
70about registered add-ons. More details will be given below.
71
72The --addon option can be specified zero or more times; each of the
73operations indicated above is carried out for every specified add-on in
74the order given by the --addon options used: that's what we call add-on
75registration order, or add-on load order. In other words, add-ons are
76registered and loaded in the order specified by the --addon options
77used.
78
79
801. Terminology
81   ~~~~~~~~~~~
82
83add-on base path
84
85  Path to a directory containing all of the add-on files. This is the
86  path passed to the --addon fgfs option, when one wants to load the
87  add-on in question.
88
89add-on identifier (id)
90
91  A string such as org.flightgear.addons.ATCChatter or
92  user.joe.MyGreatAddon, used to uniquely identify an add-on. The add-on
93  identifier is declared in <path>/addon-metadata.xml, where <path> is
94  the add-on base path.
95
96add-on registration
97
98  When a --addon option is processed, FlightGear ensures that the add-on
99  identifier found in the corresponding addon-metadata.xml file isn't
100  already used by an add-on from a previous --addon option on the same
101  command line, and stores the add-on metadata inside dedicated C++
102  objects. This process is called add-on registration.
103
104add-on loading
105
106  The following sequence of actions:
107
108    a) loading an add-on's addon-main.nas file in the namespace
109       __addon[ADDON_ID]__
110    b) calling its main() function
111
112  is performed later (see $FG_ROOT/Nasal/addons.nas) and called add-on
113  loading.
114
115
1162. The addon-metadata.xml file
117   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
118
119Every add-on must have in its base directory a file called
120'addon-metadata.xml'. This section explains how to write this file.
121
122Sample addon-metadata.xml file
123==============================
124
125Here is an example of an addon-metadata.xml file, for a hypothetical
126add-on called “Flying Turtle” distributed by Joe User:
127
128  <?xml version="1.0" encoding="UTF-8"?>
129
130  <PropertyList>
131    <meta>
132      <file-type type="string">FlightGear add-on metadata</file-type>
133      <format-version type="int">1</format-version>
134    </meta>
135
136    <addon>
137      <identifier type="string">user.joe.FlyingTurtle</identifier>
138      <name type="string">Flying Turtle</name>
139      <version type="string">1.0.0rc2</version>
140
141      <authors>
142        <author>
143          <name type="string">Joe User</name>
144          <email type="string">optional_address@example.com</email>
145          <url type="string">http://joe.example.com/foobar/</url>
146        </author>
147
148        <author>
149          <name type="string">Jane Maintainer</name>
150          <email type="string">jane@example.com</email>
151          <url type="string">https://jane.example.com/</url>
152        </author>
153      </authors>
154
155      <maintainers>
156        <maintainer>
157          <name type="string">Jane Maintainer</name>
158          <email type="string">jane@example.com</email>
159          <url type="string">https://jane.example.com/</url>
160        </maintainer>
161      </maintainers>
162
163      <short-description type="string">
164        Allow flying with new foobar powers.
165      </short-description>
166
167      <long-description type="string">
168        This add-on enables something really great involving turtles...
169      </long-description>
170
171      <localized>
172        <fr>
173          <short-description type="string">
174            Permet de voler avec de nouveaux pouvoirs foobar.
175          </short-description>
176          <name>
177          Tortue volante
178          </name>
179        </fr>
180        <de>
181        <short-description type="string">
182         Erlaube das Fliegen mit neuen Foobar-Kräften.
183          </short-description>
184        </de>
185      </localized>
186
187      <license>
188        <designation type="string">
189          GNU GPL version 2 or later
190        </designation>
191
192        <file type="string">
193          COPYING
194        </file>
195
196        <url type="string">
197          https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
198        </url>
199      </license>
200
201      <min-FG-version type="string">2017.4.0</min-FG-version>
202      <max-FG-version type="string">none</max-FG-version>
203
204      <urls>
205        <home-page type="string">
206          https://example.com/quux
207        </home-page>
208
209        <download type="string">
210          https://example.com/quux/download
211        </download>
212
213        <support type="string">
214          https://example.com/quux/support
215        </support>
216
217        <code-repository type="string">
218          https://example.com/quux/code-repository
219        </code-repository>
220      </urls>
221
222      <tags>
223        <tag type="string">first tag</tag>
224        <tag type="string">second tag</tag>
225        <tag type="string">etc.</tag>
226      </tags>
227    </addon>
228  </PropertyList>
229
230General rules
231=============
232
233We use the terms “field” or “node” interchangeably here to refer to
234nodes of the addon-metadata.xml PropertyList file (technically, a field
235always has a value, possibly empty, therefore fields are all leaf
236nodes).
237
238Leading and trailing whitespace in each field of addon-metadata.xml is
239removed. All other whitespace is a priori preserved (this could depend
240on the particular field, though).
241
242Most fields are optional. In most cases, omitting a field is the same as
243leaving it empty. But don't write empty tag fields, it is really too
244ugly. ;-)
245
246Name and id
247===========
248
249Nodes: /addon/name and /addon/identifier
250
251The add-on name is the pretty form. It should not be overly long, but
252otherwise isn't constrained. On the other hand, the add-on identifier
253(id), which serves to uniquely identify an add-on:
254  - must contain only ASCII letters (A-Z, a-z) and dots ('.');
255  - must be in reverse DNS style (even if the domain doesn't exist),
256    e.g., org.flightgear.addons.ATCChatter for an add-on distributed in
257    FGAddon, or user.joe.FlyingTurtle for Joe User's “Flying Turtle”
258    add-on. Of course, if Joe User owns a domain name and uses it to
259    distribute his add-on, he should put it here.
260
261Authors and maintainers
262=======================
263
264Nodes: /addon/authors and /addon/maintainers
265
266Authors are people who contributed significantly to the add-on.
267Maintainers are people currently in charge of maintaining it.
268
269It is possible to declare any number of authors and any number of
270maintainers---the example above shows only one maintainer for shortness,
271but this is not a restriction.
272
273For each author and maintainer, you can give a name, an email address
274and a URL. The name must be non-empty, but the email address and URL
275need not be specified or may be left empty, which is equivalent.
276Obviously, if no email address nor URL is given for any maintainer, it
277is highly desirable that /addon/urls/support contains a usable URL for
278contacting the add-on maintainers.
279
280The data in children nodes of /addon/maintainers may refer either to
281real persons or to more abstract entities such as mailing-lists. In case
282of a real person, the corresponding URL, if specified, is expected to be
283the person's home page. On the other hand, if a declared “maintainer” is
284a mailing-list, a good use for the 'url' field is to indicate the
285address of a web page from which people can subscribe to the
286mailing-list.
287
288Short and long descriptions
289===========================
290
291Nodes: /addon/short-description and /addon/long-description
292
293The short description should fit on one line (try not to exceed, say, 78
294characters), and in general consist of only one sentence.
295
296The long description is essentially free-form, but only break lines when
297you do want a line break at this point. In other words, don't wrap lines
298manually in the XML file: this will be automatically done by the
299software displaying the add-on description, according to the particular
300line width it uses (which can depend on the user's screen or
301configuration, etc.). A single \n inside a paragraph (see footnote [1])
302means a hard line break. Two \n in a row (i.e., a blank line) should be
303used to separate paragraphs. Example:
304
305This is a paragraph.
306This is the second line of the same paragraph. It can be very, very, very long and contain several sentences.
307
308This 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.
309
310Licensing terms
311===============
312
313Nodes: /addon/license/designation
314       /addon/license/file
315       /addon/license/url
316
317The /add-on/license/designation node should describe the add-on
318licensing terms in a short but accurate way, if possible. If this is not
319practically doable, use the value “Custom”. If the add-on is distributed
320under several licenses, use the value “Multiple”. In all cases, make
321sure the licensing terms are clearly specified in other files of the
322add-on (typically, at least README.txt or COPYING). Values for
323/add-on/license/designation could be “GNU GPL version 2 or later”, “CC0
3241.0 Universal”, “3-clause BSD”, etc.
325
326In most cases, the add-on should contain a file containing the full
327license text. Use the /add-on/license/file node to point to this file:
328it should contain a file path that is relative to the add-on base
329directory. This path must use slash separators ('/'), even if you use
330Windows.
331
332The /add-on/license/url node should contain a single URL if there is an
333official, stable URL for the license under which the add-on is
334distributed. The term “official” here is to be interpreted in the
335context of the particular license. For instance, for a GNU license
336(GPL2, LGPL2.1, etc.), the URL domain must be gnu.org; for a CC license
337(CC0 1.0 Universal, CC-BY-SA 4.0...), it must be creativecommons.org,
338etc.
339
340Minimum and maximum FlightGear versions
341=======================================
342
343Nodes: /addon/min-FG-version and /addon/max-FG-version
344
345These two nodes are optional and may be omitted unless the add-on is
346known not to work with particular FlightGear versions.
347/addon/min-FG-version defaults to 2017.4.0 and /addon/max-FG-version to
348the special value 'none' (only allowed for /addon/max-FG-version). Apart
349from this special case, every non-empty value present in one of these
350two fields must be a proper FlightGear version number usable with
351simgear::strutils::compare_versions(), for instance '2017.4.1'.
352
353Add-on version
354==============
355
356Node: /addon/version
357
358The /addon/version node gives the version of the add-on and must obey a
359strict syntax[2], which is a subset of what is described in PEP 440:
360
361  https://www.python.org/dev/peps/pep-0440/
362
363Valid examples are, in increasing sort order:
364
365  1.2.5.dev1      # first development release of 1.2.5
366  1.2.5.dev4      # fourth development release of 1.2.5
367  1.2.5
368  1.2.9
369  1.2.10a1.dev2   # second dev release of the first alpha release of 1.2.10
370  1.2.10a1        # first alpha release of 1.2.10
371  1.2.10b5        # fifth beta release of 1.2.10
372  1.2.10rc12      # twelfth release candidate for 1.2.10
373  1.2.10
374  1.3.0
375  2017.4.12a2
376  2017.4.12b1
377  2017.4.12rc1
378  2017.4.12
379
380.devN suffixes can of course be used on beta and release candidates too,
381just as with the 1.2.10a1.dev2 example given above for an alpha release.
382Note that a development release always sorts before the corresponding
383non-development release (e.g., 2017.2.1b5.dev4 comes before 2017.2.1b5).
384
385Translations
386============
387
388Certain nodes can be translated based on the active languages within FlightGear.
389Especially, the name and descriptions can be translated, by adding them to
390a language node beneath 'localized', as show in the example above. Where there is
391no translation for a particular value, the default one is used.
392
393Other fields
394============
395
396The other nodes of 'addon-metadata.xml' should be self-explanatory. :-)
397
398
3993. Add-ons and the Property Tree
400   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
401
402a) Add-on metadata
403   ^^^^^^^^^^^^^^^
404
405The most important metadata for each registered add-on is made
406accessible in the Property Tree under /addons/by-id/ADDON_ID and the
407property /addons/by-id/ADDON_ID/loaded can be checked or listened to, in
408order to determine when a particular add-on is loaded. There is also a
409Nasal interface to access add-on metadata in a convenient way (see
410below).
411
412More precisely, when an add-on is registered, its name, id, base path,
413version (converted to a string), loaded status (boolean) and load
414sequence number (int) become available in the Property Tree as
415/addons/by-id/ADDON_ID/{name,id,path,version,loaded,load-seq-num}. The
416loaded status is initially false, and set to true when the add-on
417loading phase is complete.
418
419There are also /addons/addon[i]/path nodes where i is 0 for the first
420registered add-on, 1 for the second one, etc.
421
422
423b) Subtree reserved for add-on developers
424   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
425
426For each add-on, the subtree of the global Property Tree starting at
427/addons/by-id/ADDON_ID/addon-devel is reserved for the specific needs of
428the add-on, where ADDON_ID stands for the add-on identifier. For
429instance, developers of the add-on whose identifier is
430user.joe.FlyingTurtle can store whatever they want under
431/addons/by-id/user.joe.FlyingTurtle/addon-devel with the assurance that
432doing this won't clash with properties used by the add-on framework.
433
434Example:
435
436  /addons/by-id/user.joe.FlyingTurtle/addon-devel/some/property and
437  /addons/by-id/user.joe.FlyingTurtle/addon-devel/other/property
438
439  could be two properties used for the specific needs of the
440  add-on whose identifier is user.joe.FlyingTurtle.
441
442Add-on developers should *not* use other places in the /addons subtree
443of the Property Tree for their custom properties.
444
445
4464. Resources under the add-on directory
447   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
448
449Many functions in FlightGear use files that are located using the
450SimGear ResourceManager class. This class allows one to point to files
451by relative path in aircraft source files and other places. The resource
452manager queries a set of providers, some of which look inside aircraft
453paths (starting with the current aircraft), others inside scenery paths,
454others under $FG_ROOT, etc. The first file that matches the specified
455resource path is used.
456
457One of these providers only handles resource paths with a very specific
458syntax, which is:
459
460  [addon=ADDON_ID]path/relative/to/the/addon/directory
461
462  (for instance, [addon=user.joe.FlyingTurtle]images/eject-button.png)
463
464When you use such a syntax in a place that is expected to contain a
465resource path, it will only find the specified file under the directory
466of the add-on whose identifier is ADDON_ID. This allows one to
467specifically target a particular file inside a particular add-on,
468instead of crossing fingers and hoping that the specified resource won't
469be found by coincidence in another place such as an aircraft directory,
470a scenery directory or inside $FG_ROOT (such mistakes can easily happen
471when unrelated places use files with rather generic names, such as
472button.png, system.xml, etc.).
473
474The [addon=ADDON_ID]relative/path syntax is useful where resources are
475specified inside non-Nasal files (e.g., in property-rule configuration
476files, which use the XML format). For the particular case of Nasal code,
477there is a better way that is explained below (see “Nasal API”): the
478resourcePath() method of addons.Addon objects returns a string like
479"[addon=ADDON_ID]relative/path" when you pass it the string
480"relative/path". This is a good thing to use, because then your Nasal
481code doesn't need to know about the particular syntax for
482add-on-specific resources and, more interestingly, doesn't have to
483hardcode the add-on identifier every time you need to access a resource
484inside the add-on directory.
485
4864.1 Resources extending $FG_ROOT
487    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
488
489Add-ons can options supply a special sub-directory which is searched when
490FlightGear looks for files normally residing in $FG_ROOT. For example input
491configurations, network protocols and other non-aircraft resources. Since
492these files cannot use the scheme above, a different approach is used. If
493an add-on defines a subdirectory called 'FGData', this is becomes an
494additional data directory to be searched for any such standard files. For
495security reasons, add-on FGData extensions are searched after the main
496$FG_ROOT location, so that an addon cannot maliciously replace a core resource.
497
498Only some files currently support being added via this mechanism, so if you
499find a non-working case which would be useful, please request it via the
500developer mailing list.
501
502
5035. Persistent storage location for add-ons
504   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
505
506If an add-on needs to store data that persists across FlightGear
507sessions, it can use a specific directory tree whose path is obtained
508with addon.storagePath, where 'addon' is an addons.Addon instance. This
509corresponds to $FG_HOME/Export/Addons/ADDON_ID, however it is simpler
510and better to use addon.storagePath instead of hardcoding and manually
511assembling this path in each add-on. Since the directory is likely not
512to exist until the add-on creates it, the recommended usage pattern is
513the following:
514
515  1) Create the add-on-specific storage directory if it doesn't already
516     exist, and optionally get its path at the same time:
517
518       storageDir = addon.createStorageDir();
519
520     Typically, you'll run this in the add-on main() function (at least,
521     early enough) if your add-on uses the storage directory. Note that
522     there is no need to check yourself whether the directory already
523     exists: addon.createStorageDir() does that for you.
524
525  2) At any time, you can get a path to the add-on-specific storage
526     directory with:
527
528       storageDir = addon.storagePath
529
530     Accessing addon.storagePath doesn't check for the existence nor the
531     type of $FG_HOME/Export/Addons/ADDON_ID, thus it is a bit faster
532     than addon.createStorageDir(). Use addon.storagePath whenever you
533     know that the directory has already been created.
534
535The features described in this section were added in FlightGear 2018.2.
536
537
5386. Add-on-specific menus and dialogs
539   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
540
541a) Add-on-specific menus
542   ^^^^^^^^^^^^^^^^^^^^^
543
544Add-ons can easily provide their own menus. If an add-on is loaded that
545has a file named 'addon-menubar-items.xml' in its base directory, the
546menus described in this file are added to the FlightGear menu bar. The
547file should look like this:
548
549<?xml version="1.0" encoding="UTF-8"?>
550
551<PropertyList>
552  <meta>
553    <file-type type="string">FlightGear add-on menu bar items</file-type>
554    <format-version type="int">1</format-version>
555  </meta>
556
557  <menubar-items>
558    <menu>
559      ...
560    </menu>
561
562    ...
563
564    <menu>
565      ...
566    </menu>
567  </menubar-items>
568</PropertyList>
569
570In this file, each <menu> element must be a valid menu description for
571the FlightGear menu system (the FlightGear standard menubar in
572$FG_ROOT/gui/menubar.xml provides good examples). Here is an example
573that adds one menu with an entry for running some Nasal code and another
574entry for opening a custom dialog (see below for add-on-specific dialogs):
575
576    <menu>
577      <label>My Menu</label>
578      <enabled type="bool">true</enabled>
579
580      <item>
581        <label>Run Foobar Nasal</label>
582        <binding>
583          <command>nasal</command>
584          <script>foobar.doBaz();</script>
585        </binding>
586      </item>
587
588      <item>
589        <label>My Foobar Dialog</label>
590        <binding>
591          <command>dialog-show</command>
592          <dialog-name>my-foobar-dialog</dialog-name>
593        </binding>
594      </item>
595    </menu>
596
597This feature was added in FlightGear 2018.2.
598
599  For older versions, one can add menus via addon-config.xml, but it's a
600  bit hackish because of the menu index problem.
601
602
603b) Add-on-specific dialogs
604   ^^^^^^^^^^^^^^^^^^^^^^^
605
606As is the case for aircraft, add-ons can provide their own dialogs by
607shipping the corresponding XML files in the subfolder gui/dialogs of the
608add-on base directory. In other words, with a file like
609
610  <addon-base-path>/gui/dialogs/my-foobar-dialog.xml
611
612starting with:
613
614  <?xml version="1.0" encoding="UTF-8"?>
615  <PropertyList>
616    <name>my-foobar-dialog</name>
617
618  ...
619
620the following <item> element inside 'addon-menubar-items.xml' (see
621above) describes a valid menu entry for showing the custom dialog.
622
623      <item>
624        <label>My Foobar Dialog</label>
625        <binding>
626          <command>dialog-show</command>
627          <dialog-name>my-foobar-dialog</dialog-name>
628        </binding>
629      </item>
630
631See $FG_ROOT/gui/dialogs to get inspiration from FlightGear's standard
632dialogs.
633
634This feature was added in FlightGear 2018.2.
635
636
6377. How to run code after an add-on is loaded
638   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
639
640You may want to set up Nasal code to be run after an add-on is loaded;
641here is how to do that:
642
643  var addonId = "user.joe.FlyingTurtle";
644  var loadedFlagNode = props.globals.getNode("/addons")
645                                    .getChild("by-id", 0, 1)
646                                    .getChild(addonId, 0, 1)
647                                    .getChild("loaded", 0, 1);
648
649  if (loadedFlagNode.getBoolValue()) {
650    logprint(5, addonId ~ " is already loaded");
651  } else {
652    # Define a function to be called after the add-on is loaded
653    var id = setlistener(
654      loadedFlagNode,
655      func(changedNode, listenedNode) {
656        if (listenedNode.getBoolValue()) {
657          removelistener(id);
658          logprint(5, addonId ~ " is loaded");
659        };
660      },
661      0, 0);
662  }
663
664
6658. Overview of the C++ API
666   ~~~~~~~~~~~~~~~~~~~~~~~
667
668The add-on C++ infrastructure mainly relies on the following classes:
669AddonManager, Addon and AddonVersion. AddonManager is used to register
670add-ons, which later leads to their loading. AddonManager relies on an
671std::map<std::string, AddonRef>, where keys are add-on identifiers and
672AddonRef is SGSharedPtr<Addon> at the time of this writing (changing it
673to another kind of smart pointer should be a mere one-line change). This
674map holds the metadata of each registered add-on. Accessor methods are
675available for:
676
677  - retrieving the lists of registered and loaded add-ons;
678
679  - checking if a particular add-on has already been registered or
680    loaded;
681
682  - for each add-on, obtaining an Addon instance which can be queried
683    for its identifier, its name, identifier, version, base path, the
684    minimum and maximum FlightGear versions it requires, its base node
685    in the Property Tree, its order in the load sequence...
686
687The AddonVersion class handles everything about add-on version numbers:
688  - initialization from the individual components or from a string;
689  - conversion to a string and output to an std::ostream;
690  - access to every component;
691  - comparisons using the standard operators: ==, !=, <, <=, >, >=.
692
693Registering an add-on using AddonManager::registerAddon() ensures
694uniqueness of the add-on identifier and makes its name, identifier, base
695path, version (converted to a string), loaded status (boolean) and load
696sequence number (int) available in the Property Tree as
697/addons/by-id/ADDON_ID/{name,id,path,version,loaded,load-seq-num}.
698
699Note: if C++ code needs to use the add-on base path, better use
700      AddonManager::addonBasePath() or Addon::getBasePath(), whose
701      return values can't be tampered with by Nasal code.
702
703AddonManager::registerAddon() fails with a specific exception if the
704running FlightGear instance doesn't match the min-FG-version and
705max-FG-version requirements declared in the addon-metadata.xml file, as
706well as in the obvious other cases (required files such as
707addon-metadata.xml not found, invalid syntax in such files, etc.). The
708code in options.cxx (fgOptAddon()) catches such exceptions and displays
709the appropriate error message with SG_LOG() and
710fatalMessageBoxThenExit().
711
712
7139. Nasal API
714   ~~~~~~~~~
715
716The Nasal add-on API all lives in the 'addons' namespace. It gives Nasal
717code easy access to add-on metadata, for instance like this:
718
719  var myAddon = addons.getAddon("user.joe.FlyingTurtle");
720  print(myAddon.id);
721  print(myAddon.name);
722  print(myAddon.version.str());
723
724  foreach (var author; myAddon.authors) {
725    print(author.name, " ", author.email, " ", author.url);
726  }
727
728  foreach (var maintainer; myAddon.maintainers) {
729    print(maintainer.name, " ", maintainer.email, " ", maintainer.url);
730  }
731
732  print(myAddon.shortDescription);
733  print(myAddon.longDescription);
734  print(myAddon.licenseDesignation);
735  print(myAddon.licenseFile);
736  print(myAddon.licenseUrl);
737  print(myAddon.basePath);
738  print(myAddon.minFGVersionRequired);
739  print(myAddon.maxFGVersionRequired);
740  print(myAddon.homePage);
741  print(myAddon.downloadUrl);
742  print(myAddon.supportUrl);
743  print(myAddon.codeRepositoryUrl);
744
745  foreach (var tag; myAddon.tags) {
746    print(tag);
747  }
748
749  print(myAddon.loadSequenceNumber);
750  # myAddon.node is a props.Node object for /addons/by-id/ADDON_ID
751  print(myAddon.node.getPath());
752
753Among other things, the Nasal add-on API allows one to get the version
754of any registered add-on as a ghost and reliably compare it to another
755instance of addons.AddonVersion:
756
757  var myAddon = addons.getAddon("user.joe.FlyingTurtle");
758  var firstVersionOK = addons.AddonVersion.new("2.12.5rc1");
759  # Or alternatively:
760  #   var firstVersionOK = addons.AddonVersion.new(2, 12, 5, "rc1");
761
762  if (myAddon.version.lowerThan(firstVersionOK)) {
763    ...
764
765Here follows the complete Nasal add-on API, at the time of this writing.
766All strings are encoded in UTF-8.
767
768Queries to the AddonManager:
769
770  addons.isAddonRegistered(string addonId) -> bool (1 or 0)
771  addons.registeredAddons()                -> vector<addons.Addon>
772                                              (in registration/load order)
773  addons.isAddonLoaded(string addonId)     -> bool (1 or 0)
774  addons.loadedAddons()                    -> vector<addons.Addon>
775                                              (in lexicographic order)
776  addons.getAddon(string addonId)          -> addons.Addon instance (ghost)
777
778Read-only data members (attributes) of addons.Addon objects:
779
780  id                    the add-on identifier, in reverse DNS style (string)
781  name                  the add-on “pretty name” (string)
782  version               the add-on version (instance of addons.AddonVersion,
783                        ghost)
784  authors               the add-on authors (vector of addons.Author ghosts)
785  maintainers           the add-on maintainers (vector of addons.Maintainer
786                        ghosts)
787  shortDescription      the add-on short description (string)
788  longDescription       the add-on long description (string)
789  licenseDesignation    licensing terms: "GNU GPL version 2 or later",
790                        "CC0 1.0 Universal", etc. (string)
791  licenseFile           relative, slash-separated path to a file under
792                        the add-on base directory containing the license
793                        text (string)
794  licenseUrl            stable, official URL for the add-on license text
795                        (string)
796  basePath              path to the add-on base directory (string)
797  storagePath           path to the add-on storage directory (string)
798                        This is $FG_HOME/Export/Addons/ADDON_ID.
799                        [added in FlightGear 2018.2]
800  minFGVersionRequired  minimum required FG version for the add-on (string)
801  maxFGVersionRequired  max. required FG version... or "none" (string)
802  homePage              add-on home page (string)
803  downloadUrl           add-on download URL (string)
804  supportUrl            add-on support URL (string)
805  codeRepositoryUrl     URL pointing to the development repository of
806                        the add-on (Git, Subversion, etc.; string)
807  tags                  vector containing the add-on tags used to help
808                        users find add-ons (vector of strings)
809  node                  base node for the add-on in the Property Tree:
810                        /addons/by-id/ADDON_ID (props.Node object)
811  loadSequenceNumber    0 for the first registered add-on, 1 for the
812                        second one, etc. (integer)
813
814Member functions (methods) of addons.Addon objects:
815
816  createStorageDir() -> string
817                        Create the add-on storage directory if it
818                        doesn't already exist (that is,
819                        $FG_HOME/Export/Addons/ADDON_ID). Return its
820                        path as a string.
821                        [added in FlightGear 2018.2]
822  resourcePath(string relPath) -> string
823                        Return a resource path suitable for use with the
824                        simgear::ResourceManager. 'relPath' must be
825                        relative to the add-on base directory, and
826                        mustn't start with a '/'. You can use this
827                        method for instance to specify an image file for
828                        display in a Canvas widget.
829
830                        In you want a full path to the resource file
831                        (e.g., for troubleshooting), call resolvepath()
832                        with the return value of addons.Addon.resourcePath().
833
834Read-only data members (attributes) of addons.AddonVersion objects:
835
836  majorNumber           non-negative integer
837  minorNumber           non-negative integer
838  patchLevel            non-negative integer
839  suffix                string such as "", "a1", "b2.dev45", "rc12"...
840
841Member functions (methods) of addons.AddonVersion objects:
842
843  new(string version)                           | construct from string
844
845  new(int major, int minor=0, int patchLevel=0, | construct
846      string suffix="")                         | from components
847
848  str()                                         | string representation
849
850  equal(addons.AddonVersion other)              |
851  nonEqual(addons.AddonVersion other)           | compare to another
852  lowerThan(addons.AddonVersion other)          | addons.AddonVersion
853  lowerThanOrEqual(addons.AddonVersion other)   | instance
854  greaterThan(addons.AddonVersion other)        |
855  greaterThanOrEqual(addons.AddonVersion other) |
856
857Read-only data members (attributes) of addons.Author objects:
858
859  name                  author name (non-empty string)
860  email                 email address of the author (string)
861  url                   home page of the author (string)
862
863Read-only data members (attributes) of addons.Maintainer objects:
864
865  name                  maintainer name (non-empty string)
866  email                 email address of the maintainer (string)
867  url                   home page of the maintainer, if a person; if the
868                        maintainer is a mailing-list, the URL can point
869                        to a web page from which people can subscribe to
870                        that mailing-list (string)
871
872
87310. Add-on development; in-sim reload of Nasal code
874---------------------------------------------------
875
876!!! WARNING:
877!!! The reload feature is meant for developers only, it should not be made
878!!! visible to end users. Unexpected side effects may occur due to reload,
879!!! if not implemented correctly.
880!!! We really don't want users to send bug reports due to reload going wrong.
881
882To make development of add-ons less time consuming, you can reload the
883Nasal part of your add-on without having to restart FlightGear. When an
884add-on is loaded, setlistener() and maketimer() wrappers are installed
885in the add-on's own namespace; these wrappers shadow and call themselves
886the standard setlistener() and maketimer() functions. The setlistener()
887and maketimer() wrapper functions keep track of every listener and timer
888they create. When the add-on is removed (e.g., as part of its reload
889sequence), removelistener() is called for each of these listeners, and
890each timer has its stop() method called.
891_
892For the time being, you have to track any other resources outside the
893namespace of your add-on by yourself and clean them up in the unload()
894function, e.g. delete canvas or close any files you opened.
895
896You can define this unload() function in the addon-main.nas file. When
897your add-on is reloaded, its unload() function, if defined, will be
898called with one argument: the addons.Addon object (a Nasal ghost)
899corresponding to your add-on. unload() is run in the add-on's own
900namespace.
901
902In FlightGear Versions before 2020.1 the reload is triggered by setting the
903property /addon/by-id/ADDON_ID/reload to true (replace ADDON_ID with your
904particular add-on identifier).
905
906!!! Since version 2020.1 reload should be done like this:
907!!! fgcommand("addon-reload", props.Node.new({'id': 'ADDON_ID'}));
908
909You can add a menu item to trigger the reload easily, but it should be removed
910before publishing your add-on to endusers (see the above warning).
911
912Please have a look at the skeleton add-on at
913https://sourceforge.net/p/flightgear/fgaddon/HEAD/tree/trunk/Addons/Skeleton/
914
915
916Footnotes
917---------
918
919[1] \n represents end-of-line in string literals of languages such as C,
920    C++, Python and many others. We use this convention here to
921    represent the end-of-line character sequence in the XML data.
922
923[2] MAJOR.MINOR.PATCHLEVEL[{a|b|rc}N1][.devN2] where MAJOR, MINOR and
924    PATCHLEVEL are non-negative integers, and N1 and N2 are positive
925    integers.
926