Nasal scripting language: Difference between revisions

Jump to navigation Jump to search
Line 969: Line 969:
=== Other useful geographical functions ===
=== Other useful geographical functions ===
Other useful geographical functions are found in geo.nas (in the <tt>[[$FG_ROOT]]/Nasal</tt> directory of a FlightGear installation). geo.nas also includes documentation/explanation of the functions available.
Other useful geographical functions are found in geo.nas (in the <tt>[[$FG_ROOT]]/Nasal</tt> directory of a FlightGear installation). geo.nas also includes documentation/explanation of the functions available.
==Developing and debugging in Nasal==
===Developing Nasal code===
Because code in the Nasal directory is parsed only at FlightGear startup, testing and debugging Nasal code can by slow and difficult.
FlightGear provides a couple of ways to work around this issue:
====Nasal Console====
The Nasal Console is available in FlightGear's menu (Debug/Nasal Console).  Selecting this menu opens a Nasal Console dialog.
This dialog has several tabs, of which each can hold separate Nasal code snippets, all of which are saved on exit
and reloaded next time. This is useful for little tests, or for executing code for which writing a key binding is just too much
work, such as "props.dump(props.globals)".
If you want to add more tabs (radio buttons in the Nasal Console dialog) to hold more code samples, just add more &lt;code&gt; nodes to autosave.xml.
[[Nasal Console]]
====Loading/reloading Nasal code without re-starting FlightGear====
A common problem in testing and debugging Nasal programs is that each testing step requires stopping and re-starting FlightGear, a slow process.
Below is described a technique for loading and executing a Nasal file while FlightGear is running.  FlightGear will parse the file, display any errors in the FlightGear console window, and then execute the code as usual.
Using this technique, you can start FlightGear, load the Nasal code you want to test observe any errors or test functionality as you wish, make changes to the Nasal file, reload it to observe parse errors or change in functionality, and so on to repeatedly and quickly run through the change/load/parse/test cycle without needing to re-start FlightGear each time.
The key to this technique is the function io.load_nasal(), which loads a nasal file into a nasal namespace.
Step-by-step instructions showing how to use this technique to load, parse, and test a Nasal file while FlightGear is running:
=====Create the Nasal file to test=====
Create a text file named $FG_ROOT/foo/test.nas with this text:
 
  print("hi!");
  var msg="My message.";
  var hello = func { print("I'm the test.hello() function") }
Notes: You can create the file in any directory you wish, as long as Nasal can read the directory--but the file IOrules in the Nasal directory restricts which directories Nasal may read and write from. 
You can give the file any name and extension you wish, though it is generally most convenient to use the .nas extension with Nasal files.
=====Load the file and test=====
Start FlightGear.  You can import the file above into FlightGear by typing the following into the Nasal Console dialog and executing the code:
  io.load_nasal(getprop("/sim/fg-root") ~ "/foo/test.nas", "example");
getprop("/sim/fg-root") gets the root directory of the FlightGear installation, ~ "/foo/test.nas" appends the directory and filename you created.  The final variable "example" tells the namespace to load for the Nasal file.
You'll see the message "hi!" on the terminal, and have function "example.hello()" immediately available. You can, for instance, type "example.hello();" into one of the Nasal console windows and press "Execute" to see the results; similarly you could execute "print (example.msg);".
If you find errors or want to make changes, simply make them in your text editor, save the file, and execute the io.load_nasal() command again in the Nasal Console to re-load the file with changes.
It's worth noting that Nasal code embedded in XML GUI dialog files can be reloaded by using the "debug" menu ("reload GUI").
You may also want to check out the remarks on [[Nasal scripting language#Memory management|Memory management]].
==== Managing timers and listeners ====
Note: If your Nasal program sets listeners, timer loops, and so on, they will remain set even when the code is reloaded, and reloading the code will set additional listeners and timer loops. 
This can lead to extremely slow framerates and unexpected behavior.  For timers you can avoid this problem by using the loopid method (described above); for listeners you can create a function to destroy all listeners your Nasal program creates, and call that function before reloading the program.  (And cleaning up timer loops and listeners is a best practice for creating Nasal programs in FlightGear regardless.)
The same problem may occur while resetting or re-initializing parts of FlightGear if your code isn't prepared for this. And obviously this applies in particular also to any worker threads you may have started, too!
For complex Nasal scripts with many timers and listeners, it is therefore generally a very good idea to implement special callbacks so that your scripts can respond to the most important simulator "signals", this can be achieved by registering script-specific listeners to signals like "reinit" or "freeze" (pause): the corresponding callbacks can then suspend or re-initialize the Nasal code by suspending listeners and timers.
Following this practice helps ensure that your code will behave properly even during simulator resets.
In other words, it makes sense to provide a separate high-level controller routine to look for important simulator events and then pause or re-initialize your main Nasal code as required.
If you are using [[Nasal scripting language#System-wide Nasal code|System-wide Nasal modules]], you should register listeners to properly re-initialize and clean up your Nasal code.
In its simplest form, this could look like this:
<syntaxhighlight lang="php">
var cleanup = func {}
setlistener("/sim/signals/reinit", cleanup);
</syntaxhighlight>
This will invoke your "cleanup" function, whenever the "reinit" signal is set by the FlighGear core.
Obviously, you now need to populate your cleanup function with some code, too.
One of the easiest ways to do this, is removing all listeners/timers manually here, i.e. by adding calls to removelistener():
<syntaxhighlight lang="php">
var cleanup = func {
  removelistener(id1);
  removelistener(id2);
  removelistener(id3);
}
</syntaxhighlight>
This would ensure that the corresponding listeners would be removed once the signal is triggered.
Now, keeping track of all listeners manually is tedious.
On the other hand, you could just as well use a vector of listener IDs here, and then use a Nasal foreach loop:
<syntaxhighlight lang="php">
var cleanup = func(id_list) {
  foreach(var id; id_list)
  removelistener(id);
}
</syntaxhighlight>
Obviously, this would require that you maintain a list of active listeners, too - so that you can actually pass a list of IDs to the cleanup function.
This is one of those things that can be easily done in Nasal, too - just by introducing a little helper wrapper:
<syntaxhighlight lang="php">
var id_list=[];
var store_listener = func(id) append(id_list,id);
</syntaxhighlight>
The only thing required here, would be replacing/wrapping the conventional "setlistener" call with calls to your helper:
<syntaxhighlight lang="php">
store_listener( setlistener("/sim/foo") );
store_listener( setlistener("/foo/bar") );
</syntaxhighlight>
If you were to do this consistently across all your Nasal code, you'd end up with a high level way to manage all your registered listeners centrally.
You could further generalize everything like this:
<syntaxhighlight lang="php">
var id_list=[];
var store_listener = func(property) append(id_list,setlistener(property) );
store_listener("/sim/foo/bar");
</syntaxhighlight>
This will ensure that any "store_listener" call stores the listener's ID in the id_list vector, which makes it possible to easily remove all listeners, too:
<syntaxhighlight lang="php">
var cleanup_listeners = func {
  foreach(var l; id_list)
    remove_listener(l);
}
</syntaxhighlight>
Similarly, you could just as well "overload" functions like settimer() setlistener() in your script, this can be easily achieved by saving a handle to the original functions and then overriding them in your script:
<syntaxhighlight lang="php">
var original_settimer = settimer;
var original_setlistener = setlistener;
</syntaxhighlight>
Now, you have handles stored to the original functions, to make sure that you are referring to the correct version, you can also refer to the "globals" namespace.
Obviously, it makes sense to only store and clean up those listeners that are not themselves required to handle initialization/resets, otherwise you'd remove the listeners for your init routines, too.
Next, you can easily override settimer/setlistener in your script:
<syntaxhighlight lang="php">
var original_settimer = settimer;
var original_setlistener = setlistener;
var settimer = func(function, time, realtime=0) {
print("Using your own settimer function now");
}
var setlistener = func(property, function, startup=0, runtime=1) {
  print("Using your own setlistener function now!");
}
</syntaxhighlight>
In order to call the old implementation, just use the two handles that you have stored:
* original_settimer
* original_setlistener
<syntaxhighlight lang="php">
var original_settimer = settimer;
var original_setlistener = setlistener;
var settimer = func(function, time, realtime=0) {
print("Using your own settimer function now");
original_settimer(function, time, realtime);
}
var setlistener = func(property, function, startup=0, runtime=1) {
  print("Using your own setlistener function now!");
  original_setlistener(property, function, startup, runtime);
}
</syntaxhighlight>
So this is a very simple and elegant way to wrap and override global behavior. So that you can easily implement script-specific semantics and behavior, i.e. to automatically store handles to registered listeners:
<syntaxhighlight lang="php">
var original_settimer = settimer;
var original_setlistener = setlistener;
var cleanup_listeners = [];
var settimer = func(function, time, realtime=0) {
print("Using your own settimer function now");
original_settimer(function, time, realtime);
}
var setlistener = func(property, function, startup=0, runtime=1) {
  print("Using your own setlistener function now!");
  var handle = original_setlistener(property, function, startup, runtime);
  append(cleanup_listeners, handle);
}
</syntaxhighlight>
Thus, you can now have a simple "cleanup" function which processes all listeners stored in "cleanup_listeners" using a foreach loop and removes them:
<syntaxhighlight lang="php">
var original_settimer = settimer;
var original_setlistener = setlistener;
var cleanup_listeners = [];
var remove_listeners = func {
foreach(var l; cleanup_listeners) {
  removelistener(l);
}
}
var settimer = func(function, time, realtime=0) {
print("Using your own settimer function now");
original_settimer(function, time, realtime);
}
var setlistener = func(property, function, startup=0, runtime=1) {
  print("Using your own setlistener function now!");
  var handle = original_setlistener(property, function, startup, runtime);
  append(cleanup_listeners, handle);
}
</syntaxhighlight>
The only thing that's needed now to handle simulator resets, is registering an "anonymous" listener which triggers your "remove_listeners" callback upon simulator reset:
<syntaxhighlight lang="php">
  _setlistener( "/sim/signals/reinit", remove_listeners );
</syntaxhighlight>
Note how we're using the low level _setlistener() call directly here, to avoid adding the listener id to the "cleanup" vector, which would mean that we're also removing this listener - i.e. the cleanup would then only work once.
Now, you'll probably have noticed that it would make sense to consider wrapping all these helpers and variables inside an enclosing helper class, this can be accomplished in Nasal using a hash.
This would enable you to to implement everything neatly organized in an object and use RAII-like patterns to manage Nasal resources like timers, listeners and even threads.
===Debugging===
The file debug.nas, included in the Nasal directory of the FlightGear distribution, has several functions useful for debugging Nasal code.  These functions are available to any Nasal program or code executed by FlightGear.
Aside from those listed below, several other useful debugging functions are found in debug.nas; see the debug.nas file for the list of functions and explanation.
Note that the debug module makes extensive use of ANSI terminal color codes.  These create colored output on Linux/Unix systems but on other systems they may add numerous visible control codes.  To turn off the color codes, go to the internal property tree and set
/sim/startup/terminal-ansi-colors=0
Or within a Nasal program:
setprop ("/sim/startup/terminal-ansi-colors",0);
====debug.dump====
debug.dump([<variable>])            ... dumps full contents of variable or of local variables if none given
The function debug.dump() dumps the contents of the given variable to the console. On Unix/Linux this is done with some syntax coloring. For example, these lines
  var as = props.globals.getNode("/velocities/airspeed-kt", 1);
  debug.dump(as);
would output
  </velocities/airspeed-kt=1.021376474393101 (DOUBLE; T)>
The "T" means that it's a "tied" property. The same letters are used here as in the property-browser. The angle brackets seem superfluous, but are useful because debug.dump() also outputs compound data types, such as vectors and hashes. For example:
  var as = props.globals.getNode("/velocities/airspeed-kt", 1);
  var ac = props.globals.getNode("/sim/aircraft", 1);
  var nodes = [as, ac];
  var hash = { airspeed_node: as, aircraft_name: ac, all_nodes: nodes };
  debug.dump(hash);
yields:
  { all_nodes : [ </velocities/airspeed-kt=1.021376474393101 (DOUBLE; T)>,
  </sim/aircraft="bo105" (STRING)> ], airspeed_node : </velocities/airspe
  ed-kt=1.021376474393101 (DOUBLE; T)>, aircraft_name : </sim/aircraft="bo
  105" (STRING)> }
====debug.backtrace====
  debug.backtrace([<comment:string>]}  ... writes backtrace with local variables
  debug.bt                            ... abbreviation for debug.backtrace
The function debug.backtrace() outputs all local variables of the current function and all parent functions.
====debug.benchmark====
debug.benchmark(<label:string>, <func> [, <repeat:int>])
... runs function <repeat> times (default: 1) and prints execution time in seconds,prefixed with <label>.
This is extremely useful for benchmarking pieces of code to determin
====debug.exit====
  debug.exit()                        ... exits fgfs


== Related content ==
== Related content ==

Navigation menu