# # Functions for XML-based tutorials # # # Each tutorial consists of the following XML sections # # - Tutorial Name # - description # - Optional: Directory to pick up audio instructions from. # Relative to FG_ROOT # - Optional: Time of day setting for tutorial: # dawn/morning/noon/afternoon etc. # - Optional: set of presets to used for start position. # See commit-presets and gui/dialog/location-*.xml for details. # # # # # # # # # - Optional: Initialization section consist of one or more # set nodes: # # - Property to set # - value # - Tutorial step - a segment of the tutorial, consisting of # the following: # - Text instruction displayed when the tutorial reaches # this step, and when neither the exit nor any error # criteria have been fulfilled # - Optional: wav filename to play when displaying # instruction # - 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: # # - property to check. # - less than value. One of , , must be defined # - greater than value. One of , , must be defined # - equal value. One of , , must be defined # - Error message to display if error criteria fulfilled. # - Optional: wav filename to play when error condition fulfilled. # - Exit criteria causing tutorial to progress to next step. Consists of # one or more check nodes. All check nodes must be fulfilled at the same # time of the tutorial to progress to the next step. # - property to check. # - less than value. One of , , must be defined # - greater than value. One of , , must be defined # - equal value. One of , , must be defined # # - Optional: Text to display when the tutorial exits the last step. # - Optional: wav filename to play when the tutorial exits the last step # # GLOBAL VARIABLES # # Time between tutorial steps. STEP_TIME = 5; # We also have a gap between the fulfillment of a step and the start # of the next step. This can be much shorter. STEP_EXIT = 1; m_currentStep = 0; m_errors = 0; m_tutorial = 0; m_firstEntry = 1; m_audioDir = ""; m_lastmsgcount = 0; # # startTutorial() # # Start a tutorial defined within xiProp # startTutorial = func { m_currentStep = 0; m_errors = 0; if (getprop("/sim/tutorial/current-tutorial") == nil) { # Tutorial not defined - exit screen.log.write("No tutorial selected"); return; } ltutorial = getprop("/sim/tutorial/current-tutorial"); lfound = 0; foreach(c; props.globals.getNode("/sim/tutorial").getChildren("tutorial")) { if (c.getChild("name").getValue() == ltutorial) { m_tutorial = c; lfound = 1; } } if (lfound == 0) { # Unable to find tutorial screen.log.write("Unable to find tutorial : " ~ ltutorial); return; } # Indicate that the tutorial is running screen.log.write("Loading tutorial: " ~ ltutorial ~ " ..."); isRunning(1); # If defined, get the audio directory if (m_tutorial.getChild("audio-dir") != nil) { fg_root = getprop("/sim/fg-root"); m_audioDir = sprintf("%s/%s/", fg_root, m_tutorial.getChild("audio-dir").getValue()); } # Set the time of day, if present timeofday = m_tutorial.getChild("timeofday"); if (timeofday != nil) { fgcommand("timeofday", props.Node.new("timeofday", timeofday.getValue())); } # First, set any presets that might be present presets = m_tutorial.getChild("presets"); if ((presets != nil) and (presets.getChildren() != nil)) { children = presets.getChildren(); foreach(c; children) { setprop("/sim/presets/" ~ c.getName(), c.getValue()); } # Apply the 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 (c; eng.getChildren("engine")) { c.getNode("magnetos", 1).setIntValue(3); c.getNode("throttle", 1).setDoubleValue(0.5); } } } } # Run through any initialization nodes inits = m_tutorial.getChild("init"); if ((inits != nil) and (inits.getChildren("set") != nil)) { children = inits.getChildren("set"); foreach(c; children) { setVal(c); } } # Pick up any weather conditions/scenarios set setprop("/environment/rebuild-layers", getprop("/environment/rebuild-layers")+1); # Set the timer to start the first tutorial step settimer(stepTutorial, STEP_TIME); } # # stopTutorial # # Stops the current tutorial . # stopTutorial = func { isRunning(0); settimer(resetStop, STEP_TIME); } # # resetStop # # Reset the stop value, having given any running tutorials the # chance to stop. Also reset the various state variables incase # they have been changed. resetStop = func { m_currentStep = 0; m_firstEntry = 1; m_errors = 0; } # # 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. # stepTutorial = func { var lerror = 0; var ltts = nil; var lsnd = nil; var lmessage = nil; if (!isRunning()) { # If we've been told to stop, just do so. return; } # If we've reached the end of the tutorial, simply indicate and exit if (m_currentStep == size(m_tutorial.getChildren("step"))) { # End of tutorial. lfinished = "Tutorial finished."; if (m_tutorial.getChild("endtext") != nil) { lfinished = m_tutorial.getChild("endtext").getValue(); } if (m_tutorial.getChild("endtext-voice") != nil) { lsnd = m_tutorial.getChild("endtext-voice").getValue(); } say(lfinished, lsnd); say("Deviations : " ~ m_errors); isRunning(0); return; } lstep = m_tutorial.getChildren("step")[m_currentStep]; lmessage = "Tutorial step " ~ m_currentStep; if (lstep.getChild("instruction") != nil) { # By default, display the current instruction lmessage = lstep.getChild("instruction").getValue(); } if (m_firstEntry == 1) { # 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. if (lstep.getChild("instruction-voice") != nil) { lsnd = lstep.getChild("instruction-voice").getValue(); } say(lmessage, lsnd); # Set any properties foreach (c; lstep.getChildren("set")) { setVal(c); } m_firstEntry = 0; settimer(stepTutorial, STEP_TIME); return; } # Check for error conditions if ((lstep.getChild("error") != nil) and (lstep.getChild("error").getChildren("check") != nil)) { #print("Checking errors"); foreach(c; lstep.getChild("error").getChildren("check")) { if (checkVal(c)) { # and error condition was fulfilled - set the error message lerror = 1; m_errors += 1; if (c.getChild("msg") != nil) { lmessage = c.getChild("msg").getValue(); if (c.getChild("msg-voice") != nil) { lsnd = c.getChild("msg-voice").getValue(); } } } } } # Check for exit condition, but only if we didn't hit any errors if (lerror == 0) { if ((lstep.getChild("exit") == nil) or (lstep.getChild("exit").getChildren("check") == nil)) { m_currentStep += 1; m_firstEntry = 1; settimer(stepTutorial, STEP_EXIT); return; } else { lexit = 1; foreach(c; lstep.getChild("exit").getChildren("check")) { if (checkVal(c) == 0) { lexit = 0; } } if (lexit == 1) { # Passed all exit steps m_currentStep += 1; m_firstEntry = 1; settimer(stepTutorial, STEP_EXIT); return; } } } # Display the resulting message and wait to go around again. say(lmessage, lsnd, lerror); settimer(stepTutorial, STEP_TIME); } # Set a value in the property tree based on an entry of the form # # /foo/bar # woof # # setVal = func(node) { if (node.getName("set")) { lprop = node.getChild("prop").getValue(); lval = node.getChild("val").getValue(); if ((lprop != nil) and (lval != nil)) { setprop(lprop, lval); } } } # Check a value in the property tree based on an entry of the form # # # /foo/bar # <_operator_>woof # # where _operator_ may be one of "eq", "lt", "gt" # checkVal = func(node) { if (node.getName("check")) { lprop = node.getChild("prop").getValue(); if (getprop(lprop) == nil) { # This is probably an error print("Undefined property: " ~ lprop); return 0; } if ((node.getChild("eq") != nil) and (getprop(lprop) == node.getChild("eq").getValue())) { return 1; } if ((node.getChild("lt") != nil) and (getprop(lprop) < node.getChild("lt").getValue() )) { return 1; } if ((node.getChild("gt") != nil) and (getprop(lprop) > node.getChild("gt").getValue() )) { return 1; } } return 0; } # # Set and return running state. Disable/enable stop menu. # isRunning = 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. # say = func(msg, snd=nil, lerror=0) { var lastmsg = getprop("/sim/tutorial/last-message"); if ((msg != lastmsg) or (lerror == 1 and m_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. lprop = { path : m_audioDir, file : snd }; fgcommand("play-audio-message", props.Node.new(lprop) ); screen.log.write(msg, 1, 1, 1); } setprop("/sim/tutorial/last-message", msg); m_lastmsgcount = 0; } else { m_lastmsgcount += 1; } }