Contents

Background

So called "fgcommands" are FlightGear extension functions which can be invoked in a number of different ways, such as embedded in various XML files (in the form of "action" Bindings), but also via Nasal scripts using the fgcommand() extension function. In addition, it is also possible to invoke fgcommands using the telnet/props interface, so that a simple form of "Remote Procedure Calling" can be implemented, i.e. triggering code via telnet to be run inside the FlightGear process.

Compared to Nasal extension functions (or cppbind bindings), the nice thing about "fgcommands" is that they are not just available to Nasal scripts, but that they can also be used by lots of other FlightGear systems directly without resorting to scripts and registering listeners, such as in the form of GUI bindings, mouse/keyboard handlers and so on.

Of course, there are some disadvantages too: fgcommands always need their arguments wrapped in a property tree structure. This is because they don't have any concept of Nasal and its internal data structures, as they need to work with just XML files and the property tree.

So there is a certain marshaling/conversion overhead involved here. While this is certainly negligible for simple commands that are run rarely, this overhead may add up once a certain fgcommand gets called repeatedly at a high rate. In such cases it may be more efficient to let the command work with argument lists (vectors) rather than just a single argument, so that multiple tasks can be completed by a single invocation.

Obviously, not all fgcommands can be implemented in Nasal, which is why you may sometimes still have to use C++ - creating a new fgcommand using C++ is also fairly simple actually. The source code you need to look at is in flightgear/src/Main/fg_commands.cxx.

However, unlike $FG_ROOT, $FG_HOME is generally accessible for writing, consider this example:

# come up with a path and filename in $FG_HOME, inside the Export sub folder, file name is test.xml
var filename = getprop("/sim/fg-home") ~ "/Export/test.xml";
# use the write_properties() helper in io.nas, which is a wrapper for the savexml fgcommand (see README.commands)
io.write_properties( path: filename, prop: "/sim" );

This will dump the sub branch of the /sim property tree into $FG_HOME/Export/test.xml

For additional examples, see $FG_ROOT/Nasal/io.nas

To learn more about PropertyList processing via loadxml and savexml, please see $FG_ROOT/Docs/README.commands

Internally, all read/write access is validated via an API called fgValidatePath(), for details please see $FG_SRC/Main/util.cxx

The simplest command can be found from line 167, the do_null() command which does nothing:

To see if that's actually doing something, you could add a printf or cout statement and rebuild FlightGear (you'll probably want to use a new git branch for these experiments, in order not to mess up your main branch - when doing that you'll just need to "git add fg_commands.cxx && git commit -m test" after making modifications):

Next, you could run the modified fgcommand by using the Nasal console (make sure to watch your console):

fgcommand("null");

Regarding the syntax of the fgcommand() function: the first parameter is the name of the command that you'd like to run, the second parameter points to an optional property node that contains all the required parameters, that are separately set up.

Argument processing

All callback functions need to accept a single argument, i.e. a const pointer: const SGPropertyNode*. This single argument is used to pass a property sub branch to each fgcommand. This means that each fgcommand is responsible for parsing and processing its function arguments.

For instance, imagine a command that accepts three different parameters to play an audio file:

path

file

volume

These all need to be first set up in the property tree separately, i.e. using setprop:

const char * getStringValue () const: Get a string value for this node.

However, before actually trying to read in an argument, it is better to first check if the node is actually available, too:

if(arg->hasValue("path"))stringpath=arg->getStringValue("path");

If an important argument is missing that you cannot set to some sane default value, you should show an error message and return false, to leave the function early:

if(arg->hasValue("path"))stringpath=arg->getStringValue("path");else{printf("Cannot play audio file without a file name");returnfalse;}

There is also a helper macro called SG_LOG available to print debug messages to the console:

if(arg->hasValue("path"))stringpath=arg->getStringValue("path");else{SG_LOG(SG_GENERAL,SG_ALERT,"Cannot play audio file without a file name");returnfalse;}

Exception handling

To be on the safe side, complex fgcommands, which may terminate for one reason or another (such as missing files), should make use of exception handling to catch exceptions and deal with them gracefully, so that they cannot affect the simulator critically.

This is accomplished by using standard C++ try/catch blocks, usually catching an sg_exception. The SG_LOG() macro can be used to print debugging information to the console. If you'd like to display error information using the native FlightGear GUI, you can use the "guiErrorMessage(const char*,sg_exception)" helper.

For example, see the callback function "do_preferences_load" from line 445:

Adding new commands

All new commands must have the previously described signature, the functions should then be added to the list of built-in commands, beginning in line 1552. The list of built-in commands maps the human-readable names used in README.commands to the names of the internal C++ functions implementing them.

Reaching out to subsystems

Quite possibly, you may need to reach out to some other subsystems to implement a certain fgcommand, such as accessing the sound system, the networking/multiplayer system or the FDM system, this is accomplished by using the globals-> pointer, which allows you to access other subsystems easily.

Subsystem specific fgcommands

In addition to directly editing the default initialization routine in fg_init.cxx, you can also dynamically add/remove fgcommands from your SGSubsystem, by getting a handle to the SGCommandMgr singleton, specifying a command name, and a corresponding callback (which can be a static member of your SGSubsystem).

fgcommands having implicit dependencies on other subsystems, should not be added in a hard-coded fashion, but via the ctor/init or postinit() methods of the corresponding subsystem itself - otherwise, such code is conflicting with James' reset/-reinit work - in particular, the very fgcommands intended to allow subsystems to be shut-down and re-initialized.

Even if such additions don't break FG immediately, they contribute to crippling the reset/re-init effort - and will make it increasingly difficult to allow run-time dependencies to be better established and formalized.

We've had a long discussion about this exact issue WRT to Nasal/CppBind bindings added unconditionally, which is causing the same problem. The general consensus was that subsystem-specific functionality must be initialized and cleaned up by the corresponding subsystem itself, instead of always assuming that all subsystems are available.

Finally

When you send patches or file merge requests for new fgcommands, please also make sure to send patches for README.commands, too - so that your new commands are documented there. Otherwise, people may have a hard time using/maintaining your fgcommands, possibly years after you added them, without you even being around by then.

Also, depending on the nature of functionality that you would like to add to FlightGear, you may want to use a more sophisticated method of adding new features, such as adding custom Nasal extension functions, or adding a dedicated SGSubsystem, a full property tree wrapper using listeners, or the cppbind framework in Simgear in order to expose full classes/objects.

This was just an introduction intended to get you started, most of the missing information can be learned by referring to the plethora of existing fgcommands and seeing how they are implemented and working.

Please get in touch if you think that there's something missing here, or just add your information directly!