# Multiplayer # =========== # # 1) Display chat messages from other aircraft to # the screen using screen.nas # # 2) Display a complete history of chat via dialog. # # 3) Allow chat messages to be written by the user. var messages = {}; var check_messages = func { var mp = props.globals.getNode("/ai/models").getChildren("multiplayer"); var lseen = {}; foreach (i; mp) { var lmsg = getprop(i.getPath() ~ "/sim/multiplay/chat"); var lcallsign = getprop(i.getPath() ~ "/callsign"); var lvalid = getprop(i.getPath() ~ "/valid"); if ((lvalid) and (lmsg != nil) and (lmsg != "") and (lcallsign != nil) and (lcallsign != "") ) { if (! contains(lseen, lcallsign)) { # Indicate that we've seen this callsign. This handles the case # where we have two aircraft with the same callsign in the MP # session. lseen[lcallsign] = 1; if ((! contains(messages, lcallsign)) or (! streq(lmsg, messages[lcallsign]))) { # Save the message so we don't repeat it. messages[lcallsign] = lmsg; # Display the message. echo_message(lmsg, lcallsign); } } } } # Check for new messages every couple of seconds. settimer(check_messages, 3); } var echo_message = func(msg, callsign) { if ((callsign != nil) and (! string.match(msg, "*" ~ callsign ~ "*"))) { # Only prefix with the callsign if the message doesn't already include it. msg = callsign ~ ": " ~ msg; } var ldisplay = getprop("/sim/multiplay/chat-display"); if ((ldisplay != nil) and (ldisplay == "1")) { # Only display the message to screen if configured. setprop("/sim/messages/ai-plane", msg); } # Add the chat to the chat history. var lchat = getprop("/sim/multiplay/chat-history"); if (lchat == nil) { setprop("/sim/multiplay/chat-history", msg); } else { if (substr(lchat, size(lchat) -1, 1) != "\n") { lchat = lchat ~ "\n"; } setprop("/sim/multiplay/chat-history", lchat ~ msg); } } settimer(func { # Call-back to ensure we see our own messages. setlistener("/sim/multiplay/chat", func(n) { echo_message(n.getValue(), getprop("/sim/multiplay/callsign")); }); # check for new messages check_messages(); }, 1); # Message composition function, activated using the - key. var prefix = "Chat Message:"; var input = ""; var kbdlistener = nil; var compose_message = func(msg = "") { input = prefix ~ msg; gui.popupTip(input, 1000000); kbdlistener = setlistener("/devices/status/keyboard/event", func (event) { var key = event.getNode("key"); # Only check the key when pressed. if (!event.getNode("pressed").getValue()) return; if (handle_key(key.getValue())) key.setValue(-1); # drop key event }); } var handle_key = func(key) { if (key == `\n` or key == `\r`) { # CR/LF -> send the message # Trim off the prefix input = substr(input, size(prefix)); # Send the message and switch off the listener. setprop("/sim/multiplay/chat", input); removelistener(kbdlistener); gui.popdown(); return 1; } elsif (key == 8) { # backspace -> remove a character if (size(input) > size(prefix)) { input = substr(input, 0, size(input) - 1); gui.popupTip(input, 1000000); return 1; } } elsif (key == 27) { # escape -> cancel removelistener(kbdlistener); gui.popdown(); return 1; } elsif ((key > 31) and (key < 128)) { # Normal character - add it to the input input ~= chr(key); gui.popupTip(input, 1000000); return 1; } else { # Unknown character - pass through return 0; } } # (l) 2008 by till busch # # multiplayer.dialog.show() -- displays the dialog and starts the update loop var PILOTSDLG_RUNNING=0; var METER_TO_NM = 0.0005399568034557235; var dialog = { init : func(x = nil, y = nil) { me.x = x; me.y = y; me.bg = [0, 0, 0, 0.3]; # background color me.fg = [[0.9, 0.9, 0.2, 1], [1.0, 1.0, 1.0, 1.0]]; # alternating foreground colors me.unit=1; me.toggle_unit(); # set to imperial # # "private" me.header = [" callsign", "model", "hdg", func { dialog.dist_hdr }, func { dialog.alt_hdr ~ " " }]; me.columns = [ {property:"callsign", format:" %s", label: "-----------", halign:"fill" }, {property:"model-short", format:"%s", label: "--------------",halign:"fill" }, {property:"bearing-to", format:" %3.0f",label: "----", halign: "right",font:{name:"FIXED_8x13"}}, {property:func {dialog.dist_node}, format:" %8.2f",label: "---------",halign: "right",font:{name:"FIXED_8x13"}}, {property:func {dialog.alt_node}, format:" %7.0f",label: "---------",halign: "right",font:{name:"FIXED_8x13"}} ]; me.name = "who-is-online"; me.namenode = props.Node.new({"dialog-name" : me.name }); me.dialog = nil; me.mp = props.globals.getNode("/ai/models"); me.listeners=[]; append(me.listeners, setlistener("/sim/startup/xsize", func { me._redraw_() })); append(me.listeners, setlistener("/sim/startup/ysize", func { me._redraw_() })); append(me.listeners, setlistener("/ai/models/model-added", func(n) { me._multiplayer_update_(n) })); append(me.listeners, setlistener("/ai/models/model-removed", func(n) { me._multiplayer_update_(n) })); }, getloc : func (p) { var ploc = geo.Coord.new(); ploc.set_xyz(p.getNode("global-x").getValue(), p.getNode("global-y").getValue(), p.getNode("global-z").getValue()); return ploc; }, create : func { if (me.dialog != nil) me.close(); me.dialog = gui.Widget.new(); me.dialog.set("name", me.name); if (me.x != nil) me.dialog.set("x", me.x); if (me.y != nil) me.dialog.set("y", me.y); me.dialog.set("layout", "vbox"); me.dialog.set("default-padding", 0); me.dialog.setColor(me.bg[0], me.bg[1], me.bg[2], me.bg[3]); var titlebar=me.dialog.addChild("group"); titlebar.set("layout", "hbox"); var w = titlebar.addChild("button"); w.node.setValues({"pref-width": 16, "pref-height": 16, legend: me.unit_button, default: 0}); w.setBinding("nasal", "multiplayer.dialog.toggle_unit(); multiplayer.dialog._redraw_()"); titlebar.addChild("empty").set("stretch", 1); titlebar.addChild("text").set("label", "Pilots: "); var p=titlebar.addChild("text"); p.node.setValues({label: "---", live: 1, format: "%d", property: me.mp.getPath() ~ "/players"}); titlebar.addChild("empty").set("stretch", 1); var w = titlebar.addChild("button"); w.node.setValues({"pref-width": 16, "pref-height": 16, legend: "", default: 0}); # key: "esc" is no good w.setBinding("nasal", "multiplayer.dialog.del()"); me.dialog.addChild("hrule"); var content=me.dialog.addChild("group"); content.set("layout", "table"); content.set("default-padding", 0); var row=0; var col=0; foreach (var h; me.header) { var w = content.addChild("text"); var l = typeof(h) == "func" ? h() : h; w.node.setValues({"label":l, "row":row, "col":col, halign:me.columns[col].halign}); w=content.addChild("hrule"); w.node.setValues({"row":row+1,"col":col}); col+=1; } row+=2; var children = me.mp.getChildren("multiplayer"); var mp_vec=[]; foreach(var c; children) { if(c.getNode("callsign") != nil and c.getNode("valid").getValue()) { append(mp_vec, { callsign:string.lc(c.getNode("callsign").getValue()), index:c.getIndex()}); } } var sorted=sort(mp_vec, func(a,b) { return cmp(a.callsign,b.callsign) }); foreach(var s; sorted) { var c = children[s.index]; var col=0; foreach (var column; me.columns) { var w = content.addChild("text"); w.node.setValues(column); var p=typeof(column.property) == "func" ? column.property() : column.property; w.node.setValues({row: row, col: col, live: 1, property: c.getPath() ~ "/" ~ p}); var odd = math.mod(row, 2); w.setColor(me.fg[odd][0], me.fg[odd][1], me.fg[odd][2], me.fg[odd][3]); col +=1; } row +=1; } fgcommand("dialog-new", me.dialog.prop()); fgcommand("dialog-show", me.namenode); }, update : func { var children = me.mp.getChildren("multiplayer"); var loc = geo.aircraft_position(); var players = 0; foreach(var c; children) { if (!c.getNode("valid").getValue()) continue; players+=1; var ploc = me.getloc(c.getNode("position")); var ploc_course = loc.course_to(ploc); var ploc_dist = loc.distance_to(ploc); var altitude_m = c.getNode("position/altitude-ft").getValue()*geo.FT2M; var pathN = c.getNode("sim/model/path"); if (pathN != nil) var path = pathN.getValue(); else var path=""; var name = split("/", path)[-1]; var pos = find("-model", name); if (pos >= 0) name = substr(name, 0, pos); if (substr(name, -4) == ".xml") name = substr(name, 0, size(name) - 4); if (substr(name, -3) == ".ac") name = substr(name, 0, size(name) - 3); c.setValues({"model-short":name, "bearing-to": ploc_course, "distance-to-km":ploc_dist/1000.0, "distance-to-nm":ploc_dist*METER_TO_NM, "position/altitude-m":altitude_m }); } me.mp.getNode("players", 1).setIntValue(players); if(PILOTSDLG_RUNNING) settimer( func { me.update() }, 0.4, 1); }, _redraw_ : func { if (me.dialog != nil) { me.close(); me.create(); } }, _multiplayer_update_ : func(node) { var name = split("/", node.getValue())[-1]; if(substr(name, 0, 11) == "multiplayer") me._redraw_() }, toggle_unit : func { me.unit = !me.unit; if(me.unit) { me.alt_node = "position/altitude-m"; me.alt_hdr = "alt-m"; me.dist_hdr = "dist-km"; me.dist_node = "distance-to-km"; me.unit_button = "IM"; } else { me.alt_node = "position/altitude-ft"; me.dist_node = "distance-to-nm"; me.alt_hdr = "alt-ft"; me.dist_hdr = "dist-nm"; me.unit_button = "SI"; } }, close : func { fgcommand("dialog-close", me.namenode); }, del : func { PILOTSDLG_RUNNING=0; me.close(); foreach(var l; me.listeners) removelistener(l); delete(gui.dialog, "\"" ~ me.name ~ "\""); }, show : func { if(!PILOTSDLG_RUNNING) { PILOTSDLG_RUNNING=1; me.init(-2, -2); me.create(); me.update(); } }, toggle : func { if(!PILOTSDLG_RUNNING) me.show(); else me.del(); } };