From 54693a663f5149e1d6e4c75a636668c6429bb2ab Mon Sep 17 00:00:00 2001 From: mfranz <mfranz> Date: Wed, 21 Mar 2007 17:31:26 +0000 Subject: [PATCH] - fix <audio> bug - check a step's errors in random order (otherwise a retarded instructor bitches minutes about the climb angle, and only then notices that the heading is totally wrong ;-) ... mabye simple scheduling would be better - document embedded <nasal> - start of <view> support --- Nasal/tutorial.nas | 155 +++++++++++++++++++++++++++++---------------- 1 file changed, 101 insertions(+), 54 deletions(-) diff --git a/Nasal/tutorial.nas b/Nasal/tutorial.nas index 9ba2381a4..409b6d258 100644 --- a/Nasal/tutorial.nas +++ b/Nasal/tutorial.nas @@ -1,5 +1,5 @@ -# # Functions for XML-based tutorials +# --------------------------------------------------------------------------------------- # # @@ -50,24 +50,38 @@ # criteria have been fulfilled # <audio> - Optional: wav filename to play when displaying # instruction +# <nasal><script> +# # <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. +# <nasal><script> # <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) +# <nasal><script> # # <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 +# <nasal><script> # # # NOTE: everywhere where <message> and/or <audio> is supported there can be # more than one <message> or <audio> entry defined, in which case one # is randomly chosen. +# All <nasal><script> run in a separate Nasal namespace. There are a few +# functions pre-defined in this namespace: +# - next(n=1) ... switch to next step (default) or n steps forward +# - previous(n=1) ... switch to previous step (default) or n steps back +# - say("...", who="copilot", delay=1) ... say message, with optional +# speaker and delay. Available speakers are "pilot", +# "copilot", "atc", "ai-plane", "apprach", "ground". +# Examples: say("Look! There!"); +# say("Oh, dear ...", "pilot", 2); # # # GLOBAL VARIABLES @@ -77,9 +91,11 @@ 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 tutorial = nil; +var steps = []; +var loop_id = 0; var current_step = nil; var num_errors = nil; -var tutorial = nil; var num_step_runs = nil; var audio_dir = nil; @@ -105,11 +121,13 @@ var startTutorial = func { return; } - screen.log.write('Loading tutorial "' ~ name ~ '" ...'); + stopTutorial(); is_running(1); + screen.log.write('Loading tutorial "' ~ name ~ '" ...'); current_step = 0; num_step_runs = 0; num_errors = 0; + steps = tutorial.getChildren("step"); set_properties(tutorial.getNode("init")); set_cursor(tutorial); @@ -143,22 +161,24 @@ var startTutorial = func { var timeofday = tutorial.getChild("timeofday"); if (timeofday != nil) { - fgcommand("timeofday", props.Node.new({"timeofday": timeofday.getValue()})); + 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); + settimer(func { stepTutorial(loop_id += 1) }, STEP_INTERVAL); } var stopTutorial = func { + loop_id += 1; is_running(0); - remove_models(); - set_cursor(props.Node.new()); + set_cursor(); } +_setlistener("/sim/crashed", stopTutorial); + # stepTutorial @@ -175,59 +195,59 @@ var stopTutorial = func { # - Otherwise display the instructions for the step. # - Sets the timer for 5 seconds again. # -var stepTutorial = func { - if (!is_running()) { - return; - } +var stepTutorial = func(id) { + id == loop_id or return; - var voice = nil; - var message = nil; + var continue_after = func(i) { settimer(func { stepTutorial(id) }, i) } - if (current_step >= size(tutorial.getChildren("step"))) { + if (current_step >= size(steps)) { # end of the tutorial - say_message(tutorial.getNode("end"), "Tutorial finished."); + var end = tutorial.getNode("end"); + say_message(end, "Tutorial finished."); say_message(nil, "Deviations: " ~ num_errors); - is_running(0); + run_nasal(end); + stopTutorial(); return; } - var step = tutorial.getChildren("step")[current_step]; + var step = steps[current_step]; set_cursor(step); + set_view(step.getNode("view")); + set_targets(tutorial.getNode("targets")); - if (!num_step_runs) { + if (num_step_runs == 0) { # first time we've encountered this step say_message(step, "Tutorial step " ~ current_step); set_properties(step); + run_nasal(step); num_step_runs += 1; - settimer(stepTutorial, STEP_INTERVAL); - return; + return continue_after(STEP_INTERVAL); } - set_targets(tutorial.getNode("targets")); - run_nasal(step); - - # Check for error conditions - foreach (var e; step.getChildren("error")) { - if (props.condition(e.getNode("condition"))) { + # check for error conditions in random order + foreach (var error; shuffle(step.getChildren("error"))) { + if (props.condition(error.getNode("condition"))) { num_errors += 1; - run_nasal(e); - say_message(e); - return settimer(stepTutorial, STEP_INTERVAL); + run_nasal(error); + say_message(error); + return continue_after(STEP_INTERVAL); } } - var e = step.getNode("exit"); - if (e != nil) { - if (!props.condition(e.getNode("condition"))) { - return settimer(stepTutorial, STEP_INTERVAL); + # check for exit condition + var exit = step.getNode("exit"); + if (exit != nil) { + if (!props.condition(exit.getNode("condition"))) { + return continue_after(STEP_INTERVAL); } - run_nasal(e); + run_nasal(exit); } + # success! current_step += 1; num_step_runs = 0; - return settimer(stepTutorial, EXIT_INTERVAL); + return continue_after(EXIT_INTERVAL); } @@ -279,7 +299,6 @@ var set_targets = func(node) { var models = []; var set_models = func(node) { - remove_models(); node != nil or return; var manager = props.globals.getNode("/models", 1); @@ -307,8 +326,20 @@ var remove_models = func { } +var set_view = func(node = nil) { + node != nil or return; + if (!size(node.getChildren())) { + var name = node.getValue(); + node = tutorial.getNode("views"); + node != nil or die("<view>name</view>, but no <views> group"); + node = node.getNode(name); + node != nil or die("<view>name</view> refers to non existing <views> group"); + } + props.copy(node, props.globals.getNode("/sim/current-view")); +} -var set_cursor = func(node) { + +var set_cursor = func(node = nil) { node != nil or return; var loc = node.getNode("marker"); if (loc == nil) { @@ -327,7 +358,6 @@ var set_cursor = func(node) { } - # Set and return running state. Disable/enable stop menu. # var is_running = func(which = nil) { @@ -340,18 +370,19 @@ var is_running = func(which = nil) { } - # Output the message and optional sound recording. # var lastmsgcount = 0; var say_message = func(node, default = nil) { var msg = default; var audio = nil; - var error = 0; + var is_error = 0; if (node != nil) { - error = node.getName() == "error"; + is_error = node.getName() == "error"; + # choose random message/audio, but make sure that in the first + # run of a step, the first ones are used var m = node.getChildren("message"); if (size(m)) { msg = m[rand() * size(m)].getValue(); @@ -359,13 +390,11 @@ var say_message = func(node, default = nil) { var a = node.getChildren("audio"); if (size(a)) { - audio = a[rand() * size(m)].getValue(); + audio = a[rand() * size(a)].getValue(); } } - var lastmsg = getprop("/sim/tutorial/last-message"); - - if (msg != lastmsg or (error == 1 and lastmsgcount == 1)) { + if (msg != last_message.getValue() or (is_error and lastmsgcount == 1)) { # Error messages are only displayed every 10 seconds (2 iterations) # Other messages are only displayed if they change if (audio != nil) { @@ -376,7 +405,7 @@ var say_message = func(node, default = nil) { setprop("/sim/messages/copilot", msg); } - setprop("/sim/tutorial/last-message", msg); + last_message.setValue(msg); lastmsgcount = 0; } else { lastmsgcount += 1; @@ -384,6 +413,17 @@ var say_message = func(node, default = nil) { } +var shuffle = func(vec) { + var s = size(vec); + forindex (var i; vec) { + var j = rand() * s; + var swap = vec[j]; + vec[j] = vec[i]; + vec[i] = swap; + } + return vec; +} + var run_nasal = func(node) { node != nil or return; @@ -396,18 +436,23 @@ var run_nasal = func(node) { } +var say = func(what, who = "copilot", delay = 0) { + settimer(func { setprop("/sim/messages/", who, what) }, delay); +} -# Set up the namespace for embedded Nasal. + +# Set up namespace "__tutorial" 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; - } + say : say, # just exporting tutorial.say as __turorial.say + next : func(n = 1) { current_step += n; num_step_runs = 0 }, + previous : func(n = 1) { + current_step -= n; num_step_runs = 0; + if (current_step < 0) { + current_step = 0; + } }, }; } @@ -421,10 +466,12 @@ var dialog = func { var marker = nil; var heading = nil; var slip = nil; +var last_message = nil; + _setlistener("/sim/signals/nasal-dir-initialized", func { marker = props.globals.getNode("/sim/model/marker", 1); heading = props.globals.getNode("/orientation/heading-deg", 1); slip = props.globals.getNode("/orientation/side-slip-deg", 1); + last_message = props.globals.getNode("/sim/tutorial/last-message", 1); }); -