diff --git a/ATC/chat-menu-entries.xml b/ATC/chat-menu-entries.xml
new file mode 100644
index 000000000..2abc2023e
--- /dev/null
+++ b/ATC/chat-menu-entries.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+ [ATC]
+
+ [Ground]
+
+ * Ground, #, request taxi instructions for departure.
+
+
+ * Ground, # request departure information.
+
+
+ * Ground, # request startup.
+
+
+ * Ground, # request pushback.
+
+
+ * Ground, # request taxi.
+
+
+ #, ready for departure.
+
+
+ Line up #.
+
+
+ Cleared for take-off #.
+
+
+ # runway vacated.
+
+
+
+ [Air]
+
+ * Tower, ^ miles ! altitude $ feet request joining instructions.
+
+
+ * Tower, ^ miles ! altitude $ feet request straight in approach runway (.
+
+
+ # crosswind.
+
+
+ # downwind
+
+ touch and go.
+
+
+ full stop.
+
+
+
+ # base.
+
+
+ # final.
+
+
+ # long final.
+
+
+ # going around.
+
+
+
+ [Enroute]
+
+ * Approach, # inbound from the !, IFR ^ miles at $ feet.
+
+
+ * Approach, # inbound from the !, VFR ^ miles at $ feet.
+
+
+ * Approach, request Radar Information Service.
+
+
+
+
+ [Formation]
+
+ Form up on my wing.
+
+
+ Break left.
+
+
+ Break right.
+
+
+
+
diff --git a/Nasal/multiplayer.nas b/Nasal/multiplayer.nas
index eabc2c76a..635f779c4 100644
--- a/Nasal/multiplayer.nas
+++ b/Nasal/multiplayer.nas
@@ -10,76 +10,158 @@
var messages = {};
-check_messages = func
+var check_messages = func
{
- var mp = props.globals.getNode("/ai/models").getChildren("multiplayer");
+ 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");
+ foreach (i; mp)
+ {
+ var lmsg = getprop(i.getPath() ~ "/sim/multiplay/chat");
+ var lcallsign = getprop(i.getPath() ~ "/callsign");
+ var lvalid = getprop(i.getPath() ~ "/valid");
- if ((lmsg != nil) and (lmsg != "") and (lcallsign != nil) and (lcallsign != ""))
- {
- #print("Call Sign: " ~ lcallsign);
- #print("lmsg: " ~ lmsg);
- #print("Freq: " ~ ltransmitfreq);
+ if ((lvalid) and
+ (lmsg != nil) and
+ (lmsg != "") and
+ (lcallsign != nil) and
+ (lcallsign != ""))
+ {
+#print("Call Sign: " ~ lcallsign);
+#print("lmsg: " ~ lmsg);
+#print("Freq: " ~ ltransmitfreq);
- if ((! contains(messages, lcallsign)) or (lmsg != messages[lcallsign]))
- {
- # Indicate we've seen this message.
- messages[lcallsign] = lmsg;
- echo_message(lmsg, 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;
- # Check for new messages every couple of seconds.
- settimer(check_messages, 3);
+ 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);
}
-echo_message = func(msg, callsign)
+var echo_message = func(msg, callsign)
{
- if (callsign != nil)
- {
- msg = callsign ~ ": " ~ msg;
- }
+ if (callsign != nil)
+ {
+ msg = callsign ~ ": " ~ msg;
+ }
- var ldisplay = getprop("/sim/multiplay/chat-display");
+ 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);
- }
+ 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");
+# 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";
- }
+ 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);
- }
+ 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")); });
+# 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();
+# 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(0); # 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;
+ }
+}
diff --git a/gui/dialogs/chat-menu.xml b/gui/dialogs/chat-menu.xml
new file mode 100644
index 000000000..6a0337d0e
--- /dev/null
+++ b/gui/dialogs/chat-menu.xml
@@ -0,0 +1,390 @@
+
+
+
+ chat-menu
+ 10
+ -30
+ vbox
+ false
+ 1
+
+ HELVETICA_12
+
+
+ 0
+ 0
+ 0
+ 0
+
+
+
+ left
+
+ 0.9
+ 0.2
+ 0.2
+ 1
+
+
+
+
+ left
+
+ 0.9
+ 0.2
+ 0.2
+ 1
+
+
+
+ left
+ /sim/multiplay/chat-menu/entry[0]
+ true
+
+ 0.9
+ 0.2
+ 0.2
+ 1
+
+
+
+ left
+ /sim/multiplay/chat-menu/entry[1]
+ true
+
+ 0.9
+ 0.2
+ 0.2
+ 1
+
+
+
+ left
+ /sim/multiplay/chat-menu/entry[2]
+ true
+
+ 0.9
+ 0.2
+ 0.2
+ 1
+
+
+
+ left
+ /sim/multiplay/chat-menu/entry[3]
+ true
+
+ 0.9
+ 0.2
+ 0.2
+ 1
+
+
+
+ left
+ /sim/multiplay/chat-menu/entry[4]
+ true
+
+ 0.9
+ 0.2
+ 0.2
+ 1
+
+
+
+ left
+ /sim/multiplay/chat-menu/entry[5]
+ true
+
+ 0.9
+ 0.2
+ 0.2
+ 1
+
+
+
+ left
+ /sim/multiplay/chat-menu/entry[6]
+ true
+
+ 0.9
+ 0.2
+ 0.2
+ 1
+
+
+
+ left
+ /sim/multiplay/chat-menu/entry[7]
+ true
+
+ 0.9
+ 0.2
+ 0.2
+ 1
+
+
+
+
+
+ = 180) wind -= 360;
+ while (wind < -180) wind += 360;
+
+ var deviation = math.abs(wind) + 1e-20;
+ var v = (0.01 * curr.length + 0.01 * curr.width) / deviation;
+
+ if (v > max) {
+ max = v;
+ active_runway = r;
+ }
+ }
+
+ # Find our distance and cardinal direction to the airport.
+ var directions = split(",", "North,North East,East,South East,South,South West,West,North West");
+ var loc = geo.aircraft_position();
+ var airportloc = geo.Coord.new();
+ airportloc.set_latlon(airport.lat, airport.lon, airport.elevation);
+
+ # We want the course _from_ the airport to the aircraft - for reporting ".. approaching from the SW"
+ var airport_course = airportloc.course_to(loc);
+ var airport_distance = int((airportloc.distance_to(loc) / 1609) + 0.5);
+
+ # Get an index into our array of directions.
+ var dir = int(math.mod((airport_course + 22.5), 360) / 45);
+ var airport_direction = directions[dir];
+
+ # Now launch the keyboard listener.
+ var kbdevent = setlistener("/devices/status/keyboard/event", func (event) {
+
+ # Only check the key when pressed.
+ if (!event.getNode("pressed").getValue())
+ return;
+
+ var key = event.getNode("key");
+
+ if (handle_key(key.getValue()))
+ key.setValue(0); # drop key event
+ });
+
+ var handle_key = func (key) {
+ # We only handle keys 0-9 and Esc
+
+ if (key == 27)
+ {
+ # escape -> cancel
+ removelistener(kbdevent);
+ gui.popdown();
+ fgcommand("dialog-close", props.Node.new({"dialog-name": "chat-menu"}));
+ return 1;
+ }
+
+ if ((key < `0`) or (key > `9`))
+ {
+ # pass the event back.
+ return 0;
+ }
+
+ if (key == `0`)
+ {
+ # Go back one level.
+ text = "";
+
+ if (pos.getName() != "config")
+ {
+ # Build up the chat string again.
+ pos = pos.getParent();
+ var p = pos;
+
+ while ((p.getName() != "config") and
+ (p.getChild("name").getValue() != nil))
+ {
+ var t = string.trim(p.getChild("name").getValue());
+
+ # Entries that begin with "[" are silent.
+ if (t[0] != `[`)
+ {
+ text = t ~ " " ~ text;
+ }
+
+ p = p.getParent();
+ }
+ }
+
+ updateDialog();
+ }
+
+ if (key == `1`)
+ {
+ # Go to edit mode using the inline editor.
+ removelistener(kbdevent);
+ gui.popdown();
+ multiplayer.compose_message(text);
+ fgcommand("dialog-close", props.Node.new({"dialog-name": "chat-menu"}));
+ }
+
+ if ((key > `1`) and (key <= `9`))
+ {
+ # Select the appropriate new node and update.
+ # The index starts from position 2.
+ var i = key - `2`;
+
+ if (i > (size(pos.getChildren("menu")) -1))
+ {
+ # Drop out if the user has entered a too large value.
+ return 0;
+ }
+
+ var t = entryNodes[i].getValue();
+
+ t = string.trim(substr(t, 3, size(t) -3));
+
+ # Entries that begin with "[" are silent.
+ if (t[0] != `[`)
+ {
+ text = text ~ " " ~ t;
+ }
+
+ pos = pos.getChildren("menu")[i];
+
+ if (size(pos.getChildren("menu")) == 0)
+ {
+ # We've come to the end of the tree - send the message and close
+ setprop("/sim/multiplay/chat", text);
+ removelistener(kbdevent);
+ gui.popdown();
+ fgcommand("dialog-close", props.Node.new({"dialog-name": "chat-menu"}));
+ }
+ else
+ {
+ # We've got more tree to traverse.
+ updateDialog();
+ }
+ }
+
+ # If we got here, we consumed the event
+ return 1;
+ }
+
+ # Substitute simple values into the string.
+ # The values we handle are
+ #
+ # % - Type (first word of /sim/description)
+ # # - callsign
+ # $ - altitude
+ # * - airport ID.
+ # & - course from airport to aircraft - "North West"
+ # ! - distance from aircraft to airport in miles.
+ #
+ var subvals = func (str) {
+ var t = "";
+
+ for (var p = 0; p < size(str); p += 1)
+ {
+ if (str[p] == `!`) t ~= airport_direction;
+ elsif (str[p] == `^`) t ~= airport_distance;
+ elsif (str[p] == `(`) t ~= active_runway;
+ elsif (str[p] == `*`) t ~= airport.name;
+ elsif (str[p] == `%`) t ~= type;
+ elsif (str[p] == `#`) t ~= callsign;
+ elsif (str[p] == `$`) t ~= altitude;
+ else t ~= chr(str[p]);
+ }
+
+ return t;
+ }
+
+ var updateDialog = func {
+
+ var children = pos.getChildren("menu");
+ var i = 0;
+ foreach(var c; children)
+ {
+ var p = i + 2;
+ var txt = p ~ ': ' ~ subvals(c.getChild("name").getValue());
+
+ if (i < 9)
+ {
+ entryNodes[i].setValue(txt);
+ }
+ i = i + 1;
+ }
+
+ # Set the rest of the dialog to blank.
+ while (i <= 9)
+ {
+ entryNodes[i].setValue("");
+ i = i + 1;
+ }
+
+ # Write the popup.
+ gui.popupTip(text, 1000000);
+ }
+
+ # Start by updating the dialog.
+ updateDialog();
+ ]]>
+
+
+
diff --git a/gui/menubar.xml b/gui/menubar.xml
index d98b355e7..fb2bd1148 100644
--- a/gui/menubar.xml
+++ b/gui/menubar.xml
@@ -161,7 +161,13 @@
chat
-
+ -
+
+
++ dialog-show
+ chat-menu
+
+
diff --git a/keyboard.xml b/keyboard.xml
index b8a2a31da..2bd9e7f05 100644
--- a/keyboard.xml
+++ b/keyboard.xml
@@ -352,7 +352,17 @@ top down before the key bindings are parsed.
-
+
+ -
+ false
+ Compose Chat
+
+ dialog-show
+ chat-menu
+
+
+
+
.
Right brake
@@ -773,7 +783,17 @@ top down before the key bindings are parsed.
-
+
+ _
+ false
+ Compose Chat
+
+ nasal
+
+
+
+
+
a
Increase speed-up.
diff --git a/preferences.xml b/preferences.xml
index a424bed49..f5200f37c 100644
--- a/preferences.xml
+++ b/preferences.xml
@@ -472,6 +472,7 @@ Started September 2000 by David Megginson, david@megginson.com
Hello
118500000
true
+