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