# # 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> # # <init> - Optional: Initialization section consist of one or more # set nodes: # <set> # <property> - Property to set # <value> - value # # <step> - Tutorial step - a segment of the tutorial, consisting of # the following: # <message> - Text instruction displayed when the tutorial reaches # this step, and when neither the exit nor any error # criteria have been fulfilled # <audio> - Optional: wav filename to play when displaying # 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: # <message> - Error message to display if error criteria fulfilled. # <audio> - Optional: wav filename to play when error condition fulfilled. # <condition> - error condition (see $FG_ROOT/Docs/README.condition) # # <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. # <audio> - Optional: wav filename to play when the tutorial exits the last step # # GLOBAL VARIABLES # 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")) { if (c.getChild("name").getValue() == name) { 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; set_properties(tutorial.getChild("init")); set_cursor(tutorial); init_nasal(); run_nasal(tutorial); var dir = tutorial.getChild("audio-dir"); 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); } var stopTutorial = func { is_running(0); set_cursor(props.Node.new()); } # 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. # 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(); } var v = end.getNode("audio"); if (v != nil) { voice = v.getValue(); } run_nasal(end); } say(message, voice); say("Deviations: " ~ num_errors); is_running(0); return; } var step = tutorial.getChildren("step")[current_step]; set_cursor(step); var instr = step.getChild("message"); 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. var v = step.getChild("audio"); if (v != nil) { voice = v.getValue(); } 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(); var v = e.getChild("audio"); 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); } # scan all <set> blocks and set their <property> to <value> # <set> # <property>/foo/bar</property> # <value>woof</value> # </set> # 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); } } } 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, }); } # Set and return running state. Disable/enable stop menu. # 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); } # Output the message and optional sound recording. # var lastmsgcount = 0; var say = func(msg, snd = nil, error = 0) { var lastmsg = getprop("/sim/tutorial/last-message"); if (msg != lastmsg or (error == 1 and lastmsgcount == 1)) { # 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. var prop = { path : audio_dir, file : snd }; fgcommand("play-audio-message", props.Node.new(prop)); screen.log.write(msg, 1, 1, 1); } setprop("/sim/tutorial/last-message", msg); lastmsgcount = 0; } else { lastmsgcount += 1; } } var run_nasal = func(node) { node != nil or return; foreach (var n; node.getChildren("nasal")) { if (n.getNode("module") == nil) { n.getNode("module", 1).setValue("__tutorial"); } fgcommand("nasal", n); } } # 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" })); } var marker = nil; _setlistener("/sim/signals/nasal-dir-initialized", func { marker = props.globals.getNode("/sim/model/marker", 1); });