diff --git a/binaries/data/mods/public/gui/common/network.js b/binaries/data/mods/public/gui/common/network.js index cc3fc0412a..533a88c922 100644 --- a/binaries/data/mods/public/gui/common/network.js +++ b/binaries/data/mods/public/gui/common/network.js @@ -86,6 +86,24 @@ function kickPlayer(username, ban) }); } +/** + * Sort GUIDs of connected users sorted by playerindex, observers last. + * Requires g_PlayerAssignments. + */ +function sortGUIDsByPlayerID() +{ + return Object.keys(g_PlayerAssignments).sort((guidA, guidB) => { + + let playerIdA = g_PlayerAssignments[guidA].player; + let playerIdB = g_PlayerAssignments[guidB].player; + + if (playerIdA == -1) return +1; + if (playerIdB == -1) return -1; + + return playerIdA - playerIdB; + }); +} + /** * Get a colorized list of usernames sorted by player slot, observers last. * Requires g_PlayerAssignments and colorizePlayernameByGUID. @@ -94,19 +112,7 @@ function kickPlayer(username, ban) */ function getUsernameList() { - let usernames = Object.keys(g_PlayerAssignments).sort((guidA, guidB) => { - - let playerIdA = g_PlayerAssignments[guidA].player; - let playerIdB = g_PlayerAssignments[guidB].player; - - // Sort observers last - if (playerIdA == -1) return +1; - if (playerIdB == -1) return -1; - - // Sort players - return playerIdA - playerIdB; - - }).map(guid => colorizePlayernameByGUID(guid)); + let usernames = sortGUIDsByPlayerID().map(guid => colorizePlayernameByGUID(guid)); return sprintf(translate("Users: %(users)s"), // Translation: This comma is used for separating first to penultimate elements in an enumeration. diff --git a/binaries/data/mods/public/gui/session/menu.js b/binaries/data/mods/public/gui/session/menu.js index 0570c0331f..ddadc4018b 100644 --- a/binaries/data/mods/public/gui/session/menu.js +++ b/binaries/data/mods/public/gui/session/menu.js @@ -203,7 +203,7 @@ function openChat() closeOpenDialogs(); - updateTeamCheckbox(false); + setTeamChat(false); Engine.GetGUIObjectByName("chatInput").focus(); // Grant focus to the input area Engine.GetGUIObjectByName("chatDialogPanel").hidden = false; @@ -217,16 +217,18 @@ function closeChat() } /** - * Chat is sent via GUID, not playerID. + * If the teamchat hotkey was pressed, set allies or observers as addressees, + * otherwise send to everyone. */ -function updateTeamCheckbox(check) +function setTeamChat(teamChat = false) { - Engine.GetGUIObjectByName("toggleTeamChatLabel").hidden = g_IsObserver; - let toggleTeamChat = Engine.GetGUIObjectByName("toggleTeamChat"); - toggleTeamChat.hidden = g_IsObserver; - toggleTeamChat.checked = !g_IsObserver && check; + let command = teamChat ? (g_IsObserver ? "/observers" : "/allies") : ""; + let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); + chatAddressee.selected = chatAddressee.list_data.indexOf(command); } - +/** + * Opens chat-window or closes it and sends the userinput. + */ function toggleChatWindow(teamChat) { if (g_Disconnected) @@ -239,7 +241,10 @@ function toggleChatWindow(teamChat) closeOpenDialogs(); if (hidden) - chatInput.focus(); // Grant focus to the input area + { + setTeamChat(teamChat); + chatInput.focus(); + } else { if (chatInput.caption.length) @@ -247,10 +252,9 @@ function toggleChatWindow(teamChat) submitChatInput(); return; } - chatInput.caption = ""; // Clear chat input + chatInput.caption = ""; } - updateTeamCheckbox(teamChat); chatWindow.hidden = !hidden; } diff --git a/binaries/data/mods/public/gui/session/messages.js b/binaries/data/mods/public/gui/session/messages.js index b6796e74a0..999aa3c1b1 100644 --- a/binaries/data/mods/public/gui/session/messages.js +++ b/binaries/data/mods/public/gui/session/messages.js @@ -85,6 +85,7 @@ var g_ChatAddresseeContext = { "/team": translate("Team"), "/allies": translate("Ally"), "/enemies": translate("Enemy"), + "/observers": translate("Observer"), "/msg": translate("Private") }; @@ -100,16 +101,19 @@ var g_IsChatAddressee = { "/allies": senderID => g_Players[senderID] && + g_Players[Engine.GetPlayerID()] && g_Players[senderID].isMutualAlly[Engine.GetPlayerID()], "/enemies": senderID => g_Players[senderID] && + g_Players[Engine.GetPlayerID()] && g_Players[senderID].isEnemy[Engine.GetPlayerID()], + "/observers": senderID => + g_IsObserver, + "/msg": (senderID, addresseeGUID) => - g_Players[Engine.GetPlayerID()] && - g_PlayerAssignments[addresseeGUID] && - g_PlayerAssignments[addresseeGUID].name == g_Players[Engine.GetPlayerID()].name + addresseeGUID == Engine.GetPlayerGUID() }; /** @@ -188,6 +192,7 @@ var g_NotificationsTypes = }); updateDiplomacy(); + updateChatAddressees(); }, "diplomacy": function(notification, player) { @@ -443,6 +448,8 @@ function handlePlayerAssignmentsMessage(message) addChatMessage({ "type": "connect", "guid": guid }); }); + updateChatAddressees(); + // Update lobby gamestatus if (g_IsController && Engine.HasXmppClient()) { @@ -451,6 +458,59 @@ function handlePlayerAssignmentsMessage(message) } } +function updateChatAddressees() +{ + let addressees = [ + { + "label": translateWithContext("chat addressee", "Everyone"), + "cmd": "" + } + ]; + + if (!g_IsObserver) + { + addressees.push({ + "label": translateWithContext("chat addressee", "Allies"), + "cmd": "/allies" + }); + addressees.push({ + "label": translateWithContext("chat addressee", "Enemies"), + "cmd": "/enemies" + }); + } + + addressees.push({ + "label": translateWithContext("chat addressee", "Observers"), + "cmd": "/observers" + }); + + // Add playernames for private messages + for (let guid of sortGUIDsByPlayerID()) + { + let username = g_PlayerAssignments[guid].name; + let playerIndex = g_PlayerAssignments[guid].player; + + if (playerIndex == Engine.GetPlayerID()) + continue; + + // Don't provide option for PM from observer to player + if (g_IsObserver && !isPlayerObserver(playerIndex)) + continue; + + let colorBox = isPlayerObserver(playerIndex) ? "" : '[color="' + rgbToGuiColor(g_Players[playerIndex].color) + '"]■ [/color]'; + + addressees.push({ + "cmd": "/msg " + username, + "label": colorBox + username + }); + } + + let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); + chatAddressee.list = addressees.map(adressee => adressee.label); + chatAddressee.list_data = addressees.map(adressee => adressee.cmd); + chatAddressee.selected = 0; +} + /** * Send text as chat. Don't look for commands. * @@ -473,7 +533,6 @@ function submitChatDirectly(text) */ function submitChatInput() { - let teamChat = Engine.GetGUIObjectByName("toggleTeamChat").checked; let input = Engine.GetGUIObjectByName("chatInput"); let text = input.caption; @@ -490,12 +549,9 @@ function submitChatInput() if (executeCheat(text)) return; - // Observers should only be able to chat with everyone. - if (g_IsObserver && text.indexOf("/") == 0 && text.indexOf("/me ") != 0) - return; - - if (teamChat && text.indexOf("/team ") != 0) - text = "/team " + text; + let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); + if (chatAddressee.selected > 0 && (text.indexOf("/") != 0 || text.indexOf("/me ") == 0)) + text = chatAddressee.list_data[chatAddressee.selected] + " " + text; submitChatDirectly(text); } @@ -623,7 +679,7 @@ function formatChatCommand(msg) return ""; let isMe = msg.text.indexOf("/me ") == 0; - if (!isMe && !checkChatAddressee(msg)) + if (!isMe && !isChatAddressee(msg)) return ""; isMe = msg.text.indexOf("/me ") == 0; @@ -659,17 +715,17 @@ function formatChatCommand(msg) /** * Checks if the current user is an addressee of the chatmessage sent by another player. + * Sets the context of that message. + * Returns true if the message should be displayed. * * @param {Object} msg */ -function checkChatAddressee(msg) +function isChatAddressee(msg) { if (msg.text[0] != '/') return true; - if (Engine.GetPlayerID() == -1) - return false; - + // Split addressee command and message-text let cmd = msg.text.split(/\s/)[0]; msg.text = msg.text.substr(cmd.length + 1); @@ -679,19 +735,37 @@ function checkChatAddressee(msg) if (cmd == "/enemy") cmd = "/enemies"; - // GUID for players, ID for bots + if (cmd == "/observer") + cmd = "/observers"; + + // GUID for players and observers, ID for bots let senderID = (g_PlayerAssignments[msg.guid] || msg).player; + let isSender = msg.guid ? msg.guid == Engine.GetPlayerGUID() : senderID == Engine.GetPlayerID(); + + // Parse private message + let isPM = cmd == "/msg"; let addresseeGUID; - if (cmd == "/msg") + let addresseeIndex; + if (isPM) { addresseeGUID = matchUsername(msg.text); let addressee = g_PlayerAssignments[addresseeGUID]; - if (!addressee || addressee.player == -1 || senderID == -1) + if (!addressee) + { + if (isSender) + warn("Couldn't match username: " + msg.text); return false; + } + + // Prohibit PM if addressee and sender are identical + if (isSender && addresseeGUID == Engine.GetPlayerGUID()) + return false; + msg.text = msg.text.substr(addressee.name.length + 1); + addresseeIndex = addressee.player; } - let isSender = senderID == Engine.GetPlayerID(); + // Set context string if (!g_ChatAddresseeContext[cmd]) { if (isSender) @@ -700,6 +774,11 @@ function checkChatAddressee(msg) } msg.context = g_ChatAddresseeContext[cmd]; + // For observers only permit public- and observer-chat and PM to observers + if (isPlayerObserver(senderID) && + (isPM && !isPlayerObserver(addresseeIndex) || !isPM && cmd != "/observers")) + return false; + return isSender || g_IsChatAddressee[cmd](senderID, addresseeGUID); } diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js index 88ae4a3c7a..1009f387a0 100644 --- a/binaries/data/mods/public/gui/session/session.js +++ b/binaries/data/mods/public/gui/session/session.js @@ -316,6 +316,8 @@ function selectViewPlayer(playerID) updateTopPanel(); + updateChatAddressees(); + // Update GUI and clear player-dependent cache onSimulationUpdate(); diff --git a/binaries/data/mods/public/gui/session/session.xml b/binaries/data/mods/public/gui/session/session.xml index c98b6f7cc4..ef83b424b4 100644 --- a/binaries/data/mods/public/gui/session/session.xml +++ b/binaries/data/mods/public/gui/session/session.xml @@ -209,8 +209,22 @@ -