1# Code to process XML-based tutorials. See $FG_ROOT/Docs/README.tutorials 2# --------------------------------------------------------------------------------------- 3 4 5var step_interval = 0; # time between tutorial steps (default is set below) 6var exit_interval = 0; # time between fulfillment of a step and the start of the next step (default is set below) 7 8var loop_id = 0; 9var tutorialN = nil; 10var steps = []; 11var current_step = nil; 12var is_first_step = nil; 13var num_errors = nil; 14var step_start_time = nil; 15var step_iter_count = 0; # number or step loop iterations 16var last_step_time = nil; # for set_targets() eta calculation 17var audio_dir = nil; 18 19# Screen display. On bottom of screen, with no auto-scroll. 20var display = screen.window.new(nil, 30, 5, 0); 21display.sticky = 0; # don't turn on; makes scrolling up messages jump left and right 22 23# property nodes (to be initialized with listener) 24var markerN = nil; 25var headingN = nil; 26var slipN = nil; 27var time_elapsedN = nil; 28var last_messageN = nil; 29var step_countN = nil; 30var step_timeN = nil; 31 32_setlistener("/nasal/tutorial/loaded", func { 33 markerN = props.globals.getNode("/sim/model/marker", 1); 34 headingN = props.globals.getNode("/orientation/heading-deg", 1); 35 slipN = props.globals.getNode("/orientation/side-slip-deg", 1); 36 time_elapsedN = props.globals.getNode("/sim/time/elapsed-sec", 1); 37 last_messageN = props.globals.getNode("/sim/tutorials/last-message", 1); 38 step_countN = props.globals.getNode("/sim/tutorials/step-count", 1); 39 step_timeN = props.globals.getNode("/sim/tutorials/step-time", 1); 40 setlistener("/sim/crashed", stopTutorial); 41}); 42 43var startTutorial = func { 44 var name = getprop("/sim/tutorials/current-tutorial"); 45 if (name == nil) { 46 screen.log.write("No tutorial selected"); 47 return; 48 } 49 50 tutorialN = nil; 51 foreach (var c; props.globals.getNode("/sim/tutorials").getChildren("tutorial")) { 52 if (c.getNode("name").getValue() == name) { 53 tutorialN = c; 54 break; 55 } 56 } 57 58 if (tutorialN == nil) { 59 screen.log.write('Unable to find tutorial "' ~ name ~ '"'); 60 return; 61 } 62 63 stopTutorial(); 64 screen.log.write('Loading tutorial "' ~ name ~ '" ...'); 65 view.point.save(); 66 init_nasal(); 67 68 current_step = 0; 69 is_first_step = 1; 70 num_errors = 0; 71 last_step_time = time_elapsedN.getValue(); 72 steps = tutorialN.getChildren("step"); 73 74 step_interval = read_int(tutorialN, "step-time", 5); # time between tutorial steps 75 exit_interval = read_int(tutorialN, "exit-time", 1); # time between fulfillment of steps 76 run_nasal(tutorialN); 77 set_models(tutorialN.getNode("models")); 78 79 var dir = tutorialN.getNode("audio-dir"); 80 if (dir != nil) 81 audio_dir = getprop("/sim/fg-root") ~ "/" ~ dir.getValue(); 82 else 83 audio_dir = ""; 84 85 var presets = tutorialN.getChild("presets"); 86 if (presets != nil) { 87 props.copy(presets, props.globals.getNode("/sim/presets")); 88 fgcommand("reposition"); 89 90 if (getprop("/sim/presets/on-ground")) { 91 var eng = props.globals.getNode("/controls/engines"); 92 if (eng != nil) { 93 foreach (var c; eng.getChildren("engine")) { 94 c.getNode("magnetos", 1).setIntValue(3); 95 c.getNode("throttle", 1).setDoubleValue(0.5); 96 } 97 } 98 } 99 } 100 101 var timeofday = tutorialN.getChild("timeofday"); 102 if (timeofday != nil) 103 fgcommand("timeofday", props.Node.new({ "timeofday" : timeofday.getValue() })); 104 105 # <init> 106 do_group(tutorialN.getNode("init")); 107 is_running(1); # needs to be after "reposition" 108 display.clear(); 109 display.show(); 110 111 # Pick up any weather conditions/scenarios set 112 setprop("/environment/rebuild-layers", getprop("/environment/rebuild-layers") + 1); 113 settimer(func { step_tutorial(loop_id += 1) }, step_interval); 114} 115 116 117 118var stopTutorial = func { 119 loop_id += 1; 120 if (is_running()) { 121 var end = tutorialN.getNode("end"); 122 set_properties(end); 123 run_nasal(end); 124 set_view(end) or view.point.restore(); 125 say("Tutorial finished."); 126 settimer(func() { if (!is_running()) { display.close(); } }, 10); 127 } 128 set_marker(); 129 is_running(0); 130} 131 132 133 134 135# - Gets the current step node from the tutorial 136# - If this is the first time the step is entered, it displays the instruction message 137# - Otherwise, it 138# - Checks if the exit conditions have been met. If so, it increments the step counter. 139# - Checks for any error conditions, in which case it displays a message to the screen and 140# increments an error counter 141# - Otherwise display the instructions for the step. 142# 143var step_tutorial = func(id) { 144 145 # Check to ensure that this is the currently running tutorial. 146 id == loop_id or return; 147 148 var continue_after = func(n, w) { 149 settimer(func { step_tutorial(id) }, w); 150 } 151 152 # <end> 153 if (current_step >= size(steps)) { 154 var end = tutorialN.getNode("end"); 155 stopTutorial(); 156 return; 157 } 158 159 var step = steps[current_step]; 160 set_marker(step); 161 set_targets(tutorialN.getNode("targets")); 162 163 # <step> 164 if (is_first_step) { 165 is_first_step = 0; 166 step_start_time = time_elapsedN.getValue(); 167 step_timeN.setDoubleValue(0); 168 step_countN.setIntValue(step_iter_count = 0); 169 170 do_group(step, "Tutorial step " ~ current_step); 171 172 # A <wait> tag affects only the initial entry to the step 173 var w = read_int(step, "wait", step_interval); 174 return continue_after(step, w); 175 } 176 177 step_countN.setIntValue(step_iter_count += 1); 178 step_timeN.setDoubleValue(time_elapsedN.getValue() - step_start_time); 179 180 # <abort> 181 var abort = step.getNode("abort"); 182 if (abort != nil) { 183 if (props.condition(abort.getNode("condition"))) { 184 do_group(abort); 185 current_step += 1; 186 is_first_step = 1; 187 return continue_after(abort, exit_interval); 188 } 189 } 190 191 # <error> 192 foreach (var error; shuffle(step.getChildren("error"))) { 193 if (props.condition(error.getNode("condition"))) { 194 num_errors += 1; 195 do_group(error); 196 return continue_after(error, step_interval); 197 } 198 } 199 200 # <exit> 201 var exit = step.getNode("exit"); 202 if (exit != nil) { 203 if (!props.condition(exit.getNode("condition"))) 204 { 205 if (time_elapsedN.getValue() - step_start_time > 15.0) 206 { 207 # What's going on? Repeat last message. 208 last_messageN.setValue(""); 209 step_start_time = time_elapsedN.getValue(); 210 do_group(step, "Tutorial step " ~ current_step); 211 } 212 return continue_after(exit, step_interval); 213 } 214 215 do_group(exit); 216 } 217 218 # success! 219 current_step += 1; 220 is_first_step = 1; 221 return continue_after(tutorialN, exit_interval); 222} 223 224 225## 226# Do the stuff that's shared by <init>, <step>, <error>, <exit>, and <abort>. 227# <end> doesn't use it. 228# 229var do_group = func(node, default_msg = nil) { 230 say_message(node, default_msg); 231 set_view(node); 232 set_properties(node); 233 run_nasal(node); 234} 235 236var read_int = func(node, child, default) { 237 var c = node.getNode(child); 238 if (c == nil) 239 return default; 240 c = int(c.getValue()); 241 return c != nil ? c : default; 242} 243 244 245## 246# scan all <set> blocks and set their <property> to <value> or 247# the value of a property that <property n="1"> points to 248# <set> 249# <property>/foo/bar</property> 250# <value>woof</value> 251# </set> 252# 253var set_properties = func(node) { 254 node != nil or return; 255 foreach (var c; node.getChildren("set")) { 256 var dest = c.getChild("property", 0); 257 var src = c.getChild("property", 1); 258 var val = c.getChild("value"); 259 260 dest != nil or die("<set> without <property>"); 261 if (val != nil) { 262 setprop(dest.getValue(), val.getValue()); 263 } elsif (src != nil) { 264 src = getprop(src.getValue()); 265 src != nil or die("<property n=\"1\"> doesn't refer to defined property"); 266 setprop(dest.getValue(), src); 267 } else { 268 die("<set> without <value> or <property n=\"1\">"); 269 } 270 } 271} 272 273 274## 275# For each <target><*><longitude-deg|latitude-deg> calculate and update 276# /sim/tutorials/targets/*/... 277# heading-deg ... absolute heading to target (0 -> North) 278# direction-deg ... relative angle to target (0 -> ahead, 90 -> to the right) 279# distance-m ... distance in meters 280# eta-min ... estimated time of arrival (assuming aircraft flies in 281# in current speed towards target) 282# 283var set_targets = func(node) { 284 node != nil or return; 285 286 var time = time_elapsedN.getValue(); 287 var dest = props.globals.getNode("/sim/tutorials/targets", 1); 288 var aircraft = geo.aircraft_position(); 289 var hdg = headingN.getValue() + slipN.getValue(); 290 291 foreach (var t; node.getChildren()) { 292 var lon = t.getNode("longitude-deg"); 293 var lat = t.getNode("latitude-deg"); 294 if (lon == nil or lat == nil) 295 die("target coords undefined"); 296 297 var target = geo.Coord.new().set_latlon(lat.getValue(), lon.getValue()); 298 var dist = aircraft.distance_to(target); 299 var course = aircraft.course_to(target); 300 var angle = geo.normdeg(course - hdg); 301 if (angle >= 180) 302 angle -= 360; 303 304 var d = dest.getChild(t.getName(), t.getIndex(), 1); 305 d.getNode("heading-deg", 1).setDoubleValue(course); 306 d.getNode("direction-deg", 1).setDoubleValue(angle); 307 var distN = d.getNode("distance-m", 1); 308 var lastdist = distN.getValue(); 309 distN.setDoubleValue(dist); 310 if (lastdist != nil) { 311 var speed = (lastdist - dist) / (time - last_step_time) + 0.00001; # m/s 312 d.getNode("eta-min", 1).setDoubleValue(dist / (speed * 60)); 313 } 314 } 315 last_step_time = time; 316} 317 318 319var models = []; 320var set_models = func(node) { 321 node != nil or return; 322 323 var manager = props.globals.getNode("/models", 1); 324 foreach (var src; node.getChildren("model")) { 325 var i = 0; 326 for (; 1; i += 1) 327 if (manager.getChild("model", i, 0) == nil) 328 break; 329 330 var dest = manager.getChild("model", i, 1); 331 props.copy(src, dest); 332 dest.getNode("load", 1); # makes the modelmgr load the model 333 dest.removeChildren("load"); 334 append(models, dest); 335 } 336} 337 338 339var remove_models = func { 340 foreach (var m; models) 341 m.getParent().removeChild(m.getName(), m.getIndex()); 342 343 models = []; 344} 345 346 347var set_view = func(node = nil) { 348 node != nil or return; 349 var v = node.getChild("view"); 350 if (v != nil) { 351 # when changing view direction, switch to view 0 (captain's view), 352 # unless another view is explicitly specified 353 v.initNode("view-number", 0, "INT", 0); 354 view.point.move(v); 355 return 1; 356 } 357 return 0; 358} 359 360 361var set_marker = func(node = nil) { 362 if (node != nil) { 363 var loc = node.getNode("marker"); 364 if (loc != nil) { 365 var s = loc.getNode("scale"); 366 markerN.setValues({ 367 "x/value": loc.getNode("x-m", 1).getValue(), 368 "y/value": loc.getNode("y-m", 1).getValue(), 369 "z/value": loc.getNode("z-m", 1).getValue(), 370 "scale/value": s != nil ? s.getValue() : 1, 371 "arrow-enabled": 1, 372 }); 373 return; 374 } 375 } 376 markerN.getNode("arrow-enabled", 1).setBoolValue(0); 377} 378 379 380# Set and return running state. Disable/enable stop menu. 381# 382var is_running = func(which = nil) { 383 var prop = "/sim/tutorials/running"; 384 if (which != nil) { 385 setprop(prop, which); 386 } 387 return getprop(prop); 388} 389 390 391# Output the message and optional sound recording. 392# 393var lastmsgcount = 0; 394var say_message = func(node, default = nil) { 395 var msg = default; 396 var audio = nil; 397 398 if (node != nil) { 399 400 var m = node.getChildren("message"); 401 if (size(m)) 402 msg = m[rand() * size(m)].getValue(); 403 404 var a = node.getChildren("audio"); 405 if (size(a)) 406 audio = a[rand() * size(a)].getValue(); 407 } 408 409 if (msg != last_messageN.getValue()) { 410 # Messages are only displayed if they change 411 if (audio != nil) { 412 var prop = { path : audio_dir, file : audio, volume : 1.0 }; 413 fgcommand("play-audio-sample", props.Node.new(prop)); 414 } 415 416 if (msg != nil) { 417 var params = []; 418 419 foreach (var p; node.getChildren("message-param")) { 420 if (p.getNode("property") != nil) { 421 append(params, getprop(p.getNode("property").getValue())); 422 } 423 } 424 425 # Ugly 426 if (size(params) == 1) { msg = sprintf(msg, params[0]); } 427 if (size(params) == 2) { msg = sprintf(msg, params[0], params[1]); } 428 if (size(params) == 3) { msg = sprintf(msg, params[0], params[1], params[2]); } 429 if (size(params) == 4) { msg = sprintf(msg, params[0], params[1], params[2], params[3]); } 430 431 display.write(msg, 1, 1, 1); 432 if (audio == nil) { 433 # Link to text-to-speech 434 setprop("/sim/sound/voices/copilot", msg); 435 } 436 437 last_messageN.setValue(msg); 438 } 439 } 440} 441 442 443var shuffle = func(vec) { 444 var s = size(vec); 445 forindex (var i; vec) { 446 var j = rand() * s; 447 if (i != j) { 448 var swap = vec[j]; 449 vec[j] = vec[i]; 450 vec[i] = swap; 451 } 452 } 453 return vec; 454} 455 456 457var run_nasal = func(node) { 458 node != nil or return; 459 foreach (var n; node.getChildren("nasal")) { 460 if (n.getNode("module") == nil) 461 n.getNode("module", 1).setValue("__tutorial"); 462 463 fgcommand("nasal", n); 464 } 465} 466 467 468var say = func(what, who = "copilot", delay = 0) { 469 settimer(func { display.write(what, 1, 1, 1) }, delay); 470} 471 472 473# Set up namespace "__tutorial" for embedded Nasal. 474# 475var init_nasal = func { 476 globals.__tutorial = { 477 say : say, # just exporting tutorial.say as __tutorial.say 478 next : func(n = 1) { current_step += n; is_first_step = 1; }, 479 previous : func(n = 1) { 480 current_step -= n; 481 is_first_step = 1; 482 if (current_step < 0) 483 current_step = 0; 484 }, 485 }; 486} 487 488 489var dialog = func { 490 fgcommand("dialog-show", props.Node.new({ "dialog-name" : "marker-adjust" })); 491} 492 493 494## 495# Tutorial loader for development purposes. 496# Usage: tutorial.load("Aircraft/bo105/Tutorials/foo.xml", 1) 497# Loads this file to tutorial slot #1 (/sim/tutorials/tutorial[1]) 498# 499var load = func(file, index = 0) { 500 props.globals.getNode("/sim/tutorials", 1).removeChild("tutorial", index); 501 io.read_properties(file, "/sim/tutorials/tutorial[" ~ index ~ "]/"); 502} 503