fedd64d89a
- use find() instead of string.match() (way faster!) - add a missing var keyword - don't store old messages in a hash, but only store the respective last message in the model's multiplayer branch - keep history \n-trimmed (instead of removing \n only when concatenating)
370 lines
11 KiB
Text
370 lines
11 KiB
Text
# 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 check_messages = func
|
|
{
|
|
|
|
var mp = props.globals.getNode("/ai/models").getChildren("multiplayer");
|
|
var lseen = {};
|
|
|
|
foreach (var n; mp)
|
|
{
|
|
var last = n.initNode("sim/multiplay/last-message", "");
|
|
var lmsg = n.getNode("sim/multiplay/chat", 1).getValue();
|
|
var lcallsign = n.getNode("callsign", 1).getValue();
|
|
var lvalid = n.getNode("valid", 1).getValue();
|
|
|
|
if (!lvalid or !lmsg or !lcallsign)
|
|
continue;
|
|
|
|
if (contains(lseen, lcallsign))
|
|
continue;
|
|
|
|
# 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 (lmsg != last.getValue())
|
|
{
|
|
# Display the message.
|
|
echo_message(lmsg, lcallsign);
|
|
last.setValue(lmsg);
|
|
}
|
|
}
|
|
|
|
# Check for new messages every couple of seconds.
|
|
settimer(check_messages, 3);
|
|
}
|
|
|
|
var echo_message = func(msg, callsign)
|
|
{
|
|
if ((callsign != nil) and (find(callsign, msg) < 0))
|
|
{
|
|
# Only prefix with the callsign if the message doesn't already include it.
|
|
msg = callsign ~ ": " ~ msg;
|
|
}
|
|
|
|
msg = string.trim(string.replace(msg, "\n", " "));
|
|
|
|
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
|
|
{
|
|
setprop("/sim/multiplay/chat-history", lchat ~ "\n" ~ 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 <buti (at) bux (dot) at>
|
|
#
|
|
# 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", "brg", 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});
|
|
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") == nil or !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()*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();
|
|
}
|
|
};
|
|
|