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