1
0
Fork 0

major overhaul/part I:

- add <marker> support (blinking magenta circle to point to switches etc.)
- use "real" conditions instead of tutorial-only ones
- list of <error> with <message> and <condition> children, instead
  of one <error> group with <check> items (necessary because of <condition>)
- only one <exit> (use <condition><and>... to define a set
- rename properties <prop><val><msg> to <property><value><message>
  (consistency with other systems)
- <end><message>/<voice> instead of <endtext> and <endtext-voice> etc.
- add <nasal> support everywhere, with separate namespace and predefined
  tutorial functions in it (currently only say(), next(), previous())
- make <timeofday> property actually work

TODO:
- more cleanup/re-organization
- add optional <view> group, to direct user view to switches (for startup
  tutorials etc.)

2-space indentation changed to tabs (with Stuarts permission), and braces
to K&R style (rationale on request :-)
This commit is contained in:
mfranz 2007-03-19 23:46:50 +00:00
parent d4863a1001
commit 7cb5ad477c

View file

@ -21,13 +21,15 @@
# <longitude-deg> # <longitude-deg>
# <heading-deg> # <heading-deg>
# <airspeed-kt> # <airspeed-kt>
#
# <init> - Optional: Initialization section consist of one or more # <init> - Optional: Initialization section consist of one or more
# set nodes: # set nodes:
# <set> # <set>
# <prop> - Property to set # <property> - Property to set
# <val> - value # <value> - value
# <step> - Tutorial step - a segment of the tutorial, consisting of #
# the following: # <step> - Tutorial step - a segment of the tutorial, consisting of
# the following:
# <instruction> - Text instruction displayed when the tutorial reaches # <instruction> - Text instruction displayed when the tutorial reaches
# this step, and when neither the exit nor any error # this step, and when neither the exit nor any error
# criteria have been fulfilled # criteria have been fulfilled
@ -36,165 +38,104 @@
# <error> - Error conditions, causing error messages to be displayed. # <error> - Error conditions, causing error messages to be displayed.
# The tutorial doesn't advance while any error conditions are # The tutorial doesn't advance while any error conditions are
# fulfilled. Consists of one or more check nodes: # fulfilled. Consists of one or more check nodes:
# <check> # <message> - Error message to display if error criteria fulfilled.
# <prop> - property to check. # <msg-voice> - Optional: wav filename to play when error condition fulfilled.
# <lt> - less than value. One of <lt>, <gt>, <eq> must be defined # <condition> - error condition (see $FG_ROOT/Docs/README.condition)
# <gt> - greater than value. One of <lt>, <gt>, <eq> must be defined
# <eq> - equal value. One of <lt>, <gt>, <eq> must be defined
# <msg> - Error message to display if error criteria fulfilled.
# <msg-voice> - Optional: wav filename to play when error condition fulfilled.
# <exit> - 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.
# <prop> - property to check.
# <lt> - less than value. One of <lt>, <gt>, <eq> must be defined
# <gt> - greater than value. One of <lt>, <gt>, <eq> must be defined
# <eq> - equal value. One of <lt>, <gt>, <eq> must be defined
# #
# <endtext> - Optional: Text to display when the tutorial exits the last step. # <exit> - Exit criteria causing tutorial to progress to next step.
# <endtext-voice> - Optional: wav filename to play when the tutorial exits the last step # <condition> - exit condition (see $FG_ROOT/Docs/README.condition)
#
# <end>
# <message>> - Optional: Text to display when the tutorial exits the last step.
# <msg-voice> - Optional: wav filename to play when the tutorial exits the last step
# #
# GLOBAL VARIABLES # GLOBAL VARIABLES
# #
# Time between tutorial steps.
STEP_TIME = 5;
# We also have a gap between the fulfillment of a step and the start var STEP_INTERVAL = 5; # time between tutorial steps
# of the next step. This can be much shorter. var EXIT_INTERVAL = 1; # time between fulfillment of a step and the start of the next step
STEP_EXIT = 1;
m_currentStep = 0; var current_step = nil;
m_errors = 0; var num_errors = nil;
m_tutorial = 0; var tutorial = nil;
m_firstEntry = 1; var num_step_runs = nil;
m_audioDir = ""; var audio_dir = nil;
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"); var startTutorial = func {
lfound = 0; var name = getprop("/sim/tutorial/current-tutorial");
if (name == nil) {
screen.log.write("No tutorial selected");
return;
}
foreach(c; props.globals.getNode("/sim/tutorial").getChildren("tutorial")) tutorial = nil;
{ foreach (var c; props.globals.getNode("/sim/tutorial").getChildren("tutorial")) {
if (c.getChild("name").getValue() == ltutorial) if (c.getChild("name").getValue() == name) {
{ tutorial = c;
m_tutorial = c; break;
lfound = 1; }
} }
}
if (lfound == 0) if (tutorial == nil) {
{ screen.log.write('Unable to find tutorial "' ~ name ~ '"');
# Unable to find tutorial return;
screen.log.write("Unable to find tutorial : " ~ ltutorial); }
return;
}
# Indicate that the tutorial is running screen.log.write('Loading tutorial "' ~ name ~ '" ...');
screen.log.write("Loading tutorial: " ~ ltutorial ~ " ..."); is_running(1);
isRunning(1); current_step = 0;
num_step_runs = 0;
num_errors = 0;
# If defined, get the audio directory set_properties(tutorial.getChild("init"));
if (m_tutorial.getChild("audio-dir") != nil) set_cursor(tutorial);
{ run_nasal(tutorial);
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 var dir = tutorial.getChild("audio-dir");
timeofday = m_tutorial.getChild("timeofday"); if (dir != nil) {
if (timeofday != nil) audio_dir = getprop("/sim/fg-root") ~ "/" ~ dir.getValue() ~ "/";
{ } else {
fgcommand("timeofday", props.Node.new("timeofday", timeofday.getValue())); audio_dir = "";
} }
# First, set any presets that might be present var presets = tutorial.getChild("presets");
presets = m_tutorial.getChild("presets"); if (presets != nil) {
props.copy(presets, props.globals.getNode("/sim/presets"));
fgcommand("presets-commit", props.Node.new());
if ((presets != nil) and (presets.getChildren() != nil)) # Set the various engines to be running
{ if (getprop("/sim/presets/on-ground")) {
children = presets.getChildren(); var eng = props.globals.getNode("/controls/engines");
foreach(c; children) if (eng != nil) {
{ foreach (var c; eng.getChildren("engine")) {
setprop("/sim/presets/" ~ c.getName(), c.getValue()); c.getNode("magnetos", 1).setIntValue(3);
} c.getNode("throttle", 1).setDoubleValue(0.5);
}
}
}
}
# Apply the presets var timeofday = tutorial.getChild("timeofday");
fgcommand("presets-commit", props.Node.new()); if (timeofday != nil) {
fgcommand("timeofday", props.Node.new({"timeofday": timeofday.getValue()}));
}
# Set the various engines to be running # Pick up any weather conditions/scenarios set
if (getprop("/sim/presets/on-ground")) setprop("/environment/rebuild-layers", getprop("/environment/rebuild-layers") + 1);
{ settimer(stepTutorial, STEP_INTERVAL);
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 var stopTutorial = func {
# is_running(0);
# Stops the current tutorial . set_cursor(props.Node.new());
#
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 # stepTutorial
@ -211,256 +152,214 @@ resetStop = func
# - Otherwise display the instructions for the step. # - Otherwise display the instructions for the step.
# - Sets the timer for 5 seconds again. # - Sets the timer for 5 seconds again.
# #
stepTutorial = func { var stepTutorial = func {
if (!is_running()) {
return;
}
var lerror = 0; var voice = nil;
var ltts = nil; var message = nil;
var lsnd = nil;
var lmessage = nil;
if (!isRunning()) # If we've reached the end of the tutorial, simply indicate and exit
{ if (current_step >= size(tutorial.getChildren("step"))) {
# If we've been told to stop, just do so. message = "Tutorial finished.";
return;
}
# If we've reached the end of the tutorial, simply indicate and exit var end = tutorial.getNode("end");
if (m_currentStep == size(m_tutorial.getChildren("step"))) if (end != nil) {
{ var m = end.getNode("message");
# End of tutorial. if (m != nil) {
message = m.getValue();
}
lfinished = "Tutorial finished."; var v = end.getNode("voice");
if (v != nil) {
voice = v.getValue();
}
run_nasal(end);
}
say(message, voice);
say("Deviations : " ~ num_errors);
is_running(0);
return;
}
if (m_tutorial.getChild("endtext") != nil) var step = tutorial.getChildren("step")[current_step];
{ set_cursor(step);
lfinished = m_tutorial.getChild("endtext").getValue();
}
if (m_tutorial.getChild("endtext-voice") != nil) var instr = step.getChild("instruction");
{ message = instr != nil ? instr.getValue() : "Tutorial step " ~ current_step;
lsnd = m_tutorial.getChild("endtext-voice").getValue();
}
say(lfinished, lsnd); if (!num_step_runs) {
say("Deviations : " ~ m_errors); # If this is the first time we've encountered this step :
isRunning(0); # - Set any values required
return; # - 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.
lstep = m_tutorial.getChildren("step")[m_currentStep]; if (step.getChild("instruction-voice") != nil) {
voice = step.getChild("instruction-voice").getValue();
}
lmessage = "Tutorial step " ~ m_currentStep; 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("msg-voice");
voice = v != nil ? v.getValue() : nil;
}
}
}
if (lstep.getChild("instruction") != nil) # Check for exit condition, but only if we didn't hit any errors
{ if (!error) {
# By default, display the current instruction var e = step.getNode("exit");
lmessage = lstep.getChild("instruction").getValue(); 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);
}
}
if (m_firstEntry == 1) # Display the resulting message and wait to go around again.
{ say(message, voice, error);
# If this is the first time we've encountered this step : settimer(stepTutorial, STEP_INTERVAL);
# - 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
# scan all <set> blocks and set their <property> to <value>
# <set> # <set>
# <prop>/foo/bar</prop> # <property>/foo/bar</property>
# <val>woof</val> # <value>woof</value>
# </set> # </set>
# #
setVal = func(node) { 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 (node.getName("set")) if (p != nil and v != nil) {
{ setprop(p, v);
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
#
# <check>
# <prop>/foo/bar</prop>
# <_operator_>woof</_operator_>
#
# where _operator_ may be one of "eq", "lt", "gt"
#
checkVal = func(node) {
if (node.getName("check")) var set_cursor = func(node) {
{ node != nil or return;
lprop = node.getChild("prop").getValue(); var loc = node.getNode("marker");
if (loc == nil) {
marker.getNode("arrow-enabled", 1).setBoolValue(0);
return;
}
if (getprop(lprop) == nil) var s = loc.getNode("scale");
{ marker.setValues({
# This is probably an error "x/value": loc.getNode("x", 1).getValue(),
print("Undefined property: " ~ lprop); "y/value": loc.getNode("y", 1).getValue(),
return 0; "z/value": loc.getNode("z", 1).getValue(),
} "scale/value": s != nil ? s.getValue() : 1,
"arrow-enabled": 1,
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. # Set and return running state. Disable/enable stop menu.
# #
isRunning = func(which=nil) var is_running = func(which = nil) {
{ var prop = "/sim/tutorial/running";
var prop = "/sim/tutorial/running"; if (which != nil) {
if (which != nil) setprop(prop, which);
{ gui.menuEnable("tutorial-stop", which);
setprop(prop, which); }
gui.menuEnable("tutorial-stop", which); return getprop(prop);
}
return getprop(prop);
} }
# #
# Output the message and optional sound recording. # Output the message and optional sound recording.
# #
say = func(msg, snd=nil, lerror=0) var m_lastmsgcount = 0;
{ var say = func(msg, snd = nil, lerror = 0) {
var lastmsg = getprop("/sim/tutorial/last-message"); var lastmsg = getprop("/sim/tutorial/last-message");
if ((msg != lastmsg) or (lerror == 1 and m_lastmsgcount == 1)) if ((msg != lastmsg) or (lerror == 1 and m_lastmsgcount == 1)) {
{ # Error messages are only displayed every 10 seconds (2 iterations)
# Error messages are only displayed every 10 seconds (2 iterations) # Other messages are only displayed if they change
# Other messages are only displayed if they change if (snd == nil) {
if (snd == nil) # Simply set to the co-pilot channel. TTS is picked up automatically.
{ setprop("/sim/messages/copilot", msg);
# Simply set to the co-pilot channel. TTS is picked up automatically. } else {
setprop("/sim/messages/copilot", msg); # Play the audio, and write directly to the screen-logger to avoid
} # any tts being sent to festival.
else lprop = { path : audio_dir, file : snd };
{ fgcommand("play-audio-message", props.Node.new(lprop) );
# Play the audio, and write directly to the screen-logger to avoid screen.log.write(msg, 1, 1, 1);
# 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); setprop("/sim/tutorial/last-message", msg);
m_lastmsgcount = 0; m_lastmsgcount = 0;
} } else {
else m_lastmsgcount += 1;
{ }
m_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("__tut");
}
fgcommand("nasal", n);
}
}
globals.__tut = {
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 marker = nil;
_setlistener("/sim/signals/nasal-dir-initialized", func {
marker = props.globals.getNode("/sim/model/marker", 1);
});