Ticket #3383: lobby_coding_convention_v4.patch

File lobby_coding_convention_v4.patch, 49.5 KB (added by elexis, 9 years ago)
  • binaries/data/mods/public/gui/lobby/lobby.js

     
     1var g_MapSizes = {};
     2const g_MapTypesText = [translateWithContext("map", "Skirmish"), translateWithContext("map", "Random"), translate("Scenario")];
     3const g_MapTypes = ["skirmish", "random", "scenario"];
     4
    15var g_ChatMessages = [];
    2 var g_Name = "unknown";
     6const g_ShowTimestamp = Engine.ConfigDB_GetValue("user", "lobby.chattimestamp") == "true";
     7
     8/** This object looks like { "name": [numMessagesSinceReset, lastReset, timeBlocked] } when in use. */
     9var g_SpamMonitor = {};
     10const g_SpamBlockLength = 30;
     11
     12var g_Username = "unknown";
     13var g_UserRating = "";
     14
     15const g_ModPrefix = "@";
     16/** The user roles are defined in XmppClient::GetRoleString */
     17const g_UserRoles = {
     18    "none": {
     19        "color": "192 192 192",
     20        "title": translateWithContext("user role", "None"),
     21    },
     22    "visitor": {
     23        "color": "192 192 192",
     24        "title": translateWithContext("user role", "Visitor"),
     25    },
     26    "participant": {
     27        "color": "192 192 192",
     28        "title": translateWithContext("user role", "Participant"),
     29    },
     30    "moderator": {
     31        "color": "0 125 0",
     32        "title": translateWithContext("user role", "Moderator"),
     33    },
     34    "invalid": {
     35        "color": "192 192 192",
     36        "title": translateWithContext("user role", "Invalid"),
     37    }
     38};
     39
     40const g_PlayerColors = {
     41    "system": "255 0 0",
     42    "@WFGbot": "255 24 24"
     43};
     44const g_SystemColor = "150 0 0";
     45
    346var g_GameList = {}
     47/** Name of the column to be used for sorting. */
    448var g_GameListSortBy = "name";
     49/** Sorting order: +1 ascending, -1 descending. */
     50var g_GameListOrder = 1;
     51/** Games will be sorted according to the order given here. */
     52const g_GameStatusColor = {
     53    "init": "0 125 0",
     54    "waiting": "255 127 0",
     55    "running": "255 0 0",
     56    "unknown": "192 192 192"
     57}
     58
     59/** Name of the column to be used for sorting. */
    560var g_PlayerListSortBy = "name";
    6 var g_GameListOrder = 1; // 1 for ascending sort, and -1 for descending
     61/** Sorting order: +1 ascending, -1 descending. */
    762var g_PlayerListOrder = 1;
    8 var g_specialKey = Math.random();
    9 // This object looks like {"name":[numMessagesSinceReset, lastReset, timeBlocked]} when in use.
    10 var g_spamMonitor = {};
    11 var g_timestamp = Engine.ConfigDB_GetValue("user", "lobby.chattimestamp") == "true";
    12 var g_mapSizes = {};
    13 const g_mapTypesText = [translateWithContext("map", "Skirmish"), translateWithContext("map", "Random"), translate("Scenario")];
    14 const g_mapTypes = ["skirmish", "random", "scenario"];
    15 var g_userRating = ""; // Rating of user, defaults to Unrated
    16 var g_modPrefix = "@";
    17 // Block spammers for 30 seconds.
    18 var SPAM_BLOCK_LENGTH = 30;
     63/**
     64 * If sorted by status, the playerlist will be ordered according to the order given here.
     65 * The possible player statuses are defined in XmppClient::GetPresenceString.
     66 */
     67const g_PlayerStatuses = {
     68    "available": {
     69        "status": translate("Online"),
     70        "color": "0 125 0"
     71    },
     72    "away": {
     73        "status": translate("Away"),
     74        "color": "229 76 13"
     75    },
     76    "gone": {
     77        "status": translate("Gone"),
     78        "color": "229 76 13"
     79    },
     80    "playing": {
     81        "status": translate("Busy"),
     82        "color": "125 0 0"
     83    },
     84    "offline": {
     85        "status": translate("Offline"),
     86        "color": "0 0 0"
     87    },
     88    "unknown": {
     89        "status": translateWithContext("lobby presence", "Unknown"),
     90        "color": "178 178 178"
     91    }
     92}
    1993
    2094////////////////////////////////////////////////////////////////////////////////////////////////
    2195
    2296function init(attribs)
    2397{
    2498    // Play menu music
    2599    initMusic();
    26100    global.music.setState(global.music.states.MENU);
    27101
    28     g_Name = Engine.LobbyGetNick();
     102    g_Username = Engine.LobbyGetNick();
    29103
    30     g_mapSizes = initMapSizes();
    31     g_mapSizes.shortNames.splice(0, 0, translateWithContext("map size", "Any"));
    32     g_mapSizes.tiles.splice(0, 0, "");
     104    g_MapSizes = initMapSizes();
     105    g_MapSizes.shortNames.splice(0, 0, translateWithContext("map size", "Any"));
     106    g_MapSizes.tiles.splice(0, 0, "");
    33107
    34108    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
    35     mapSizeFilter.list = g_mapSizes.shortNames;
    36     mapSizeFilter.list_data = g_mapSizes.tiles;
     109    mapSizeFilter.list = g_MapSizes.shortNames;
     110    mapSizeFilter.list_data = g_MapSizes.tiles;
    37111
    38112    var playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
    39113    playersNumberFilter.list = [translateWithContext("player number", "Any"),2,3,4,5,6,7,8];
    40114    playersNumberFilter.list_data = ["",2,3,4,5,6,7,8];
    41115
    42116    var mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
    43     mapTypeFilter.list = [translateWithContext("map", "Any")].concat(g_mapTypesText);
    44     mapTypeFilter.list_data = [""].concat(g_mapTypes);
     117    mapTypeFilter.list = [translateWithContext("map", "Any")].concat(g_MapTypesText);
     118    mapTypeFilter.list_data = [""].concat(g_MapTypes);
    45119
    46120    Engine.LobbySetPlayerPresence("available");
    47121    Engine.SendGetGameList();
    48122    Engine.SendGetBoardList();
    49123    updatePlayerList();
     
    103177    applyFilters();
    104178}
    105179
    106180function applyFilters()
    107181{
    108     // Update the list of games
    109182    updateGameList();
    110 
    111     // Update info box about the game currently selected
    112183    updateGameSelection();
    113184}
    114185
    115186/**
    116187 * Filter a game based on the status of the filter dropdowns.
     
    118189 * @param game Game to be tested.
    119190 * @return True if game should not be displayed.
    120191 */
    121192function filterGame(game)
    122193{
    123     var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
    124     var playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
    125     var mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
    126     var showFullFilter = Engine.GetGUIObjectByName("showFullFilter");
    127194    // We assume index 0 means display all for any given filter.
     195    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
    128196    if (mapSizeFilter.selected != 0 && game.mapSize != mapSizeFilter.list_data[mapSizeFilter.selected])
    129197        return true;
     198
     199    var playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
    130200    if (playersNumberFilter.selected != 0 && game.tnbp != playersNumberFilter.list_data[playersNumberFilter.selected])
    131201        return true;
     202
     203    var mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
    132204    if (mapTypeFilter.selected != 0 && game.mapType != mapTypeFilter.list_data[mapTypeFilter.selected])
    133205        return true;
     206
     207    var showFullFilter = Engine.GetGUIObjectByName("showFullFilter");
    134208    if (!showFullFilter.checked && game.tnbp <= game.nbp)
    135209        return true;
    136210
    137211    return false;
    138212}
     
    145219function updateSubject(newSubject)
    146220{
    147221    var subject = Engine.GetGUIObjectByName("subject");
    148222    var subjectBox = Engine.GetGUIObjectByName("subjectBox");
    149223    var logo = Engine.GetGUIObjectByName("logo");
     224
    150225    // Load new subject and un-escape newlines.
    151226    subject.caption = newSubject;
     227
    152228    // If the subject is only whitespace, hide it and reposition the logo.
    153229    if (subject.caption.match(/^([\s\t\r\n]*)$/g))
    154230    {
    155231        subjectBox.hidden = true;
    156232        logo.size = "50%-110 50%-50 50%+110 50%+50";
     
    167243 *
    168244 * @return Array containing the player, presence, nickname, and rating listings.
    169245 */
    170246function updatePlayerList()
    171247{
    172     var playersBox = Engine.GetGUIObjectByName("playersBox");
    173     var playerList = [];
    174     var presenceList = [];
    175     var nickList = [];
    176     var ratingList = [];
    177     var cleanPlayerList = Engine.GetPlayerList();
    178248    // Sort the player list, ignoring case.
    179     cleanPlayerList.sort(function(a,b)
     249    var cleanPlayerList = Engine.GetPlayerList().sort((a,b) =>
    180250    {
     251        let statusOrder = Object.keys(g_PlayerStatuses);
     252        let sortA, sortB;
    181253        switch (g_PlayerListSortBy)
    182254        {
    183255        case 'rating':
    184             if (a.rating < b.rating)
    185                 return -g_PlayerListOrder;
    186             else if (a.rating > b.rating)
    187                 return g_PlayerListOrder;
    188             return 0;
     256            sortA = a.rating;
     257            sortB = b.rating;
     258            break;
    189259        case 'status':
    190             let order = ["available", "away", "playing", "gone", "offline"];
    191             let presenceA = order.indexOf(a.presence);
    192             let presenceB = order.indexOf(b.presence);
    193             if (presenceA < presenceB)
    194                 return -g_PlayerListOrder;
    195             else if (presenceA > presenceB)
    196                 return g_PlayerListOrder;
    197             return 0;
     260            sortA = statusOrder.indexOf(a.presence);
     261            sortB = statusOrder.indexOf(b.presence);
     262            break;
    198263        case 'name':
    199264        default:
    200             var aName = a.name.toLowerCase();
    201             var bName = b.name.toLowerCase();
    202             if (aName < bName)
    203                 return -g_PlayerListOrder;
    204             else if (aName > bName)
    205                 return g_PlayerListOrder;
    206             return 0;
     265            sortA = a.name.toLowerCase();
     266            sortB = b.name.toLowerCase();
    207267        }
     268
     269        if (sortA < sortB)
     270            return -g_PlayerListOrder;
     271        else if (sortA > sortB)
     272            return g_PlayerListOrder;
     273        return 0;
    208274    });
    209     for (var i = 0; i < cleanPlayerList.length; i++)
     275
     276    // Create GUI entries
     277    var playerList = [];
     278    var presenceList = [];
     279    var nickList = [];
     280    var ratingList = [];
     281    for (let player of cleanPlayerList)
    210282    {
    211283        // Identify current user's rating.
    212         if (cleanPlayerList[i].name == g_Name && cleanPlayerList[i].rating)
    213             g_userRating = cleanPlayerList[i].rating;
    214         // Add a "-" for unrated players.
    215         if (!cleanPlayerList[i].rating)
    216             cleanPlayerList[i].rating = "-";
    217         // Colorize.
    218         var [name, status, rating] = formatPlayerListEntry(cleanPlayerList[i].name, cleanPlayerList[i].presence, cleanPlayerList[i].rating, cleanPlayerList[i].role);
     284        if (player.name == g_Username && player.rating)
     285            g_UserRating = player.rating;
     286
     287        let [name, status, rating] = formatPlayerListEntry(player.name, player.presence, player.rating, player.role);
     288
    219289        // Push to lists.
    220290        playerList.push(name);
    221291        presenceList.push(status);
    222         nickList.push(cleanPlayerList[i].name);
    223         var ratingSpaces = "  ";
    224         for (var index = 0; index < 4 - Math.ceil(Math.log(cleanPlayerList[i].rating) / Math.LN10); index++)
    225             ratingSpaces += "  ";
    226         ratingList.push(String(ratingSpaces + rating));
     292        ratingList.push(rating);
     293        nickList.push(player.name);
    227294    }
     295
     296    // Push to GUI
     297    var playersBox = Engine.GetGUIObjectByName("playersBox");
    228298    playersBox.list_name = playerList;
    229299    playersBox.list_status = presenceList;
    230300    playersBox.list_rating = ratingList;
    231301    playersBox.list = nickList;
    232302    if (playersBox.selected >= playersBox.list.length)
    233303        playersBox.selected = -1;
    234     return [playerList, presenceList, nickList, ratingList];
    235304}
    236305
    237306/**
    238307 * Display the profile of the selected player.
    239308 * Displays N/A for all stats until updateProfile is called when the stats
    240309 *  are actually received from the bot.
    241  * 
     310 *
    242311 * @param caller From which screen is the user requesting data from?
    243312 */
    244313function displayProfile(caller)
    245314{
    246     var playerList, rating;
     315    var playerList;
    247316    if (caller == "leaderboard")
    248317        playerList = Engine.GetGUIObjectByName("leaderboardBox");
    249318    else if (caller == "lobbylist")
    250319        playerList = Engine.GetGUIObjectByName("playersBox");
    251320    else if (caller == "fetch")
     
    261330        Engine.GetGUIObjectByName("profileArea").hidden = true;
    262331        return;
    263332    }
    264333    Engine.GetGUIObjectByName("profileArea").hidden = false;
    265334
    266     Engine.SendGetProfile(playerList.list[playerList.selected]);   
     335    Engine.SendGetProfile(playerList.list[playerList.selected]);
    267336
    268     var user = playerList.list_name[playerList.selected];
    269337    var role = Engine.LobbyGetPlayerRole(playerList.list[playerList.selected]);
    270     var userList = Engine.GetGUIObjectByName("playersBox");
    271338    if (role && caller == "lobbylist")
    272339    {
    273         // Make the role uppercase.
    274         role = role.charAt(0).toUpperCase() + role.slice(1);
    275         if (role == "Moderator")
    276             role = '[color="0 125 0"]' + translate(role) + '[/color]';
     340        if (!g_UserRoles[role])
     341        {
     342            error("Unknown user role '" + role + "'");
     343            role = "invalid";
     344        }
     345        role = colorize(g_UserRoles[role].color, g_UserRoles[role].title);
    277346    }
    278347    else
    279348        role = "";
    280349
    281     Engine.GetGUIObjectByName("usernameText").caption = user;
    282     Engine.GetGUIObjectByName("roleText").caption = translate(role);
     350    Engine.GetGUIObjectByName("usernameText").caption = playerList.list_name[playerList.selected];
     351    Engine.GetGUIObjectByName("roleText").caption = role;
    283352    Engine.GetGUIObjectByName("rankText").caption = translate("N/A");
    284353    Engine.GetGUIObjectByName("highestRatingText").caption = translate("N/A");
    285354    Engine.GetGUIObjectByName("totalGamesText").caption = translate("N/A");
    286355    Engine.GetGUIObjectByName("winsText").caption = translate("N/A");
    287356    Engine.GetGUIObjectByName("lossesText").caption = translate("N/A");
    288357    Engine.GetGUIObjectByName("ratioText").caption = translate("N/A");
    289358}
    290359
    291360/**
    292361 * Update the profile of the selected player with data from the bot.
    293  *
    294362 */
    295363function updateProfile()
    296364{
    297365    var playerList, user;
    298366    var attributes = Engine.GetProfile();
     
    306374            Engine.GetGUIObjectByName("profileErrorText").hidden = false;
    307375            return;
    308376        }
    309377        Engine.GetGUIObjectByName("profileWindowArea").hidden = false;
    310378        Engine.GetGUIObjectByName("profileErrorText").hidden = true;
    311        
     379
    312380        if (attributes[0].rating != "")
    313             user = sprintf(translate("%(nick)s (%(rating)s)"), { nick: user, rating: attributes[0].rating });
     381            user = sprintf(translate("%(nick)s (%(rating)s)"), { "nick": user, "rating": attributes[0].rating });
    314382
    315383        Engine.GetGUIObjectByName("profileUsernameText").caption = user;
    316384        Engine.GetGUIObjectByName("profileRankText").caption = attributes[0].rank;
    317385        Engine.GetGUIObjectByName("profileHighestRatingText").caption = attributes[0].highestRating;
    318386        Engine.GetGUIObjectByName("profileTotalGamesText").caption = attributes[0].totalGamesPlayed;
    319387        Engine.GetGUIObjectByName("profileWinsText").caption = attributes[0].wins;
    320388        Engine.GetGUIObjectByName("profileLossesText").caption = attributes[0].losses;
    321389
    322390        var winRate = (attributes[0].wins / attributes[0].totalGamesPlayed * 100).toFixed(2);
    323391        if (attributes[0].totalGamesPlayed != 0)
    324             Engine.GetGUIObjectByName("profileRatioText").caption = sprintf(translate("%(percentage)s%%"), { percentage: winRate });
     392            Engine.GetGUIObjectByName("profileRatioText").caption = sprintf(translate("%(percentage)s%%"), { "percentage": winRate });
    325393        else
    326394            Engine.GetGUIObjectByName("profileRatioText").caption = translateWithContext("Used for an undefined winning rate", "-");
    327395        return;
    328396    }
    329397    else if (!Engine.GetGUIObjectByName("leaderboard").hidden)
    330398        playerList = Engine.GetGUIObjectByName("leaderboardBox");
    331399    else
    332400        playerList = Engine.GetGUIObjectByName("playersBox");
    333    
     401
    334402    if (attributes[0].rating == "-2")
    335403        return;
     404
    336405    // Make sure the stats we have received coincide with the selected player.
    337406    if (attributes[0].player != playerList.list[playerList.selected])
    338407        return;
     408
    339409    user = playerList.list_name[playerList.selected];
    340410    if (attributes[0].rating != "")
    341         user = sprintf(translate("%(nick)s (%(rating)s)"), { nick: user, rating: attributes[0].rating });
     411        user = sprintf(translate("%(nick)s (%(rating)s)"), { "nick": user, "rating": attributes[0].rating });
    342412
    343413    Engine.GetGUIObjectByName("usernameText").caption = user;
    344414    Engine.GetGUIObjectByName("rankText").caption = attributes[0].rank;
    345415    Engine.GetGUIObjectByName("highestRatingText").caption = attributes[0].highestRating;
    346416    Engine.GetGUIObjectByName("totalGamesText").caption = attributes[0].totalGamesPlayed;
    347417    Engine.GetGUIObjectByName("winsText").caption = attributes[0].wins;
    348418    Engine.GetGUIObjectByName("lossesText").caption = attributes[0].losses;
    349419
    350420    var winRate = (attributes[0].wins / attributes[0].totalGamesPlayed * 100).toFixed(2);
    351421    if (attributes[0].totalGamesPlayed != 0)
    352         Engine.GetGUIObjectByName("ratioText").caption = sprintf(translate("%(percentage)s%%"), { percentage: winRate });
     422        Engine.GetGUIObjectByName("ratioText").caption = sprintf(translate("%(percentage)s%%"), { "percentage": winRate });
    353423    else
    354424        Engine.GetGUIObjectByName("ratioText").caption = translateWithContext("Used for an undefined winning rate", "-");
    355425}
    356426
    357427/**
    358428 * Update the leaderboard from data cached in C++.
    359429 */
    360430function updateLeaderboard()
    361431{
    362     // Get list from C++
    363     var boardList = Engine.GetBoardList();
    364     // Get GUI leaderboard object
    365     var leaderboard = Engine.GetGUIObjectByName("leaderboardBox");
    366     // Sort list in acending order by rating
    367     boardList.sort(function(a, b) b.rating - a.rating);
     432    // Sort users by rating
     433    var boardList = Engine.GetBoardList().sort((a, b) => b.rating - a.rating);
    368434
     435    // Create GUI entries
    369436    var list = [];
    370437    var list_name = [];
    371438    var list_rank = [];
    372439    var list_rating = [];
    373440
    374     // Push changes
    375     for (var i = 0; i < boardList.length; i++)
     441    for (let i in boardList)
    376442    {
    377443        list_name.push(boardList[i].name);
    378444        list_rating.push(boardList[i].rating);
    379445        list_rank.push(i+1);
    380446        list.push(boardList[i].name);
    381447    }
    382448
     449    // Push to GUI
     450    var leaderboard = Engine.GetGUIObjectByName("leaderboardBox");
    383451    leaderboard.list_name = list_name;
    384452    leaderboard.list_rating = list_rating;
    385453    leaderboard.list_rank = list_rank;
    386454    leaderboard.list = list;
    387455
     
    392460/**
    393461 * Update the game listing from data cached in C++.
    394462 */
    395463function updateGameList()
    396464{
    397     var gamesBox = Engine.GetGUIObjectByName("gamesBox");
    398     var gameList = Engine.GetGameList();
    399     // Store the game whole game list data so that we can access it later
    400     // to update the game info panel.
    401     g_GameList = gameList;
    402 
    403     // Sort the list of games to that games 'waiting' are displayed at the top, followed by 'init', followed by 'running'.
    404     var gameStatuses = ['waiting', 'init', 'running'];
    405     g_GameList.sort(function (a,b) {
     465    var gameStatusOrder = Object.keys(g_GameStatusColor);
     466
     467    g_GameList = Engine.GetGameList().sort((a,b) =>
     468    {
    406469        switch (g_GameListSortBy)
    407470        {
    408471        case 'name':
    409472        case 'mapSize':
    410473            // mapSize contains the number of tiles for random maps
    411474            // scenario maps always display default size
     475
    412476        case 'mapType':
    413477            if (a[g_GameListSortBy] < b[g_GameListSortBy])
    414478                return -g_GameListOrder;
    415479            else if (a[g_GameListSortBy] > b[g_GameListSortBy])
    416480                return g_GameListOrder;
    417481            return 0;
     482
    418483        case 'mapName':
    419484            if (translate(a.niceMapName) < translate(b.niceMapName))
    420485                return -g_GameListOrder;
    421486            else if (translate(a.niceMapName) > translate(b.niceMapName))
    422487                return g_GameListOrder;
    423488            return 0;
     489
    424490        case 'nPlayers':
    425491            // Numerical comparison of player count ratio.
    426492            if (a.nbp * b.tnbp < b.nbp * a.tnbp) // ratio a = a.nbp / a.tnbp, ratio b = b.nbp / b.tnbp
    427493                return -g_GameListOrder;
    428494            else if (a.nbp * b.tnbp > b.nbp * a.tnbp)
    429495                return g_GameListOrder;
    430496            return 0;
     497
    431498        default:
    432             if (gameStatuses.indexOf(a.state) < gameStatuses.indexOf(b.state))
     499            if (gameStatusOrder.indexOf(a.state) < gameStatusOrder.indexOf(b.state))
    433500                return -1;
    434             else if (gameStatuses.indexOf(a.state) > gameStatuses.indexOf(b.state))
     501            else if (gameStatusOrder.indexOf(a.state) > gameStatusOrder.indexOf(b.state))
    435502                return 1;
    436503
    437504            // Alphabetical comparison of names as tiebreaker.
    438505            if (a.name < b.name)
    439506                return -1;
     
    441508                return 1;
    442509            return 0;
    443510        }
    444511    });
    445512
     513    // Create GUI entries
    446514    var list_name = [];
    447515    var list_ip = [];
    448516    var list_mapName = [];
    449517    var list_mapSize = [];
    450518    var list_mapType = [];
    451519    var list_nPlayers = [];
    452520    var list = [];
    453521    var list_data = [];
    454522
    455     var c = 0;
    456     for (var g of gameList)
     523    for (let i in g_GameList)
    457524    {
    458         if (!filterGame(g))
     525        let g = g_GameList[i];
     526
     527        if (filterGame(g))
     528            continue;
     529
     530        if (!g_GameStatusColor[g.state])
    459531        {
    460             // 'waiting' games are highlighted in orange, 'running' in red, and 'init' in green.
    461             let name = escapeText(g.name);
    462             if (g.state == 'init')
    463                 name = '[color="0 125 0"]' + name + '[/color]';
    464             else if (g.state == 'waiting')
    465                 name = '[color="255 127 0"]' + name + '[/color]';
    466             else
    467                 name = '[color="255 0 0"]' + name + '[/color]';
    468             list_name.push(name);
    469             list_ip.push(g.ip);
    470             list_mapName.push(translate(g.niceMapName));
    471             list_mapSize.push(translatedMapSize(g.mapSize));
    472             let idx = g_mapTypes.indexOf(g.mapType);
    473             list_mapType.push(idx != -1 ? g_mapTypesText[idx] : "");
    474             list_nPlayers.push(g.nbp + "/" +g.tnbp);
    475             list.push(name);
    476             list_data.push(c);
     532            error("Unknown game status '" + g.state + "'");
     533            g.state = "unknown";
    477534        }
    478         c++;
     535
     536        let name = colorize(g_GameStatusColor[g.state], escapeText(g.name));
     537
     538        list_name.push(name);
     539        list_ip.push(g.ip);
     540        list_mapName.push(translate(g.niceMapName));
     541        list_mapSize.push(translatedMapSize(g.mapSize));
     542        let idx = g_MapTypes.indexOf(g.mapType);
     543        list_mapType.push(idx != -1 ? g_MapTypesText[idx] : "");
     544        list_nPlayers.push(g.nbp + "/" + g.tnbp);
     545        list.push(name);
     546        list_data.push(i);
    479547    }
    480548
     549    // Push to GUI
     550    var gamesBox = Engine.GetGUIObjectByName("gamesBox");
    481551    gamesBox.list_name = list_name;
    482552    gamesBox.list_mapName = list_mapName;
    483553    gamesBox.list_mapSize = list_mapSize;
    484554    gamesBox.list_mapType = list_mapType;
    485555    gamesBox.list_nPlayers = list_nPlayers;
     
    494564}
    495565
    496566/**
    497567 * Colorize and format the entries in the player list.
    498568 *
    499  * @param nickname Name of player.
    500  * @param presence Presence of player.
    501  * @param rating Rating of player.
    502  * @return Colorized versions of name, status, and rating.
     569 * @return {Array} [name, status, rating] with each item being colorized
    503570 */
    504571function formatPlayerListEntry(nickname, presence, rating)
    505572{
    506     // Set colors based on player status
    507     var color;
    508     var status;
    509     switch (presence)
    510     {
    511     case "playing":
    512         color = "125 0 0";
    513         status = translate("Busy");
    514         break;
    515     case "gone":
    516     case "away":
    517         color = "229 76 13";
    518         status = translate("Away");
    519         break;
    520     case "available":
    521         color = "0 125 0";
    522         status = translate("Online");
    523         break;
    524     case "offline":
    525         color = "0 0 0";
    526         status = translate("Offline");
    527         break;
    528     default:
    529         warn(sprintf("Unknown presence '%(presence)s'", { presence: presence }));
    530         color = "178 178 178";
    531         status = translateWithContext("lobby presence", "Unknown");
    532         break;
     573    if (!g_PlayerStatuses[presence])
     574    {
     575        error("Unknown presence '" + presence + "'");
     576        presence = "unknown";
    533577    }
    534     // Center the unrated symbol.
    535     if (rating == "-")
    536         rating = "    -";
    537     var formattedStatus = '[color="' + color + '"]' + status + "[/color]";
    538     var formattedRating = '[color="' + color + '"]' + rating + "[/color]";
    539     var role = Engine.LobbyGetPlayerRole(nickname);
    540     if (role == "moderator")
    541         nickname = g_modPrefix + nickname;
    542     var formattedName = colorPlayerName(nickname);
    543578
    544     // Push this player's name and status onto the list
    545     return [formattedName, formattedStatus, formattedRating];
     579    // Center the rating (optional placeholder)
     580    if (!rating)
     581        rating = "-";
     582    rating = ("    " + rating).substr(-5);
     583
     584    // Add moderator prefix
     585    if (Engine.LobbyGetPlayerRole(nickname) == "moderator")
     586        nickname = g_ModPrefix + nickname;
     587
     588    // Push colorized name, status and rating to the list
     589    var color = g_PlayerStatuses[presence].color;
     590    return [
     591        colorizePlayername(nickname),
     592        colorize(color, g_PlayerStatuses[presence].status),
     593        colorize(color, rating)
     594    ];
    546595}
    547596
    548597/**
    549598 * Given a map size, returns that map size translated into the current
    550599 * language.
     
    552601function translatedMapSize(mapSize)
    553602{
    554603    if (+mapSize !== +mapSize) // NaN
    555604        return translate(mapSize);
    556605    else
    557         return g_mapSizes.shortNames[g_mapSizes.tiles.indexOf(+mapSize)];
     606        return g_MapSizes.shortNames[g_MapSizes.tiles.indexOf(+mapSize)];
    558607}
    559608
    560609/**
    561610 * Populate the game info area with information on the current game selection.
    562611 */
     
    575624    var mapData;
    576625    var g = Engine.GetGUIObjectByName("gamesBox").list_data[selected];
    577626
    578627    // Load map data
    579628    if (g_GameList[g].mapType == "random" && g_GameList[g].mapName == "random")
    580         mapData = {"settings": {"Description": translate("A randomly selected map.")}};
     629        mapData = { "settings": { "Description": translate("A randomly selected map.") } };
    581630    else if (g_GameList[g].mapType == "random" && Engine.FileExists(g_GameList[g].mapName + ".json"))
    582631        mapData = Engine.ReadJSONFile(g_GameList[g].mapName + ".json");
    583632    else if (Engine.FileExists(g_GameList[g].mapName + ".xml"))
    584633        mapData = Engine.LoadMapSettings(g_GameList[g].mapName + ".xml");
    585634    else
    586         // Warn the player if we can't find the map.
    587         warn(sprintf("Map '%(mapName)s' not found locally.", { mapName: g_GameList[g].mapName }));
     635        warn("Map '" + g_GameList[g].mapName + "' not found locally.");
    588636
    589637    // Show the game info panel and join button.
    590638    Engine.GetGUIObjectByName("gameInfo").hidden = false;
    591639    Engine.GetGUIObjectByName("joinGameButton").hidden = false;
    592640    Engine.GetGUIObjectByName("gameInfoEmpty").hidden = true;
     
    594642    // Display the map name, number of players, the names of the players, the map size and the map type.
    595643    Engine.GetGUIObjectByName("sgMapName").caption = translate(g_GameList[g].niceMapName);
    596644    Engine.GetGUIObjectByName("sgNbPlayers").caption = g_GameList[g].nbp + "/" + g_GameList[g].tnbp;
    597645    Engine.GetGUIObjectByName("sgPlayersNames").caption = g_GameList[g].players;
    598646    Engine.GetGUIObjectByName("sgMapSize").caption = translatedMapSize(g_GameList[g].mapSize);
    599     let idx = g_mapTypes.indexOf(g_GameList[g].mapType);
    600     Engine.GetGUIObjectByName("sgMapType").caption = idx != -1 ? g_mapTypesText[idx] : "";
     647    let idx = g_MapTypes.indexOf(g_GameList[g].mapType);
     648    Engine.GetGUIObjectByName("sgMapType").caption = idx != -1 ? g_MapTypesText[idx] : "";
    601649
    602650    // Display map description if it exists, otherwise display a placeholder.
    603651    if (mapData && mapData.settings.Description)
    604652        var mapDescription = translate(mapData.settings.Description);
    605653    else
     
    619667 * Start the joining process on the currectly selected game.
    620668 */
    621669function joinSelectedGame()
    622670{
    623671    var gamesBox = Engine.GetGUIObjectByName("gamesBox");
    624     if (gamesBox.selected >= 0)
    625     {
    626         var g = gamesBox.list_data[gamesBox.selected];
    627         var sname = g_Name;
    628         var sip = g_GameList[g].ip;
    629 
    630         // TODO: What about valid host names?
    631         // Check if it looks like an ip address
    632         if (sip.split('.').length != 4)
    633         {
    634             addChatMessage({ "from": "system", "text": sprintf(translate("This game's address '%(ip)s' does not appear to be valid."), { ip: sip }) });
    635             return;
    636         }
     672    if (gamesBox.selected < 0)
     673        return;
    637674
    638         // Open Multiplayer connection window with join option.
    639         Engine.PushGuiPage("page_gamesetup_mp.xml", { multiplayerGameType: "join", name: sname, ip: sip, rating: g_userRating });
     675    // TODO: What about valid host names?
     676    // Check if it looks like an ip address
     677    var ip = g_GameList[gamesBox.list_data[gamesBox.selected]].ip;
     678    if (ip.split('.').length != 4)
     679    {
     680        addChatMessage({ "from": "system", "text": sprintf(translate("This game's address '%(ip)s' does not appear to be valid."), { "ip": ip }) });
     681        return;
    640682    }
     683
     684    // Open Multiplayer connection window with join option.
     685    Engine.PushGuiPage("page_gamesetup_mp.xml", {
     686        "multiplayerGameType": "join",
     687        "ip": ip,
     688        "name": g_Username,
     689        "rating": g_UserRating
     690    });
    641691}
    642692
    643693/**
    644694 * Start the hosting process.
    645695 */
    646696function hostGame()
    647697{
    648698    // Open Multiplayer connection window with host option.
    649     Engine.PushGuiPage("page_gamesetup_mp.xml", { multiplayerGameType: "host", name: g_Name, rating: g_userRating });
     699    Engine.PushGuiPage("page_gamesetup_mp.xml", {
     700        "multiplayerGameType": "host",
     701        "name": g_Username,
     702        "rating": g_UserRating
     703    });
    650704}
    651705
    652706////////////////////////////////////////////////////////////////////////////////////////////////
    653707// Utils
    654708////////////////////////////////////////////////////////////////////////////////////////////////
     
    695749            var ratingList = playersBox.list_rating;
    696750            var nickIndex = nickList.indexOf(nick);
    697751            switch(message.level)
    698752            {
    699753            case "join":
    700                 var [name, status, rating] = formatPlayerListEntry(nick, presence, "-");
     754                var [name, status, rating] = formatPlayerListEntry(nick, presence, null);
    701755                playerList.push(name);
    702756                presenceList.push(status);
    703757                nickList.push(nick);
    704758                ratingList.push(String(rating));
    705                 addChatMessage({ "text": "/special " + sprintf(translate("%(nick)s has joined."), { nick: nick }), "key": g_specialKey });
     759                addChatMessage({ "text": "/special " + sprintf(translate("%(nick)s has joined."), { "nick": nick }), "isSpecial": true });
    706760                break;
    707761            case "leave":
    708762                if (nickIndex == -1) // Left, but not present (TODO: warn about this?)
    709763                    break;
    710764                playerList.splice(nickIndex, 1);
    711765                presenceList.splice(nickIndex, 1);
    712766                nickList.splice(nickIndex, 1);
    713767                ratingList.splice(nickIndex, 1);
    714                 addChatMessage({ "text": "/special " + sprintf(translate("%(nick)s has left."), { nick: nick }), "key": g_specialKey });
     768                addChatMessage({ "text": "/special " + sprintf(translate("%(nick)s has left."), { "nick": nick }), "isSpecial": true });
    715769                break;
    716770            case "nick":
    717771                if (nickIndex == -1) // Changed nick, but not present (shouldn't ever happen)
    718772                    break;
    719                 if (!isValidNick(message.data))
    720                 {
    721                     addChatMessage({ "from": "system", "text": sprintf(translate("Invalid nickname: %(nick)s"), { nick: message.data })});
    722                     break;
    723                 }
    724773                var [name, status, rating] = formatPlayerListEntry(message.data, presence, stripColorCodes(ratingList[nickIndex])); // TODO: actually we don't want to change the presence here, so use what was used before
    725774                playerList[nickIndex] = name;
    726775                // presence stays the same
    727776                nickList[nickIndex] = message.data;
    728                 addChatMessage({ "text": "/special " + sprintf(translate("%(oldnick)s is now known as %(newnick)s."), { oldnick: nick, newnick: message.data }), "key": g_specialKey });
     777                addChatMessage({ "text": "/special " + sprintf(translate("%(oldnick)s is now known as %(newnick)s."), { "oldnick": nick, "newnick": message.data }), "isSpecial": true });
    729778                break;
    730779            case "presence":
    731780                if (nickIndex == -1) // Changed presence, but not online (shouldn't ever happen)
    732781                    break;
    733782                var [name, status, rating] = formatPlayerListEntry(nick, presence, stripColorCodes(ratingList[nickIndex]));
     
    737786                break;
    738787            case "subject":
    739788                updateSubject(message.text);
    740789                break;
    741790            default:
    742                 warn(sprintf("Unknown message.level '%(msglvl)s'", { msglvl: message.level }));
    743                 break;
     791                warn("Unknown message.level '" + message.level + "'");
    744792            }
    745793            // Push new data to GUI
    746794            playersBox.list_name = playerList;
    747795            playersBox.list_status = presenceList;
    748796            playersBox.list_rating = ratingList;
    749             playersBox.list = nickList;     
     797            playersBox.list = nickList;
    750798            if (playersBox.selected >= playersBox.list.length)
    751799                playersBox.selected = -1;
    752800            break;
    753801        case "system":
    754802            switch (message.level)
    755803            {
    756804            case "standard":
    757                 addChatMessage({ "from": "system", "text": text, "color": "150 0 0" });
     805                addChatMessage({ "from": "system", "text": text, "color": g_SystemColor });
    758806                if (message.text == "disconnected")
    759807                {
    760808                    // Clear the list of games and the list of players
    761809                    updateGameList();
    762810                    updateLeaderboard();
    763811                    updatePlayerList();
    764812                    // Disable the 'host' button
    765813                    Engine.GetGUIObjectByName("hostButton").enabled = false;
    766814                }
    767815                else if (message.text == "connected")
    768                 {
    769816                    Engine.GetGUIObjectByName("hostButton").enabled = true;
    770                 }
     817
    771818                break;
    772819            case "error":
    773                 addChatMessage({ "from": "system", "text": text, "color": "150 0 0" });
     820                addChatMessage({ "from": "system", "text": text, "color": g_SystemColor });
    774821                break;
    775822            case "internal":
    776823                switch (message.text)
    777824                {
    778825                case "gamelist updated":
     
    790837                }
    791838                break;
    792839            }
    793840            break;
    794841        default:
    795             error(sprintf("Unrecognised message type %(msgtype)s", { msgtype: message.type }));
     842            error("Unrecognised message type '" + message.type + "'");
    796843        }
    797844    }
    798845}
    799846
    800 /* Messages */
     847function returnToMainMenu()
     848{
     849    lobbyStop();
     850    Engine.SwitchGuiPage("page_pregame.xml");
     851}
     852
    801853function submitChatInput()
    802854{
    803855    var input = Engine.GetGUIObjectByName("chatInput");
    804856    var text = input.caption;
    805     if (text.length)
    806     {
    807         if (!handleSpecialCommand(text) && !isSpam(text, g_Name))
    808             Engine.LobbySendMessage(text);
    809         input.caption = "";
    810     }
    811 }
     857    if (!text.length)
     858        return;
    812859
    813 function isValidNick(nick)
    814 {
    815     var prohibitedNicks = ["system"];
    816     return prohibitedNicks.indexOf(nick) == -1;
     860    if (!handleSpecialCommand(text) && !isSpam(text, g_Username))
     861        Engine.LobbySendMessage(text);
     862
     863    input.caption = "";
    817864}
    818865
    819866/**
    820867 * Handle all '/' commands.
    821868 *
     
    825872function handleSpecialCommand(text)
    826873{
    827874    if (text[0] != '/')
    828875        return false;
    829876
    830     var [cmd, nick] = ircSplit(text);
     877    var [cmd, nick] = splitCommand(text);
    831878
    832879    switch (cmd)
    833880    {
    834881    case "away":
    835882        Engine.LobbySetPlayerPresence("away");
     
    843890        break;
    844891    case "ban": // TODO: Split reason from nick and pass it too, for now just support "/ban nick"
    845892        Engine.LobbyBan(nick, "");
    846893        break;
    847894    case "quit":
    848         lobbyStop();
    849         Engine.SwitchGuiPage("page_pregame.xml");
     895        returnToMainMenu();
    850896        break;
    851897    case "say":
    852898    case "me":
    853899        return false;
    854900    default:
    855         addChatMessage({ "from":"system", "text": sprintf(translate("We're sorry, the '%(cmd)s' command is not supported."), { cmd: cmd})});
     901        addChatMessage({ "from":"system", "text": sprintf(translate("We're sorry, the '%(cmd)s' command is not supported."), { "cmd": cmd }) });
    856902    }
     903
    857904    return true;
    858905}
    859906
    860907/**
    861908 * Process and, if appropriate, display a formatted message.
     
    863910 * @param msg The message to be processed.
    864911 */
    865912function addChatMessage(msg)
    866913{
    867914    // Some calls of this function will leave some msg parameters empty. Text is required though.
    868     if (msg.from)
    869     {
    870         // Display the moderator symbol in the chatbox.
    871         var playerRole = Engine.LobbyGetPlayerRole(msg.from);
    872         if (playerRole == "moderator")
    873             msg.from = g_modPrefix + msg.from;
    874     }
    875     else
    876         msg.from = null;
    877     if (!msg.color)
    878         msg.color = null;
    879     if (!msg.key)
    880         msg.key = null;
    881     if (!msg.datetime)
    882         msg.datetime = null;
     915    msg.from = msg.from || "";
     916    msg.color = msg.color || "";
     917    msg.isSpecial = msg.isSpecial || false;
     918    msg.datetime = msg.datetime || "";
     919
     920    // Add moderator prefix
     921    if (Engine.LobbyGetPlayerRole(msg.from) == "moderator")
     922        msg.from = g_ModPrefix + msg.from;
    883923
    884924    // Highlight local user's nick
    885     if (msg.text.indexOf(g_Name) != -1 && g_Name != msg.from)
    886         msg.text = msg.text.replace(new RegExp('\\b' + '\\' + g_Name + '\\b', "g"), colorPlayerName(g_Name));
     925    if (msg.text.indexOf(g_Username) != -1 && g_Username != msg.from)
     926        msg.text = msg.text.replace(new RegExp('\\b' + '\\' + g_Username + '\\b', "g"), colorizePlayername(g_Username));
    887927
    888928    // Run spam test if it's not a historical message
    889929    if (!msg.datetime)
    890930        updateSpamMonitor(msg.from);
     931
    891932    if (isSpam(msg.text, msg.from))
    892933        return;
    893934
    894935    // Format Text
    895     var formatted = ircFormat(msg.text, msg.from, msg.color, msg.key, msg.datetime);
     936    var formatted = formatChatMessage(msg.text, msg.from, msg.color, msg.isSpecial, msg.datetime);
    896937
    897938    // If there is text, add it to the chat box.
    898939    if (formatted)
    899940    {
    900941        g_ChatMessages.push(formatted);
    901942        Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
    902943    }
    903944}
    904945
    905 function ircSplit(string)
     946/**
     947 * Given user input, return the command without the slash and the argument (everything after the first space)
     948 * @returns {Array} [command, nick]
     949 */
     950function splitCommand(string)
    906951{
    907952    var idx = string.indexOf(' ');
    908953    if (idx != -1)
    909954        return [string.substr(1,idx-1), string.substr(idx+1)];
    910955    return [string.substr(1), ""];
    911956}
    912957
    913958/**
    914959 * Format text in an IRC-like way.
    915960 *
    916  * @param text Body of the message.
    917  * @param from Sender of the message.
    918  * @param color Optional color of sender.
    919  * @param key Key to verify join/leave messages with. TODO: Remove this, it only provides synthetic security.
    920  * @param datetime Current date and time of message, only used for historical messages
     961 * @param text {string} Body of the message.
     962 * @param from {string} Sender of the message.
     963 * @param color {string} Optional color of sender.
     964 * @param isSpecial {Boolean} Whether or not this message is a system message.
     965 * @param datetime {string} Current date and time of message, only used for historical messages
    921966 * @return Formatted text.
    922967 */
    923 function ircFormat(text, from, color, key, datetime)
     968function formatChatMessage(text, from, color, isSpecial, datetime)
    924969{
    925     // Generate and apply color to uncolored names,
    926     if (!color && from)
    927         var coloredFrom = colorPlayerName(from);
    928     else if (color && from)
    929         var coloredFrom = '[color="' + color + '"]' + from + "[/color]";
     970    if (from)
     971        var coloredFrom = color ? colorize(color, from) : colorizePlayername(from);
    930972
    931973    // Handle commands allowed past handleSpecialCommand.
    932974    if (text[0] == '/')
    933975    {
    934         var [command, message] = ircSplit(text);
     976        var [command, message] = splitCommand(text);
    935977        switch (command)
    936978        {
    937979            case "me":
    938980                // Translation: IRC message prefix when the sender uses the /me command.
    939                 var senderString = '[font="sans-bold-13"]' + sprintf(translate("* %(sender)s"), { sender: coloredFrom }) + '[/font]';
     981                var senderString = '[font="sans-bold-13"]' + sprintf(translate("* %(sender)s"), { "sender": coloredFrom }) + '[/font]';
    940982                // Translation: IRC message issued using the ‘/me’ command.
    941                 var formattedMessage = sprintf(translate("%(sender)s %(action)s"), { sender: senderString, action: message });
     983                var formattedMessage = sprintf(translate("%(sender)s %(action)s"), { "sender": senderString, "action": message });
    942984                break;
     985
    943986            case "say":
    944987                // Translation: IRC message prefix.
    945                 var senderString = '[font="sans-bold-13"]' + sprintf(translate("<%(sender)s>"), { sender: coloredFrom }) + '[/font]';
     988                var senderString = '[font="sans-bold-13"]' + sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }) + '[/font]';
    946989                // Translation: IRC message.
    947                 var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { sender: senderString, message: message });
     990                var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderString, "message": message });
    948991                break
     992
    949993            case "special":
    950                 if (key === g_specialKey)
     994                if (isSpecial)
    951995                    // Translation: IRC system message.
    952                     var formattedMessage = '[font="sans-bold-13"]' + sprintf(translate("== %(message)s"), { message: message }) + '[/font]';
     996                    var formattedMessage = '[font="sans-bold-13"]' + sprintf(translate("== %(message)s"), { "message": message }) + '[/font]';
    953997                else
    954998                {
    955999                    // Translation: IRC message prefix.
    956                     var senderString = '[font="sans-bold-13"]' + sprintf(translate("<%(sender)s>"), { sender: coloredFrom }) + '[/font]';
     1000                    var senderString = '[font="sans-bold-13"]' + sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }) + '[/font]';
    9571001                    // Translation: IRC message.
    958                     var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { sender: senderString, message: message });
     1002                    var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderString, "message": message });
    9591003                }
    9601004                break;
     1005
    9611006            default:
    9621007                // This should never happen.
    9631008                var formattedMessage = "";
     1009                break;
    9641010        }
    9651011    }
    9661012    else
    9671013    {
    9681014        // Translation: IRC message prefix.
    969         var senderString = '[font="sans-bold-13"]' + sprintf(translate("<%(sender)s>"), { sender: coloredFrom }) + '[/font]';
     1015        var senderString = '[font="sans-bold-13"]' + sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }) + '[/font]';
    9701016        // Translation: IRC message.
    971         var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { sender: senderString, message: text });
     1017        var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderString, "message": text });
    9721018    }
    9731019
    9741020    // Build time header if enabled
    975     if (g_timestamp)
    976     {
    977 
    978         var time;
    979         if (datetime)
    980         {
    981             var parserDate = datetime.split("T")[0].split("-");
    982             var parserTime = datetime.split("T")[1].split(":");
    983             // See http://xmpp.org/extensions/xep-0082.html#sect-idp285136 for format of datetime
    984             // Date takes Year, Month, Day, Hour, Minute, Second
    985             time = new Date(Date.UTC(parserDate[0], parserDate[1], parserDate[2], parserTime[0], parserTime[1], parserTime[2].split("Z")[0]));
    986         }
    987         else
    988             time = new Date(Date.now());
    989 
    990         // Translation: Time as shown in the multiplayer lobby (when you enable it in the options page).
    991         // For a list of symbols that you can use, see:
    992         // https://sites.google.com/site/icuprojectuserguide/formatparse/datetime?pli=1#TOC-Date-Field-Symbol-Table
    993         var timeString = Engine.FormatMillisecondsIntoDateString(time.getTime(), translate("HH:mm"));
     1021    if (g_ShowTimestamp)
     1022        return prefixTimestamp(formattedMessage, datetime);
    9941023
    995         // Translation: Time prefix as shown in the multiplayer lobby (when you enable it in the options page).
    996         var timePrefixString = '[font="sans-bold-13"]' + sprintf(translate("\\[%(time)s]"), { time: timeString }) + '[/font]';
     1024    return formattedMessage;
     1025}
    9971026
    998         // Translation: IRC message format when there is a time prefix.
    999         return sprintf(translate("%(time)s %(message)s"), { time: timePrefixString, message: formattedMessage });
     1027function prefixTimestamp(formattedMessage, datetime)
     1028{
     1029    var time;
     1030    if (datetime)
     1031    {
     1032        var parserDate = datetime.split("T")[0].split("-");
     1033        var parserTime = datetime.split("T")[1].split(":");
     1034        // See http://xmpp.org/extensions/xep-0082.html#sect-idp285136 for format of datetime
     1035        // Date takes Year, Month, Day, Hour, Minute, Second
     1036        time = new Date(Date.UTC(parserDate[0], parserDate[1], parserDate[2], parserTime[0], parserTime[1], parserTime[2].split("Z")[0]));
    10001037    }
    10011038    else
    1002         return formattedMessage;
     1039        time = new Date(Date.now());
     1040
     1041    // Translation: Time as shown in the multiplayer lobby (when you enable it in the options page).
     1042    // For a list of symbols that you can use, see:
     1043    // https://sites.google.com/site/icuprojectuserguide/formatparse/datetime?pli=1#TOC-Date-Field-Symbol-Table
     1044    var timeString = Engine.FormatMillisecondsIntoDateString(time.getTime(), translate("HH:mm"));
     1045
     1046    // Translation: Time prefix as shown in the multiplayer lobby (when you enable it in the options page).
     1047    var timePrefixString = '[font="sans-bold-13"]' + sprintf(translate("\\[%(time)s]"), { "time": timeString }) + '[/font]';
     1048
     1049    // Translation: IRC message format when there is a time prefix.
     1050    return sprintf(translate("%(time)s %(message)s"), { "time": timePrefixString, "message": formattedMessage });
    10031051}
    10041052
    10051053/**
    10061054 * Update the spam monitor.
    10071055 *
     
    10111059{
    10121060    // Integer time in seconds.
    10131061    var time = Math.floor(Date.now() / 1000);
    10141062
    10151063    // Update or initialize the user in the spam monitor.
    1016     if (g_spamMonitor[from])
    1017         g_spamMonitor[from][0]++;
     1064    if (g_SpamMonitor[from])
     1065        ++g_SpamMonitor[from][0];
    10181066    else
    1019         g_spamMonitor[from] = [1, time, 0];
     1067        g_SpamMonitor[from] = [1, time, 0];
    10201068}
    10211069
    10221070/**
    10231071 * Check if a message is spam.
    10241072 *
     
    10301078{
    10311079    // Integer time in seconds.
    10321080    var time = Math.floor(Date.now() / 1000);
    10331081
    10341082    // Initialize if not already in the database.
    1035     if (!g_spamMonitor[from])
    1036         g_spamMonitor[from] = [1, time, 0];
     1083    if (!g_SpamMonitor[from])
     1084        g_SpamMonitor[from] = [1, time, 0];
    10371085
    10381086    // Block blank lines.
    10391087    if (text == " ")
    10401088        return true;
     1089
    10411090    // Block users who are still within their spam block period.
    1042     else if (g_spamMonitor[from][2] + SPAM_BLOCK_LENGTH >= time)
     1091    if (g_SpamMonitor[from][2] + g_SpamBlockLength >= time)
    10431092        return true;
     1093
    10441094    // Block users who exceed the rate of 1 message per second for five seconds and are not already blocked. TODO: Make this smarter and block profanity.
    1045     else if (g_spamMonitor[from][0] == 6)
     1095    if (g_SpamMonitor[from][0] == 6)
    10461096    {
    1047         g_spamMonitor[from][2] = time;
    1048         if (from == g_Name)
     1097        g_SpamMonitor[from][2] = time;
     1098        if (from == g_Username)
    10491099            addChatMessage({ "from": "system", "text": translate("Please do not spam. You have been blocked for thirty seconds.") });
    10501100        return true;
    10511101    }
     1102
    10521103    // Return false if everything is clear.
    1053     else
    1054         return false;
     1104    return false;
    10551105}
    10561106
    10571107/**
    10581108 * Reset timer used to measure message send speed.
    10591109 */
     
    10611111{
    10621112    // Integer time in seconds.
    10631113    var time = Math.floor(Date.now() / 1000);
    10641114
    10651115    // Clear message count every 5 seconds.
    1066     for each (var stats in g_spamMonitor)
     1116    for (let i in g_SpamMonitor)
    10671117    {
    1068         if (stats[1] + 5 <= time)
     1118        if (g_SpamMonitor[i][1] + 5 <= time)
    10691119        {
    1070             stats[1] = time;
    1071             stats[0] = 0;
     1120            g_SpamMonitor[i][1] = time;
     1121            g_SpamMonitor[i][0] = 0;
    10721122        }
    10731123    }
     1124}
    10741125
     1126function colorize(color, text)
     1127{
     1128    return '[color="' + color + '"]' + text + "[/color]";
     1129}
     1130
     1131function colorizePlayername(playername)
     1132{
     1133    return colorize(g_PlayerColors[playername] ? g_PlayerColors[playername] : getPlayerColor(playername.replace(g_ModPrefix, "")), playername);
    10751134}
    10761135
    1077 /* Utilities */
    1078 // Generate a (mostly) unique color for this player based on their name.
    1079 // See http://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-jquery-javascript
     1136/**
     1137 * Generate a (mostly) unique color for this player based on their name.
     1138 * See http://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-jquery-javascript
     1139 */
    10801140function getPlayerColor(playername)
    10811141{
    10821142    // Generate a probably-unique hash for the player name and use that to create a color.
    10831143    var hash = 0;
    1084     for (var i = 0; i < playername.length; i++)
     1144    for (let i in playername)
    10851145        hash = playername.charCodeAt(i) + ((hash << 5) - hash);
    10861146
    10871147    // First create the color in RGB then HSL, clamp the lightness so it's not too dark to read, and then convert back to RGB to display.
    10881148    // The reason for this roundabout method is this algorithm can generate values from 0 to 255 for RGB but only 0 to 100 for HSL; this gives
    10891149    // us much more variety if we generate in RGB. Unfortunately, enforcing that RGB values are a certain lightness is very difficult, so
    10901150    // we convert to HSL to do the computation. Since our GUI code only displays RGB colors, we have to convert back.
    10911151    var [h, s, l] = rgbToHsl(hash >> 24 & 0xFF, hash >> 16 & 0xFF, hash >> 8 & 0xFF);
    10921152    return hslToRgb(h, s, Math.max(0.4, l)).join(" ");
    10931153}
    10941154
    1095 function repeatString(times, string) {
    1096     return Array(times + 1).join(string);
    1097 }
    1098 
    1099 // Some names are special and should always appear in certain colors.
    1100 var fixedColors = { "system": repeatString(7, "255.0.0."), "@WFGbot": repeatString(7, "255.24.24."),
    1101                     "pyrogenesis": repeatString(2, "97.0.0.") + repeatString(2, "124.0.0.") + "138.0.0." +
    1102                         repeatString(2, "174.0.0.") + repeatString(2, "229.40.0.") + repeatString(2, "243.125.15.") };
    1103 function colorPlayerName(playername)
    1104 {
    1105     var color = fixedColors[playername];
    1106     if (color) {
    1107     color = color.split(".");
    1108     return ('[color="' + playername.split("").map(function (c, i) color.slice(i * 3, i * 3 + 3).join(" ") + '"]' + c + '[/color][color="')
    1109                 .join("") + '"]').slice(0, -10);
    1110     }
    1111     return '[color="' + getPlayerColor(playername.replace(g_modPrefix, "")) + '"]' + playername + '[/color]';
    1112 }
    1113 
    1114 // Ensure `value` is between 0 and 1.
     1155/**
     1156 * Ensure `value` is between 0 and 1.
     1157 */
    11151158function clampColorValue(value)
    11161159{
    1117     return Math.abs(1 - Math.abs(value - 1));
     1160    return Math.max(0, Math.min(value, 1));
    11181161}
    11191162
    1120 // See http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
     1163/**
     1164 * See http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
     1165 */
    11211166function rgbToHsl(r, g, b)
    11221167{
    11231168    r /= 255;
    11241169    g /= 255;
    11251170    b /= 255;
     
    11671212        r = hue2rgb(p, q, h + 1/3);
    11681213        g = hue2rgb(p, q, h);
    11691214        b = hue2rgb(p, q, h - 1/3);
    11701215    }
    11711216
    1172     return [r, g, b].map(function (n) Math.round(n * 255));
     1217    return [r, g, b].map(n => Math.round(n * 255));
    11731218}
    1174 
    1175 (function () {
    1176 function hexToRgb(hex) {
    1177     return parseInt(hex.slice(0, 2), 16) + "." + parseInt(hex.slice(2, 4), 16) + "." + parseInt(hex.slice(4, 6), 16) + ".";
    1178 }
    1179 function r(times, hex) {
    1180     return repeatString(times, hexToRgb(hex));
    1181 }
    1182 
    1183 fixedColors["Twilight_Sparkle"] = r(2, "d19fe3") + r(2, "b689c8") + r(2, "a76bc2") +
    1184     r(4, "263773") + r(2, "131f46") + r(2, "662d8a") + r(2, "ed438a");
    1185 fixedColors["Applejack"] = r(3, "ffc261") + r(3, "efb05d") + r(3, "f26f31");
    1186 fixedColors["Rarity"] = r(1, "ebeff1") + r(1, "dee3e4") + r(1, "bec2c3") +
    1187     r(1, "83509f") + r(1, "4b2568") + r(1, "4917d6");
    1188 fixedColors["Rainbow_Dash"] = r(2, "ee4144") + r(1, "f37033") + r(1, "fdf6af") +
    1189     r(1, "62bc4d") + r(1, "1e98d3") + r(2, "672f89") + r(1, "9edbf9") +
    1190     r(1, "88c4eb") + r(1, "77b0e0") + r(1, "1e98d3");
    1191 fixedColors["Pinkie_Pie"] = r(2, "f3b6cf") + r(2, "ec9dc4") + r(4, "eb81b4") +
    1192     r(1, "ed458b") + r(1, "be1d77");
    1193 fixedColors["Fluttershy"] = r(2, "fdf6af") + r(2, "fee78f") + r(2, "ead463") +
    1194     r(2, "f3b6cf") + r(2, "eb81b4");
    1195 fixedColors["Sweetie_Belle"] = r(2, "efedee") + r(3, "e2dee3") + r(3, "cfc8d1") +
    1196     r(2, "b28dc0") + r(2, "f6b8d2") + r(1, "795b8a");
    1197 fixedColors["Apple_Bloom"] = r(2, "f4f49b") + r(2, "e7e793") + r(2, "dac582") +
    1198     r(2, "f46091") + r(2, "f8415f") + r(1, "c52451");
    1199 fixedColors["Scootaloo"] = r(2, "fbba64") + r(2, "f2ab56") + r(2, "f37003") +
    1200     r(2, "bf5d95") + r(1, "bf1f79");
    1201 fixedColors["Luna"] = r(1, "7ca7fa") + r(1, "5d6fc1") + r(1, "656cb9") + r(1, "393993");
    1202 fixedColors["Celestia"] = r(1, "fdfafc") + r(1, "f7eaf2") + r(1, "d99ec5") +
    1203     r(1, "00aec5") + r(1, "f7c6dc") + r(1, "98d9ef") + r(1, "ced7ed") + r(1, "fed17b");
    1204 })();
  • binaries/data/mods/public/gui/lobby/lobby.xml

     
    161161                </action>
    162162            </object>
    163163
    164164            <object type="button" style="ModernButtonRed" size="0 100%-25 100% 100%">
    165165                <translatableAttribute id="caption">Main Menu</translatableAttribute>
    166                 <action on="Press">
    167                     lobbyStop();
    168                     Engine.SwitchGuiPage("page_pregame.xml");
    169                 </action>
     166                <action on="Press">returnToMainMenu();</action>
    170167            </object>
    171168        </object>
    172169
    173170        <!-- Middle panel: Filters, game list, chat box. -->
    174171        <object name="middlePanel" size="20%+5 5% 100%-255 97.2%">
  • binaries/data/mods/public/gui/session/messages.js

     
    366366        return;
    367367
    368368    if (Engine.GetGUIObjectByName("toggleTeamChat").checked)
    369369        text = "/team " + text;
    370370
    371     if (g_IsNetworked)
    372         Engine.SendNetworkChat(text);
    373     else
    374         addChatMessage({ "type": "message", "guid": "local", "text": text });
     371    submitChatDirectly(text);
    375372}
    376373
    377374function addChatMessage(msg)
    378375{
    379376    var playerColor, username;