Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -398,6 +398,11 @@ server = "lobby.wildfiregames.com" ; Address of lobby server xpartamupp = "wfgbot23" ; Name of the server-side xmpp client that manage games buddies = "," ; Comma separated list of playernames that the current user has marked as buddies +chatsenderpresence = false ; Dynamically changing brightness of messages by presence of sent player +chatinfogames = "disabled" ; Give info in chat about new/ended games for "disabled", "buddies", "all" +chatinfogamesjoin = "disabled" ; Give info in chat about player join games for "disabled", "buddies", "all" +chatsenderpresencedot = "false" ; Show presence dot in sender in lobby chat. +chatsenderrating = "false" ; Show rating of the player on sender in chat messages. [lobby.columns] gamerating = false ; Show the average rating of the participating players in a column of the gamelist Index: binaries/data/mods/public/gui/common/color.js =================================================================== --- binaries/data/mods/public/gui/common/color.js +++ binaries/data/mods/public/gui/common/color.js @@ -141,6 +141,16 @@ return [r, g, b].map(n => Math.round(n * 255)); } +/** + * @param {string} color - RGB Color as string with three numbers as pattern "[r] [g] [b]". + * @param {float} brightness - Adjust color brightness from [0-1]. + * @returns {string} brightend color + */ +function setColorBrightness(color, brightness) +{ + return color.split(" ").map(number => Math.round(number * brightness)).join(" "); +} + function colorizeHotkey(text, hotkey) { let key = Engine.ConfigDB_GetValue("user", "hotkey." + hotkey); Index: binaries/data/mods/public/gui/lobby/lobby.js =================================================================== --- binaries/data/mods/public/gui/lobby/lobby.js +++ binaries/data/mods/public/gui/lobby/lobby.js @@ -41,11 +41,11 @@ * The playerlist will be assembled using these values. */ var g_PlayerStatuses = { - "available": { "color": "0 219 0", "status": translate("Online") }, - "away": { "color": "229 76 13", "status": translate("Away") }, - "playing": { "color": "200 0 0", "status": translate("Busy") }, - "offline": { "color": "0 0 0", "status": translate("Offline") }, - "unknown": { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") } + "available": { "color": "0 219 0", "brightness": 1, "status": translate("Online") }, + "away": { "color": "229 76 13", "brightness": 0.8, "status": translate("Away") }, + "playing": { "color": "200 0 0", "brightness": 0.6, "status": translate("Busy") }, + "offline": { "color": "0 0 0", "brightness": 0.4, "status": translate("Offline") }, + "unknown": { "color": "178 178 178", "brightness": 0.4, "status": translateWithContext("lobby presence", "Unknown") } }; var g_RoleNames = { @@ -88,7 +88,7 @@ /** * All games currently running. */ -var g_GameList = {}; +var g_GameList; /** * Used to restore the selection after updating the playerlist. @@ -111,6 +111,11 @@ var g_Kicked = false; /** + * List of online players, their ratings, presence state and messages in chat. + */ +var g_PlayerList; + +/** * Processing of notifications sent by XmppClient.cpp. * * @returns true if the playerlist GUI must be updated. @@ -273,7 +278,7 @@ }, "game": { "gamelist": msg => { - updateGameList(); + updateGameList(true); return false; }, "profile": msg => { @@ -597,10 +602,54 @@ } /** + * Restore attributes from previous list to objects on new list. + * Identify objects by name. Try to find at index first, then search the list. + * + * @param {object} obj - New object, which gets attributes from previous list + * @param {integer} i - Index to look at in previous list + * @param {array} list - Previous list + * @param {array} [att, defVal] - List of attributes to restore and default value if attribute not exist. + * @returns {integer} - Index in previous list or null + */ +function restoreAttsFromPrevList(obj, i, list, atts) +{ + let iFound = list[i] && list[i].name == obj.name ? i : + list.findIndex(otherObj => otherObj.name == obj.name); + atts.forEach(([att, defVal]) => obj[att] = iFound != -1 ? list[iFound][att] : defVal); + return iFound; +} + +function updatePlayerMsgs(msgs, presence, rating) +{ + let msgsUpdated = false; + msgs.forEach(msg => { + msg.playerPresence = presence; + msg.playerRating = rating; + msgsUpdated = ircFormat(msg) || msgsUpdated; + }); + return msgsUpdated; +} + +/** * Do a full update of the player listing, including ratings from cached C++ information. + * @param {boolean} networked - Update from network. */ -function updatePlayerList() +function updatePlayerList(networked) { + let isInitialPlayerList = !Array.isArray(g_PlayerList); + let newPlayerList = Engine.GetPlayerList(); + + // Catch for initial data, so we can determine between live and initial data. + if (isInitialPlayerList) + { + // Wait for initial playerlist from network (length != 0, gui also calls this functionsw with empty playerlist). + if (!networked && newPlayerList.length == 0) + return; + g_PlayerList = []; + } + + let previousPlayerList = g_PlayerList; + let playersBox = Engine.GetGUIObjectByName("playersBox"); let sortBy = playersBox.selected_column || "name"; let sortOrder = playersBox.selected_column_order || 1; @@ -611,10 +660,35 @@ let nickList = []; let ratingList = []; - let cleanPlayerList = Engine.GetPlayerList().map(player => { + let isUpdatedChat = false; + let indexPrev = 0; + + g_PlayerList = newPlayerList.map(player => { + let found = restoreAttsFromPrevList(player, indexPrev, g_PlayerList, [ [ "msgs", [] ], [ "games", [] ] ]); + if (found != -1) + { + let playPrev = g_PlayerList[found]; + playPrev.exist = true; + if (playPrev.rating != player.rating || playPrev.presence != player.presence) + isUpdatedChat = updatePlayerMsgs(player.msgs, player.presence, playPrev.rating) || isUpdatedChat; + indexPrev = found + 1; // Step forward + } + else + g_ChatMessages.forEach(msg => player.name == msg.from && addMsgToPlayer(msg, false, player) && (isUpdatedChat = ircFormat(msg) || isUpdatedChat)); + player.isBuddy = g_Buddies.indexOf(player.name) != -1; return player; - }).sort((a, b) => { + }); + + // Check unassigned msgs + if (isInitialPlayerList) + g_ChatMessages.forEach(msg => addMsgToPlayer(msg, true) && (isUpdatedChat = ircFormat(msg) || isUpdatedChat)); + + previousPlayerList.forEach(playerPrev => + playerPrev.exist === undefined && + (isUpdatedChat = updatePlayerMsgs(playerPrev.msgs, "offline", playerPrev.rating) || isUpdatedChat)); + + let cleanPlayerList = g_PlayerList.sort((a, b) => { let sortA, sortB; let statusOrder = Object.keys(g_PlayerStatuses); let statusA = statusOrder.indexOf(a.presence) + a.name.toLowerCase(); @@ -671,6 +745,9 @@ playersBox.list = nickList; playersBox.selected = playersBox.list.indexOf(g_SelectedPlayer); + + if (isUpdatedChat) + updateChatWindow(); } /** @@ -889,37 +966,97 @@ } /** + * @param {boolean} joined + * @param {string} gameName + * @param {string} gameState + * @param {boolean} hasBuddies + * @param {string} playerName + * @param {string} playerRating + */ +function gameJoinOpenChatInfo(joined, gameName, gameState, hasBuddies, playerName, playerRating) +{ + addChatMessage({ + "text": sprintf(translate("%(name)s has %(action)s %(game)s."), { + "name": colorPlayerName(playerName, playerRating, + g_Buddies.indexOf(playerName) != -1), + "action": joined ? translate("joined") : translate("opened"), + "game": coloredText((hasBuddies ? g_BuddySymbol + " " : "") + + gameName, g_GameColors[gameState]), + }), + "info": true + }); +} + +/** * Update the game listing from data cached in C++. + * @param {boolean} networked - Update from network. */ -function updateGameList() +function updateGameList(networked) { + let isInitial = !Array.isArray(g_GameList); + let newGameList = Engine.GetGameList(); + + // Catch for initial data, so we can determine between live and initial data. + if (isInitial) + { + // Wait for initial gameList from network (length != 0, gui also calls this functions with empty gamelist). + if (!networked && newGameList.length == 0) + return; + g_GameList = []; + } + let gamesBox = Engine.GetGUIObjectByName("gamesBox"); let sortBy = gamesBox.selected_column; let sortOrder = gamesBox.selected_column_order; + let infoGames = Engine.ConfigDB_GetValue("user", "lobby.chatinfogames"); + let infoGamesJoin = Engine.ConfigDB_GetValue("user", "lobby.chatinfogamesjoin"); + let previousGameList = g_GameList; + if (gamesBox.selected > -1) { g_SelectedGameIP = g_GameList[gamesBox.selected].ip; g_SelectedGamePort = g_GameList[gamesBox.selected].port; } - g_GameList = Engine.GetGameList().map(game => { + let indexPrevious = 0; + + g_GameList = newGameList.map((game, i) => { game.hasBuddies = 0; + let found = restoreAttsFromPrevList(game, indexPrevious, previousGameList, [ [ "new", !isInitial ] ]); + + if (found != -1) + previousGameList[found].exist = true; // Compute average rating of participating players let playerRatings = []; - for (let player of stringifiedTeamListToPlayerData(game.players)) + for (let gamePlayer of stringifiedTeamListToPlayerData(game.players)) { - let playerNickRating = splitRatingFromNick(player.Name); + let playerNickRating = splitRatingFromNick(gamePlayer.Name); - if (player.Team != "observer") + if (gamePlayer.Team != "observer") playerRatings.push(playerNickRating.rating || g_DefaultLobbyRating); // Sort games with playing buddies above games with spectating buddies if (game.hasBuddies < 2 && g_Buddies.indexOf(playerNickRating.nick) != -1) - game.hasBuddies = player.Team == "observer" ? 1 : 2; + game.hasBuddies = gamePlayer.Team == "observer" ? 1 : 2; + + if (!Array.isArray(g_PlayerList)) + continue; + + let player = g_PlayerList.find(playerList => playerList.name == playerNickRating.nick); + if (player && player.games.indexOf(game.name) == -1) + { + player.games.push(game.name); + + if (playerNickRating.nick == game.hostUsername || + infoGamesJoin == "disabled" || infoGamesJoin == "buddies" && !game.hasBuddies) + continue; + + gameJoinOpenChatInfo(true, game.name, game.state, game.hasBuddies, playerNickRating.nick, playerNickRating.rating); + } } game.gameRating = @@ -927,6 +1064,13 @@ Math.round(playerRatings.reduce((sum, current) => sum + current) / playerRatings.length) : g_DefaultLobbyRating; + if (game.new) + { + if (infoGames == "all" || infoGames == "buddies" && game.hasBuddies) + gameJoinOpenChatInfo(false, game.name, game.state, game.hasBuddies, game.hostUsername); + game.new = false; + } + return game; }).filter(game => !filterGame(game)).sort((a, b) => { let sortA, sortB; @@ -960,6 +1104,55 @@ return 0; }); + previousGameList.forEach(gamePrev => { + if (gamePrev.exist === undefined) + { + let teams = {}; + + for (let gamePlayer of stringifiedTeamListToPlayerData(gamePrev.players)) + { + let nick = splitRatingFromNick(gamePlayer.Name).nick; + let player = g_PlayerList.find(playerList => playerList.name == nick); + + if (player) + { + let index = player.games.indexOf(gamePrev.name); + if (index != -1) + player.games.splice(index, 1); + } + + if (Number.isInteger(+gamePlayer.Team)) // !observer + { + let team = +gamePlayer.Team + 1; + if (!teams[team]) + teams[team] = []; + teams[team].push(colorPlayerName(nick, 0, g_Buddies.indexOf(nick) != -1)); + } + } + + if (false && (infoGames == "disabled" || infoGames == "buddies" && !game.hasBuddies)) + return; + + let playersString = Object.keys(teams).map(team => { + if (team == 0) // No Team + return teams[team].join(" vs "); + return teams[team].join(", "); + }).join(" vs "); + + playersString = playersString ? sprintf(translate("(%(playersString)s)"), { + "playersString": playersString }) : ""; + + addChatMessage({ + "text": sprintf(translate("%(game)s has ended. %(players)s"), { + "game": coloredText((gamePrev.hasBuddies ? g_BuddySymbol + " " : "") + + gamePrev.name, g_GameColors[gamePrev.state]), + "players": playersString + }), + "info": true + }); + } + }); + let list_buddy = []; let list_name = []; let list_mapName = []; @@ -1195,7 +1388,7 @@ // To improve performance, only update the playerlist GUI when // the last update in the current stack is processed if (updateList) - updatePlayerList(); + updatePlayerList(true); } /** @@ -1257,6 +1450,29 @@ } /** + * @param {object} msg - Message + * @param {boolean} checkAdded - Check msg already exist at player msgs. Message from history can + * arrive before player exist, when lobby init. + * @param {object} player - Optional player object or it will search in list. + * @return {boolean} - True if added, false else. + */ +function addMsgToPlayer(msg, checkAdded, player) +{ + let playerFrom = player || g_PlayerList && g_PlayerList.find(player => player.name && player.name == msg.from); + + if (playerFrom && + (playerFrom.msgs || (playerFrom.msgs = [])) && + (!checkAdded || playerFrom.msgs.indexOf(msg) == -1)) + { + playerFrom.msgs.push(msg); + msg.playerPresence = playerFrom.presence; + msg.playerRating = playerFrom.rating; + return true; + } + return false; +} + +/** * Process and if appropriate, display a formatted message. * * @param {Object} msg - The message to be processed. @@ -1265,6 +1481,8 @@ { if (msg.from) { + addMsgToPlayer(msg); + if (Engine.LobbyGetPlayerRole(msg.from) == "moderator") msg.from = g_ModeratorPrefix + msg.from; @@ -1278,12 +1496,16 @@ } } - let formatted = ircFormat(msg); - if (!formatted) + if (!ircFormat(msg)) return; - g_ChatMessages.push(formatted); - Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); + g_ChatMessages.push(msg); + updateChatWindow(); +} + +function updateChatWindow() +{ + Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.map(msg => msg.formatted).join("\n"); } /** @@ -1300,15 +1522,28 @@ } /** - * Format text in an IRC-like way. + * Format text in an IRC-like way and add it to msg.formatted. * * @param {Object} msg - Received chat message. - * @returns {string} - Formatted text. + * @returns {boolean} - Text has been formatted. */ function ircFormat(msg) { let formattedMessage = ""; - let coloredFrom = msg.from && colorPlayerName(msg.from); + + let presence = Engine.ConfigDB_GetValue("user", "lobby.chatsenderpresence") == "false" || + !msg.from || + msg.from == "system" ? "available" : msg.playerPresence || "offline"; + + let brightness = g_PlayerStatuses[presence].brightness; + + let coloredFrom = msg.from ? (Engine.ConfigDB_GetValue("user", "lobby.chatsenderpresencedot") == "true" ? + coloredText(" • ", g_PlayerStatuses[presence].color) : "") + coloredText( + msg.from + (msg.playerRating && Engine.ConfigDB_GetValue("user", "lobby.chatsenderrating") == "true" ? + " (" + msg.playerRating + ")" : ""), + setColorBrightness(getPlayerColor( + msg.from), brightness) + ) : ""; // Handle commands allowed past handleChatCommand. if (msg.text[0] == '/') @@ -1367,9 +1602,14 @@ break; } default: - return ""; + return false; } } + else if (msg.info) + formattedMessage = sprintf(translate("%(sender)s: %(message)s"), { + "sender": coloredText("Info", "yellow"), + "message": msg.text + }); else { let senderString; @@ -1392,9 +1632,11 @@ }); } + msg.formatted = coloredText(formattedMessage, setColorBrightness("255 255 255", brightness)); + // Add chat message timestamp if (Engine.ConfigDB_GetValue("user", "chat.timestamp") != "true") - return formattedMessage; + return true; // Translation: Time as shown in the multiplayer lobby (when you enable it in the options page). // For a list of symbols that you can use, see: @@ -1407,10 +1649,15 @@ }); // Translation: IRC message format when there is a time prefix. - return sprintf(translate("%(time)s %(message)s"), { - "time": senderFont(timePrefixString), - "message": formattedMessage - }); + msg.formatted = coloredText( + sprintf(translate("%(time)s %(message)s"), { + "time": senderFont(timePrefixString), + "message": formattedMessage + }), + setColorBrightness("255 255 255", brightness) + ); + + return true; } /** @@ -1441,16 +1688,20 @@ * * @param {string} playername * @param {string} rating + * @param {boolean} isBuddy */ -function colorPlayerName(playername, rating) +function colorPlayerName(playername, rating, isBuddy) { - return coloredText( - (rating ? sprintf( - translate("%(nick)s (%(rating)s)"), { - "nick": playername, - "rating": rating - }) : playername - ), + let name = rating ? sprintf( + translate("%(nick)s (%(rating)s)"), { + "nick": playername, + "rating": rating + }) : playername; + return coloredText(isBuddy ? + sprintf(translate("%(buddy)s %(name)s"), { + "buddy": g_BuddySymbol, + "name": name + }) : name, getPlayerColor(playername.replace(g_ModeratorPrefix, ""))); } Index: binaries/data/mods/public/gui/options/options.json =================================================================== --- binaries/data/mods/public/gui/options/options.json +++ binaries/data/mods/public/gui/options/options.json @@ -423,6 +423,46 @@ "label": "Game Rating Column", "tooltip": "Show the average rating of the participating players in a column of the gamelist.", "config": "lobby.columns.gamerating" + }, + { + "type": "boolean", + "label": "Chat Player Presence", + "tooltip": "Show brightness of messages in chat according to the presence of the sender.", + "config": "lobby.chatsenderpresence" + }, + { + "type": "boolean", + "label": "Chat Player Presence Dot", + "tooltip": "Show a dot at the sender of a message in chat colored to the presence of the sender.", + "config": "lobby.chatsenderpresencedot" + }, + { + "type": "boolean", + "label": "Chat Player Rating", + "tooltip": "Show rating of the player on sender in chat messages.", + "config": "lobby.chatsenderrating" + }, + { + "type": "dropdown", + "label": "Chat Info Games Open Or End", + "tooltip": "Gives an info into the chat, when a new game opens or a game ends.", + "config": "lobby.chatinfogames", + "list": [ + { "value": "disabled", "label": "Disabled" }, + { "value": "buddies", "label": "Buddies" }, + { "value": "all", "label": "Everyone" } + ] + }, + { + "type": "dropdown", + "label": "Chat Info Players Join Games", + "tooltip": "Gives an info into the chat, when a player joins a game.", + "config": "lobby.chatinfogamesjoin", + "list": [ + { "value": "disabled", "label": "Disabled" }, + { "value": "buddies", "label": "Buddies" }, + { "value": "all", "label": "Everyone" } + ] } ] },