/**
* Used for the gamelist-filtering.
*/
const g_MapSizes = prepareForDropdown(g_Settings ? g_Settings.MapSizes : undefined);
/**
* Used for the gamelist-filtering.
*/
const g_MapTypes = prepareForDropdown(g_Settings ? g_Settings.MapTypes : undefined);
/**
* Whether or not to display timestamps in the chat window.
*/
const g_ShowTimestamp = Engine.ConfigDB_GetValue("user", "lobby.chattimestamp") == "true";
/**
* Mute clients who exceed the rate of 1 message per second for this time
*/
const g_SpamBlockTimeframe = 5;
/**
* Mute spammers for this time.
*/
const g_SpamBlockDuration = 30;
/**
* A symbol which is prepended to the username of moderators.
*/
const g_ModeratorPrefix = "@";
/**
* Current username. Cannot contain whitespace.
*/
const g_Username = Engine.LobbyGetNick();
/**
* Current games will be listed in these colors.
*/
const g_GameColors = {
"init": "0 219 0",
"waiting": "255 127 0",
"running": "219 0 0"
};
/**
* Initial sorting order of the gamelist.
*/
const g_GameStatusOrder = ["init", "waiting", "running"];
/**
* The playerlist will be assembled using these values.
*/
const g_PlayerStatuses = {
"available": { "color": "0 219 0", "status": translate("Online") },
"away": { "color": "229 76 13", "status": translate("Away") },
"playing": { "color": "200 0 0", "status": translate("Busy") },
"offline": { "color": "0 0 0", "status": translate("Offline") },
"unknown": { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
};
/**
* Color for error messages in the chat.
*/
const g_SystemColor = "150 0 0";
/**
* Used for highlighting the sender of chat messages.
*/
const g_SenderFont = "sans-bold-13";
/**
* All chat messages received since init (i.e. after lobby join and after returning from a game).
*/
var g_ChatMessages = [];
/**
* Rating of the current user.
* Contains the number or an empty string in case the user has no rating.
*/
var g_UserRating = "";
/**
* All games currently running.
*/
var g_GameList = {};
/**
* Remembers how many messages were sent by each user since the last reset.
*
* For example { "username": [numMessagesSinceReset, lastReset, timeBlocked] }
*/
var g_SpamMonitor = {};
/**
* Used to restore the selection after updating the playerlist.
*/
var g_SelectedPlayer = "";
/**
* Used to restore the selection after updating the gamelist.
*/
var g_SelectedGameIP = "";
/**
* Notifications sent by XmppClient.cpp
*/
var g_NetMessageTypes = {
"system": {
// Three cases are handled in prelobby.js
"registered": msg => {
},
"connected": msg => {
},
"disconnected": msg => {
updateGameList();
updateLeaderboard();
updatePlayerList();
Engine.GetGUIObjectByName("hostButton").enabled = false;
addChatMessage({ "from": "system", "text": translate("Disconnected.") + msg.text, "color": g_SystemColor });
},
"error": msg => {
addChatMessage({ "from": "system", "text": msg.text, "color": g_SystemColor });
}
},
"chat": {
"subject": msg => {
updateSubject(msg.text);
},
"join": msg => {
addChatMessage({
"text": "/special " + sprintf(translate("%(nick)s has joined."), { "nick": msg.text }),
"isSpecial": true
});
Engine.SendGetRatingList();
},
"leave": msg => {
addChatMessage({
"text": "/special " + sprintf(translate("%(nick)s has left."), { "nick": msg.text }),
"isSpecial": true
});
},
"presence": msg => {
},
"nick": msg => {
addChatMessage({
"text": "/special " + sprintf(translate("%(oldnick)s is now known as %(newnick)s."), {
"oldnick": msg.text,
"newnick": msg.data
}),
"isSpecial": true
});
},
"room-message": msg => {
addChatMessage({
"from": escapeText(msg.from),
"text": escapeText(msg.text),
"datetime": msg.datetime
});
},
"private-message": msg => {
if (Engine.LobbyGetPlayerRole(msg.from) == "moderator")
addChatMessage({
"from": "(Private) " + escapeText(msg.from), // TODO: placeholder
"text": escapeText(msg.text.trim()), // some XMPP clients send trailing whitespace
"datetime": msg.datetime
});
}
},
"game": {
"gamelist": msg => updateGameList(),
"profile": msg => updateProfile(),
"leaderboard": msg => updateLeaderboard(),
"ratinglist": msg => updatePlayerList()
}
};
/**
* Called after the XmppConnection succeeded and when returning from a game.
*
* @param {Object} attribs
*/
function init(attribs)
{
if (!g_Settings)
{
returnToMainMenu();
return;
}
initMusic();
global.music.setState(global.music.states.MENU);
initGameFilters();
Engine.LobbySetPlayerPresence("available");
Engine.SendGetGameList();
// When rejoining the lobby after a game, we don't need to process presence changes
Engine.LobbyClearPresenceUpdates();
updatePlayerList();
updateSubject(Engine.LobbyGetRoomSubject());
}
function returnToMainMenu()
{
Engine.StopXmppClient();
Engine.SwitchGuiPage("page_pregame.xml");
}
function initGameFilters()
{
var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
mapSizeFilter.list = [translateWithContext("map size", "Any")].concat(g_MapSizes.Name);
mapSizeFilter.list_data = [""].concat(g_MapSizes.Tiles);
var playersArray = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); // 1, 2, ... MaxPlayers
var playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
playersNumberFilter.list = [translateWithContext("player number", "Any")].concat(playersArray);
playersNumberFilter.list_data = [""].concat(playersArray);
var mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
mapTypeFilter.list = [translateWithContext("map", "Any")].concat(g_MapTypes.Title);
mapTypeFilter.list_data = [""].concat(g_MapTypes.Name);
resetFilters();
}
function resetFilters()
{
Engine.GetGUIObjectByName("mapSizeFilter").selected = 0;
Engine.GetGUIObjectByName("playersNumberFilter").selected = 0;
Engine.GetGUIObjectByName("mapTypeFilter").selected = g_MapTypes.Default;
Engine.GetGUIObjectByName("showFullFilter").checked = false;
applyFilters();
}
function applyFilters()
{
updateGameList();
updateGameSelection();
}
/**
* Filter a game based on the status of the filter dropdowns.
*
* @param {Object} game
* @returns {boolean} - True if game should not be displayed.
*/
function filterGame(game)
{
var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
var playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
var mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
var showFullFilter = Engine.GetGUIObjectByName("showFullFilter");
// We assume index 0 means display all for any given filter.
if (mapSizeFilter.selected != 0 && game.mapSize != mapSizeFilter.list_data[mapSizeFilter.selected])
return true;
if (playersNumberFilter.selected != 0 && game.tnbp != playersNumberFilter.list_data[playersNumberFilter.selected])
return true;
if (mapTypeFilter.selected != 0 && game.mapType != mapTypeFilter.list_data[mapTypeFilter.selected])
return true;
if (!showFullFilter.checked && game.tnbp <= game.nbp)
return true;
return false;
}
/**
* Update the subject GUI object.
*
* @param {string} newSubject
*/
function updateSubject(newSubject)
{
var subject = Engine.GetGUIObjectByName("subject").caption = newSubject;
var subjectBox = Engine.GetGUIObjectByName("subjectBox");
var logo = Engine.GetGUIObjectByName("logo");
// If the subject is only whitespace, hide it and reposition the logo.
if (!newSubject.trim())
{
subjectBox.hidden = true;
logo.size = "50%-110 50%-50 50%+110 50%+50";
}
else
{
subjectBox.hidden = false;
logo.size = "50%-110 40 50%+110 140";
}
}
/**
* Do a full update of the player listing, including ratings from cached C++ information.
*/
function updatePlayerList()
{
var playersBox = Engine.GetGUIObjectByName("playersBox");
var sortBy = playersBox.selected_column || "name";
var sortOrder = playersBox.selected_column_order || 1;
if (playersBox.selected > -1)
g_SelectedPlayer = playersBox.list[playersBox.selected];
var playerList = [];
var presenceList = [];
var nickList = [];
var ratingList = [];
var cleanPlayerList = Engine.GetPlayerList().sort((a, b) => {
var sortA, sortB;
switch (sortBy)
{
case 'rating':
sortA = +a.rating;
sortB = +b.rating;
break;
case 'status':
let statusOrder = Object.keys(g_PlayerStatuses);
sortA = statusOrder.indexOf(a.presence);
sortB = statusOrder.indexOf(b.presence);
break;
case 'name':
default:
sortA = a.name.toLowerCase();
sortB = b.name.toLowerCase();
break;
}
if (sortA < sortB) return -sortOrder;
if (sortA > sortB) return +sortOrder;
return 0;
});
// Colorize list entries
for (let player of cleanPlayerList)
{
if (player.rating && player.name == g_Username)
g_UserRating = player.rating;
let rating = player.rating ? (" " + player.rating).substr(-5) : " -";
let presence = g_PlayerStatuses[player.presence] ? player.presence : "unknown";
if (presence == "unknown")
warn("Unknown presence:" + player.presence);
let statusColor = g_PlayerStatuses[presence].color;
let coloredName = colorPlayerName((player.role == "moderator" ? g_ModeratorPrefix : "") + player.name);
let coloredPresence = '[color="' + statusColor + '"]' + g_PlayerStatuses[presence].status + "[/color]";
let coloredRating = '[color="' + statusColor + '"]' + rating + "[/color]";
playerList.push(coloredName);
presenceList.push(coloredPresence);
ratingList.push(coloredRating);
nickList.push(player.name);
}
playersBox.list_name = playerList;
playersBox.list_status = presenceList;
playersBox.list_rating = ratingList;
playersBox.list = nickList;
// To reduce rating-server load, only send the GUI event if the selection actually changed
if (playersBox.selected != playersBox.list.indexOf(g_SelectedPlayer))
playersBox.selected = playersBox.list.indexOf(g_SelectedPlayer);
}
/**
* Display the profile of the selected player.
* Displays N/A for all stats until updateProfile is called when the stats
* are actually received from the bot.
*
* @param {string} caller - From which screen is the user requesting data from?
*/
function displayProfile(caller)
{
var playerList, rating;
if (caller == "leaderboard")
playerList = Engine.GetGUIObjectByName("leaderboardBox");
else if (caller == "lobbylist")
playerList = Engine.GetGUIObjectByName("playersBox");
else if (caller == "fetch")
{
Engine.SendGetProfile(Engine.GetGUIObjectByName("fetchInput").caption);
return;
}
else
return;
var playerName = playerList.list[playerList.selected];
Engine.GetGUIObjectByName("profileArea").hidden = !playerName;
if (!playerName)
return;
Engine.SendGetProfile(playerName);
var isModerator = Engine.LobbyGetPlayerRole(playerName) == "moderator";
Engine.GetGUIObjectByName("usernameText").caption = playerList.list_name[playerList.selected];
Engine.GetGUIObjectByName("roleText").caption = isModerator ? translate("Moderator") : translate("Player");
Engine.GetGUIObjectByName("rankText").caption = translate("N/A");
Engine.GetGUIObjectByName("highestRatingText").caption = translate("N/A");
Engine.GetGUIObjectByName("totalGamesText").caption = translate("N/A");
Engine.GetGUIObjectByName("winsText").caption = translate("N/A");
Engine.GetGUIObjectByName("lossesText").caption = translate("N/A");
Engine.GetGUIObjectByName("ratioText").caption = translate("N/A");
}
/**
* Update the profile of the selected player with data from the bot.
*/
function updateProfile()
{
var user;
var playerList;
var attributes = Engine.GetProfile();
if (!Engine.GetGUIObjectByName("profileFetch").hidden)
{
let profileFound = attributes[0].rating != "-2";
Engine.GetGUIObjectByName("profileWindowArea").hidden = !profileFound;
Engine.GetGUIObjectByName("profileErrorText").hidden = profileFound;
if (!profileFound)
return;
user = attributes[0].player;
if (attributes[0].rating != "")
user = sprintf(translate("%(nick)s (%(rating)s)"), { "nick": user, "rating": attributes[0].rating });
Engine.GetGUIObjectByName("profileUsernameText").caption = user;
Engine.GetGUIObjectByName("profileRankText").caption = attributes[0].rank;
Engine.GetGUIObjectByName("profileHighestRatingText").caption = attributes[0].highestRating;
Engine.GetGUIObjectByName("profileTotalGamesText").caption = attributes[0].totalGamesPlayed;
Engine.GetGUIObjectByName("profileWinsText").caption = attributes[0].wins;
Engine.GetGUIObjectByName("profileLossesText").caption = attributes[0].losses;
var winRate = (attributes[0].wins / attributes[0].totalGamesPlayed * 100).toFixed(2);
if (attributes[0].totalGamesPlayed != 0)
Engine.GetGUIObjectByName("profileRatioText").caption = sprintf(translate("%(percentage)s%%"), { "percentage": winRate });
else
Engine.GetGUIObjectByName("profileRatioText").caption = translateWithContext("Used for an undefined winning rate", "-");
return;
}
else if (!Engine.GetGUIObjectByName("leaderboard").hidden)
playerList = Engine.GetGUIObjectByName("leaderboardBox");
else
playerList = Engine.GetGUIObjectByName("playersBox");
if (attributes[0].rating == "-2")
return;
// Make sure the stats we have received coincide with the selected player.
if (attributes[0].player != playerList.list[playerList.selected])
return;
user = playerList.list_name[playerList.selected];
if (attributes[0].rating != "")
user = sprintf(translate("%(nick)s (%(rating)s)"), { "nick": user, "rating": attributes[0].rating });
Engine.GetGUIObjectByName("usernameText").caption = user;
Engine.GetGUIObjectByName("rankText").caption = attributes[0].rank;
Engine.GetGUIObjectByName("highestRatingText").caption = attributes[0].highestRating;
Engine.GetGUIObjectByName("totalGamesText").caption = attributes[0].totalGamesPlayed;
Engine.GetGUIObjectByName("winsText").caption = attributes[0].wins;
Engine.GetGUIObjectByName("lossesText").caption = attributes[0].losses;
var winRate = (attributes[0].wins / attributes[0].totalGamesPlayed * 100).toFixed(2);
if (attributes[0].totalGamesPlayed != 0)
Engine.GetGUIObjectByName("ratioText").caption = sprintf(translate("%(percentage)s%%"), { "percentage": winRate });
else
Engine.GetGUIObjectByName("ratioText").caption = translateWithContext("Used for an undefined winning rate", "-");
}
/**
* Update the leaderboard from data cached in C++.
*/
function updateLeaderboard()
{
var leaderboard = Engine.GetGUIObjectByName("leaderboardBox");
var boardList = Engine.GetBoardList().sort((a, b) => b.rating - a.rating);
var list = [];
var list_name = [];
var list_rank = [];
var list_rating = [];
for (let i in boardList)
{
list_name.push(boardList[i].name);
list_rating.push(boardList[i].rating);
list_rank.push(+i+1);
list.push(boardList[i].name);
}
leaderboard.list_name = list_name;
leaderboard.list_rating = list_rating;
leaderboard.list_rank = list_rank;
leaderboard.list = list;
if (leaderboard.selected >= leaderboard.list.length)
leaderboard.selected = -1;
}
/**
* Update the game listing from data cached in C++.
*/
function updateGameList()
{
var gamesBox = Engine.GetGUIObjectByName("gamesBox");
var sortBy = gamesBox.selected_column || "status";
var sortOrder = gamesBox.selected_column_order || 1;
if (gamesBox.selected > -1)
g_SelectedGameIP = g_GameList[gamesBox.selected].ip;
g_GameList = Engine.GetGameList().filter(game => !filterGame(game)).sort((a, b) => {
var sortA, sortB;
switch (sortBy)
{
case 'name':
case 'mapSize':
case 'mapType':
sortA = a[sortBy];
sortB = b[sortBy];
break;
case 'mapName':
sortA = translate(a.niceMapName);
sortB = translate(b.niceMapName);
break;
case 'nPlayers':
// Compare playercount ratio
sortA = a.nbp * b.tnbp;
sortB = b.nbp * a.tnbp;
break;
case 'status':
default:
sortA = g_GameStatusOrder.indexOf(a.state);
sortB = g_GameStatusOrder.indexOf(b.state);
break;
}
if (sortA < sortB) return -sortOrder;
if (sortA > sortB) return +sortOrder;
return 0;
});
var list_name = [];
var list_mapName = [];
var list_mapSize = [];
var list_mapType = [];
var list_nPlayers = [];
var list = [];
var list_data = [];
var selectedGameIndex = -1;
for (let i in g_GameList)
{
let game = g_GameList[i];
let gameName = escapeText(game.name);
let mapTypeIdx = g_MapTypes.Name.indexOf(game.mapType);
if (game.ip == g_SelectedGameIP)
selectedGameIndex = +i;
list_name.push('[color="' + g_GameColors[game.state] + '"]' + gameName);
list_mapName.push(translate(game.niceMapName));
list_mapSize.push(translateMapSize(game.mapSize));
list_mapType.push(mapTypeIdx != -1 ? g_MapTypes.Title[mapTypeIdx] : "");
list_nPlayers.push(game.nbp + "/" + game.tnbp);
list.push(gameName);
list_data.push(i);
}
gamesBox.list_name = list_name;
gamesBox.list_mapName = list_mapName;
gamesBox.list_mapSize = list_mapSize;
gamesBox.list_mapType = list_mapType;
gamesBox.list_nPlayers = list_nPlayers;
// Change these last, otherwise crash
gamesBox.list = list;
gamesBox.list_data = list_data;
gamesBox.selected = selectedGameIndex;
updateGameSelection();
}
/**
* Populate the game info area with information on the current game selection.
*/
function updateGameSelection()
{
let game = selectedGame();
if (!game)
{
Engine.GetGUIObjectByName("gameInfo").hidden = true;
Engine.GetGUIObjectByName("joinGameButton").hidden = true;
Engine.GetGUIObjectByName("gameInfoEmpty").hidden = false;
return;
}
// Show the game info panel and join button.
Engine.GetGUIObjectByName("gameInfo").hidden = false;
Engine.GetGUIObjectByName("joinGameButton").hidden = false;
Engine.GetGUIObjectByName("gameInfoEmpty").hidden = true;
// Display the map name, number of players, the names of the players, the map size and the map type.
Engine.GetGUIObjectByName("sgMapName").caption = translate(game.niceMapName);
Engine.GetGUIObjectByName("sgNbPlayers").caption = translate("Players:") + " " + game.nbp + "/" + game.tnbp;
Engine.GetGUIObjectByName("sgPlayersNames").caption = game.players;
Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(game.mapSize);
let mapTypeIdx = g_MapTypes.Name.indexOf(game.mapType);
Engine.GetGUIObjectByName("sgMapType").caption = mapTypeIdx != -1 ? g_MapTypes.Title[mapTypeIdx] : "";
var mapData = getMapDescriptionAndPreview(game.mapType, game.mapName);
Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description;
setMapPreviewImage("sgMapPreview", mapData.preview);
}
function selectedGame()
{
let gamesBox = Engine.GetGUIObjectByName("gamesBox");
if (gamesBox.selected < 0)
return undefined;
return g_GameList[gamesBox.list_data[gamesBox.selected]];
}
/**
* Immediately rejoin and join gamesetups. Otherwise confirm late-observer join attempt.
*/
function joinButton()
{
let game = selectedGame();
if (!game)
return;
let username = g_UserRating ? g_Username + " (" + g_UserRating + ")" : g_Username;
if (game.state == "init" || game.players.split(", ").indexOf(username) > -1)
joinSelectedGame();
else
messageBox(
400, 200,
translate("The game has already started.") + "\n" +
translate("Do you want to join as observer?"),
translate("Confirmation"),
0,
[translate("No"), translate("Yes")],
[null, joinSelectedGame]
);
}
/**
* Attempt to join the selected game without asking for confirmation.
*/
function joinSelectedGame()
{
let game = selectedGame();
if (!game)
return;
if (game.ip.split('.').length != 4)
{
addChatMessage({
"from": "system",
"text": sprintf(
translate("This game's address '%(ip)s' does not appear to be valid."),
{ "ip": game.ip }
)
});
return;
}
Engine.PushGuiPage("page_gamesetup_mp.xml", {
"multiplayerGameType": "join",
"ip": game.ip,
"name": g_Username,
"rating": g_UserRating
});
}
/**
* Open the dialog box to enter the game name.
*/
function hostGame()
{
Engine.PushGuiPage("page_gamesetup_mp.xml", {
"multiplayerGameType": "host",
"name": g_Username,
"rating": g_UserRating
});
}
/**
* Processes GUI messages sent by the XmppClient.
*/
function onTick()
{
updateTimers();
checkSpamMonitor();
while (true)
{
let msg = Engine.LobbyGuiPollMessage();
if (!msg)
break;
if (!g_NetMessageTypes[msg.type])
{
warn("Unrecognised message type: " + msg.type);
continue;
}
if (!g_NetMessageTypes[msg.type][msg.level])
{
warn("Unrecognised message level: " + msg.level);
continue;
}
g_NetMessageTypes[msg.type][msg.level](msg);
// To improve performance, only update the playerlist GUI when the last update in the current stack is processed
if (msg.type == "chat" && Engine.LobbyGetMucMessageCount() == 0)
updatePlayerList();
}
}
/**
* Executes a lobby command or sends GUI input directly as chat.
*/
function submitChatInput()
{
var input = Engine.GetGUIObjectByName("chatInput");
var text = input.caption;
if (!text.length)
return;
if (!handleSpecialCommand(text) && !isSpam(text, g_Username))
Engine.LobbySendMessage(text);
input.caption = "";
}
/**
* Handle all '/' commands.
*
* @param {string} text - Text to be checked for commands.
* @returns {boolean} true if more text processing is needed, false otherwise.
*/
function handleSpecialCommand(text)
{
if (text[0] != '/')
return false;
var [cmd, nick] = ircSplit(text);
switch (cmd)
{
case "away":
Engine.LobbySetPlayerPresence("away");
break;
case "back":
Engine.LobbySetPlayerPresence("available");
break;
case "kick": // TODO: Split reason from nick and pass it too, for now just support "/kick nick"
// also allow quoting nicks (and/or prevent users from changing it here, but that doesn't help if the spammer uses a different client)
Engine.LobbyKick(nick, "");
break;
case "ban": // TODO: Split reason from nick and pass it too, for now just support "/ban nick"
Engine.LobbyBan(nick, "");
break;
case "quit":
returnToMainMenu();
break;
case "clear":
clearChatMessages();
break;
case "say":
case "me":
return false;
default:
addChatMessage({
"from": "system",
"text": sprintf(translate("We're sorry, the '%(cmd)s' command is not supported."), { "cmd": cmd })
});
}
return true;
}
/**
* Process and if appropriate, display a formatted message.
*
* @param {Object} msg - The message to be processed.
*/
function addChatMessage(msg)
{
if (msg.from)
{
if (Engine.LobbyGetPlayerRole(msg.from) == "moderator")
msg.from = g_ModeratorPrefix + msg.from;
// Highlight local user's nick
if (g_Username != msg.from)
msg.text = msg.text.replace(g_Username, colorPlayerName(g_Username));
// Run spam test if it's not a historical message
if (!msg.datetime)
{
updateSpamMonitor(msg.from);
if (isSpam(msg.text, msg.from))
return;
}
}
var formatted = ircFormat(msg);
if (!formatted)
return;
g_ChatMessages.push(formatted);
Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
}
/**
* Splits given input into command and argument.
*
* @param {string} string
* @returns {Array}
*/
function ircSplit(string)
{
var idx = string.indexOf(' ');
if (idx != -1)
return [string.substr(1,idx-1), string.substr(idx+1)];
return [string.substr(1), ""];
}
/**
* Format text in an IRC-like way.
*
* @param {Object} msg - Received chat message.
* @returns {string} - Formatted text.
*/
function ircFormat(msg)
{
var senderString = "";
var formattedMessage = "";
var coloredFrom = !msg.from ? "" : (msg.color ? '[color="' + msg.color + '"]' + msg.from + "[/color]" : colorPlayerName(msg.from));
// Handle commands allowed past handleSpecialCommand.
if (msg.text[0] == '/')
{
var [command, message] = ircSplit(msg.text);
switch (command)
{
case "me":
// Translation: IRC message prefix when the sender uses the /me command.
senderString = '[font="' + g_SenderFont + '"]' + sprintf(translate("* %(sender)s"), { "sender": coloredFrom }) + '[/font]';
// Translation: IRC message issued using the ‘/me’ command.
formattedMessage = sprintf(translate("%(sender)s %(action)s"), { "sender": senderString, "action": message });
break;
case "say":
// Translation: IRC message prefix.
senderString = '[font="' + g_SenderFont + '"]' + sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }) + '[/font]';
// Translation: IRC message.
formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderString, "message": message });
break;
case "special":
if (msg.isSpecial)
// Translation: IRC system message.
formattedMessage = '[font="' + g_SenderFont + '"]' + sprintf(translate("== %(message)s"), { "message": message }) + '[/font]';
else
{
// Translation: IRC message prefix.
senderString = '[font="' + g_SenderFont + '"]' + sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }) + '[/font]';
// Translation: IRC message.
formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderString, "message": message });
}
break;
}
}
else
{
// Translation: IRC message prefix.
senderString = '[font="' + g_SenderFont + '"]' + sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }) + '[/font]';
// Translation: IRC message.
formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderString, "message": msg.text });
}
// Add chat message timestamp
if (!g_ShowTimestamp)
return formattedMessage;
var time;
if (msg.datetime)
{
let dTime = msg.datetime.split("T");
let parserDate = dTime[0].split("-");
let parserTime = dTime[1].split(":");
// See http://xmpp.org/extensions/xep-0082.html#sect-idp285136 for format of datetime
// Date takes Year, Month, Day, Hour, Minute, Second
time = new Date(Date.UTC(parserDate[0], parserDate[1], parserDate[2], parserTime[0], parserTime[1], parserTime[2].split("Z")[0]));
}
else
time = new Date(Date.now());
// Translation: Time as shown in the multiplayer lobby (when you enable it in the options page).
// For a list of symbols that you can use, see:
// https://sites.google.com/site/icuprojectuserguide/formatparse/datetime?pli=1#TOC-Date-Field-Symbol-Table
var timeString = Engine.FormatMillisecondsIntoDateString(time.getTime(), translate("HH:mm"));
// Translation: Time prefix as shown in the multiplayer lobby (when you enable it in the options page).
var timePrefixString = '[font="' + g_SenderFont + '"]' + sprintf(translate("\\[%(time)s]"), { "time": timeString }) + '[/font]';
// Translation: IRC message format when there is a time prefix.
return sprintf(translate("%(time)s %(message)s"), { "time": timePrefixString, "message": formattedMessage });
}
/**
* Update the spam monitor.
*
* @param {string} from - User to update.
*/
function updateSpamMonitor(from)
{
if (g_SpamMonitor[from])
++g_SpamMonitor[from].count;
else
g_SpamMonitor[from] = {
"count": 1,
"lastSend": Math.floor(Date.now() / 1000),
"lastBlock": 0
};
}
/**
* Check if a message is spam.
*
* @param {string} text - Body of message.
* @param {string} from - Sender of message.
*
* @returns {boolean} - True if message should be blocked.
*/
function isSpam(text, from)
{
// Integer time in seconds.
var time = Math.floor(Date.now() / 1000);
// Initialize if not already in the database.
if (!g_SpamMonitor[from])
g_SpamMonitor[from] = {
"count": 1,
"lastSend": time,
"lastBlock": 0
};
// Block blank lines.
if (!text.trim())
return true;
// Block users who are still within their spam block period.
if (g_SpamMonitor[from].lastBlock + g_SpamBlockDuration >= time)
return true;
// Block users who exceed the rate of 1 message per second for five seconds and are not already blocked.
if (g_SpamMonitor[from].count == g_SpamBlockTimeframe + 1)
{
g_SpamMonitor[from].lastBlock = time;
if (from == g_Username)
addChatMessage({ "from": "system", "text": translate("Please do not spam. You have been blocked for thirty seconds.") });
return true;
}
return false;
}
/**
* Reset timer used to measure message send speed.
* Clear message count every 5 seconds.
*/
function checkSpamMonitor()
{
var time = Math.floor(Date.now() / 1000);
for (let i in g_SpamMonitor)
{
// Reset the spam-status after being silent long enough
if (g_SpamMonitor[i].lastSend + g_SpamBlockTimeframe <= time)
{
g_SpamMonitor[i].count = 0;
g_SpamMonitor[i].lastSend = time;
}
}
}
/**
* Generate a (mostly) unique color for this player based on their name.
* See http://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-jquery-javascript
*
* @param {string} playername
*/
function getPlayerColor(playername)
{
// Generate a probably-unique hash for the player name and use that to create a color.
var hash = 0;
for (var i = 0; i < playername.length; ++i)
hash = playername.charCodeAt(i) + ((hash << 5) - hash);
// 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.
// 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
// us much more variety if we generate in RGB. Unfortunately, enforcing that RGB values are a certain lightness is very difficult, so
// we convert to HSL to do the computation. Since our GUI code only displays RGB colors, we have to convert back.
var [h, s, l] = rgbToHsl(hash >> 24 & 0xFF, hash >> 16 & 0xFF, hash >> 8 & 0xFF);
return hslToRgb(h, s, Math.max(0.7, l)).join(" ");
}
/**
* Returns the given playername wrapped in an appropriate color-tag.
*
* @param {string} playername
*/
function colorPlayerName(playername)
{
return '[color="' + getPlayerColor(playername.replace(g_ModeratorPrefix, "")) + '"]' + playername + '[/color]';
}