Ticket #1767: chat_addressee_dropdown_v1.patch

File chat_addressee_dropdown_v1.patch, 16.4 KB (added by elexis, 8 years ago)

Implements a chat-addressee dropdown, observer-only chat and private-messaging for observers and also fixes #3441.

  • binaries/data/mods/public/gui/common/network.js

    function kickPlayer(username, ban)  
    8585            })
    8686        });
    8787}
    8888
    8989/**
    90  * Get a colorized list of usernames sorted by player slot, observers last.
    91  * Requires g_PlayerAssignments and colorizePlayernameByGUID.
    92  *
    93  * @returns {string}
     90 * Sort GUIDs of connected users sorted by playerindex, observers last.
     91 * Requires g_PlayerAssignments.
    9492 */
    95 function getUsernameList()
     93function sortGUIDsByPlayerID()
    9694{
    97     let usernames = Object.keys(g_PlayerAssignments).sort((guidA, guidB) => {
     95    return Object.keys(g_PlayerAssignments).sort((guidA, guidB) => {
    9896
    9997        let playerIdA = g_PlayerAssignments[guidA].player;
    10098        let playerIdB = g_PlayerAssignments[guidB].player;
    10199
    102         // Sort observers last
    103100        if (playerIdA == -1) return +1;
    104101        if (playerIdB == -1) return -1;
    105102
    106         // Sort players
    107103        return playerIdA - playerIdB;
     104    });
     105}
    108106
    109     }).map(guid => colorizePlayernameByGUID(guid));
     107/**
     108 * Get a colorized list of usernames sorted by player slot, observers last.
     109 * Requires g_PlayerAssignments and colorizePlayernameByGUID.
     110 *
     111 * @returns {string}
     112 */
     113function getUsernameList()
     114{
     115    let usernames = sortGUIDsByPlayerID().map(guid => colorizePlayernameByGUID(guid));
    110116
    111117    return sprintf(translate("Users: %(users)s"),
    112118        // Translation: This comma is used for separating first to penultimate elements in an enumeration.
    113119        { "users": usernames.join(translate(", ")) });
    114120}
  • binaries/data/mods/public/gui/session/menu.js

    function openChat()  
    201201    if (g_Disconnected)
    202202        return;
    203203
    204204    closeOpenDialogs();
    205205
    206     updateTeamCheckbox(false);
     206    setTeamChat(false);
    207207
    208208    Engine.GetGUIObjectByName("chatInput").focus(); // Grant focus to the input area
    209209    Engine.GetGUIObjectByName("chatDialogPanel").hidden = false;
    210210}
    211211
    function closeChat()  
    215215    Engine.GetGUIObjectByName("chatInput").blur(); // Remove focus
    216216    Engine.GetGUIObjectByName("chatDialogPanel").hidden = true;
    217217}
    218218
    219219/**
    220  * Chat is sent via GUID, not playerID.
     220 * If the teamchat hotkey was pressed, set allies or observers as addressees,
     221 * otherwise send to everyone.
    221222 */
    222 function updateTeamCheckbox(check)
     223function setTeamChat(teamChat = false)
    223224{
    224     Engine.GetGUIObjectByName("toggleTeamChatLabel").hidden = g_IsObserver;
    225     let toggleTeamChat = Engine.GetGUIObjectByName("toggleTeamChat");
    226     toggleTeamChat.hidden = g_IsObserver;
    227     toggleTeamChat.checked = !g_IsObserver && check;
     225    let command = teamChat ? (g_IsObserver ? "/observers" : "/allies") : "";
     226    let chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
     227    chatAddressee.selected = chatAddressee.list_data.indexOf(command);
    228228}
    229 
     229/**
     230 * Opens chat-window or closes it and sends the userinput.
     231 */
    230232function toggleChatWindow(teamChat)
    231233{
    232234    if (g_Disconnected)
    233235        return;
    234236
    function toggleChatWindow(teamChat)  
    237239    let hidden = chatWindow.hidden;
    238240
    239241    closeOpenDialogs();
    240242
    241243    if (hidden)
    242         chatInput.focus(); // Grant focus to the input area
     244    {
     245        setTeamChat(teamChat);
     246        chatInput.focus();
     247    }
    243248    else
    244249    {
    245250        if (chatInput.caption.length)
    246251        {
    247252            submitChatInput();
    248253            return;
    249254        }
    250         chatInput.caption = ""; // Clear chat input
     255        chatInput.caption = "";
    251256    }
    252257
    253     updateTeamCheckbox(teamChat);
    254258    chatWindow.hidden = !hidden;
    255259}
    256260
    257261function setDiplomacy(data)
    258262{
  • binaries/data/mods/public/gui/session/messages.js

    var g_ChatCommands = {  
    8383
    8484var g_ChatAddresseeContext = {
    8585    "/team": translate("Team"),
    8686    "/allies": translate("Ally"),
    8787    "/enemies": translate("Enemy"),
     88    "/observers": translate("Observer"),
    8889    "/msg": translate("Private")
    8990};
    9091
    9192/**
    9293 * Returns true if the current player is an addressee, given the chat message type and sender.
    var g_IsChatAddressee = {  
    9899        g_Players[Engine.GetPlayerID()].team != -1 &&
    99100        g_Players[Engine.GetPlayerID()].team == g_Players[senderID].team,
    100101
    101102    "/allies": senderID =>
    102103        g_Players[senderID] &&
     104        g_Players[Engine.GetPlayerID()] &&
    103105        g_Players[senderID].isMutualAlly[Engine.GetPlayerID()],
    104106
    105107    "/enemies": senderID =>
    106108        g_Players[senderID] &&
     109        g_Players[Engine.GetPlayerID()] &&
    107110        g_Players[senderID].isEnemy[Engine.GetPlayerID()],
    108111
     112    "/observers": senderID =>
     113        g_IsObserver,
     114
    109115    "/msg": (senderID, addresseeGUID) =>
    110         g_Players[Engine.GetPlayerID()] &&
    111         g_PlayerAssignments[addresseeGUID] &&
    112         g_PlayerAssignments[addresseeGUID].name == g_Players[Engine.GetPlayerID()].name
     116        addresseeGUID == Engine.GetPlayerGUID()
    113117};
    114118
    115119/**
    116120 * Chatmessage shown on diplomacy change.
    117121 */
    var g_NotificationsTypes =  
    186190            "guid": findGuidForPlayerID(player),
    187191            "player": player
    188192        });
    189193
    190194        updateDiplomacy();
     195        updateChatAddressees();
    191196    },
    192197    "diplomacy": function(notification, player)
    193198    {
    194199        addChatMessage({
    195200            "type": "diplomacy",
    function handlePlayerAssignmentsMessage(  
    441446        }
    442447
    443448        addChatMessage({ "type": "connect", "guid": guid });
    444449    });
    445450
     451    updateChatAddressees();
     452
    446453    // Update lobby gamestatus
    447454    if (g_IsController && Engine.HasXmppClient())
    448455    {
    449456        let players = Object.keys(g_PlayerAssignments).map(guid => g_PlayerAssignments[guid].name);
    450457        Engine.SendChangeStateGame(Object.keys(g_PlayerAssignments).length, players.join(", "));
    451458    }
    452459}
    453460
     461function updateChatAddressees()
     462{
     463    let addressees = [
     464        {
     465            "label": translateWithContext("chat addressee", "Everyone"),
     466            "cmd": ""
     467        }
     468    ];
     469
     470    if (!g_IsObserver)
     471    {
     472        addressees.push({
     473            "label": translateWithContext("chat addressee", "Allies"),
     474            "cmd": "/allies"
     475        });
     476        addressees.push({
     477            "label": translateWithContext("chat addressee", "Enemies"),
     478            "cmd": "/enemies"
     479        });
     480    }
     481
     482    addressees.push({
     483        "label": translateWithContext("chat addressee", "Observers"),
     484        "cmd": "/observers"
     485    });
     486
     487    // Add playernames for private messages
     488    for (let guid of sortGUIDsByPlayerID())
     489    {
     490        let username = g_PlayerAssignments[guid].name;
     491        let playerIndex = g_PlayerAssignments[guid].player;
     492
     493        if (playerIndex == Engine.GetPlayerID())
     494            continue;
     495
     496        // Don't provide option for PM from observer to player
     497        if (g_IsObserver && !isPlayerObserver(playerIndex))
     498            continue;
     499
     500        let colorBox = isPlayerObserver(playerIndex) ? "" : '[color="' + rgbToGuiColor(g_Players[playerIndex].color) + '"]■ [/color]';
     501
     502        addressees.push({
     503            "cmd": "/msg " + username,
     504            "label": colorBox + username
     505        });
     506    }
     507
     508    let chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
     509    chatAddressee.list = addressees.map(adressee => adressee.label);
     510    chatAddressee.list_data = addressees.map(adressee => adressee.cmd);
     511    chatAddressee.selected = 0;
     512}
     513
    454514/**
    455515 * Send text as chat. Don't look for commands.
    456516 *
    457517 * @param {string} text
    458518 */
    function submitChatDirectly(text)  
    471531 * Loads the text from the GUI window, checks if it is a local command
    472532 * or cheat and executes it. Otherwise sends it as chat.
    473533 */
    474534function submitChatInput()
    475535{
    476     let teamChat = Engine.GetGUIObjectByName("toggleTeamChat").checked;
    477536    let input = Engine.GetGUIObjectByName("chatInput");
    478537    let text = input.caption;
    479538
    480539    input.blur(); // Remove focus
    481540    input.caption = ""; // Clear chat input
    function submitChatInput()  
    488547        return;
    489548
    490549    if (executeCheat(text))
    491550        return;
    492551
    493     // Observers should only be able to chat with everyone.
    494     if (g_IsObserver && text.indexOf("/") == 0 && text.indexOf("/me ") != 0)
    495         return;
    496 
    497     if (teamChat && text.indexOf("/team ") != 0)
    498         text = "/team " + text;
     552    let chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
     553    if (chatAddressee.selected > 0 && (text.indexOf("/") != 0 || text.indexOf("/me ") == 0))
     554        text = chatAddressee.list_data[chatAddressee.selected] + " " + text;
    499555
    500556    submitChatDirectly(text);
    501557}
    502558
    503559/**
    function formatChatCommand(msg)  
    621677{
    622678    if (!msg.text)
    623679        return "";
    624680
    625681    let isMe = msg.text.indexOf("/me ") == 0;
    626     if (!isMe && !checkChatAddressee(msg))
     682    if (!isMe && !isChatAddressee(msg))
    627683        return "";
    628684
    629685    isMe = msg.text.indexOf("/me ") == 0;
    630686    if (isMe)
    631687        msg.text = msg.text.substr("/me ".length);
    function formatChatCommand(msg)  
    657713    });
    658714}
    659715
    660716/**
    661717 * Checks if the current user is an addressee of the chatmessage sent by another player.
     718 * Sets the context of that message.
     719 * Returns true if the message should be displayed.
    662720 *
    663721 * @param {Object} msg
    664722 */
    665 function checkChatAddressee(msg)
     723function isChatAddressee(msg)
    666724{
    667725    if (msg.text[0] != '/')
    668726        return true;
    669727
    670     if (Engine.GetPlayerID() == -1)
    671         return false;
    672 
     728    // Split addressee command and message-text
    673729    let cmd = msg.text.split(/\s/)[0];
    674730    msg.text = msg.text.substr(cmd.length + 1);
    675731
    676732    if (cmd == "/ally")
    677733        cmd = "/allies";
    678734
    679735    if (cmd == "/enemy")
    680736        cmd = "/enemies";
    681737
    682     // GUID for players, ID for bots
     738    if (cmd == "/observer")
     739        cmd = "/observers";
     740
     741    // GUID for players and observers, ID for bots
    683742    let senderID = (g_PlayerAssignments[msg.guid] || msg).player;
     743    let isSender = msg.guid ? msg.guid == Engine.GetPlayerGUID() : senderID == Engine.GetPlayerID();
     744
     745    // Parse private message
     746    let isPM = cmd == "/msg";
    684747    let addresseeGUID;
    685     if (cmd == "/msg")
     748    let addresseeIndex;
     749    if (isPM)
    686750    {
    687751        addresseeGUID = matchUsername(msg.text);
    688752        let addressee = g_PlayerAssignments[addresseeGUID];
    689         if (!addressee || addressee.player == -1 || senderID == -1)
     753        if (!addressee)
     754        {
     755            if (isSender)
     756                warn("Couldn't match username: " + msg.text);
     757            return false;
     758        }
     759
     760        // Prohibit PM if addressee and sender are identical
     761        if (isSender && addresseeGUID == Engine.GetPlayerGUID())
    690762            return false;
     763
    691764        msg.text = msg.text.substr(addressee.name.length + 1);
     765        addresseeIndex = addressee.player;
    692766    }
    693767
    694     let isSender = senderID == Engine.GetPlayerID();
     768    // Set context string
    695769    if (!g_ChatAddresseeContext[cmd])
    696770    {
    697771        if (isSender)
    698772            warn("Unknown chat command: " + cmd);
    699773        return false;
    700774    }
    701775    msg.context = g_ChatAddresseeContext[cmd];
    702776
     777    // For observers only permit public- and observer-chat and PM to observers
     778    if (isPlayerObserver(senderID) &&
     779            (isPM && !isPlayerObserver(addresseeIndex) || !isPM && cmd != "/observers"))
     780        return false;
     781
    703782    return isSender || g_IsChatAddressee[cmd](senderID, addresseeGUID);
    704783}
    705784
    706785/**
    707786 * Returns the guid of the user with the longest name that is a prefix of the given string.
  • binaries/data/mods/public/gui/session/session.js

    function selectViewPlayer(playerID)  
    314314
    315315    Engine.SetViewedPlayer(g_ViewedPlayer);
    316316
    317317    updateTopPanel();
    318318
     319    updateChatAddressees();
     320
    319321    // Update GUI and clear player-dependent cache
    320322    onSimulationUpdate();
    321323
    322324    let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
    323325    viewPlayer.hidden = !changeView;
  • binaries/data/mods/public/gui/session/session.xml

     
    207207    <object name="chatPanel" size="0 130 100% 100%-240" type="image" ghost="true" z="0" absolute="true">
    208208        <object name="chatText" size="3 1 100%-1 100%-1" type="text" style="chatPanel" ghost="true"/>
    209209    </object>
    210210
    211211    <!-- Chat window -->
    212     <object name="chatDialogPanel" size="50%-180 50%-48 50%+180 50%+36" type="image" hidden="true" sprite="genericPanel">
    213         <object name="chatInput" size="16 12 100%-16 36" type="input" style="ModernInput" max_length="80">
     212    <object name="chatDialogPanel" size="50%-180 50%-66 50%+180 50%+54" type="image" hidden="true" sprite="genericPanel">
     213
     214        <!-- Message addressee -->
     215        <object size="16 14 50 38" type="text" style="chatPanel">
     216            <translatableAttribute id="caption" context="chat input">To:</translatableAttribute>
     217        </object>
     218        <object size="75 12 100%-16 36" name="chatAddressee" type="dropdown" style="ModernDropDown" tooltip_style="sessionToolTipBold">
     219            <translatableAttribute id="tooltip" context="chat input">Select chatmessage addresse</translatableAttribute>
     220        </object>
     221
     222        <!-- Message text -->
     223        <object size="16 46 50 70" type="text" style="chatPanel">
     224            <translatableAttribute id="caption" context="chat input">Text:</translatableAttribute>
     225        </object>
     226        <object name="chatInput" size="75 44 100%-16 68" type="input" style="ModernInput" max_length="80">
     227            <translatableAttribute id="tooltip" context="chat input">Type the message to send.</translatableAttribute>
    214228            <action on="Press">submitChatInput();</action>
    215229            <action on="Tab">
    216230                var players = [];
    217231                for (var player in g_PlayerAssignments)
    218232                    players.push(g_PlayerAssignments[player]);
    219233                autoCompleteNick("chatInput", players);
    220234            </action>
    221235        </object>
    222236
     237        <!-- Cancel -->
    223238        <object size="16 100%-40 30%+16 100%-12" type="button" style="StoneButton">
    224239            <translatableAttribute id="caption">Cancel</translatableAttribute>
    225240            <action on="Press">closeChat();</action>
    226241        </object>
    227242
    228         <object name="toggleTeamChat" size="30%+22 100%-36 30%+40 100%-6" type="checkbox" style="ModernTickBox"/>
    229         <object name="toggleTeamChatLabel" size="30%+40 100%-40 60%+16 100%-12" type="text" style="ModernLeftLabelText">
    230             <translatableAttribute id="caption">Team Only</translatableAttribute>
    231         </object>
    232 
     243        <!-- Send -->
    233244        <object size="60%+16 100%-40 100%-16 100%-12" type="button" style="StoneButton">
    234245            <translatableAttribute id="caption">Send</translatableAttribute>
    235246            <action on="Press">submitChatInput();</action>
    236247        </object>
    237248    </object>
  • source/gui/scripting/ScriptFunctions.cpp

    void DisconnectNetworkGame(ScriptInterfa  
    379379    SAFE_DELETE(g_NetServer);
    380380    SAFE_DELETE(g_NetClient);
    381381    SAFE_DELETE(g_Game);
    382382}
    383383
     384std::string GetPlayerGUID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
     385{
     386    if (!g_NetClient)
     387        return "";
     388
     389    return g_NetClient->GetGUID();
     390}
     391
    384392bool KickPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& playerName, bool ban)
    385393{
    386394    if (!g_NetServer)
    387395        return false;
    388396
    void GuiScriptingInit(ScriptInterface& s  
    10141022    scriptInterface.RegisterFunction<void, JS::HandleValue, int, &StartGame>("StartGame");
    10151023    scriptInterface.RegisterFunction<void, &Script_EndGame>("EndGame");
    10161024    scriptInterface.RegisterFunction<void, std::wstring, &StartNetworkHost>("StartNetworkHost");
    10171025    scriptInterface.RegisterFunction<void, std::wstring, std::string, &StartNetworkJoin>("StartNetworkJoin");
    10181026    scriptInterface.RegisterFunction<void, &DisconnectNetworkGame>("DisconnectNetworkGame");
     1027    scriptInterface.RegisterFunction<std::string, &GetPlayerGUID>("GetPlayerGUID");
    10191028    scriptInterface.RegisterFunction<bool, CStrW, bool, &KickPlayer>("KickPlayer");
    10201029    scriptInterface.RegisterFunction<JS::Value, &PollNetworkClient>("PollNetworkClient");
    10211030    scriptInterface.RegisterFunction<void, JS::HandleValue, &SetNetworkGameAttributes>("SetNetworkGameAttributes");
    10221031    scriptInterface.RegisterFunction<void, int, std::string, &AssignNetworkPlayer>("AssignNetworkPlayer");
    10231032    scriptInterface.RegisterFunction<void, std::string, int, &SetNetworkPlayerStatus>("SetNetworkPlayerStatus");
  • source/network/NetClient.h

    public:  
    8787     * This must not be called after the connection setup.
    8888     */
    8989    void SetUserName(const CStrW& username);
    9090
    9191    /**
     92     * Returns the GUID of the local client.
     93     * Used for distinguishing observers.
     94     */
     95    CStr GetGUID() const { return m_GUID; }
     96
     97    /**
    9298     * Set up a connection to the remote networked server.
    9399     * @param server IP address or host name to connect to
    94100     * @return true on success, false on connection failure
    95101     */
    96102    bool SetupConnection(const CStr& server);