Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -372,6 +372,7 @@ room = "arena22" ; Default MUC room to join server = "lobby.wildfiregames.com" ; Address of lobby server xpartamupp = "wfgbot22" ; Name of the server-side xmpp client that manage games +buddies = "," ; Comma separated list of playernames that the current user has marked as buddies [mod] enabledmods = "mod public" 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 @@ -69,6 +69,22 @@ const g_SenderFont = "sans-bold-13"; /** + * Shown in the player and gamelist. + */ +var g_BuddySymbol = '●'; + +/** + * XEP-0172 doesn't restrict nicknames, but our lobby policy does. + * So use this human readable delimiter to separate buddy names in the config file. + */ +var g_BuddyListDelimiter = ","; + +/** + * Array of playernames that the current user has marked as buddies. + */ +var g_Buddies = Engine.ConfigDB_GetValue("user", "lobby.buddies").split(g_BuddyListDelimiter); + +/** * All chat messages received since init (i.e. after lobby join and after returning from a game). */ var g_ChatMessages = []; @@ -382,23 +398,34 @@ if (playersBox.selected > -1) g_SelectedPlayer = playersBox.list[playersBox.selected]; + let buddyStatusList = []; let playerList = []; let presenceList = []; let nickList = []; let ratingList = []; - let cleanPlayerList = Engine.GetPlayerList().sort((a, b) => { + let cleanPlayerList = Engine.GetPlayerList().map(player => { + player.isBuddy = g_Buddies.indexOf(player.name) != -1; + return player; + }).sort((a, b) => { let sortA, sortB; + let statusOrder = Object.keys(g_PlayerStatuses); + let statusA = statusOrder.indexOf(a.presence) + a.name.toLowerCase(); + let statusB = statusOrder.indexOf(b.presence) + b.name.toLowerCase(); + switch (sortBy) { + case 'buddy': + sortA = (a.isBuddy ? 1 : 2) + statusA; + sortB = (b.isBuddy ? 1 : 2) + statusB; + break; case 'rating': sortA = +a.rating; sortB = +b.rating; break; case 'status': - let statusOrder = Object.keys(g_PlayerStatuses); - sortA = statusOrder.indexOf(a.presence); - sortB = statusOrder.indexOf(b.presence); + sortA = statusA; + sortB = statusB; break; case 'name': default: @@ -427,12 +454,14 @@ let coloredPresence = '[color="' + statusColor + '"]' + g_PlayerStatuses[presence].status + "[/color]"; let coloredRating = '[color="' + statusColor + '"]' + rating + "[/color]"; + buddyStatusList.push(player.isBuddy ? '[color="' + statusColor + '"]' + g_BuddySymbol + '[/color]' : ""); playerList.push(coloredName); presenceList.push(coloredPresence); ratingList.push(coloredRating); nickList.push(player.name); } + playersBox.list_buddy = buddyStatusList; playersBox.list_name = playerList; playersBox.list_status = presenceList; playersBox.list_rating = ratingList; @@ -444,6 +473,31 @@ } /** +* Toggle buddy state for a player in playerlist within the user config +*/ +function toggleBuddy() +{ + let playerList = Engine.GetGUIObjectByName("playersBox"); + let name = playerList.list[playerList.selected]; + + if (!name || name == g_Username || name.indexOf(g_BuddyListDelimiter) != -1) + return; + + let index = g_Buddies.indexOf(name); + if (index != -1) + g_Buddies.splice(index, 1); + else + g_Buddies.push(name); + + let buddies = g_Buddies.join(g_BuddyListDelimiter); + Engine.ConfigDB_CreateValue("user", "lobby.buddies", buddies); + Engine.ConfigDB_WriteValueToFile("user", "lobby.buddies", buddies, "config/user.cfg"); + + updatePlayerList(); + updateGameList(); +} + +/** * Select the game listing the selected player when toggling the full games filter. */ function selectGameFromSelectedPlayername() @@ -465,9 +519,7 @@ for (let i = 0; i < g_GameList.length; ++i) for (let player of stringifiedTeamListToPlayerData(g_GameList[i].players)) { - let result = /^(\S+)\ \(\d+\)$/g.exec(player.Name); - let nick = result ? result[1] : player.Name; - + let nick = removeRatingFromNick(player.Name); if (playerName != nick) continue; @@ -617,6 +669,12 @@ leaderboard.selected = -1; } +function removeRatingFromNick(playerName) +{ + let result = /^(\S+)\ \(\d+\)$/g.exec(playerName); + return result ? result[1] : playerName; +} + /** * Update the game listing from data cached in C++. */ @@ -632,7 +690,20 @@ g_SelectedGamePort = g_GameList[gamesBox.selected].port; } - g_GameList = Engine.GetGameList().filter(game => !filterGame(game)).sort((a, b) => { + g_GameList = Engine.GetGameList().map(game => { + game.hasBuddies = 0; + for (let player of stringifiedTeamListToPlayerData(game.players)) + { + let nick = removeRatingFromNick(player.Name); + + // Sort games with playing buddies above games with spectating buddies + if (game.hasBuddies < 2 && g_Buddies.indexOf(nick) != -1) + game.hasBuddies = player.Team == "observer" ? 1 : 2; + } + return game; + }); + + g_GameList = g_GameList.filter(game => !filterGame(game)).sort((a, b) => { let sortA, sortB; switch (sortBy) { @@ -642,6 +713,10 @@ sortA = a[sortBy]; sortB = b[sortBy]; break; + case 'buddy': + sortA = String(b.hasBuddies) + g_GameStatusOrder.indexOf(a.state) + a.name.toLowerCase(); + sortB = String(a.hasBuddies) + g_GameStatusOrder.indexOf(b.state) + b.name.toLowerCase(); + break; case 'mapName': sortA = translate(a.niceMapName); sortB = translate(b.niceMapName); @@ -661,6 +736,7 @@ return 0; }); + let list_buddy = []; let list_name = []; let list_mapName = []; let list_mapSize = []; @@ -679,6 +755,7 @@ if (game.ip == g_SelectedGameIP && game.port == g_SelectedGamePort) selectedGameIndex = +i; + list_buddy.push(game.hasBuddies ? '[color="' + g_GameColors[game.state] + '"]' + g_BuddySymbol + '[/color]' : ""); list_name.push('[color="' + g_GameColors[game.state] + '"]' + gameName); list_mapName.push(translateMapTitle(game.niceMapName)); list_mapSize.push(translateMapSize(game.mapSize)); @@ -688,6 +765,7 @@ list_data.push(i); } + gamesBox.list_buddy = list_buddy; gamesBox.list_name = list_name; gamesBox.list_mapName = list_mapName; gamesBox.list_mapSize = list_mapSize; Index: binaries/data/mods/public/gui/lobby/lobby.xml =================================================================== --- binaries/data/mods/public/gui/lobby/lobby.xml +++ binaries/data/mods/public/gui/lobby/lobby.xml @@ -23,7 +23,7 @@ - + + + + Status - + Name @@ -51,10 +54,11 @@ updatePlayerList(); + toggleBuddy(); - + - - + + + Toggle Buddy + + toggleBuddy(); + + + Leaderboard Engine.SendGetBoardList(); @@ -97,7 +107,7 @@ displayProfile("leaderboard"); - + User Profile Lookup Engine.GetGUIObjectByName("profileFetch").hidden = false; @@ -199,7 +209,10 @@ updateGameSelection(); applyFilters(); joinButton(); - + + + + Name Index: binaries/data/mods/public/gui/session/input.js =================================================================== --- binaries/data/mods/public/gui/session/input.js +++ binaries/data/mods/public/gui/session/input.js @@ -42,10 +42,6 @@ // Number of pixels the mouse can move before the action is considered a drag var maxDragDelta = 4; -// Time in milliseconds in which a double click is recognized -const doubleClickTime = 500; -var doubleClickTimer = 0; -var doubleClicked = false; // Store the previously clicked entity - ensure a double/triple click happens on the same entity var prevClickedEntity = 0; @@ -862,6 +858,8 @@ { dragStart = [ ev.x, ev.y ]; inputState = INPUT_SELECTING; + if (ev.clicks == 1) + prevClickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); return true; } else if (ev.button == SDL_BUTTON_RIGHT) @@ -966,9 +964,12 @@ case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { - var ents = []; - var selectedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); - if (selectedEntity == INVALID_ENTITY) + // If camera following and we select different unit, stop + if (Engine.GetFollowedEntity() != prevClickedEntity) + { + Engine.CameraFollow(0); + } + if (prevClickedEntity == INVALID_ENTITY) { if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) { @@ -978,16 +979,12 @@ inputState = INPUT_NORMAL; return true; } - - var now = new Date(); - - // If camera following and we select different unit, stop - if (Engine.GetFollowedEntity() != selectedEntity) + var ents = []; + if (ev.clicks == 1) { - Engine.CameraFollow(0); + ents = [prevClickedEntity]; } - - if (now.getTime() - doubleClickTimer < doubleClickTime && selectedEntity == prevClickedEntity) + else { // Double click or triple click has occurred var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); @@ -995,39 +992,25 @@ var templateToMatch; // Check for double click or triple click - if (!doubleClicked) + if (ev.clicks == 2) { - // If double click hasn't already occurred, this is a double click. // Select similar units regardless of rank - templateToMatch = GetEntityState(selectedEntity).identity.selectionGroupName; + templateToMatch = GetEntityState(prevClickedEntity).identity.selectionGroupName; if (templateToMatch) matchRank = false; else // No selection group name defined, so fall back to exact match - templateToMatch = GetEntityState(selectedEntity).template; + templateToMatch = GetEntityState(prevClickedEntity).template; - doubleClicked = true; - // Reset the timer so the user has an extra period 'doubleClickTimer' to do a triple-click - doubleClickTimer = now.getTime(); } else - // Double click has already occurred, so this is a triple click. + // triple click. // Select units matching exact template name (same rank) - templateToMatch = GetEntityState(selectedEntity).template; + templateToMatch = GetEntityState(prevClickedEntity).template; // TODO: Should we handle "control all units" here as well? ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false); } - else - { - // It's single click right now but it may become double or triple click - doubleClicked = false; - doubleClickTimer = now.getTime(); - prevClickedEntity = selectedEntity; - - // We only want to include the first picked unit in the selection - ents = [selectedEntity]; - } // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) Index: source/gui/scripting/GuiScriptConversions.cpp =================================================================== --- source/gui/scripting/GuiScriptConversions.cpp +++ source/gui/scripting/GuiScriptConversions.cpp @@ -102,6 +102,7 @@ SET(obj, "state", (int)val.ev.button.state); SET(obj, "x", (int)val.ev.button.x); SET(obj, "y", (int)val.ev.button.y); + SET(obj, "clicks", (int)val.ev.button.clicks); break; } case SDL_HOTKEYDOWN: