2006-02-22 17:03:42 +00:00
|
|
|
#
|
|
|
|
# Functions for XML-based tutorials
|
|
|
|
#
|
|
|
|
|
|
|
|
#
|
|
|
|
# Each tutorial consists of the following XML sections
|
|
|
|
#
|
|
|
|
# <name> - Tutorial Name
|
|
|
|
# <description> - description
|
|
|
|
# <audio-dir> - Optional: Directory to pick up audio instructions from.
|
|
|
|
# Relative to FG_ROOT
|
|
|
|
# <timeofday> - Optional: Time of day setting for tutorial:
|
|
|
|
# dawn/morning/noon/afternoon etc.
|
|
|
|
# <presets> - Optional: set of presets to used for start position.
|
|
|
|
# See commit-presets and gui/dialog/location-*.xml for details.
|
|
|
|
# <airport-id>
|
|
|
|
# <on-ground>
|
|
|
|
# <runway>
|
|
|
|
# <altitude-ft>
|
|
|
|
# <latitude-deg>
|
|
|
|
# <longitude-deg>
|
|
|
|
# <heading-deg>
|
|
|
|
# <airspeed-kt>
|
2007-03-19 23:46:50 +00:00
|
|
|
#
|
2007-03-20 17:32:28 +00:00
|
|
|
# <models>
|
|
|
|
# <model> - scenery object definition
|
|
|
|
# <path> - path to model (relative to $FG_ROOT)
|
|
|
|
# <longitude-deg>
|
|
|
|
# <latitude-deg>
|
|
|
|
# <elevation-ft>
|
|
|
|
# <heading-deg>
|
|
|
|
# <pitch-deg>
|
|
|
|
# <roll-deg
|
|
|
|
#
|
2006-02-22 17:03:42 +00:00
|
|
|
# <init> - Optional: Initialization section consist of one or more
|
|
|
|
# set nodes:
|
2007-03-19 23:46:50 +00:00
|
|
|
# <set>
|
|
|
|
# <property> - Property to set
|
|
|
|
# <value> - value
|
|
|
|
#
|
|
|
|
# <step> - Tutorial step - a segment of the tutorial, consisting of
|
|
|
|
# the following:
|
2007-03-20 16:22:26 +00:00
|
|
|
# <message> - Text instruction displayed when the tutorial reaches
|
2006-02-22 17:03:42 +00:00
|
|
|
# this step, and when neither the exit nor any error
|
|
|
|
# criteria have been fulfilled
|
2007-03-20 16:22:26 +00:00
|
|
|
# <audio> - Optional: wav filename to play when displaying
|
2006-02-22 17:03:42 +00:00
|
|
|
# instruction
|
|
|
|
# <error> - Error conditions, causing error messages to be displayed.
|
|
|
|
# The tutorial doesn't advance while any error conditions are
|
|
|
|
# fulfilled. Consists of one or more check nodes:
|
2007-03-20 16:22:26 +00:00
|
|
|
# <message> - Error message to display if error criteria fulfilled.
|
|
|
|
# <audio> - Optional: wav filename to play when error condition fulfilled.
|
2007-03-19 23:46:50 +00:00
|
|
|
# <condition> - error condition (see $FG_ROOT/Docs/README.condition)
|
2006-02-22 17:03:42 +00:00
|
|
|
#
|
2007-03-19 23:46:50 +00:00
|
|
|
# <exit> - Exit criteria causing tutorial to progress to next step.
|
|
|
|
# <condition> - exit condition (see $FG_ROOT/Docs/README.condition)
|
|
|
|
#
|
|
|
|
# <end>
|
|
|
|
# <message>> - Optional: Text to display when the tutorial exits the last step.
|
2007-03-20 16:22:26 +00:00
|
|
|
# <audio> - Optional: wav filename to play when the tutorial exits the last step
|
2006-02-22 17:03:42 +00:00
|
|
|
|
|
|
|
#
|
|
|
|
# GLOBAL VARIABLES
|
|
|
|
#
|
|
|
|
|
|
|
|
|
2007-03-19 23:46:50 +00:00
|
|
|
var STEP_INTERVAL = 5; # time between tutorial steps
|
|
|
|
var EXIT_INTERVAL = 1; # time between fulfillment of a step and the start of the next step
|
|
|
|
|
|
|
|
var current_step = nil;
|
|
|
|
var num_errors = nil;
|
|
|
|
var tutorial = nil;
|
|
|
|
var num_step_runs = nil;
|
|
|
|
var audio_dir = nil;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var startTutorial = func {
|
|
|
|
var name = getprop("/sim/tutorial/current-tutorial");
|
|
|
|
if (name == nil) {
|
|
|
|
screen.log.write("No tutorial selected");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
tutorial = nil;
|
|
|
|
foreach (var c; props.globals.getNode("/sim/tutorial").getChildren("tutorial")) {
|
2007-03-20 17:32:28 +00:00
|
|
|
if (c.getNode("name").getValue() == name) {
|
2007-03-19 23:46:50 +00:00
|
|
|
tutorial = c;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tutorial == nil) {
|
|
|
|
screen.log.write('Unable to find tutorial "' ~ name ~ '"');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
screen.log.write('Loading tutorial "' ~ name ~ '" ...');
|
|
|
|
is_running(1);
|
|
|
|
current_step = 0;
|
|
|
|
num_step_runs = 0;
|
|
|
|
num_errors = 0;
|
|
|
|
|
2007-03-20 17:32:28 +00:00
|
|
|
set_properties(tutorial.getNode("init"));
|
2007-03-19 23:46:50 +00:00
|
|
|
set_cursor(tutorial);
|
2007-03-20 17:32:28 +00:00
|
|
|
set_models(tutorial.getNode("models"));
|
2007-03-20 16:22:26 +00:00
|
|
|
init_nasal();
|
2007-03-19 23:46:50 +00:00
|
|
|
run_nasal(tutorial);
|
|
|
|
|
2007-03-20 17:32:28 +00:00
|
|
|
var dir = tutorial.getNode("audio-dir");
|
2007-03-19 23:46:50 +00:00
|
|
|
if (dir != nil) {
|
|
|
|
audio_dir = getprop("/sim/fg-root") ~ "/" ~ dir.getValue() ~ "/";
|
|
|
|
} else {
|
|
|
|
audio_dir = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
var presets = tutorial.getChild("presets");
|
|
|
|
if (presets != nil) {
|
|
|
|
props.copy(presets, props.globals.getNode("/sim/presets"));
|
|
|
|
fgcommand("presets-commit", props.Node.new());
|
|
|
|
|
|
|
|
# Set the various engines to be running
|
|
|
|
if (getprop("/sim/presets/on-ground")) {
|
|
|
|
var eng = props.globals.getNode("/controls/engines");
|
|
|
|
if (eng != nil) {
|
|
|
|
foreach (var c; eng.getChildren("engine")) {
|
|
|
|
c.getNode("magnetos", 1).setIntValue(3);
|
|
|
|
c.getNode("throttle", 1).setDoubleValue(0.5);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var timeofday = tutorial.getChild("timeofday");
|
|
|
|
if (timeofday != nil) {
|
|
|
|
fgcommand("timeofday", props.Node.new({"timeofday": timeofday.getValue()}));
|
|
|
|
}
|
|
|
|
|
|
|
|
# Pick up any weather conditions/scenarios set
|
|
|
|
setprop("/environment/rebuild-layers", getprop("/environment/rebuild-layers") + 1);
|
|
|
|
settimer(stepTutorial, STEP_INTERVAL);
|
|
|
|
}
|
2006-02-22 17:03:42 +00:00
|
|
|
|
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
|
2007-03-19 23:46:50 +00:00
|
|
|
var stopTutorial = func {
|
|
|
|
is_running(0);
|
2007-03-20 17:32:28 +00:00
|
|
|
remove_models();
|
2007-03-19 23:46:50 +00:00
|
|
|
set_cursor(props.Node.new());
|
2006-02-22 17:03:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# stepTutorial
|
|
|
|
#
|
|
|
|
# This function does the actual work. It is executed every 5 seconds.
|
|
|
|
#
|
|
|
|
# Each iteration it:
|
|
|
|
# - Gets the current step node from the tutorial
|
|
|
|
# - If this is the first time the step is entered, it displays the instruction message
|
|
|
|
# - Otherwise, it
|
|
|
|
# - Checks if the exit conditions have been met. If so, it increments the step counter.
|
|
|
|
# - Checks for any error conditions, in which case it displays a message to the screen and
|
|
|
|
# increments an error counter
|
|
|
|
# - Otherwise display the instructions for the step.
|
|
|
|
# - Sets the timer for 5 seconds again.
|
|
|
|
#
|
2007-03-19 23:46:50 +00:00
|
|
|
var stepTutorial = func {
|
|
|
|
if (!is_running()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var voice = nil;
|
|
|
|
var message = nil;
|
|
|
|
|
|
|
|
# If we've reached the end of the tutorial, simply indicate and exit
|
|
|
|
if (current_step >= size(tutorial.getChildren("step"))) {
|
|
|
|
message = "Tutorial finished.";
|
|
|
|
|
|
|
|
var end = tutorial.getNode("end");
|
|
|
|
if (end != nil) {
|
|
|
|
var m = end.getNode("message");
|
|
|
|
if (m != nil) {
|
|
|
|
message = m.getValue();
|
|
|
|
}
|
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
var v = end.getNode("audio");
|
2007-03-19 23:46:50 +00:00
|
|
|
if (v != nil) {
|
|
|
|
voice = v.getValue();
|
|
|
|
}
|
|
|
|
run_nasal(end);
|
|
|
|
}
|
|
|
|
say(message, voice);
|
2007-03-20 16:22:26 +00:00
|
|
|
say("Deviations: " ~ num_errors);
|
2007-03-19 23:46:50 +00:00
|
|
|
is_running(0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var step = tutorial.getChildren("step")[current_step];
|
|
|
|
set_cursor(step);
|
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
var instr = step.getChild("message");
|
2007-03-19 23:46:50 +00:00
|
|
|
message = instr != nil ? instr.getValue() : "Tutorial step " ~ current_step;
|
|
|
|
|
|
|
|
if (!num_step_runs) {
|
|
|
|
# If this is the first time we've encountered this step :
|
|
|
|
# - Set any values required
|
|
|
|
# - Display any messages
|
|
|
|
# - Play any instructions.
|
|
|
|
#
|
|
|
|
# We then do not go through the error or exit processing, giving the user
|
|
|
|
# time to react to the instructions.
|
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
var v = step.getChild("audio");
|
|
|
|
if (v != nil) {
|
|
|
|
voice = v.getValue();
|
2007-03-19 23:46:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
say(message, voice);
|
|
|
|
set_properties(step);
|
|
|
|
|
|
|
|
num_step_runs += 1;
|
|
|
|
settimer(stepTutorial, STEP_INTERVAL);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
run_nasal(step);
|
|
|
|
|
|
|
|
var error = 0;
|
|
|
|
# Check for error conditions
|
|
|
|
foreach (var e; step.getChildren("error")) {
|
|
|
|
if (props.condition(e.getNode("condition"))) {
|
|
|
|
error = 1;
|
|
|
|
num_errors += 1;
|
|
|
|
run_nasal(e);
|
|
|
|
|
|
|
|
var m = e.getNode("message");
|
|
|
|
if (m != nil) {
|
|
|
|
message = m.getValue();
|
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
var v = e.getChild("audio");
|
2007-03-19 23:46:50 +00:00
|
|
|
voice = v != nil ? v.getValue() : nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Check for exit condition, but only if we didn't hit any errors
|
|
|
|
if (!error) {
|
|
|
|
var e = step.getNode("exit");
|
|
|
|
if (e != nil) {
|
|
|
|
if (props.condition(e.getNode("condition"))) {
|
|
|
|
run_nasal(e);
|
|
|
|
current_step += 1;
|
|
|
|
num_step_runs = 0;
|
|
|
|
return settimer(stepTutorial, EXIT_INTERVAL);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
current_step += 1;
|
|
|
|
num_step_runs = 0;
|
|
|
|
return settimer(stepTutorial, EXIT_INTERVAL);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Display the resulting message and wait to go around again.
|
|
|
|
say(message, voice, error);
|
|
|
|
settimer(stepTutorial, STEP_INTERVAL);
|
2006-02-22 17:03:42 +00:00
|
|
|
}
|
|
|
|
|
2007-03-19 23:46:50 +00:00
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
|
2007-03-19 23:46:50 +00:00
|
|
|
# scan all <set> blocks and set their <property> to <value>
|
2006-02-22 17:03:42 +00:00
|
|
|
# <set>
|
2007-03-19 23:46:50 +00:00
|
|
|
# <property>/foo/bar</property>
|
|
|
|
# <value>woof</value>
|
2006-02-22 17:03:42 +00:00
|
|
|
# </set>
|
|
|
|
#
|
2007-03-19 23:46:50 +00:00
|
|
|
var set_properties = func(node) {
|
|
|
|
node != nil or return;
|
|
|
|
foreach (var c; node.getChildren("set")) {
|
|
|
|
var p = c.getChild("property").getValue();
|
|
|
|
var v = c.getChild("value").getValue();
|
|
|
|
|
|
|
|
if (p != nil and v != nil) {
|
|
|
|
setprop(p, v);
|
|
|
|
}
|
|
|
|
}
|
2006-02-22 17:03:42 +00:00
|
|
|
}
|
|
|
|
|
2007-03-19 23:46:50 +00:00
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
|
2007-03-20 17:32:28 +00:00
|
|
|
var models = [];
|
|
|
|
var set_models = func(node) {
|
|
|
|
remove_models();
|
|
|
|
|
|
|
|
node != nil or return;
|
|
|
|
var manager = props.globals.getNode("/models", 1);
|
|
|
|
foreach (var src; node.getChildren("model")) {
|
|
|
|
var i = 0;
|
|
|
|
while (i += 1) {
|
|
|
|
if (manager.getChild("model", i, 0) == nil) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var dest = manager.getChild("model", i, 1);
|
|
|
|
props.copy(src, dest);
|
|
|
|
dest.getNode("load", 1); # this make the modelmgr load the model
|
|
|
|
dest.removeChildren("load");
|
|
|
|
append(models, dest);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var remove_models = func {
|
|
|
|
foreach (var m; models) {
|
|
|
|
m.getParent().removeChild(m.getName(), m.getIndex());
|
|
|
|
}
|
|
|
|
models = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2007-03-19 23:46:50 +00:00
|
|
|
var set_cursor = func(node) {
|
|
|
|
node != nil or return;
|
|
|
|
var loc = node.getNode("marker");
|
|
|
|
if (loc == nil) {
|
|
|
|
marker.getNode("arrow-enabled", 1).setBoolValue(0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var s = loc.getNode("scale");
|
|
|
|
marker.setValues({
|
|
|
|
"x/value": loc.getNode("x", 1).getValue(),
|
|
|
|
"y/value": loc.getNode("y", 1).getValue(),
|
|
|
|
"z/value": loc.getNode("z", 1).getValue(),
|
|
|
|
"scale/value": s != nil ? s.getValue() : 1,
|
|
|
|
"arrow-enabled": 1,
|
|
|
|
});
|
2006-02-22 17:03:42 +00:00
|
|
|
}
|
|
|
|
|
2007-03-19 23:46:50 +00:00
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
|
2006-02-24 11:03:22 +00:00
|
|
|
# Set and return running state. Disable/enable stop menu.
|
|
|
|
#
|
2007-03-19 23:46:50 +00:00
|
|
|
var is_running = func(which = nil) {
|
|
|
|
var prop = "/sim/tutorial/running";
|
|
|
|
if (which != nil) {
|
|
|
|
setprop(prop, which);
|
|
|
|
gui.menuEnable("tutorial-stop", which);
|
|
|
|
}
|
|
|
|
return getprop(prop);
|
2006-02-24 11:03:22 +00:00
|
|
|
}
|
|
|
|
|
2007-03-19 23:46:50 +00:00
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
|
2006-02-22 17:03:42 +00:00
|
|
|
# Output the message and optional sound recording.
|
|
|
|
#
|
2007-03-20 16:22:26 +00:00
|
|
|
var lastmsgcount = 0;
|
|
|
|
var say = func(msg, snd = nil, error = 0) {
|
2007-03-19 23:46:50 +00:00
|
|
|
var lastmsg = getprop("/sim/tutorial/last-message");
|
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
if (msg != lastmsg or (error == 1 and lastmsgcount == 1)) {
|
2007-03-19 23:46:50 +00:00
|
|
|
# Error messages are only displayed every 10 seconds (2 iterations)
|
|
|
|
# Other messages are only displayed if they change
|
|
|
|
if (snd == nil) {
|
|
|
|
# Simply set to the co-pilot channel. TTS is picked up automatically.
|
|
|
|
setprop("/sim/messages/copilot", msg);
|
|
|
|
} else {
|
|
|
|
# Play the audio, and write directly to the screen-logger to avoid
|
|
|
|
# any tts being sent to festival.
|
2007-03-20 16:22:26 +00:00
|
|
|
var prop = { path : audio_dir, file : snd };
|
|
|
|
fgcommand("play-audio-message", props.Node.new(prop));
|
2007-03-19 23:46:50 +00:00
|
|
|
screen.log.write(msg, 1, 1, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
setprop("/sim/tutorial/last-message", msg);
|
2007-03-20 16:22:26 +00:00
|
|
|
lastmsgcount = 0;
|
2007-03-19 23:46:50 +00:00
|
|
|
} else {
|
2007-03-20 16:22:26 +00:00
|
|
|
lastmsgcount += 1;
|
2007-03-19 23:46:50 +00:00
|
|
|
}
|
2006-02-22 17:03:42 +00:00
|
|
|
}
|
2007-03-19 23:46:50 +00:00
|
|
|
|
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
|
2007-03-19 23:46:50 +00:00
|
|
|
var run_nasal = func(node) {
|
|
|
|
node != nil or return;
|
|
|
|
foreach (var n; node.getChildren("nasal")) {
|
|
|
|
if (n.getNode("module") == nil) {
|
2007-03-20 16:22:26 +00:00
|
|
|
n.getNode("module", 1).setValue("__tutorial");
|
2007-03-19 23:46:50 +00:00
|
|
|
}
|
|
|
|
fgcommand("nasal", n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2007-03-20 16:22:26 +00:00
|
|
|
|
|
|
|
# Set up the namespace for embedded Nasal.
|
|
|
|
#
|
|
|
|
var init_nasal = func {
|
|
|
|
globals.__tutorial = {
|
|
|
|
say : say,
|
|
|
|
next : func { current_step += 1; num_step_runs = 0 },
|
|
|
|
previous : func {
|
|
|
|
if (current_step > 0) {
|
|
|
|
current_step -= 1;
|
|
|
|
}
|
|
|
|
num_step_runs = 0;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var dialog = func {
|
|
|
|
fgcommand("dialog-show", props.Node.new({ "dialog-name" : "marker-adjust" }));
|
|
|
|
}
|
2007-03-19 23:46:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
var marker = nil;
|
|
|
|
_setlistener("/sim/signals/nasal-dir-initialized", func {
|
|
|
|
marker = props.globals.getNode("/sim/model/marker", 1);
|
|
|
|
});
|
|
|
|
|
|
|
|
|