forked from 0ad/0ad
Rewrite lobby page to use class semantics, add more gamedetails labels, improve performance using batch processing and caching and gain possibility for game creation/player-join/leave events, refs #5387.
Game selection details features: * Display victory conditions following their sending but missing display followingbffe917914
, refs7b0f6f530c
. * Display the host of the match and the game name in the selected game details following61261d14fc
, refs D1666. * Display mods if the mods differ (without having to attempt to join the game prior) followingeca956a513
. Performance features: * Implement batch message processing in the XmppClient to rebuild GUI objects only once when receiving backlog or returning from a match. * Implement Game class to cache gamelist, filter and sorting values, as they rarely change but are accessed often. * Cache sprintf objects. Security fixes: * Add escapeText in lobby/ to avoid players breaking the lobby for every participant, supersedes D720, comments by bb. * Do not hide broadcasted unrecognized chat commands that mods used as leaking private channels, fixes #5615. Defect fixes: * Fix XmppClient.cpp storing unused historic message types resulting in memory waste and unintentional replay of for instance disconnect/announcements messages following both e8dfde9ba6/D819 and 6bf74902a7/D2265, refs #3306. * Fix XmppClient.cpp victoryCondition -> victoryConditions gamesetup.js change from 6d54ab4c1f/D1240. * Fix leaderboard/profile page cancel hotkey closing the lobby dialog as well and removes cancel hotkey note from lobby_panels.xml from 960f2d7c31/D817 since the described issue was fixed by f9b529f2fb/D1701. * Fix lobby playing menu sound in a running game after having closed the lobby dialog following introduction in 960f2d7c31/D817. * Fix GUI on nick change by updating g_Username. * Update profile panel only with data matching the player requested. Hack erasure: * Object semantics make it cheap to add state and cache values, storing literals in properties while removing globals, adding events while decoupling components and gaining moddability. * Erase comments and translation comments stating that this would be IRC!!, supersedes D1136. * Introduce Status chat message type to supersede "/special" chat command + "isSpecial" property frombffe917914
(formerly g_specialKeye6840f5fca
) deluxe hack. * Introduce System chat message type to supersede system errors disguising as chat from a mock user called "system". Code cleanups: * Move code from XML to JS. * Move size values from JS to XML, especially following 960f2d7c31/D817 and 7752219cef/D1051. * Rename "user" to "player". * Fix lobby/ eslint warnings, refs D2261. * Remove message.nick emptiness check from 0940db3fc0/D835, since XEP-0045 dictates that it is non-empty. * Add translated string for deleted subjects. * Add TODOs for some evident COList issues, refs #5638. Differential Revision: https://code.wildfiregames.com/D2412 This was SVN commit r23172.
This commit is contained in:
parent
d9e34fc88e
commit
2cccd9825d
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="leaderboardPage" hidden="true">
|
||||
|
||||
<!-- Translucent black background -->
|
||||
<object type="image" z="100" sprite="ModernFade"/>
|
||||
|
||||
<object type="image" style="ModernDialog" size="50%-224 50%-160 50%+224 50%+160" z="101">
|
||||
|
||||
<object style="ModernLabelText" type="text" size="50%-128 -18 50%+128 14">
|
||||
<translatableAttribute id="caption">Leaderboard</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<object name="leaderboardBox" style="ModernList" type="olist" size="19 19 100%-19 100%-62">
|
||||
<column id="rank" color="255 255 255" width="15%">
|
||||
<translatableAttribute id="heading">Rank</translatableAttribute>
|
||||
</column>
|
||||
<column id="name" color="255 255 255" width="55%">
|
||||
<translatableAttribute id="heading">Name</translatableAttribute>
|
||||
</column>
|
||||
<column id="rating" color="255 255 255" width="30%">
|
||||
<translatableAttribute id="heading">Rating</translatableAttribute>
|
||||
</column>
|
||||
</object>
|
||||
|
||||
<object type="button" name="leaderboardPageBack" style="ModernButtonRed" size="50%-133 100%-45 50%-5 100%-17">
|
||||
<translatableAttribute id="caption">Back</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<object type="button" name="leaderboardUpdateButton" style="ModernButtonRed" size="50%+5 100%-45 50%+133 100%-17">
|
||||
<translatableAttribute id="caption">Update</translatableAttribute>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
25
binaries/data/mods/public/gui/lobby/Lobby.js
Normal file
25
binaries/data/mods/public/gui/lobby/Lobby.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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");
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
@ -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");
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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"
|
||||
};
|
@ -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"
|
||||
};
|
@ -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"
|
||||
};
|
@ -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 = "";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="chatPanel" type="image" sprite="ModernDarkBoxGold">
|
||||
<object name="chatText" size="0 0 100% 100%-28" type="text" style="ChatPanel" font="sans-13"/>
|
||||
<object name="chatInput" size="0 100%-26 100%-72 100%-4" type="input" style="ModernInput" font="sans-13"/>
|
||||
<object name="chatSubmit" size="100%-72 100%-26 100%-4 100%-4" type="button" style="StoneButton">
|
||||
<translatableAttribute id="caption">Send</translatableAttribute>
|
||||
</object>
|
||||
</object>
|
@ -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.");
|
@ -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"
|
||||
};
|
@ -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.");
|
@ -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"
|
||||
};
|
@ -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");
|
@ -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();
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
338
binaries/data/mods/public/gui/lobby/LobbyPage/Game.js
Normal file
338
binaries/data/mods/public/gui/lobby/LobbyPage/Game.js
Normal file
@ -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"
|
||||
}
|
||||
};
|
141
binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js
Normal file
141
binaries/data/mods/public/gui/lobby/LobbyPage/GameDetails.js
Normal file
@ -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"
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object type="image" sprite="ModernDarkBoxGold">
|
||||
|
||||
<!-- Map Name -->
|
||||
<object name="sgMapName" size="0 5 100% 20" type="text" style="ModernLabelText"/>
|
||||
|
||||
<!-- Map Preview -->
|
||||
<object name="sgMapPreview" size="5 25 100%-5 185" type="image"/>
|
||||
|
||||
<!-- Map Description -->
|
||||
<object type="image" sprite="ModernDarkBoxWhite" size="5 190 100%-5 60%">
|
||||
<object name="sgMapDescription" type="text" style="ModernText" font="sans-14"/>
|
||||
</object>
|
||||
|
||||
<object type="image" sprite="ModernDarkBoxWhite" size="5 60%+5 100%-5 100%-1">
|
||||
|
||||
<!-- Number of players, hostname, gamestart time -->
|
||||
<object name="sgGame" size="5 5 100%-5 5" type="text" style="ModernLabelText"/>
|
||||
|
||||
<!-- Player Names -->
|
||||
<object name="sgPlayersNames" type="text" style="MapPlayerList"/>
|
||||
|
||||
</object>
|
||||
|
||||
</object>
|
221
binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js
Normal file
221
binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
29
binaries/data/mods/public/gui/lobby/LobbyPage/GameList.xml
Normal file
29
binaries/data/mods/public/gui/lobby/LobbyPage/GameList.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="gamesBox"
|
||||
style="ModernSortedList"
|
||||
selected_column="gameName"
|
||||
selected_column_order="1"
|
||||
type="olist"
|
||||
sortable="true"
|
||||
font="sans-stroke-13"
|
||||
>
|
||||
<column id="buddy" width="12"/>
|
||||
<column id="gameName" color="128 128 128" width="33%-12">
|
||||
<translatableAttribute id="heading">Name</translatableAttribute>
|
||||
</column>
|
||||
<column id="mapName" color="128 128 128" width="25%">
|
||||
<translatableAttribute id="heading">Map Name</translatableAttribute>
|
||||
</column>
|
||||
<column id="mapSize" color="128 128 128" width="16%">
|
||||
<translatableAttribute id="heading" context="map">Size</translatableAttribute>
|
||||
</column>
|
||||
<column id="mapType" color="0 160 160" width="16%">
|
||||
<translatableAttribute id="heading" context="map">Type</translatableAttribute>
|
||||
</column>
|
||||
<column id="maxnbp" color="0 128 128" width="10%">
|
||||
<translatableAttribute id="heading">Players</translatableAttribute>
|
||||
</column>
|
||||
<column id="gameRating" color="0 160 160" width="16%">
|
||||
<translatableAttribute id="heading">Rating</translatableAttribute>
|
||||
</column>
|
||||
</object>
|
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="filterPanel">
|
||||
|
||||
<object type="text" size="22 0 52%-10 100%" text_align="left" textcolor="white">
|
||||
<translatableAttribute id="caption">Show only open games</translatableAttribute>
|
||||
</object>
|
||||
<object name="filterOpenGames" type="checkbox" checked="false" style="ModernTickBox" size="0 0 20 20"/>
|
||||
|
||||
<object name="mapSizeFilter"
|
||||
type="dropdown"
|
||||
style="ModernDropDown"
|
||||
size="58%-5 0 74%-10 100%"
|
||||
font="sans-bold-13">
|
||||
</object>
|
||||
|
||||
<object name="mapTypeFilter"
|
||||
type="dropdown"
|
||||
style="ModernDropDown"
|
||||
size="74%-5 0 90%-10 100%"
|
||||
font="sans-bold-13">
|
||||
</object>
|
||||
|
||||
<object name="playersNumberFilter"
|
||||
type="dropdown"
|
||||
style="ModernDropDown"
|
||||
size="90%-5 0 100%-10 100%"
|
||||
font="sans-bold-13">
|
||||
</object>
|
||||
|
||||
<object name="gameRatingFilter"
|
||||
type="dropdown"
|
||||
style="ModernDropDown"
|
||||
size="84%-5 0 100%-10 100%"
|
||||
font="sans-bold-13">
|
||||
</object>
|
||||
|
||||
</object>
|
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
};
|
@ -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"
|
||||
];
|
49
binaries/data/mods/public/gui/lobby/LobbyPage/KickStrings.js
Normal file
49
binaries/data/mods/public/gui/lobby/LobbyPage/KickStrings.js
Normal file
@ -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");
|
73
binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.js
Normal file
73
binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.js
Normal file
@ -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;
|
55
binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.xml
Normal file
55
binaries/data/mods/public/gui/lobby/LobbyPage/LobbyPage.xml
Normal file
@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object>
|
||||
|
||||
<!-- Translucent black background -->
|
||||
<object type="image" sprite="ModernFade"/>
|
||||
|
||||
<object name="lobbyPage" type="image" sprite="ModernWindow">
|
||||
|
||||
<object name="lobbyPageTitle" type="text" style="ModernLabelText" size="50%-128 4 50%+128 36">
|
||||
<translatableAttribute id="caption">Multiplayer Lobby</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<object name="lobbyPanels" size="0 40 100% 100%-20">
|
||||
|
||||
<object name="leftPanel" size="20 0 20% 100%+20">
|
||||
<object size="0 0 100% 100%-315">
|
||||
<include file="gui/lobby/LobbyPage/PlayerList.xml"/>
|
||||
</object>
|
||||
<object size="0 100%-310 100% 100%-104">
|
||||
<include file="gui/lobby/LobbyPage/ProfilePanel.xml"/>
|
||||
</object>
|
||||
<object size="0 100%-98 100% 100%">
|
||||
<object name="toggleBuddyButton" type="button" style="ModernButtonRed" size="0 0 100% 25"/>
|
||||
<object name="leaderboardButton" type="button" style="ModernButtonRed" size="0 27 100% 52"/>
|
||||
<object name="profileButton" type="button" style="ModernButtonRed" size="0 54 100% 79"/>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object name="middlePanel" size="20%+5 0 100%-255 100%">
|
||||
<object size="0 0 100% 24">
|
||||
<include file="gui/lobby/LobbyPage/GameListFilters.xml"/>
|
||||
</object>
|
||||
<object size="0 25 100% 48%">
|
||||
<include file="gui/lobby/LobbyPage/GameList.xml"/>
|
||||
</object>
|
||||
<object size="0 49% 100% 100%">
|
||||
<include file="gui/lobby/LobbyPage/Chat/ChatPanel.xml"/>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object name="rightPanel" size="100%-250 0 100%-20 100%">
|
||||
<object name="gameDetails" hidden="true">
|
||||
<include file="gui/lobby/LobbyPage/GameDetails.xml"/>
|
||||
</object>
|
||||
<object name="subjectPanel">
|
||||
<include file="gui/lobby/LobbyPage/Subject.xml"/>
|
||||
</object>
|
||||
<object name="joinButton" size="0 100%-79 100% 100%-54" type="button" style="ModernButtonRed"/>
|
||||
<object name="hostButton" size="0 100%-52 100% 100%-27" type="button" style="ModernButtonRed"/>
|
||||
<object name="leaveButton" size="0 100%-25 100% 100%" type="button" style="ModernButtonRed"/>
|
||||
</object>
|
||||
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
53
binaries/data/mods/public/gui/lobby/LobbyPage/PlayerColor.js
Normal file
53
binaries/data/mods/public/gui/lobby/LobbyPage/PlayerColor.js
Normal file
@ -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;
|
207
binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.js
Normal file
207
binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.js
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
};
|
20
binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.xml
Normal file
20
binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="playersBox"
|
||||
style="ModernSortedList"
|
||||
selected_column="name"
|
||||
selected_column_order="1"
|
||||
type="olist"
|
||||
sortable="true"
|
||||
font="sans-bold-stroke-13"
|
||||
>
|
||||
<column id="buddy" width="12"/>
|
||||
<column id="status" width="26%">
|
||||
<translatableAttribute id="heading">Status</translatableAttribute>
|
||||
</column>
|
||||
<column id="name" width="48%-12">
|
||||
<translatableAttribute id="heading">Name</translatableAttribute>
|
||||
</column>
|
||||
<column id="rating" width="26%">
|
||||
<translatableAttribute id="heading">Rating</translatableAttribute>
|
||||
</column>
|
||||
</object>
|
125
binaries/data/mods/public/gui/lobby/LobbyPage/ProfilePanel.js
Normal file
125
binaries/data/mods/public/gui/lobby/LobbyPage/ProfilePanel.js
Normal file
@ -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)
|
||||
});
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This is the display of the player selected in the playerlist, not the profile page. -->
|
||||
<object name="profileBox" type="image" sprite="ModernDarkBoxGold">
|
||||
<object name="profileArea" hidden="true">
|
||||
<object name="playernameText" size="0 0 100% 45" type="text" style="ModernLabelText" font="sans-bold-16" />
|
||||
<object name="roleText" size="0 45 100% 70" type="text" style="ModernLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 70 40%+40 90" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Current Rank:</translatableAttribute>
|
||||
</object>
|
||||
<object name="rankText" size="40%+45 70 100% 90" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 90 40%+40 110" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Highest Rating:</translatableAttribute>
|
||||
</object>
|
||||
<object name="highestRatingText" size="40%+45 90 100% 110" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 110 40%+40 130" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Total Games:</translatableAttribute>
|
||||
</object>
|
||||
<object name="totalGamesText" size="40%+45 110 100% 130" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 130 40%+40 150" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Wins:</translatableAttribute>
|
||||
</object>
|
||||
<object name="winsText" size="40%+45 130 100% 150" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 150 40%+40 170" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Losses:</translatableAttribute>
|
||||
</object>
|
||||
<object name="lossesText" size="40%+45 150 100% 170" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 170 40%+40 190" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Win Rate:</translatableAttribute>
|
||||
</object>
|
||||
<object name="ratioText" size="40%+45 170 100% 190" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
</object>
|
||||
</object>
|
45
binaries/data/mods/public/gui/lobby/LobbyPage/Subject.js
Normal file
45
binaries/data/mods/public/gui/lobby/LobbyPage/Subject.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
11
binaries/data/mods/public/gui/lobby/LobbyPage/Subject.xml
Normal file
11
binaries/data/mods/public/gui/lobby/LobbyPage/Subject.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object type="image" sprite="ModernDarkBoxGold">
|
||||
|
||||
<object name="logoTop" size="50%-110 40 50%+110 140" type="image" sprite="logo"/>
|
||||
<object name="logoCenter" size="50%-110 50%-50 50%+110 50%+50" type="image" sprite="logo"/>
|
||||
|
||||
<object name="subjectBox" size="5 180 100%-5 100%-10" type="image" sprite="ModernDarkBoxWhite">
|
||||
<object name="subjectText" size="5 5 100%-5 100%-10" type="text" style="ModernText" text_align="center"/>
|
||||
</object>
|
||||
|
||||
</object>
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="profilePage" hidden="true">
|
||||
|
||||
<!-- Translucent black background -->
|
||||
<object type="image" z="100" sprite="ModernFade"/>
|
||||
|
||||
<object type="image" style="ModernDialog" size="50%-224 50%-160 50%+224 50%+160" z="102">
|
||||
|
||||
<object style="ModernLabelText" type="text" size="50%-128 -18 50%+128 14">
|
||||
<translatableAttribute id="caption">Player Profile Lookup</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<object type="text" size="15 25 40% 50" text_align="right" textcolor="white">
|
||||
<translatableAttribute id="caption">Enter playername:</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<object name="fetchInput" size="40%+10 25 100%-25 50" type="input" style="ModernInput" font="sans-13"/>
|
||||
|
||||
<object name="viewProfileButton" type="button" style="ModernButtonRed" size="50%-64 60 50%+64 85">
|
||||
<translatableAttribute id="caption">View Profile</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<object name="profileWindowPanel" size="25 95 100%-25 100%-60">
|
||||
<object name="profileWindowBox" type="image" sprite="ModernDarkBoxGold">
|
||||
<object name="profileWindowArea" hidden="true">
|
||||
<object name="profilePlayernameText" size="0 0 100% 25" type="text" style="ModernLabelText" font="sans-bold-16" />
|
||||
<object size="0 30 40%+40 50" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Current Rank:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileRankText" size="40%+45 30 100% 50" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 50 40%+40 70" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Highest Rating:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileHighestRatingText" size="40%+45 50 100% 70" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 70 40%+40 90" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Total Games:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileTotalGamesText" size="40%+45 70 100% 90" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 90 40%+40 110" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Wins:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileWinsText" size="40%+45 90 100% 110" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 110 40%+40 130" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Losses:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileLossesText" size="40%+45 110 100% 130" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 130 40%+40 150" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Win Rate:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileRatioText" size="40%+45 130 100% 150" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
</object>
|
||||
<object name="profileErrorText" size="25% 25% 75% 75%" type="text" style="ModernLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Please enter a player name.</translatableAttribute>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object type="button" name="profileBackButton" style="ModernButtonRed" size="50%-64 100%-50 50%+64 100%-25" hotkey="cancel">
|
||||
<translatableAttribute id="caption">Back</translatableAttribute>
|
||||
</object>
|
||||
|
||||
</object>
|
||||
</object>
|
115
binaries/data/mods/public/gui/lobby/XmppMessages.js
Normal file
115
binaries/data/mods/public/gui/lobby/XmppMessages.js
Normal file
@ -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"
|
||||
]
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -4,17 +4,21 @@
|
||||
|
||||
<script directory="gui/common/"/>
|
||||
<script directory="gui/lobby/"/>
|
||||
<script directory="gui/lobby/LeaderboardPage/"/>
|
||||
<script directory="gui/lobby/LobbyPage/"/>
|
||||
<script directory="gui/lobby/LobbyPage/Buttons/"/>
|
||||
<script directory="gui/lobby/LobbyPage/Chat/"/>
|
||||
<script directory="gui/lobby/LobbyPage/Chat/ChatMessages/"/>
|
||||
<script directory="gui/lobby/LobbyPage/Chat/StatusMessages/"/>
|
||||
<script directory="gui/lobby/LobbyPage/Chat/SystemMessages/"/>
|
||||
<script directory="gui/lobby/LobbyPage/GameListFilters/"/>
|
||||
<script directory="gui/lobby/ProfilePage/"/>
|
||||
|
||||
<object type="image" sprite="ModernFade" name="fadeImage"/>
|
||||
<object>
|
||||
|
||||
<object type="image" style="ModernWindow" name="lobbyWindow">
|
||||
|
||||
<object name="lobbyWindowTitle" style="ModernLabelText" type="text" size="50%-128 0%+4 50%+128 36">
|
||||
<translatableAttribute id="caption">Multiplayer Lobby</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<include file="gui/lobby/lobby_panels.xml"/>
|
||||
<include file="gui/lobby/LobbyPage/LobbyPage.xml"/>
|
||||
<include file="gui/lobby/LeaderboardPage/LeaderboardPage.xml"/>
|
||||
<include file="gui/lobby/ProfilePage/ProfilePage.xml"/>
|
||||
|
||||
</object>
|
||||
|
||||
</objects>
|
||||
|
@ -1,366 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<object>
|
||||
|
||||
<action on="Tick">
|
||||
onTick();
|
||||
</action>
|
||||
|
||||
<object hotkey="lobby" name="lobbyDialogToggle"/>
|
||||
|
||||
<!-- Left panel: Player list. -->
|
||||
<object name="leftPanel">
|
||||
<object name="playersBox"
|
||||
style="ModernSortedList"
|
||||
selected_column="name"
|
||||
selected_column_order="1"
|
||||
type="olist"
|
||||
sortable="true"
|
||||
font="sans-bold-stroke-13"
|
||||
>
|
||||
<column id="buddy" width="12"/>
|
||||
<column id="status" width="26%">
|
||||
<translatableAttribute id="heading">Status</translatableAttribute>
|
||||
</column>
|
||||
<column id="name" width="48%-12">
|
||||
<translatableAttribute id="heading">Name</translatableAttribute>
|
||||
</column>
|
||||
<column id="rating" width="26%">
|
||||
<translatableAttribute id="heading">Rating</translatableAttribute>
|
||||
</column>
|
||||
<action on="SelectionChange">
|
||||
onPlayerListSelection();
|
||||
</action>
|
||||
<action on="SelectionColumnChange">
|
||||
updatePlayerList();
|
||||
</action>
|
||||
<action on="mouseleftclickitem">
|
||||
// In case of clicking on the same player again
|
||||
selectGameFromPlayername();
|
||||
</action>
|
||||
<action on="mouseleftdoubleclickitem">toggleBuddy();</action>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object name="profilePanel" size="20 100%-310 20% 100%-104">
|
||||
<object name="profileBox" type="image" sprite="ModernDarkBoxGold">
|
||||
<object name="profileArea" hidden="true">
|
||||
<object name="usernameText" size="0 0 100% 45" type="text" style="ModernLabelText" font="sans-bold-16" />
|
||||
<object name="roleText" size="0 45 100% 70" type="text" style="ModernLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 70 40%+40 90" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Current Rank:</translatableAttribute>
|
||||
</object>
|
||||
<object name="rankText" size="40%+45 70 100% 90" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 90 40%+40 110" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Highest Rating:</translatableAttribute>
|
||||
</object>
|
||||
<object name="highestRatingText" size="40%+45 90 100% 110" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 110 40%+40 130" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Total Games:</translatableAttribute>
|
||||
</object>
|
||||
<object name="totalGamesText" size="40%+45 110 100% 130" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 130 40%+40 150" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Wins:</translatableAttribute>
|
||||
</object>
|
||||
<object name="winsText" size="40%+45 130 100% 150" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 150 40%+40 170" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Losses:</translatableAttribute>
|
||||
</object>
|
||||
<object name="lossesText" size="40%+45 150 100% 170" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 170 40%+40 190" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Win Rate:</translatableAttribute>
|
||||
</object>
|
||||
<object name="ratioText" size="40%+45 170 100% 190" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object name="leftButtonPanel" size="20 100%-105 20% 100%-20">
|
||||
<object name="toggleBuddyButton" type="button" style="ModernButtonRed" size="0 100%-79 100% 100%-54">
|
||||
<action on="Press">
|
||||
toggleBuddy();
|
||||
</action>
|
||||
</object>
|
||||
<object name="leaderboardButton" type="button" style="ModernButtonRed" size="0 100%-52 100% 100%-27">
|
||||
<translatableAttribute id="caption">Leaderboard</translatableAttribute>
|
||||
<action on="Press">
|
||||
setLeaderboardVisibility(true);
|
||||
</action>
|
||||
</object>
|
||||
<object name="userprofileButton" type="button" style="ModernButtonRed" size="0 100%-25 100% 100%">
|
||||
<translatableAttribute id="caption">User Profile Lookup</translatableAttribute>
|
||||
<action on="Press">
|
||||
setUserProfileVisibility(true);
|
||||
</action>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<!-- Right panel: Game details. -->
|
||||
<object name="rightPanel">
|
||||
<object name="gameInfoEmpty" type="image" sprite="ModernDarkBoxGold">
|
||||
<object name="logo" size="50%-110 40 50%+110 140" type="image" sprite="logo"/>
|
||||
<object name="subjectBox" type="image" sprite="ModernDarkBoxWhite" size="3% 180 97% 99%">
|
||||
<object name="subject" size="5 5 100%-5 100%-5" type="text" style="ModernText" text_align="center"/>
|
||||
</object>
|
||||
</object>
|
||||
<object name="gameInfo" type="image" sprite="ModernDarkBoxGold" hidden="true">
|
||||
|
||||
<!-- Map Name -->
|
||||
<object name="sgMapName" size="0 5 100% 20" type="text" style="ModernLabelText"/>
|
||||
|
||||
<!-- Map Preview -->
|
||||
<object name="sgMapPreview" size="5 25 100%-5 190" type="image" sprite=""/>
|
||||
|
||||
<object size="5 194 100%-5 195" type="image" sprite="ModernWhiteLine" z="25"/>
|
||||
|
||||
<!-- Map Type -->
|
||||
<object size="5 195 50% 240" type="image" sprite="ModernItemBackShadeLeft">
|
||||
<object size="0 0 100%-10 100%" type="text" style="ModernRightLabelText">
|
||||
<translatableAttribute id="caption">Map Type:</translatableAttribute>
|
||||
</object>
|
||||
</object>
|
||||
<object size="50% 195 100%-5 240" type="image" sprite="ModernItemBackShadeRight">
|
||||
<object name="sgMapType" type="text" style="ModernLeftLabelText"/>
|
||||
</object>
|
||||
|
||||
<object size="5 239 100%-5 240" type="image" sprite="ModernWhiteLine" z="25"/>
|
||||
|
||||
<!-- Map Size -->
|
||||
<object size="5 240 50% 285" type="image" sprite="ModernItemBackShadeLeft">
|
||||
<object size="0 0 100%-10 100%" type="text" style="ModernRightLabelText">
|
||||
<translatableAttribute id="caption">Map Size:</translatableAttribute>
|
||||
</object>
|
||||
</object>
|
||||
<object size="50% 240 100%-5 285" type="image" sprite="ModernItemBackShadeRight">
|
||||
<object name="sgMapSize" type="text" style="ModernLeftLabelText"/>
|
||||
</object>
|
||||
|
||||
<object size="5 284 100%-5 285" type="image" sprite="ModernWhiteLine" z="25"/>
|
||||
|
||||
<!-- Map Description -->
|
||||
<object type="image" sprite="ModernDarkBoxWhite" size="3% 290 97% 65%">
|
||||
<object name="sgMapDescription" type="text" style="ModernText" font="sans-12"/>
|
||||
</object>
|
||||
|
||||
<object type="image" sprite="ModernDarkBoxWhite" size="3% 65%+5 97% 99%">
|
||||
<!-- Number of Players -->
|
||||
<object name="sgNbPlayers" size="3% 3% 97% 3%+15" type="text" style="ModernLabelText"/>
|
||||
<!-- Game Start Time -->
|
||||
<object name="sgGameStartTime" size="3%+24 6% 97% 3%+35" type="text" style="ModernLabelText"/>
|
||||
|
||||
<!-- Player Names -->
|
||||
<object name="sgPlayersNames" type="text" style="MapPlayerList"/>
|
||||
</object>
|
||||
</object>
|
||||
<object name="joinGameButton" type="button" style="ModernButtonRed" size="0 100%-79 100% 100%-54" hidden="true">
|
||||
<translatableAttribute id="caption" comment="Join the game currently selected in the list.">Join Game</translatableAttribute>
|
||||
<action on="Press">
|
||||
joinButton();
|
||||
</action>
|
||||
</object>
|
||||
<object name="hostButton" type="button" style="ModernButtonRed" size="0 100%-52 100% 100%-27">
|
||||
<translatableAttribute id="caption">Host Game</translatableAttribute>
|
||||
<action on="Press">
|
||||
hostGame();
|
||||
</action>
|
||||
</object>
|
||||
|
||||
<object name="leaveButton" type="button" style="ModernButtonRed" size="0 100%-25 100% 100%">
|
||||
<action on="Press">leaveLobby();</action>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<!-- Middle panel: Filters, game list, chat box. -->
|
||||
<object name="middlePanel">
|
||||
<object name="gamesBox"
|
||||
style="ModernSortedList"
|
||||
selected_column="name"
|
||||
selected_column_order="1"
|
||||
type="olist"
|
||||
sortable="true"
|
||||
size="0 25 100% 48%"
|
||||
font="sans-stroke-13"
|
||||
>
|
||||
<action on="SelectionChange">updateGameSelection();</action>
|
||||
<action on="SelectionColumnChange">applyFilters();</action>
|
||||
<action on="mouseleftdoubleclickitem">joinButton();</action>
|
||||
<column id="buddy" width="12"/>
|
||||
<column id="name" color="0 60 0" width="33%-12">
|
||||
<translatableAttribute id="heading">Name</translatableAttribute>
|
||||
</column>
|
||||
<column id="mapName" color="128 128 128" width="25%">
|
||||
<translatableAttribute id="heading">Map Name</translatableAttribute>
|
||||
</column>
|
||||
<column id="mapSize" color="128 128 128" width="16%">
|
||||
<translatableAttribute id="heading" context="map">Size</translatableAttribute>
|
||||
</column>
|
||||
<column id="mapType" color="0 160 160" width="16%">
|
||||
<translatableAttribute id="heading" context="map">Type</translatableAttribute>
|
||||
</column>
|
||||
<column id="nPlayers" color="0 128 128" width="10%">
|
||||
<translatableAttribute id="heading">Players</translatableAttribute>
|
||||
</column>
|
||||
<column id="gameRating" color="0 160 160" width="16%">
|
||||
<translatableAttribute id="heading">Rating</translatableAttribute>
|
||||
</column>
|
||||
</object>
|
||||
|
||||
<object name="filterPanel" size="0 0 100% 24">
|
||||
|
||||
<object name="mapSizeFilter"
|
||||
type="dropdown"
|
||||
style="ModernDropDown"
|
||||
size="58%-5 0 74%-10 100%"
|
||||
font="sans-bold-13">
|
||||
<action on="SelectionChange">applyFilters();</action>
|
||||
</object>
|
||||
|
||||
<object name="mapTypeFilter"
|
||||
type="dropdown"
|
||||
style="ModernDropDown"
|
||||
size="74%-5 0 90%-10 100%"
|
||||
font="sans-bold-13">
|
||||
<action on="SelectionChange">applyFilters();</action>
|
||||
</object>
|
||||
|
||||
<object name="playersNumberFilter"
|
||||
type="dropdown"
|
||||
style="ModernDropDown"
|
||||
size="90%-5 0 100%-10 100%"
|
||||
font="sans-bold-13">
|
||||
<action on="SelectionChange">applyFilters();</action>
|
||||
</object>
|
||||
|
||||
<object name="gameRatingFilter"
|
||||
type="dropdown"
|
||||
style="ModernDropDown"
|
||||
size="84%-5 0 100%-10 100%"
|
||||
font="sans-bold-13">
|
||||
<action on="SelectionChange">applyFilters();</action>
|
||||
</object>
|
||||
|
||||
<object type="text" size="22 0 52%-10 100%" text_align="left" textcolor="white">
|
||||
<translatableAttribute id="caption">Show only open games</translatableAttribute>
|
||||
</object>
|
||||
<object name="filterOpenGames" type="checkbox" checked="false" style="ModernTickBox" size="0 0 20 20">
|
||||
<action on="Press">
|
||||
applyFilters();
|
||||
</action>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object name="chatPanel" size="0 49% 100% 100%" type="image" sprite="ModernDarkBoxGold">
|
||||
<object name="chatText" size="0 0 100% 100%-28" type="text" style="ChatPanel" font="sans-13"/>
|
||||
<object name="chatInput" size="0 100%-26 100%-72 100%-4" type="input" style="ModernInput" font="sans-13">
|
||||
<action on="Press">submitChatInput();</action>
|
||||
<action on="Tab">
|
||||
autoCompleteText(this, Engine.GetPlayerList().map(player => player.name));
|
||||
</action>
|
||||
</object>
|
||||
<object size="100%-72 100%-26 100%-4 100%-4" type="button" style="StoneButton">
|
||||
<translatableAttribute id="caption">Send</translatableAttribute>
|
||||
<action on="Press">submitChatInput();</action>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<!-- Translucent black background -->
|
||||
<object hidden="true" name="fade" type="image" z="100" sprite="ModernFade"/>
|
||||
|
||||
<!-- Leaderboard -->
|
||||
<object hidden="true" name="leaderboard" type="image" style="ModernDialog" size="50%-224 50%-160 50%+224 50%+160" z="101">
|
||||
<object style="ModernLabelText" type="text" size="50%-128 -18 50%+128 14">
|
||||
<translatableAttribute id="caption">Leaderboard</translatableAttribute>
|
||||
</object>
|
||||
<object name="leaderboardBox"
|
||||
style="ModernList"
|
||||
type="olist"
|
||||
size="19 19 100%-19 100%-62">
|
||||
<column id="rank" color="255 255 255" width="15%">
|
||||
<translatableAttribute id="heading">Rank</translatableAttribute>
|
||||
</column>
|
||||
<column id="name" color="255 255 255" width="55%">
|
||||
<translatableAttribute id="heading">Name</translatableAttribute>
|
||||
</column>
|
||||
<column id="rating" color="255 255 255" width="30%">
|
||||
<translatableAttribute id="heading">Rating</translatableAttribute>
|
||||
</column>
|
||||
<action on="SelectionChange">
|
||||
lookupSelectedUserProfile(this.name);
|
||||
</action>
|
||||
</object>
|
||||
<object type="button" style="ModernButtonRed" size="50%-133 100%-45 50%-5 100%-17" hotkey="cancel">
|
||||
<translatableAttribute id="caption">Back</translatableAttribute>
|
||||
<action on="Press">
|
||||
setLeaderboardVisibility(false);
|
||||
</action>
|
||||
</object>
|
||||
<object type="button" style="ModernButtonRed" size="50%+5 100%-45 50%+133 100%-17">
|
||||
<translatableAttribute id="caption">Update</translatableAttribute>
|
||||
<action on="Press">Engine.SendGetBoardList();</action>
|
||||
</object>
|
||||
</object>
|
||||
<!-- End of leaderboard -->
|
||||
|
||||
<!-- User profile lookup -->
|
||||
<object hidden="true" name="profileFetch" type="image" style="ModernDialog" size="50%-224 50%-160 50%+224 50%+160" z="102">
|
||||
<object style="ModernLabelText" type="text" size="50%-128 -18 50%+128 14">
|
||||
<translatableAttribute id="caption">User Profile Lookup</translatableAttribute>
|
||||
</object>
|
||||
<object type="text" size="15 25 40% 50" text_align="right" textcolor="white">
|
||||
<translatableAttribute id="caption">Enter username:</translatableAttribute>
|
||||
</object>
|
||||
<object name="fetchInput" size="40%+10 25 100%-25 50" type="input" style="ModernInput" font="sans-13">
|
||||
<action on="Press">lookupUserProfile();</action>
|
||||
</object>
|
||||
<object type="button" style="ModernButtonRed" size="50%-64 60 50%+64 85">
|
||||
<translatableAttribute id="caption">View Profile</translatableAttribute>
|
||||
<action on="Press">lookupUserProfile();</action>
|
||||
</object>
|
||||
<object name="profileWindowPanel" size="25 95 100%-25 100%-60">
|
||||
<object name="profileWindowBox" type="image" sprite="ModernDarkBoxGold">
|
||||
<object name="profileWindowArea" hidden="true">
|
||||
<object name="profileUsernameText" size="0 0 100% 25" type="text" style="ModernLabelText" font="sans-bold-16" />
|
||||
<object size="0 30 40%+40 50" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Current Rank:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileRankText" size="40%+45 30 100% 50" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 50 40%+40 70" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Highest Rating:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileHighestRatingText" size="40%+45 50 100% 70" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 70 40%+40 90" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Total Games:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileTotalGamesText" size="40%+45 70 100% 90" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 90 40%+40 110" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Wins:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileWinsText" size="40%+45 90 100% 110" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 110 40%+40 130" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Losses:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileLossesText" size="40%+45 110 100% 130" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
<object size="0 130 40%+40 150" type="text" style="ModernRightLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Win Rate:</translatableAttribute>
|
||||
</object>
|
||||
<object name="profileRatioText" size="40%+45 130 100% 150" type="text" style="ModernLeftLabelText" font="sans-bold-stroke-12" />
|
||||
</object>
|
||||
<object name="profileErrorText" size="25% 25% 75% 75%" type="text" style="ModernLabelText" font="sans-bold-stroke-13">
|
||||
<translatableAttribute id="caption">Please enter a player name.</translatableAttribute>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object type="button" style="ModernButtonRed" size="50%-64 100%-50 50%+64 100%-25" hotkey="cancel">
|
||||
<translatableAttribute id="caption">Back</translatableAttribute>
|
||||
<action on="Press">
|
||||
setUserProfileVisibility(false);
|
||||
</action>
|
||||
</object>
|
||||
</object>
|
||||
<!-- End of user profile lookup -->
|
||||
|
||||
<!-- Keep this hotkey at the end of the gui, because the other cancel hotkeys must be processed first! -->
|
||||
<object hotkey="cancel" name="cancelDialog"/>
|
||||
|
||||
</object>
|
@ -4,9 +4,11 @@
|
||||
<include>common/modern/styles.xml</include>
|
||||
<include>common/modern/sprites.xml</include>
|
||||
|
||||
<include>common/global.xml</include>
|
||||
<include>common/styles.xml</include>
|
||||
<include>common/sprites.xml</include>
|
||||
|
||||
<include>lobby/lobby.xml</include>
|
||||
|
||||
<include>common/global.xml</include>
|
||||
|
||||
</page>
|
||||
|
@ -10,19 +10,20 @@ var g_LobbyMessages = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
@ -49,13 +49,14 @@ public:
|
||||
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;
|
||||
|
||||
|
@ -573,7 +573,7 @@ void XmppClient::GUIGetGameList(const ScriptInterface& scriptInterface, JS::Muta
|
||||
|
||||
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)
|
||||
{
|
||||
@ -687,48 +687,79 @@ void XmppClient::CreateGUIMessage(
|
||||
|
||||
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);
|
||||
|
||||
JS::RootedValue messageCopy(cx);
|
||||
if (JS_StructuredClone(cx, message, &messageCopy, nullptr, nullptr))
|
||||
int j = 0;
|
||||
|
||||
for (const JS::Heap<JS::Value>& message : m_GuiMessageQueue)
|
||||
{
|
||||
scriptInterface.SetProperty(messageCopy, "historic", true);
|
||||
scriptInterface.FreezeObject(messageCopy, true);
|
||||
m_HistoricGuiMessages.push_back(JS::Heap<JS::Value>(messageCopy));
|
||||
}
|
||||
else
|
||||
LOGERROR("Could not clone historic lobby GUI message!");
|
||||
scriptInterface.SetPropertyInt(messages, j++, message);
|
||||
|
||||
return 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<JS::Value>(historicMessage));
|
||||
}
|
||||
else
|
||||
LOGERROR("Could not clone historic lobby GUI 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<JS::Value>& message : m_HistoricGuiMessages)
|
||||
scriptInterface.SetPropertyInt(ret, j++, message);
|
||||
scriptInterface.SetPropertyInt(messages, j++, message);
|
||||
|
||||
return ret;
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1087,6 +1118,20 @@ const char* XmppClient::GetRole(const std::string& nick)
|
||||
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 *
|
||||
*****************************************************/
|
||||
|
@ -90,6 +90,7 @@ public:
|
||||
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);
|
||||
@ -150,7 +151,7 @@ protected:
|
||||
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);
|
||||
|
@ -56,7 +56,7 @@ void JSI_Lobby::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
|
||||
scriptInterface.RegisterFunction<JS::Value, &JSI_Lobby::GetGameList>("GetGameList");
|
||||
scriptInterface.RegisterFunction<JS::Value, &JSI_Lobby::GetBoardList>("GetBoardList");
|
||||
scriptInterface.RegisterFunction<JS::Value, &JSI_Lobby::GetProfile>("GetProfile");
|
||||
scriptInterface.RegisterFunction<JS::Value, &JSI_Lobby::LobbyGuiPollNewMessage>("LobbyGuiPollNewMessage");
|
||||
scriptInterface.RegisterFunction<JS::Value, &JSI_Lobby::LobbyGuiPollNewMessages>("LobbyGuiPollNewMessages");
|
||||
scriptInterface.RegisterFunction<JS::Value, &JSI_Lobby::LobbyGuiPollHistoricMessages>("LobbyGuiPollHistoricMessages");
|
||||
scriptInterface.RegisterFunction<bool, &JSI_Lobby::LobbyGuiPollHasPlayerListUpdate>("LobbyGuiPollHasPlayerListUpdate");
|
||||
scriptInterface.RegisterFunction<void, std::wstring, &JSI_Lobby::LobbySendMessage>("LobbySendMessage");
|
||||
@ -67,6 +67,7 @@ void JSI_Lobby::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
|
||||
scriptInterface.RegisterFunction<void, std::wstring, std::wstring, &JSI_Lobby::LobbyBan>("LobbyBan");
|
||||
scriptInterface.RegisterFunction<const char*, std::wstring, &JSI_Lobby::LobbyGetPlayerPresence>("LobbyGetPlayerPresence");
|
||||
scriptInterface.RegisterFunction<const char*, std::wstring, &JSI_Lobby::LobbyGetPlayerRole>("LobbyGetPlayerRole");
|
||||
scriptInterface.RegisterFunction<std::wstring, std::wstring, &JSI_Lobby::LobbyGetPlayerRating>("LobbyGetPlayerRating");
|
||||
scriptInterface.RegisterFunction<std::wstring, std::wstring, std::wstring, &JSI_Lobby::EncryptPassword>("EncryptPassword");
|
||||
scriptInterface.RegisterFunction<std::wstring, &JSI_Lobby::LobbyGetRoomSubject>("LobbyGetRoomSubject");
|
||||
#endif // CONFIG2_LOBBY
|
||||
@ -243,12 +244,12 @@ bool JSI_Lobby::LobbyGuiPollHasPlayerListUpdate(ScriptInterface::CxPrivate* UNUS
|
||||
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)
|
||||
@ -325,6 +326,14 @@ const char* JSI_Lobby::LobbyGetPlayerRole(ScriptInterface::CxPrivate* UNUSED(pCx
|
||||
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
|
||||
|
@ -48,7 +48,7 @@ namespace JSI_Lobby
|
||||
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);
|
||||
@ -59,6 +59,7 @@ namespace JSI_Lobby
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user