4 # 1) Display chat messages from other aircraft to
5 # the screen using screen.nas
7 # 2) Display a complete history of chat via dialog.
9 # 3) Allow chat messages to be written by the user.
16 var log_listeners = [];
18 var check_messages = func(loop_id) {
19 if (loop_id != msg_loop_id) return;
20 foreach (var mp; values(model.callsign)) {
21 var msg = mp.node.getNode("sim/multiplay/chat", 1).getValue();
22 if (msg and msg != lastmsg[mp.callsign]) {
23 if (!contains(ignore, mp.callsign))
24 echo_message(mp.callsign, msg);
25 lastmsg[mp.callsign] = msg;
28 settimer(func check_messages(loop_id), 1);
31 var echo_message = func(callsign, msg) {
32 msg = string.trim(string.replace(msg, "\n", " "));
34 # Only prefix with the callsign if the message doesn't already include it.
35 if (find(callsign, msg) < 0)
36 msg = callsign ~ ": " ~ msg;
38 setprop("/sim/messages/mp-plane", msg);
40 # Add the chat to the chat history.
41 if (var history = getprop("/sim/multiplay/chat-history"))
42 msg = history ~ "\n" ~ msg;
44 setprop("/sim/multiplay/chat-history", msg);
47 var timeout_handler = func()
49 var t = props.globals.getNode("/sim/time/elapsed-sec").getValue();
53 setprop("/sim/multiplay/chat", "");
56 settimer(timeout_handler, msg_timeout - t);
59 var chat_listener = func(n)
61 var msg = n.getValue();
64 # ensure we see our own messages.
65 echo_message(getprop("/sim/multiplay/callsign"), msg);
69 settimer(timeout_handler, 10); # need new timer
70 msg_timeout = 10 + props.globals.getNode("/sim/time/elapsed-sec").getValue();
76 # Message composition function, activated using the - key.
77 var prefix = "Chat Message:";
79 var kbdlistener = nil;
81 var compose_message = func(msg = "")
84 gui.popupTip(input, 1000000);
86 kbdlistener = setlistener("/devices/status/keyboard/event", func (event) {
87 var key = event.getNode("key");
89 # Only check the key when pressed.
90 if (!event.getNode("pressed").getValue())
93 if (handle_key(key.getValue()))
94 key.setValue(-1); # drop key event
98 var handle_key = func(key)
100 if (key == `\n` or key == `\r`)
102 # CR/LF -> send the message
104 # Trim off the prefix
105 input = substr(input, size(prefix));
106 # Send the message and switch off the listener.
107 setprop("/sim/multiplay/chat", input);
108 removelistener(kbdlistener);
114 # backspace -> remove a character
116 if (size(input) > size(prefix))
118 input = substr(input, 0, size(input) - 1);
119 gui.popupTip(input, 1000000);
122 # Always handle key so excessive backspacing doesn't toggle the heading autopilot
128 removelistener(kbdlistener);
132 elsif ((key > 31) and (key < 128))
134 # Normal character - add it to the input
136 gui.popupTip(input, 1000000);
141 # Unknown character - pass through
148 # multiplayer.dialog.show() -- displays pilot list dialog
150 var PILOTSDLG_RUNNING = 0;
153 init: func(x = nil, y = nil) {
156 me.bg = [0, 0, 0, 0.3]; # background color
157 me.fg = [[0.9, 0.9, 0.2, 1], [1, 1, 1, 1], [1, 0.5, 0, 1]]; # alternative active & disabled color
159 me.toggle_unit(); # set to imperial
162 var font = { name: "FIXED_8x13" };
163 me.header = ["chat", " callsign"," code"," model", " brg", func dialog.dist_hdr, func dialog.alt_hdr ~ " ", "ignore" ~ " "];
165 { type: "button", legend: "", halign: "right", callback: "multiplayer.compose_message", "pref-height": 14, "pref-width": 14},
166 { type: "text", property: "callsign", format: " %s", label: "-----------", halign: "fill" },
167 { type: "text", property: "id-code", format: " %s", label: "-----", halign: "fill" },
168 { type: "text", property: "model-short", format: "%s", label: "--------------", halign: "fill" },
169 { type: "text", property: "bearing-to", format: " %3.0f", label: "----", halign: "right", font: font },
170 { type: "text", property: func dialog.dist_node, format:" %8.2f", label: "---------", halign: "right", font: font },
171 { type: "text", property: func dialog.alt_node, format:" %7.0f", label: "---------", halign: "right", font: font },
172 { type: "checkbox", property: "controls/invisible", callback: "multiplayer.dialog.toggle_ignore",
173 argprop: "callsign", label: "---------", halign: "right", font: font },
176 me.name = "who-is-online";
181 append(me.listeners, setlistener("/sim/startup/xsize", func me._redraw_()));
182 append(me.listeners, setlistener("/sim/startup/ysize", func me._redraw_()));
183 append(me.listeners, setlistener("/sim/signals/reinit-gui", func me._redraw_()));
184 append(me.listeners, setlistener("/sim/signals/multiplayer-updated", func me._redraw_()));
187 if (me.dialog != nil)
190 me.dialog = gui.dialog[me.name] = gui.Widget.new();
191 me.dialog.set("name", me.name);
192 me.dialog.set("dialog-name", me.name);
194 me.dialog.set("x", me.x);
196 me.dialog.set("y", me.y);
198 me.dialog.set("layout", "vbox");
199 me.dialog.set("default-padding", 0);
201 me.dialog.setColor(me.bg[0], me.bg[1], me.bg[2], me.bg[3]);
203 var titlebar = me.dialog.addChild("group");
204 titlebar.set("layout", "hbox");
206 var w = titlebar.addChild("button");
207 w.node.setValues({ "pref-width": 16, "pref-height": 16, legend: me.unit_button, default: 0 });
208 w.setBinding("nasal", "multiplayer.dialog.toggle_unit(); multiplayer.dialog._redraw_()");
210 titlebar.addChild("empty").set("stretch", 1);
211 titlebar.addChild("text").set("label", "Pilots: ");
213 var p = titlebar.addChild("text");
214 p.node.setValues({ label: "---", live: 1, format: "%d", property: "ai/models/num-players" });
215 titlebar.addChild("empty").set("stretch", 1);
217 var w = titlebar.addChild("button");
218 w.node.setValues({ "pref-width": 16, "pref-height": 16, legend: "", default: 0 });
219 # "Esc" causes dialog-close
221 w.setBinding("nasal", "multiplayer.dialog.del()");
223 me.dialog.addChild("hrule");
225 var content = me.dialog.addChild("group");
226 content.set("layout", "table");
227 content.set("default-padding", 0);
231 foreach (var h; me.header) {
232 var w = content.addChild("text");
233 var l = typeof(h) == "func" ? h() : h;
234 w.node.setValues({ "label": l, "row": row, "col": col, halign: me.columns[col].halign });
235 w = content.addChild("hrule");
236 w.node.setValues({ "row": row + 1, "col": col });
241 foreach (var mp; model.list) {
243 var color = mp.node.getNode("model-installed").getValue() ? me.fg[odd = !odd] : me.fg[2];
244 foreach (var column; me.columns) {
246 if (column.type == "button") {
247 w = content.addChild("button");
248 w.node.setValues(column);
249 w.setBinding("nasal", column.callback ~ "(\"" ~ mp.callsign ~ ", \");");
250 w.node.setValues({ row: row, col: col});
252 var p = typeof(column.property) == "func" ? column.property() : column.property;
253 if (column.type == "text") {
254 w = content.addChild("text");
255 w.node.setValues(column);
256 } elsif (column.type == "checkbox") {
257 w = content.addChild("checkbox");
258 w.setBinding("nasal", column.callback ~ "(getprop(\"" ~ mp.root ~ "/" ~ column.argprop ~ "\"))");
260 w.node.setValues({ row: row, col: col, live: 1, property: mp.root ~ "/" ~ p });
262 w.setColor(color[0], color[1], color[2], color[3]);
268 me.dialog.set("x", me.x);
270 me.dialog.set("y", me.y);
271 me.update(me.loopid += 1);
272 fgcommand("dialog-new", me.dialog.prop());
273 fgcommand("dialog-show", me.dialog.prop());
276 id == me.loopid or return;
277 var self = geo.aircraft_position();
278 foreach (var mp; model.list) {
280 var x = n.getNode("position/global-x").getValue();
281 var y = n.getNode("position/global-y").getValue();
282 var z = n.getNode("position/global-z").getValue();
283 var ac = geo.Coord.new().set_xyz(x, y, z);
286 idcode = me.IDCode(n.getNode("instrumentation/transponder/transmitted-id").getValue());
288 call(func distance = self.distance_to(ac), nil, var err = []);
290 if ((size(err))or(distance==nil)) {
291 # Oops, have errors. Bogus position data (and distance==nil).
292 if (me.cs_warnings[mp.callsign]!=1) {
293 # report each callsign once only (avoid cluttering)
294 me.cs_warnings[mp.callsign] = 1;
295 print("Received invalid position data: " ~ debug._error(mp.callsign));
297 # debug.printerror(err);
298 # debug.dump(self, ac, mp);
299 # debug.tree(mp.node);
303 # Node with valid position data (and "distance!=nil").
305 "model-short": n.getNode("model-installed").getValue() ? mp.model : "[" ~ mp.model ~ "]",
306 "bearing-to": self.course_to(ac),
307 "distance-to-km": distance / 1000.0,
308 "distance-to-nm": distance * M2NM,
309 "position/altitude-m": n.getNode("position/altitude-ft").getValue() * FT2M,
310 "controls/invisible": contains(ignore, mp.callsign),
315 if (PILOTSDLG_RUNNING)
316 settimer(func me.update(id), 1, 1);
319 if (me.dialog != nil) {
327 me.alt_node = "position/altitude-m";
328 me.alt_hdr = "alt-m";
329 me.dist_hdr = "dist-km";
330 me.dist_node = "distance-to-km";
331 me.unit_button = "IM";
333 me.alt_node = "position/altitude-ft";
334 me.dist_node = "distance-to-nm";
335 me.alt_hdr = "alt-ft";
336 me.dist_hdr = "dist-nm";
337 me.unit_button = "SI";
340 toggle_ignore: func (callsign) {
341 if (contains(ignore, callsign)) {
342 delete(ignore, callsign);
344 ignore[callsign] = 1;
348 if (me.dialog != nil) {
349 me.x = me.dialog.prop().getNode("x").getValue();
350 me.y = me.dialog.prop().getNode("y").getValue();
352 fgcommand("dialog-close", me.dialog.prop());
355 PILOTSDLG_RUNNING = 0;
357 foreach (var l; me.listeners)
359 delete(gui.dialog, me.name);
362 if (!PILOTSDLG_RUNNING) {
363 PILOTSDLG_RUNNING = 1;
366 me.update(me.loopid += 1);
370 if (!PILOTSDLG_RUNNING)
387 idcode = sprintf("%04d", code);
397 # Autonomous singleton class that monitors multiplayer aircraft,
398 # maintains data in various structures, and raises signal
399 # "/sim/signals/multiplayer-updated" whenever an aircraft
400 # joined or left. Available data containers are:
402 # multiplayer.model.data: hash, key := /ai/models/~ path
403 # multiplayer.model.callsign hash, key := callsign
404 # multiplayer.model.list vector, sorted alphabetically (ASCII, case insensitive)
406 # All of them contain hash entries of this form:
409 # callsign: "BiMaus",
410 # path: "Aircraft/bo105/Models/bo105.xml", # relative file path
411 # root: "/ai/models/multiplayer[4]", # root property
412 # node: {...}, # root property as props.Node hash
413 # model: "bo105", # model name (extracted from path)
414 # sort: "bimaus", # callsign in lower case (for sorting)
420 append(me.L, setlistener("ai/models/model-added", func(n) {
421 # Defer update() to the next convenient time to allow the
422 # new MP entry to become fully initialized.
423 settimer(func me.update(n.getValue()), 0);
425 append(me.L, setlistener("ai/models/model-removed", func(n) {
426 # Defer update() to the next convenient time to allow the
427 # old MP entry to become fully deactivated.
428 settimer(func me.update(n.getValue()), 0);
432 update: func(n = nil) {
433 var changedNode = props.globals.getNode( n, 1 );
434 if (n != nil and changedNode.getName() != "multiplayer")
440 foreach (var n; props.globals.getNode("ai/models", 1).getChildren("multiplayer")) {
441 if ((var valid = n.getNode("valid")) == nil or (!valid.getValue()))
443 if ((var callsign = n.getNode("callsign")) == nil or !(callsign = callsign.getValue()))
445 if (!(callsign = string.trim(callsign)))
448 var path = n.getNode("sim/model/path").getValue();
449 var model = split(".", split("/", path)[-1])[0];
450 model = me.remove_suffix(model, "-model");
451 model = me.remove_suffix(model, "-anim");
453 var root = n.getPath();
454 var data = { node: n, callsign: callsign, model: model, root: root,
455 sort: string.lc(callsign) };
457 me.data[root] = data;
458 me.callsign[callsign] = data;
461 me.list = sort(values(me.data), func(a, b) cmp(a.sort, b.sort));
463 setprop("ai/models/num-players", size(me.list));
464 setprop("sim/signals/multiplayer-updated", 1);
466 remove_suffix: func(s, x) {
468 if (substr(s, -len) == x)
469 return substr(s, 0, size(s) - len);
474 var mp_mode_changed = func(n) {
475 var is_online = n.getBoolValue();
476 foreach (var menuitem;["mp-chat","mp-chat-menu","mp-list","mp-carrier"])
478 gui.menuEnable(menuitem, is_online);
482 if (getprop("/sim/multiplay/write-message-log") and (log_file == nil)) {
483 var t = props.globals.getNode("/sim/time/real");
486 # not ready yet, delay...
487 settimer(func mp_mode_changed(n), 0.1);
492 var ac = getprop("/sim/aircraft");
493 var cs = getprop("/sim/multiplay/callsign");
494 var apt = airportinfo().id;
495 var file = string.normpath(getprop("/sim/fg-home") ~ "/mp-message.log");
497 log_file = io.open(file, "a");
498 io.write(log_file, sprintf("\n===== %s %04d/%02d/%02d\t%s\t%s\t%s\n",
499 ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][t.weekday],
500 t.year, t.month, t.day, apt, ac, cs));
503 append(log_listeners, setlistener("/sim/signals/exit", func io.write(log_file, "===== EXIT\n") and io.close(log_file)));
504 append(log_listeners, setlistener("/sim/messages/mp-plane", func(n) {
505 io.write(log_file, sprintf("%02d:%02d %s\n",
506 getprop("/sim/time/real/hour"),
507 getprop("/sim/time/real/minute"),
513 check_messages(msg_loop_id += 1);
521 io.write(log_file, "===== DISCONNECT\n");
524 foreach (var l; log_listeners)
532 _setlistener("/sim/signals/nasal-dir-initialized", func {
536 setlistener("/sim/multiplay/online", mp_mode_changed, 1, 1);
538 # Call-back to ensure we see our own messages.
539 setlistener("/sim/multiplay/chat", chat_listener);