Index: ps/trunk/binaries/data/mods/public/gui/lobby/lobby_panels.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/lobby_panels.xml (revision 23171)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/lobby_panels.xml (nonexistent)
@@ -1,366 +0,0 @@
-
-
-
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/lobby_panels.xml
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardList.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardList.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardList.js (revision 23172)
@@ -0,0 +1,64 @@
+/**
+ * This is class manages the content of the leaderboardlist, i.e. the list of highest rated players.
+ */
+class LeaderboardList
+{
+ constructor(xmppMessages)
+ {
+ this.selectionChangeHandlers = new Set();
+
+ this.leaderboardBox = Engine.GetGUIObjectByName("leaderboardBox");
+ this.leaderboardBox.onSelectionChange = this.onSelectionChange.bind(this);
+
+ let rebuild = this.rebuild.bind(this);
+ xmppMessages.registerXmppMessageHandler("game", "leaderboard", rebuild);
+ xmppMessages.registerXmppMessageHandler("system", "disconnected", rebuild);
+
+ this.rebuild();
+ }
+
+ registerSelectionChangeHandler(handler)
+ {
+ this.selectionChangeHandlers.add(handler);
+ }
+
+ onSelectionChange()
+ {
+ let playerName = this.selectedPlayer();
+ for (let handler of this.selectionChangeHandlers)
+ handler(playerName);
+ }
+
+ selectedPlayer()
+ {
+ return this.leaderboardBox.list[this.leaderboardBox.selected] || undefined;
+ }
+
+ /**
+ * Update the leaderboard from data cached in C++.
+ */
+ rebuild()
+ {
+ // TODO: Display placeholder if the data is not available
+ let boardList = Engine.GetBoardList().sort(
+ (a, b) => b.rating - a.rating);
+
+ let list_name = [];
+ let list_rank = [];
+ let list_rating = [];
+
+ boardList.forEach((entry, i) => {
+ list_name.push(escapeText(entry.name));
+ list_rating.push(entry.rating);
+ list_rank.push(i + 1);
+ });
+
+ this.leaderboardBox.list_name = list_name;
+ this.leaderboardBox.list_rating = list_rating;
+ this.leaderboardBox.list_rank = list_rank;
+ this.leaderboardBox.list = list_name;
+
+ if (this.leaderboardBox.selected >= this.leaderboardBox.list.length)
+ this.leaderboardBox.selected = -1;
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardList.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardPage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardPage.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardPage.js (revision 23172)
@@ -0,0 +1,52 @@
+/**
+ * The leaderboard page allows the player to view the highest rated players and update that list.
+ */
+class LeaderboardPage
+{
+ constructor(xmppMessages)
+ {
+ this.openPageHandlers = new Set();
+ this.closePageHandlers = new Set();
+
+ this.leaderboardList = new LeaderboardList(xmppMessages);
+
+ this.leaderboardPage = Engine.GetGUIObjectByName("leaderboardPage");
+
+ Engine.GetGUIObjectByName("leaderboardUpdateButton").onPress = this.onPressUpdate.bind(this);
+ Engine.GetGUIObjectByName("leaderboardPageBack").onPress = this.onPressClose.bind(this);
+ }
+
+ registerOpenPageHandler(handler)
+ {
+ this.openPageHandlers.add(handler);
+ }
+
+ registerClosePageHandler(handler)
+ {
+ this.closePageHandlers.add(handler);
+ }
+
+ openPage()
+ {
+ this.leaderboardPage.hidden = false;
+ Engine.SetGlobalHotkey("cancel", this.onPressClose.bind(this));
+ Engine.SendGetBoardList();
+
+ let playerName = this.leaderboardList.selectedPlayer();
+ for (let handler of this.openPageHandlers)
+ handler(playerName);
+ }
+
+ onPressUpdate()
+ {
+ Engine.SendGetBoardList();
+ }
+
+ onPressClose()
+ {
+ this.leaderboardPage.hidden = true;
+
+ for (let handler of this.closePageHandlers)
+ handler();
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardPage.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardPage.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardPage.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardPage.xml (revision 23172)
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+ Leaderboard
+
+
+
+
+ Rank
+
+
+ Name
+
+
+ Rating
+
+
+
+
+ Back
+
+
+
+ Update
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LeaderboardPage/LeaderboardPage.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/Lobby.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/Lobby.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/Lobby.js (revision 23172)
@@ -0,0 +1,25 @@
+/**
+ * This class owns the page handlers.
+ */
+class Lobby
+{
+ constructor(dialog)
+ {
+ this.xmppMessages = new XmppMessages();
+
+ this.profilePage = new ProfilePage(this.xmppMessages);
+ this.leaderboardPage = new LeaderboardPage(this.xmppMessages);
+ this.lobbyPage = new LobbyPage(dialog, this.xmppMessages, this.leaderboardPage, this.profilePage);
+
+ this.xmppMessages.processHistoricMessages();
+
+ if (Engine.LobbyGetPlayerPresence(g_Nickname) != "available")
+ Engine.LobbySetPlayerPresence("available");
+
+ if (!dialog)
+ {
+ initMusic();
+ global.music.setState(global.music.states.MENU);
+ }
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/Lobby.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/AnnouncementHandler.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/AnnouncementHandler.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/AnnouncementHandler.js (revision 23172)
@@ -0,0 +1,20 @@
+/**
+ * This class informs clients of the server if an announcement had been broadcasted.
+ */
+class AnnouncementHandler
+{
+ constructor(xmppMessages)
+ {
+ xmppMessages.registerXmppMessageHandler("chat", "private-message", this.onPrivateMessage.bind(this));
+ }
+
+ onPrivateMessage(message)
+ {
+ // Announcements and the Message of the Day are sent by the server directly
+ if (!message.from)
+ messageBox(
+ 400, 250,
+ message.text.trim(),
+ translate("Notice"));
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/AnnouncementHandler.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/BuddyButton.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/BuddyButton.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/BuddyButton.js (revision 23172)
@@ -0,0 +1,70 @@
+/**
+ * This class manages the button that enables the player to add or remove buddies.
+ */
+class BuddyButton
+{
+ constructor(xmppMessages)
+ {
+ this.buddyChangedHandlers = new Set();
+ this.playerName = undefined;
+
+ this.toggleBuddyButton = Engine.GetGUIObjectByName("toggleBuddyButton");
+ this.toggleBuddyButton.onPress = this.onPress.bind(this);
+
+ let rebuild = this.rebuild.bind(this);
+ xmppMessages.registerXmppMessageHandler("system", "connected", rebuild);
+ xmppMessages.registerXmppMessageHandler("system", "disconnected", rebuild);
+
+ this.rebuild();
+ }
+
+ registerBuddyChangeHandler(handler)
+ {
+ this.buddyChangedHandlers.add(handler);
+ }
+
+ onPlayerSelectionChange(playerName)
+ {
+ this.playerName = playerName;
+ this.rebuild();
+ }
+
+ rebuild()
+ {
+ this.toggleBuddyButton.caption =
+ g_Buddies.indexOf(this.playerName) != -1 ?
+ this.UnmarkString :
+ this.MarkString;
+
+ this.toggleBuddyButton.enabled = Engine.IsXmppClientConnected() && !!this.playerName && this.playerName != g_Nickname;
+ }
+
+ /**
+ * Toggle the buddy state of the selected player.
+ */
+ onPress()
+ {
+ if (!this.playerName || this.playerName == g_Nickname || this.playerName.indexOf(g_BuddyListDelimiter) != -1)
+ return;
+
+ let index = g_Buddies.indexOf(this.playerName);
+ if (index != -1)
+ g_Buddies.splice(index, 1);
+ else
+ g_Buddies.push(this.playerName);
+
+ Engine.ConfigDB_CreateAndWriteValueToFile(
+ "user",
+ "lobby.buddies",
+ g_Buddies.filter(nick => nick).join(g_BuddyListDelimiter) || g_BuddyListDelimiter,
+ "config/user.cfg");
+
+ this.rebuild();
+
+ for (let handler of this.buddyChangedHandlers)
+ handler();
+ }
+}
+
+BuddyButton.prototype.MarkString = translate("Mark as Buddy");
+BuddyButton.prototype.UnmarkString = translate("Unmark as Buddy");
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/BuddyButton.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/HostButton.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/HostButton.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/HostButton.js (revision 23172)
@@ -0,0 +1,32 @@
+/**
+ * This class manages the button that enables the player to configure the start a new hosted multiplayer match.
+ */
+class HostButton
+{
+ constructor(dialog, xmppMessages)
+ {
+ this.hostButton = Engine.GetGUIObjectByName("hostButton");
+ this.hostButton.onPress = this.onPress.bind(this);
+ this.hostButton.caption = translate("Host Game");
+ this.hostButton.hidden = dialog;
+
+ let onConnectionStatusChange = this.onConnectionStatusChange.bind(this);
+ xmppMessages.registerXmppMessageHandler("system", "connected", onConnectionStatusChange);
+ xmppMessages.registerXmppMessageHandler("system", "disconnected", onConnectionStatusChange);
+ this.onConnectionStatusChange();
+ }
+
+ onConnectionStatusChange()
+ {
+ this.hostButton.enabled = Engine.IsXmppClientConnected();
+ }
+
+ onPress()
+ {
+ Engine.PushGuiPage("page_gamesetup_mp.xml", {
+ "multiplayerGameType": "host",
+ "name": g_Nickname,
+ "rating": Engine.LobbyGetPlayerRating(g_Nickname)
+ });
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/HostButton.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js (revision 23172)
@@ -0,0 +1,130 @@
+/**
+ * This class manages the button that enables the player to join a lobby game hosted by a remote player.
+ */
+class JoinButton
+{
+ constructor(dialog, gameList)
+ {
+ this.gameList = gameList;
+
+ this.joinButton = Engine.GetGUIObjectByName("joinButton");
+ this.joinButton.caption = this.Caption;
+ this.joinButton.hidden = dialog;
+ if (!dialog)
+ this.joinButton.onPress = this.onPress.bind(this);
+
+ gameList.gamesBox.onMouseLeftDoubleClickItem = this.onPress.bind(this);
+ gameList.registerSelectionChangeHandler(this.onSelectedGameChange.bind(this, dialog));
+ }
+
+ onSelectedGameChange(dialog, selectedGame)
+ {
+ this.joinButton.hidden = dialog || !selectedGame;
+ }
+
+ /**
+ * Immediately rejoin and join gamesetups. Otherwise confirm late-observer join attempt.
+ */
+ onPress()
+ {
+ let game = this.gameList.selectedGame();
+ if (!game)
+ return;
+
+ let rating = this.getRejoinRating(game);
+ let playername = rating ? g_Nickname + " (" + rating + ")" : g_Nickname;
+
+ if (!game.isCompatible)
+ messageBox(
+ 400, 200,
+ translate("Your active mods do not match the mods of this game.") + "\n\n" +
+ comparedModsString(game.mods, Engine.GetEngineInfo().mods) + "\n\n" +
+ translate("Do you want to switch to the mod selection page?"),
+ translate("Incompatible mods"),
+ [translate("No"), translate("Yes")],
+ [null, this.openModSelectionPage.bind(this)]
+ );
+ else if (game.stanza.state == "init" || game.players.some(player => player.Name == playername))
+ this.joinSelectedGame();
+ else
+ messageBox(
+ 400, 200,
+ translate("The game has already started. Do you want to join as observer?"),
+ translate("Confirmation"),
+ [translate("No"), translate("Yes")],
+ [null, this.joinSelectedGame.bind(this)]);
+ }
+
+ /**
+ * Attempt to join the selected game without asking for confirmation.
+ */
+ joinSelectedGame()
+ {
+ if (this.joinButton.hidden)
+ return;
+
+ let game = this.gameList.selectedGame();
+ if (!game)
+ return;
+
+ let ip;
+ let port;
+ let stanza = game.stanza;
+ if (stanza.stunIP)
+ {
+ ip = stanza.stunIP;
+ port = stanza.stunPort;
+ }
+ else
+ {
+ ip = stanza.ip;
+ port = stanza.port;
+ }
+
+ if (ip.split('.').length != 4)
+ {
+ messageBox(
+ 400, 250,
+ sprintf(
+ translate("This game's address '%(ip)s' does not appear to be valid."),
+ { "ip": escapeText(stanza.ip) }),
+ translate("Error"));
+ return;
+ }
+
+ Engine.PushGuiPage("page_gamesetup_mp.xml", {
+ "multiplayerGameType": "join",
+ "ip": ip,
+ "port": port,
+ "name": g_Nickname,
+ "rating": this.getRejoinRating(stanza),
+ "useSTUN": !!stanza.stunIP,
+ "hostJID": stanza.hostUsername + "@" + Engine.ConfigDB_GetValue("user", "lobby.server") + "/0ad"
+ });
+ }
+
+ openModSelectionPage()
+ {
+ Engine.StopXmppClient();
+ Engine.SwitchGuiPage("page_modmod.xml", {
+ "cancelbutton": true
+ });
+ }
+
+ /**
+ * Rejoin games with the original playername, even if the rating changed meanwhile.
+ */
+ getRejoinRating(game)
+ {
+ for (let player of game.players)
+ {
+ let playerNickRating = splitRatingFromNick(player.Name);
+ if (playerNickRating.nick == g_Nickname)
+ return playerNickRating.rating;
+ }
+ return Engine.LobbyGetPlayerRating(g_Nickname);
+ }
+}
+
+// Translation: Join the game currently selected in the list.
+JoinButton.prototype.Caption = translate("Join Game");
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/LeaderboardButton.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/LeaderboardButton.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/LeaderboardButton.js (revision 23172)
@@ -0,0 +1,22 @@
+/**
+ * This class deals with the button that opens the leaderboard page.
+ */
+class LeaderboardButton
+{
+ constructor(xmppMessages, leaderboardPage)
+ {
+ this.leaderboardButton = Engine.GetGUIObjectByName("leaderboardButton");
+ this.leaderboardButton.caption = translate("Leaderboard");
+ this.leaderboardButton.onPress = leaderboardPage.openPage.bind(leaderboardPage);
+
+ let onConnectionStatusChange = this.onConnectionStatusChange.bind(this);
+ xmppMessages.registerXmppMessageHandler("system", "connected", onConnectionStatusChange);
+ xmppMessages.registerXmppMessageHandler("system", "disconnected", onConnectionStatusChange);
+ this.onConnectionStatusChange();
+ }
+
+ onConnectionStatusChange()
+ {
+ this.leaderboardButton.enabled = Engine.IsXmppClientConnected();
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/LeaderboardButton.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/ProfileButton.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/ProfileButton.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/ProfileButton.js (revision 23172)
@@ -0,0 +1,22 @@
+/**
+ * This class deals with the button that opens the profile view page.
+ */
+class ProfileButton
+{
+ constructor(xmppMessages, profilePage)
+ {
+ this.profileButton = Engine.GetGUIObjectByName("profileButton");
+ this.profileButton.caption = translate("Player Profile Lookup");
+ this.profileButton.onPress = profilePage.openPage.bind(profilePage, false);
+
+ let onConnectionStatusChange = this.onConnectionStatusChange.bind(this);
+ xmppMessages.registerXmppMessageHandler("system", "connected", onConnectionStatusChange);
+ xmppMessages.registerXmppMessageHandler("system", "disconnected", onConnectionStatusChange);
+ this.onConnectionStatusChange();
+ }
+
+ onConnectionStatusChange()
+ {
+ this.profileButton.enabled = Engine.IsXmppClientConnected();
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/ProfileButton.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/QuitButton.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/QuitButton.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/QuitButton.js (revision 23172)
@@ -0,0 +1,40 @@
+/**
+ * This class manages the button that allows the player to close the lobby page.
+ */
+class QuitButton
+{
+ constructor(dialog, leaderboardPage, profilePage)
+ {
+ let closeDialog = this.closeDialog.bind(this);
+ let returnToMainMenu = this.returnToMainMenu.bind(this);
+ let onPress = dialog ? closeDialog : returnToMainMenu;
+
+ let leaveButton = Engine.GetGUIObjectByName("leaveButton");
+ leaveButton.onPress = onPress;
+ leaveButton.caption = dialog ?
+ translateWithContext("previous page", "Back") :
+ translateWithContext("previous page", "Main Menu");
+
+ if (dialog)
+ {
+ Engine.SetGlobalHotkey("lobby", onPress);
+ Engine.SetGlobalHotkey("cancel", onPress);
+
+ let cancelHotkey = Engine.SetGlobalHotkey.bind(Engine, "cancel", onPress);
+ leaderboardPage.registerClosePageHandler(cancelHotkey);
+ profilePage.registerClosePageHandler(cancelHotkey);
+ }
+ }
+
+ closeDialog()
+ {
+ Engine.LobbySetPlayerPresence("playing");
+ Engine.PopGuiPage();
+ }
+
+ returnToMainMenu()
+ {
+ Engine.StopXmppClient();
+ Engine.SwitchGuiPage("page_pregame.xml");
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/QuitButton.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatCommandHandler.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatCommandHandler.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatCommandHandler.js (revision 23172)
@@ -0,0 +1,166 @@
+/**
+ * The purpose of this class is to test if a given textual input of the current player
+ * is not a chat message to be sent but a command to be performed locally or on the
+ * server, and if so perform it.
+ */
+class ChatCommandHandler
+{
+ constructor(chatMessagesPanel, systemMessageFormat)
+ {
+ this.chatMessagesPanel = chatMessagesPanel;
+ this.systemMessageFormat = systemMessageFormat;
+ }
+
+ /**
+ * @returns {boolean} true if the input was successfully parsed as a chat command.
+ */
+ handleChatCommand(text)
+ {
+ if (!text.startsWith('/'))
+ return false;
+
+ let index = text.indexOf(" ");
+ let command = text.substr(1, index == -1 ? undefined : index - 1);
+ let args = index == -1 ? "" : text.substr(index + 1);
+
+ let commandObj = this.ChatCommands[command] || undefined;
+ if (!commandObj)
+ {
+ this.chatMessagesPanel.addText(
+ Date.now() / 1000,
+ this.systemMessageFormat.format(
+ sprintf(translate("The command '%(cmd)s' is not supported."), {
+ "cmd": setStringTags(escapeText(command), this.ChatCommandTags)
+ })));
+ this.chatMessagesPanel.flushMessages();
+ return true;
+ }
+
+ if (commandObj.moderatorOnly && Engine.LobbyGetPlayerRole(g_Nickname) != "moderator")
+ {
+ this.chatMessagesPanel.addText(
+ Date.now() / 1000,
+ this.systemMessageFormat.format(
+ sprintf(translate("The command '%(cmd)s' is restricted to moderators."), {
+ "cmd": setStringTags(escapeText(command), this.ChatCommandTags)
+ })));
+ this.chatMessagesPanel.flushMessages();
+ return true;
+ }
+
+ let handler = commandObj && commandObj.handler || undefined;
+ if (!handler)
+ return false;
+
+ return handler.call(this, args);
+ }
+
+ argumentCount(commandName, args)
+ {
+ if (args.trim())
+ return false;
+
+ this.chatMessagesPanel.addText(
+ Date.now() / 1000,
+ this.systemMessageFormat.format(
+ sprintf(translate("The command '%(cmd)s' requires at least one argument."), {
+ "cmd": setStringTags(commandName, this.ChatCommandTags)
+ })));
+ this.chatMessagesPanel.flushMessages();
+ return true;
+ }
+}
+
+/**
+ * Color to highlight chat commands in the explanation.
+ */
+ChatCommandHandler.prototype.ChatCommandTags = {
+ "color": "200 200 255"
+};
+
+/**
+ * Commands that can be entered by clients via chat input.
+ * A handler returns true if the user input should be sent as a chat message.
+ */
+ChatCommandHandler.prototype.ChatCommands = {
+ "away": {
+ "description": translate("Set your state to 'Away'."),
+ "handler": function(args) {
+ Engine.LobbySetPlayerPresence("away");
+ return true;
+ }
+ },
+ "back": {
+ "description": translate("Set your state to 'Online'."),
+ "handler": function(args) {
+ Engine.LobbySetPlayerPresence("available");
+ return true;
+ }
+ },
+ "kick": {
+ "description": translate("Kick a specified user from the lobby. Usage: /kick nick reason"),
+ "handler": function(args) {
+ let index = args.indexOf(" ");
+ if (index == -1)
+ Engine.LobbyKick(args, "");
+ else
+ Engine.LobbyKick(args.substr(0, index), args.substr(index + 1));
+ return true;
+ },
+ "moderatorOnly": true
+ },
+ "ban": {
+ "description": translate("Ban a specified user from the lobby. Usage: /ban nick reason"),
+ "handler": function(args) {
+ let index = args.indexOf(" ");
+ if (index == -1)
+ Engine.LobbyBan(args, "");
+ else
+ Engine.LobbyBan(args.substr(0, index), args.substr(index + 1));
+ return true;
+ },
+ "moderatorOnly": true
+ },
+ "help": {
+ "description": translate("Show this help."),
+ "handler": function(args) {
+ let isModerator = Engine.LobbyGetPlayerRole(g_Nickname) == "moderator";
+ let txt = translate("Chat commands:");
+ for (let command in this.ChatCommands)
+ if (!this.ChatCommands[command].moderatorOnly || isModerator)
+ // Translation: Chat command help format
+ txt += "\n" + sprintf(translate("%(command)s - %(description)s"), {
+ "command": setStringTags(command, this.ChatCommandTags),
+ "description": this.ChatCommands[command].description
+ });
+
+ this.chatMessagesPanel.addText(
+ Date.now() / 1000,
+ this.systemMessageFormat.format(txt));
+
+ this.chatMessagesPanel.flushMessages();
+ return true;
+ }
+ },
+ "me": {
+ "description": translate("Send a chat message about yourself. Example: /me goes swimming."),
+ "handler": function(args) {
+ // Translation: Chat command
+ return this.argumentCount(translate("/me"), args);
+ }
+ },
+ "say": {
+ "description": translate("Send text as a chat message (even if it starts with slash). Example: /say /help is a great command."),
+ "handler": function(args) {
+ // Translation: Chat command
+ return this.argumentCount(translate("/say"), args);
+ }
+ },
+ "clear": {
+ "description": translate("Clear all chat scrollback."),
+ "handler": function(args) {
+ this.chatMessagesPanel.clearChatMessages();
+ return true;
+ }
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatCommandHandler.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatInputPanel.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatInputPanel.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatInputPanel.js (revision 23172)
@@ -0,0 +1,52 @@
+/**
+ * The purpose of this class is to process the chat input of the local player and
+ * either submit the input as chat or perform it as a local or remote command.
+ */
+class ChatInputPanel
+{
+ constructor(xmppMessages, chatMessagesPanel, systemMessageFormat)
+ {
+ this.chatCommandHandler = new ChatCommandHandler(chatMessagesPanel, systemMessageFormat);
+
+ this.chatSubmit = Engine.GetGUIObjectByName("chatSubmit");
+ this.chatSubmit.onPress = this.submitChatInput.bind(this);
+
+ this.chatInput = Engine.GetGUIObjectByName("chatInput");
+ this.chatInput.onPress = this.submitChatInput.bind(this);
+ this.chatInput.onTab = this.autocomplete.bind(this);
+ this.chatInput.tooltip = colorizeAutocompleteHotkey();
+
+ let update = this.update.bind(this);
+ xmppMessages.registerXmppMessageHandler("system", "connected", update);
+ xmppMessages.registerXmppMessageHandler("system", "disconnected", update);
+ xmppMessages.registerXmppMessageHandler("chat", "role", update);
+
+ this.update();
+ }
+
+ update()
+ {
+ let hidden = !Engine.IsXmppClientConnected() || Engine.LobbyGetPlayerRole(g_Nickname) == "visitor";
+ this.chatInput.hidden = hidden;
+ this.chatSubmit.hidden = hidden;
+ }
+
+ submitChatInput()
+ {
+ let text = this.chatInput.caption;
+ if (!text.length)
+ return;
+
+ if (!this.chatCommandHandler.handleChatCommand(text))
+ Engine.LobbySendMessage(text);
+
+ this.chatInput.caption = "";
+ }
+
+ autocomplete()
+ {
+ autoCompleteText(
+ this.chatInput,
+ Engine.GetPlayerList().map(player => player.name));
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatInputPanel.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageEvents.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageEvents.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageEvents.js (revision 23172)
@@ -0,0 +1,26 @@
+/**
+ * This is the class (and only class) that formats textual messages submitted by chat participants.
+ */
+ChatMessageEvents.PlayerChat = class
+{
+ constructor(xmppMessages, chatMessagesPanel)
+ {
+ this.chatMessagesPanel = chatMessagesPanel;
+ this.chatMessageFormat = new ChatMessageFormat();
+ xmppMessages.registerXmppMessageHandler("chat", "room-message", this.onRoomMessage.bind(this));
+ xmppMessages.registerXmppMessageHandler("chat", "private-message", this.onPrivateMessage.bind(this));
+ }
+
+ onRoomMessage(message)
+ {
+ this.chatMessagesPanel.addText(message.time, this.chatMessageFormat.format(message));
+ }
+
+ onPrivateMessage(message)
+ {
+ // We intend to not support private messages between users
+ if (!message.from || Engine.LobbyGetPlayerRole(message.from) == "moderator")
+ // some XMPP clients send trailing whitespace
+ this.chatMessagesPanel.addText(message.time, this.chatMessageFormat.format(message));
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageEvents.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormat.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormat.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormat.js (revision 23172)
@@ -0,0 +1,69 @@
+/**
+ * Formats a chat message sent by a player (i.e. not a chat notification),
+ * accounting for chat format commands such as /me or /say and private messages.
+ *
+ * Plays an acoustic notification if the playername was mentioned
+ */
+class ChatMessageFormat
+{
+ constructor()
+ {
+ this.chatMessageFormatMe = new ChatMessageFormatMe();
+ this.chatMessageFormatSay = new ChatMessageFormatSay();
+ this.chatMessagePrivateWrapper = new ChatMessagePrivateWrapper();
+ }
+
+ /**
+ * Message properties: from, text, historic, optionally private
+ */
+ format(message)
+ {
+ let text = escapeText(message.text);
+ if (g_Nickname != message.from)
+ {
+ // Highlight nicknames, assume they do not contain escapaped characters
+ text = text.replace(g_Nickname, PlayerColor.ColorPlayerName(g_Nickname));
+
+ // Notify local player
+ if (!message.historic && text.toLowerCase().indexOf(g_Nickname.toLowerCase()) != -1)
+ soundNotification("nick");
+ }
+
+ let sender = PlayerColor.ColorPlayerName(message.from, undefined, Engine.LobbyGetPlayerRole(message.from));
+
+ // Handle chat format commands
+ let formattedMessage;
+ let index = text.indexOf(" ");
+ if (text.startsWith("/") && index != -1)
+ {
+ let command = text.substr(1, index - 1);
+ let commandText = text.substr(index + 1);
+
+ switch (command)
+ {
+ case "me":
+ {
+ formattedMessage = this.chatMessageFormatMe.format(sender, commandText);
+ break;
+ }
+ case "say":
+ {
+ formattedMessage = this.chatMessageFormatSay.format(sender, commandText);
+ break;
+ }
+ default:
+ {
+ formattedMessage = this.chatMessageFormatSay.format(sender, text);
+ break;
+ }
+ }
+ }
+ else
+ formattedMessage = this.chatMessageFormatSay.format(sender, text);
+
+ if (message.level == "private-message")
+ formattedMessage = this.chatMessagePrivateWrapper.format(formattedMessage);
+
+ return formattedMessage;
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormat.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormatMe.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormatMe.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormatMe.js (revision 23172)
@@ -0,0 +1,31 @@
+/**
+ * This class formats a chat message that was sent using the /me format command.
+ * For example "/me goes away".
+ */
+class ChatMessageFormatMe
+{
+ constructor()
+ {
+ this.args = {};
+ }
+
+ /**
+ * Sender is formatted, escapeText is the responsibility of the caller.
+ */
+ format(sender, text)
+ {
+ this.args.sender = setStringTags(sender, this.SenderTags);
+ this.args.message = text;
+ return sprintf(this.Format, this.args);
+ }
+}
+
+// Translation: Chat message issued using the ‘/me’ command.
+ChatMessageFormatMe.prototype.Format = translate("* %(sender)s %(message)s");
+
+/**
+ * Used for highlighting the sender of chat messages.
+ */
+ChatMessageFormatMe.prototype.SenderTags = {
+ "font": "sans-bold-13"
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormatMe.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormatSay.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormatSay.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormatSay.js (revision 23172)
@@ -0,0 +1,38 @@
+/**
+ * This class formats a chat message that was not formatted with any commands.
+ * The nickname and the message content will be assumed to be player input, thus escaped,
+ * meaning that one cannot use colorized messages here.
+ */
+class ChatMessageFormatSay
+{
+ constructor()
+ {
+ this.senderArgs = {};
+ this.messageArgs = {};
+ }
+
+ /**
+ * Sender is formatted, escapeText is the responsibility of the caller.
+ */
+ format(sender, text)
+ {
+ this.senderArgs.sender = sender;
+ this.messageArgs.message = text;
+ this.messageArgs.sender = setStringTags(
+ sprintf(this.ChatSenderFormat, this.senderArgs),
+ this.SenderTags);
+
+ return sprintf(this.ChatMessageFormat, this.messageArgs);
+ }
+}
+
+ChatMessageFormatSay.prototype.ChatSenderFormat = translate("<%(sender)s>");
+
+ChatMessageFormatSay.prototype.ChatMessageFormat = translate("%(sender)s %(message)s");
+
+/**
+ * Used for highlighting the sender of chat messages.
+ */
+ChatMessageFormatSay.prototype.SenderTags = {
+ "font": "sans-bold-13"
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessageFormatSay.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessagePrivateWrapper.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessagePrivateWrapper.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessagePrivateWrapper.js (revision 23172)
@@ -0,0 +1,32 @@
+/**
+ * This class adds an indication that the chat message was a private message to the given text.
+ */
+class ChatMessagePrivateWrapper
+{
+ constructor()
+ {
+ this.args = {
+ "private": setStringTags(this.PrivateFormat, this.PrivateMessageTags)
+ };
+ }
+
+ /**
+ * Text is formatted, escapeText is the responsibility of the caller.
+ */
+ format(text)
+ {
+ this.args.message = text;
+ return sprintf(this.PrivateMessageFormat, this.args);
+ }
+}
+
+ChatMessagePrivateWrapper.prototype.PrivateFormat = translate("Private");
+
+ChatMessagePrivateWrapper.prototype.PrivateMessageFormat = translate("(%(private)s) %(message)s");
+
+/**
+ * Color for private messages in the chat.
+ */
+ChatMessagePrivateWrapper.prototype.PrivateMessageTags = {
+ "color": "0 150 0"
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessages/ChatMessagePrivateWrapper.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessagesPanel.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessagesPanel.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessagesPanel.js (revision 23172)
@@ -0,0 +1,52 @@
+/**
+ * This class stores and displays the chat history since the login and
+ * displays timestamps if enabled.
+ */
+class ChatMessagesPanel
+{
+ constructor(xmppMessages)
+ {
+ this.xmppMessages = xmppMessages;
+
+ this.chatText = Engine.GetGUIObjectByName("chatText");
+ this.chatHistory = "";
+
+ if (Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true")
+ this.timestampWrapper = new TimestampWrapper();
+
+ this.hasUpdate = false;
+ this.flushEvent = this.flushMessages.bind(this);
+ }
+
+ addText(timestamp, text)
+ {
+ if (this.timestampWrapper)
+ text = this.timestampWrapper.format(timestamp, text);
+
+ this.chatHistory += this.chatHistory ? "\n" + text : text;
+
+ if (!this.hasUpdate)
+ {
+ this.hasUpdate = true;
+ // Most xmpp messages are not chat messages, hence
+ // only subscribe the event handler when relevant to improve performance.
+ this.xmppMessages.registerMessageBatchProcessedHandler(this.flushEvent);
+ }
+ }
+
+ flushMessages()
+ {
+ if (this.hasUpdate)
+ {
+ this.chatText.caption = this.chatHistory;
+ this.hasUpdate = false;
+ this.xmppMessages.unregisterMessageBatchProcessedHandler(this.flushEvent);
+ }
+ }
+
+ clearChatMessages()
+ {
+ this.chatHistory = "";
+ this.chatText.caption = "";
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatMessagesPanel.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatPanel.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatPanel.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatPanel.js (revision 23172)
@@ -0,0 +1,26 @@
+/**
+ * Properties of this prototype are classes that subscribe to one or more events and
+ * construct a formatted chat message to be displayed on that event.
+ *
+ * Important: Apply escapeText on player provided input to avoid players breaking the game for everybody.
+ */
+class ChatMessageEvents
+{
+}
+
+class ChatPanel
+{
+ constructor(xmppMessages)
+ {
+ this.systemMessageFormat = new SystemMessageFormat();
+ this.statusMessageFormat = new StatusMessageFormat();
+
+ this.chatMessagesPanel = new ChatMessagesPanel(xmppMessages);
+ this.chatInputPanel = new ChatInputPanel(xmppMessages, this.chatMessagesPanel, this.systemMessageFormat);
+
+ this.chatMessageEvents = {};
+ for (let name in ChatMessageEvents)
+ this.chatMessageEvents[name] = new ChatMessageEvents[name](
+ xmppMessages, this.chatMessagesPanel, this.statusMessageFormat, this.systemMessageFormat);
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatPanel.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatPanel.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatPanel.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatPanel.xml (revision 23172)
@@ -0,0 +1,8 @@
+
+
+
+
+
+ Send
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/ChatPanel.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/StatusMessages/StatusMessageEvents.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/StatusMessages/StatusMessageEvents.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/StatusMessages/StatusMessageEvents.js (revision 23172)
@@ -0,0 +1,153 @@
+/**
+ * @file The classes in this file trigger notifications about occurrences in the multi-user
+ * chat room that are not chat messages, nor SystemMessages.
+ */
+
+ChatMessageEvents.ClientEvents = class
+{
+ constructor(xmppMessages, chatMessagesPanel, statusMessageFormat)
+ {
+ this.chatMessagesPanel = chatMessagesPanel;
+ this.statusMessageFormat = statusMessageFormat;
+ this.kickStrings = new KickStrings();
+ this.nickArgs = {};
+
+ xmppMessages.registerXmppMessageHandler("chat", "join", this.onClientJoin.bind(this));
+ xmppMessages.registerXmppMessageHandler("chat", "leave", this.onClientLeave.bind(this));
+ xmppMessages.registerXmppMessageHandler("chat", "kicked", this.onClientKicked.bind(this, false));
+ xmppMessages.registerXmppMessageHandler("chat", "banned", this.onClientKicked.bind(this, true));
+ }
+
+ onClientJoin(message)
+ {
+ this.nickArgs.nick = escapeText(message.nick);
+ this.chatMessagesPanel.addText(
+ message.time,
+ this.statusMessageFormat.format(sprintf(this.FormatJoin, this.nickArgs)));
+ }
+
+ onClientLeave(message)
+ {
+ this.nickArgs.nick = escapeText(message.nick);
+ this.chatMessagesPanel.addText(
+ message.time,
+ this.statusMessageFormat.format(sprintf(this.FormatLeave, this.nickArgs)));
+ }
+
+ onClientKicked(banned, message)
+ {
+ // If the local player had been kicked, that is logged more vividly than a neutral status message
+ if (message.nick != g_Nickname)
+ this.chatMessagesPanel.addText(
+ message.time,
+ this.statusMessageFormat.format(this.kickStrings.get(banned, message)));
+ }
+};
+
+ChatMessageEvents.ClientEvents.prototype.FormatJoin = translate("%(nick)s has joined.");
+ChatMessageEvents.ClientEvents.prototype.FormatLeave = translate("%(nick)s has left.");
+
+ChatMessageEvents.Nick = class
+{
+ constructor(xmppMessages, chatMessagesPanel, statusMessageFormat)
+ {
+ this.chatMessagesPanel = chatMessagesPanel;
+ this.statusMessageFormat = statusMessageFormat;
+ this.args = {};
+ xmppMessages.registerXmppMessageHandler("chat", "nick", this.onNickChange.bind(this));
+ }
+
+ onNickChange(message)
+ {
+ this.args.oldnick = escapeText(message.oldnick);
+ this.args.newnick = escapeText(message.newnick);
+ this.chatMessagesPanel.addText(
+ message.time,
+ this.statusMessageFormat.format(sprintf(this.Format, this.args)));
+ }
+};
+
+ChatMessageEvents.Nick.prototype.Format = translate("%(oldnick)s is now known as %(newnick)s.");
+
+ChatMessageEvents.Role = class
+{
+ constructor(xmppMessages, chatMessagesPanel, statusMessageFormat)
+ {
+ this.chatMessagesPanel = chatMessagesPanel;
+ this.statusMessageFormat = statusMessageFormat;
+ this.args = {};
+ xmppMessages.registerXmppMessageHandler("chat", "role", this.onRoleChange.bind(this));
+ }
+
+ onRoleChange(message)
+ {
+ let roleType = this.RoleStrings.find(type =>
+ type.newrole == message.newrole &&
+ (!type.oldrole || type.oldrole == message.oldrole));
+
+ let txt;
+ if (message.nick == g_Nickname)
+ txt = roleType.you;
+ else
+ {
+ this.args.nick = escapeText(message.nick);
+ txt = sprintf(roleType.nick, this.args);
+ }
+
+ this.chatMessagesPanel.addText(
+ message.time,
+ this.statusMessageFormat.format(txt));
+ }
+};
+
+ChatMessageEvents.Role.prototype.RoleStrings =
+[
+ {
+ "newrole": "visitor",
+ "you": translate("You have been muted."),
+ "nick": translate("%(nick)s has been muted.")
+ },
+ {
+ "newrole": "moderator",
+ "you": translate("You are now a moderator."),
+ "nick": translate("%(nick)s is now a moderator.")
+ },
+ {
+ "newrole": "participant",
+ "oldrole": "visitor",
+ "you": translate("You have been unmuted."),
+ "nick": translate("%(nick)s has been unmuted.")
+ },
+ {
+ "newrole": "participant",
+ "oldrole": "moderator",
+ "you": translate("You are not a moderator anymore."),
+ "nick": translate("%(nick)s is not a moderator anymore.")
+ }
+];
+
+ChatMessageEvents.Subject = class
+{
+ constructor(xmppMessages, chatMessagesPanel, statusMessageFormat)
+ {
+ this.chatMessagesPanel = chatMessagesPanel;
+ this.statusMessageFormat = statusMessageFormat;
+ this.args = {};
+ xmppMessages.registerXmppMessageHandler("chat", "subject", this.onSubjectChange.bind(this));
+ }
+
+ onSubjectChange(message)
+ {
+ this.args.nick = escapeText(message.nick);
+ let subject = message.subject.trim();
+ this.chatMessagesPanel.addText(
+ message.time,
+ this.statusMessageFormat.format(
+ subject ?
+ sprintf(this.FormatChange, this.args) + "\n" + subject :
+ sprintf(this.FormatDelete, this.args)));
+ }
+};
+
+ChatMessageEvents.Subject.prototype.FormatChange = translate("%(nick)s changed the lobby subject to:");
+ChatMessageEvents.Subject.prototype.FormatDelete = translate("%(nick)s deleted the lobby subject.");
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/StatusMessages/StatusMessageEvents.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/StatusMessages/StatusMessageFormat.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/StatusMessages/StatusMessageFormat.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/StatusMessages/StatusMessageFormat.js (revision 23172)
@@ -0,0 +1,29 @@
+/**
+ * Status messages are textual event notifications triggered by multi-user chat room actions.
+ */
+class StatusMessageFormat
+{
+ constructor()
+ {
+ this.args = {};
+ }
+
+ /**
+ * escapeText is the responsibility of the caller.
+ */
+ format(text)
+ {
+ this.args.message = text;
+ return setStringTags(
+ sprintf(this.MessageFormat, this.args),
+ this.MessageTags);
+ }
+}
+
+StatusMessageFormat.prototype.MessageFormat =
+ // Translation: Chat status message
+ translate("== %(message)s");
+
+StatusMessageFormat.prototype.MessageTags = {
+ "font": "sans-bold-13"
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/StatusMessages/StatusMessageFormat.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/SystemMessages/SystemMessageEvents.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/SystemMessages/SystemMessageEvents.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/SystemMessages/SystemMessageEvents.js (revision 23172)
@@ -0,0 +1,56 @@
+/**
+ * System messages are highlighted chat notifications that concern the current player.
+ */
+ChatMessageEvents.System = class
+{
+ constructor(xmppMessages, chatMessagesPanel, statusMessageFormat, systemMessageFormat)
+ {
+ this.chatMessagesPanel = chatMessagesPanel;
+ this.systemMessageFormat = systemMessageFormat;
+ this.kickStrings = new KickStrings();
+
+ xmppMessages.registerXmppMessageHandler("system", "connected", this.onConnected.bind(this));
+ xmppMessages.registerXmppMessageHandler("system", "disconnected", this.onDisconnected.bind(this));
+ xmppMessages.registerXmppMessageHandler("system", "error", this.onSystemError.bind(this));
+ xmppMessages.registerXmppMessageHandler("chat", "kicked", this.onClientKicked.bind(this, false));
+ xmppMessages.registerXmppMessageHandler("chat", "banned", this.onClientKicked.bind(this, true));
+ }
+
+ // TODO: XmppClient StanzaErrorServiceUnavailable is thrown if the ratings bot is not serving
+ // This should be caught more transparently than an unrelatable "Service unavailable" system error chat message
+ onSystemError(message)
+ {
+ this.chatMessagesPanel.addText(
+ message.time,
+ this.systemMessageFormat.format(
+ escapeText(message.text)));
+ }
+
+ onConnected(message)
+ {
+ this.chatMessagesPanel.addText(
+ message.time,
+ this.systemMessageFormat.format(this.ConnectedCaption));
+ }
+
+ onDisconnected(message)
+ {
+ this.chatMessagesPanel.addText(
+ message.time,
+ this.systemMessageFormat.format(
+ this.DisconnectedCaption + " " +
+ escapeText(message.reason + " " + message.certificate_status)));
+ }
+
+ onClientKicked(banned, message)
+ {
+ if (message.nick == g_Nickname)
+ this.chatMessagesPanel.addText(
+ message.time,
+ this.systemMessageFormat.format(
+ this.kickStrings.get(banned, message)));
+ }
+};
+
+ChatMessageEvents.System.prototype.ConnectedCaption = translate("Connected.");
+ChatMessageEvents.System.prototype.DisconnectedCaption = translate("Disconnected.");
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/SystemMessages/SystemMessageEvents.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/SystemMessages/SystemMessageFormat.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/SystemMessages/SystemMessageFormat.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/SystemMessages/SystemMessageFormat.js (revision 23172)
@@ -0,0 +1,36 @@
+/**
+ * Status messages are textual event notifications triggered by local events.
+ * The messages may be colorized, hence the caller needs to apply escapeText on player input.
+ */
+class SystemMessageFormat
+{
+ constructor()
+ {
+ this.args = {
+ "system": setStringTags(this.System, this.SystemTags)
+ };
+ }
+
+ format(text)
+ {
+ this.args.message = text;
+ return setStringTags(
+ sprintf(this.MessageFormat, this.args),
+ this.MessageTags);
+ }
+}
+
+SystemMessageFormat.prototype.System =
+ // Translation: Caption for system notifications shown in the chat panel
+ translate("System:");
+
+SystemMessageFormat.prototype.SystemTags = {
+ "color": "150 0 0"
+};
+
+SystemMessageFormat.prototype.MessageFormat =
+ translate("=== %(system)s %(message)s");
+
+SystemMessageFormat.prototype.MessageTags = {
+ "font": "sans-bold-13"
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/SystemMessages/SystemMessageFormat.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ConnectionHandler.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ConnectionHandler.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ConnectionHandler.js (revision 23172)
@@ -0,0 +1,74 @@
+/**
+ * This class will ask the player to rejoin the lobby after having been disconnected.
+ */
+class ConnectionHandler
+{
+ constructor(xmppMessages)
+ {
+ // Whether the current player has been kicked or banned
+ this.kicked = false;
+
+ // Avoid stacking of multiple dialog boxes
+ this.askingReconnect = false;
+
+ xmppMessages.registerXmppMessageHandler("chat", "leave", this.onClientLeave.bind(this));
+ xmppMessages.registerXmppMessageHandler("chat", "kicked", this.onClientKicked.bind(this, false));
+ xmppMessages.registerXmppMessageHandler("system", "disconnected", this.askReconnect.bind(this));
+ xmppMessages.registerXmppMessageHandler("chat", "nick", this.onNickChange.bind(this));
+ }
+
+ onNickChange(message)
+ {
+ if (message.oldnick == g_Nickname)
+ g_Nickname = message.newnick;
+ }
+
+ onClientLeave(message)
+ {
+ if (message.nick == g_Nickname)
+ Engine.DisconnectXmppClient();
+ }
+
+ onClientKicked(banned, message)
+ {
+ if (message.nick != g_Nickname)
+ return;
+
+ this.kicked = true;
+
+ // The current player has been kicked from the room, not from the server
+ Engine.DisconnectXmppClient();
+
+ messageBox(
+ 400, 250,
+ new KickStrings().get(banned, message),
+ banned ? translate("BANNED") : translate("KICKED"));
+ }
+
+ askReconnect()
+ {
+ if (this.kicked)
+ return;
+
+ // Ignore stacked disconnect messages
+ if (Engine.IsXmppClientConnected() || this.askingReconnect)
+ return;
+
+ this.askingReconnect = true;
+
+ messageBox(
+ 400, 200,
+ translate("You have been disconnected from the lobby. Do you want to reconnect?"),
+ translate("Confirmation"),
+ [translate("No"), translate("Yes")],
+ [
+ () => {
+ this.askingReconnect = false;
+ },
+ () => {
+ this.askingReconnect = false;
+ Engine.ConnectXmppClient();
+ }
+ ]);
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ConnectionHandler.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js (revision 23172)
@@ -0,0 +1,141 @@
+/**
+ * The purpose of this class is to display information about the selected game.
+ */
+class GameDetails
+{
+ constructor(dialog, gameList)
+ {
+ this.playernameArgs = {};
+ this.playerCountArgs = {};
+ this.gameStartArgs = {};
+
+ this.lastGame = {};
+
+ this.gameDetails = Engine.GetGUIObjectByName("gameDetails");
+
+ this.sgMapName = Engine.GetGUIObjectByName("sgMapName");
+ this.sgGame = Engine.GetGUIObjectByName("sgGame");
+ this.sgPlayersNames = Engine.GetGUIObjectByName("sgPlayersNames");
+ this.sgMapSize = Engine.GetGUIObjectByName("sgMapSize");
+ this.sgMapPreview = Engine.GetGUIObjectByName("sgMapPreview");
+ this.sgMapDescription = Engine.GetGUIObjectByName("sgMapDescription");
+
+ gameList.registerSelectionChangeHandler(this.onGameListSelectionChange.bind(this));
+
+ this.resize(dialog);
+ }
+
+ resize(dialog)
+ {
+ let bottom = Engine.GetGUIObjectByName(dialog ? "leaveButton" : "joinButton").size.top - 5;
+ let size = this.gameDetails.size;
+ size.bottom = bottom;
+ this.gameDetails.size = size;
+ }
+
+ /**
+ * Populate the game info area with information on the current game selection.
+ */
+ onGameListSelectionChange(game)
+ {
+ this.gameDetails.hidden = !game;
+ if (!game)
+ return;
+
+ Engine.ProfileStart("GameDetails");
+
+ let stanza = game.stanza;
+ if (stanza.mapType != this.lastGame.mapType || stanza.mapName != this.lastGame.mapName)
+ {
+ let mapData = getMapDescriptionAndPreview(stanza.mapType, stanza.mapName);
+ this.sgMapPreview.sprite = getMapPreviewImage(mapData.preview);
+ this.mapDescription = mapData.description;
+ }
+
+ let displayData = game.displayData;
+ this.sgMapName.caption = displayData.mapName;
+
+ {
+ let txt;
+ if (game.isCompatible)
+ txt =
+ setStringTags(this.VictoryConditionsFormat, this.CaptionTags) + " " +
+ (stanza.victoryConditions ?
+ stanza.victoryConditions.split(",").map(translateVictoryCondition).join(this.Comma) :
+ translateWithContext("victory condition", "Endless Game"));
+ else
+ txt =
+ setStringTags(this.ModsFormat, this.CaptionTags) + " " +
+ escapeText(modsToString(game.mods, Engine.GetEngineInfo().mods));
+
+ txt +=
+ "\n" + setStringTags(this.MapTypeFormat, this.CaptionTags) + " " + displayData.mapType +
+ "\n" + setStringTags(this.MapSizeFormat, this.CaptionTags) + " " + displayData.mapSize +
+ "\n" + setStringTags(this.MapDescriptionFormat, this.CaptionTags) + " " + this.mapDescription;
+
+ this.sgMapDescription.caption = txt;
+ }
+
+ {
+ let txt = escapeText(stanza.name);
+
+ this.playernameArgs.playername = escapeText(stanza.hostUsername);
+ txt += "\n" + sprintf(this.HostFormat, this.playernameArgs);
+
+ this.playerCountArgs.current = escapeText(stanza.nbp);
+ this.playerCountArgs.total = escapeText(stanza.maxnbp);
+ txt += "\n" + sprintf(this.PlayerCountFormat, this.playerCountArgs);
+
+ if (stanza.startTime)
+ {
+ this.gameStartArgs.time = Engine.FormatMillisecondsIntoDateStringLocal(+stanza.startTime * 1000, this.TimeFormat);
+ txt += "\n" + sprintf(this.GameStartFormat, this.gameStartArgs);
+ }
+
+ this.sgGame.caption = txt;
+ }
+
+ {
+ let textHeight = this.sgGame.getTextSize().height;
+
+ let sgGameSize = this.sgGame.size;
+ sgGameSize.bottom = textHeight;
+ this.sgGame.size = sgGameSize;
+
+ let sgPlayersNamesSize = this.sgPlayersNames.size;
+ sgPlayersNamesSize.top = textHeight + 5;
+ this.sgPlayersNames.size = sgPlayersNamesSize;
+ }
+
+ this.sgPlayersNames.caption = formatPlayerInfo(game.players);
+
+ this.lastGame = game;
+ Engine.ProfileStop();
+ }
+}
+
+GameDetails.prototype.HostFormat = translate("Host: %(playername)s");
+
+GameDetails.prototype.PlayerCountFormat = translate("Players: %(current)s/%(total)s");
+
+GameDetails.prototype.VictoryConditionsFormat = translate("Victory Conditions:");
+
+// Translation: Comma used to concatenate victory conditions
+GameDetails.prototype.Comma = translate(", ");
+
+GameDetails.prototype.ModsFormat = translate("Mods:");
+
+// Translation: %(time)s is the hour and minute here.
+GameDetails.prototype.GameStartFormat = translate("Game started at %(time)s");
+
+GameDetails.prototype.TimeFormat = translate("HH:mm");
+
+GameDetails.prototype.MapTypeFormat = translate("Map Type:");
+
+GameDetails.prototype.MapSizeFormat = translate("Map Size:");
+
+GameDetails.prototype.MapDescriptionFormat = translate("Map Description:");
+
+GameDetails.prototype.CaptionTags = {
+ "font": "sans-bold-14"
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js (revision 23172)
@@ -0,0 +1,221 @@
+/**
+ * Each property of this class handles one specific map filter and is defined in external files.
+ */
+class GameListFilters
+{
+}
+
+/**
+ * This class displays the list of multiplayer matches that are currently being set up or running,
+ * filtered and sorted depending on player selection.
+ */
+class GameList
+{
+ constructor(xmppMessages, buddyButton)
+ {
+ // Array of Game class instances, where the keys are ip+port strings, used for quick lookups
+ this.games = {};
+
+ // Array of Game class instances sorted by display order
+ this.gameList = [];
+
+ this.selectionChangeHandlers = new Set();
+
+ this.gamesBox = Engine.GetGUIObjectByName("gamesBox");
+ this.gamesBox.onSelectionChange = this.onSelectionChange.bind(this);
+ this.gamesBox.onSelectionColumnChange = this.onFilterChange.bind(this);
+ let ratingColumn = Engine.ConfigDB_GetValue("user", "lobby.columns.gamerating") == "true";
+ this.gamesBox.hidden_mapType = ratingColumn;
+ this.gamesBox.hidden_gameRating = !ratingColumn;
+
+ // Avoid repeated array construction
+ this.list_buddy = [];
+ this.list_gameName = [];
+ this.list_mapName = [];
+ this.list_mapSize = [];
+ this.list_mapType = [];
+ this.list_maxnbp = [];
+ this.list_gameRating = [];
+ this.list = [];
+
+ this.filters = [];
+ for (let name in GameListFilters)
+ this.filters.push(new GameListFilters[name](this.onFilterChange.bind(this)));
+
+ xmppMessages.registerXmppMessageHandler("game", "gamelist", this.rebuildGameList.bind(this));
+ xmppMessages.registerXmppMessageHandler("system", "disconnected", this.rebuildGameList.bind(this));
+
+ buddyButton.registerBuddyChangeHandler(this.onBuddyChange.bind(this));
+
+ this.rebuildGameList();
+ }
+
+ registerSelectionChangeHandler(handler)
+ {
+ this.selectionChangeHandlers.add(handler);
+ }
+
+ onFilterChange()
+ {
+ this.rebuildGameList();
+ }
+
+ onBuddyChange()
+ {
+ for (let name in this.games)
+ this.games[name].onBuddyChange();
+
+ this.rebuildGameList();
+ }
+
+ onSelectionChange()
+ {
+ let game = this.selectedGame();
+ for (let handler of this.selectionChangeHandlers)
+ handler(game);
+ }
+
+ selectedGame()
+ {
+ return this.gameList[this.gamesBox.selected] || undefined;
+ }
+
+ rebuildGameList()
+ {
+ Engine.ProfileStart("rebuildGameList");
+
+ Engine.ProfileStart("getGameList");
+ let selectedGame = this.selectedGame();
+ let gameListData = Engine.GetGameList();
+ Engine.ProfileStop();
+
+ {
+ Engine.ProfileStart("updateGames");
+ let selectedColumn = this.gamesBox.selected_column;
+ let newGames = {};
+ for (let stanza of gameListData)
+ {
+ let game = this.games[stanza.ip] || undefined;
+ let exists = !!game;
+ if (!exists)
+ game = new Game();
+
+ game.update(stanza, selectedColumn);
+ newGames[stanza.ip] = game;
+ }
+ this.games = newGames;
+ Engine.ProfileStop();
+ }
+
+ {
+ Engine.ProfileStart("filterGameList");
+ this.gameList.length = 0;
+ for (let ip in this.games)
+ {
+ let game = this.games[ip];
+ if (this.filters.every(filter => filter.filter(game)))
+ this.gameList.push(game);
+ }
+ Engine.ProfileStop();
+ }
+
+ {
+ Engine.ProfileStart("sortGameList");
+ let sortOrder = this.gamesBox.selected_column_order;
+ this.gameList.sort((game1, game2) => {
+ if (game1.sortValue < game2.sortValue) return -sortOrder;
+ if (game1.sortValue > game2.sortValue) return +sortOrder;
+ return 0;
+ });
+ Engine.ProfileStop();
+ }
+
+ let selectedGameIndex = -1;
+
+ {
+ Engine.ProfileStart("setupGameList");
+ let length = this.gameList.length;
+ this.list_buddy.length = length;
+ this.list_gameName.length = length;
+ this.list_mapName.length = length;
+ this.list_mapSize.length = length;
+ this.list_mapType.length = length;
+ this.list_maxnbp.length = length;
+ this.list_gameRating.length = length;
+ this.list.length = length;
+
+ this.gameList.forEach((game, i) => {
+
+ let displayData = game.displayData;
+ this.list_buddy[i] = displayData.buddy;
+ this.list_gameName[i] = displayData.gameName;
+ this.list_mapName[i] = displayData.mapName;
+ this.list_mapSize[i] = displayData.mapSize;
+ this.list_mapType[i] = displayData.mapType;
+ this.list_maxnbp[i] = displayData.playerCount;
+ this.list_gameRating[i] = game.gameRating;
+ this.list[i] = "";
+
+ if (selectedGame && game.stanza.ip == selectedGame.stanza.ip && game.stanza.port == selectedGame.stanza.port)
+ selectedGameIndex = i;
+ });
+ Engine.ProfileStop();
+ }
+
+ {
+ Engine.ProfileStart("copyToGUI");
+ let gamesBox = this.gamesBox;
+ gamesBox.list_buddy = this.list_buddy;
+ gamesBox.list_gameName = this.list_gameName;
+ gamesBox.list_mapName = this.list_mapName;
+ gamesBox.list_mapSize = this.list_mapSize;
+ gamesBox.list_mapType = this.list_mapType;
+ gamesBox.list_maxnbp = this.list_maxnbp;
+ gamesBox.list_gameRating = this.list_gameRating;
+
+ // Change these last, otherwise crash
+ gamesBox.list = this.list;
+ gamesBox.list_data = this.list;
+ gamesBox.auto_scroll = false;
+ Engine.ProfileStop();
+
+ gamesBox.selected = selectedGameIndex;
+ }
+
+ Engine.ProfileStop();
+ }
+
+ /**
+ * Select the game where the selected player is currently playing, observing or offline.
+ * Selects in that order to account for players that occur in multiple games.
+ */
+ selectGameFromPlayername(playerName)
+ {
+ if (!playerName)
+ return;
+
+ let foundAsObserver = false;
+
+ for (let i = 0; i < this.gameList.length; ++i)
+ for (let player of this.gameList[i].players)
+ {
+ if (playerName != splitRatingFromNick(player.Name).nick)
+ continue;
+
+ this.gamesBox.auto_scroll = true;
+ if (player.Team == "observer")
+ {
+ foundAsObserver = true;
+ this.gamesBox.selected = i;
+ }
+ else if (!player.Offline)
+ {
+ this.gamesBox.selected = i;
+ return;
+ }
+
+ if (!foundAsObserver)
+ this.gamesBox.selected = i;
+ }
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/MapSize.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/MapSize.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/MapSize.js (revision 23172)
@@ -0,0 +1,25 @@
+GameListFilters.MapSize = class
+{
+ constructor(onFilterChange)
+ {
+ this.selected = 0;
+ this.onFilterChange = onFilterChange;
+
+ this.mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
+ this.mapSizeFilter.list = [translateWithContext("map size", "Any"), ...g_MapSizes.Name];
+ this.mapSizeFilter.list_data = ["", ...g_MapSizes.Tiles];
+ this.mapSizeFilter.selected = 0;
+ this.mapSizeFilter.onSelectionChange = this.onSelectionChange.bind(this);
+ }
+
+ onSelectionChange()
+ {
+ this.selected = this.mapSizeFilter.list_data[this.mapSizeFilter.selected];
+ this.onFilterChange();
+ }
+
+ filter(game)
+ {
+ return !this.selected || game.stanza.mapSize == this.selected;
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/MapSize.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/PlayerCount.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/PlayerCount.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/PlayerCount.js (revision 23172)
@@ -0,0 +1,26 @@
+GameListFilters.PlayerCount = class
+{
+ constructor(onFilterChange)
+ {
+ this.selected = "";
+ this.onFilterChange = onFilterChange;
+
+ let playersArray = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); // 1, 2, ... MaxPlayers
+ this.playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
+ this.playersNumberFilter.list = [translateWithContext("player number", "Any")].concat(playersArray);
+ this.playersNumberFilter.list_data = [""].concat(playersArray);
+ this.playersNumberFilter.selected = 0;
+ this.playersNumberFilter.onSelectionChange = this.onSelectionChange.bind(this);
+ }
+
+ onSelectionChange()
+ {
+ this.selected = this.playersNumberFilter.list_data[this.playersNumberFilter.selected];
+ this.onFilterChange();
+ }
+
+ filter(game)
+ {
+ return !this.selected || game.stanza.maxnbp == this.selected;
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/PlayerCount.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/KickStrings.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/KickStrings.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/KickStrings.js (revision 23172)
@@ -0,0 +1,49 @@
+/**
+ * This class provides translated kick event notification strings,
+ * consumed by chat and notification message box.
+ */
+class KickStrings
+{
+ constructor()
+ {
+ this.nickArgs = {};
+ this.reasonArgs = {};
+ }
+
+ get(banned, message)
+ {
+ let level = banned ? 1 : 0;
+ let me = message.nick == g_Nickname;
+
+ let txt;
+ if (me)
+ txt = this.Strings.Local[level];
+ else
+ {
+ this.nickArgs.nick = escapeText(message.nick);
+ txt = sprintf(this.Strings.Remote[level], this.nickArgs);
+ }
+
+ if (message.reason)
+ {
+ this.reasonArgs.reason = escapeText(message.reason);
+ txt += " " + sprintf(this.Reason, this.reasonArgs);
+ }
+
+ return txt;
+ }
+}
+
+KickStrings.prototype.Strings = {
+ "Local": [
+ translate("You have been kicked from the lobby!"),
+ translate("You have been banned from the lobby!")
+ ],
+ "Remote": [
+ translate("%(nick)s has been kicked from the lobby."),
+ translate("%(nick)s has been banned from the lobby.")
+ ]
+};
+
+KickStrings.prototype.Reason =
+ translateWithContext("lobby kick", "Reason: %(reason)s");
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/KickStrings.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerColor.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerColor.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerColor.js (revision 23172)
@@ -0,0 +1,53 @@
+/**
+ * The purpose of this class is to determine a color per playername and to apply that color,
+ * escape reserved characters and add a moderator prefix when displaying playernames.
+ */
+class PlayerColor
+{
+}
+
+/**
+ * Generate a (mostly) unique color for this player based on their name.
+ * @see https://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-jquery-javascript
+ */
+PlayerColor.GetPlayerColor = function(playername)
+{
+ // Generate a probably-unique hash for the player name and use that to create a color.
+ let hash = 0;
+ for (let i in playername)
+ 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.
+ let [h, s, l] = rgbToHsl(hash >> 24 & 0xFF, hash >> 16 & 0xFF, hash >> 8 & 0xFF);
+ return hslToRgb(h, s, Math.max(0.7, l)).join(" ");
+};
+
+/**
+ * Colorizes the given nickname with a color unique and deterministic for that player.
+ */
+PlayerColor.ColorPlayerName = function(playername, rating, role)
+{
+ let name = rating ?
+ sprintf(translate("%(nick)s (%(rating)s)"), {
+ "nick": playername,
+ "rating": rating
+ }) :
+ playername;
+
+ if (role == "moderator")
+ name = PlayerColor.ModeratorPrefix + name;
+
+ return coloredText(escapeText(name), PlayerColor.GetPlayerColor(playername));
+};
+
+/**
+ * A symbol which is prepended to the nickname of moderators.
+ */
+PlayerColor.ModeratorPrefix = "@";
+
+
+// TODO: Remove global required by formatPlayerInfo
+var getPlayerColor = PlayerColor.GetPlayerColor;
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerColor.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ProfilePanel.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ProfilePanel.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ProfilePanel.js (revision 23172)
@@ -0,0 +1,125 @@
+/**
+ * This class fetches and displays player profile data,
+ * where the player had been selected in the playerlist or leaderboard.
+ */
+class ProfilePanel
+{
+ constructor(xmppMessages, playerList, leaderboardPage)
+ {
+ // Playerlist or leaderboard selection
+ this.requestedPlayer = undefined;
+
+ // Playerlist selection
+ this.selectedPlayer = undefined;
+
+ this.roleText = Engine.GetGUIObjectByName("roleText");
+ this.ratioText = Engine.GetGUIObjectByName("ratioText");
+ this.lossesText = Engine.GetGUIObjectByName("lossesText");
+ this.winsText = Engine.GetGUIObjectByName("winsText");
+ this.totalGamesText = Engine.GetGUIObjectByName("totalGamesText");
+ this.highestRatingText = Engine.GetGUIObjectByName("highestRatingText");
+ this.rankText = Engine.GetGUIObjectByName("rankText");
+ this.fade = Engine.GetGUIObjectByName("fade");
+ this.playernameText = Engine.GetGUIObjectByName("playernameText");
+ this.profileArea = Engine.GetGUIObjectByName("profileArea");
+
+ xmppMessages.registerXmppMessageHandler("game", "profile", this.onProfile.bind(this));
+ xmppMessages.registerXmppMessageHandler("chat", "role", this.onRoleChange.bind(this));
+
+ playerList.registerSelectionChangeHandler(this.onPlayerListSelection.bind(this));
+
+ leaderboardPage.registerOpenPageHandler(this.onLeaderboardOpenPage.bind(this));
+ leaderboardPage.registerClosePageHandler(this.onLeaderboardClosePage.bind(this));
+ leaderboardPage.leaderboardList.registerSelectionChangeHandler(this.onLeaderboardSelectionChange.bind(this));
+ }
+
+ onPlayerListSelection(playerName)
+ {
+ this.selectedPlayer = playerName;
+ this.requestProfile(playerName);
+ }
+
+ onRoleChange(message)
+ {
+ if (message.nick == this.requestedPlayer)
+ this.updatePlayerRoleText(this.requestedPlayer);
+ }
+
+ onLeaderboardOpenPage(playerName)
+ {
+ this.requestProfile(playerName);
+ }
+
+ onLeaderboardSelectionChange(playerName)
+ {
+ this.requestProfile(playerName);
+ }
+
+ onLeaderboardClosePage()
+ {
+ this.requestProfile(this.selectedPlayer);
+ }
+
+ updatePlayerRoleText(playerName)
+ {
+ this.roleText.caption = this.RoleNames[Engine.LobbyGetPlayerRole(playerName) || "participant"];
+ }
+
+ requestProfile(playerName)
+ {
+ this.profileArea.hidden = !playerName && !this.playernameText.caption;
+ this.requestedPlayer = playerName;
+ if (!playerName)
+ return;
+
+ this.playernameText.caption = playerName;
+ this.updatePlayerRoleText(playerName);
+
+ this.rankText.caption = this.NotAvailable;
+ this.highestRatingText.caption = this.NotAvailable;
+ this.totalGamesText.caption = this.NotAvailable;
+ this.winsText.caption = this.NotAvailable;
+ this.lossesText.caption = this.NotAvailable;
+ this.ratioText.caption = this.NotAvailable;
+
+ Engine.SendGetProfile(playerName);
+ }
+
+ onProfile()
+ {
+ let attributes = Engine.GetProfile()[0];
+ if (attributes.rating == "-2" || attributes.player != this.requestedPlayer)
+ return;
+
+ this.playernameText.caption = attributes.player;
+ this.updatePlayerRoleText(attributes.player);
+
+ this.rankText.caption = attributes.rank;
+ this.highestRatingText.caption = attributes.highestRating;
+ this.totalGamesText.caption = attributes.totalGamesPlayed;
+ this.winsText.caption = attributes.wins;
+ this.lossesText.caption = attributes.losses;
+ this.ratioText.caption = ProfilePanel.FormatWinRate(attributes);
+ }
+}
+
+ProfilePanel.prototype.NotAvailable = translate("N/A");
+
+/**
+ * These role names correspond to the names constructed by the XmppClient.
+ */
+ProfilePanel.prototype.RoleNames = {
+ "moderator": translate("Moderator"),
+ "participant": translate("Player"),
+ "visitor": translate("Muted Player")
+};
+
+ProfilePanel.FormatWinRate = function(attr)
+{
+ if (!attr.totalGamesPlayed)
+ return translateWithContext("Used for an undefined winning rate", "-");
+
+ return sprintf(translate("%(percentage)s%%"), {
+ "percentage": (attr.wins / attr.totalGamesPlayed * 100).toFixed(2)
+ });
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ProfilePanel.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Subject.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Subject.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Subject.xml (revision 23172)
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Subject.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/OpenGame.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/OpenGame.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/OpenGame.js (revision 23172)
@@ -0,0 +1,24 @@
+GameListFilters.OpenGame = class
+{
+ constructor(onFilterChange)
+ {
+ this.checked = false;
+ this.onFilterChange = onFilterChange;
+
+ this.filterOpenGames = Engine.GetGUIObjectByName("filterOpenGames");
+ this.filterOpenGames.checked = false;
+ this.filterOpenGames.onPress = this.onPress.bind(this);
+ }
+
+ onPress()
+ {
+ this.checked = this.filterOpenGames.checked;
+ this.onFilterChange();
+ }
+
+ filter(game)
+ {
+ let stanza = game.stanza;
+ return !this.checked || stanza.state == "init" && stanza.nbp < stanza.maxnbp;
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/OpenGame.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters.xml (revision 23172)
@@ -0,0 +1,37 @@
+
+
+
+
+ Show only open games
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.xml (revision 23172)
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+ Multiplayer Lobby
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.xml (revision 23172)
@@ -0,0 +1,20 @@
+
+
+
+
+ Status
+
+
+ Name
+
+
+ Rating
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Subject.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Subject.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Subject.js (revision 23172)
@@ -0,0 +1,45 @@
+/**
+ * The purpose of this class is to display the room subject in a panel and to
+ * update it when a new one was received.
+ */
+// TODO: It should be easily possible to view the subject after a game has been selected
+class Subject
+{
+ constructor(dialog, xmppMessages, gameList)
+ {
+ this.subjectPanel = Engine.GetGUIObjectByName("subjectPanel");
+ this.subjectText = Engine.GetGUIObjectByName("subjectText");
+ this.subjectBox = Engine.GetGUIObjectByName("subjectBox");
+ this.logoTop = Engine.GetGUIObjectByName("logoTop");
+ this.logoCenter = Engine.GetGUIObjectByName("logoCenter");
+
+ this.updateSubject(Engine.LobbyGetRoomSubject());
+
+ xmppMessages.registerXmppMessageHandler("chat", "subject", this.onSubject.bind(this));
+ gameList.registerSelectionChangeHandler(this.onGameListSelectionChange.bind(this));
+
+ let bottom = Engine.GetGUIObjectByName(dialog ? "leaveButton" : "hostButton").size.top - 5;
+ let size = this.subjectPanel.size;
+ size.bottom = bottom;
+ this.subjectPanel.size = size;
+ }
+
+ onGameListSelectionChange(game)
+ {
+ this.subjectPanel.hidden = !!game;
+ }
+
+ onSubject(message)
+ {
+ this.updateSubject(message.subject);
+ }
+
+ updateSubject(subject)
+ {
+ subject = subject.trim();
+ this.subjectBox.hidden = !subject;
+ this.subjectText.caption = subject;
+ this.logoTop.hidden = !subject;
+ this.logoCenter.hidden = !!subject;
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Subject.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/TimestampWrapper.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/TimestampWrapper.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/TimestampWrapper.js (revision 23172)
@@ -0,0 +1,33 @@
+/**
+ * This class wraps a string with a timestamp dating to when the message was sent.
+ */
+class TimestampWrapper
+{
+ constructor()
+ {
+ this.timeArgs = {};
+ this.timestampArgs = {};
+ }
+
+ format(timestamp, text)
+ {
+ this.timeArgs.time =
+ Engine.FormatMillisecondsIntoDateStringLocal(timestamp * 1000, this.TimeFormat);
+
+ this.timestampArgs.time = sprintf(this.TimestampFormat, this.timeArgs);
+ this.timestampArgs.message = text;
+
+ return sprintf(this.TimestampedMessageFormat, this.timestampArgs);
+ }
+}
+
+// Translation: Chat message format when there is a time prefix.
+TimestampWrapper.prototype.TimestampedMessageFormat = translate("%(time)s %(message)s");
+
+// Translation: Time prefix as shown in the multiplayer lobby (when you enable it in the options page).
+TimestampWrapper.prototype.TimestampFormat = translate("\\[%(time)s]");
+
+// 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
+TimestampWrapper.prototype.TimeFormat = translate("HH:mm");
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Chat/TimestampWrapper.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js (revision 23172)
@@ -0,0 +1,338 @@
+/**
+ * This class represents a multiplayer match hosted by a player in the lobby.
+ * Having this represented as a class allows to leverage significant performance
+ * gains by caching computed, escaped, translated strings and sorting keys.
+ *
+ * Additionally class representation allows implementation of events such as
+ * a new match being hosted, a match having ended, or a buddy having joined a match.
+ *
+ * Ensure that escapeText is applied to player controlled data for display.
+ *
+ * Users of the properties of this class:
+ * GameList, GameDetails, MapFilters, JoinButton, any user of GameList.selectedGame()
+ */
+class Game
+{
+ constructor()
+ {
+ // Stanza data, object with exclusively string values
+ // Used to compare which part of the stanza data changed,
+ // perform partial updates and trigger event notifications.
+ this.stanza = {};
+ for (let name of this.StanzaKeys)
+ this.stanza[name] = "";
+
+ // This will be displayed in the GameList and GameDetails
+ // Important: Player input must be processed with escapeText
+ this.displayData = {
+ "tags": {}
+ };
+
+ // Cache the values used for sorting
+ this.sortValues = {
+ "state": "",
+ "compatibility": "",
+ "hasBuddyString": ""
+ };
+
+ // Array of objects, result of stringifiedTeamListToPlayerData
+ this.players = [];
+
+ // Whether the current player has the same mods launched as the host of this game
+ this.isCompatible = undefined;
+
+ // Used to display which mods are missing if the player attempts a join
+ this.mods = undefined;
+
+ // Used by the rating column and rating filer
+ this.gameRating = undefined;
+
+ // 'Persistent temporary' sprintf arguments object to avoid repeated object construction
+ this.playerCountArgs = {};
+ }
+
+ /**
+ * Called from GameList to ensure call order.
+ */
+ onBuddyChange()
+ {
+ this.updatePlayers(this.stanza);
+ }
+
+ /**
+ * This function computes values that will either certainly or
+ * most likely be used later (i.e. by filtering, sorting and gamelist display).
+ *
+ * The performance benefit arises from the fact that for a new gamelist stanza
+ * many if not most games and game properties did not change.
+ */
+ update(newStanza, sortKey)
+ {
+ let oldStanza = this.stanza;
+ let displayData = this.displayData;
+ let sortValues = this.sortValues;
+
+ if (oldStanza.name != newStanza.name)
+ {
+ Engine.ProfileStart("gameName");
+ sortValues.gameName = newStanza.name.toLowerCase();
+ this.updateGameName(newStanza);
+ Engine.ProfileStop();
+ }
+
+ if (oldStanza.state != newStanza.state)
+ {
+ Engine.ProfileStart("gameState");
+ this.updateGameTags(newStanza);
+ sortValues.state = this.GameStatusOrder.indexOf(newStanza.state);
+ Engine.ProfileStop();
+ }
+
+ if (oldStanza.niceMapName != newStanza.niceMapName)
+ {
+ Engine.ProfileStart("niceMapName");
+ displayData.mapName = escapeText(translateMapTitle(newStanza.niceMapName));
+ Engine.ProfileStop();
+ }
+
+ if (oldStanza.mapName != newStanza.mapName)
+ {
+ Engine.ProfileStart("mapName");
+ sortValues.mapName = displayData.mapName;
+ Engine.ProfileStop();
+ }
+
+ if (oldStanza.mapType != newStanza.mapType)
+ {
+ Engine.ProfileStart("mapType");
+ displayData.mapType = g_MapTypes.Title[g_MapTypes.Name.indexOf(newStanza.mapType)] || "";
+ sortValues.mapType = newStanza.mapType;
+ Engine.ProfileStop();
+ }
+
+ if (oldStanza.mapSize != newStanza.mapSize)
+ {
+ Engine.ProfileStart("mapSize");
+ displayData.mapSize = translateMapSize(newStanza.mapSize);
+ sortValues.mapSize = newStanza.mapSize;
+ Engine.ProfileStop();
+ }
+
+ let playersChanged = oldStanza.players != newStanza.players;
+ if (playersChanged)
+ {
+ Engine.ProfileStart("playerData");
+ this.updatePlayers(newStanza);
+ Engine.ProfileStop();
+ }
+
+ if (oldStanza.nbp != newStanza.nbp ||
+ oldStanza.maxnbp != newStanza.maxnbp ||
+ playersChanged)
+ {
+ Engine.ProfileStart("playerCount");
+ displayData.playerCount = this.getTranslatedPlayerCount(newStanza);
+ sortValues.maxnbp = newStanza.maxnbp;
+ Engine.ProfileStop();
+ }
+
+ if (oldStanza.mods != newStanza.mods)
+ {
+ Engine.ProfileStart("mods");
+ this.updateMods(newStanza);
+ Engine.ProfileStop();
+ }
+
+ this.stanza = newStanza;
+ this.sortValue = this.sortValues[sortKey];
+ }
+
+ updatePlayers(newStanza)
+ {
+ let players;
+ {
+ Engine.ProfileStart("stringifiedTeamListToPlayerData");
+ players = stringifiedTeamListToPlayerData(newStanza.players);
+ this.players = players;
+ Engine.ProfileStop();
+ }
+
+ {
+ Engine.ProfileStart("parsePlayers");
+ let observerCount = 0;
+ let hasBuddies = 0;
+
+ let playerRatingTotal = 0;
+ for (let player of players)
+ {
+ let playerNickRating = splitRatingFromNick(player.Name);
+
+ if (player.Team == "observer")
+ ++observerCount;
+ else
+ playerRatingTotal += playerNickRating.rating || g_DefaultLobbyRating;
+
+ // Sort games with playing buddies above games with spectating buddies
+ if (hasBuddies < 2 && g_Buddies.indexOf(playerNickRating.nick) != -1)
+ hasBuddies = player.Team == "observer" ? 1 : 2;
+ }
+
+ this.observerCount = observerCount;
+ this.hasBuddies = hasBuddies;
+
+ let displayData = this.displayData;
+ let sortValues = this.sortValues;
+ displayData.buddy = this.hasBuddies ? setStringTags(g_BuddySymbol, displayData.tags) : "";
+ sortValues.hasBuddyString = String(hasBuddies);
+ sortValues.buddy = sortValues.hasBuddyString + sortValues.gameName;
+
+ let playerCount = players.length - observerCount;
+ let gameRating =
+ playerCount ?
+ Math.round(playerRatingTotal / playerCount) :
+ g_DefaultLobbyRating;
+ this.gameRating = gameRating;
+ sortValues.gameRating = gameRating;
+ Engine.ProfileStop();
+ }
+ }
+
+ updateMods(newStanza)
+ {
+ {
+ Engine.ProfileStart("JSON.parse");
+ try
+ {
+ this.mods = JSON.parse(newStanza.mods);
+ }
+ catch (e)
+ {
+ this.mods = [];
+ }
+ Engine.ProfileStop();
+ }
+
+ {
+ Engine.ProfileStart("hasSameMods");
+ let isCompatible = this.mods && hasSameMods(this.mods, Engine.GetEngineInfo().mods);
+ if (this.isCompatible != isCompatible)
+ {
+ this.isCompatible = isCompatible;
+ this.updateGameTags(newStanza);
+ this.sortValues.compatibility = String(isCompatible);
+ }
+ Engine.ProfileStop();
+ }
+ }
+
+ updateGameTags(newStanza)
+ {
+ let displayData = this.displayData;
+ displayData.tags = this.isCompatible ? this.StateTags[newStanza.state] : this.IncompatibleTags;
+ displayData.buddy = this.hasBuddies ? setStringTags(g_BuddySymbol, displayData.tags) : "";
+ this.updateGameName(newStanza);
+ }
+
+ updateGameName(newStanza)
+ {
+ let displayData = this.displayData;
+ displayData.gameName = setStringTags(escapeText(newStanza.name), displayData.tags);
+
+ let sortValues = this.sortValues;
+ sortValues.gameName = sortValues.compatibility + sortValues.state + sortValues.gameName;
+ sortValues.buddy = sortValues.hasBuddyString + sortValues.gameName;
+ }
+
+ getTranslatedPlayerCount(newStanza)
+ {
+ let playerCountArgs = this.playerCountArgs;
+ playerCountArgs.current = setStringTags(escapeText(newStanza.nbp), this.PlayerCountTags.CurrentPlayers);
+ playerCountArgs.max = setStringTags(escapeText(newStanza.maxnbp), this.PlayerCountTags.MaxPlayers);
+
+ let txt;
+ if (this.observerCount)
+ {
+ playerCountArgs.observercount = setStringTags(this.observerCount, this.PlayerCountTags.Observers);
+ txt = this.PlayerCountObservers;
+ }
+ else
+ txt = this.PlayerCountNoObservers;
+
+ return sprintf(txt, playerCountArgs);
+ }
+}
+
+/**
+ * These are all keys that occur in a gamelist stanza sent by XPartaMupp.
+ */
+Game.prototype.StanzaKeys = [
+ "name",
+ "ip",
+ "port",
+ "stunIP",
+ "stunPort",
+ "hostUsername",
+ "state",
+ "nbp",
+ "maxnbp",
+ "players",
+ "mapName",
+ "niceMapName",
+ "mapSize",
+ "mapType",
+ "victoryConditions",
+ "startTime",
+ "mods"
+];
+
+/**
+ * Initial sorting order of the gamelist.
+ */
+Game.prototype.GameStatusOrder = [
+ "init",
+ "waiting",
+ "running"
+];
+
+// Translation: The number of players and observers in this game
+Game.prototype.PlayerCountObservers = translate("%(current)s/%(max)s +%(observercount)s");
+
+// Translation: The number of players in this game
+Game.prototype.PlayerCountNoObservers = translate("%(current)s/%(max)s");
+
+/**
+ * Compatible games will be listed in these colors.
+ */
+Game.prototype.StateTags = {
+ "init": {
+ "color": "0 219 0"
+ },
+ "waiting": {
+ "color": "255 127 0"
+ },
+ "running": {
+ "color": "219 0 0"
+ }
+};
+
+/**
+ * Games that require different mods than the ones launched by the current player are grayed out.
+ */
+Game.prototype.IncompatibleTags = {
+ "color": "gray"
+};
+
+/**
+ * Color for the player count number in the games list.
+ */
+Game.prototype.PlayerCountTags = {
+ "CurrentPlayers": {
+ "color": "0 160 160"
+ },
+ "MaxPlayers": {
+ "color": "0 160 160"
+ },
+ "Observers": {
+ "color": "0 128 128"
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml (revision 23172)
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.xml (revision 23172)
@@ -0,0 +1,29 @@
+
+
+
+
+ Name
+
+
+ Map Name
+
+
+ Size
+
+
+ Type
+
+
+ Players
+
+
+ Rating
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/MapType.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/MapType.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/MapType.js (revision 23172)
@@ -0,0 +1,25 @@
+GameListFilters.MapType = class
+{
+ constructor(onFilterChange)
+ {
+ this.selected = "";
+ this.onFilterChange = onFilterChange;
+
+ this.mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
+ this.mapTypeFilter.list = [translateWithContext("map", "Any")].concat(g_MapTypes.Title);
+ this.mapTypeFilter.list_data = [""].concat(g_MapTypes.Name);
+ this.mapTypeFilter.selected = g_MapTypes.Default;
+ this.mapTypeFilter.onSelectionChange = this.onSelectionChange.bind(this);
+ }
+
+ onSelectionChange()
+ {
+ this.selected = this.mapTypeFilter.list_data[this.mapTypeFilter.selected];
+ this.onFilterChange();
+ }
+
+ filter(game)
+ {
+ return !this.selected || game.stanza.mapType == this.selected;
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/MapType.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/Rating.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/Rating.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/Rating.js (revision 23172)
@@ -0,0 +1,75 @@
+GameListFilters.Rating = class
+{
+ constructor(onFilterChange)
+ {
+ this.enabled = undefined;
+ this.onFilterChange = onFilterChange;
+ this.filter = () => true;
+
+ this.gameRatingFilter = Engine.GetGUIObjectByName("gameRatingFilter");
+ this.gameRatingFilter.list = [
+ translateWithContext("map", "Any"),
+ ...this.RatingFilters
+ ];
+ this.gameRatingFilter.list_data = [
+ "",
+ ...this.RatingFilters.map(r =>
+ sprintf(
+ r[0] == ">" ?
+ translateWithContext("gamelist filter", "> %(rating)s") :
+ translateWithContext("gamelist filter", "< %(rating)s"),
+ {
+ "rating": r.substr(1)
+ }))
+ ];
+
+ this.gameRatingFilter.selected = 0;
+ this.gameRatingFilter.onSelectionChange = this.onSelectionChange.bind(this);
+
+ this.setEnabled(Engine.ConfigDB_GetValue("user", "lobby.columns.gamerating") == "true");
+ }
+
+ setEnabled(enabled)
+ {
+ this.enabled = enabled;
+ this.gameRatingFilter.hidden = !this.enabled;
+ if (!enabled)
+ return;
+
+ // TODO: COList should expose the precise column width
+ // Hide element to compensate width
+ let mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
+ mapTypeFilter.hidden = enabled;
+ let playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
+ let mapTypeFilterSize = mapTypeFilter.size;
+ let size = playersNumberFilter.size;
+ size.rleft = mapTypeFilterSize.rleft;
+ size.rright = this.gameRatingFilter.size.rleft;
+ playersNumberFilter.size = size;
+ }
+
+ onSelectionChange()
+ {
+ let selectedType = this.gameRatingFilter.list_data[this.gameRatingFilter.selected];
+ let selectedRating = +selectedType.substr(1);
+
+ this.filter =
+ (!this.enabled || !selectedType) ?
+ () => true :
+ selectedType.startsWith(">") ?
+ game => game.gameRating >= selectedRating :
+ game => game.gameRating < selectedRating;
+
+ this.onFilterChange();
+ }
+};
+
+GameListFilters.Rating.prototype.RatingFilters = [
+ ">1500",
+ ">1400",
+ ">1300",
+ ">1200",
+ "<1200",
+ "<1100",
+ "<1000"
+];
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameListFilters/Rating.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.js (revision 23172)
@@ -0,0 +1,73 @@
+/**
+ * This class stores the handlers for all GUI objects in the lobby page,
+ * (excluding other pages in the same context such as leaderboard and profile page).
+ */
+class LobbyPage
+{
+ constructor(dialog, xmppMessages, leaderboardPage, profilePage)
+ {
+ Engine.ProfileStart("Create LobbyPage");
+ let buddyButton = new BuddyButton(xmppMessages);
+ let gameList = new GameList(xmppMessages, buddyButton);
+ let playerList = new PlayerList(xmppMessages, buddyButton, gameList);
+
+ this.lobbyPage = {
+ "buttons": {
+ "buddyButton": buddyButton,
+ "hostButton": new HostButton(dialog, xmppMessages),
+ "joinButton": new JoinButton(dialog, gameList),
+ "leaderboardButton": new LeaderboardButton(xmppMessages, leaderboardPage),
+ "profileButton": new ProfileButton(xmppMessages, profilePage),
+ "quitButton": new QuitButton(dialog, leaderboardPage, profilePage)
+ },
+ "panels": {
+ "chatPanel": new ChatPanel(xmppMessages),
+ "gameDetails": new GameDetails(dialog, gameList),
+ "gameList": gameList,
+ "playerList": playerList,
+ "profilePanel": new ProfilePanel(xmppMessages, playerList, leaderboardPage),
+ "subject": new Subject(dialog, xmppMessages, gameList)
+ },
+ "eventHandlers": {
+ "announcementHandler": new AnnouncementHandler(xmppMessages),
+ "connectionHandler": new ConnectionHandler(xmppMessages),
+ }
+ };
+
+ if (dialog)
+ this.setDialogStyle();
+ Engine.ProfileStop();
+ }
+
+ setDialogStyle()
+ {
+ {
+ let lobbyPage = Engine.GetGUIObjectByName("lobbyPage");
+ lobbyPage.sprite = "ModernDialog";
+
+ let size = lobbyPage.size;
+ size.left = this.WindowMargin;
+ size.top = this.WindowMargin;
+ size.right = -this.WindowMargin;
+ size.bottom = -this.WindowMargin;
+ lobbyPage.size = size;
+ }
+
+ {
+ let lobbyPageTitle = Engine.GetGUIObjectByName("lobbyPageTitle");
+ let size = lobbyPageTitle.size;
+ size.top -= this.WindowMargin / 2;
+ size.bottom -= this.WindowMargin / 2;
+ lobbyPageTitle.size = size;
+ }
+
+ {
+ let lobbyPanels = Engine.GetGUIObjectByName("lobbyPanels");
+ let size = lobbyPanels.size;
+ size.top -= this.WindowMargin / 2;
+ lobbyPanels.size = size;
+ }
+ }
+}
+
+LobbyPage.prototype.WindowMargin = 40;
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.js (revision 23172)
@@ -0,0 +1,207 @@
+/**
+ * This class is concerned with displaying players who are online and
+ * triggering handlers when selecting or doubleclicking on a player.
+ */
+class PlayerList
+{
+ constructor(xmppMessages, buddyButton, gameList)
+ {
+ this.gameList = gameList;
+ this.selectedPlayer = undefined;
+ this.statusOrder = Object.keys(this.PlayerStatuses);
+
+ // Avoid repeated array construction for performance
+ this.buddyStatusList = [];
+ this.playerList = [];
+ this.presenceList = [];
+ this.nickList = [];
+ this.ratingList = [];
+
+ this.selectionChangeHandlers = new Set();
+ this.mouseLeftDoubleClickItemHandlers = new Set();
+
+ this.playersBox = Engine.GetGUIObjectByName("playersBox");
+ this.playersBox.onSelectionChange = this.onPlayerListSelection.bind(this);
+ this.playersBox.onSelectionColumnChange = this.rebuildPlayerList.bind(this);
+ this.playersBox.onMouseLeftClickItem = this.onMouseLeftClickItem.bind(this);
+ this.playersBox.onMouseLeftDoubleClickItem = this.onMouseLeftDoubleClickItem.bind(this);
+
+ buddyButton.registerBuddyChangeHandler(this.onBuddyChange.bind(this));
+ xmppMessages.registerPlayerListUpdateHandler(this.rebuildPlayerList.bind(this));
+ this.registerSelectionChangeHandler(buddyButton.onPlayerSelectionChange.bind(buddyButton));
+ this.registerMouseLeftDoubleClickItemHandler(buddyButton.onPress.bind(buddyButton));
+
+ this.rebuildPlayerList();
+ }
+
+ registerSelectionChangeHandler(handler)
+ {
+ this.selectionChangeHandlers.add(handler);
+ }
+
+ registerMouseLeftDoubleClickItemHandler(handler)
+ {
+ this.mouseLeftDoubleClickItemHandlers.add(handler);
+ }
+
+ onBuddyChange()
+ {
+ this.rebuildPlayerList();
+ }
+
+ onMouseLeftDoubleClickItem()
+ {
+ for (let handler of this.mouseLeftDoubleClickItemHandlers)
+ handler();
+ }
+
+ onMouseLeftClickItem()
+ {
+ // In case of clicking on the same player again
+ this.gameList.selectGameFromPlayername(this.selectedPlayer);
+ }
+
+
+ onPlayerListSelection()
+ {
+ if (this.playersBox.selected == this.playersBox.list.indexOf(this.selectedPlayer))
+ return;
+
+ this.selectedPlayer = this.playersBox.list[this.playersBox.selected];
+
+ this.gameList.selectGameFromPlayername(this.selectedPlayer);
+
+ for (let handler of this.selectionChangeHandlers)
+ handler(this.selectedPlayer);
+ }
+
+ parsePlayer(sortKey, player)
+ {
+ player.isBuddy = g_Buddies.indexOf(player.name) != -1;
+
+ switch (sortKey)
+ {
+ case 'buddy':
+ player.sortValue = (player.isBuddy ? 1 : 2) + this.statusOrder.indexOf(player.presence) + player.name.toLowerCase();
+ break;
+ case 'rating':
+ player.sortValue = +player.rating;
+ break;
+ case 'status':
+ player.sortValue = this.statusOrder.indexOf(player.presence) + player.name.toLowerCase();
+ break;
+ case 'name':
+ default:
+ player.sortValue = player.name.toLowerCase();
+ break;
+ }
+ }
+
+ /**
+ * Do a full update of the player listing, including ratings from cached C++ information.
+ * Important: This should only be performed once if
+ * there have been multiple messages received changing this list.
+ */
+ rebuildPlayerList()
+ {
+ Engine.ProfileStart("rebuildPlaersList");
+
+ Engine.ProfileStart("getPlayerList");
+ let playerList = Engine.GetPlayerList();
+ Engine.ProfileStop();
+
+ Engine.ProfileStart("parsePlayers");
+ playerList.forEach(this.parsePlayer.bind(this, this.playersBox.selected_column));
+ Engine.ProfileStop();
+
+ Engine.ProfileStart("sortPlayers");
+ playerList.sort(this.sortPlayers.bind(this, this.playersBox.selected_column_order));
+ Engine.ProfileStop();
+
+ Engine.ProfileStart("prepareList");
+ let length = playerList.length;
+ this.buddyStatusList.length = length;
+ this.playerList.length = length;
+ this.presenceList.length = length;
+ this.nickList.length = length;
+ this.ratingList.length = length;
+
+ playerList.forEach((player, i) => {
+ // TODO: COList.cpp columns should support horizontal center align
+ let rating = player.rating ? (" " + player.rating).substr(-5) : " -";
+
+ let presence = this.PlayerStatuses[player.presence] ? player.presence : "unknown";
+ if (presence == "unknown")
+ warn("Unknown presence:" + player.presence);
+
+ let statusTags = this.PlayerStatuses[presence].tags;
+ this.buddyStatusList[i] = player.isBuddy ? setStringTags(g_BuddySymbol, statusTags) : "";
+ this.playerList[i] = PlayerColor.ColorPlayerName(player.name, "", player.role);
+ this.presenceList[i] = setStringTags(this.PlayerStatuses[presence].status, statusTags);
+ this.ratingList[i] = setStringTags(rating, statusTags);
+ this.nickList[i] = escapeText(player.name);
+ });
+ Engine.ProfileStop();
+
+ Engine.ProfileStart("copyToGUI");
+ this.playersBox.list_buddy = this.buddyStatusList;
+ this.playersBox.list_name = this.playerList;
+ this.playersBox.list_status = this.presenceList;
+ this.playersBox.list_rating = this.ratingList;
+ this.playersBox.list = this.nickList;
+ Engine.ProfileStop();
+
+ Engine.ProfileStart("selectionChange");
+ this.playersBox.selected = this.playersBox.list.indexOf(this.selectedPlayer);
+ Engine.ProfileStop();
+
+ Engine.ProfileStop();
+ }
+
+ sortPlayers(sortOrder, player1, player2)
+ {
+ if (player1.sortValue < player2.sortValue)
+ return -sortOrder;
+
+ if (player1.sortValue > player2.sortValue)
+ return +sortOrder;
+
+ return 0;
+ }
+}
+
+/**
+ * The playerlist will be assembled using these values.
+ */
+PlayerList.prototype.PlayerStatuses = {
+ "available": {
+ "status": translate("Online"),
+ "tags": {
+ "color": "0 219 0"
+ }
+ },
+ "away": {
+ "status": translate("Away"),
+ "tags": {
+ "color": "229 76 13"
+ }
+ },
+ "playing": {
+ "status": translate("Busy"),
+ "tags": {
+ "color": "200 0 0"
+ }
+ },
+ "offline": {
+ "status": translate("Offline"),
+ "tags": {
+ "color": "0 0 0"
+ }
+ },
+ "unknown": {
+ "status": translateWithContext("lobby presence", "Unknown"),
+ "tags": {
+ "color": "178 178 178"
+ }
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ProfilePanel.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ProfilePanel.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ProfilePanel.xml (revision 23172)
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+ Current Rank:
+
+
+
+ Highest Rating:
+
+
+
+ Total Games:
+
+
+
+ Wins:
+
+
+
+ Losses:
+
+
+
+ Win Rate:
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/ProfilePanel.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/ProfilePage/ProfilePage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/ProfilePage/ProfilePage.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/ProfilePage/ProfilePage.js (revision 23172)
@@ -0,0 +1,84 @@
+/**
+ * The profile page enables the player to lookup statistics of an arbitrary player.
+ */
+class ProfilePage
+{
+ constructor(xmppMessages)
+ {
+ this.requestedPlayer = undefined;
+ this.closePageHandlers = new Set();
+
+ this.profilePage = Engine.GetGUIObjectByName("profilePage");
+
+ this.fetchInput = Engine.GetGUIObjectByName("fetchInput");
+ this.fetchInput.onPress = this.onPressLookup.bind(this);
+
+ Engine.GetGUIObjectByName("viewProfileButton").onPress = this.onPressLookup.bind(this);
+ Engine.GetGUIObjectByName("profileBackButton").onPress = this.onPressClose.bind(this, true);
+
+ this.profilePlayernameText = Engine.GetGUIObjectByName("profilePlayernameText");
+ this.profileRankText = Engine.GetGUIObjectByName("profileRankText");
+ this.profileHighestRatingText = Engine.GetGUIObjectByName("profileHighestRatingText");
+ this.profileTotalGamesText = Engine.GetGUIObjectByName("profileTotalGamesText");
+ this.profileWinsText = Engine.GetGUIObjectByName("profileWinsText");
+ this.profileLossesText = Engine.GetGUIObjectByName("profileLossesText");
+ this.profileRatioText = Engine.GetGUIObjectByName("profileRatioText");
+ this.profileErrorText = Engine.GetGUIObjectByName("profileErrorText");
+ this.profileWindowArea = Engine.GetGUIObjectByName("profileWindowArea");
+
+ xmppMessages.registerXmppMessageHandler("game", "profile", this.onProfile.bind(this));
+ }
+
+ registerClosePageHandler(handler)
+ {
+ this.closePageHandlers.add(handler);
+ }
+
+ openPage()
+ {
+ this.profilePage.hidden = false;
+ Engine.SetGlobalHotkey("cancel", this.onPressClose.bind(this));
+ }
+
+ onPressLookup()
+ {
+ this.requestedPlayer = this.fetchInput.caption;
+ Engine.SendGetProfile(this.requestedPlayer);
+ }
+
+ onPressClose()
+ {
+ this.profilePage.hidden = true;
+
+ for (let handler of this.closePageHandlers)
+ handler();
+ }
+
+ onProfile()
+ {
+ let attributes = Engine.GetProfile()[0];
+ if (this.profilePage.hidden || this.requestedPlayer != attributes.player)
+ return;
+
+ let profileFound = attributes.rating != "-2";
+ this.profileWindowArea.hidden = !profileFound;
+ this.profileErrorText.hidden = profileFound;
+
+ if (!profileFound)
+ {
+ this.profileErrorText.caption =
+ sprintf(translate("Player \"%(nick)s\" not found."), {
+ "nick": escapeText(attributes.player)
+ });
+ return;
+ }
+
+ this.profilePlayernameText.caption = escapeText(attributes.player);
+ this.profileRankText.caption = attributes.rank;
+ this.profileHighestRatingText.caption = attributes.highestRating;
+ this.profileTotalGamesText.caption = attributes.totalGamesPlayed;
+ this.profileWinsText.caption = attributes.wins;
+ this.profileLossesText.caption = attributes.losses;
+ this.profileRatioText.caption = ProfilePanel.FormatWinRate(attributes);
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/ProfilePage/ProfilePage.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/lobby/ProfilePage/ProfilePage.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/ProfilePage/ProfilePage.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/ProfilePage/ProfilePage.xml (revision 23172)
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+ Player Profile Lookup
+
+
+
+ Enter playername:
+
+
+
+
+
+ View Profile
+
+
+
+
+
+
+
+ Current Rank:
+
+
+
+ Highest Rating:
+
+
+
+ Total Games:
+
+
+
+ Wins:
+
+
+
+ Losses:
+
+
+
+ Win Rate:
+
+
+
+
+ Please enter a player name.
+
+
+
+
+
+ Back
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/ProfilePage/ProfilePage.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/page_lobby.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/page_lobby.xml (revision 23171)
+++ ps/trunk/binaries/data/mods/public/gui/page_lobby.xml (revision 23172)
@@ -1,12 +1,14 @@
common/modern/setup.xml
common/modern/styles.xml
common/modern/sprites.xml
- common/global.xml
common/styles.xml
common/sprites.xml
lobby/lobby.xml
+
+ common/global.xml
+
Index: ps/trunk/source/lobby/XmppClient.h
===================================================================
--- ps/trunk/source/lobby/XmppClient.h (revision 23171)
+++ ps/trunk/source/lobby/XmppClient.h (revision 23172)
@@ -1,198 +1,199 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef XXXMPPCLIENT_H
#define XXXMPPCLIENT_H
#include "IXmppClient.h"
#include
#include "glooxwrapper/glooxwrapper.h"
#include "scriptinterface/ScriptVal.h"
class ScriptInterface;
namespace glooxwrapper
{
class Client;
struct CertInfo;
}
class XmppClient : public IXmppClient, public glooxwrapper::ConnectionListener, public glooxwrapper::MUCRoomHandler, public glooxwrapper::IqHandler, public glooxwrapper::RegistrationHandler, public glooxwrapper::MessageHandler, public glooxwrapper::Jingle::SessionHandler
{
NONCOPYABLE(XmppClient);
private:
// Components
glooxwrapper::Client* m_client;
glooxwrapper::MUCRoom* m_mucRoom;
glooxwrapper::Registration* m_registration;
glooxwrapper::SessionManager* m_sessionManager;
// Account infos
std::string m_username;
std::string m_password;
std::string m_server;
std::string m_room;
std::string m_nick;
std::string m_xpartamuppId;
std::string m_echelonId;
// State
gloox::CertStatus m_certStatus;
bool m_initialLoadComplete;
bool m_isConnected;
public:
// Basic
XmppClient(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize = 0, const bool regOpt = false);
virtual ~XmppClient();
// JS::Heap is better for GC performance than JS::PersistentRooted
static void Trace(JSTracer *trc, void *data)
{
static_cast(data)->TraceMember(trc);
}
void TraceMember(JSTracer *trc);
// Network
void connect();
void disconnect();
bool isConnected();
void recv();
void SendIqGetBoardList();
void SendIqGetProfile(const std::string& player);
void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data);
void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data);
void SendIqUnregisterGame();
void SendIqChangeStateGame(const std::string& nbp, const std::string& players);
void SendIqLobbyAuth(const std::string& to, const std::string& token);
void SetNick(const std::string& nick);
void GetNick(std::string& nick);
void kick(const std::string& nick, const std::string& reason);
void ban(const std::string& nick, const std::string& reason);
void SetPresence(const std::string& presence);
const char* GetPresence(const std::string& nickname);
const char* GetRole(const std::string& nickname);
+ std::wstring GetRating(const std::string& nickname);
const std::wstring& GetSubject();
void GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
void GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
void GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
void GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
void SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJID);
/**
* Convert gloox values to string or time.
*/
static const char* GetPresenceString(const gloox::Presence::PresenceType presenceType);
static const char* GetRoleString(const gloox::MUCRoomRole role);
static std::string StanzaErrorToString(gloox::StanzaError err);
static std::string RegistrationResultToString(gloox::RegistrationResult res);
static std::string ConnectionErrorToString(gloox::ConnectionError err);
static std::string CertificateErrorToString(gloox::CertStatus status);
static std::time_t ComputeTimestamp(const glooxwrapper::Message& msg);
protected:
/* Xmpp handlers */
/* MUC handlers */
virtual void handleMUCParticipantPresence(glooxwrapper::MUCRoom& room, const glooxwrapper::MUCRoomParticipant, const glooxwrapper::Presence&);
virtual void handleMUCError(glooxwrapper::MUCRoom& room, gloox::StanzaError);
virtual void handleMUCMessage(glooxwrapper::MUCRoom& room, const glooxwrapper::Message& msg, bool priv);
virtual void handleMUCSubject(glooxwrapper::MUCRoom& room, const glooxwrapper::string& nick, const glooxwrapper::string& subject);
/* MUC handlers not supported by glooxwrapper */
// virtual bool handleMUCRoomCreation(glooxwrapper::MUCRoom*) {return false;}
// virtual void handleMUCInviteDecline(glooxwrapper::MUCRoom*, const glooxwrapper::JID&, const std::string&) {}
// virtual void handleMUCInfo(glooxwrapper::MUCRoom*, int, const std::string&, const glooxwrapper::DataForm*) {}
// virtual void handleMUCItems(glooxwrapper::MUCRoom*, const std::list >&) {}
/* Log handler */
virtual void handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message);
/* ConnectionListener handlers*/
virtual void onConnect();
virtual void onDisconnect(gloox::ConnectionError e);
virtual bool onTLSConnect(const glooxwrapper::CertInfo& info);
/* Iq Handlers */
virtual bool handleIq(const glooxwrapper::IQ& iq);
virtual void handleIqID(const glooxwrapper::IQ&, int) {}
/* Registration Handlers */
virtual void handleRegistrationFields(const glooxwrapper::JID& /*from*/, int fields, glooxwrapper::string instructions );
virtual void handleRegistrationResult(const glooxwrapper::JID& /*from*/, gloox::RegistrationResult result);
virtual void handleAlreadyRegistered(const glooxwrapper::JID& /*from*/);
virtual void handleDataForm(const glooxwrapper::JID& /*from*/, const glooxwrapper::DataForm& /*form*/);
virtual void handleOOB(const glooxwrapper::JID& /*from*/, const glooxwrapper::OOB& oob);
/* Message Handler */
virtual void handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession* session);
/* Session Handler */
virtual void handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle);
virtual void handleSessionInitiation(glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle);
public:
- JS::Value GuiPollNewMessage(const ScriptInterface& scriptInterface);
+ JS::Value GuiPollNewMessages(const ScriptInterface& scriptInterface);
JS::Value GuiPollHistoricMessages(const ScriptInterface& scriptInterface);
bool GuiPollHasPlayerListUpdate();
void SendMUCMessage(const std::string& message);
protected:
template
void CreateGUIMessage(
const std::string& type,
const std::string& level,
const std::time_t time,
Args const&... args);
private:
struct SPlayer {
SPlayer(const gloox::Presence::PresenceType presence, const gloox::MUCRoomRole role, const glooxwrapper::string& rating)
: m_Presence(presence), m_Role(role), m_Rating(rating)
{
}
gloox::Presence::PresenceType m_Presence;
gloox::MUCRoomRole m_Role;
glooxwrapper::string m_Rating;
};
using PlayerMap = std::map;
/// Map of players
PlayerMap m_PlayerMap;
/// Whether or not the playermap has changed since the last time the GUI checked.
bool m_PlayerMapUpdate;
/// List of games
std::vector m_GameList;
/// List of rankings
std::vector m_BoardList;
/// Profile data
std::vector m_Profile;
/// ScriptInterface to root the values
const ScriptInterface* m_ScriptInterface;
/// Queue of messages for the GUI
std::deque > m_GuiMessageQueue;
/// Cache of all GUI messages received since the login
std::vector > m_HistoricGuiMessages;
/// Current room subject/topic.
std::wstring m_Subject;
};
#endif // XMPPCLIENT_H
Index: ps/trunk/binaries/data/mods/public/gui/lobby/lobby.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/lobby.xml (revision 23171)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/lobby.xml (revision 23172)
@@ -1,20 +1,24 @@
-
-
-
-
-
-
- Multiplayer Lobby
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
Index: ps/trunk/source/lobby/XmppClient.cpp
===================================================================
--- ps/trunk/source/lobby/XmppClient.cpp (revision 23171)
+++ ps/trunk/source/lobby/XmppClient.cpp (revision 23172)
@@ -1,1336 +1,1381 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "XmppClient.h"
#include "StanzaExtensions.h"
#ifdef WIN32
# include
#endif
#include "i18n/L10n.h"
#include "lib/external_libraries/enet.h"
#include "lib/utf8.h"
#include "network/NetServer.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Pyrogenesis.h"
#include "scriptinterface/ScriptInterface.h"
#include
//debug
#if 1
#define DbgXMPP(x)
#else
#define DbgXMPP(x) std::cout << x << std::endl;
static std::string tag_xml(const glooxwrapper::IQ& iq)
{
std::string ret;
glooxwrapper::Tag* tag = iq.tag();
ret = tag->xml().to_string();
glooxwrapper::Tag::free(tag);
return ret;
}
#endif
static std::string tag_name(const glooxwrapper::IQ& iq)
{
std::string ret;
glooxwrapper::Tag* tag = iq.tag();
ret = tag->name().to_string();
glooxwrapper::Tag::free(tag);
return ret;
}
IXmppClient* IXmppClient::create(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize,bool regOpt)
{
return new XmppClient(scriptInterface, sUsername, sPassword, sRoom, sNick, historyRequestSize, regOpt);
}
/**
* Construct the XMPP client.
*
* @param scriptInterface - ScriptInterface to be used for storing GUI messages.
* Can be left blank for non-visual applications.
* @param sUsername Username to login with of register.
* @param sPassword Password to login with or register.
* @param sRoom MUC room to join.
* @param sNick Nick to join with.
* @param historyRequestSize Number of stanzas of room history to request.
* @param regOpt If we are just registering or not.
*/
XmppClient::XmppClient(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize, bool regOpt)
: m_ScriptInterface(scriptInterface),
m_client(nullptr),
m_mucRoom(nullptr),
m_registration(nullptr),
m_username(sUsername),
m_password(sPassword),
m_room(sRoom),
m_nick(sNick),
m_initialLoadComplete(false),
m_isConnected(false),
m_sessionManager(nullptr),
m_certStatus(gloox::CertStatus::CertOk),
m_PlayerMapUpdate(false)
{
if (m_ScriptInterface)
JS_AddExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), XmppClient::Trace, this);
// Read lobby configuration from default.cfg
std::string sXpartamupp;
std::string sEchelon;
CFG_GET_VAL("lobby.server", m_server);
CFG_GET_VAL("lobby.xpartamupp", sXpartamupp);
CFG_GET_VAL("lobby.echelon", sEchelon);
m_xpartamuppId = sXpartamupp + "@" + m_server + "/CC";
m_echelonId = sEchelon + "@" + m_server + "/CC";
glooxwrapper::JID clientJid(sUsername + "@" + m_server + "/0ad");
glooxwrapper::JID roomJid(m_room + "@conference." + m_server + "/" + sNick);
// If we are connecting, use the full jid and a password
// If we are registering, only use the server name
if (!regOpt)
m_client = new glooxwrapper::Client(clientJid, sPassword);
else
m_client = new glooxwrapper::Client(m_server);
// Optionally join without a TLS certificate, so a local server can be tested quickly.
// Security risks from malicious JS mods can be mitigated if this option and also the hostname and login are shielded from JS access.
bool tls = true;
CFG_GET_VAL("lobby.tls", tls);
m_client->setTls(tls ? gloox::TLSRequired : gloox::TLSDisabled);
// Disable use of the SASL PLAIN mechanism, to prevent leaking credentials
// if the server doesn't list any supported SASL mechanism or the response
// has been modified to exclude those.
const int mechs = gloox::SaslMechAll ^ gloox::SaslMechPlain;
m_client->setSASLMechanisms(mechs);
m_client->registerConnectionListener(this);
m_client->setPresence(gloox::Presence::Available, -1);
m_client->disco()->setVersion("Pyrogenesis", engine_version);
m_client->disco()->setIdentity("client", "bot");
m_client->setCompression(false);
m_client->registerStanzaExtension(new GameListQuery());
m_client->registerIqHandler(this, EXTGAMELISTQUERY);
m_client->registerStanzaExtension(new BoardListQuery());
m_client->registerIqHandler(this, EXTBOARDLISTQUERY);
m_client->registerStanzaExtension(new ProfileQuery());
m_client->registerIqHandler(this, EXTPROFILEQUERY);
m_client->registerStanzaExtension(new LobbyAuth());
m_client->registerIqHandler(this, EXTLOBBYAUTH);
m_client->registerMessageHandler(this);
// Uncomment to see the raw stanzas
//m_client->getWrapped()->logInstance().registerLogHandler( gloox::LogLevelDebug, gloox::LogAreaAll, this );
if (!regOpt)
{
// Create a Multi User Chat Room
m_mucRoom = new glooxwrapper::MUCRoom(m_client, roomJid, this, 0);
// Get room history.
m_mucRoom->setRequestHistory(historyRequestSize, gloox::MUCRoom::HistoryMaxStanzas);
}
else
{
// Registration
m_registration = new glooxwrapper::Registration(m_client);
m_registration->registerRegistrationHandler(this);
}
m_sessionManager = new glooxwrapper::SessionManager(m_client, this);
// Register plugins to allow gloox parse them in incoming sessions
m_sessionManager->registerPlugins();
}
/**
* Destroy the xmpp client
*/
XmppClient::~XmppClient()
{
DbgXMPP("XmppClient destroyed");
delete m_registration;
delete m_mucRoom;
delete m_sessionManager;
// Workaround for memory leak in gloox 1.0/1.0.1
m_client->removePresenceExtension(gloox::ExtCaps);
delete m_client;
for (const glooxwrapper::Tag* const& t : m_GameList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_BoardList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_Profile)
glooxwrapper::Tag::free(t);
if (m_ScriptInterface)
JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), XmppClient::Trace, this);
}
void XmppClient::TraceMember(JSTracer* trc)
{
for (JS::Heap& guiMessage : m_GuiMessageQueue)
JS_CallValueTracer(trc, &guiMessage, "m_GuiMessageQueue");
for (JS::Heap& guiMessage : m_HistoricGuiMessages)
JS_CallValueTracer(trc, &guiMessage, "m_HistoricGuiMessages");
}
/// Network
void XmppClient::connect()
{
m_initialLoadComplete = false;
m_client->connect(false);
}
void XmppClient::disconnect()
{
m_client->disconnect();
}
bool XmppClient::isConnected()
{
return m_isConnected;
}
void XmppClient::recv()
{
m_client->recv(1);
}
/**
* Log (debug) Handler
*/
void XmppClient::handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message)
{
std::cout << "log: level: " << level << ", area: " << area << ", message: " << message << std::endl;
}
/*****************************************************
* Connection handlers *
*****************************************************/
/**
* Handle connection
*/
void XmppClient::onConnect()
{
if (m_mucRoom)
{
m_isConnected = true;
CreateGUIMessage("system", "connected", std::time(nullptr));
m_mucRoom->join();
}
if (m_registration)
m_registration->fetchRegistrationFields();
}
/**
* Handle disconnection
*/
void XmppClient::onDisconnect(gloox::ConnectionError error)
{
// Make sure we properly leave the room so that
// everything works if we decide to come back later
if (m_mucRoom)
m_mucRoom->leave();
// Clear game, board and player lists.
for (const glooxwrapper::Tag* const& t : m_GameList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_BoardList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_Profile)
glooxwrapper::Tag::free(t);
m_BoardList.clear();
m_GameList.clear();
m_PlayerMap.clear();
m_PlayerMapUpdate = true;
m_Profile.clear();
m_HistoricGuiMessages.clear();
m_isConnected = false;
CreateGUIMessage(
"system",
"disconnected",
std::time(nullptr),
"reason", error,
"certificate_status", m_certStatus);
}
/**
* Handle TLS connection.
*/
bool XmppClient::onTLSConnect(const glooxwrapper::CertInfo& info)
{
DbgXMPP("onTLSConnect");
DbgXMPP(
"status: " << info.status <<
"\nissuer: " << info.issuer <<
"\npeer: " << info.server <<
"\nprotocol: " << info.protocol <<
"\nmac: " << info.mac <<
"\ncipher: " << info.cipher <<
"\ncompression: " << info.compression );
m_certStatus = static_cast(info.status);
// Optionally accept invalid certificates, see require_tls option.
bool verify_certificate = true;
CFG_GET_VAL("lobby.verify_certificate", verify_certificate);
return info.status == gloox::CertOk || !verify_certificate;
}
/**
* Handle MUC room errors
*/
void XmppClient::handleMUCError(glooxwrapper::MUCRoom& UNUSED(room), gloox::StanzaError err)
{
DbgXMPP("MUC Error " << ": " << StanzaErrorToString(err));
CreateGUIMessage("system", "error", std::time(nullptr), "text", err);
}
/*****************************************************
* Requests to server *
*****************************************************/
/**
* Request the leaderboard data from the server.
*/
void XmppClient::SendIqGetBoardList()
{
glooxwrapper::JID echelonJid(m_echelonId);
// Send IQ
BoardListQuery* b = new BoardListQuery();
b->m_Command = "getleaderboard";
glooxwrapper::IQ iq(gloox::IQ::Get, echelonJid, m_client->getID());
iq.addExtension(b);
DbgXMPP("SendIqGetBoardList [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Request the profile data from the server.
*/
void XmppClient::SendIqGetProfile(const std::string& player)
{
glooxwrapper::JID echelonJid(m_echelonId);
// Send IQ
ProfileQuery* b = new ProfileQuery();
b->m_Command = player;
glooxwrapper::IQ iq(gloox::IQ::Get, echelonJid, m_client->getID());
iq.addExtension(b);
DbgXMPP("SendIqGetProfile [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Send game report containing numerous game properties to the server.
*
* @param data A JS array of game statistics
*/
void XmppClient::SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data)
{
glooxwrapper::JID echelonJid(m_echelonId);
// Setup some base stanza attributes
GameReport* game = new GameReport();
glooxwrapper::Tag* report = glooxwrapper::Tag::allocate("game");
// Iterate through all the properties reported and add them to the stanza.
std::vector properties;
scriptInterface.EnumeratePropertyNamesWithPrefix(data, "", properties);
for (const std::string& p : properties)
{
std::wstring value;
scriptInterface.GetProperty(data, p.c_str(), value);
report->addAttribute(p, utf8_from_wstring(value));
}
// Add stanza to IQ
game->m_GameReport.emplace_back(report);
// Send IQ
glooxwrapper::IQ iq(gloox::IQ::Set, echelonJid, m_client->getID());
iq.addExtension(game);
DbgXMPP("SendGameReport [" << tag_xml(iq) << "]");
m_client->send(iq);
};
/**
* Send a request to register a game to the server.
*
* @param data A JS array of game attributes
*/
void XmppClient::SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data)
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Setup some base stanza attributes
GameListQuery* g = new GameListQuery();
g->m_Command = "register";
glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game");
// Add a fake ip which will be overwritten by the ip stamp XMPP module on the server.
game->addAttribute("ip", "fake");
// Iterate through all the properties reported and add them to the stanza.
std::vector properties;
scriptInterface.EnumeratePropertyNamesWithPrefix(data, "", properties);
for (const std::string& p : properties)
{
std::wstring value;
scriptInterface.GetProperty(data, p.c_str(), value);
game->addAttribute(p, utf8_from_wstring(value));
}
// Push the stanza onto the IQ
g->m_GameList.emplace_back(game);
// Send IQ
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqRegisterGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Send a request to unregister a game to the server.
*/
void XmppClient::SendIqUnregisterGame()
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Send IQ
GameListQuery* g = new GameListQuery();
g->m_Command = "unregister";
g->m_GameList.emplace_back(glooxwrapper::Tag::allocate("game"));
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqUnregisterGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Send a request to change the state of a registered game on the server.
*
* A game can either be in the 'running' or 'waiting' state - the server
* decides which - but we need to update the current players that are
* in-game so the server can make the calculation.
*/
void XmppClient::SendIqChangeStateGame(const std::string& nbp, const std::string& players)
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Send IQ
GameListQuery* g = new GameListQuery();
g->m_Command = "changestate";
glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game");
game->addAttribute("nbp", nbp);
game->addAttribute("players", players);
g->m_GameList.emplace_back(game);
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqChangeStateGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/*****************************************************
* iq to clients *
*****************************************************/
/**
* Send lobby authentication token.
*/
void XmppClient::SendIqLobbyAuth(const std::string& to, const std::string& token)
{
LobbyAuth* auth = new LobbyAuth();
auth->m_Token = token;
glooxwrapper::JID clientJid(to + "@" + m_server + "/0ad");
glooxwrapper::IQ iq(gloox::IQ::Set, clientJid, m_client->getID());
iq.addExtension(auth);
DbgXMPP("SendIqLobbyAuth [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/*****************************************************
* Account registration *
*****************************************************/
void XmppClient::handleRegistrationFields(const glooxwrapper::JID&, int fields, glooxwrapper::string)
{
glooxwrapper::RegistrationFields vals;
vals.username = m_username;
vals.password = m_password;
m_registration->createAccount(fields, vals);
}
void XmppClient::handleRegistrationResult(const glooxwrapper::JID&, gloox::RegistrationResult result)
{
if (result == gloox::RegistrationSuccess)
CreateGUIMessage("system", "registered", std::time(nullptr));
else
CreateGUIMessage("system", "error", std::time(nullptr), "text", result);
disconnect();
}
void XmppClient::handleAlreadyRegistered(const glooxwrapper::JID&)
{
DbgXMPP("the account already exists");
}
void XmppClient::handleDataForm(const glooxwrapper::JID&, const glooxwrapper::DataForm&)
{
DbgXMPP("dataForm received");
}
void XmppClient::handleOOB(const glooxwrapper::JID&, const glooxwrapper::OOB&)
{
DbgXMPP("OOB registration requested");
}
/*****************************************************
* Requests from GUI *
*****************************************************/
/**
* Handle requests from the GUI for the list of players.
*
* @return A JS array containing all known players and their presences
*/
void XmppClient::GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
ScriptInterface::CreateArray(cx, ret);
int j = 0;
for (const std::pair& p : m_PlayerMap)
{
JS::RootedValue player(cx);
ScriptInterface::CreateObject(
cx,
&player,
"name", p.first,
"presence", p.second.m_Presence,
"rating", p.second.m_Rating,
"role", p.second.m_Role);
scriptInterface.SetPropertyInt(ret, j++, player);
}
}
/**
* Handle requests from the GUI for the list of all active games.
*
* @return A JS array containing all known games
*/
void XmppClient::GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
ScriptInterface::CreateArray(cx, ret);
int j = 0;
const char* stats[] = { "name", "ip", "port", "stunIP", "stunPort", "hostUsername", "state",
"nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType",
- "victoryCondition", "startTime", "mods" };
+ "victoryConditions", "startTime", "mods" };
for(const glooxwrapper::Tag* const& t : m_GameList)
{
JS::RootedValue game(cx);
ScriptInterface::CreateObject(cx, &game);
for (size_t i = 0; i < ARRAY_SIZE(stats); ++i)
scriptInterface.SetProperty(game, stats[i], t->findAttribute(stats[i]));
scriptInterface.SetPropertyInt(ret, j++, game);
}
}
/**
* Handle requests from the GUI for leaderboard data.
*
* @return A JS array containing all known leaderboard data
*/
void XmppClient::GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
ScriptInterface::CreateArray(cx, ret);
int j = 0;
const char* attributes[] = { "name", "rank", "rating" };
for(const glooxwrapper::Tag* const& t : m_BoardList)
{
JS::RootedValue board(cx);
ScriptInterface::CreateObject(cx, &board);
for (size_t i = 0; i < ARRAY_SIZE(attributes); ++i)
scriptInterface.SetProperty(board, attributes[i], t->findAttribute(attributes[i]));
scriptInterface.SetPropertyInt(ret, j++, board);
}
}
/**
* Handle requests from the GUI for profile data.
*
* @return A JS array containing the specific user's profile data
*/
void XmppClient::GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
ScriptInterface::CreateArray(cx, ret);
int j = 0;
const char* stats[] = { "player", "rating", "totalGamesPlayed", "highestRating", "wins", "losses", "rank" };
for (const glooxwrapper::Tag* const& t : m_Profile)
{
JS::RootedValue profile(cx);
ScriptInterface::CreateObject(cx, &profile);
for (size_t i = 0; i < ARRAY_SIZE(stats); ++i)
scriptInterface.SetProperty(profile, stats[i], t->findAttribute(stats[i]));
scriptInterface.SetPropertyInt(ret, j++, profile);
}
}
/*****************************************************
* Message interfaces *
*****************************************************/
void SetGUIMessageProperty(JSContext* UNUSED(cx), JS::HandleObject UNUSED(messageObj))
{
}
template
void SetGUIMessageProperty(JSContext* cx, JS::HandleObject messageObj, const std::string& propertyName, const T& propertyValue, Args const&... args)
{
// JSAutoRequest is the responsibility of the caller
JS::RootedValue scriptPropertyValue(cx);
ScriptInterface::AssignOrToJSVal(cx, &scriptPropertyValue, propertyValue);
JS_DefineProperty(cx, messageObj, propertyName.c_str(), scriptPropertyValue, JSPROP_ENUMERATE);
SetGUIMessageProperty(cx, messageObj, args...);
}
template
void XmppClient::CreateGUIMessage(
const std::string& type,
const std::string& level,
const std::time_t time,
Args const&... args)
{
if (!m_ScriptInterface)
return;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue message(cx);
ScriptInterface::CreateObject(
cx,
&message,
"type", type,
"level", level,
"historic", false,
"time", static_cast(time));
JS::RootedObject messageObj(cx, message.toObjectOrNull());
SetGUIMessageProperty(cx, messageObj, args...);
m_ScriptInterface->FreezeObject(message, true);
m_GuiMessageQueue.push_back(JS::Heap(message));
}
bool XmppClient::GuiPollHasPlayerListUpdate()
{
+ // The initial playerlist will be received in multiple messages
+ // Only inform the GUI after all of these playerlist fragments were received.
+ if (!m_initialLoadComplete)
+ return false;
+
bool hasUpdate = m_PlayerMapUpdate;
m_PlayerMapUpdate = false;
return hasUpdate;
}
-JS::Value XmppClient::GuiPollNewMessage(const ScriptInterface& scriptInterface)
+JS::Value XmppClient::GuiPollNewMessages(const ScriptInterface& scriptInterface)
{
- if (m_GuiMessageQueue.empty())
+ if (!m_initialLoadComplete || m_GuiMessageQueue.empty())
return JS::UndefinedValue();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
- JS::RootedValue message(cx, m_GuiMessageQueue.front());
- m_GuiMessageQueue.pop_front();
+ // Optimize for batch message processing that is more
+ // performance demanding than processing a lone message.
+ JS::RootedValue messages(cx);
+ ScriptInterface::CreateArray(cx, &messages);
+
+ int j = 0;
- JS::RootedValue messageCopy(cx);
- if (JS_StructuredClone(cx, message, &messageCopy, nullptr, nullptr))
+ for (const JS::Heap& message : m_GuiMessageQueue)
{
- scriptInterface.SetProperty(messageCopy, "historic", true);
- scriptInterface.FreezeObject(messageCopy, true);
- m_HistoricGuiMessages.push_back(JS::Heap(messageCopy));
+ scriptInterface.SetPropertyInt(messages, j++, message);
+
+ // Store historic chat messages.
+ // Only store relevant messages to minimize memory footprint.
+ JS::RootedValue rootedMessage(cx, message);
+ std::string type;
+ scriptInterface.GetProperty(rootedMessage, "type", type);
+ if (type != "chat")
+ continue;
+
+ std::string level;
+ scriptInterface.GetProperty(rootedMessage, "level", level);
+ if (level != "room-message" && level != "private-message")
+ continue;
+
+ JS::RootedValue historicMessage(cx);
+ if (JS_StructuredClone(cx, rootedMessage, &historicMessage, nullptr, nullptr))
+ {
+ scriptInterface.SetProperty(historicMessage, "historic", true);
+ scriptInterface.FreezeObject(historicMessage, true);
+ m_HistoricGuiMessages.push_back(JS::Heap(historicMessage));
+ }
+ else
+ LOGERROR("Could not clone historic lobby GUI message!");
}
- else
- LOGERROR("Could not clone historic lobby GUI message!");
- return message;
+ m_GuiMessageQueue.clear();
+ return messages;
}
JS::Value XmppClient::GuiPollHistoricMessages(const ScriptInterface& scriptInterface)
{
+ if (m_HistoricGuiMessages.empty())
+ return JS::UndefinedValue();
+
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
- JS::RootedValue ret(cx);
- ScriptInterface::CreateArray(cx, &ret);
+ JS::RootedValue messages(cx);
+ ScriptInterface::CreateArray(cx, &messages);
int j = 0;
for (const JS::Heap& message : m_HistoricGuiMessages)
- scriptInterface.SetPropertyInt(ret, j++, message);
+ scriptInterface.SetPropertyInt(messages, j++, message);
- return ret;
+ return messages;
}
/**
* Send a standard MUC textual message.
*/
void XmppClient::SendMUCMessage(const std::string& message)
{
m_mucRoom->send(message);
}
/**
* Handle a room message.
*/
void XmppClient::handleMUCMessage(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::Message& msg, bool priv)
{
DbgXMPP(msg.from().resource() << " said " << msg.body());
CreateGUIMessage(
"chat",
priv ? "private-message" : "room-message",
ComputeTimestamp(msg),
"from", msg.from().resource(),
"text", msg.body());
}
/**
* Handle a private message.
*/
void XmppClient::handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession*)
{
DbgXMPP("type " << msg.subtype() << ", subject " << msg.subject()
<< ", message " << msg.body() << ", thread id " << msg.thread());
CreateGUIMessage(
"chat",
"private-message",
ComputeTimestamp(msg),
"from", msg.from().resource(),
"text", msg.body());
}
/**
* Handle portions of messages containing custom stanza extensions.
*/
bool XmppClient::handleIq(const glooxwrapper::IQ& iq)
{
DbgXMPP("handleIq [" << tag_xml(iq) << "]");
if (iq.subtype() == gloox::IQ::Result)
{
const GameListQuery* gq = iq.findExtension(EXTGAMELISTQUERY);
const BoardListQuery* bq = iq.findExtension(EXTBOARDLISTQUERY);
const ProfileQuery* pq = iq.findExtension(EXTPROFILEQUERY);
if (gq)
{
for (const glooxwrapper::Tag* const& t : m_GameList)
glooxwrapper::Tag::free(t);
m_GameList.clear();
for (const glooxwrapper::Tag* const& t : gq->m_GameList)
m_GameList.emplace_back(t->clone());
CreateGUIMessage("game", "gamelist", std::time(nullptr));
}
if (bq)
{
if (bq->m_Command == "boardlist")
{
for (const glooxwrapper::Tag* const& t : m_BoardList)
glooxwrapper::Tag::free(t);
m_BoardList.clear();
for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList)
m_BoardList.emplace_back(t->clone());
CreateGUIMessage("game", "leaderboard", std::time(nullptr));
}
else if (bq->m_Command == "ratinglist")
{
for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList)
{
const PlayerMap::iterator it = m_PlayerMap.find(t->findAttribute("name"));
if (it != m_PlayerMap.end())
{
it->second.m_Rating = t->findAttribute("rating");
m_PlayerMapUpdate = true;
}
}
CreateGUIMessage("game", "ratinglist", std::time(nullptr));
}
}
if (pq)
{
for (const glooxwrapper::Tag* const& t : m_Profile)
glooxwrapper::Tag::free(t);
m_Profile.clear();
for (const glooxwrapper::Tag* const& t : pq->m_StanzaProfile)
m_Profile.emplace_back(t->clone());
CreateGUIMessage("game", "profile", std::time(nullptr));
}
}
else if (iq.subtype() == gloox::IQ::Set)
{
const LobbyAuth* lobbyAuth = iq.findExtension(EXTLOBBYAUTH);
if (lobbyAuth)
{
LOGMESSAGE("XmppClient: Received lobby auth: %s from %s", lobbyAuth->m_Token.to_string(), iq.from().username());
glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id());
m_client->send(response);
if (g_NetServer)
g_NetServer->OnLobbyAuth(iq.from().username(), lobbyAuth->m_Token.to_string());
else
LOGERROR("Received lobby authentication request, but not hosting currently!");
}
}
else if (iq.subtype() == gloox::IQ::Error)
CreateGUIMessage("system", "error", std::time(nullptr), "text", iq.error_error());
else
{
CreateGUIMessage("system", "error", std::time(nullptr), "text", wstring_from_utf8(g_L10n.Translate("unknown subtype (see logs)")));
LOGMESSAGE("unknown subtype '%s'", tag_name(iq).c_str());
}
return true;
}
/**
* Update local data when a user changes presence.
*/
void XmppClient::handleMUCParticipantPresence(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::MUCRoomParticipant participant, const glooxwrapper::Presence& presence)
{
const glooxwrapper::string& nick = participant.nick->resource();
if (presence.presence() == gloox::Presence::Unavailable)
{
if (!participant.newNick.empty() && (participant.flags & (gloox::UserNickChanged | gloox::UserSelf)))
{
// we have a nick change
if (m_PlayerMap.find(participant.newNick) == m_PlayerMap.end())
m_PlayerMap.emplace(
std::piecewise_construct,
std::forward_as_tuple(participant.newNick),
std::forward_as_tuple(presence.presence(), participant.role, std::move(m_PlayerMap.at(nick).m_Rating)));
else
LOGERROR("Nickname changed to an existing nick!");
DbgXMPP(nick << " is now known as " << participant.newNick);
CreateGUIMessage(
"chat",
"nick",
std::time(nullptr),
"oldnick", nick,
"newnick", participant.newNick);
}
else if (participant.flags & gloox::UserKicked)
{
DbgXMPP(nick << " was kicked. Reason: " << participant.reason);
CreateGUIMessage(
"chat",
"kicked",
std::time(nullptr),
"nick", nick,
"reason", participant.reason);
}
else if (participant.flags & gloox::UserBanned)
{
DbgXMPP(nick << " was banned. Reason: " << participant.reason);
CreateGUIMessage(
"chat",
"banned",
std::time(nullptr),
"nick", nick,
"reason", participant.reason);
}
else
{
DbgXMPP(nick << " left the room (flags " << participant.flags << ")");
CreateGUIMessage(
"chat",
"leave",
std::time(nullptr),
"nick", nick);
}
m_PlayerMap.erase(nick);
}
else
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
/* During the initialization process, we receive join messages for everyone
* currently in the room. We don't want to display these, so we filter them
* out. We will always be the last to join during initialization.
*/
if (!m_initialLoadComplete)
{
if (m_mucRoom->nick() == nick)
m_initialLoadComplete = true;
}
else if (it == m_PlayerMap.end())
{
CreateGUIMessage(
"chat",
"join",
std::time(nullptr),
"nick", nick);
}
else if (it->second.m_Role != participant.role)
{
CreateGUIMessage(
"chat",
"role",
std::time(nullptr),
"nick", nick,
"oldrole", it->second.m_Role,
"newrole", participant.role);
}
else
{
// Don't create a GUI message for regular presence changes, because
// several hundreds of them accumulate during a match, impacting performance terribly and
// the only way they are used is to determine whether to update the playerlist.
}
DbgXMPP(
nick << " is in the room, "
"presence: " << GetPresenceString(presence.presence()) << ", "
"role: "<< GetRoleString(participant.role));
if (it == m_PlayerMap.end())
{
m_PlayerMap.emplace(
std::piecewise_construct,
std::forward_as_tuple(nick),
std::forward_as_tuple(presence.presence(), participant.role, std::string()));
}
else
{
it->second.m_Presence = presence.presence();
it->second.m_Role = participant.role;
}
}
m_PlayerMapUpdate = true;
}
/**
* Update local cache when subject changes.
*/
void XmppClient::handleMUCSubject(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::string& nick, const glooxwrapper::string& subject)
{
m_Subject = wstring_from_utf8(subject.to_string());
CreateGUIMessage(
"chat",
"subject",
std::time(nullptr),
"nick", nick,
"subject", m_Subject);
}
/**
* Get current subject.
*/
const std::wstring& XmppClient::GetSubject()
{
return m_Subject;
}
/**
* Request nick change, real change via mucRoomHandler.
*
* @param nick Desired nickname
*/
void XmppClient::SetNick(const std::string& nick)
{
m_mucRoom->setNick(nick);
}
/**
* Get current nickname.
*
* @param nick Variable to store the nickname in.
*/
void XmppClient::GetNick(std::string& nick)
{
nick = m_mucRoom->nick().to_string();
}
/**
* Kick a player from the current room.
*
* @param nick Nickname to be kicked
* @param reason Reason the player was kicked
*/
void XmppClient::kick(const std::string& nick, const std::string& reason)
{
m_mucRoom->kick(nick, reason);
}
/**
* Ban a player from the current room.
*
* @param nick Nickname to be banned
* @param reason Reason the player was banned
*/
void XmppClient::ban(const std::string& nick, const std::string& reason)
{
m_mucRoom->ban(nick, reason);
}
/**
* Change the xmpp presence of the client.
*
* @param presence A string containing the desired presence
*/
void XmppClient::SetPresence(const std::string& presence)
{
#define IF(x,y) if (presence == x) m_mucRoom->setPresence(gloox::Presence::y)
IF("available", Available);
else IF("chat", Chat);
else IF("away", Away);
else IF("playing", DND);
else IF("offline", Unavailable);
// The others are not to be set
#undef IF
else LOGERROR("Unknown presence '%s'", presence.c_str());
}
/**
* Get the current xmpp presence of the given nick.
*/
const char* XmppClient::GetPresence(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return "offline";
return GetPresenceString(it->second.m_Presence);
}
/**
* Get the current xmpp role of the given nick.
*/
const char* XmppClient::GetRole(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return "";
return GetRoleString(it->second.m_Role);
}
+/**
+ * Get the most recent received rating of the given nick.
+ * Notice that this doesn't request a rating profile if it hasn't been received yet.
+ */
+std::wstring XmppClient::GetRating(const std::string& nick)
+{
+ const PlayerMap::iterator it = m_PlayerMap.find(nick);
+
+ if (it == m_PlayerMap.end())
+ return std::wstring();
+
+ return wstring_from_utf8(it->second.m_Rating.to_string());
+}
+
/*****************************************************
* Utilities *
*****************************************************/
/**
* Parse and return the timestamp of a historic chat message and return the current time for new chat messages.
* Historic chat messages are implement as DelayedDelivers as specified in XEP-0203.
* Hence, their timestamp MUST be in UTC and conform to the DateTime format XEP-0082.
*
* @returns Seconds since the epoch.
*/
std::time_t XmppClient::ComputeTimestamp(const glooxwrapper::Message& msg)
{
// Only historic messages contain a timestamp!
if (!msg.when())
return std::time(nullptr);
// The locale is irrelevant, because the XMPP date format doesn't contain written month names
for (const std::string& format : std::vector{ "Y-M-d'T'H:m:sZ", "Y-M-d'T'H:m:s.SZ" })
{
UDate dateTime = g_L10n.ParseDateTime(msg.when()->stamp().to_string(), format, icu::Locale::getUS());
if (dateTime)
return dateTime / 1000.0;
}
return std::time(nullptr);
}
/**
* Convert a gloox presence type to an untranslated string literal to be used as an identifier by the scripts.
*/
const char* XmppClient::GetPresenceString(const gloox::Presence::PresenceType presenceType)
{
switch (presenceType)
{
#define CASE(X,Y) case gloox::Presence::X: return Y
CASE(Available, "available");
CASE(Chat, "chat");
CASE(Away, "away");
CASE(DND, "playing");
CASE(XA, "away");
CASE(Unavailable, "offline");
CASE(Probe, "probe");
CASE(Error, "error");
CASE(Invalid, "invalid");
default:
LOGERROR("Unknown presence type '%d'", static_cast(presenceType));
return "";
#undef CASE
}
}
/**
* Convert a gloox role type to an untranslated string literal to be used as an identifier by the scripts.
*/
const char* XmppClient::GetRoleString(const gloox::MUCRoomRole role)
{
switch (role)
{
#define CASE(X, Y) case gloox::X: return Y
CASE(RoleNone, "none");
CASE(RoleVisitor, "visitor");
CASE(RoleParticipant, "participant");
CASE(RoleModerator, "moderator");
CASE(RoleInvalid, "invalid");
default:
LOGERROR("Unknown role type '%d'", static_cast(role));
return "";
#undef CASE
}
}
/**
* Translates a gloox certificate error codes, i.e. gloox certificate statuses except CertOk.
* Keep in sync with specifications.
*/
std::string XmppClient::CertificateErrorToString(gloox::CertStatus status)
{
std::map certificateErrorStrings = {
{ gloox::CertInvalid, g_L10n.Translate("The certificate is not trusted.") },
{ gloox::CertSignerUnknown, g_L10n.Translate("The certificate hasn't got a known issuer.") },
{ gloox::CertRevoked, g_L10n.Translate("The certificate has been revoked.") },
{ gloox::CertExpired, g_L10n.Translate("The certificate has expired.") },
{ gloox::CertNotActive, g_L10n.Translate("The certifiacte is not yet active.") },
{ gloox::CertWrongPeer, g_L10n.Translate("The certificate has not been issued for the peer connected to.") },
{ gloox::CertSignerNotCa, g_L10n.Translate("The certificate signer is not a certificate authority.") }
};
std::string result;
for (std::map::iterator it = certificateErrorStrings.begin(); it != certificateErrorStrings.end(); ++it)
if (status & it->first)
result += "\n" + it->second;
return result;
}
/**
* Convert a gloox stanza error type to string.
* Keep in sync with Gloox documentation
*
* @param err Error to be converted
* @return Converted error string
*/
std::string XmppClient::StanzaErrorToString(gloox::StanzaError err)
{
#define CASE(X, Y) case gloox::X: return Y
#define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")"
switch (err)
{
CASE(StanzaErrorUndefined, g_L10n.Translate("No error"));
DEBUG_CASE(StanzaErrorBadRequest, "Server received malformed XML");
CASE(StanzaErrorConflict, g_L10n.Translate("Player already logged in"));
DEBUG_CASE(StanzaErrorFeatureNotImplemented, "Server does not implement requested feature");
CASE(StanzaErrorForbidden, g_L10n.Translate("Forbidden"));
DEBUG_CASE(StanzaErrorGone, "Unable to find message receipiant");
CASE(StanzaErrorInternalServerError, g_L10n.Translate("Internal server error"));
DEBUG_CASE(StanzaErrorItemNotFound, "Message receipiant does not exist");
DEBUG_CASE(StanzaErrorJidMalformed, "JID (XMPP address) malformed");
DEBUG_CASE(StanzaErrorNotAcceptable, "Receipiant refused message. Possible policy issue");
CASE(StanzaErrorNotAllowed, g_L10n.Translate("Not allowed"));
CASE(StanzaErrorNotAuthorized, g_L10n.Translate("Not authorized"));
DEBUG_CASE(StanzaErrorNotModified, "Requested item has not changed since last request");
DEBUG_CASE(StanzaErrorPaymentRequired, "This server requires payment");
CASE(StanzaErrorRecipientUnavailable, g_L10n.Translate("Recipient temporarily unavailable"));
DEBUG_CASE(StanzaErrorRedirect, "Request redirected");
CASE(StanzaErrorRegistrationRequired, g_L10n.Translate("Registration required"));
DEBUG_CASE(StanzaErrorRemoteServerNotFound, "Remote server not found");
DEBUG_CASE(StanzaErrorRemoteServerTimeout, "Remote server timed out");
DEBUG_CASE(StanzaErrorResourceConstraint, "The recipient is unable to process the message due to resource constraints");
CASE(StanzaErrorServiceUnavailable, g_L10n.Translate("Service unavailable"));
DEBUG_CASE(StanzaErrorSubscribtionRequired, "Service requires subscription");
DEBUG_CASE(StanzaErrorUnexpectedRequest, "Attempt to send from invalid stanza address");
DEBUG_CASE(StanzaErrorUnknownSender, "Invalid 'from' address");
default:
return g_L10n.Translate("Unknown error");
}
#undef DEBUG_CASE
#undef CASE
}
/**
* Convert a gloox connection error enum to string
* Keep in sync with Gloox documentation
*
* @param err Error to be converted
* @return Converted error string
*/
std::string XmppClient::ConnectionErrorToString(gloox::ConnectionError err)
{
#define CASE(X, Y) case gloox::X: return Y
#define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")"
switch (err)
{
CASE(ConnNoError, g_L10n.Translate("No error"));
CASE(ConnStreamError, g_L10n.Translate("Stream error"));
CASE(ConnStreamVersionError, g_L10n.Translate("The incoming stream version is unsupported"));
CASE(ConnStreamClosed, g_L10n.Translate("The stream has been closed by the server"));
DEBUG_CASE(ConnProxyAuthRequired, "The HTTP/SOCKS5 proxy requires authentication");
DEBUG_CASE(ConnProxyAuthFailed, "HTTP/SOCKS5 proxy authentication failed");
DEBUG_CASE(ConnProxyNoSupportedAuth, "The HTTP/SOCKS5 proxy requires an unsupported authentication mechanism");
CASE(ConnIoError, g_L10n.Translate("An I/O error occurred"));
DEBUG_CASE(ConnParseError, "An XML parse error occurred");
CASE(ConnConnectionRefused, g_L10n.Translate("The connection was refused by the server"));
CASE(ConnDnsError, g_L10n.Translate("Resolving the server's hostname failed"));
CASE(ConnOutOfMemory, g_L10n.Translate("This system is out of memory"));
DEBUG_CASE(ConnNoSupportedAuth, "The authentication mechanisms the server offered are not supported or no authentication mechanisms were available");
CASE(ConnTlsFailed, g_L10n.Translate("The server's certificate could not be verified or the TLS handshake did not complete successfully"));
CASE(ConnTlsNotAvailable, g_L10n.Translate("The server did not offer required TLS encryption"));
DEBUG_CASE(ConnCompressionFailed, "Negotiation/initializing compression failed");
CASE(ConnAuthenticationFailed, g_L10n.Translate("Authentication failed. Incorrect password or account does not exist"));
CASE(ConnUserDisconnected, g_L10n.Translate("The user or system requested a disconnect"));
CASE(ConnNotConnected, g_L10n.Translate("There is no active connection"));
default:
return g_L10n.Translate("Unknown error");
}
#undef DEBUG_CASE
#undef CASE
}
/**
* Convert a gloox registration result enum to string
* Keep in sync with Gloox documentation
*
* @param err Enum to be converted
* @return Converted string
*/
std::string XmppClient::RegistrationResultToString(gloox::RegistrationResult res)
{
#define CASE(X, Y) case gloox::X: return Y
#define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")"
switch (res)
{
CASE(RegistrationSuccess, g_L10n.Translate("Your account has been successfully registered"));
CASE(RegistrationNotAcceptable, g_L10n.Translate("Not all necessary information provided"));
CASE(RegistrationConflict, g_L10n.Translate("Username already exists"));
DEBUG_CASE(RegistrationNotAuthorized, "Account removal timeout or insufficiently secure channel for password change");
DEBUG_CASE(RegistrationBadRequest, "Server received an incomplete request");
DEBUG_CASE(RegistrationForbidden, "Registration forbidden");
DEBUG_CASE(RegistrationRequired, "Account cannot be removed as it does not exist");
DEBUG_CASE(RegistrationUnexpectedRequest, "This client is unregistered with the server");
DEBUG_CASE(RegistrationNotAllowed, "Server does not permit password changes");
default:
return "";
}
#undef DEBUG_CASE
#undef CASE
}
void XmppClient::SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJIDStr)
{
DbgXMPP("SendStunEndpointToHost " << hostJIDStr);
char ipStr[256] = "(error)";
ENetAddress addr;
addr.host = ntohl(stunEndpoint.ip);
enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr));
glooxwrapper::JID hostJID(hostJIDStr);
glooxwrapper::Jingle::Session session = m_sessionManager->createSession(hostJID);
session.sessionInitiate(ipStr, stunEndpoint.port);
}
void XmppClient::handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle)
{
if (action == gloox::Jingle::SessionInitiate)
handleSessionInitiation(session, jingle);
}
void XmppClient::handleSessionInitiation(glooxwrapper::Jingle::Session& UNUSED(session), const glooxwrapper::Jingle::Session::Jingle& jingle)
{
glooxwrapper::Jingle::ICEUDP::Candidate candidate = jingle.getCandidate();
if (candidate.ip.empty())
{
LOGERROR("Failed to retrieve Jingle candidate");
return;
}
if (!g_NetServer)
{
LOGERROR("Received STUN connection request, but not hosting currently!");
return;
}
g_NetServer->SendHolePunchingMessage(candidate.ip.to_string(), candidate.port);
}
Index: ps/trunk/binaries/data/mods/public/gui/lobby/lobby.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/lobby.js (revision 23171)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/lobby.js (revision 23172)
@@ -1,1594 +1,40 @@
/**
+ * Used for gameselection details.
+ */
+const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
+
+/**
* Used for the gamelist-filtering.
*/
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
/**
* Used for the gamelist-filtering.
*/
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
/**
* Used for civ settings display of the selected game.
*/
const g_CivData = loadCivData(false, false);
/**
- * A symbol which is prepended to the username of moderators.
- */
-var g_ModeratorPrefix = "@";
-
-/**
- * Current username. Cannot contain whitespace.
- */
-const g_Username = Engine.LobbyGetNick();
-
-/**
- * Lobby server address to construct host JID.
- */
-const g_LobbyServer = Engine.ConfigDB_GetValue("user", "lobby.server");
-
-/**
- * Current games will be listed in these colors.
- */
-var g_GameColors = {
- "init": "0 219 0",
- "waiting": "255 127 0",
- "running": "219 0 0",
- "incompatible": "gray"
-};
-
-/**
- * Initial sorting order of the gamelist.
- */
-var g_GameStatusOrder = ["init", "waiting", "running", "incompatible"];
-
-/**
- * The playerlist will be assembled using these values.
- */
-var g_PlayerStatuses = {
- "available": { "color": "0 219 0", "status": translate("Online") },
- "away": { "color": "229 76 13", "status": translate("Away") },
- "playing": { "color": "200 0 0", "status": translate("Busy") },
- "offline": { "color": "0 0 0", "status": translate("Offline") },
- "unknown": { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
-};
-
-var g_RoleNames = {
- "moderator": translate("Moderator"),
- "participant": translate("Player"),
- "visitor": translate("Muted Player")
-};
-
-/**
- * Color for error messages in the chat.
- */
-var g_SystemColor = "150 0 0";
-
-/**
- * Color for private messages in the chat.
+ * Current nickname.
*/
-var g_PrivateMessageColor = "0 150 0";
+var g_Nickname = Engine.LobbyGetNick();
/**
- * Used for highlighting the sender of chat messages.
+ * This class organizes all components of this GUI page.
*/
-var g_SenderFont = "sans-bold-13";
-
-/**
- * Color to highlight chat commands in the explanation.
- */
-var g_ChatCommandColor = "200 200 255";
-
-/**
- * Color for the player count number in the games list.
- */
-var g_PlayerCountTags = {
- "CurrentPlayers": { "color": "0 160 160" },
- "MaxPlayers": { "color": "0 160 160" },
- "Observers": { "color": "0 128 128" }
-};
-
-/**
- * Indicates if the lobby is opened as a dialog or window.
- */
-var g_Dialog = false;
-
-/**
- * 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 = [];
-
-/**
- * Used to restore the selection after updating the playerlist.
- */
-var g_SelectedPlayer = "";
-
-/**
- * Used to restore the selection after updating the gamelist.
- */
-var g_SelectedGameIP = "";
-
-/**
- * Used to restore the selection after updating the gamelist.
- */
-var g_SelectedGamePort = "";
-
-/**
- * Whether the current user has been kicked or banned.
- */
-var g_Kicked = false;
-
-/**
- * Whether the player was already asked to reconnect to the lobby.
- * Ensures that no more than one message box is opened at a time.
- */
-var g_AskedReconnect = false;
-
-/**
- * Processing of notifications sent by XmppClient.cpp.
- *
- * @returns true if the playerlist GUI must be updated.
- */
-var g_NetMessageTypes = {
- "system": {
- // Three cases are handled in prelobby.js
- "registered": msg => {
- },
- "connected": msg => {
-
- g_AskedReconnect = false;
- updateConnectedState();
- },
- "disconnected": msg => {
-
- updateGameList();
- updateLeaderboard();
- updateConnectedState();
-
- if (!g_Kicked)
- {
- addChatMessage({
- "from": "system",
- "time": msg.time,
- "text": translate("Disconnected.") + " " + msg.reason + msg.certificate_status
- });
- reconnectMessageBox();
- }
- },
- "error": msg => {
- addChatMessage({
- "from": "system",
- "time": msg.time,
- "text": msg.text
- });
- }
- },
- "chat": {
- "subject": msg => {
- updateSubject(msg.subject);
-
- if (msg.nick)
- addChatMessage({
- "text": "/special " + sprintf(translate("%(nick)s changed the lobby subject to %(subject)s"), {
- "nick": msg.nick,
- "subject": msg.subject
- }),
- "time": msg.time,
- "isSpecial": true
- });
- },
- "join": msg => {
- addChatMessage({
- "text": "/special " + sprintf(translate("%(nick)s has joined."), {
- "nick": msg.nick
- }),
- "time": msg.time,
- "isSpecial": true
- });
- },
- "leave": msg => {
- addChatMessage({
- "text": "/special " + sprintf(translate("%(nick)s has left."), {
- "nick": msg.nick
- }),
- "time": msg.time,
- "isSpecial": true
- });
-
- if (msg.nick == g_Username)
- Engine.DisconnectXmppClient();
- },
- "role": msg => {
- Engine.GetGUIObjectByName("chatInput").hidden = Engine.LobbyGetPlayerRole(g_Username) == "visitor";
-
- let me = g_Username == msg.nick;
- let txt =
- msg.newrole == "visitor" ?
- me ?
- translate("You have been muted.") :
- translate("%(nick)s has been muted.") :
- msg.newrole == "moderator" ?
- me ?
- translate("You are now a moderator.") :
- translate("%(nick)s is now a moderator.") :
- msg.oldrole == "visitor" ?
- me ?
- translate("You have been unmuted.") :
- translate("%(nick)s has been unmuted.") :
- me ?
- translate("You are not a moderator anymore.") :
- translate("%(nick)s is not a moderator anymore.");
-
- addChatMessage({
- "text": "/special " + sprintf(txt, { "nick": msg.nick }),
- "time": msg.time,
- "isSpecial": true
- });
-
- if (g_SelectedPlayer == msg.nick)
- updateUserRoleText(g_SelectedPlayer);
- },
- "nick": msg => {
- addChatMessage({
- "text": "/special " + sprintf(translate("%(oldnick)s is now known as %(newnick)s."), {
- "oldnick": msg.oldnick,
- "newnick": msg.newnick
- }),
- "time": msg.time,
- "isSpecial": true
- });
- },
- "kicked": msg => {
- handleKick(false, msg.nick, msg.reason, msg.time, msg.historic);
- },
- "banned": msg => {
- handleKick(true, msg.nick, msg.reason, msg.time, msg.historic);
- },
- "room-message": msg => {
- addChatMessage({
- "from": escapeText(msg.from),
- "text": escapeText(msg.text),
- "time": msg.time,
- "historic": msg.historic
- });
- },
- "private-message": msg => {
- // Announcements and the Message of the Day are sent by the server directly
- if (!msg.from)
- messageBox(
- 400, 250,
- msg.text.trim(),
- translate("Notice"));
-
- // We intend to not support private messages between users
- if (!msg.from || Engine.LobbyGetPlayerRole(msg.from) == "moderator")
- // some XMPP clients send trailing whitespace
- addChatMessage({
- "from": escapeText(msg.from || "system"),
- "text": escapeText(msg.text.trim()),
- "time": msg.time,
- "historic": msg.historic,
- "private": true
- });
- }
- },
- "game": {
- "gamelist": msg => {
- updateGameList();
- },
- "profile": msg => {
- updateProfile();
- },
- "leaderboard": msg => {
- updateLeaderboard();
- },
- "ratinglist": msg => {
- }
- }
-};
-
-/**
- * Commands that can be entered by clients via chat input.
- * A handler returns true if the user input should be sent as a chat message.
- */
-var g_ChatCommands = {
- "away": {
- "description": translate("Set your state to 'Away'."),
- "handler": args => {
- Engine.LobbySetPlayerPresence("away");
- return false;
- }
- },
- "back": {
- "description": translate("Set your state to 'Online'."),
- "handler": args => {
- Engine.LobbySetPlayerPresence("available");
- return false;
- }
- },
- "kick": {
- "description": translate("Kick a specified user from the lobby. Usage: /kick nick reason"),
- "handler": args => {
- Engine.LobbyKick(args[0] || "", args[1] || "");
- return false;
- },
- "moderatorOnly": true
- },
- "ban": {
- "description": translate("Ban a specified user from the lobby. Usage: /ban nick reason"),
- "handler": args => {
- Engine.LobbyBan(args[0] || "", args[1] || "");
- return false;
- },
- "moderatorOnly": true
- },
- "help": {
- "description": translate("Show this help."),
- "handler": args => {
- let isModerator = Engine.LobbyGetPlayerRole(g_Username) == "moderator";
- let text = translate("Chat commands:");
- for (let command in g_ChatCommands)
- if (!g_ChatCommands[command].moderatorOnly || isModerator)
- // Translation: Chat command help format
- text += "\n" + sprintf(translate("%(command)s - %(description)s"), {
- "command": coloredText(command, g_ChatCommandColor),
- "description": g_ChatCommands[command].description
- });
-
- addChatMessage({
- "from": "system",
- "text": text
- });
- return false;
- }
- },
- "me": {
- "description": translate("Send a chat message about yourself. Example: /me goes swimming."),
- "handler": args => true
- },
- "say": {
- "description": translate("Send text as a chat message (even if it starts with slash). Example: /say /help is a great command."),
- "handler": args => true
- },
- "clear": {
- "description": translate("Clear all chat scrollback."),
- "handler": args => {
- clearChatMessages();
- return false;
- }
- },
- "quit": {
- "description": translate("Return to the main menu."),
- "handler": args => {
- leaveLobby();
- return false;
- }
- }
-};
+var g_Lobby;
/**
* Called after the XmppConnection succeeded and when returning from a game.
- *
- * @param {Object} attribs
*/
function init(attribs)
{
- g_Dialog = attribs && attribs.dialog;
-
- if (!g_Settings)
- {
- leaveLobby();
- return;
- }
-
- initMusic();
- global.music.setState(global.music.states.MENU);
-
- initDialogStyle();
- initGameFilters();
- updateConnectedState();
-
- Engine.LobbySetPlayerPresence("available");
-
- updatePlayerList();
- updateSubject(Engine.LobbyGetRoomSubject());
- updateLobbyColumns();
-
- updateToggleBuddy();
- Engine.GetGUIObjectByName("chatInput").tooltip = colorizeAutocompleteHotkey();
-
- // Get all messages since the login
- for (let msg of Engine.LobbyGuiPollHistoricMessages())
- g_NetMessageTypes[msg.type][msg.level](msg);
-
- if (!Engine.IsXmppClientConnected())
- reconnectMessageBox();
-}
-
-function reconnectMessageBox()
-{
- if (g_AskedReconnect)
- return;
-
- g_AskedReconnect = true;
-
- messageBox(
- 400, 200,
- translate("You have been disconnected from the lobby. Do you want to reconnect?"),
- translate("Confirmation"),
- [translate("No"), translate("Yes")],
- [null, Engine.ConnectXmppClient]);
-}
-
-/**
- * Set style of GUI elements and the window style.
- */
-function initDialogStyle()
-{
- let lobbyWindow = Engine.GetGUIObjectByName("lobbyWindow");
- lobbyWindow.sprite = g_Dialog ? "ModernDialog" : "ModernWindow";
- lobbyWindow.size = g_Dialog ? "42 42 100%-42 100%-42" : "0 0 100% 100%";
- Engine.GetGUIObjectByName("lobbyWindowTitle").size = g_Dialog ? "50%-128 -16 50%+128 16" : "50%-128 4 50%+128 36";
-
- Engine.GetGUIObjectByName("leaveButton").caption = g_Dialog ?
- translateWithContext("previous page", "Back") :
- translateWithContext("previous page", "Main Menu");
-
- Engine.GetGUIObjectByName("hostButton").hidden = g_Dialog;
- Engine.GetGUIObjectByName("joinGameButton").hidden = g_Dialog;
- Engine.GetGUIObjectByName("gameInfoEmpty").size = "0 0 100% 100%-24" + (g_Dialog ? "" : "-30");
- Engine.GetGUIObjectByName("gameInfo").size = "0 0 100% 100%-24" + (g_Dialog ? "" : "-60");
-
- Engine.GetGUIObjectByName("middlePanel").size = "20%+5 " + (g_Dialog ? "18" : "40") + " 100%-255 100%-20";
- Engine.GetGUIObjectByName("rightPanel").size = "100%-250 " + (g_Dialog ? "18" : "40") + " 100%-20 100%-20";
- Engine.GetGUIObjectByName("leftPanel").size = "20 " + (g_Dialog ? "18" : "40") + " 20% 100%-315";
-
- if (g_Dialog)
- {
- Engine.GetGUIObjectByName("lobbyDialogToggle").onPress = leaveLobby;
- Engine.GetGUIObjectByName("cancelDialog").onPress = leaveLobby;
- }
-}
-
-/**
- * Set style of GUI elements according to the connection state of the lobby.
- */
-function updateConnectedState()
-{
- Engine.GetGUIObjectByName("chatInput").hidden = !Engine.IsXmppClientConnected();
-
- for (let button of ["host", "leaderboard", "userprofile", "toggleBuddy"])
- Engine.GetGUIObjectByName(button + "Button").enabled = Engine.IsXmppClientConnected();
-}
-
-function updateLobbyColumns()
-{
- let gameRating = Engine.ConfigDB_GetValue("user", "lobby.columns.gamerating") == "true";
-
- // Only show the selected columns
- let gamesBox = Engine.GetGUIObjectByName("gamesBox");
- gamesBox.hidden_mapType = gameRating;
- gamesBox.hidden_gameRating = !gameRating;
-
- // Only show the filters of selected columns
- let mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
- mapTypeFilter.hidden = gameRating;
- let gameRatingFilter = Engine.GetGUIObjectByName("gameRatingFilter");
- gameRatingFilter.hidden = !gameRating;
-
- // Keep filters right above the according column
- let playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
- let size = playersNumberFilter.size;
- size.rleft = gameRating ? 74 : 90;
- size.rright = gameRating ? 84 : 100;
- playersNumberFilter.size = size;
-}
-
-function leaveLobby()
-{
- if (g_Dialog)
- {
- Engine.LobbySetPlayerPresence("playing");
- Engine.PopGuiPage();
- }
- else
- {
- Engine.StopXmppClient();
- Engine.SwitchGuiPage("page_pregame.xml");
- }
-}
-
-function initGameFilters()
-{
- let mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
- mapSizeFilter.list = [translateWithContext("map size", "Any")].concat(g_MapSizes.Name);
- mapSizeFilter.list_data = [""].concat(g_MapSizes.Tiles);
-
- let playersArray = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); // 1, 2, ... MaxPlayers
- let playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
- playersNumberFilter.list = [translateWithContext("player number", "Any")].concat(playersArray);
- playersNumberFilter.list_data = [""].concat(playersArray);
-
- let mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
- mapTypeFilter.list = [translateWithContext("map", "Any")].concat(g_MapTypes.Title);
- mapTypeFilter.list_data = [""].concat(g_MapTypes.Name);
-
- let gameRatingOptions = [">1500", ">1400", ">1300", ">1200", "<1200", "<1100", "<1000"];
- gameRatingOptions = prepareForDropdown(gameRatingOptions.map(r => ({
- "value": r,
- "label": sprintf(
- r[0] == ">" ?
- translateWithContext("gamelist filter", "> %(rating)s") :
- translateWithContext("gamelist filter", "< %(rating)s"),
- { "rating": r.substr(1) })
- })));
-
- let gameRatingFilter = Engine.GetGUIObjectByName("gameRatingFilter");
- gameRatingFilter.list = [translateWithContext("map", "Any")].concat(gameRatingOptions.label);
- gameRatingFilter.list_data = [""].concat(gameRatingOptions.value);
-
- resetFilters();
-}
-
-function resetFilters()
-{
- Engine.GetGUIObjectByName("mapSizeFilter").selected = 0;
- Engine.GetGUIObjectByName("playersNumberFilter").selected = 0;
- Engine.GetGUIObjectByName("mapTypeFilter").selected = g_MapTypes.Default;
- Engine.GetGUIObjectByName("gameRatingFilter").selected = 0;
- Engine.GetGUIObjectByName("filterOpenGames").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)
-{
- let mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
- let playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
- let mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
- let gameRatingFilter = Engine.GetGUIObjectByName("gameRatingFilter");
- let filterOpenGames = Engine.GetGUIObjectByName("filterOpenGames");
-
- // 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.maxnbp != playersNumberFilter.list_data[playersNumberFilter.selected])
- return true;
-
- if (mapTypeFilter.selected != 0 &&
- game.mapType != mapTypeFilter.list_data[mapTypeFilter.selected])
- return true;
-
- if (filterOpenGames.checked && (game.nbp >= game.maxnbp || game.state != "init"))
- return true;
-
- if (gameRatingFilter.selected > 0)
- {
- let selected = gameRatingFilter.list_data[gameRatingFilter.selected];
- if (selected.startsWith(">") && +selected.substr(1) >= game.gameRating ||
- selected.startsWith("<") && +selected.substr(1) <= game.gameRating)
- return true;
- }
-
- return false;
-}
-
-function handleKick(banned, nick, reason, time, historic)
-{
- let kickString = nick == g_Username ?
- banned ?
- translate("You have been banned from the lobby!") :
- translate("You have been kicked from the lobby!") :
- banned ?
- translate("%(nick)s has been banned from the lobby.") :
- translate("%(nick)s has been kicked from the lobby.");
-
- if (reason)
- reason = sprintf(translateWithContext("lobby kick", "Reason: %(reason)s"), {
- "reason": reason
- });
-
- if (nick != g_Username)
- {
- addChatMessage({
- "text": "/special " + sprintf(kickString, { "nick": nick }) + " " + reason,
- "time": time,
- "historic": historic,
- "isSpecial": true
- });
- return;
- }
-
- addChatMessage({
- "from": "system",
- "time": time,
- "text": kickString + " " + reason,
- });
-
- g_Kicked = true;
-
- Engine.DisconnectXmppClient();
-
- messageBox(
- 400, 250,
- kickString + "\n" + reason,
- banned ? translate("BANNED") : translate("KICKED")
- );
-}
-
-/**
- * Update the subject GUI object.
- */
-function updateSubject(newSubject)
-{
- Engine.GetGUIObjectByName("subject").caption = newSubject;
-
- // If the subject is only whitespace, hide it and reposition the logo.
- let subjectBox = Engine.GetGUIObjectByName("subjectBox");
- subjectBox.hidden = !newSubject.trim();
-
- let logo = Engine.GetGUIObjectByName("logo");
- if (subjectBox.hidden)
- logo.size = "50%-110 50%-50 50%+110 50%+50";
+ if (g_Settings)
+ g_Lobby = new Lobby(attribs && attribs.dialog);
else
- logo.size = "50%-110 40 50%+110 140";
-}
-
-/**
- * Update the caption of the toggle buddy button.
- */
-function updateToggleBuddy()
-{
- let playerList = Engine.GetGUIObjectByName("playersBox");
- let playerName = playerList.list[playerList.selected];
-
- let toggleBuddyButton = Engine.GetGUIObjectByName("toggleBuddyButton");
- toggleBuddyButton.caption = g_Buddies.indexOf(playerName) != -1 ? translate("Unmark as Buddy") : translate("Mark as Buddy");
- toggleBuddyButton.enabled = !!playerName && playerName != g_Username;
-}
-
-/**
- * Do a full update of the player listing, including ratings from cached C++ information.
- */
-function updatePlayerList()
-{
- let playersBox = Engine.GetGUIObjectByName("playersBox");
- let sortBy = playersBox.selected_column || "name";
- let sortOrder = playersBox.selected_column_order || 1;
-
- let buddyStatusList = [];
- let playerList = [];
- let presenceList = [];
- let nickList = [];
- let ratingList = [];
-
- let cleanPlayerList = Engine.GetPlayerList().map(player => {
- player.isBuddy = g_Buddies.indexOf(player.name) != -1;
- return player;
- }).sort((a, b) => {
- let sortA, sortB;
- let statusOrder = Object.keys(g_PlayerStatuses);
- let statusA = statusOrder.indexOf(a.presence) + a.name.toLowerCase();
- let statusB = statusOrder.indexOf(b.presence) + b.name.toLowerCase();
-
- switch (sortBy)
- {
- case 'buddy':
- sortA = (a.isBuddy ? 1 : 2) + statusA;
- sortB = (b.isBuddy ? 1 : 2) + statusB;
- break;
- case 'rating':
- sortA = +a.rating;
- sortB = +b.rating;
- break;
- case 'status':
- sortA = statusA;
- sortB = statusB;
- 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;
- buddyStatusList.push(player.isBuddy ? coloredText(g_BuddySymbol, statusColor) : "");
- playerList.push(colorPlayerName((player.role == "moderator" ? g_ModeratorPrefix : "") + player.name));
- presenceList.push(coloredText(g_PlayerStatuses[presence].status, statusColor));
- ratingList.push(coloredText(rating, statusColor));
- nickList.push(player.name);
- }
-
- playersBox.list_buddy = buddyStatusList;
- playersBox.list_name = playerList;
- playersBox.list_status = presenceList;
- playersBox.list_rating = ratingList;
- playersBox.list = nickList;
-
- playersBox.selected = playersBox.list.indexOf(g_SelectedPlayer);
-}
-
-/**
-* Toggle buddy state for a player in playerlist within the user config
-*/
-function toggleBuddy()
-{
- let playerList = Engine.GetGUIObjectByName("playersBox");
- let name = playerList.list[playerList.selected];
-
- if (!name || name == g_Username || name.indexOf(g_BuddyListDelimiter) != -1)
- return;
-
- let index = g_Buddies.indexOf(name);
- if (index != -1)
- g_Buddies.splice(index, 1);
- else
- g_Buddies.push(name);
-
- updateToggleBuddy();
-
- Engine.ConfigDB_CreateAndWriteValueToFile("user", "lobby.buddies", g_Buddies.filter(nick => nick).join(g_BuddyListDelimiter) || g_BuddyListDelimiter, "config/user.cfg");
-
- updatePlayerList();
- updateGameList();
-}
-
-/**
- * Select the game where the selected player is currently playing, observing or offline.
- * Selects in that order to account for players that occur in multiple games.
- */
-function selectGameFromPlayername()
-{
- if (!g_SelectedPlayer)
- return;
-
- let gameList = Engine.GetGUIObjectByName("gamesBox");
- let foundAsObserver = false;
-
- for (let i = 0; i < g_GameList.length; ++i)
- for (let player of stringifiedTeamListToPlayerData(g_GameList[i].players))
- {
- if (g_SelectedPlayer != splitRatingFromNick(player.Name).nick)
- continue;
-
- gameList.auto_scroll = true;
- if (player.Team == "observer")
- {
- foundAsObserver = true;
- gameList.selected = i;
- }
- else if (!player.Offline)
- {
- gameList.selected = i;
- return;
- }
- else if (!foundAsObserver)
- gameList.selected = i;
- }
-}
-
-function onPlayerListSelection()
-{
- let playerList = Engine.GetGUIObjectByName("playersBox");
- if (playerList.selected == playerList.list.indexOf(g_SelectedPlayer))
- return;
-
- g_SelectedPlayer = playerList.list[playerList.selected];
- lookupSelectedUserProfile("playersBox");
- updateToggleBuddy();
- selectGameFromPlayername();
-}
-
-function setLeaderboardVisibility(visible)
-{
- if (visible)
- Engine.SendGetBoardList();
-
- lookupSelectedUserProfile(visible ? "leaderboardBox" : "playersBox");
- Engine.GetGUIObjectByName("leaderboard").hidden = !visible;
- Engine.GetGUIObjectByName("fade").hidden = !visible;
-}
-
-function setUserProfileVisibility(visible)
-{
- Engine.GetGUIObjectByName("profileFetch").hidden = !visible;
- Engine.GetGUIObjectByName("fade").hidden = !visible;
-}
-
-/**
- * Display the profile of the player in the user profile window.
- */
-function lookupUserProfile()
-{
- Engine.SendGetProfile(Engine.GetGUIObjectByName("fetchInput").caption);
-}
-
-/**
- * Display the profile of the selected player in the main window.
- * Displays N/A for all stats until updateProfile is called when the stats
- * are actually received from the bot.
- */
-function lookupSelectedUserProfile(guiObjectName)
-{
- let playerList = Engine.GetGUIObjectByName(guiObjectName);
- let playerName = playerList.list[playerList.selected];
-
- Engine.GetGUIObjectByName("profileArea").hidden = !playerName && !Engine.GetGUIObjectByName("usernameText").caption;
- if (!playerName)
- return;
-
- Engine.SendGetProfile(playerName);
-
- Engine.GetGUIObjectByName("usernameText").caption = playerName;
- 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");
-
- updateUserRoleText(playerName);
-}
-
-function updateUserRoleText(playerName)
-{
- Engine.GetGUIObjectByName("roleText").caption = g_RoleNames[Engine.LobbyGetPlayerRole(playerName) || "participant"];
-}
-
-/**
- * Update the profile of the selected player with data from the bot.
- */
-function updateProfile()
-{
- let attributes = Engine.GetProfile()[0];
-
- let user = colorPlayerName(attributes.player, attributes.rating);
-
- if (!Engine.GetGUIObjectByName("profileFetch").hidden)
- {
- let profileFound = attributes.rating != "-2";
- Engine.GetGUIObjectByName("profileWindowArea").hidden = !profileFound;
- Engine.GetGUIObjectByName("profileErrorText").hidden = profileFound;
-
- if (!profileFound)
- {
- Engine.GetGUIObjectByName("profileErrorText").caption = sprintf(
- translate("Player \"%(nick)s\" not found."),
- { "nick": attributes.player }
- );
- return;
- }
-
- Engine.GetGUIObjectByName("profileUsernameText").caption = user;
- Engine.GetGUIObjectByName("profileRankText").caption = attributes.rank;
- Engine.GetGUIObjectByName("profileHighestRatingText").caption = attributes.highestRating;
- Engine.GetGUIObjectByName("profileTotalGamesText").caption = attributes.totalGamesPlayed;
- Engine.GetGUIObjectByName("profileWinsText").caption = attributes.wins;
- Engine.GetGUIObjectByName("profileLossesText").caption = attributes.losses;
- Engine.GetGUIObjectByName("profileRatioText").caption = formatWinRate(attributes);
- return;
- }
-
- let playerList;
- if (!Engine.GetGUIObjectByName("leaderboard").hidden)
- playerList = Engine.GetGUIObjectByName("leaderboardBox");
- else
- playerList = Engine.GetGUIObjectByName("playersBox");
-
- if (attributes.rating == "-2")
- return;
-
- // Make sure the stats we have received coincide with the selected player.
- if (attributes.player != playerList.list[playerList.selected])
- return;
-
- Engine.GetGUIObjectByName("usernameText").caption = user;
- Engine.GetGUIObjectByName("rankText").caption = attributes.rank;
- Engine.GetGUIObjectByName("highestRatingText").caption = attributes.highestRating;
- Engine.GetGUIObjectByName("totalGamesText").caption = attributes.totalGamesPlayed;
- Engine.GetGUIObjectByName("winsText").caption = attributes.wins;
- Engine.GetGUIObjectByName("lossesText").caption = attributes.losses;
- Engine.GetGUIObjectByName("ratioText").caption = formatWinRate(attributes);
-}
-
-/**
- * Update the leaderboard from data cached in C++.
- */
-function updateLeaderboard()
-{
- let leaderboard = Engine.GetGUIObjectByName("leaderboardBox");
- let boardList = Engine.GetBoardList().sort((a, b) => b.rating - a.rating);
-
- let list = [];
- let list_name = [];
- let list_rank = [];
- let 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()
-{
- let gamesBox = Engine.GetGUIObjectByName("gamesBox");
- let sortBy = gamesBox.selected_column;
- let sortOrder = gamesBox.selected_column_order;
-
- if (gamesBox.selected > -1)
- {
- g_SelectedGameIP = g_GameList[gamesBox.selected].ip;
- g_SelectedGamePort = g_GameList[gamesBox.selected].port;
- }
-
- g_GameList = Engine.GetGameList().map(game => {
-
- game.hasBuddies = 0;
- game.observerCount = 0;
-
- // Compute average rating of participating players
- let playerRatings = [];
-
- for (let player of stringifiedTeamListToPlayerData(game.players))
- {
- let playerNickRating = splitRatingFromNick(player.Name);
-
- if (player.Team != "observer")
- playerRatings.push(playerNickRating.rating || g_DefaultLobbyRating);
- else
- ++game.observerCount;
-
- // Sort games with playing buddies above games with spectating buddies
- if (game.hasBuddies < 2 && g_Buddies.indexOf(playerNickRating.nick) != -1)
- game.hasBuddies = player.Team == "observer" ? 1 : 2;
- }
-
- game.gameRating =
- playerRatings.length ?
- Math.round(playerRatings.reduce((sum, current) => sum + current) / playerRatings.length) :
- g_DefaultLobbyRating;
-
- try
- {
- game.mods = JSON.parse(game.mods);
- }
- catch (e)
- {
- game.mods = [];
- }
-
- if (!hasSameMods(game.mods, Engine.GetEngineInfo().mods))
- game.state = "incompatible";
-
- return game;
- }).filter(game => !filterGame(game)).sort((a, b) => {
- let sortA, sortB;
- switch (sortBy)
- {
- case 'name':
- sortA = g_GameStatusOrder.indexOf(a.state) + a.name.toLowerCase();
- sortB = g_GameStatusOrder.indexOf(b.state) + b.name.toLowerCase();
- break;
- case 'gameRating':
- case 'mapSize':
- case 'mapType':
- sortA = a[sortBy];
- sortB = b[sortBy];
- break;
- case 'buddy':
- sortA = String(b.hasBuddies) + g_GameStatusOrder.indexOf(a.state) + a.name.toLowerCase();
- sortB = String(a.hasBuddies) + g_GameStatusOrder.indexOf(b.state) + b.name.toLowerCase();
- break;
- case 'mapName':
- sortA = translate(a.niceMapName);
- sortB = translate(b.niceMapName);
- break;
- case 'nPlayers':
- sortA = a.maxnbp;
- sortB = b.maxnbp;
- break;
- }
- if (sortA < sortB) return -sortOrder;
- if (sortA > sortB) return +sortOrder;
- return 0;
- });
-
- let list_buddy = [];
- let list_name = [];
- let list_mapName = [];
- let list_mapSize = [];
- let list_mapType = [];
- let list_nPlayers = [];
- let list_gameRating = [];
- let list = [];
- let list_data = [];
- let 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 && game.port == g_SelectedGamePort)
- selectedGameIndex = +i;
-
- list_buddy.push(game.hasBuddies ? coloredText(g_BuddySymbol, g_GameColors[game.state]) : "");
- list_name.push(coloredText(gameName, g_GameColors[game.state]));
- list_mapName.push(translateMapTitle(game.niceMapName));
- list_mapSize.push(translateMapSize(game.mapSize));
- list_mapType.push(g_MapTypes.Title[mapTypeIdx] || "");
- list_nPlayers.push(
- sprintf(
- game.observerCount ?
- // Translation: The number of players and observers in this game
- translate("%(current)s/%(max)s +%(observercount)s") :
- // Translation: The number of players in this game
- translate("%(current)s/%(max)s"),
- {
- "current": setStringTags(game.nbp, g_PlayerCountTags.CurrentPlayers),
- "max": setStringTags(game.maxnbp, g_PlayerCountTags.MaxPlayers),
- "observercount": setStringTags(game.observerCount, g_PlayerCountTags.Observers)
- }));
- list_gameRating.push(game.gameRating);
- list.push(gameName);
- list_data.push(i);
- }
-
- gamesBox.list_buddy = list_buddy;
- 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;
- gamesBox.list_gameRating = list_gameRating;
-
- // Change these last, otherwise crash
- gamesBox.list = list;
- gamesBox.list_data = list_data;
-
- gamesBox.auto_scroll = false;
- gamesBox.selected = selectedGameIndex;
-
- updateGameSelection();
-}
-
-/**
- * Populate the game info area with information on the current game selection.
- */
-function updateGameSelection()
-{
- let game = selectedGame();
-
- Engine.GetGUIObjectByName("gameInfo").hidden = !game;
- Engine.GetGUIObjectByName("joinGameButton").hidden = g_Dialog || !game;
- Engine.GetGUIObjectByName("gameInfoEmpty").hidden = !!game;
-
- if (!game)
- return;
-
- Engine.GetGUIObjectByName("sgMapName").caption = translateMapTitle(game.niceMapName);
-
- let sgGameStartTime = Engine.GetGUIObjectByName("sgGameStartTime");
- let sgNbPlayers = Engine.GetGUIObjectByName("sgNbPlayers");
- let sgPlayersNames = Engine.GetGUIObjectByName("sgPlayersNames");
-
- let playersNamesSize = sgPlayersNames.size;
- playersNamesSize.top = game.startTime ? sgGameStartTime.size.bottom : sgNbPlayers.size.bottom;
- playersNamesSize.rtop = game.startTime ? sgGameStartTime.size.rbottom : sgNbPlayers.size.rbottom;
- sgPlayersNames.size = playersNamesSize;
-
- sgGameStartTime.hidden = !game.startTime;
- if (game.startTime)
- sgGameStartTime.caption = sprintf(
- // Translation: %(time)s is the hour and minute here.
- translate("Game started at %(time)s"), {
- "time": Engine.FormatMillisecondsIntoDateStringLocal(+game.startTime * 1000, translate("HH:mm"))
- });
-
- sgNbPlayers.caption = sprintf(
- translate("Players: %(current)s/%(total)s"), {
- "current": game.nbp,
- "total": game.maxnbp
- });
-
- sgPlayersNames.caption = formatPlayerInfo(stringifiedTeamListToPlayerData(game.players));
- Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(game.mapSize);
-
- let mapTypeIdx = g_MapTypes.Name.indexOf(game.mapType);
- Engine.GetGUIObjectByName("sgMapType").caption = g_MapTypes.Title[mapTypeIdx] || "";
-
- let mapData = getMapDescriptionAndPreview(game.mapType, game.mapName);
- Engine.GetGUIObjectByName("sgMapPreview").sprite = getMapPreviewImage(mapData.preview);
- Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description;
-}
-
-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 || g_Dialog)
- return;
-
- let rating = getRejoinRating(game);
- let username = rating ? g_Username + " (" + rating + ")" : g_Username;
-
- if (game.state == "incompatible")
- messageBox(
- 400, 200,
- translate("Your active mods do not match the mods of this game.") + "\n\n" +
- comparedModsString(game.mods, Engine.GetEngineInfo().mods) + "\n\n" +
- translate("Do you want to switch to the mod selection page?"),
- translate("Incompatible mods"),
- [translate("No"), translate("Yes")],
- [
- null,
- () => {
- Engine.StopXmppClient();
- Engine.SwitchGuiPage("page_modmod.xml", {
- "cancelbutton": true
- });
- }
- ]
- );
- else if (game.state == "init" || stringifiedTeamListToPlayerData(game.players).some(player => player.Name == username))
- joinSelectedGame();
- else
- messageBox(
- 400, 200,
- translate("The game has already started. Do you want to join as observer?"),
- translate("Confirmation"),
- [translate("No"), translate("Yes")],
- [null, joinSelectedGame]
- );
-}
-
-/**
- * Attempt to join the selected game without asking for confirmation.
- */
-function joinSelectedGame()
-{
- let game = selectedGame();
- if (!game)
- return;
-
- let ip;
- let port;
- if (game.stunIP)
- {
- ip = game.stunIP;
- port = game.stunPort;
- }
- else
- {
- ip = game.ip;
- port = game.port;
- }
-
- if (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": ip,
- "port": port,
- "name": g_Username,
- "rating": getRejoinRating(game),
- "useSTUN": !!game.stunIP,
- "hostJID": game.hostUsername + "@" + g_LobbyServer + "/0ad"
- });
-}
-
-/**
- * Rejoin games with the original playername, even if the rating changed meanwhile.
- */
-function getRejoinRating(game)
-{
- for (let player of stringifiedTeamListToPlayerData(game.players))
- {
- let playerNickRating = splitRatingFromNick(player.Name);
- if (playerNickRating.nick == g_Username)
- return playerNickRating.rating;
- }
- return 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();
-
- let updateList = false;
-
- while (true)
- {
- let msg = Engine.LobbyGuiPollNewMessage();
- 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);
- }
-
- if (Engine.LobbyGuiPollHasPlayerListUpdate())
- updatePlayerList();
-}
-
-/**
- * Executes a lobby command or sends GUI input directly as chat.
- */
-function submitChatInput()
-{
- let input = Engine.GetGUIObjectByName("chatInput");
- let text = input.caption;
-
- if (!text.length)
- return;
-
- if (handleChatCommand(text))
- Engine.LobbySendMessage(text);
-
- input.caption = "";
-}
-
-/**
- * Handle all '/' commands.
- *
- * @param {string} text - Text to be checked for commands.
- * @returns {boolean} true if the text should be sent via chat.
- */
-function handleChatCommand(text)
-{
- if (text[0] != '/')
- return true;
-
- let [cmd, args] = ircSplit(text);
- args = ircSplit("/" + args);
-
- if (!g_ChatCommands[cmd])
- {
- addChatMessage({
- "from": "system",
- "text": sprintf(
- translate("The command '%(cmd)s' is not supported."), {
- "cmd": coloredText(cmd, g_ChatCommandColor)
- })
- });
- return false;
- }
-
- if (g_ChatCommands[cmd].moderatorOnly && Engine.LobbyGetPlayerRole(g_Username) != "moderator")
- {
- addChatMessage({
- "from": "system",
- "text": sprintf(
- translate("The command '%(cmd)s' is restricted to moderators."), {
- "cmd": coloredText(cmd, g_ChatCommandColor)
- })
- });
- return false;
- }
-
- return g_ChatCommands[cmd].handler(args);
-}
-
-/**
- * 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));
-
- if (!msg.historic && msg.text.toLowerCase().indexOf(g_Username.toLowerCase()) != -1)
- soundNotification("nick");
- }
- }
-
- let formatted = ircFormat(msg);
- if (!formatted)
- return;
-
- g_ChatMessages.push(formatted);
- Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
-}
-
-function clearChatMessages()
-{
- g_ChatMessages.length = 0;
- Engine.GetGUIObjectByName("chatText").caption = "";
-}
-
-/**
- * Splits given input into command and argument.
- */
-function ircSplit(string)
-{
- let 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)
-{
- let formattedMessage = "";
- let coloredFrom = msg.from && colorPlayerName(msg.from);
-
- // Handle commands allowed past handleChatCommand.
- if (msg.text && msg.text[0] == '/')
- {
- let [command, message] = ircSplit(msg.text);
- switch (command)
- {
- case "me":
- {
- // Translation: IRC message prefix when the sender uses the /me command.
- let senderString = sprintf(translate("* %(sender)s"), {
- "sender": coloredFrom
- });
-
- // Translation: IRC message issued using the ‘/me’ command.
- formattedMessage = sprintf(translate("%(sender)s %(action)s"), {
- "sender": senderFont(senderString),
- "action": message
- });
- break;
- }
- case "say":
- {
- // Translation: IRC message prefix.
- let senderString = sprintf(translate("<%(sender)s>"), {
- "sender": coloredFrom
- });
-
- // Translation: IRC message.
- formattedMessage = sprintf(translate("%(sender)s %(message)s"), {
- "sender": senderFont(senderString),
- "message": message
- });
- break;
- }
- case "special":
- {
- if (msg.isSpecial)
- // Translation: IRC system message.
- formattedMessage = senderFont(sprintf(translate("== %(message)s"), {
- "message": message
- }));
- else
- {
- // Translation: IRC message prefix.
- let senderString = sprintf(translate("<%(sender)s>"), {
- "sender": coloredFrom
- });
-
- // Translation: IRC message.
- formattedMessage = sprintf(translate("%(sender)s %(message)s"), {
- "sender": senderFont(senderString),
- "message": message
- });
- }
- break;
- }
- default:
- return "";
- }
- }
- else
- {
- let senderString;
-
- // Translation: IRC message prefix.
- if (msg.private)
- senderString = sprintf(translateWithContext("lobby private message", "(%(private)s) <%(sender)s>"), {
- "private": coloredText(translate("Private"), g_PrivateMessageColor),
- "sender": coloredFrom
- });
- else
- senderString = sprintf(translate("<%(sender)s>"), {
- "sender": coloredFrom
- });
-
- // Translation: IRC message.
- formattedMessage = sprintf(translate("%(sender)s %(message)s"), {
- "sender": senderFont(senderString),
- "message": msg.text
- });
- }
-
- // Add chat message timestamp
- if (Engine.ConfigDB_GetValue("user", "chat.timestamp") != "true")
- return formattedMessage;
-
- // 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
- let timeString = Engine.FormatMillisecondsIntoDateStringLocal(msg.time ? msg.time * 1000 : Date.now(), translate("HH:mm"));
-
- // Translation: Time prefix as shown in the multiplayer lobby (when you enable it in the options page).
- let timePrefixString = sprintf(translate("\\[%(time)s]"), {
- "time": timeString
- });
-
- // Translation: IRC message format when there is a time prefix.
- return sprintf(translate("%(time)s %(message)s"), {
- "time": timePrefixString,
- "message": formattedMessage
- });
-}
-
-/**
- * Generate a (mostly) unique color for this player based on their name.
- * @see https://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-jquery-javascript
- * @param {string} playername
- */
-function getPlayerColor(playername)
-{
- if (playername == "system")
- return g_SystemColor;
-
- // Generate a probably-unique hash for the player name and use that to create a color.
- let hash = 0;
- for (let i in playername)
- 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.
- let [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
- * @param {string} rating
- */
-function colorPlayerName(playername, rating)
-{
- return coloredText(
- (rating ? sprintf(
- translate("%(nick)s (%(rating)s)"), {
- "nick": playername,
- "rating": rating
- }) : playername
- ),
- getPlayerColor(playername.replace(g_ModeratorPrefix, "")));
-}
-
-function senderFont(text)
-{
- return '[font="' + g_SenderFont + '"]' + text + "[/font]";
-}
-
-function formatWinRate(attr)
-{
- if (!attr.totalGamesPlayed)
- return translateWithContext("Used for an undefined winning rate", "-");
-
- return sprintf(translate("%(percentage)s%%"), {
- "percentage": (attr.wins / attr.totalGamesPlayed * 100).toFixed(2)
- });
+ error("Could not load settings");
}
Index: ps/trunk/source/lobby/IXmppClient.h
===================================================================
--- ps/trunk/source/lobby/IXmppClient.h (revision 23171)
+++ ps/trunk/source/lobby/IXmppClient.h (revision 23172)
@@ -1,69 +1,70 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef IXMPPCLIENT_H
#define IXMPPCLIENT_H
#include "scriptinterface/ScriptTypes.h"
class ScriptInterface;
namespace StunClient {
struct StunEndpoint;
}
class IXmppClient
{
public:
static IXmppClient* create(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize = 0, bool regOpt = false);
virtual ~IXmppClient() {}
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual bool isConnected() = 0;
virtual void recv() = 0;
virtual void SendIqGetBoardList() = 0;
virtual void SendIqGetProfile(const std::string& player) = 0;
virtual void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0;
virtual void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0;
virtual void SendIqUnregisterGame() = 0;
virtual void SendIqChangeStateGame(const std::string& nbp, const std::string& players) = 0;
virtual void SendIqLobbyAuth(const std::string& to, const std::string& token) = 0;
virtual void SetNick(const std::string& nick) = 0;
virtual void GetNick(std::string& nick) = 0;
virtual void kick(const std::string& nick, const std::string& reason) = 0;
virtual void ban(const std::string& nick, const std::string& reason) = 0;
virtual void SetPresence(const std::string& presence) = 0;
virtual const char* GetPresence(const std::string& nickname) = 0;
virtual const char* GetRole(const std::string& nickname) = 0;
+ virtual std::wstring GetRating(const std::string& nickname) = 0;
virtual const std::wstring& GetSubject() = 0;
virtual void GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0;
virtual void GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0;
virtual void GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0;
virtual void GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0;
- virtual JS::Value GuiPollNewMessage(const ScriptInterface& scriptInterface) = 0;
+ virtual JS::Value GuiPollNewMessages(const ScriptInterface& scriptInterface) = 0;
virtual JS::Value GuiPollHistoricMessages(const ScriptInterface& scriptInterface) = 0;
virtual bool GuiPollHasPlayerListUpdate() = 0;
virtual void SendMUCMessage(const std::string& message) = 0;
virtual void SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJID) = 0;
};
extern IXmppClient *g_XmppClient;
extern bool g_rankedGame;
#endif // XMPPCLIENT_H
Index: ps/trunk/source/lobby/scripting/JSInterface_Lobby.h
===================================================================
--- ps/trunk/source/lobby/scripting/JSInterface_Lobby.h (revision 23171)
+++ ps/trunk/source/lobby/scripting/JSInterface_Lobby.h (revision 23172)
@@ -1,72 +1,73 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_JSI_LOBBY
#define INCLUDED_JSI_LOBBY
#include "lib/config2.h"
#include "scriptinterface/ScriptInterface.h"
#include
namespace JSI_Lobby
{
void RegisterScriptFunctions(const ScriptInterface& scriptInterface);
bool HasXmppClient(ScriptInterface::CxPrivate* pCxPrivate);
bool IsRankedGame(ScriptInterface::CxPrivate* pCxPrivate);
void SetRankedGame(ScriptInterface::CxPrivate* pCxPrivate, bool isRanked);
#if CONFIG2_LOBBY
void StartXmppClient(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& username, const std::wstring& password, const std::wstring& room, const std::wstring& nick, int historyRequestSize);
void StartRegisterXmppClient(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& username, const std::wstring& password);
void StopXmppClient(ScriptInterface::CxPrivate* pCxPrivate);
void ConnectXmppClient(ScriptInterface::CxPrivate* pCxPrivate);
void DisconnectXmppClient(ScriptInterface::CxPrivate* pCxPrivate);
bool IsXmppClientConnected(ScriptInterface::CxPrivate* pCxPrivate);
void SendGetBoardList(ScriptInterface::CxPrivate* pCxPrivate);
void SendGetProfile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& player);
void SendGameReport(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data);
void SendRegisterGame(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data);
void SendUnregisterGame(ScriptInterface::CxPrivate* pCxPrivate);
void SendChangeStateGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nbp, const std::wstring& players);
JS::Value GetPlayerList(ScriptInterface::CxPrivate* pCxPrivate);
JS::Value GetGameList(ScriptInterface::CxPrivate* pCxPrivate);
JS::Value GetBoardList(ScriptInterface::CxPrivate* pCxPrivate);
JS::Value GetProfile(ScriptInterface::CxPrivate* pCxPrivate);
- JS::Value LobbyGuiPollNewMessage(ScriptInterface::CxPrivate* pCxPrivate);
+ JS::Value LobbyGuiPollNewMessages(ScriptInterface::CxPrivate* pCxPrivate);
JS::Value LobbyGuiPollHistoricMessages(ScriptInterface::CxPrivate* pCxPrivate);
bool LobbyGuiPollHasPlayerListUpdate(ScriptInterface::CxPrivate* pCxPrivate);
void LobbySendMessage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& message);
void LobbySetPlayerPresence(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& presence);
void LobbySetNick(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nick);
std::wstring LobbyGetNick(ScriptInterface::CxPrivate* pCxPrivate);
void LobbyKick(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nick, const std::wstring& reason);
void LobbyBan(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nick, const std::wstring& reason);
const char* LobbyGetPlayerPresence(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nickname);
const char* LobbyGetPlayerRole(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nickname);
+ std::wstring LobbyGetPlayerRating(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nickname);
std::wstring LobbyGetRoomSubject(ScriptInterface::CxPrivate* pCxPrivate);
// Non-public secure PBKDF2 hash function with salting and 1,337 iterations
std::string EncryptPassword(const std::string& password, const std::string& username);
// Public hash interface.
std::wstring EncryptPassword(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& pass, const std::wstring& user);
#endif // CONFIG2_LOBBY
}
#endif // INCLUDED_JSI_LOBBY
Index: ps/trunk/binaries/data/mods/public/gui/lobby/XmppMessages.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/XmppMessages.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/XmppMessages.js (revision 23172)
@@ -0,0 +1,115 @@
+/**
+ * This class stores and triggers the event handlers for the GUI messages constructed by the XmppClient.
+ */
+class XmppMessages
+{
+ constructor()
+ {
+ this.xmppMessageHandlers = {};
+ for (let type in this.MessageTypes)
+ {
+ this.xmppMessageHandlers[type] = {};
+ for (let level of this.MessageTypes[type])
+ this.xmppMessageHandlers[type][level] = new Set();
+ }
+
+ this.messageBatchProcessedHandlers = new Set();
+ this.playerListUpdateHandlers = new Set();
+
+ Engine.GetGUIObjectByName("lobbyPage").onTick = this.onTick.bind(this);
+ }
+
+ registerXmppMessageHandler(type, level, handler)
+ {
+ this.xmppMessageHandlers[type][level].add(handler);
+ }
+
+ unregisterXmppMessageHandler(type, level, handler)
+ {
+ this.xmppMessageHandlers[type][level].delete(handler);
+ }
+
+ registerMessageBatchProcessedHandler(handler)
+ {
+ this.messageBatchProcessedHandlers.add(handler);
+ }
+
+ unregisterMessageBatchProcessedHandler(handler)
+ {
+ this.messageBatchProcessedHandlers.delete(handler);
+ }
+
+ registerPlayerListUpdateHandler(handler)
+ {
+ this.playerListUpdateHandlers.add(handler);
+ }
+
+ unregisterPlayerListUpdateHandler(handler)
+ {
+ this.playerListUpdateHandlers.delete(handler);
+ }
+
+ onTick()
+ {
+ this.handleMessages(Engine.LobbyGuiPollNewMessages);
+
+ if (Engine.LobbyGuiPollHasPlayerListUpdate())
+ for (let handler of this.playerListUpdateHandlers)
+ handler();
+ }
+
+ processHistoricMessages()
+ {
+ this.handleMessages(Engine.LobbyGuiPollHistoricMessages);
+ }
+
+ handleMessages(getMessages)
+ {
+ let messages = getMessages();
+ if (!messages)
+ return;
+
+ for (let message of messages)
+ {
+ if (!this.xmppMessageHandlers[message.type])
+ error("Unrecognized message type: " + message.type);
+ else if (!this.xmppMessageHandlers[message.type][message.level])
+ error("Unrecognized message level: " + message.level);
+ else
+ for (let handler of this.xmppMessageHandlers[message.type][message.level])
+ handler(message);
+ }
+
+ for (let handler of this.messageBatchProcessedHandlers)
+ handler();
+ }
+}
+
+/**
+ * Processing of notifications sent by XmppClient.cpp.
+ */
+XmppMessages.prototype.MessageTypes = {
+ "system": [
+ "registered",
+ "connected",
+ "disconnected",
+ "error"
+ ],
+ "chat": [
+ "subject",
+ "join",
+ "leave",
+ "role",
+ "nick",
+ "kicked",
+ "banned",
+ "room-message",
+ "private-message"
+ ],
+ "game": [
+ "gamelist",
+ "profile",
+ "leaderboard",
+ "ratinglist"
+ ]
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/lobby/XmppMessages.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/prelobby/common/feedback/feedback.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/prelobby/common/feedback/feedback.js (revision 23171)
+++ ps/trunk/binaries/data/mods/public/gui/prelobby/common/feedback/feedback.js (revision 23172)
@@ -1,39 +1,40 @@
var g_LobbyMessages = {
"error": message => {
setFeedback(message.text ||
translate("Unknown error. This usually occurs because the same IP address is not allowed to register more than one account within one hour."));
Engine.StopXmppClient();
},
"disconnected": message => {
setFeedback(message.reason + message.certificate_status);
Engine.StopXmppClient();
}
};
+/**
+ * Other message types (such as gamelists) may be received in case of the current player being logged in and
+ * logging in in a second program instance with the same account name.
+ * Therefore messages without handlers are ignored without reporting them here.
+ */
function onTick()
{
- while (true)
- {
- let message = Engine.LobbyGuiPollNewMessage();
- if (!message)
- break;
+ let messages = Engine.LobbyGuiPollNewMessages();
+ if (!messages)
+ return;
+ for (let message of messages)
if (message.type == "system" && message.level)
g_LobbyMessages[message.level](message);
- else
- warn("Unknown prelobby message: " + uneval(message));
- }
}
function setFeedback(feedbackText)
{
Engine.GetGUIObjectByName("feedback").caption = feedbackText;
Engine.GetGUIObjectByName("continue").enabled = !feedbackText;
}
function cancelButton()
{
if (Engine.HasXmppClient())
Engine.StopXmppClient();
Engine.PopGuiPage();
}
Index: ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp
===================================================================
--- ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp (revision 23171)
+++ ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp (revision 23172)
@@ -1,383 +1,392 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "JSInterface_Lobby.h"
#include "gui/GUIManager.h"
#include "lib/utf8.h"
#include "lobby/IXmppClient.h"
#include "network/NetServer.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Util.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptVal.h"
#include "third_party/encryption/pkcs5_pbkdf2.h"
#include
void JSI_Lobby::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
// Lobby functions
scriptInterface.RegisterFunction("HasXmppClient");
scriptInterface.RegisterFunction("IsRankedGame");
scriptInterface.RegisterFunction("SetRankedGame");
#if CONFIG2_LOBBY // Allow the lobby to be disabled
scriptInterface.RegisterFunction("StartXmppClient");
scriptInterface.RegisterFunction("StartRegisterXmppClient");
scriptInterface.RegisterFunction("StopXmppClient");
scriptInterface.RegisterFunction("ConnectXmppClient");
scriptInterface.RegisterFunction("DisconnectXmppClient");
scriptInterface.RegisterFunction("IsXmppClientConnected");
scriptInterface.RegisterFunction("SendGetBoardList");
scriptInterface.RegisterFunction("SendGetProfile");
scriptInterface.RegisterFunction("SendRegisterGame");
scriptInterface.RegisterFunction("SendGameReport");
scriptInterface.RegisterFunction("SendUnregisterGame");
scriptInterface.RegisterFunction("SendChangeStateGame");
scriptInterface.RegisterFunction("GetPlayerList");
scriptInterface.RegisterFunction("GetGameList");
scriptInterface.RegisterFunction("GetBoardList");
scriptInterface.RegisterFunction("GetProfile");
- scriptInterface.RegisterFunction("LobbyGuiPollNewMessage");
+ scriptInterface.RegisterFunction("LobbyGuiPollNewMessages");
scriptInterface.RegisterFunction("LobbyGuiPollHistoricMessages");
scriptInterface.RegisterFunction("LobbyGuiPollHasPlayerListUpdate");
scriptInterface.RegisterFunction("LobbySendMessage");
scriptInterface.RegisterFunction("LobbySetPlayerPresence");
scriptInterface.RegisterFunction("LobbySetNick");
scriptInterface.RegisterFunction("LobbyGetNick");
scriptInterface.RegisterFunction("LobbyKick");
scriptInterface.RegisterFunction("LobbyBan");
scriptInterface.RegisterFunction("LobbyGetPlayerPresence");
scriptInterface.RegisterFunction("LobbyGetPlayerRole");
+ scriptInterface.RegisterFunction("LobbyGetPlayerRating");
scriptInterface.RegisterFunction("EncryptPassword");
scriptInterface.RegisterFunction("LobbyGetRoomSubject");
#endif // CONFIG2_LOBBY
}
bool JSI_Lobby::HasXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_XmppClient;
}
bool JSI_Lobby::IsRankedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_rankedGame;
}
void JSI_Lobby::SetRankedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool isRanked)
{
g_rankedGame = isRanked;
}
#if CONFIG2_LOBBY
void JSI_Lobby::StartXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& username, const std::wstring& password, const std::wstring& room, const std::wstring& nick, int historyRequestSize)
{
ENSURE(!g_XmppClient);
g_XmppClient = IXmppClient::create(g_GUI->GetScriptInterface().get(), utf8_from_wstring(username), utf8_from_wstring(password),
utf8_from_wstring(room), utf8_from_wstring(nick), historyRequestSize);
g_rankedGame = true;
}
void JSI_Lobby::StartRegisterXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& username, const std::wstring& password)
{
ENSURE(!g_XmppClient);
g_XmppClient = IXmppClient::create(g_GUI->GetScriptInterface().get(), utf8_from_wstring(username), utf8_from_wstring(password),
"", "", 0, true);
}
void JSI_Lobby::StopXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_XmppClient);
SAFE_DELETE(g_XmppClient);
g_rankedGame = false;
}
void JSI_Lobby::ConnectXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_XmppClient);
g_XmppClient->connect();
}
void JSI_Lobby::DisconnectXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_XmppClient);
g_XmppClient->disconnect();
}
bool JSI_Lobby::IsXmppClientConnected(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_XmppClient);
return g_XmppClient->isConnected();
}
void JSI_Lobby::SendGetBoardList(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (!g_XmppClient)
return;
g_XmppClient->SendIqGetBoardList();
}
void JSI_Lobby::SendGetProfile(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& player)
{
if (!g_XmppClient)
return;
g_XmppClient->SendIqGetProfile(utf8_from_wstring(player));
}
void JSI_Lobby::SendGameReport(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data)
{
if (!g_XmppClient)
return;
g_XmppClient->SendIqGameReport(*(pCxPrivate->pScriptInterface), data);
}
void JSI_Lobby::SendRegisterGame(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data)
{
if (!g_XmppClient)
return;
// Prevent JS mods to register matches in the lobby that were started with lobby authentication disabled
if (!g_NetServer || !g_NetServer->UseLobbyAuth())
{
LOGERROR("Registering games in the lobby requires lobby authentication to be enabled!");
return;
}
g_XmppClient->SendIqRegisterGame(*(pCxPrivate->pScriptInterface), data);
}
void JSI_Lobby::SendUnregisterGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (!g_XmppClient)
return;
g_XmppClient->SendIqUnregisterGame();
}
void JSI_Lobby::SendChangeStateGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nbp, const std::wstring& players)
{
if (!g_XmppClient)
return;
g_XmppClient->SendIqChangeStateGame(utf8_from_wstring(nbp), utf8_from_wstring(players));
}
JS::Value JSI_Lobby::GetPlayerList(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_XmppClient)
return JS::UndefinedValue();
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue playerList(cx);
g_XmppClient->GUIGetPlayerList(*(pCxPrivate->pScriptInterface), &playerList);
return playerList;
}
JS::Value JSI_Lobby::GetGameList(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_XmppClient)
return JS::UndefinedValue();
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue gameList(cx);
g_XmppClient->GUIGetGameList(*(pCxPrivate->pScriptInterface), &gameList);
return gameList;
}
JS::Value JSI_Lobby::GetBoardList(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_XmppClient)
return JS::UndefinedValue();
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue boardList(cx);
g_XmppClient->GUIGetBoardList(*(pCxPrivate->pScriptInterface), &boardList);
return boardList;
}
JS::Value JSI_Lobby::GetProfile(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_XmppClient)
return JS::UndefinedValue();
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue profileFetch(cx);
g_XmppClient->GUIGetProfile(*(pCxPrivate->pScriptInterface), &profileFetch);
return profileFetch;
}
bool JSI_Lobby::LobbyGuiPollHasPlayerListUpdate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_XmppClient && g_XmppClient->GuiPollHasPlayerListUpdate();
}
-JS::Value JSI_Lobby::LobbyGuiPollNewMessage(ScriptInterface::CxPrivate* pCxPrivate)
+JS::Value JSI_Lobby::LobbyGuiPollNewMessages(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_XmppClient)
return JS::UndefinedValue();
- return g_XmppClient->GuiPollNewMessage(*(pCxPrivate->pScriptInterface));
+ return g_XmppClient->GuiPollNewMessages(*(pCxPrivate->pScriptInterface));
}
JS::Value JSI_Lobby::LobbyGuiPollHistoricMessages(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_XmppClient)
return JS::UndefinedValue();
return g_XmppClient->GuiPollHistoricMessages(*(pCxPrivate->pScriptInterface));
}
void JSI_Lobby::LobbySendMessage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& message)
{
if (!g_XmppClient)
return;
g_XmppClient->SendMUCMessage(utf8_from_wstring(message));
}
void JSI_Lobby::LobbySetPlayerPresence(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& presence)
{
if (!g_XmppClient)
return;
g_XmppClient->SetPresence(utf8_from_wstring(presence));
}
void JSI_Lobby::LobbySetNick(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nick)
{
if (!g_XmppClient)
return;
g_XmppClient->SetNick(utf8_from_wstring(nick));
}
std::wstring JSI_Lobby::LobbyGetNick(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (!g_XmppClient)
return L"";
std::string nick;
g_XmppClient->GetNick(nick);
return wstring_from_utf8(nick);
}
void JSI_Lobby::LobbyKick(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nick, const std::wstring& reason)
{
if (!g_XmppClient)
return;
g_XmppClient->kick(utf8_from_wstring(nick), utf8_from_wstring(reason));
}
void JSI_Lobby::LobbyBan(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nick, const std::wstring& reason)
{
if (!g_XmppClient)
return;
g_XmppClient->ban(utf8_from_wstring(nick), utf8_from_wstring(reason));
}
const char* JSI_Lobby::LobbyGetPlayerPresence(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nickname)
{
if (!g_XmppClient)
return "";
return g_XmppClient->GetPresence(utf8_from_wstring(nickname));
}
const char* JSI_Lobby::LobbyGetPlayerRole(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nickname)
{
if (!g_XmppClient)
return "";
return g_XmppClient->GetRole(utf8_from_wstring(nickname));
}
+std::wstring JSI_Lobby::LobbyGetPlayerRating(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nickname)
+{
+ if (!g_XmppClient)
+ return std::wstring();
+
+ return g_XmppClient->GetRating(utf8_from_wstring(nickname));
+}
+
// Non-public secure PBKDF2 hash function with salting and 1,337 iterations
//
// TODO: We should use libsodium's crypto_pwhash instead of this. The first reason is that
// libsodium doesn't propose a bare PBKDF2 hash in its API and it's too bad to rely on custom
// code when we have a fully-fledged library available; the second reason is that Argon2 (the
// default algorithm for crypto_pwhash) is better than what we use (and it's the default one
// in the lib for a reason).
// However changing the hashing method should be planned carefully, by trying to login with a
// password hashed the old way, and, if successful, updating the password in the database using
// the new hashing method. Dropping the old hashing code can only be done either by giving users
// a way to reset their password, or by keeping track of successful password updates and dropping
// old unused accounts after some time.
std::string JSI_Lobby::EncryptPassword(const std::string& password, const std::string& username)
{
ENSURE(sodium_init() >= 0);
const int DIGESTSIZE = crypto_hash_sha256_BYTES;
const int ITERATIONS = 1337;
cassert(DIGESTSIZE == 32);
static const unsigned char salt_base[DIGESTSIZE] = {
244, 243, 249, 244, 32, 33, 34, 35, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 32, 33, 244, 224, 127, 129, 130, 140, 153, 133, 123, 234, 123 };
// initialize the salt buffer
unsigned char salt_buffer[DIGESTSIZE] = {0};
crypto_hash_sha256_state state;
crypto_hash_sha256_init(&state);
crypto_hash_sha256_update(&state, salt_base, sizeof(salt_base));
crypto_hash_sha256_update(&state, (unsigned char*)username.c_str(), username.length());
crypto_hash_sha256_final(&state, salt_buffer);
// PBKDF2 to create the buffer
unsigned char encrypted[DIGESTSIZE];
pbkdf2(encrypted, (unsigned char*)password.c_str(), password.length(), salt_buffer, DIGESTSIZE, ITERATIONS);
return CStr(Hexify(encrypted, DIGESTSIZE)).UpperCase();
}
std::wstring JSI_Lobby::EncryptPassword(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& pass, const std::wstring& user)
{
return wstring_from_utf8(JSI_Lobby::EncryptPassword(utf8_from_wstring(pass), utf8_from_wstring(user)));
}
std::wstring JSI_Lobby::LobbyGetRoomSubject(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (!g_XmppClient)
return L"";
return g_XmppClient->GetSubject();
}
#endif