1 if (globals["tanker"] != nil) {
2 # reload with io.load_nasal(getprop("/sim/fg-root") ~ "/Nasal/tanker.nas");
3 print("reloading " ~ caller(0)[2]);
4 var _setlistener = reinit;
7 #--------------------------------------------------------------------------------------------------
9 var oclock = func(bearing) int(0.5 + geo.normdeg(bearing) / 30) or 12;
12 var tanker_msg = func setprop("sim/messages/ai-plane", call(sprintf, arg));
13 var pilot_msg = func setprop("/sim/messages/pilot", call(sprintf, arg));
14 var atc_msg = func setprop("sim/messages/atc", call(sprintf, arg));
17 var skip_cloud_layer = func(alt) {
19 foreach (var layer; props.globals.getNode("/environment/clouds").getChildren("layer")) {
20 var elev = (layer.getNode("elevation-ft", 1).getValue() or -9999) * FT2M;
21 var thck = (layer.getNode("thickness-ft", 1).getValue() or 0) * FT2M;
23 append(c, { bottom: elev - thck * 0.5 - 100, top: elev + thck * 0.5 + 100 });
26 foreach (var layer; c) {
27 if (alt > layer.bottom and alt < layer.top) {
39 # return free AI id number and least used, free callsign/channel pair
40 var data = {}; # copy of me.pool
41 var revdata = {}; # channel->callsign
42 foreach (var k; keys(me.pool)) {
44 revdata[me.pool[k][0]] = k;
47 foreach (var t; props.globals.getNode("ai/models", 1).getChildren()) {
48 if ((var c = t.getNode("callsign")) != nil)
49 delete(data, c.getValue() or "");
50 if ((var c = t.getNode("navaids/tacan/channel-ID")) != nil)
51 delete(data, revdata[c.getValue() or ""]);
52 if ((var c = t.getNode("id")) != nil)
53 id_used[c.getValue()] = 1;
55 for (var aiid = -2; aiid; aiid -= 1)
59 return [aiid, "MOBIL3", "062X"];
60 var d = sort(keys(data), func(a, b) data[a][1] - data[b][1])[0];
62 return [aiid, d, data[d][0]];
65 ESSO1: ["040X", rand()], ESSO2: ["041X", rand()], ESSO3: ["042X", rand()],
66 TEXACO1: ["050X", rand()], TEXACO2: ["051X", rand()], TEXACO3: ["052X", rand()],
67 MOBIL1: ["060X", rand()], MOBIL2: ["061X", rand()], MOBIL3: ["062X", rand()],
73 new: func(aiid, callsign, tacan, type, model, kias, maxfuel, pattern, contacts, heading, coord) {
74 var m = { parents: [Tanker] };
75 m.callsign = callsign;
78 m.heading = m.course = m.track_course = heading;
79 m.out_of_range_time = 0;
82 m.contacts = contacts;
84 m.coord = geo.Coord.new(coord);
85 m.anchor = geo.Coord.new(coord).apply_course_distance(m.track_course, m.length); # ARCP
86 m.goal = [nil, m.anchor];
89 m.rollrate = 2; # deg/s
92 var n = props.globals.getNode("models", 1);
93 for (var i = 0; 1; i += 1)
94 if (n.getChild("model", i, 0) == nil)
96 m.model = n.getChild("model", i, 1);
98 var n = props.globals.getNode("ai/models", 1);
99 for (var i = 0; 1; i += 1)
100 if (n.getChild("tanker", i, 0) == nil)
102 m.ai = n.getChild("tanker", i, 1);
104 m.ai.getNode("id", 1).setIntValue(aiid);
105 m.ai.getNode("callsign", 1).setValue(m.callsign ~ "");
106 m.ai.getNode("tanker", 1).setBoolValue(1);
107 m.ai.getNode("valid", 1).setBoolValue(1);
108 m.ai.getNode("navaids/tacan/channel-ID", 1).setValue(m.tacan);
109 m.ai.getNode("refuel/type", 1).setValue(type);
110 m.ai.getNode("refuel/max-fuel-transfer-lbs-min", 1).setValue(maxfuel);
111 m.ai.getNode("refuel/contact", 1).setBoolValue(0);
112 m.ai.getNode("radar/in-range", 1).setBoolValue(1);
114 m.latN = m.ai.getNode("position/latitude-deg", 1);
115 m.lonN = m.ai.getNode("position/longitude-deg", 1);
116 m.altN = m.ai.getNode("position/altitude-ft", 1);
117 m.hdgN = m.ai.getNode("orientation/true-heading-deg", 1);
118 m.pitchN = m.ai.getNode("orientation/pitch-deg", 1);
119 m.rollN = m.ai.getNode("orientation/roll-deg", 1);
120 m.ktasN = m.ai.getNode("velocities/true-airspeed-kt", 1);
121 m.vertN = m.ai.getNode("velocities/vertical-speed-fps", 1);
122 m.rangeN = m.ai.getNode("radar/range-nm", 1);
123 m.brgN = m.ai.getNode("radar/bearing-deg", 1);
124 m.elevN = m.ai.getNode("radar/elevation-deg", 1);
125 m.contactN = m.ai.getNode("refuel/contact", 1);
126 m.hOffsetN = m.ai.getNode("radar/h-offset", 1);
127 m.vOffsetN = m.ai.getNode("radar/v-offset", 1);
131 m.model.getNode("path", 1).setValue(model);
132 m.model.getNode("latitude-deg-prop", 1).setValue(m.latN.getPath());
133 m.model.getNode("longitude-deg-prop", 1).setValue(m.lonN.getPath());
134 m.model.getNode("elevation-ft-prop", 1).setValue(m.altN.getPath());
135 m.model.getNode("heading-deg-prop", 1).setValue(m.hdgN.getPath());
136 m.model.getNode("pitch-deg-prop", 1).setValue(m.pitchN.getPath());
137 m.model.getNode("roll-deg-prop", 1).setValue(m.rollN.getPath());
138 m.model.getNode("load", 1).remove();
140 return Tanker.active[m.callsign] = m;
143 tanker_msg(me.callsign ~ " returns to base");
146 delete(Tanker.active, me.callsign);
149 var dt = getprop("sim/time/delta-sec");
150 var alt = me.coord.alt();
152 if ((me.interval += dt) >= 5) {
154 me.headwind = aircraft.wind_speed_from(me.course);
155 me.ktas = aircraft.kias_to_ktas(me.kias, alt);
158 var distance = dt * (me.ktas - me.headwind) * NM2M / 3600;
159 var deviation = me.roll ? dt * 1085.941 * math.tan(me.roll * D2R) / me.ktas : 0;
161 if (me.mode == "leg") {
162 if (me.lastmode != "leg") {
164 # swap ARCP anchor and tanker exit point as leg end points
166 me.goal[0] = me.goal[1];
169 me.course = me.coord.course_to(me.goal[0]);
170 me.leg_remaining = me.coord.distance_to(me.goal[0]);
174 if ((me.leg_remaining -= distance) < 0)
177 } else { # me.mode == "turn"
178 if (me.lastmode != "turn") {
179 me.lastmode = "turn";
180 me.full_bank_turn_angle = 0;
181 me.turn_remaining = 180;
184 if (!me.full_bank_turn_angle and me.roll >= me.roll_target)
185 me.full_bank_turn_angle = geo.normdeg(180 - me.turn_remaining);
187 if (me.turn_remaining < me.full_bank_turn_angle)
190 if ((me.turn_remaining -= deviation) < 0) {
191 if (me.goal[1] == nil) # define tanker exit point (opposite of anchor point/ARCP)
192 me.goal[1] = geo.Coord.new(me.coord).apply_course_distance(me.track_course - 180,
198 me.coord.apply_course_distance(me.course -= deviation, distance);
200 me.ac = geo.aircraft_position();
201 me.distance = me.ac.distance_to(me.coord);
202 me.bearing = me.ac.course_to(me.coord);
204 var dalt = alt - me.ac.alt();
205 var ac_hdg = getprop("/orientation/heading-deg");
206 var ac_pitch = getprop("/orientation/pitch-deg");
207 var ac_contact_dist = getprop("/systems/refuel/contact-radius-m");
208 var elev = math.atan2(dalt, me.distance) * R2D;
210 me.latN.setDoubleValue(me.coord.lat());
211 me.lonN.setDoubleValue(me.coord.lon());
212 me.altN.setDoubleValue(alt * M2FT);
213 me.hdgN.setDoubleValue(me.heading = me.course);
214 me.pitchN.setDoubleValue(0);
215 me.rollN.setDoubleValue(-me.roll);
216 me.ktasN.setDoubleValue(me.ktas);
217 me.vertN.setDoubleValue(0);
218 me.rangeN.setDoubleValue(me.distance * M2NM);
219 me.brgN.setDoubleValue(me.bearing);
220 me.elevN.setDoubleValue(elev);
222 # Determine if any of the contact points are in contact
223 var offset_x = getprop("/systems/refuel/offset-x-m") or 0;
224 var offset_y = getprop("/systems/refuel/offset-y-m") or 0;
225 var offset_z = getprop("/systems/refuel/offset-z-m") or 0;
226 var roll = getprop("/orientation/roll-deg") * globals.D2R;
228 # Determine contact position
229 var probe_pos = geo.Coord.new(me.ac);
231 probe_pos.apply_course_distance(ac_hdg, offset_x);
232 probe_pos.apply_course_distance(ac_hdg + 90, offset_y * math.cos(roll) + offset_z * math.sin(roll));
233 probe_pos.set_alt(me.ac.alt() + offset_z * math.cos(roll) - offset_y * math.sin(roll));
235 me.contactN.setBoolValue(0);
237 foreach (var c; me.contacts) {
238 var drogue_pos = geo.Coord.new(me.coord);
240 # Offset longitudonally
241 drogue_pos.apply_course_distance(me.course, c.x);
243 var r = me.roll * globals.D2R;
245 # Offset laterally, taking into account any roll
246 drogue_pos.apply_course_distance(me.course +90, c.y * math.cos(r) + c.z * math.sin(r));
248 # Offset vertically, again, taking into account any roll
249 drogue_pos.set_alt(drogue_pos.alt() + c.z * math.cos(r) - c.y * math.sin(r));
251 #print("Distance: " ~ probe_pos.distance_to(drogue_pos) ~ " vs. " ~ me.distance);
253 if (probe_pos.distance_to(drogue_pos) < ac_contact_dist and
254 abs(view.normdeg(me.course - ac_hdg)) < 20) {
256 me.contactN.setBoolValue(1);
260 me.hOffsetN.setDoubleValue(view.normdeg(me.bearing - ac_hdg));
261 me.vOffsetN.setDoubleValue(view.normdeg(elev - ac_pitch));
263 var droll = me.roll_target - me.roll;
265 me.roll += me.rollrate * dt;
266 if (me.roll > me.roll_target)
267 me.roll = me.roll_target;
268 } elsif (droll < 0) {
269 me.roll -= me.rollrate * dt;
270 if (me.roll < me.roll_target)
271 me.roll = me.roll_target;
274 if (!me.leg_warning and me.leg_remaining < NM2M) {
275 tanker_msg(me.callsign ~ ", turn in one mile");
279 me.now = getprop("/sim/time/elapsed-sec");
280 if (me.distance < 90000)
281 me.out_of_range_time = me.now;
282 elsif (me.now - me.out_of_range_time > 600)
284 settimer(func me.update(), 0);
287 me.out_of_range_time = me.now;
288 var alt = int((me.coord.alt() * M2FT + 50) / 100) * 100;
289 tanker_msg("%s at %.0f, heading %.0f with %.0f knots, TACAN %s",
290 me.callsign, alt, me.course, me.kias, me.tacan);
293 me.out_of_range_time = me.now;
294 var dist = int(me.distance * M2NM);
295 var hdg = getprop("orientation/heading-deg");
296 var diff = (me.coord.alt() - me.ac.alt()) * M2FT;
297 var qual = diff > 3000 ? " well" : abs(diff) > 1000 ? " slightly" : "";
298 var rel = diff > 1000 ? " above" : diff < -1000 ? " below" : "";
299 atc_msg("Tanker %s is at %s o'clock%s",
300 me.callsign, oclock(me.ac.course_to(me.coord) - hdg),
308 # Create a tanker based on a given /sim/ai/tankers/tanker property node
309 var create_tanker = func(tanker_node, course) {
310 var (aiid, callsign, tacanid) =_= identity.get();
311 var model = tanker_node.getNode("model", 1).getValue();
312 var type = tanker_node.getNode("type", 1).getValue();
313 var spd = tanker_node.getNode("speed-kts", 1).getValue() or 250;
314 var pattern = (tanker_node.getNode("pattern-length-nm", 1).getValue() or 50) * NM2M;
315 var maxfuel = tanker_node.getNode("max-fuel-transfer-lbs-min", 1).getValue() or 6000;
319 foreach (var contact; tanker_node.getChildren("contact")) {
320 var x = (contact.getNode("x-m") != nil) ? contact.getNode("x-m").getValue() : 0;
321 var y = (contact.getNode("y-m") != nil) ? contact.getNode("y-m").getValue() : 0;
322 var z = (contact.getNode("z-m") != nil) ? contact.getNode("z-m").getValue() : 0;
323 append(contacts, { "x" : x, "y" : y, "z" : z });
327 if (size(contacts) == 0) {
328 append(contacts, {x: 0, y:0, z:0});
331 var alt = int(10 + rand() * 15) * 1000; # FL100--FL250
332 alt = skip_cloud_layer(alt * FT2M);
333 var dist = 6000 + rand() * 4000;
334 var coord = geo.aircraft_position().apply_course_distance(course, dist).set_alt(alt);
336 Tanker.new(aiid, callsign, tacanid, type, model, spd, maxfuel, pattern, contacts, course, coord);
339 # Request a new tanker
340 var request_new = func(tanker_node=nil) {
341 var tanker = values(Tanker.active);
342 if (size(tanker)) tanker[0].del();
343 request(tanker_node);
346 var request = func(tanker_node=nil) {
347 var tanker = values(Tanker.active);
349 return tanker[0].identify();
351 if (tanker_node == nil) {
352 var type = props.globals.getNode("systems/refuel", 1).getChildren("type");
355 type = type[rand() * size(type)].getValue();
357 var tankers = props.globals.getNode("/sim/ai/tankers", 1).getChildren("tanker");
358 foreach (var tanker; tankers) {
359 if (tanker.getNode("type", 1).getValue() == type) {
360 tanker_node = tanker;
366 var hdg = getprop("orientation/heading-deg");
367 var course = hdg + (rand() - 0.5) * 60;
369 create_tanker(tanker_node, course);
372 var request_random = func(tanker_node=nil) {
373 var tanker = values(Tanker.active);
375 return tanker[0].identify();
377 if (tanker_node == nil) {
378 var type = props.globals.getNode("systems/refuel", 1).getChildren("type");
381 type = type[rand() * size(type)].getValue();
383 var tankers = props.globals.getNode("/sim/ai/tankers", 1).getChildren("tanker");
384 foreach (var tanker; tankers) {
385 if (tanker.getNode("type", 1).getValue() == type) {
386 tanker_node = tanker;
392 var course = rand() * 360;
393 create_tanker(tanker_node, course);
398 var tanker = values(Tanker.active);
405 foreach (var t; values(Tanker.active))
410 _setlistener("/sim/signals/nasal-dir-initialized", func {
411 var aar_capable = size(props.globals.getNode("systems/refuel", 1).getChildren("type"));
412 gui.menuEnable("tanker", aar_capable);
414 request = func { atc_msg("no tanker in range") }; # braces mandatory
416 setlistener("/sim/signals/reinit", reinit, 1);