Ticket #3383: lobby_coding_convention_v3.patch

File lobby_coding_convention_v3.patch, 50.1 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
     15/** The user is not allowed to change the nick to any of these names. */
     16var g_ProhibitedNicks = ["system"];
     17
     18const g_ModPrefix = "@";
     19/** The user roles are defined in XmppClient::GetRoleString */
     20const g_UserRoles =
     21{
     22    "none":
     23    {
     24        "color": "192 192 192",
     25        "title": translateWithContext("user role", "None"),
     26    },
     27    "visitor":
     28    {
     29        "color": "192 192 192",
     30        "title": translateWithContext("user role", "Visitor"),
     31    },
     32    "participant":
     33    {
     34        "color": "192 192 192",
     35        "title": translateWithContext("user role", "Participant"),
     36    },
     37    "moderator":
     38    {
     39        "color": "0 125 0",
     40        "title": translateWithContext("user role", "Moderator"),
     41    },
     42    "invalid":
     43    {
     44        "color": "192 192 192",
     45        "title": translateWithContext("user role", "Invalid"),
     46    }
     47};
     48
     49const g_UserFont = "sans-bold-13";
     50const g_PlayerColors = {
     51    "system": "255 0 0",
     52    "@WFGbot": "255 24 24"
     53};
     54const g_SystemColor = "150 0 0";
     55
    356var g_GameList = {}
     57/** Name of the column to be used for sorting. */
    458var g_GameListSortBy = "name";
     59/** Sorting order: +1 ascending, -1 descending. */
     60var g_GameListOrder = 1;
     61/** Games will be sorted according to the order given here. */
     62const g_GameStatusColor =
     63{
     64    "init": "0 125 0",
     65    "waiting": "255 127 0",
     66    "running": "255 0 0",
     67    "unknown": "192 192 192"
     68}
     69
     70/** Name of the column to be used for sorting. */
    571var g_PlayerListSortBy = "name";
    6 var g_GameListOrder = 1; // 1 for ascending sort, and -1 for descending
     72/** Sorting order: +1 ascending, -1 descending. */
    773var 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;
     74/** If sorted by status, the playerlist will be ordered according to the order given here. */
     75const g_PlayerStatuses =
     76{
     77    "available":
     78    {
     79        "status": translate("Online"),
     80        "color": "0 125 0"
     81    },
     82    "away":
     83    {
     84        "status": translate("Away"),
     85        "color": "229 76 13"
     86    },
     87    //TODO: can this status actually occur?
     88    "gone":
     89    {
     90        "status": translate("Gone"),
     91        "color": "229 76 13"
     92    },
     93    "playing":
     94    {
     95        "status": translate("Busy"),
     96        "color": "125 0 0"
     97    },
     98    "offline":
     99    {
     100        "status": translate("Offline"),
     101        "color": "0 0 0"
     102    },
     103    "unknown":
     104    {
     105        "status": translateWithContext("lobby presence", "Unknown"),
     106        "color": "178 178 178"
     107    }
     108}
    19109
    20110////////////////////////////////////////////////////////////////////////////////////////////////
    21111
    22112function init(attribs)
    23113{
    24114    // Play menu music
    25115    initMusic();
    26116    global.music.setState(global.music.states.MENU);
    27117
    28     g_Name = Engine.LobbyGetNick();
     118    g_Username = Engine.LobbyGetNick();
    29119
    30     g_mapSizes = initMapSizes();
    31     g_mapSizes.shortNames.splice(0, 0, translateWithContext("map size", "Any"));
    32     g_mapSizes.tiles.splice(0, 0, "");
     120    g_MapSizes = initMapSizes();
     121    g_MapSizes.shortNames.splice(0, 0, translateWithContext("map size", "Any"));
     122    g_MapSizes.tiles.splice(0, 0, "");
    33123
    34124    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
    35     mapSizeFilter.list = g_mapSizes.shortNames;
    36     mapSizeFilter.list_data = g_mapSizes.tiles;
     125    mapSizeFilter.list = g_MapSizes.shortNames;
     126    mapSizeFilter.list_data = g_MapSizes.tiles;
    37127
    38128    var playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
    39129    playersNumberFilter.list = [translateWithContext("player number", "Any"),2,3,4,5,6,7,8];
    40130    playersNumberFilter.list_data = ["",2,3,4,5,6,7,8];
    41131
    42132    var mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
    43     mapTypeFilter.list = [translateWithContext("map", "Any")].concat(g_mapTypesText);
    44     mapTypeFilter.list_data = [""].concat(g_mapTypes);
     133    mapTypeFilter.list = [translateWithContext("map", "Any")].concat(g_MapTypesText);
     134    mapTypeFilter.list_data = [""].concat(g_MapTypes);
    45135
    46136    Engine.LobbySetPlayerPresence("available");
    47137    Engine.SendGetGameList();
    48138    Engine.SendGetBoardList();
    49139    updatePlayerList();
     
    103193    applyFilters();
    104194}
    105195
    106196function applyFilters()
    107197{
    108     // Update the list of games
    109198    updateGameList();
    110 
    111     // Update info box about the game currently selected
    112199    updateGameSelection();
    113200}
    114201
    115202/**
    116203 * Filter a game based on the status of the filter dropdowns.
     
    118205 * @param game Game to be tested.
    119206 * @return True if game should not be displayed.
    120207 */
    121208function filterGame(game)
    122209{
    123     var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
    124     var playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
    125     var mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
    126     var showFullFilter = Engine.GetGUIObjectByName("showFullFilter");
    127210    // We assume index 0 means display all for any given filter.
     211    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
    128212    if (mapSizeFilter.selected != 0 && game.mapSize != mapSizeFilter.list_data[mapSizeFilter.selected])
    129213        return true;
     214
     215    var playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
    130216    if (playersNumberFilter.selected != 0 && game.tnbp != playersNumberFilter.list_data[playersNumberFilter.selected])
    131217        return true;
     218
     219    var mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
    132220    if (mapTypeFilter.selected != 0 && game.mapType != mapTypeFilter.list_data[mapTypeFilter.selected])
    133221        return true;
     222
     223    var showFullFilter = Engine.GetGUIObjectByName("showFullFilter");
    134224    if (!showFullFilter.checked && game.tnbp <= game.nbp)
    135225        return true;
    136226
    137227    return false;
    138228}
     
    145235function updateSubject(newSubject)
    146236{
    147237    var subject = Engine.GetGUIObjectByName("subject");
    148238    var subjectBox = Engine.GetGUIObjectByName("subjectBox");
    149239    var logo = Engine.GetGUIObjectByName("logo");
     240
    150241    // Load new subject and un-escape newlines.
    151242    subject.caption = newSubject;
     243
    152244    // If the subject is only whitespace, hide it and reposition the logo.
    153245    if (subject.caption.match(/^([\s\t\r\n]*)$/g))
    154246    {
    155247        subjectBox.hidden = true;
    156248        logo.size = "50%-110 50%-50 50%+110 50%+50";
     
    167259 *
    168260 * @return Array containing the player, presence, nickname, and rating listings.
    169261 */
    170262function updatePlayerList()
    171263{
    172     var playersBox = Engine.GetGUIObjectByName("playersBox");
    173     var playerList = [];
    174     var presenceList = [];
    175     var nickList = [];
    176     var ratingList = [];
    177     var cleanPlayerList = Engine.GetPlayerList();
    178264    // Sort the player list, ignoring case.
    179     cleanPlayerList.sort(function(a,b)
     265    var cleanPlayerList = Engine.GetPlayerList().sort((a,b) =>
    180266    {
     267        let statusOrder = Object.keys(g_PlayerStatuses);
     268        let sortA, sortB;
    181269        switch (g_PlayerListSortBy)
    182270        {
    183271        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;
     272            sortA = a.rating;
     273            sortB = b.rating;
     274            break;
    189275        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;
     276            sortA = statusOrder.indexOf(a.presence);
     277            sortB = statusOrder.indexOf(b.presence);
     278            break;
    198279        case 'name':
    199280        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;
     281            sortA = a.name.toLowerCase();
     282            sortB = b.name.toLowerCase();
    207283        }
     284
     285        if (sortA < sortB)
     286            return -g_PlayerListOrder;
     287        else if (sortA > sortB)
     288            return g_PlayerListOrder;
     289        return 0;
    208290    });
    209     for (var i = 0; i < cleanPlayerList.length; i++)
     291
     292    // Create GUI entries
     293    var playerList = [];
     294    var presenceList = [];
     295    var nickList = [];
     296    var ratingList = [];
     297    for (let player of cleanPlayerList)
    210298    {
    211299        // 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);
     300        if (player.name == g_Username && player.rating)
     301            g_UserRating = player.rating;
     302
     303        let [name, status, rating] = formatPlayerListEntry(player.name, player.presence, player.rating, player.role);
     304
    219305        // Push to lists.
    220306        playerList.push(name);
    221307        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));
     308        ratingList.push(rating);
     309        nickList.push(player.name);
    227310    }
     311
     312    // Push to GUI
     313    var playersBox = Engine.GetGUIObjectByName("playersBox");
    228314    playersBox.list_name = playerList;
    229315    playersBox.list_status = presenceList;
    230316    playersBox.list_rating = ratingList;
    231317    playersBox.list = nickList;
    232318    if (playersBox.selected >= playersBox.list.length)
    233319        playersBox.selected = -1;
    234     return [playerList, presenceList, nickList, ratingList];
    235320}
    236321
    237322/**
    238323 * Display the profile of the selected player.
    239324 * Displays N/A for all stats until updateProfile is called when the stats
    240325 *  are actually received from the bot.
    241  * 
     326 *
    242327 * @param caller From which screen is the user requesting data from?
    243328 */
    244329function displayProfile(caller)
    245330{
    246     var playerList, rating;
     331    var playerList;
    247332    if (caller == "leaderboard")
    248333        playerList = Engine.GetGUIObjectByName("leaderboardBox");
    249334    else if (caller == "lobbylist")
    250335        playerList = Engine.GetGUIObjectByName("playersBox");
    251336    else if (caller == "fetch")
     
    261346        Engine.GetGUIObjectByName("profileArea").hidden = true;
    262347        return;
    263348    }
    264349    Engine.GetGUIObjectByName("profileArea").hidden = false;
    265350
    266     Engine.SendGetProfile(playerList.list[playerList.selected]);   
     351    Engine.SendGetProfile(playerList.list[playerList.selected]);
    267352
    268     var user = playerList.list_name[playerList.selected];
    269353    var role = Engine.LobbyGetPlayerRole(playerList.list[playerList.selected]);
    270     var userList = Engine.GetGUIObjectByName("playersBox");
    271354    if (role && caller == "lobbylist")
    272355    {
    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]';
     356        if (!g_UserRoles[role])
     357        {
     358            error("Unknown user role '" + role + "'");
     359            role = "invalid";
     360        }
     361        role = colorize(g_UserRoles[role].color, g_UserRoles[role].title);
    277362    }
    278363    else
    279364        role = "";
    280365
    281     Engine.GetGUIObjectByName("usernameText").caption = user;
    282     Engine.GetGUIObjectByName("roleText").caption = translate(role);
     366    Engine.GetGUIObjectByName("usernameText").caption = playerList.list_name[playerList.selected];
     367    Engine.GetGUIObjectByName("roleText").caption = role;
    283368    Engine.GetGUIObjectByName("rankText").caption = translate("N/A");
    284369    Engine.GetGUIObjectByName("highestRatingText").caption = translate("N/A");
    285370    Engine.GetGUIObjectByName("totalGamesText").caption = translate("N/A");
    286371    Engine.GetGUIObjectByName("winsText").caption = translate("N/A");
    287372    Engine.GetGUIObjectByName("lossesText").caption = translate("N/A");
    288373    Engine.GetGUIObjectByName("ratioText").caption = translate("N/A");
    289374}
    290375
    291376/**
    292377 * Update the profile of the selected player with data from the bot.
    293  *
    294378 */
    295379function updateProfile()
    296380{
    297381    var playerList, user;
    298382    var attributes = Engine.GetProfile();
     
    306390            Engine.GetGUIObjectByName("profileErrorText").hidden = false;
    307391            return;
    308392        }
    309393        Engine.GetGUIObjectByName("profileWindowArea").hidden = false;
    310394        Engine.GetGUIObjectByName("profileErrorText").hidden = true;
    311        
     395
    312396        if (attributes[0].rating != "")
    313             user = sprintf(translate("%(nick)s (%(rating)s)"), { nick: user, rating: attributes[0].rating });
     397            user = sprintf(translate("%(nick)s (%(rating)s)"), { "nick": user, "rating": attributes[0].rating });
    314398
    315399        Engine.GetGUIObjectByName("profileUsernameText").caption = user;
    316400        Engine.GetGUIObjectByName("profileRankText").caption = attributes[0].rank;
    317401        Engine.GetGUIObjectByName("profileHighestRatingText").caption = attributes[0].highestRating;
    318402        Engine.GetGUIObjectByName("profileTotalGamesText").caption = attributes[0].totalGamesPlayed;
    319403        Engine.GetGUIObjectByName("profileWinsText").caption = attributes[0].wins;
    320404        Engine.GetGUIObjectByName("profileLossesText").caption = attributes[0].losses;
    321405
    322406        var winRate = (attributes[0].wins / attributes[0].totalGamesPlayed * 100).toFixed(2);
    323407        if (attributes[0].totalGamesPlayed != 0)
    324             Engine.GetGUIObjectByName("profileRatioText").caption = sprintf(translate("%(percentage)s%%"), { percentage: winRate });
     408            Engine.GetGUIObjectByName("profileRatioText").caption = sprintf(translate("%(percentage)s%%"), { "percentage": winRate });
    325409        else
    326410            Engine.GetGUIObjectByName("profileRatioText").caption = translateWithContext("Used for an undefined winning rate", "-");
    327411        return;
    328412    }
    329413    else if (!Engine.GetGUIObjectByName("leaderboard").hidden)
    330414        playerList = Engine.GetGUIObjectByName("leaderboardBox");
    331415    else
    332416        playerList = Engine.GetGUIObjectByName("playersBox");
    333    
     417
    334418    if (attributes[0].rating == "-2")
    335419        return;
     420
    336421    // Make sure the stats we have received coincide with the selected player.
    337422    if (attributes[0].player != playerList.list[playerList.selected])
    338423        return;
     424
    339425    user = playerList.list_name[playerList.selected];
    340426    if (attributes[0].rating != "")
    341         user = sprintf(translate("%(nick)s (%(rating)s)"), { nick: user, rating: attributes[0].rating });
     427        user = sprintf(translate("%(nick)s (%(rating)s)"), { "nick": user, "rating": attributes[0].rating });
    342428
    343429    Engine.GetGUIObjectByName("usernameText").caption = user;
    344430    Engine.GetGUIObjectByName("rankText").caption = attributes[0].rank;
    345431    Engine.GetGUIObjectByName("highestRatingText").caption = attributes[0].highestRating;
    346432    Engine.GetGUIObjectByName("totalGamesText").caption = attributes[0].totalGamesPlayed;
    347433    Engine.GetGUIObjectByName("winsText").caption = attributes[0].wins;
    348434    Engine.GetGUIObjectByName("lossesText").caption = attributes[0].losses;
    349435
    350436    var winRate = (attributes[0].wins / attributes[0].totalGamesPlayed * 100).toFixed(2);
    351437    if (attributes[0].totalGamesPlayed != 0)
    352         Engine.GetGUIObjectByName("ratioText").caption = sprintf(translate("%(percentage)s%%"), { percentage: winRate });
     438        Engine.GetGUIObjectByName("ratioText").caption = sprintf(translate("%(percentage)s%%"), { "percentage": winRate });
    353439    else
    354440        Engine.GetGUIObjectByName("ratioText").caption = translateWithContext("Used for an undefined winning rate", "-");
    355441}
    356442
    357443/**
    358444 * Update the leaderboard from data cached in C++.
    359445 */
    360446function updateLeaderboard()
    361447{
    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);
     448    // Sort users by rating
     449    var boardList = Engine.GetBoardList().sort((a, b) => b.rating - a.rating);
    368450
     451    // Create GUI entries
    369452    var list = [];
    370453    var list_name = [];
    371454    var list_rank = [];
    372455    var list_rating = [];
    373456
    374     // Push changes
    375     for (var i = 0; i < boardList.length; i++)
     457    for (let i in boardList)
    376458    {
    377459        list_name.push(boardList[i].name);
    378460        list_rating.push(boardList[i].rating);
    379461        list_rank.push(i+1);
    380462        list.push(boardList[i].name);
    381463    }
    382464
     465    // Push to GUI
     466    var leaderboard = Engine.GetGUIObjectByName("leaderboardBox");
    383467    leaderboard.list_name = list_name;
    384468    leaderboard.list_rating = list_rating;
    385469    leaderboard.list_rank = list_rank;
    386470    leaderboard.list = list;
    387471
     
    392476/**
    393477 * Update the game listing from data cached in C++.
    394478 */
    395479function updateGameList()
    396480{
    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) {
     481    var gameStatusOrder = Object.keys(g_GameStatusColor);
     482
     483    g_GameList = Engine.GetGameList().sort((a,b) =>
     484    {
    406485        switch (g_GameListSortBy)
    407486        {
    408487        case 'name':
    409488        case 'mapSize':
    410489            // mapSize contains the number of tiles for random maps
    411490            // scenario maps always display default size
     491
    412492        case 'mapType':
    413493            if (a[g_GameListSortBy] < b[g_GameListSortBy])
    414494                return -g_GameListOrder;
    415495            else if (a[g_GameListSortBy] > b[g_GameListSortBy])
    416496                return g_GameListOrder;
    417497            return 0;
     498
    418499        case 'mapName':
    419500            if (translate(a.niceMapName) < translate(b.niceMapName))
    420501                return -g_GameListOrder;
    421502            else if (translate(a.niceMapName) > translate(b.niceMapName))
    422503                return g_GameListOrder;
    423504            return 0;
     505
    424506        case 'nPlayers':
    425507            // Numerical comparison of player count ratio.
    426508            if (a.nbp * b.tnbp < b.nbp * a.tnbp) // ratio a = a.nbp / a.tnbp, ratio b = b.nbp / b.tnbp
    427509                return -g_GameListOrder;
    428510            else if (a.nbp * b.tnbp > b.nbp * a.tnbp)
    429511                return g_GameListOrder;
    430512            return 0;
     513
    431514        default:
    432             if (gameStatuses.indexOf(a.state) < gameStatuses.indexOf(b.state))
     515            if (gameStatusOrder.indexOf(a.state) < gameStatusOrder.indexOf(b.state))
    433516                return -1;
    434             else if (gameStatuses.indexOf(a.state) > gameStatuses.indexOf(b.state))
     517            else if (gameStatusOrder.indexOf(a.state) > gameStatusOrder.indexOf(b.state))
    435518                return 1;
    436519
    437520            // Alphabetical comparison of names as tiebreaker.
    438521            if (a.name < b.name)
    439522                return -1;
     
    441524                return 1;
    442525            return 0;
    443526        }
    444527    });
    445528
     529    // Create GUI entries
    446530    var list_name = [];
    447531    var list_ip = [];
    448532    var list_mapName = [];
    449533    var list_mapSize = [];
    450534    var list_mapType = [];
    451535    var list_nPlayers = [];
    452536    var list = [];
    453537    var list_data = [];
    454538
    455     var c = 0;
    456     for (var g of gameList)
     539    for (let i in g_GameList)
    457540    {
    458         if (!filterGame(g))
     541        let g = g_GameList[i];
     542
     543        if (filterGame(g))
     544            continue;
     545
     546        if (!g_GameStatusColor[g.state])
    459547        {
    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);
     548            error("Unknown game status '" + g.state + "'");
     549            g.state = "unknown";
    477550        }
    478         c++;
     551
     552        let name = colorize(g_GameStatusColor[g.state], escapeText(g.name));
     553
     554        list_name.push(name);
     555        list_ip.push(g.ip);
     556        list_mapName.push(translate(g.niceMapName));
     557        list_mapSize.push(translatedMapSize(g.mapSize));
     558        let idx = g_MapTypes.indexOf(g.mapType);
     559        list_mapType.push(idx != -1 ? g_MapTypesText[idx] : "");
     560        list_nPlayers.push(g.nbp + "/" + g.tnbp);
     561        list.push(name);
     562        list_data.push(i);
    479563    }
    480564
     565    // Push to GUI
     566    var gamesBox = Engine.GetGUIObjectByName("gamesBox");
    481567    gamesBox.list_name = list_name;
    482568    gamesBox.list_mapName = list_mapName;
    483569    gamesBox.list_mapSize = list_mapSize;
    484570    gamesBox.list_mapType = list_mapType;
    485571    gamesBox.list_nPlayers = list_nPlayers;
     
    494580}
    495581
    496582/**
    497583 * Colorize and format the entries in the player list.
    498584 *
    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.
     585 * @return {Array} [name, status, rating] with each item being colorized
    503586 */
    504587function formatPlayerListEntry(nickname, presence, rating)
    505588{
    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;
     589    if (!g_PlayerStatuses[presence])
     590    {
     591        error("Unknown presence '" + presence + "'");
     592        presence = "unknown";
    533593    }
    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);
    543594
    544     // Push this player's name and status onto the list
    545     return [formattedName, formattedStatus, formattedRating];
     595    // Center the rating (optional placeholder)
     596    if (!rating)
     597        rating = "-";
     598    rating = ("    " + rating).substr(-5);
     599
     600    // Add moderator prefix
     601    if (Engine.LobbyGetPlayerRole(nickname) == "moderator")
     602        nickname = g_ModPrefix + nickname;
     603
     604    // Push colorized name, status and rating to the list
     605    var color = g_PlayerStatuses[presence].color;
     606    return [
     607        colorizePlayername(nickname),
     608        colorize(color, g_PlayerStatuses[presence].status),
     609        colorize(color, rating)
     610    ];
    546611}
    547612
    548613/**
    549614 * Given a map size, returns that map size translated into the current
    550615 * language.
     
    552617function translatedMapSize(mapSize)
    553618{
    554619    if (+mapSize !== +mapSize) // NaN
    555620        return translate(mapSize);
    556621    else
    557         return g_mapSizes.shortNames[g_mapSizes.tiles.indexOf(+mapSize)];
     622        return g_MapSizes.shortNames[g_MapSizes.tiles.indexOf(+mapSize)];
    558623}
    559624
    560625/**
    561626 * Populate the game info area with information on the current game selection.
    562627 */
     
    575640    var mapData;
    576641    var g = Engine.GetGUIObjectByName("gamesBox").list_data[selected];
    577642
    578643    // Load map data
    579644    if (g_GameList[g].mapType == "random" && g_GameList[g].mapName == "random")
    580         mapData = {"settings": {"Description": translate("A randomly selected map.")}};
     645        mapData = { "settings": { "Description": translate("A randomly selected map.") } };
    581646    else if (g_GameList[g].mapType == "random" && Engine.FileExists(g_GameList[g].mapName + ".json"))
    582647        mapData = Engine.ReadJSONFile(g_GameList[g].mapName + ".json");
    583648    else if (Engine.FileExists(g_GameList[g].mapName + ".xml"))
    584649        mapData = Engine.LoadMapSettings(g_GameList[g].mapName + ".xml");
    585650    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 }));
     651        warn("Map '" + g_GameList[g].mapName + "' not found locally.");
    588652
    589653    // Show the game info panel and join button.
    590654    Engine.GetGUIObjectByName("gameInfo").hidden = false;
    591655    Engine.GetGUIObjectByName("joinGameButton").hidden = false;
    592656    Engine.GetGUIObjectByName("gameInfoEmpty").hidden = true;
     
    594658    // Display the map name, number of players, the names of the players, the map size and the map type.
    595659    Engine.GetGUIObjectByName("sgMapName").caption = translate(g_GameList[g].niceMapName);
    596660    Engine.GetGUIObjectByName("sgNbPlayers").caption = g_GameList[g].nbp + "/" + g_GameList[g].tnbp;
    597661    Engine.GetGUIObjectByName("sgPlayersNames").caption = g_GameList[g].players;
    598662    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] : "";
     663    let idx = g_MapTypes.indexOf(g_GameList[g].mapType);
     664    Engine.GetGUIObjectByName("sgMapType").caption = idx != -1 ? g_MapTypesText[idx] : "";
    601665
    602666    // Display map description if it exists, otherwise display a placeholder.
    603667    if (mapData && mapData.settings.Description)
    604668        var mapDescription = translate(mapData.settings.Description);
    605669    else
     
    619683 * Start the joining process on the currectly selected game.
    620684 */
    621685function joinSelectedGame()
    622686{
    623687    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         }
     688    if (gamesBox.selected < 0)
     689        return;
    637690
    638         // Open Multiplayer connection window with join option.
    639         Engine.PushGuiPage("page_gamesetup_mp.xml", { multiplayerGameType: "join", name: sname, ip: sip, rating: g_userRating });
     691    // TODO: What about valid host names?
     692    // Check if it looks like an ip address
     693    var ip = g_GameList[gamesBox.list_data[gamesBox.selected]].ip;
     694    if (ip.split('.').length != 4)
     695    {
     696        addChatMessage({ "from": "system", "text": sprintf(translate("This game's address '%(ip)s' does not appear to be valid."), { "ip": ip }) });
     697        return;
    640698    }
     699
     700    // Open Multiplayer connection window with join option.
     701    Engine.PushGuiPage("page_gamesetup_mp.xml",
     702    {
     703        "multiplayerGameType": "join",
     704        "ip": ip,
     705        "name": g_Username,
     706        "rating": g_UserRating
     707    });
    641708}
    642709
    643710/**
    644711 * Start the hosting process.
    645712 */
    646713function hostGame()
    647714{
    648715    // Open Multiplayer connection window with host option.
    649     Engine.PushGuiPage("page_gamesetup_mp.xml", { multiplayerGameType: "host", name: g_Name, rating: g_userRating });
     716    Engine.PushGuiPage("page_gamesetup_mp.xml",
     717    {
     718        "multiplayerGameType": "host",
     719        "name": g_Username,
     720        "rating": g_UserRating
     721    });
    650722}
    651723
    652724////////////////////////////////////////////////////////////////////////////////////////////////
    653725// Utils
    654726////////////////////////////////////////////////////////////////////////////////////////////////
     
    695767            var ratingList = playersBox.list_rating;
    696768            var nickIndex = nickList.indexOf(nick);
    697769            switch(message.level)
    698770            {
    699771            case "join":
    700                 var [name, status, rating] = formatPlayerListEntry(nick, presence, "-");
     772                var [name, status, rating] = formatPlayerListEntry(nick, presence, null);
    701773                playerList.push(name);
    702774                presenceList.push(status);
    703775                nickList.push(nick);
    704776                ratingList.push(String(rating));
    705                 addChatMessage({ "text": "/special " + sprintf(translate("%(nick)s has joined."), { nick: nick }), "key": g_specialKey });
     777                addChatMessage({ "text": "/special " + sprintf(translate("%(nick)s has joined."), { "nick": nick }), "isSpecial": true });
    706778                break;
    707779            case "leave":
    708780                if (nickIndex == -1) // Left, but not present (TODO: warn about this?)
    709781                    break;
    710782                playerList.splice(nickIndex, 1);
    711783                presenceList.splice(nickIndex, 1);
    712784                nickList.splice(nickIndex, 1);
    713785                ratingList.splice(nickIndex, 1);
    714                 addChatMessage({ "text": "/special " + sprintf(translate("%(nick)s has left."), { nick: nick }), "key": g_specialKey });
     786                addChatMessage({ "text": "/special " + sprintf(translate("%(nick)s has left."), { "nick": nick }), "isSpecial": true });
    715787                break;
    716788            case "nick":
    717789                if (nickIndex == -1) // Changed nick, but not present (shouldn't ever happen)
    718790                    break;
    719791                if (!isValidNick(message.data))
    720792                {
    721                     addChatMessage({ "from": "system", "text": sprintf(translate("Invalid nickname: %(nick)s"), { nick: message.data })});
     793                    addChatMessage({ "from": "system", "text": sprintf(translate("Invalid nickname: %(nick)s"), { "nick": message.data }) });
    722794                    break;
    723795                }
    724796                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
    725797                playerList[nickIndex] = name;
    726798                // presence stays the same
    727799                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 });
     800                addChatMessage({ "text": "/special " + sprintf(translate("%(oldnick)s is now known as %(newnick)s."), { "oldnick": nick, "newnick": message.data }), "isSpecial": true });
    729801                break;
    730802            case "presence":
    731803                if (nickIndex == -1) // Changed presence, but not online (shouldn't ever happen)
    732804                    break;
    733805                var [name, status, rating] = formatPlayerListEntry(nick, presence, stripColorCodes(ratingList[nickIndex]));
     
    737809                break;
    738810            case "subject":
    739811                updateSubject(message.text);
    740812                break;
    741813            default:
    742                 warn(sprintf("Unknown message.level '%(msglvl)s'", { msglvl: message.level }));
    743                 break;
     814                warn("Unknown message.level '" + message.level + "'");
    744815            }
    745816            // Push new data to GUI
    746817            playersBox.list_name = playerList;
    747818            playersBox.list_status = presenceList;
    748819            playersBox.list_rating = ratingList;
    749             playersBox.list = nickList;     
     820            playersBox.list = nickList;
    750821            if (playersBox.selected >= playersBox.list.length)
    751822                playersBox.selected = -1;
    752823            break;
    753824        case "system":
    754825            switch (message.level)
    755826            {
    756827            case "standard":
    757                 addChatMessage({ "from": "system", "text": text, "color": "150 0 0" });
     828                addChatMessage({ "from": "system", "text": text, "color": g_SystemColor });
    758829                if (message.text == "disconnected")
    759830                {
    760831                    // Clear the list of games and the list of players
    761832                    updateGameList();
    762833                    updateLeaderboard();
    763834                    updatePlayerList();
    764835                    // Disable the 'host' button
    765836                    Engine.GetGUIObjectByName("hostButton").enabled = false;
    766837                }
    767838                else if (message.text == "connected")
    768                 {
    769839                    Engine.GetGUIObjectByName("hostButton").enabled = true;
    770                 }
     840
    771841                break;
    772842            case "error":
    773                 addChatMessage({ "from": "system", "text": text, "color": "150 0 0" });
     843                addChatMessage({ "from": "system", "text": text, "color": g_SystemColor });
    774844                break;
    775845            case "internal":
    776846                switch (message.text)
    777847                {
    778848                case "gamelist updated":
     
    790860                }
    791861                break;
    792862            }
    793863            break;
    794864        default:
    795             error(sprintf("Unrecognised message type %(msgtype)s", { msgtype: message.type }));
     865            error("Unrecognised message type '" + message.type + "'");
    796866        }
    797867    }
    798868}
    799869
    800 /* Messages */
     870function returnToMainMenu()
     871{
     872    lobbyStop();
     873    Engine.SwitchGuiPage("page_pregame.xml");
     874}
     875
    801876function submitChatInput()
    802877{
    803878    var input = Engine.GetGUIObjectByName("chatInput");
    804879    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     }
     880    if (!text.length)
     881        return;
     882
     883    if (!handleSpecialCommand(text) && !isSpam(text, g_Username))
     884        Engine.LobbySendMessage(text);
     885
     886    input.caption = "";
    811887}
    812888
    813889function isValidNick(nick)
    814890{
    815     var prohibitedNicks = ["system"];
    816     return prohibitedNicks.indexOf(nick) == -1;
     891    return g_ProhibitedNicks.indexOf(nick) == -1;
    817892}
    818893
    819894/**
    820895 * Handle all '/' commands.
    821896 *
     
    825900function handleSpecialCommand(text)
    826901{
    827902    if (text[0] != '/')
    828903        return false;
    829904
    830     var [cmd, nick] = ircSplit(text);
     905    var [cmd, nick] = splitCommand(text);
    831906
    832907    switch (cmd)
    833908    {
    834909    case "away":
    835910        Engine.LobbySetPlayerPresence("away");
    836911        break;
     912
    837913    case "back":
    838914        Engine.LobbySetPlayerPresence("available");
    839915        break;
     916
    840917    case "kick": // TODO: Split reason from nick and pass it too, for now just support "/kick nick"
    841918            // also allow quoting nicks (and/or prevent users from changing it here, but that doesn't help if the spammer uses a different client)
    842919        Engine.LobbyKick(nick, "");
    843920        break;
     921
    844922    case "ban": // TODO: Split reason from nick and pass it too, for now just support "/ban nick"
    845923        Engine.LobbyBan(nick, "");
    846924        break;
     925
    847926    case "quit":
    848         lobbyStop();
    849         Engine.SwitchGuiPage("page_pregame.xml");
     927        returnToMainMenu();
    850928        break;
     929
    851930    case "say":
    852931    case "me":
    853932        return false;
     933
    854934    default:
    855         addChatMessage({ "from":"system", "text": sprintf(translate("We're sorry, the '%(cmd)s' command is not supported."), { cmd: cmd})});
     935        addChatMessage({ "from":"system", "text": sprintf(translate("We're sorry, the '%(cmd)s' command is not supported."), { "cmd": cmd }) });
    856936    }
     937
    857938    return true;
    858939}
    859940
    860941/**
    861942 * Process and, if appropriate, display a formatted message.
     
    863944 * @param msg The message to be processed.
    864945 */
    865946function addChatMessage(msg)
    866947{
    867948    // 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;
     949    if (!msg.from)
     950        msg.from = "";
    877951    if (!msg.color)
    878         msg.color = null;
    879     if (!msg.key)
    880         msg.key = null;
     952        msg.color = "";
     953    if (!msg.isSpecial)
     954        msg.isSpecial = false;
    881955    if (!msg.datetime)
    882         msg.datetime = null;
     956        msg.datetime = "";
     957
     958    // Add moderator prefix
     959    if (Engine.LobbyGetPlayerRole(msg.from) == "moderator")
     960        msg.from = g_ModPrefix + msg.from;
    883961
    884962    // 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));
     963    if (msg.text.indexOf(g_Username) != -1 && g_Username != msg.from)
     964        msg.text = msg.text.replace(new RegExp('\\b' + '\\' + g_Username + '\\b', "g"), colorizePlayername(g_Username));
    887965
    888966    // Run spam test if it's not a historical message
    889967    if (!msg.datetime)
    890968        updateSpamMonitor(msg.from);
     969
    891970    if (isSpam(msg.text, msg.from))
    892971        return;
    893972
    894973    // Format Text
    895     var formatted = ircFormat(msg.text, msg.from, msg.color, msg.key, msg.datetime);
     974    var formatted = formatChatMessage(msg.text, msg.from, msg.color, msg.isSpecial, msg.datetime);
    896975
    897976    // If there is text, add it to the chat box.
    898977    if (formatted)
    899978    {
    900979        g_ChatMessages.push(formatted);
    901980        Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
    902981    }
    903982}
    904983
    905 function ircSplit(string)
     984/**
     985 * Given user input, return the command without the slash and the argument (everything after the first space)
     986 * @returns {Array} [command, nick]
     987 */
     988function splitCommand(string)
    906989{
    907990    var idx = string.indexOf(' ');
    908991    if (idx != -1)
    909992        return [string.substr(1,idx-1), string.substr(idx+1)];
    910993    return [string.substr(1), ""];
    911994}
    912995
    913996/**
    914997 * Format text in an IRC-like way.
    915998 *
    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
     999 * @param text {string} Body of the message.
     1000 * @param from {string} Sender of the message.
     1001 * @param color {string} Optional color of sender.
     1002 * @param isSpecial {Boolean} Whether or not this message is a system message.
     1003 * @param datetime {string} Current date and time of message, only used for historical messages
    9211004 * @return Formatted text.
    9221005 */
    923 function ircFormat(text, from, color, key, datetime)
     1006function formatChatMessage(text, from, color, isSpecial, datetime)
    9241007{
    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]";
     1008    if (from)
     1009        var coloredFrom = color ? colorize(color, from) : colorizePlayername(from);
    9301010
    9311011    // Handle commands allowed past handleSpecialCommand.
    9321012    if (text[0] == '/')
    9331013    {
    934         var [command, message] = ircSplit(text);
     1014        var [command, message] = splitCommand(text);
    9351015        switch (command)
    9361016        {
    9371017            case "me":
    9381018                // 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]';
     1019                var senderString = setSenderFont(sprintf(translate("* %(sender)s"), { "sender": coloredFrom }));
    9401020                // Translation: IRC message issued using the ‘/me’ command.
    941                 var formattedMessage = sprintf(translate("%(sender)s %(action)s"), { sender: senderString, action: message });
     1021                var formattedMessage = sprintf(translate("%(sender)s %(action)s"), { "sender": senderString, "action": message });
    9421022                break;
     1023
    9431024            case "say":
    9441025                // Translation: IRC message prefix.
    945                 var senderString = '[font="sans-bold-13"]' + sprintf(translate("<%(sender)s>"), { sender: coloredFrom }) + '[/font]';
     1026                var senderString = setSenderFont(sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }));
    9461027                // Translation: IRC message.
    947                 var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { sender: senderString, message: message });
     1028                var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderString, "message": message });
    9481029                break
     1030
    9491031            case "special":
    950                 if (key === g_specialKey)
     1032                if (isSpecial)
    9511033                    // Translation: IRC system message.
    952                     var formattedMessage = '[font="sans-bold-13"]' + sprintf(translate("== %(message)s"), { message: message }) + '[/font]';
     1034                    var formattedMessage = setSenderFont(sprintf(translate("== %(message)s"), { "message": message }));
    9531035                else
    9541036                {
    9551037                    // Translation: IRC message prefix.
    956                     var senderString = '[font="sans-bold-13"]' + sprintf(translate("<%(sender)s>"), { sender: coloredFrom }) + '[/font]';
     1038                    var senderString = setSenderFont(sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }));
    9571039                    // Translation: IRC message.
    958                     var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { sender: senderString, message: message });
     1040                    var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderString, "message": message });
    9591041                }
    9601042                break;
     1043
    9611044            default:
    9621045                // This should never happen.
    9631046                var formattedMessage = "";
     1047                break;
    9641048        }
    9651049    }
    9661050    else
    9671051    {
    9681052        // Translation: IRC message prefix.
    969         var senderString = '[font="sans-bold-13"]' + sprintf(translate("<%(sender)s>"), { sender: coloredFrom }) + '[/font]';
     1053        var senderString = setSenderFont(sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }));
    9701054        // Translation: IRC message.
    971         var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { sender: senderString, message: text });
     1055        var formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderString, "message": text });
    9721056    }
    9731057
    9741058    // 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());
     1059    if (g_ShowTimestamp)
     1060        return prefixTimestamp(formattedMessage, datetime);
    9891061
    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"));
    994 
    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]';
     1062    return formattedMessage;
     1063}
    9971064
    998         // Translation: IRC message format when there is a time prefix.
    999         return sprintf(translate("%(time)s %(message)s"), { time: timePrefixString, message: formattedMessage });
     1065function prefixTimestamp(formattedMessage, datetime)
     1066{
     1067    var time;
     1068    if (datetime)
     1069    {
     1070        var parserDate = datetime.split("T")[0].split("-");
     1071        var parserTime = datetime.split("T")[1].split(":");
     1072        // See http://xmpp.org/extensions/xep-0082.html#sect-idp285136 for format of datetime
     1073        // Date takes Year, Month, Day, Hour, Minute, Second
     1074        time = new Date(Date.UTC(parserDate[0], parserDate[1], parserDate[2], parserTime[0], parserTime[1], parserTime[2].split("Z")[0]));
    10001075    }
    10011076    else
    1002         return formattedMessage;
     1077        time = new Date(Date.now());
     1078
     1079    // Translation: Time as shown in the multiplayer lobby (when you enable it in the options page).
     1080    // For a list of symbols that you can use, see:
     1081    // https://sites.google.com/site/icuprojectuserguide/formatparse/datetime?pli=1#TOC-Date-Field-Symbol-Table
     1082    var timeString = Engine.FormatMillisecondsIntoDateString(time.getTime(), translate("HH:mm"));
     1083
     1084    // Translation: Time prefix as shown in the multiplayer lobby (when you enable it in the options page).
     1085    var timePrefixString = setSenderFont(sprintf(translate("\\[%(time)s]"), { "time": timeString }));
     1086
     1087    // Translation: IRC message format when there is a time prefix.
     1088    return sprintf(translate("%(time)s %(message)s"), { "time": timePrefixString, "message": formattedMessage });
    10031089}
    10041090
    10051091/**
    10061092 * Update the spam monitor.
    10071093 *
     
    10111097{
    10121098    // Integer time in seconds.
    10131099    var time = Math.floor(Date.now() / 1000);
    10141100
    10151101    // Update or initialize the user in the spam monitor.
    1016     if (g_spamMonitor[from])
    1017         g_spamMonitor[from][0]++;
     1102    if (g_SpamMonitor[from])
     1103        ++g_SpamMonitor[from][0];
    10181104    else
    1019         g_spamMonitor[from] = [1, time, 0];
     1105        g_SpamMonitor[from] = [1, time, 0];
    10201106}
    10211107
    10221108/**
    10231109 * Check if a message is spam.
    10241110 *
     
    10301116{
    10311117    // Integer time in seconds.
    10321118    var time = Math.floor(Date.now() / 1000);
    10331119
    10341120    // Initialize if not already in the database.
    1035     if (!g_spamMonitor[from])
    1036         g_spamMonitor[from] = [1, time, 0];
     1121    if (!g_SpamMonitor[from])
     1122        g_SpamMonitor[from] = [1, time, 0];
    10371123
    10381124    // Block blank lines.
    10391125    if (text == " ")
    10401126        return true;
     1127
    10411128    // Block users who are still within their spam block period.
    1042     else if (g_spamMonitor[from][2] + SPAM_BLOCK_LENGTH >= time)
     1129    if (g_SpamMonitor[from][2] + g_SpamBlockLength >= time)
    10431130        return true;
     1131
    10441132    // 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)
     1133    if (g_SpamMonitor[from][0] == 6)
    10461134    {
    1047         g_spamMonitor[from][2] = time;
    1048         if (from == g_Name)
     1135        g_SpamMonitor[from][2] = time;
     1136        if (from == g_Username)
    10491137            addChatMessage({ "from": "system", "text": translate("Please do not spam. You have been blocked for thirty seconds.") });
    10501138        return true;
    10511139    }
     1140
    10521141    // Return false if everything is clear.
    1053     else
    1054         return false;
     1142    return false;
    10551143}
    10561144
    10571145/**
    10581146 * Reset timer used to measure message send speed.
    10591147 */
     
    10611149{
    10621150    // Integer time in seconds.
    10631151    var time = Math.floor(Date.now() / 1000);
    10641152
    10651153    // Clear message count every 5 seconds.
    1066     for each (var stats in g_spamMonitor)
     1154    for (let i in g_SpamMonitor)
    10671155    {
    1068         if (stats[1] + 5 <= time)
     1156        if (g_SpamMonitor[i][1] + 5 <= time)
    10691157        {
    1070             stats[1] = time;
    1071             stats[0] = 0;
     1158            g_SpamMonitor[i][1] = time;
     1159            g_SpamMonitor[i][0] = 0;
    10721160        }
    10731161    }
     1162}
    10741163
     1164function setSenderFont(text)
     1165{
     1166    return '[font="' + g_UserFont + '"]' + text + '[/font]';
    10751167}
    10761168
    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
     1169function colorize(color, text)
     1170{
     1171    return '[color="' + color + '"]' + text + "[/color]";
     1172}
     1173
     1174function colorizePlayername(playername)
     1175{
     1176    return colorize(g_PlayerColors[playername] ? g_PlayerColors[playername] : getPlayerColor(playername.replace(g_ModPrefix, "")), playername);
     1177}
     1178
     1179/**
     1180 * Generate a (mostly) unique color for this player based on their name.
     1181 * See http://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-jquery-javascript
     1182 */
    10801183function getPlayerColor(playername)
    10811184{
    10821185    // Generate a probably-unique hash for the player name and use that to create a color.
    10831186    var hash = 0;
    1084     for (var i = 0; i < playername.length; i++)
     1187    for (let i in playername)
    10851188        hash = playername.charCodeAt(i) + ((hash << 5) - hash);
    10861189
    10871190    // 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.
    10881191    // 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
    10891192    // us much more variety if we generate in RGB. Unfortunately, enforcing that RGB values are a certain lightness is very difficult, so
    10901193    // we convert to HSL to do the computation. Since our GUI code only displays RGB colors, we have to convert back.
    10911194    var [h, s, l] = rgbToHsl(hash >> 24 & 0xFF, hash >> 16 & 0xFF, hash >> 8 & 0xFF);
    10921195    return hslToRgb(h, s, Math.max(0.4, l)).join(" ");
    10931196}
    10941197
    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.
     1198/**
     1199 * Ensure `value` is between 0 and 1.
     1200 */
    11151201function clampColorValue(value)
    11161202{
    1117     return Math.abs(1 - Math.abs(value - 1));
     1203    return Math.max(0, Math.min(value, 1));
    11181204}
    11191205
    1120 // See http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
     1206/**
     1207 * See http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
     1208 */
    11211209function rgbToHsl(r, g, b)
    11221210{
    11231211    r /= 255;
    11241212    g /= 255;
    11251213    b /= 255;
     
    11671255        r = hue2rgb(p, q, h + 1/3);
    11681256        g = hue2rgb(p, q, h);
    11691257        b = hue2rgb(p, q, h - 1/3);
    11701258    }
    11711259
    1172     return [r, g, b].map(function (n) Math.round(n * 255));
     1260    return [r, g, b].map(n => Math.round(n * 255));
    11731261}
    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;