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