Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/LobbyGameRegistration.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/LobbyGameRegistration.js (revision 25406) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/LobbyGameRegistration.js (revision 25407) @@ -1,146 +1,147 @@ /** * If there is an XmppClient, this class informs the XPartaMuPP lobby bot that * this match is being setup so that others can join. * It informs of the lobby of some setting values and the participating clients. */ class LobbyGameRegistrationController { constructor(initData, setupWindow, netMessages, mapCache, playerAssignmentsController) { this.mapCache = mapCache; this.serverName = initData.serverName; this.hasPassword = initData.hasPassword; this.mods = JSON.stringify(Engine.GetEngineInfo().mods); this.timer = undefined; // Only send a lobby update when its data changed this.lastStanza = undefined; // Events setupWindow.registerClosePageHandler(this.onClosePage.bind(this)); netMessages.registerNetMessageHandler("start", this.onGameStart.bind(this)); playerAssignmentsController.registerPlayerAssignmentsChangeHandler(this.sendImmediately.bind(this)); g_GameSettings.map.watch(() => this.onSettingsChange(), ["map", "type"]); g_GameSettings.mapSize.watch(() => this.onSettingsChange(), ["size"]); g_GameSettings.victoryConditions.watch(() => this.onSettingsChange(), ["active"]); g_GameSettings.playerCount.watch(() => this.onSettingsChange(), ["nbPlayers"]); } onSettingsChange() { if (this.lastStanza) this.sendDelayed(); else this.sendImmediately(); } onGameStart() { this.sendImmediately(); let clients = this.formatClientsForStanza(); Engine.SendChangeStateGame(clients.connectedPlayers, clients.list); } onClosePage() { if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); } /** * Send the relevant game settings to the lobby bot in a deferred manner. */ sendDelayed() { if (!g_IsController || !Engine.HasXmppClient()) return; // Already sending an update - do nothing. if (this.timer !== undefined) return; this.timer = setTimeout(this.sendImmediately.bind(this), this.Timeout); } /** * Send the relevant game settings to the lobby bot immediately. */ sendImmediately() { if (!g_IsController || !Engine.HasXmppClient()) return; // Wait until a map has been selected. if (!g_GameSettings.map.map) return; Engine.ProfileStart("sendRegisterGameStanza"); if (this.timer !== undefined) { clearTimeout(this.timer); this.timer = undefined; } let clients = this.formatClientsForStanza(); let stanza = { "name": this.serverName, "hostUsername": Engine.LobbyGetNick(), + "hostJID": "", // Overwritten by C++, placeholder. "mapName": g_GameSettings.map.map, // TODO: if the map name was always up-to-date we wouldn't need the mapcache here. "niceMapName": this.mapCache.getTranslatableMapName(g_GameSettings.map.type, g_GameSettings.map.map), "mapSize": g_GameSettings.map.type == "random" ? g_GameSettings.mapSize.size : "Default", "mapType": g_GameSettings.map.type, "victoryConditions": Array.from(g_GameSettings.victoryConditions.active).join(","), "nbp": clients.connectedPlayers, "maxnbp": g_GameSettings.playerCount.nbPlayers, "players": clients.list, "mods": this.mods, "hasPassword": this.hasPassword || "" }; // Only send the stanza if one of these properties changed if (this.lastStanza && Object.keys(stanza).every(prop => this.lastStanza[prop] == stanza[prop])) return; this.lastStanza = stanza; Engine.SendRegisterGame(stanza); Engine.ProfileStop(); } /** * Send a list of playernames and distinct between players and observers. * Don't send teams, AIs or anything else until the game was started. */ formatClientsForStanza() { let connectedPlayers = 0; let playerData = []; for (let guid in g_PlayerAssignments) { let pData = { "Name": g_PlayerAssignments[guid].name }; if (g_PlayerAssignments[guid].player <= g_GameSettings.playerCount.nbPlayers) ++connectedPlayers; else pData.Team = "observer"; playerData.push(pData); } return { "list": playerDataToStringifiedTeamList(playerData), "connectedPlayers": connectedPlayers }; } } /** * Send the current game settings to the lobby bot if the settings didn't change for this number of milliseconds. */ LobbyGameRegistrationController.prototype.Timeout = 2000; Index: ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js (revision 25406) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js (revision 25407) @@ -1,469 +1,471 @@ /** * Whether we are attempting to join or host a game. */ var g_IsConnecting = false; /** * "server" or "client" */ var g_GameType; /** * Server title shown in the lobby gamelist. */ var g_ServerName = ""; /** * Identifier if server is using password. */ var g_ServerHasPassword = false; var g_ServerId; var g_IsRejoining = false; var g_PlayerAssignments; // used when rejoining var g_UserRating; function init(attribs) { g_UserRating = attribs.rating; switch (attribs.multiplayerGameType) { case "join": { if (!Engine.HasXmppClient()) { switchSetupPage("pageJoin"); break; } if (attribs.hasPassword) { g_ServerName = attribs.name; g_ServerId = attribs.hostJID; switchSetupPage("pagePassword"); } else if (startJoinFromLobby(attribs.name, attribs.hostJID, "")) switchSetupPage("pageConnecting"); break; } case "host": { let hasXmppClient = Engine.HasXmppClient(); Engine.GetGUIObjectByName("hostSTUNWrapper").hidden = !hasXmppClient; Engine.GetGUIObjectByName("hostPasswordWrapper").hidden = !hasXmppClient; if (hasXmppClient) { Engine.GetGUIObjectByName("hostPlayerName").caption = attribs.name; Engine.GetGUIObjectByName("hostServerName").caption = sprintf(translate("%(name)s's game"), { "name": attribs.name }); Engine.GetGUIObjectByName("useSTUN").checked = Engine.ConfigDB_GetValue("user", "lobby.stun.enabled") == "true"; } switchSetupPage("pageHost"); break; } default: error("Unrecognised multiplayer game type: " + attribs.multiplayerGameType); break; } } function cancelSetup() { if (g_IsConnecting) Engine.DisconnectNetworkGame(); if (Engine.HasXmppClient()) Engine.LobbySetPlayerPresence("available"); // Keep the page open if an attempt to join/host by ip failed if (!g_IsConnecting || (Engine.HasXmppClient() && g_GameType == "client")) { Engine.PopGuiPage(); return; } g_IsConnecting = false; Engine.GetGUIObjectByName("hostFeedback").caption = ""; if (g_GameType == "client") switchSetupPage("pageJoin"); else if (g_GameType == "server") switchSetupPage("pageHost"); else error("cancelSetup: Unrecognised multiplayer game type: " + g_GameType); } function confirmPassword() { if (Engine.GetGUIObjectByName("pagePassword").hidden) return; if (startJoinFromLobby(g_ServerName, g_ServerId, Engine.GetGUIObjectByName("clientPassword").caption)) switchSetupPage("pageConnecting"); } function confirmSetup() { if (!Engine.GetGUIObjectByName("pageJoin").hidden) { let joinPlayerName = Engine.GetGUIObjectByName("joinPlayerName").caption; let joinServer = Engine.GetGUIObjectByName("joinServer").caption; let joinPort = Engine.GetGUIObjectByName("joinPort").caption; - if (startJoin(joinPlayerName, joinServer, getValidPort(joinPort), false, "")) + if (startJoin(joinPlayerName, joinServer, getValidPort(joinPort))) switchSetupPage("pageConnecting"); } else if (!Engine.GetGUIObjectByName("pageHost").hidden) { let hostServerName = Engine.GetGUIObjectByName("hostServerName").caption; if (!hostServerName) { Engine.GetGUIObjectByName("hostFeedback").caption = translate("Please enter a valid server name."); return; } let hostPort = Engine.GetGUIObjectByName("hostPort").caption; if (getValidPort(hostPort) != +hostPort) { Engine.GetGUIObjectByName("hostFeedback").caption = sprintf( translate("Server port number must be between %(min)s and %(max)s."), { "min": g_ValidPorts.min, "max": g_ValidPorts.max }); return; } let hostPlayerName = Engine.GetGUIObjectByName("hostPlayerName").caption; let hostPassword = Engine.GetGUIObjectByName("hostPassword").caption; if (startHost(hostPlayerName, hostServerName, getValidPort(hostPort), hostPassword)) switchSetupPage("pageConnecting"); } } function startConnectionStatus(type) { g_GameType = type; g_IsConnecting = true; g_IsRejoining = false; Engine.GetGUIObjectByName("connectionStatus").caption = translate("Connecting to server..."); } function onTick() { if (!g_IsConnecting) return; pollAndHandleNetworkClient(); } function getConnectionFailReason(reason) { switch (reason) { case "not_server": return translate("Server is not running."); case "invalid_password": return translate("Password is invalid."); case "banned": return translate("You have been banned."); default: warn("Unknown connection failure reason: " + reason); return sprintf(translate("\\[Invalid value %(reason)s]"), { "reason": reason }); } } function reportConnectionFail(reason) { messageBox( 400, 200, (translate("Failed to connect to the server.") ) + "\n\n" + getConnectionFailReason(reason), translate("Connection failed") ); } function pollAndHandleNetworkClient() { while (true) { var message = Engine.PollNetworkClient(); if (!message) break; log(sprintf(translate("Net message: %(message)s"), { "message": uneval(message) })); // If we're rejoining an active game, we don't want to actually display // the game setup screen, so perform similar processing to gamesetup.js // in this screen if (g_IsRejoining) { switch (message.type) { case "serverdata": switch (message.status) { case "failed": cancelSetup(); reportConnectionFail(message.reason, false); return; default: error("Unrecognised netstatus type: " + message.status); break; } break; case "netstatus": switch (message.status) { case "disconnected": cancelSetup(); reportDisconnect(message.reason, false); return; default: error("Unrecognised netstatus type: " + message.status); break; } break; case "players": g_PlayerAssignments = message.newAssignments; break; case "start": Engine.SwitchGuiPage("page_loading.xml", { "attribs": message.initAttributes, "isRejoining": g_IsRejoining, "playerAssignments": g_PlayerAssignments }); // Process further pending netmessages in the session page return; case "chat": break; case "netwarn": break; default: error("Unrecognised net message type: " + message.type); } } else // Not rejoining - just trying to connect to server. { switch (message.type) { case "serverdata": switch (message.status) { case "failed": cancelSetup(); reportConnectionFail(message.reason, false); return; default: error("Unrecognised netstatus type: " + message.status); break; } break; case "netstatus": switch (message.status) { case "connected": Engine.GetGUIObjectByName("connectionStatus").caption = translate("Registering with server..."); break; case "authenticated": if (message.rejoining) { Engine.GetGUIObjectByName("connectionStatus").caption = translate("Game has already started, rejoining..."); g_IsRejoining = true; return; // we'll process the game setup messages in the next tick } Engine.SwitchGuiPage("page_gamesetup.xml", { "serverName": g_ServerName, "hasPassword": g_ServerHasPassword }); return; // don't process any more messages - leave them for the game GUI loop case "disconnected": cancelSetup(); reportDisconnect(message.reason, false); return; default: error("Unrecognised netstatus type: " + message.status); break; } break; case "netwarn": break; default: error("Unrecognised net message type: " + message.type); break; } } } } function switchSetupPage(newPage) { let multiplayerPages = Engine.GetGUIObjectByName("multiplayerPages"); for (let page of multiplayerPages.children) if (page.name.startsWith("page")) page.hidden = true; if (newPage == "pageJoin" || newPage == "pageHost") { let pageSize = multiplayerPages.size; let halfHeight = newPage == "pageJoin" ? 130 : Engine.HasXmppClient() ? 125 : 110; pageSize.top = -halfHeight; pageSize.bottom = halfHeight; multiplayerPages.size = pageSize; } else if (newPage == "pagePassword") { let pageSize = multiplayerPages.size; let halfHeight = 60; pageSize.top = -halfHeight; pageSize.bottom = halfHeight; multiplayerPages.size = pageSize; } Engine.GetGUIObjectByName(newPage).hidden = false; Engine.GetGUIObjectByName("hostPlayerNameWrapper").hidden = Engine.HasXmppClient(); Engine.GetGUIObjectByName("hostServerNameWrapper").hidden = !Engine.HasXmppClient(); Engine.GetGUIObjectByName("continueButton").hidden = newPage == "pageConnecting" || newPage == "pagePassword"; } function startHost(playername, servername, port, password) { startConnectionStatus("server"); Engine.ConfigDB_CreateAndWriteValueToFile("user", "playername.multiplayer", playername, "config/user.cfg"); Engine.ConfigDB_CreateAndWriteValueToFile("user", "multiplayerhosting.port", port, "config/user.cfg"); let hostFeedback = Engine.GetGUIObjectByName("hostFeedback"); // Disallow identically named games in the multiplayer lobby if (Engine.HasXmppClient() && Engine.GetGameList().some(game => game.name == servername)) { cancelSetup(); hostFeedback.caption = translate("Game name already in use."); return false; } let useSTUN = Engine.HasXmppClient() && Engine.GetGUIObjectByName("useSTUN").checked; try { - Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port, playername, useSTUN, password); + Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port, useSTUN, password); } catch (e) { cancelSetup(); messageBox( 400, 200, sprintf(translate("Cannot host game: %(message)s."), { "message": e.message }), translate("Error") ); return false; } g_ServerName = servername; g_ServerHasPassword = !!password; if (Engine.HasXmppClient()) Engine.LobbySetPlayerPresence("playing"); return true; } /** - * Connects via STUN if the hostJID is given. + * Connect via direct IP (used by the 'simple' MP screen) */ -function startJoin(playername, ip, port, useSTUN, hostJID) +function startJoin(playername, ip, port) { try { - Engine.StartNetworkJoin(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), ip, port, useSTUN, hostJID); + Engine.StartNetworkJoin(playername, ip, port); } catch (e) { cancelSetup(); messageBox( 400, 200, sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }), translate("Error") ); return false; } startConnectionStatus("client"); + // Future-proofing: there could be an XMPP client even if we join a game directly. if (Engine.HasXmppClient()) Engine.LobbySetPlayerPresence("playing"); - else - { - // Only save the player name and host address if they're valid and we're not in the lobby - Engine.ConfigDB_CreateAndWriteValueToFile("user", "playername.multiplayer", playername, "config/user.cfg"); - Engine.ConfigDB_CreateAndWriteValueToFile("user", "multiplayerserver", ip, "config/user.cfg"); - Engine.ConfigDB_CreateAndWriteValueToFile("user", "multiplayerjoining.port", port, "config/user.cfg"); - } + + // Only save the player name and host address if they're valid. + Engine.ConfigDB_CreateAndWriteValueToFile("user", "playername.multiplayer", playername, "config/user.cfg"); + Engine.ConfigDB_CreateAndWriteValueToFile("user", "multiplayerserver", ip, "config/user.cfg"); + Engine.ConfigDB_CreateAndWriteValueToFile("user", "multiplayerjoining.port", port, "config/user.cfg"); return true; } +/** + * Connect via the lobby. + */ function startJoinFromLobby(playername, hostJID, password) { if (!Engine.HasXmppClient()) { cancelSetup(); messageBox( 400, 200, sprintf("You cannot join a lobby game without logging in to the lobby."), translate("Error") ); return false; } try { Engine.StartNetworkJoinLobby(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), hostJID, password); } catch (e) { cancelSetup(); messageBox( 400, 200, sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }), translate("Error") ); return false; } startConnectionStatus("client"); Engine.LobbySetPlayerPresence("playing"); return true; } function getDefaultGameName() { return sprintf(translate("%(playername)s's game"), { "playername": multiplayerName() }); } function getDefaultPassword() { return ""; } Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js (revision 25406) +++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js (revision 25407) @@ -1,104 +1,104 @@ /** * 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 game setups. 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 stanza = game.stanza; Engine.PushGuiPage("page_gamesetup_mp.xml", { "multiplayerGameType": "join", "name": g_Nickname, "rating": this.getRejoinRating(stanza), "hasPassword": !!stanza.hasPassword, - "hostJID": stanza.hostUsername + "@" + Engine.ConfigDB_GetValue("user", "lobby.server") + "/0ad" + "hostJID": stanza.hostJID }); } 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"); Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js (revision 25406) +++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js (revision 25407) @@ -1,348 +1,349 @@ /** * 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(mapCache) { this.mapCache = mapCache; // 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"); if (this.mapCache.checkIfExists(newStanza.mapType, newStanza.mapName)) { displayData.mapName = escapeText(this.mapCache.translateMapName(newStanza.niceMapName)); displayData.mapDescription = this.mapCache.getTranslatedMapDescription(newStanza.mapType, newStanza.mapName); } else { displayData.mapName = escapeText(newStanza.niceMapName); displayData.mapDescription = ""; } 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(); } displayData.private = newStanza.hasPassword ? '[icon="icon_private"]' : '[icon="icon_public"]'; 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", "hasPassword", "hostUsername", + "hostJID", "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" } }; Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js (revision 25406) +++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js (revision 25407) @@ -1,226 +1,226 @@ /** * 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, mapCache) { this.mapCache = mapCache; // 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_private = []; 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.hostUsername] || undefined; + let game = this.games[stanza.hostJID] || undefined; let exists = !!game; if (!exists) game = new Game(this.mapCache); game.update(stanza, selectedColumn); - newGames[stanza.hostUsername] = game; + newGames[stanza.hostJID] = 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_private.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_private[i] = displayData.private; 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.hostUsername == selectedGame.stanza.hostUsername && game.stanza.name == selectedGame.stanza.name) + if (selectedGame && game.stanza.hostJID == selectedGame.stanza.hostJID && game.stanza.name == selectedGame.stanza.name) selectedGameIndex = i; }); Engine.ProfileStop(); } { Engine.ProfileStart("copyToGUI"); let gamesBox = this.gamesBox; gamesBox.list_private = this.list_private; 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; } } } Index: ps/trunk/source/lobby/IXmppClient.h =================================================================== --- ps/trunk/source/lobby/IXmppClient.h (revision 25406) +++ ps/trunk/source/lobby/IXmppClient.h (revision 25407) @@ -1,71 +1,72 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef IXMPPCLIENT_H #define IXMPPCLIENT_H #include "scriptinterface/ScriptTypes.h" class ScriptInterface; namespace StunClient { struct StunEndpoint; } class IXmppClient { public: static IXmppClient* create(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize = 0, bool regOpt = false); virtual ~IXmppClient() {} virtual void connect() = 0; virtual void disconnect() = 0; virtual bool isConnected() = 0; virtual void recv() = 0; virtual void SendIqGetBoardList() = 0; virtual void SendIqGetProfile(const std::string& player) = 0; virtual void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0; virtual void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0; virtual void SendIqGetConnectionData(const std::string& jid, const std::string& password) = 0; virtual void SendIqUnregisterGame() = 0; virtual void SendIqChangeStateGame(const std::string& nbp, const std::string& players) = 0; virtual void SendIqLobbyAuth(const std::string& to, const std::string& token) = 0; virtual void SetNick(const std::string& nick) = 0; - virtual std::string GetNick() = 0; + virtual std::string GetNick() const = 0; + virtual std::string GetJID() const = 0; virtual void kick(const std::string& nick, const std::string& reason) = 0; virtual void ban(const std::string& nick, const std::string& reason) = 0; virtual void SetPresence(const std::string& presence) = 0; virtual const char* GetPresence(const std::string& nickname) = 0; virtual const char* GetRole(const std::string& nickname) = 0; virtual std::wstring GetRating(const std::string& nickname) = 0; virtual const std::wstring& GetSubject() = 0; virtual JS::Value GUIGetPlayerList(const ScriptInterface& scriptInterface) = 0; virtual JS::Value GUIGetGameList(const ScriptInterface& scriptInterface) = 0; virtual JS::Value GUIGetBoardList(const ScriptInterface& scriptInterface) = 0; virtual JS::Value GUIGetProfile(const ScriptInterface& scriptInterface) = 0; virtual JS::Value GuiPollNewMessages(const ScriptInterface& scriptInterface) = 0; virtual JS::Value GuiPollHistoricMessages(const ScriptInterface& scriptInterface) = 0; virtual bool GuiPollHasPlayerListUpdate() = 0; virtual void SendMUCMessage(const std::string& message) = 0; virtual void SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJID) = 0; }; extern IXmppClient *g_XmppClient; extern bool g_rankedGame; #endif // XMPPCLIENT_H Index: ps/trunk/source/lobby/XmppClient.cpp =================================================================== --- ps/trunk/source/lobby/XmppClient.cpp (revision 25406) +++ ps/trunk/source/lobby/XmppClient.cpp (revision 25407) @@ -1,1498 +1,1515 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "XmppClient.h" #include "StanzaExtensions.h" #ifdef WIN32 # include #endif #include "i18n/L10n.h" #include "lib/external_libraries/enet.h" #include "lib/utf8.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" +#include "ps/GUID.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptExtraHeaders.h" // StructuredClone #include "scriptinterface/ScriptInterface.h" #include //debug #if 1 #define DbgXMPP(x) #else #define DbgXMPP(x) std::cout << x << std::endl; static std::string tag_xml(const glooxwrapper::IQ& iq) { std::string ret; glooxwrapper::Tag* tag = iq.tag(); ret = tag->xml().to_string(); glooxwrapper::Tag::free(tag); return ret; } #endif static std::string tag_name(const glooxwrapper::IQ& iq) { std::string ret; glooxwrapper::Tag* tag = iq.tag(); ret = tag->name().to_string(); glooxwrapper::Tag::free(tag); return ret; } IXmppClient* IXmppClient::create(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize,bool regOpt) { return new XmppClient(scriptInterface, sUsername, sPassword, sRoom, sNick, historyRequestSize, regOpt); } /** * Construct the XMPP client. * * @param scriptInterface - ScriptInterface to be used for storing GUI messages. * Can be left blank for non-visual applications. * @param sUsername Username to login with of register. * @param sPassword Password to login with or register. * @param sRoom MUC room to join. * @param sNick Nick to join with. * @param historyRequestSize Number of stanzas of room history to request. * @param regOpt If we are just registering or not. */ XmppClient::XmppClient(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize, bool regOpt) : m_ScriptInterface(scriptInterface), m_client(nullptr), m_mucRoom(nullptr), m_registration(nullptr), m_username(sUsername), m_password(sPassword), m_room(sRoom), m_nick(sNick), m_initialLoadComplete(false), m_isConnected(false), m_sessionManager(nullptr), m_certStatus(gloox::CertStatus::CertOk), m_PlayerMapUpdate(false), m_connectionDataJid(), m_connectionDataIqId() { if (m_ScriptInterface) JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), XmppClient::Trace, this); // Read lobby configuration from default.cfg std::string sXpartamupp; std::string sEchelon; CFG_GET_VAL("lobby.server", m_server); CFG_GET_VAL("lobby.xpartamupp", sXpartamupp); CFG_GET_VAL("lobby.echelon", sEchelon); m_xpartamuppId = sXpartamupp + "@" + m_server + "/CC"; m_echelonId = sEchelon + "@" + m_server + "/CC"; - glooxwrapper::JID clientJid(sUsername + "@" + m_server + "/0ad"); + // Generate a unique, unpredictable resource to allow multiple 0 A.D. instances to connect to the lobby. + glooxwrapper::JID clientJid(sUsername + "@" + m_server + "/0ad-" + ps_generate_guid()); glooxwrapper::JID roomJid(m_room + "@conference." + m_server + "/" + sNick); // If we are connecting, use the full jid and a password // If we are registering, only use the server name if (!regOpt) m_client = new glooxwrapper::Client(clientJid, sPassword); else m_client = new glooxwrapper::Client(m_server); // Optionally join without a TLS certificate, so a local server can be tested quickly. // Security risks from malicious JS mods can be mitigated if this option and also the hostname and login are shielded from JS access. bool tls = true; CFG_GET_VAL("lobby.tls", tls); m_client->setTls(tls ? gloox::TLSRequired : gloox::TLSDisabled); // Disable use of the SASL PLAIN mechanism, to prevent leaking credentials // if the server doesn't list any supported SASL mechanism or the response // has been modified to exclude those. const int mechs = gloox::SaslMechAll ^ gloox::SaslMechPlain; m_client->setSASLMechanisms(mechs); m_client->registerConnectionListener(this); m_client->setPresence(gloox::Presence::Available, -1); m_client->disco()->setVersion("Pyrogenesis", engine_version); m_client->disco()->setIdentity("client", "bot"); m_client->setCompression(false); m_client->registerStanzaExtension(new GameListQuery()); m_client->registerIqHandler(this, EXTGAMELISTQUERY); m_client->registerStanzaExtension(new BoardListQuery()); m_client->registerIqHandler(this, EXTBOARDLISTQUERY); m_client->registerStanzaExtension(new ProfileQuery()); m_client->registerIqHandler(this, EXTPROFILEQUERY); m_client->registerStanzaExtension(new LobbyAuth()); m_client->registerIqHandler(this, EXTLOBBYAUTH); m_client->registerStanzaExtension(new ConnectionData()); m_client->registerIqHandler(this, EXTCONNECTIONDATA); m_client->registerMessageHandler(this); // Uncomment to see the raw stanzas //m_client->getWrapped()->logInstance().registerLogHandler( gloox::LogLevelDebug, gloox::LogAreaAll, this ); if (!regOpt) { // Create a Multi User Chat Room m_mucRoom = new glooxwrapper::MUCRoom(m_client, roomJid, this, 0); // Get room history. m_mucRoom->setRequestHistory(historyRequestSize, gloox::MUCRoom::HistoryMaxStanzas); } else { // Registration m_registration = new glooxwrapper::Registration(m_client); m_registration->registerRegistrationHandler(this); } m_sessionManager = new glooxwrapper::SessionManager(m_client, this); // Register plugins to allow gloox parse them in incoming sessions m_sessionManager->registerPlugins(); } /** * Destroy the xmpp client */ XmppClient::~XmppClient() { DbgXMPP("XmppClient destroyed"); delete m_registration; delete m_mucRoom; delete m_sessionManager; // Workaround for memory leak in gloox 1.0/1.0.1 m_client->removePresenceExtension(gloox::ExtCaps); delete m_client; for (const glooxwrapper::Tag* const& t : m_GameList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_BoardList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_Profile) glooxwrapper::Tag::free(t); if (m_ScriptInterface) JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), XmppClient::Trace, this); } void XmppClient::TraceMember(JSTracer* trc) { for (JS::Heap& guiMessage : m_GuiMessageQueue) JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue"); for (JS::Heap& guiMessage : m_HistoricGuiMessages) JS::TraceEdge(trc, &guiMessage, "m_HistoricGuiMessages"); } /// Network void XmppClient::connect() { m_initialLoadComplete = false; m_client->connect(false); } void XmppClient::disconnect() { m_client->disconnect(); } bool XmppClient::isConnected() { return m_isConnected; } void XmppClient::recv() { m_client->recv(1); } /** * Log (debug) Handler */ void XmppClient::handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message) { std::cout << "log: level: " << level << ", area: " << area << ", message: " << message << std::endl; } /***************************************************** * Connection handlers * *****************************************************/ /** * Handle connection */ void XmppClient::onConnect() { if (m_mucRoom) { m_isConnected = true; CreateGUIMessage("system", "connected", std::time(nullptr)); m_mucRoom->join(); } if (m_registration) m_registration->fetchRegistrationFields(); } /** * Handle disconnection */ void XmppClient::onDisconnect(gloox::ConnectionError error) { // Make sure we properly leave the room so that // everything works if we decide to come back later if (m_mucRoom) m_mucRoom->leave(); // Clear game, board and player lists. for (const glooxwrapper::Tag* const& t : m_GameList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_BoardList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_Profile) glooxwrapper::Tag::free(t); m_BoardList.clear(); m_GameList.clear(); m_PlayerMap.clear(); m_PlayerMapUpdate = true; m_Profile.clear(); m_HistoricGuiMessages.clear(); m_isConnected = false; m_initialLoadComplete = false; CreateGUIMessage( "system", "disconnected", std::time(nullptr), "reason", error, "certificate_status", m_certStatus); } /** * Handle TLS connection. */ bool XmppClient::onTLSConnect(const glooxwrapper::CertInfo& info) { DbgXMPP("onTLSConnect"); DbgXMPP( "status: " << info.status << "\nissuer: " << info.issuer << "\npeer: " << info.server << "\nprotocol: " << info.protocol << "\nmac: " << info.mac << "\ncipher: " << info.cipher << "\ncompression: " << info.compression ); m_certStatus = static_cast(info.status); // Optionally accept invalid certificates, see require_tls option. bool verify_certificate = true; CFG_GET_VAL("lobby.verify_certificate", verify_certificate); return info.status == gloox::CertOk || !verify_certificate; } /** * Handle MUC room errors */ void XmppClient::handleMUCError(glooxwrapper::MUCRoom& UNUSED(room), gloox::StanzaError err) { DbgXMPP("MUC Error " << ": " << StanzaErrorToString(err)); CreateGUIMessage("system", "error", std::time(nullptr), "text", err); } /***************************************************** * Requests to server * *****************************************************/ /** * Request the leaderboard data from the server. */ void XmppClient::SendIqGetBoardList() { glooxwrapper::JID echelonJid(m_echelonId); // Send IQ BoardListQuery* b = new BoardListQuery(); b->m_Command = "getleaderboard"; glooxwrapper::IQ iq(gloox::IQ::Get, echelonJid, m_client->getID()); iq.addExtension(b); DbgXMPP("SendIqGetBoardList [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Request the profile data from the server. */ void XmppClient::SendIqGetProfile(const std::string& player) { glooxwrapper::JID echelonJid(m_echelonId); // Send IQ ProfileQuery* b = new ProfileQuery(); b->m_Command = player; glooxwrapper::IQ iq(gloox::IQ::Get, echelonJid, m_client->getID()); iq.addExtension(b); DbgXMPP("SendIqGetProfile [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Request the Connection data (ip, port...) from the server. */ void XmppClient::SendIqGetConnectionData(const std::string& jid, const std::string& password) { glooxwrapper::JID targetJID(jid); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Password = password; glooxwrapper::IQ iq(gloox::IQ::Get, targetJID, m_client->getID()); iq.addExtension(connectionData); m_connectionDataJid = iq.from().full(); m_connectionDataIqId = iq.id().to_string(); DbgXMPP("SendIqGetConnectionData [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Send game report containing numerous game properties to the server. * * @param data A JS array of game statistics */ void XmppClient::SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data) { glooxwrapper::JID echelonJid(m_echelonId); // Setup some base stanza attributes GameReport* game = new GameReport(); glooxwrapper::Tag* report = glooxwrapper::Tag::allocate("game"); // Iterate through all the properties reported and add them to the stanza. std::vector properties; scriptInterface.EnumeratePropertyNames(data, true, properties); for (const std::string& p : properties) { std::wstring value; scriptInterface.GetProperty(data, p.c_str(), value); report->addAttribute(p, utf8_from_wstring(value)); } // Add stanza to IQ game->m_GameReport.emplace_back(report); // Send IQ glooxwrapper::IQ iq(gloox::IQ::Set, echelonJid, m_client->getID()); iq.addExtension(game); DbgXMPP("SendGameReport [" << tag_xml(iq) << "]"); m_client->send(iq); }; /** * Send a request to register a game to the server. * * @param data A JS array of game attributes */ void XmppClient::SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Setup some base stanza attributes - GameListQuery* g = new GameListQuery(); + std::unique_ptr g = std::make_unique(); g->m_Command = "register"; glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game"); // Iterate through all the properties reported and add them to the stanza. std::vector properties; scriptInterface.EnumeratePropertyNames(data, true, properties); for (const std::string& p : properties) { - std::wstring value; - scriptInterface.GetProperty(data, p.c_str(), value); - game->addAttribute(p, utf8_from_wstring(value)); + std::string value; + if (!scriptInterface.GetProperty(data, p.c_str(), value)) + { + LOGERROR("Could not parse attribute '%s' as string.", p); + return; + } + game->addAttribute(p, value); } + // Overwrite some attributes to make it slightly less trivial to do bad things, + // and explicit some invariants. + + // The JID must point to ourself. + game->addAttribute("hostJID", GetJID()); + // Push the stanza onto the IQ g->m_GameList.emplace_back(game); // Send IQ glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID()); - iq.addExtension(g); + iq.addExtension(g.release()); DbgXMPP("SendIqRegisterGame [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Send a request to unregister a game to the server. */ void XmppClient::SendIqUnregisterGame() { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Send IQ GameListQuery* g = new GameListQuery(); g->m_Command = "unregister"; g->m_GameList.emplace_back(glooxwrapper::Tag::allocate("game")); glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID()); iq.addExtension(g); DbgXMPP("SendIqUnregisterGame [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Send a request to change the state of a registered game on the server. * * A game can either be in the 'running' or 'waiting' state - the server * decides which - but we need to update the current players that are * in-game so the server can make the calculation. */ void XmppClient::SendIqChangeStateGame(const std::string& nbp, const std::string& players) { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Send IQ GameListQuery* g = new GameListQuery(); g->m_Command = "changestate"; glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game"); game->addAttribute("nbp", nbp); game->addAttribute("players", players); g->m_GameList.emplace_back(game); glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID()); iq.addExtension(g); DbgXMPP("SendIqChangeStateGame [" << tag_xml(iq) << "]"); m_client->send(iq); } /***************************************************** * iq to clients * *****************************************************/ /** * Send lobby authentication token. */ void XmppClient::SendIqLobbyAuth(const std::string& to, const std::string& token) { LobbyAuth* auth = new LobbyAuth(); auth->m_Token = token; - glooxwrapper::JID clientJid(to + "@" + m_server + "/0ad"); + glooxwrapper::JID clientJid(to); glooxwrapper::IQ iq(gloox::IQ::Set, clientJid, m_client->getID()); iq.addExtension(auth); DbgXMPP("SendIqLobbyAuth [" << tag_xml(iq) << "]"); m_client->send(iq); } /***************************************************** * Account registration * *****************************************************/ void XmppClient::handleRegistrationFields(const glooxwrapper::JID&, int fields, glooxwrapper::string) { glooxwrapper::RegistrationFields vals; vals.username = m_username; vals.password = m_password; m_registration->createAccount(fields, vals); } void XmppClient::handleRegistrationResult(const glooxwrapper::JID&, gloox::RegistrationResult result) { if (result == gloox::RegistrationSuccess) CreateGUIMessage("system", "registered", std::time(nullptr)); else CreateGUIMessage("system", "error", std::time(nullptr), "text", result); disconnect(); } void XmppClient::handleAlreadyRegistered(const glooxwrapper::JID&) { DbgXMPP("the account already exists"); } void XmppClient::handleDataForm(const glooxwrapper::JID&, const glooxwrapper::DataForm&) { DbgXMPP("dataForm received"); } void XmppClient::handleOOB(const glooxwrapper::JID&, const glooxwrapper::OOB&) { DbgXMPP("OOB registration requested"); } /***************************************************** * Requests from GUI * *****************************************************/ /** * Handle requests from the GUI for the list of players. * * @return A JS array containing all known players and their presences */ JS::Value XmppClient::GUIGetPlayerList(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); JS::RootedValue ret(rq.cx); ScriptInterface::CreateArray(rq, &ret); int j = 0; for (const std::pair& p : m_PlayerMap) { JS::RootedValue player(rq.cx); ScriptInterface::CreateObject( rq, &player, "name", p.first, "presence", p.second.m_Presence, "rating", p.second.m_Rating, "role", p.second.m_Role); scriptInterface.SetPropertyInt(ret, j++, player); } return ret; } /** * Handle requests from the GUI for the list of all active games. * * @return A JS array containing all known games */ JS::Value XmppClient::GUIGetGameList(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); JS::RootedValue ret(rq.cx); ScriptInterface::CreateArray(rq, &ret); int j = 0; - const char* stats[] = { "name", "hostUsername", "state", "hasPassword", + const char* stats[] = { "name", "hostUsername", "hostJID", "state", "hasPassword", "nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType", "victoryConditions", "startTime", "mods" }; for(const glooxwrapper::Tag* const& t : m_GameList) { JS::RootedValue game(rq.cx); ScriptInterface::CreateObject(rq, &game); for (size_t i = 0; i < ARRAY_SIZE(stats); ++i) scriptInterface.SetProperty(game, stats[i], t->findAttribute(stats[i])); scriptInterface.SetPropertyInt(ret, j++, game); } return ret; } /** * Handle requests from the GUI for leaderboard data. * * @return A JS array containing all known leaderboard data */ JS::Value XmppClient::GUIGetBoardList(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); JS::RootedValue ret(rq.cx); ScriptInterface::CreateArray(rq, &ret); int j = 0; const char* attributes[] = { "name", "rank", "rating" }; for(const glooxwrapper::Tag* const& t : m_BoardList) { JS::RootedValue board(rq.cx); ScriptInterface::CreateObject(rq, &board); for (size_t i = 0; i < ARRAY_SIZE(attributes); ++i) scriptInterface.SetProperty(board, attributes[i], t->findAttribute(attributes[i])); scriptInterface.SetPropertyInt(ret, j++, board); } return ret; } /** * Handle requests from the GUI for profile data. * * @return A JS array containing the specific user's profile data */ JS::Value XmppClient::GUIGetProfile(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); JS::RootedValue ret(rq.cx); ScriptInterface::CreateArray(rq, &ret); int j = 0; const char* stats[] = { "player", "rating", "totalGamesPlayed", "highestRating", "wins", "losses", "rank" }; for (const glooxwrapper::Tag* const& t : m_Profile) { JS::RootedValue profile(rq.cx); ScriptInterface::CreateObject(rq, &profile); for (size_t i = 0; i < ARRAY_SIZE(stats); ++i) scriptInterface.SetProperty(profile, stats[i], t->findAttribute(stats[i])); scriptInterface.SetPropertyInt(ret, j++, profile); } return ret; } /***************************************************** * Message interfaces * *****************************************************/ void SetGUIMessageProperty(const ScriptRequest& UNUSED(rq), JS::HandleObject UNUSED(messageObj)) { } template void SetGUIMessageProperty(const ScriptRequest& rq, JS::HandleObject messageObj, const std::string& propertyName, const T& propertyValue, Args const&... args) { JS::RootedValue scriptPropertyValue(rq.cx); ScriptInterface::AssignOrToJSVal(rq, &scriptPropertyValue, propertyValue); JS_DefineProperty(rq.cx, messageObj, propertyName.c_str(), scriptPropertyValue, JSPROP_ENUMERATE); SetGUIMessageProperty(rq, messageObj, args...); } template void XmppClient::CreateGUIMessage( const std::string& type, const std::string& level, const std::time_t time, Args const&... args) { if (!m_ScriptInterface) return; ScriptRequest rq(m_ScriptInterface); JS::RootedValue message(rq.cx); ScriptInterface::CreateObject( rq, &message, "type", type, "level", level, "historic", false, "time", static_cast(time)); JS::RootedObject messageObj(rq.cx, message.toObjectOrNull()); SetGUIMessageProperty(rq, messageObj, args...); m_ScriptInterface->FreezeObject(message, true); m_GuiMessageQueue.push_back(JS::Heap(message)); } bool XmppClient::GuiPollHasPlayerListUpdate() { // The initial playerlist will be received in multiple messages // Only inform the GUI after all of these playerlist fragments were received. if (!m_initialLoadComplete) return false; bool hasUpdate = m_PlayerMapUpdate; m_PlayerMapUpdate = false; return hasUpdate; } JS::Value XmppClient::GuiPollNewMessages(const ScriptInterface& scriptInterface) { if ((m_isConnected && !m_initialLoadComplete) || m_GuiMessageQueue.empty()) return JS::UndefinedValue(); ScriptRequest rq(m_ScriptInterface); // Optimize for batch message processing that is more // performance demanding than processing a lone message. JS::RootedValue messages(rq.cx); ScriptInterface::CreateArray(rq, &messages); int j = 0; for (const JS::Heap& message : m_GuiMessageQueue) { m_ScriptInterface->SetPropertyInt(messages, j++, message); // Store historic chat messages. // Only store relevant messages to minimize memory footprint. JS::RootedValue rootedMessage(rq.cx, message); std::string type; m_ScriptInterface->GetProperty(rootedMessage, "type", type); if (type != "chat") continue; std::string level; m_ScriptInterface->GetProperty(rootedMessage, "level", level); if (level != "room-message" && level != "private-message") continue; JS::RootedValue historicMessage(rq.cx); if (JS_StructuredClone(rq.cx, rootedMessage, &historicMessage, nullptr, nullptr)) { m_ScriptInterface->SetProperty(historicMessage, "historic", true); m_ScriptInterface->FreezeObject(historicMessage, true); m_HistoricGuiMessages.push_back(JS::Heap(historicMessage)); } else LOGERROR("Could not clone historic lobby GUI message!"); } m_GuiMessageQueue.clear(); // Copy the messages over to the caller script interface. return scriptInterface.CloneValueFromOtherCompartment(*m_ScriptInterface, messages); } JS::Value XmppClient::GuiPollHistoricMessages(const ScriptInterface& scriptInterface) { if (m_HistoricGuiMessages.empty()) return JS::UndefinedValue(); ScriptRequest rq(m_ScriptInterface); JS::RootedValue messages(rq.cx); ScriptInterface::CreateArray(rq, &messages); int j = 0; for (const JS::Heap& message : m_HistoricGuiMessages) m_ScriptInterface->SetPropertyInt(messages, j++, message); // Copy the messages over to the caller script interface. return scriptInterface.CloneValueFromOtherCompartment(*m_ScriptInterface, messages); } /** * Send a standard MUC textual message. */ void XmppClient::SendMUCMessage(const std::string& message) { m_mucRoom->send(message); } /** * Handle a room message. */ void XmppClient::handleMUCMessage(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::Message& msg, bool priv) { DbgXMPP(msg.from().resource() << " said " << msg.body()); CreateGUIMessage( "chat", priv ? "private-message" : "room-message", ComputeTimestamp(msg), "from", msg.from().resource(), "text", msg.body()); } /** * Handle a private message. */ void XmppClient::handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession*) { DbgXMPP("type " << msg.subtype() << ", subject " << msg.subject() << ", message " << msg.body() << ", thread id " << msg.thread()); CreateGUIMessage( "chat", "private-message", ComputeTimestamp(msg), "from", msg.from().resource(), "text", msg.body()); } /** * Handle portions of messages containing custom stanza extensions. */ bool XmppClient::handleIq(const glooxwrapper::IQ& iq) { DbgXMPP("handleIq [" << tag_xml(iq) << "]"); if (iq.subtype() == gloox::IQ::Result) { const GameListQuery* gq = iq.findExtension(EXTGAMELISTQUERY); const BoardListQuery* bq = iq.findExtension(EXTBOARDLISTQUERY); const ProfileQuery* pq = iq.findExtension(EXTPROFILEQUERY); const ConnectionData* cd = iq.findExtension(EXTCONNECTIONDATA); if (cd) { if (g_NetServer || !g_NetClient) return true; if (!m_connectionDataJid.empty() && m_connectionDataJid.compare(iq.from().full()) != 0) { LOGMESSAGE("XmppClient: Received connection data from invalid host: %s", iq.from().username()); return true; } if (!m_connectionDataIqId.empty() && m_connectionDataIqId.compare(iq.id().to_string()) != 0) { - LOGWARNING("XmppClient: Received connection data with invalid id"); + LOGMESSAGE("XmppClient: Received connection data with invalid id"); return true; } if (!cd->m_Error.empty()) { g_NetClient->HandleGetServerDataFailed(cd->m_Error.c_str()); return true; } g_NetClient->SetupServerData(cd->m_Ip.to_string(), stoi(cd->m_Port.to_string()), !cd->m_UseSTUN.empty()); g_NetClient->TryToConnect(iq.from().full()); } if (gq) { if (iq.from().full() == m_xpartamuppId && gq->m_Command == "register" && g_NetServer && !g_NetServer->GetUseSTUN()) { if (gq->m_GameList.empty()) { LOGWARNING("XmppClient: Received empty game list in response to Game Register"); return true; } std::string publicIP = gq->m_GameList.front()->findAttribute("ip").to_string(); if (publicIP.empty()) { LOGWARNING("XmppClient: Received game with no IP in response to Game Register"); return true; } g_NetServer->SetConnectionData(publicIP, g_NetServer->GetPublicPort(), false); return true; } for (const glooxwrapper::Tag* const& t : m_GameList) glooxwrapper::Tag::free(t); m_GameList.clear(); for (const glooxwrapper::Tag* const& t : gq->m_GameList) m_GameList.emplace_back(t->clone()); CreateGUIMessage("game", "gamelist", std::time(nullptr)); } if (bq) { if (bq->m_Command == "boardlist") { for (const glooxwrapper::Tag* const& t : m_BoardList) glooxwrapper::Tag::free(t); m_BoardList.clear(); for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList) m_BoardList.emplace_back(t->clone()); CreateGUIMessage("game", "leaderboard", std::time(nullptr)); } else if (bq->m_Command == "ratinglist") { for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList) { const PlayerMap::iterator it = m_PlayerMap.find(t->findAttribute("name")); if (it != m_PlayerMap.end()) { it->second.m_Rating = t->findAttribute("rating"); m_PlayerMapUpdate = true; } } CreateGUIMessage("game", "ratinglist", std::time(nullptr)); } } if (pq) { for (const glooxwrapper::Tag* const& t : m_Profile) glooxwrapper::Tag::free(t); m_Profile.clear(); for (const glooxwrapper::Tag* const& t : pq->m_StanzaProfile) m_Profile.emplace_back(t->clone()); CreateGUIMessage("game", "profile", std::time(nullptr)); } } else if (iq.subtype() == gloox::IQ::Set) { const LobbyAuth* lobbyAuth = iq.findExtension(EXTLOBBYAUTH); if (lobbyAuth) { LOGMESSAGE("XmppClient: Received lobby auth: %s from %s", lobbyAuth->m_Token.to_string(), iq.from().username()); glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); m_client->send(response); if (g_NetServer) g_NetServer->OnLobbyAuth(iq.from().username(), lobbyAuth->m_Token.to_string()); else - LOGERROR("Received lobby authentication request, but not hosting currently!"); + LOGMESSAGE("Received lobby authentication request, but not hosting currently!"); } } else if (iq.subtype() == gloox::IQ::Get) { const ConnectionData* cd = iq.findExtension(EXTCONNECTIONDATA); if (cd) { LOGMESSAGE("XmppClient: Received request for connection data from %s", iq.from().username()); if (!g_NetServer) { glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Error = "not_server"; response.addExtension(connectionData); m_client->send(response); return true; } if (g_NetServer->IsBanned(iq.from().username())) { glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Error = "banned"; response.addExtension(connectionData); m_client->send(response); return true; } if (!g_NetServer->CheckPasswordAndIncrement(CStr(cd->m_Password.c_str()), iq.from().username())) { glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Error = "invalid_password"; response.addExtension(connectionData); m_client->send(response); return true; } glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Ip = g_NetServer->GetPublicIp();; connectionData->m_Port = std::to_string(g_NetServer->GetPublicPort()); connectionData->m_UseSTUN = g_NetServer->GetUseSTUN() ? "true" : ""; response.addExtension(connectionData); m_client->send(response); } } else if (iq.subtype() == gloox::IQ::Error) CreateGUIMessage("system", "error", std::time(nullptr), "text", iq.error_error()); else { CreateGUIMessage("system", "error", std::time(nullptr), "text", wstring_from_utf8(g_L10n.Translate("unknown subtype (see logs)"))); LOGMESSAGE("unknown subtype '%s'", tag_name(iq).c_str()); } return true; } /** * Update local data when a user changes presence. */ void XmppClient::handleMUCParticipantPresence(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::MUCRoomParticipant participant, const glooxwrapper::Presence& presence) { const glooxwrapper::string& nick = participant.nick->resource(); if (presence.presence() == gloox::Presence::Unavailable) { if (!participant.newNick.empty() && (participant.flags & (gloox::UserNickChanged | gloox::UserSelf))) { // we have a nick change if (m_PlayerMap.find(participant.newNick) == m_PlayerMap.end()) m_PlayerMap.emplace( std::piecewise_construct, std::forward_as_tuple(participant.newNick), std::forward_as_tuple(presence.presence(), participant.role, std::move(m_PlayerMap.at(nick).m_Rating))); else LOGERROR("Nickname changed to an existing nick!"); DbgXMPP(nick << " is now known as " << participant.newNick); CreateGUIMessage( "chat", "nick", std::time(nullptr), "oldnick", nick, "newnick", participant.newNick); } else if (participant.flags & gloox::UserKicked) { DbgXMPP(nick << " was kicked. Reason: " << participant.reason); CreateGUIMessage( "chat", "kicked", std::time(nullptr), "nick", nick, "reason", participant.reason); } else if (participant.flags & gloox::UserBanned) { DbgXMPP(nick << " was banned. Reason: " << participant.reason); CreateGUIMessage( "chat", "banned", std::time(nullptr), "nick", nick, "reason", participant.reason); } else { DbgXMPP(nick << " left the room (flags " << participant.flags << ")"); CreateGUIMessage( "chat", "leave", std::time(nullptr), "nick", nick); } m_PlayerMap.erase(nick); } else { const PlayerMap::iterator it = m_PlayerMap.find(nick); /* During the initialization process, we receive join messages for everyone * currently in the room. We don't want to display these, so we filter them * out. We will always be the last to join during initialization. */ if (!m_initialLoadComplete) { if (m_mucRoom->nick() == nick) m_initialLoadComplete = true; } else if (it == m_PlayerMap.end()) { CreateGUIMessage( "chat", "join", std::time(nullptr), "nick", nick); } else if (it->second.m_Role != participant.role) { CreateGUIMessage( "chat", "role", std::time(nullptr), "nick", nick, "oldrole", it->second.m_Role, "newrole", participant.role); } else { // Don't create a GUI message for regular presence changes, because // several hundreds of them accumulate during a match, impacting performance terribly and // the only way they are used is to determine whether to update the playerlist. } DbgXMPP( nick << " is in the room, " "presence: " << GetPresenceString(presence.presence()) << ", " "role: "<< GetRoleString(participant.role)); if (it == m_PlayerMap.end()) { m_PlayerMap.emplace( std::piecewise_construct, std::forward_as_tuple(nick), std::forward_as_tuple(presence.presence(), participant.role, std::string())); } else { it->second.m_Presence = presence.presence(); it->second.m_Role = participant.role; } } m_PlayerMapUpdate = true; } /** * Update local cache when subject changes. */ void XmppClient::handleMUCSubject(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::string& nick, const glooxwrapper::string& subject) { m_Subject = wstring_from_utf8(subject.to_string()); CreateGUIMessage( "chat", "subject", std::time(nullptr), "nick", nick, "subject", m_Subject); } /** * Get current subject. */ const std::wstring& XmppClient::GetSubject() { return m_Subject; } /** * Request nick change, real change via mucRoomHandler. * * @param nick Desired nickname */ void XmppClient::SetNick(const std::string& nick) { m_mucRoom->setNick(nick); } /** * Get current nickname. */ -std::string XmppClient::GetNick() +std::string XmppClient::GetNick() const { return m_mucRoom->nick().to_string(); } +std::string XmppClient::GetJID() const +{ + return m_client->getJID().to_string(); +} + /** * Kick a player from the current room. * * @param nick Nickname to be kicked * @param reason Reason the player was kicked */ void XmppClient::kick(const std::string& nick, const std::string& reason) { m_mucRoom->kick(nick, reason); } /** * Ban a player from the current room. * * @param nick Nickname to be banned * @param reason Reason the player was banned */ void XmppClient::ban(const std::string& nick, const std::string& reason) { m_mucRoom->ban(nick, reason); } /** * Change the xmpp presence of the client. * * @param presence A string containing the desired presence */ void XmppClient::SetPresence(const std::string& presence) { #define IF(x,y) if (presence == x) m_mucRoom->setPresence(gloox::Presence::y) IF("available", Available); else IF("chat", Chat); else IF("away", Away); else IF("playing", DND); else IF("offline", Unavailable); // The others are not to be set #undef IF else LOGERROR("Unknown presence '%s'", presence.c_str()); } /** * Get the current xmpp presence of the given nick. */ const char* XmppClient::GetPresence(const std::string& nick) { const PlayerMap::iterator it = m_PlayerMap.find(nick); if (it == m_PlayerMap.end()) return "offline"; return GetPresenceString(it->second.m_Presence); } /** * Get the current xmpp role of the given nick. */ const char* XmppClient::GetRole(const std::string& nick) { const PlayerMap::iterator it = m_PlayerMap.find(nick); if (it == m_PlayerMap.end()) return ""; return GetRoleString(it->second.m_Role); } /** * Get the most recent received rating of the given nick. * Notice that this doesn't request a rating profile if it hasn't been received yet. */ std::wstring XmppClient::GetRating(const std::string& nick) { const PlayerMap::iterator it = m_PlayerMap.find(nick); if (it == m_PlayerMap.end()) return std::wstring(); return wstring_from_utf8(it->second.m_Rating.to_string()); } /***************************************************** * Utilities * *****************************************************/ /** * Parse and return the timestamp of a historic chat message and return the current time for new chat messages. * Historic chat messages are implement as DelayedDelivers as specified in XEP-0203. * Hence, their timestamp MUST be in UTC and conform to the DateTime format XEP-0082. * * @returns Seconds since the epoch. */ std::time_t XmppClient::ComputeTimestamp(const glooxwrapper::Message& msg) { // Only historic messages contain a timestamp! if (!msg.when()) return std::time(nullptr); // The locale is irrelevant, because the XMPP date format doesn't contain written month names for (const std::string& format : std::vector{ "Y-M-d'T'H:m:sZ", "Y-M-d'T'H:m:s.SZ" }) { UDate dateTime = g_L10n.ParseDateTime(msg.when()->stamp().to_string(), format, icu::Locale::getUS()); if (dateTime) return dateTime / 1000.0; } return std::time(nullptr); } /** * Convert a gloox presence type to an untranslated string literal to be used as an identifier by the scripts. */ const char* XmppClient::GetPresenceString(const gloox::Presence::PresenceType presenceType) { switch (presenceType) { #define CASE(X,Y) case gloox::Presence::X: return Y CASE(Available, "available"); CASE(Chat, "chat"); CASE(Away, "away"); CASE(DND, "playing"); CASE(XA, "away"); CASE(Unavailable, "offline"); CASE(Probe, "probe"); CASE(Error, "error"); CASE(Invalid, "invalid"); default: LOGERROR("Unknown presence type '%d'", static_cast(presenceType)); return ""; #undef CASE } } /** * Convert a gloox role type to an untranslated string literal to be used as an identifier by the scripts. */ const char* XmppClient::GetRoleString(const gloox::MUCRoomRole role) { switch (role) { #define CASE(X, Y) case gloox::X: return Y CASE(RoleNone, "none"); CASE(RoleVisitor, "visitor"); CASE(RoleParticipant, "participant"); CASE(RoleModerator, "moderator"); CASE(RoleInvalid, "invalid"); default: LOGERROR("Unknown role type '%d'", static_cast(role)); return ""; #undef CASE } } /** * Translates a gloox certificate error codes, i.e. gloox certificate statuses except CertOk. * Keep in sync with specifications. */ std::string XmppClient::CertificateErrorToString(gloox::CertStatus status) { std::map certificateErrorStrings = { { gloox::CertInvalid, g_L10n.Translate("The certificate is not trusted.") }, { gloox::CertSignerUnknown, g_L10n.Translate("The certificate hasn't got a known issuer.") }, { gloox::CertRevoked, g_L10n.Translate("The certificate has been revoked.") }, { gloox::CertExpired, g_L10n.Translate("The certificate has expired.") }, { gloox::CertNotActive, g_L10n.Translate("The certificate is not yet active.") }, { gloox::CertWrongPeer, g_L10n.Translate("The certificate has not been issued for the peer connected to.") }, { gloox::CertSignerNotCa, g_L10n.Translate("The certificate signer is not a certificate authority.") } }; std::string result; for (std::map::iterator it = certificateErrorStrings.begin(); it != certificateErrorStrings.end(); ++it) if (status & it->first) result += "\n" + it->second; return result; } /** * Convert a gloox stanza error type to string. * Keep in sync with Gloox documentation * * @param err Error to be converted * @return Converted error string */ std::string XmppClient::StanzaErrorToString(gloox::StanzaError err) { #define CASE(X, Y) case gloox::X: return Y #define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")" switch (err) { CASE(StanzaErrorUndefined, g_L10n.Translate("No error")); DEBUG_CASE(StanzaErrorBadRequest, "Server received malformed XML"); CASE(StanzaErrorConflict, g_L10n.Translate("Player already logged in")); DEBUG_CASE(StanzaErrorFeatureNotImplemented, "Server does not implement requested feature"); CASE(StanzaErrorForbidden, g_L10n.Translate("Forbidden")); DEBUG_CASE(StanzaErrorGone, "Unable to find message receipiant"); CASE(StanzaErrorInternalServerError, g_L10n.Translate("Internal server error")); DEBUG_CASE(StanzaErrorItemNotFound, "Message receipiant does not exist"); DEBUG_CASE(StanzaErrorJidMalformed, "JID (XMPP address) malformed"); DEBUG_CASE(StanzaErrorNotAcceptable, "Receipiant refused message. Possible policy issue"); CASE(StanzaErrorNotAllowed, g_L10n.Translate("Not allowed")); CASE(StanzaErrorNotAuthorized, g_L10n.Translate("Not authorized")); DEBUG_CASE(StanzaErrorNotModified, "Requested item has not changed since last request"); DEBUG_CASE(StanzaErrorPaymentRequired, "This server requires payment"); CASE(StanzaErrorRecipientUnavailable, g_L10n.Translate("Recipient temporarily unavailable")); DEBUG_CASE(StanzaErrorRedirect, "Request redirected"); CASE(StanzaErrorRegistrationRequired, g_L10n.Translate("Registration required")); DEBUG_CASE(StanzaErrorRemoteServerNotFound, "Remote server not found"); DEBUG_CASE(StanzaErrorRemoteServerTimeout, "Remote server timed out"); DEBUG_CASE(StanzaErrorResourceConstraint, "The recipient is unable to process the message due to resource constraints"); CASE(StanzaErrorServiceUnavailable, g_L10n.Translate("Service unavailable")); DEBUG_CASE(StanzaErrorSubscribtionRequired, "Service requires subscription"); DEBUG_CASE(StanzaErrorUnexpectedRequest, "Attempt to send from invalid stanza address"); DEBUG_CASE(StanzaErrorUnknownSender, "Invalid 'from' address"); default: return g_L10n.Translate("Unknown error"); } #undef DEBUG_CASE #undef CASE } /** * Convert a gloox connection error enum to string * Keep in sync with Gloox documentation * * @param err Error to be converted * @return Converted error string */ std::string XmppClient::ConnectionErrorToString(gloox::ConnectionError err) { #define CASE(X, Y) case gloox::X: return Y #define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")" switch (err) { CASE(ConnNoError, g_L10n.Translate("No error")); CASE(ConnStreamError, g_L10n.Translate("Stream error")); CASE(ConnStreamVersionError, g_L10n.Translate("The incoming stream version is unsupported")); CASE(ConnStreamClosed, g_L10n.Translate("The stream has been closed by the server")); DEBUG_CASE(ConnProxyAuthRequired, "The HTTP/SOCKS5 proxy requires authentication"); DEBUG_CASE(ConnProxyAuthFailed, "HTTP/SOCKS5 proxy authentication failed"); DEBUG_CASE(ConnProxyNoSupportedAuth, "The HTTP/SOCKS5 proxy requires an unsupported authentication mechanism"); CASE(ConnIoError, g_L10n.Translate("An I/O error occurred")); DEBUG_CASE(ConnParseError, "An XML parse error occurred"); CASE(ConnConnectionRefused, g_L10n.Translate("The connection was refused by the server")); CASE(ConnDnsError, g_L10n.Translate("Resolving the server's hostname failed")); CASE(ConnOutOfMemory, g_L10n.Translate("This system is out of memory")); DEBUG_CASE(ConnNoSupportedAuth, "The authentication mechanisms the server offered are not supported or no authentication mechanisms were available"); CASE(ConnTlsFailed, g_L10n.Translate("The server's certificate could not be verified or the TLS handshake did not complete successfully")); CASE(ConnTlsNotAvailable, g_L10n.Translate("The server did not offer required TLS encryption")); DEBUG_CASE(ConnCompressionFailed, "Negotiation/initializing compression failed"); CASE(ConnAuthenticationFailed, g_L10n.Translate("Authentication failed. Incorrect password or account does not exist")); CASE(ConnUserDisconnected, g_L10n.Translate("The user or system requested a disconnect")); CASE(ConnNotConnected, g_L10n.Translate("There is no active connection")); default: return g_L10n.Translate("Unknown error"); } #undef DEBUG_CASE #undef CASE } /** * Convert a gloox registration result enum to string * Keep in sync with Gloox documentation * * @param err Enum to be converted * @return Converted string */ std::string XmppClient::RegistrationResultToString(gloox::RegistrationResult res) { #define CASE(X, Y) case gloox::X: return Y #define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")" switch (res) { CASE(RegistrationSuccess, g_L10n.Translate("Your account has been successfully registered")); CASE(RegistrationNotAcceptable, g_L10n.Translate("Not all necessary information provided")); CASE(RegistrationConflict, g_L10n.Translate("Username already exists")); DEBUG_CASE(RegistrationNotAuthorized, "Account removal timeout or insufficiently secure channel for password change"); DEBUG_CASE(RegistrationBadRequest, "Server received an incomplete request"); DEBUG_CASE(RegistrationForbidden, "Registration forbidden"); DEBUG_CASE(RegistrationRequired, "Account cannot be removed as it does not exist"); DEBUG_CASE(RegistrationUnexpectedRequest, "This client is unregistered with the server"); DEBUG_CASE(RegistrationNotAllowed, "Server does not permit password changes"); default: return ""; } #undef DEBUG_CASE #undef CASE } void XmppClient::SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJIDStr) { DbgXMPP("SendStunEndpointToHost " << hostJIDStr); char ipStr[256] = "(error)"; ENetAddress addr; addr.host = ntohl(stunEndpoint.ip); enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr)); glooxwrapper::JID hostJID(hostJIDStr); glooxwrapper::Jingle::Session session = m_sessionManager->createSession(hostJID); session.sessionInitiate(ipStr, stunEndpoint.port); } void XmppClient::handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle) { if (action == gloox::Jingle::SessionInitiate) handleSessionInitiation(session, jingle); } void XmppClient::handleSessionInitiation(glooxwrapper::Jingle::Session& UNUSED(session), const glooxwrapper::Jingle::Session::Jingle& jingle) { glooxwrapper::Jingle::ICEUDP::Candidate candidate = jingle.getCandidate(); if (candidate.ip.empty()) { LOGERROR("Failed to retrieve Jingle candidate"); return; } if (!g_NetServer) { LOGERROR("Received STUN connection request, but not hosting currently!"); return; } g_NetServer->SendHolePunchingMessage(candidate.ip.to_string(), candidate.port); } Index: ps/trunk/source/lobby/XmppClient.h =================================================================== --- ps/trunk/source/lobby/XmppClient.h (revision 25406) +++ ps/trunk/source/lobby/XmppClient.h (revision 25407) @@ -1,206 +1,207 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef XXXMPPCLIENT_H #define XXXMPPCLIENT_H #include "IXmppClient.h" #include "glooxwrapper/glooxwrapper.h" #include #include #include #include class ScriptInterface; namespace glooxwrapper { class Client; struct CertInfo; } class XmppClient : public IXmppClient, public glooxwrapper::ConnectionListener, public glooxwrapper::MUCRoomHandler, public glooxwrapper::IqHandler, public glooxwrapper::RegistrationHandler, public glooxwrapper::MessageHandler, public glooxwrapper::Jingle::SessionHandler { NONCOPYABLE(XmppClient); private: // Components glooxwrapper::Client* m_client; glooxwrapper::MUCRoom* m_mucRoom; glooxwrapper::Registration* m_registration; glooxwrapper::SessionManager* m_sessionManager; // Account infos std::string m_username; std::string m_password; std::string m_server; std::string m_room; std::string m_nick; std::string m_xpartamuppId; std::string m_echelonId; // Security std::string m_connectionDataJid; std::string m_connectionDataIqId; // State gloox::CertStatus m_certStatus; bool m_initialLoadComplete; bool m_isConnected; public: // Basic XmppClient(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize = 0, const bool regOpt = false); virtual ~XmppClient(); // JS::Heap is better for GC performance than JS::PersistentRooted static void Trace(JSTracer *trc, void *data) { static_cast(data)->TraceMember(trc); } void TraceMember(JSTracer *trc); // Network void connect(); void disconnect(); bool isConnected(); void recv(); void SendIqGetBoardList(); void SendIqGetProfile(const std::string& player); void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data); void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data); void SendIqGetConnectionData(const std::string& jid, const std::string& password); void SendIqUnregisterGame(); void SendIqChangeStateGame(const std::string& nbp, const std::string& players); void SendIqLobbyAuth(const std::string& to, const std::string& token); void SetNick(const std::string& nick); - std::string GetNick(); + std::string GetNick() const; + std::string GetJID() const; void kick(const std::string& nick, const std::string& reason); void ban(const std::string& nick, const std::string& reason); void SetPresence(const std::string& presence); const char* GetPresence(const std::string& nickname); const char* GetRole(const std::string& nickname); std::wstring GetRating(const std::string& nickname); const std::wstring& GetSubject(); JS::Value GUIGetPlayerList(const ScriptInterface& scriptInterface); JS::Value GUIGetGameList(const ScriptInterface& scriptInterface); JS::Value GUIGetBoardList(const ScriptInterface& scriptInterface); JS::Value GUIGetProfile(const ScriptInterface& scriptInterface); void SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJID); /** * Convert gloox values to string or time. */ static const char* GetPresenceString(const gloox::Presence::PresenceType presenceType); static const char* GetRoleString(const gloox::MUCRoomRole role); static std::string StanzaErrorToString(gloox::StanzaError err); static std::string RegistrationResultToString(gloox::RegistrationResult res); static std::string ConnectionErrorToString(gloox::ConnectionError err); static std::string CertificateErrorToString(gloox::CertStatus status); static std::time_t ComputeTimestamp(const glooxwrapper::Message& msg); protected: /* Xmpp handlers */ /* MUC handlers */ virtual void handleMUCParticipantPresence(glooxwrapper::MUCRoom& room, const glooxwrapper::MUCRoomParticipant, const glooxwrapper::Presence&); virtual void handleMUCError(glooxwrapper::MUCRoom& room, gloox::StanzaError); virtual void handleMUCMessage(glooxwrapper::MUCRoom& room, const glooxwrapper::Message& msg, bool priv); virtual void handleMUCSubject(glooxwrapper::MUCRoom& room, const glooxwrapper::string& nick, const glooxwrapper::string& subject); /* MUC handlers not supported by glooxwrapper */ // virtual bool handleMUCRoomCreation(glooxwrapper::MUCRoom*) {return false;} // virtual void handleMUCInviteDecline(glooxwrapper::MUCRoom*, const glooxwrapper::JID&, const std::string&) {} // virtual void handleMUCInfo(glooxwrapper::MUCRoom*, int, const std::string&, const glooxwrapper::DataForm*) {} // virtual void handleMUCItems(glooxwrapper::MUCRoom*, const std::list >&) {} /* Log handler */ virtual void handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message); /* ConnectionListener handlers*/ virtual void onConnect(); virtual void onDisconnect(gloox::ConnectionError e); virtual bool onTLSConnect(const glooxwrapper::CertInfo& info); /* Iq Handlers */ virtual bool handleIq(const glooxwrapper::IQ& iq); virtual void handleIqID(const glooxwrapper::IQ&, int) {} /* Registration Handlers */ virtual void handleRegistrationFields(const glooxwrapper::JID& /*from*/, int fields, glooxwrapper::string instructions ); virtual void handleRegistrationResult(const glooxwrapper::JID& /*from*/, gloox::RegistrationResult result); virtual void handleAlreadyRegistered(const glooxwrapper::JID& /*from*/); virtual void handleDataForm(const glooxwrapper::JID& /*from*/, const glooxwrapper::DataForm& /*form*/); virtual void handleOOB(const glooxwrapper::JID& /*from*/, const glooxwrapper::OOB& oob); /* Message Handler */ virtual void handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession* session); /* Session Handler */ virtual void handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle); virtual void handleSessionInitiation(glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle); public: JS::Value GuiPollNewMessages(const ScriptInterface& scriptInterface); JS::Value GuiPollHistoricMessages(const ScriptInterface& scriptInterface); bool GuiPollHasPlayerListUpdate(); void SendMUCMessage(const std::string& message); protected: template void CreateGUIMessage( const std::string& type, const std::string& level, const std::time_t time, Args const&... args); private: struct SPlayer { SPlayer(const gloox::Presence::PresenceType presence, const gloox::MUCRoomRole role, const glooxwrapper::string& rating) : m_Presence(presence), m_Role(role), m_Rating(rating) { } gloox::Presence::PresenceType m_Presence; gloox::MUCRoomRole m_Role; glooxwrapper::string m_Rating; }; using PlayerMap = std::map; /// Map of players PlayerMap m_PlayerMap; /// Whether or not the playermap has changed since the last time the GUI checked. bool m_PlayerMapUpdate; /// List of games std::vector m_GameList; /// List of rankings std::vector m_BoardList; /// Profile data std::vector m_Profile; /// ScriptInterface to root the values const ScriptInterface* m_ScriptInterface; /// Queue of messages for the GUI std::deque > m_GuiMessageQueue; /// Cache of all GUI messages received since the login std::vector > m_HistoricGuiMessages; /// Current room subject/topic. std::wstring m_Subject; }; #endif // XMPPCLIENT_H Index: ps/trunk/source/lobby/glooxwrapper/glooxwrapper.cpp =================================================================== --- ps/trunk/source/lobby/glooxwrapper/glooxwrapper.cpp (revision 25406) +++ ps/trunk/source/lobby/glooxwrapper/glooxwrapper.cpp (revision 25407) @@ -1,895 +1,900 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "glooxwrapper.h" #include #include #include #include #if OS_WIN const std::string gloox::EmptyString = ""; #endif void* glooxwrapper::glooxwrapper_alloc(size_t size) { void* p = malloc(size); if (p == NULL) throw std::bad_alloc(); return p; } void glooxwrapper::glooxwrapper_free(void* p) { free(p); } namespace glooxwrapper { class ConnectionListenerWrapper : public gloox::ConnectionListener { glooxwrapper::ConnectionListener* m_Wrapped; public: ConnectionListenerWrapper(glooxwrapper::ConnectionListener* wrapped) : m_Wrapped(wrapped) {} virtual void onConnect() { m_Wrapped->onConnect(); } virtual void onDisconnect(gloox::ConnectionError e) { m_Wrapped->onDisconnect(e); } virtual bool onTLSConnect(const gloox::CertInfo& info) { glooxwrapper::CertInfo infoWrapped; #define COPY(n) infoWrapped.n = info.n COPY(status); COPY(chain); COPY(issuer); COPY(server); COPY(date_from); COPY(date_to); COPY(protocol); COPY(cipher); COPY(mac); COPY(compression); #undef COPY return m_Wrapped->onTLSConnect(infoWrapped); } }; class IqHandlerWrapper : public gloox::IqHandler { glooxwrapper::IqHandler* m_Wrapped; public: IqHandlerWrapper(glooxwrapper::IqHandler* wrapped) : m_Wrapped(wrapped) {} virtual bool handleIq(const gloox::IQ& iq) { glooxwrapper::IQ iqWrapper(iq); return m_Wrapped->handleIq(iqWrapper); } virtual void handleIqID(const gloox::IQ& iq, int context) { glooxwrapper::IQ iqWrapper(iq); m_Wrapped->handleIqID(iqWrapper, context); } }; class MessageHandlerWrapper : public gloox::MessageHandler { glooxwrapper::MessageHandler* m_Wrapped; public: MessageHandlerWrapper(glooxwrapper::MessageHandler* wrapped) : m_Wrapped(wrapped) {} virtual void handleMessage(const gloox::Message& msg, gloox::MessageSession* UNUSED(session)) { /* MessageSession not supported */ glooxwrapper::Message msgWrapper(const_cast(&msg), false); m_Wrapped->handleMessage(msgWrapper, NULL); } }; class MUCRoomHandlerWrapper : public gloox::MUCRoomHandler { glooxwrapper::MUCRoomHandler* m_Wrapped; public: MUCRoomHandlerWrapper(glooxwrapper::MUCRoomHandler* wrapped) : m_Wrapped(wrapped) {} virtual ~MUCRoomHandlerWrapper() {} virtual void handleMUCParticipantPresence(gloox::MUCRoom* room, const gloox::MUCRoomParticipant participant, const gloox::Presence& presence) { glooxwrapper::MUCRoom roomWrapper(room, false); glooxwrapper::MUCRoomParticipant part; glooxwrapper::JID nick(*participant.nick); glooxwrapper::JID jid(*participant.jid); glooxwrapper::JID actor(*participant.actor); glooxwrapper::JID alternate(*participant.alternate); part.nick = participant.nick ? &nick : NULL; part.affiliation = participant.affiliation; part.role = participant.role; part.jid = participant.jid ? &jid : NULL; part.flags = participant.flags; part.reason = participant.reason; part.actor = participant.actor ? &actor : NULL; part.newNick = participant.newNick; part.status = participant.status; part.alternate = participant.alternate ? &alternate : NULL; m_Wrapped->handleMUCParticipantPresence(roomWrapper, part, glooxwrapper::Presence(presence.presence())); /* gloox 1.0 leaks some JIDs (fixed in 1.0.1), so clean them up */ #if GLOOXVERSION == 0x10000 delete participant.jid; delete participant.actor; delete participant.alternate; #endif } virtual void handleMUCMessage(gloox::MUCRoom* room, const gloox::Message& msg, bool priv) { glooxwrapper::MUCRoom roomWrapper(room, false); glooxwrapper::Message msgWrapper(const_cast(&msg), false); m_Wrapped->handleMUCMessage(roomWrapper, msgWrapper, priv); } virtual bool handleMUCRoomCreation(gloox::MUCRoom* UNUSED(room)) { /* Not supported */ return false; } virtual void handleMUCSubject(gloox::MUCRoom* room, const std::string& nick, const std::string& subject) { glooxwrapper::MUCRoom roomWrapper(room, false); m_Wrapped->handleMUCSubject(roomWrapper, nick, subject); } virtual void handleMUCInviteDecline(gloox::MUCRoom* UNUSED(room), const gloox::JID& UNUSED(invitee), const std::string& UNUSED(reason)) { /* Not supported */ } virtual void handleMUCError(gloox::MUCRoom* room, gloox::StanzaError error) { glooxwrapper::MUCRoom roomWrapper(room, false); m_Wrapped->handleMUCError(roomWrapper, error); } virtual void handleMUCInfo(gloox::MUCRoom* UNUSED(room), int UNUSED(features), const std::string& UNUSED(name), const gloox::DataForm* UNUSED(infoForm)) { /* Not supported */ } virtual void handleMUCItems(gloox::MUCRoom* UNUSED(room), const gloox::Disco::ItemList& UNUSED(items)) { /* Not supported */ } }; class RegistrationHandlerWrapper : public gloox::RegistrationHandler { glooxwrapper::RegistrationHandler* m_Wrapped; public: RegistrationHandlerWrapper(glooxwrapper::RegistrationHandler* wrapped) : m_Wrapped(wrapped) {} virtual void handleRegistrationFields(const gloox::JID& from, int fields, std::string instructions) { glooxwrapper::JID fromWrapped(from); m_Wrapped->handleRegistrationFields(fromWrapped, fields, instructions); } virtual void handleAlreadyRegistered(const gloox::JID& from) { glooxwrapper::JID fromWrapped(from); m_Wrapped->handleAlreadyRegistered(fromWrapped); } virtual void handleRegistrationResult(const gloox::JID& from, gloox::RegistrationResult regResult) { glooxwrapper::JID fromWrapped(from); m_Wrapped->handleRegistrationResult(fromWrapped, regResult); } virtual void handleDataForm(const gloox::JID& UNUSED(from), const gloox::DataForm& UNUSED(form)) { /* DataForm not supported */ } virtual void handleOOB(const gloox::JID& UNUSED(from), const gloox::OOB& UNUSED(oob)) { /* OOB not supported */ } }; class StanzaExtensionWrapper : public gloox::StanzaExtension { public: const glooxwrapper::StanzaExtension* m_Wrapped; bool m_Owned; std::string m_FilterString; StanzaExtensionWrapper(const glooxwrapper::StanzaExtension* wrapped, bool owned) : gloox::StanzaExtension(wrapped->extensionType()), m_Wrapped(wrapped), m_Owned(owned) { m_FilterString = m_Wrapped->filterString().to_string(); } ~StanzaExtensionWrapper() { if (m_Owned) delete m_Wrapped; } virtual const std::string& filterString() const { return m_FilterString; } virtual gloox::StanzaExtension* newInstance(const gloox::Tag* tag) const { glooxwrapper::Tag* tagWrapper = glooxwrapper::Tag::allocate(const_cast(tag), false); gloox::StanzaExtension* ret = new StanzaExtensionWrapper(m_Wrapped->newInstance(tagWrapper), true); glooxwrapper::Tag::free(tagWrapper); return ret; } virtual gloox::Tag* tag() const { glooxwrapper::Tag* wrapper = m_Wrapped->tag(); gloox::Tag* ret = wrapper->stealWrapped(); glooxwrapper::Tag::free(wrapper); return ret; } virtual gloox::StanzaExtension* clone() const { return new StanzaExtensionWrapper(m_Wrapped->clone(), true); } }; class SessionHandlerWrapper : public gloox::Jingle::SessionHandler { public: glooxwrapper::Jingle::SessionHandler* m_Wrapped; bool m_Owned; SessionHandlerWrapper(glooxwrapper::Jingle::SessionHandler* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) {} ~SessionHandlerWrapper() { if (m_Owned) delete m_Wrapped; } virtual void handleSessionAction(gloox::Jingle::Action action, gloox::Jingle::Session* session, const gloox::Jingle::Session::Jingle* jingle) { glooxwrapper::Jingle::Session sessionWrapper(session, false); glooxwrapper::Jingle::Session::Jingle jingleWrapper(jingle, false); m_Wrapped->handleSessionAction(action, sessionWrapper, jingleWrapper); } virtual void handleSessionActionError(gloox::Jingle::Action UNUSED(action), gloox::Jingle::Session* UNUSED(session), const gloox::Error* UNUSED(error)) { } virtual void handleIncomingSession(gloox::Jingle::Session* UNUSED(session)) { } }; class ClientImpl { public: // List of registered callback wrappers, to get deleted when Client is deleted std::list > m_ConnectionListeners; std::list > m_MessageHandlers; std::list > m_IqHandlers; }; static const std::string XMLNS = "xmlns"; static const std::string XMLNS_JINGLE_0AD_GAME = "urn:xmpp:jingle:apps:0ad-game:1"; } // namespace glooxwrapper glooxwrapper::Client::Client(const string& server) { m_Wrapped = new gloox::Client(server.to_string()); m_DiscoWrapper = new glooxwrapper::Disco(m_Wrapped->disco()); m_Impl = new ClientImpl; } glooxwrapper::Client::Client(const JID& jid, const string& password, int port) { m_Wrapped = new gloox::Client(jid.getWrapped(), password.to_string(), port); m_DiscoWrapper = new glooxwrapper::Disco(m_Wrapped->disco()); m_Impl = new ClientImpl; } glooxwrapper::Client::~Client() { delete m_Wrapped; delete m_DiscoWrapper; delete m_Impl; } bool glooxwrapper::Client::connect(bool block) { return m_Wrapped->connect(block); } gloox::ConnectionError glooxwrapper::Client::recv(int timeout) { return m_Wrapped->recv(timeout); } const glooxwrapper::string glooxwrapper::Client::getID() const { return m_Wrapped->getID(); } +const glooxwrapper::string glooxwrapper::Client::getJID() const +{ + return m_Wrapped->jid().full(); +} + void glooxwrapper::Client::send(const IQ& iq) { m_Wrapped->send(iq.getWrapped()); } void glooxwrapper::Client::setTls(gloox::TLSPolicy tls) { m_Wrapped->setTls(tls); } void glooxwrapper::Client::setCompression(bool compression) { m_Wrapped->setCompression(compression); } void glooxwrapper::Client::setSASLMechanisms(int mechanisms) { m_Wrapped->setSASLMechanisms(mechanisms); } void glooxwrapper::Client::disconnect() { m_Wrapped->disconnect(); } void glooxwrapper::Client::registerStanzaExtension(glooxwrapper::StanzaExtension* ext) { // ~StanzaExtensionFactory() deletes this new StanzaExtensionWrapper m_Wrapped->registerStanzaExtension(new StanzaExtensionWrapper(ext, true)); } void glooxwrapper::Client::registerConnectionListener(glooxwrapper::ConnectionListener* hnd) { gloox::ConnectionListener* listener = new ConnectionListenerWrapper(hnd); m_Wrapped->registerConnectionListener(listener); m_Impl->m_ConnectionListeners.push_back(shared_ptr(listener)); } void glooxwrapper::Client::registerMessageHandler(glooxwrapper::MessageHandler* hnd) { gloox::MessageHandler* handler = new MessageHandlerWrapper(hnd); m_Wrapped->registerMessageHandler(handler); m_Impl->m_MessageHandlers.push_back(shared_ptr(handler)); } void glooxwrapper::Client::registerIqHandler(glooxwrapper::IqHandler* ih, int exttype) { gloox::IqHandler* handler = new IqHandlerWrapper(ih); m_Wrapped->registerIqHandler(handler, exttype); m_Impl->m_IqHandlers.push_back(shared_ptr(handler)); } bool glooxwrapper::Client::removePresenceExtension(int type) { return m_Wrapped->removePresenceExtension(type); } void glooxwrapper::Client::setPresence(gloox::Presence::PresenceType pres, int priority, const string& status) { m_Wrapped->setPresence(pres, priority, status.to_string()); } glooxwrapper::DelayedDelivery::DelayedDelivery(const gloox::DelayedDelivery* wrapped) { m_Wrapped = wrapped; } const glooxwrapper::string glooxwrapper::DelayedDelivery::stamp() const { return m_Wrapped->stamp(); } glooxwrapper::Disco::Disco(gloox::Disco* wrapped) { m_Wrapped = wrapped; } void glooxwrapper::Disco::setVersion(const string& name, const string& version, const string& os) { m_Wrapped->setVersion(name.to_string(), version.to_string(), os.to_string()); } void glooxwrapper::Disco::setIdentity(const string& category, const string& type, const string& name) { m_Wrapped->setIdentity(category.to_string(), type.to_string(), name.to_string()); } glooxwrapper::IQ::IQ(gloox::IQ::IqType type, const JID& to, const string& id) { m_Wrapped = new gloox::IQ(type, to.getWrapped(), id.to_string()); m_Owned = true; } glooxwrapper::IQ::~IQ() { if (m_Owned) delete m_Wrapped; } void glooxwrapper::IQ::addExtension(const glooxwrapper::StanzaExtension* se) { m_Wrapped->addExtension(new StanzaExtensionWrapper(se, true)); } const glooxwrapper::StanzaExtension* glooxwrapper::IQ::findExtension(int type) const { const gloox::StanzaExtension* ext = m_Wrapped->findExtension(type); if (!ext) return NULL; return static_cast(ext)->m_Wrapped; } gloox::StanzaError glooxwrapper::IQ::error_error() const { const gloox::Error* error = m_Wrapped->error(); if (!error) return gloox::StanzaErrorInternalServerError; return error->error(); } glooxwrapper::Tag* glooxwrapper::IQ::tag() const { return Tag::allocate(m_Wrapped->tag(), true); } gloox::IQ::IqType glooxwrapper::IQ::subtype() const { return m_Wrapped->subtype(); } const glooxwrapper::string glooxwrapper::IQ::id() const { return m_Wrapped->id(); } const gloox::JID& glooxwrapper::IQ::from() const { return m_Wrapped->from(); } glooxwrapper::JID::JID() { m_Wrapped = new gloox::JID(); m_Owned = true; } glooxwrapper::JID::JID(const glooxwrapper::string& jid) { m_Wrapped = new gloox::JID(jid.to_string()); m_Owned = true; } void glooxwrapper::JID::init(const char* data, size_t len) { m_Wrapped = new gloox::JID(std::string(data, data+len)); m_Owned = true; } glooxwrapper::JID::~JID() { if (m_Owned) delete m_Wrapped; } glooxwrapper::string glooxwrapper::JID::username() const { return m_Wrapped->username(); } glooxwrapper::string glooxwrapper::JID::resource() const { return m_Wrapped->resource(); }; glooxwrapper::Message::Message(gloox::Message* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned), m_From(m_Wrapped->from()), m_DelayedDelivery(m_Wrapped->when() ? new glooxwrapper::DelayedDelivery(m_Wrapped->when()) : nullptr) { } glooxwrapper::Message::~Message() { if (m_Owned) delete m_Wrapped; delete m_DelayedDelivery; } gloox::Message::MessageType glooxwrapper::Message::subtype() const { return m_Wrapped->subtype(); } const glooxwrapper::JID& glooxwrapper::Message::from() const { return m_From; } glooxwrapper::string glooxwrapper::Message::body() const { return m_Wrapped->body(); } glooxwrapper::string glooxwrapper::Message::subject(const string& lang) const { return m_Wrapped->subject(lang.to_string()); } glooxwrapper::string glooxwrapper::Message::thread() const { return m_Wrapped->thread(); } const glooxwrapper::DelayedDelivery* glooxwrapper::Message::when() const { return m_DelayedDelivery; } glooxwrapper::MUCRoom::MUCRoom(gloox::MUCRoom* room, bool owned) : m_Wrapped(room), m_Owned(owned), m_HandlerWrapper(nullptr) { } glooxwrapper::MUCRoom::MUCRoom(Client* parent, const JID& nick, MUCRoomHandler* mrh, MUCRoomConfigHandler* UNUSED(mrch)) { m_HandlerWrapper = new MUCRoomHandlerWrapper(mrh); m_Wrapped = new gloox::MUCRoom(parent ? parent->getWrapped() : NULL, nick.getWrapped(), m_HandlerWrapper); m_Owned = true; } glooxwrapper::MUCRoom::~MUCRoom() { if (m_Owned) delete m_Wrapped; delete m_HandlerWrapper; } const glooxwrapper::string glooxwrapper::MUCRoom::nick() const { return m_Wrapped->nick(); } const glooxwrapper::string glooxwrapper::MUCRoom::name() const { return m_Wrapped->name(); } const glooxwrapper::string glooxwrapper::MUCRoom::service() const { return m_Wrapped->service(); } void glooxwrapper::MUCRoom::join(gloox::Presence::PresenceType type, const string& status, int priority) { m_Wrapped->join(type, status.to_string(), priority); } void glooxwrapper::MUCRoom::leave(const string& msg) { m_Wrapped->leave(msg.to_string()); } void glooxwrapper::MUCRoom::send(const string& message) { m_Wrapped->send(message.to_string()); } void glooxwrapper::MUCRoom::setNick(const string& nick) { m_Wrapped->setNick(nick.to_string()); } void glooxwrapper::MUCRoom::setPresence(gloox::Presence::PresenceType presence, const string& msg) { m_Wrapped->setPresence(presence, msg.to_string()); } void glooxwrapper::MUCRoom::setRequestHistory(int value, gloox::MUCRoom::HistoryRequestType type) { m_Wrapped->setRequestHistory(value, type); } void glooxwrapper::MUCRoom::kick(const string& nick, const string& reason) { m_Wrapped->kick(nick.to_string(), reason.to_string()); } void glooxwrapper::MUCRoom::ban(const string& nick, const string& reason) { m_Wrapped->ban(nick.to_string(), reason.to_string()); } glooxwrapper::Registration::Registration(Client* parent) { m_Wrapped = new gloox::Registration(parent->getWrapped()); } glooxwrapper::Registration::~Registration() { delete m_Wrapped; } void glooxwrapper::Registration::fetchRegistrationFields() { m_Wrapped->fetchRegistrationFields(); } bool glooxwrapper::Registration::createAccount(int fields, const glooxwrapper::RegistrationFields& values) { gloox::RegistrationFields valuesUnwrapped; #define COPY(n) valuesUnwrapped.n = values.n.to_string() COPY(username); COPY(nick); COPY(password); COPY(name); COPY(first); COPY(last); COPY(email); COPY(address); COPY(city); COPY(state); COPY(zip); COPY(phone); COPY(url); COPY(date); COPY(misc); COPY(text); #undef COPY return m_Wrapped->createAccount(fields, valuesUnwrapped); } void glooxwrapper::Registration::registerRegistrationHandler(RegistrationHandler* rh) { gloox::RegistrationHandler* handler = new RegistrationHandlerWrapper(rh); m_Wrapped->registerRegistrationHandler(handler); m_RegistrationHandlers.push_back(shared_ptr(handler)); } glooxwrapper::Tag::Tag(const string& name) { m_Wrapped = new gloox::Tag(name.to_string()); m_Owned = true; } glooxwrapper::Tag::Tag(const string& name, const string& cdata) { m_Wrapped = new gloox::Tag(name.to_string(), cdata.to_string()); m_Owned = true; } glooxwrapper::Tag::~Tag() { if (m_Owned) delete m_Wrapped; } glooxwrapper::Tag* glooxwrapper::Tag::allocate(const string& name) { return new glooxwrapper::Tag(name); } glooxwrapper::Tag* glooxwrapper::Tag::allocate(const string& name, const string& cdata) { return new glooxwrapper::Tag(name, cdata); } glooxwrapper::Tag* glooxwrapper::Tag::allocate(gloox::Tag* wrapped, bool owned) { return new glooxwrapper::Tag(wrapped, owned); } void glooxwrapper::Tag::free(const glooxwrapper::Tag* tag) { delete tag; } bool glooxwrapper::Tag::addAttribute(const string& name, const string& value) { return m_Wrapped->addAttribute(name.to_string(), value.to_string()); } glooxwrapper::string glooxwrapper::Tag::findAttribute(const string& name) const { return m_Wrapped->findAttribute(name.to_string()); } glooxwrapper::Tag* glooxwrapper::Tag::clone() const { return new glooxwrapper::Tag(m_Wrapped->clone(), true); } glooxwrapper::string glooxwrapper::Tag::xmlns() const { return m_Wrapped->xmlns(); } bool glooxwrapper::Tag::setXmlns(const string& xmlns) { return m_Wrapped->setXmlns(xmlns.to_string()); } glooxwrapper::string glooxwrapper::Tag::xml() const { return m_Wrapped->xml(); } void glooxwrapper::Tag::addChild(Tag* child) { m_Wrapped->addChild(child->stealWrapped()); Tag::free(child); } glooxwrapper::string glooxwrapper::Tag::name() const { return m_Wrapped->name(); } glooxwrapper::string glooxwrapper::Tag::cdata() const { return m_Wrapped->cdata(); } const glooxwrapper::Tag* glooxwrapper::Tag::findTag_clone(const string& expression) const { const gloox::Tag* tag = m_Wrapped->findTag(expression.to_string()); if (!tag) return NULL; return new glooxwrapper::Tag(const_cast(tag), false); } glooxwrapper::ConstTagList glooxwrapper::Tag::findTagList_clone(const string& expression) const { glooxwrapper::ConstTagList tagListWrapper; for (const gloox::Tag* const& t : m_Wrapped->findTagList(expression.to_string())) tagListWrapper.push_back(new glooxwrapper::Tag(const_cast(t), false)); return tagListWrapper; } glooxwrapper::Jingle::Plugin::~Plugin() { if (m_Owned) delete m_Wrapped; } const glooxwrapper::Jingle::Plugin glooxwrapper::Jingle::Plugin::findPlugin(int type) const { return glooxwrapper::Jingle::Plugin(m_Wrapped->findPlugin(type), false); } glooxwrapper::Jingle::ICEUDP::Candidate glooxwrapper::Jingle::Session::Jingle::getCandidate() const { const gloox::Jingle::Content* content = static_cast(m_Wrapped->plugins().front()); if (!content) return glooxwrapper::Jingle::ICEUDP::Candidate(); const gloox::Jingle::ICEUDP* iceUDP = static_cast(content->findPlugin(gloox::Jingle::PluginICEUDP)); if (!iceUDP) return glooxwrapper::Jingle::ICEUDP::Candidate(); gloox::Jingle::ICEUDP::Candidate glooxCandidate = iceUDP->candidates().front(); return glooxwrapper::Jingle::ICEUDP::Candidate{glooxCandidate.ip, glooxCandidate.port}; } glooxwrapper::Jingle::Session::Session(gloox::Jingle::Session* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) { } glooxwrapper::Jingle::Session::~Session() { if (m_Owned) delete m_Wrapped; } bool glooxwrapper::Jingle::Session::sessionInitiate(char* ipStr, u16 port) { gloox::Jingle::ICEUDP::CandidateList candidateList; candidateList.push_back(gloox::Jingle::ICEUDP::Candidate { "1", // component_id, "1", // foundation "0", // andidate_generation "1", // candidate_id ipStr, "0", // network port, 0, // priotiry "udp", "", // base_ip 0, // base_port gloox::Jingle::ICEUDP::ServerReflexive }); // sessionInitiate deletes the new Content, and // the Plugin destructor inherited by Content frees the ICEUDP plugin. gloox::Jingle::PluginList pluginList; pluginList.push_back(new gloox::Jingle::ICEUDP(/*local_pwd*/"", /*local_ufrag*/"", candidateList)); return m_Wrapped->sessionInitiate(new gloox::Jingle::Content(std::string("game-data"), pluginList)); } glooxwrapper::SessionManager::SessionManager(Client* parent, Jingle::SessionHandler* sh) { m_HandlerWrapper = new SessionHandlerWrapper(sh, false); m_Wrapped = new gloox::Jingle::SessionManager(parent->getWrapped(), m_HandlerWrapper); } glooxwrapper::SessionManager::~SessionManager() { delete m_Wrapped; delete m_HandlerWrapper; } void glooxwrapper::SessionManager::registerPlugins() { // This calls m_factory.registerPlugin (see jinglesessionmanager.cpp), hence // ~PluginFactory() will delete these new plugin templates. m_Wrapped->registerPlugin(new gloox::Jingle::Content()); m_Wrapped->registerPlugin(new gloox::Jingle::ICEUDP()); } glooxwrapper::Jingle::Session glooxwrapper::SessionManager::createSession(const JID& callee) { // The wrapped gloox SessionManager keeps track of this session and deletes it on ~SessionManager(). gloox::Jingle::Session* glooxSession = m_Wrapped->createSession(callee.getWrapped(), m_HandlerWrapper); // Hence the glooxwrapper::Jingle::Session may not own the gloox::Jingle::Session. return glooxwrapper::Jingle::Session(glooxSession, false); } Index: ps/trunk/source/lobby/glooxwrapper/glooxwrapper.h =================================================================== --- ps/trunk/source/lobby/glooxwrapper/glooxwrapper.h (revision 25406) +++ ps/trunk/source/lobby/glooxwrapper/glooxwrapper.h (revision 25407) @@ -1,702 +1,703 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_GLOOXWRAPPER_H #define INCLUDED_GLOOXWRAPPER_H /* The gloox API uses various STL types (std::string, std::list, etc), and it has functions that acquire/release ownership of objects and expect the library's user's 'new'/'delete' functions to be compatible with the library's. These assumptions are invalid when the game and library are built with different compiler versions (or the same version with different build flags): the STL types have different layouts, and new/delete can use different heaps. We want to let people build the game on Windows with any compiler version (any supported Visual Studio version, and debug vs release), without requiring them to rebuild the gloox library themselves. And we don't want to provide ~8 different prebuilt versions of the library. glooxwrapper replaces the gloox API with a version that is safe to use across compiler versions. glooxwrapper and gloox must be compiled together with the same version, but the resulting library can be used with any other compiler. This is the small subset of the API that the game currently uses, with no attempt to be comprehensive. General design and rules: * There is a strict boundary between gloox+glooxwrapper.cpp, and the game code that includes glooxwrapper.h. Objects allocated with new/delete on one side of the boundary must be freed/allocated on the same side. Objects allocated with glooxwrapper_alloc()/glooxwrapper_delete() can be freely shared across the boundary. * glooxwrapper.h and users of glooxwrapper must not use any types from the gloox namespace, except for enums. * std::string is replaced with glooxwrapper::string, std::list with glooxwrapper::list * Most glooxwrapper classes are simple wrappers around gloox classes. Some always take ownership of their wrapped gloox object (i.e. their destructor will delete the wrapped object too); some never do; and some can be used either way (indicated by an m_Owned field). */ #if OS_WIN # include "lib/sysdep/os/win/win.h" // Prevent gloox pulling in windows.h # define _WINDOWS_ #endif #include #include #include #include #include #include #include #include #include // Gloox leaves some #define up, we need to undefine them. #undef lookup #undef lookup2 #undef deflookup #undef deflookup2 #if OS_WIN #define GLOOXWRAPPER_API __declspec(dllexport) #else #define GLOOXWRAPPER_API #endif namespace glooxwrapper { class Client; class DataForm; class DelayedDelivery; class Disco; class IQ; class JID; class MUCRoom; class MUCRoomConfigHandler; class Message; class MessageSession; class OOB; class Presence; class StanzaError; class StanzaExtension; class Tag; class ClientImpl; class MUCRoomHandlerWrapper; class SessionHandlerWrapper; GLOOXWRAPPER_API void* glooxwrapper_alloc(size_t size); GLOOXWRAPPER_API void glooxwrapper_free(void* p); class string { private: size_t m_Size; char* m_Data; public: string() { m_Size = 0; m_Data = (char*)glooxwrapper_alloc(1); m_Data[0] = '\0'; } string(const string& str) { m_Size = str.m_Size; m_Data = (char*)glooxwrapper_alloc(m_Size + 1); memcpy(m_Data, str.m_Data, m_Size + 1); } string(const std::string& str) : m_Data(NULL) { m_Size = str.size(); m_Data = (char*)glooxwrapper_alloc(m_Size + 1); memcpy(m_Data, str.c_str(), m_Size + 1); } string(const char* str) { m_Size = strlen(str); m_Data = (char*)glooxwrapper_alloc(m_Size + 1); memcpy(m_Data, str, m_Size + 1); } string& operator=(const string& str) { if (this != &str) { glooxwrapper_free(m_Data); m_Size = str.m_Size; m_Data = (char*)glooxwrapper_alloc(m_Size + 1); memcpy(m_Data, str.m_Data, m_Size + 1); } return *this; } ~string() { glooxwrapper_free(m_Data); } /** * Gloox strings are UTF encoded, so don't forget to decode it before passing it to the GUI! */ std::string to_string() const { return std::string(m_Data, m_Size); } const char* c_str() const { return m_Data; } bool empty() const { return m_Size == 0; } bool operator==(const char* str) const { return strcmp(m_Data, str) == 0; } bool operator!=(const char* str) const { return strcmp(m_Data, str) != 0; } bool operator==(const string& str) const { return strcmp(m_Data, str.m_Data) == 0; } bool operator<(const string& str) const { return strcmp(m_Data, str.m_Data) < 0; } }; static inline std::ostream& operator<<(std::ostream& stream, const string& string) { return stream << string.c_str(); } template class list { private: struct node { node(const T& item) : m_Item(item), m_Next(NULL) {} T m_Item; node* m_Next; }; node* m_Head; node* m_Tail; public: struct const_iterator { const node* m_Node; const_iterator(const node* n) : m_Node(n) {} bool operator!=(const const_iterator& it) { return m_Node != it.m_Node; } const_iterator& operator++() { m_Node = m_Node->m_Next; return *this; } const T& operator*() { return m_Node->m_Item; } }; const_iterator begin() const { return const_iterator(m_Head); } const_iterator end() const { return const_iterator(NULL); } list() : m_Head(NULL), m_Tail(NULL) {} list(const list& src) : m_Head(NULL), m_Tail(NULL) { *this = src; } list& operator=(const list& src) { if (this != &src) { clear(); for (node* n = src.m_Head; n; n = n->m_Next) push_back(n->m_Item); } return *this; } ~list() { clear(); } void push_back(const T& item) { node* n = new (glooxwrapper_alloc(sizeof(node))) node(item); if (m_Tail) m_Tail->m_Next = n; m_Tail = n; if (!m_Head) m_Head = n; } void clear() { node* n = m_Head; while (n) { node* next = n->m_Next; glooxwrapper_free(n); n = next; } m_Head = m_Tail = NULL; } const T& front() const { return *begin(); } }; typedef glooxwrapper::list TagList; typedef glooxwrapper::list ConstTagList; struct CertInfo { int status; bool chain; string issuer; string server; int date_from; int date_to; string protocol; string cipher; string mac; string compression; }; struct RegistrationFields { string username; string nick; string password; string name; string first; string last; string email; string address; string city; string state; string zip; string phone; string url; string date; string misc; string text; }; struct MUCRoomParticipant { JID* nick; gloox::MUCRoomAffiliation affiliation; gloox::MUCRoomRole role; JID* jid; int flags; string reason; JID* actor; string newNick; string status; JID* alternate; }; class GLOOXWRAPPER_API ConnectionListener { public: virtual ~ConnectionListener() {} virtual void onConnect() = 0; virtual void onDisconnect(gloox::ConnectionError e) = 0; virtual bool onTLSConnect(const CertInfo& info) = 0; }; class GLOOXWRAPPER_API IqHandler { public: virtual ~IqHandler() {} virtual bool handleIq(const IQ& iq) = 0; virtual void handleIqID(const IQ& iq, int context) = 0; }; class GLOOXWRAPPER_API MessageHandler { public: virtual ~MessageHandler() {} virtual void handleMessage(const Message& msg, MessageSession* session = 0) = 0; // MessageSession not supported }; class GLOOXWRAPPER_API MUCRoomHandler { public: virtual ~MUCRoomHandler() {} virtual void handleMUCParticipantPresence(MUCRoom& room, const MUCRoomParticipant participant, const Presence& presence) = 0; virtual void handleMUCMessage(MUCRoom& room, const Message& msg, bool priv) = 0; virtual void handleMUCError(MUCRoom& room, gloox::StanzaError error) = 0; virtual void handleMUCSubject(MUCRoom& room, const string& nick, const string& subject) = 0; }; class GLOOXWRAPPER_API RegistrationHandler { public: virtual ~RegistrationHandler() {} virtual void handleRegistrationFields(const JID& from, int fields, string instructions) = 0; virtual void handleAlreadyRegistered(const JID& from) = 0; virtual void handleRegistrationResult(const JID& from, gloox::RegistrationResult regResult) = 0; virtual void handleDataForm(const JID& from, const DataForm& form) = 0; // DataForm not supported virtual void handleOOB(const JID& from, const OOB& oob) = 0; // OOB not supported }; class GLOOXWRAPPER_API StanzaExtension { public: StanzaExtension(int type) : m_extensionType(type) {} virtual ~StanzaExtension() {} virtual const string& filterString() const = 0; virtual StanzaExtension* newInstance(const Tag* tag) const = 0; virtual glooxwrapper::Tag* tag() const = 0; virtual StanzaExtension* clone() const = 0; int extensionType() const { return m_extensionType; } private: int m_extensionType; }; class GLOOXWRAPPER_API Client { NONCOPYABLE(Client); private: gloox::Client* m_Wrapped; ClientImpl* m_Impl; Disco* m_DiscoWrapper; public: gloox::Client* getWrapped() { return m_Wrapped; } bool connect(bool block = true); gloox::ConnectionError recv(int timeout = -1); const string getID() const; + const string getJID() const; void send(const IQ& iq); void setTls(gloox::TLSPolicy tls); void setCompression(bool compression); void setSASLMechanisms(int mechanisms); void registerStanzaExtension(StanzaExtension* ext); void registerConnectionListener(ConnectionListener* cl); void registerIqHandler(IqHandler* ih, int exttype); void registerMessageHandler(MessageHandler* mh); bool removePresenceExtension(int type); Disco* disco() const { return m_DiscoWrapper; } Client(const string& server); Client(const JID& jid, const string& password, int port = -1); ~Client(); void setPresence(gloox::Presence::PresenceType pres, int priority, const string& status = ""); void disconnect(); }; class GLOOXWRAPPER_API DelayedDelivery { NONCOPYABLE(DelayedDelivery); private: const gloox::DelayedDelivery* m_Wrapped; public: DelayedDelivery(const gloox::DelayedDelivery* wrapped); const string stamp() const; }; class GLOOXWRAPPER_API Disco { NONCOPYABLE(Disco); private: gloox::Disco* m_Wrapped; public: Disco(gloox::Disco* wrapped); void setVersion(const string& name, const string& version, const string& os = ""); void setIdentity(const string& category, const string& type, const string& name = ""); }; class GLOOXWRAPPER_API IQ { NONCOPYABLE(IQ); private: gloox::IQ* m_Wrapped; bool m_Owned; public: const gloox::IQ& getWrapped() const { return *m_Wrapped; } IQ(const gloox::IQ& iq) : m_Wrapped(const_cast(&iq)), m_Owned(false) { } IQ(gloox::IQ::IqType type, const JID& to, const string& id); ~IQ(); void addExtension(const StanzaExtension* se); const StanzaExtension* findExtension(int type) const; template inline const T* findExtension(int type) const { return static_cast(findExtension(type)); } gloox::IQ::IqType subtype() const; const string id() const; const gloox::JID& from() const; gloox::StanzaError error_error() const; // wrapper for ->error()->error() Tag* tag() const; }; class GLOOXWRAPPER_API JID { NONCOPYABLE(JID); private: gloox::JID* m_Wrapped; bool m_Owned; void init(const char* data, size_t len); public: const gloox::JID& getWrapped() const { return *m_Wrapped; } JID(const gloox::JID& jid) : m_Wrapped(const_cast(&jid)), m_Owned(false) { } JID(); JID(const string& jid); JID(const std::string& jid) { init(jid.c_str(), jid.size()); } ~JID(); string username() const; string resource() const; }; class GLOOXWRAPPER_API Message { NONCOPYABLE(Message); private: gloox::Message* m_Wrapped; bool m_Owned; glooxwrapper::JID m_From; glooxwrapper::DelayedDelivery* m_DelayedDelivery; public: Message(gloox::Message* wrapped, bool owned); ~Message(); gloox::Message::MessageType subtype() const; const JID& from() const; string body() const; string subject(const string& lang = "default") const; string thread() const; const glooxwrapper::DelayedDelivery* when() const; }; class GLOOXWRAPPER_API MUCRoom { NONCOPYABLE(MUCRoom); private: gloox::MUCRoom* m_Wrapped; MUCRoomHandlerWrapper* m_HandlerWrapper; bool m_Owned; public: MUCRoom(gloox::MUCRoom* room, bool owned); MUCRoom(Client* parent, const JID& nick, MUCRoomHandler* mrh, MUCRoomConfigHandler* mrch = 0); ~MUCRoom(); const string nick() const; const string name() const; const string service() const; void join(gloox::Presence::PresenceType type = gloox::Presence::Available, const string& status = "", int priority = 0); void leave(const string& msg = ""); void send(const string& message); void setNick(const string& nick); void setPresence(gloox::Presence::PresenceType presence, const string& msg = ""); void setRequestHistory(int value, gloox::MUCRoom::HistoryRequestType type); void kick(const string& nick, const string& reason); void ban(const string& nick, const string& reason); }; class GLOOXWRAPPER_API Presence { gloox::Presence::PresenceType m_Presence; public: Presence(gloox::Presence::PresenceType presence) : m_Presence(presence) {} gloox::Presence::PresenceType presence() const { return m_Presence; } }; class GLOOXWRAPPER_API Registration { NONCOPYABLE(Registration); private: gloox::Registration* m_Wrapped; std::list > m_RegistrationHandlers; public: Registration(Client* parent); ~Registration(); void fetchRegistrationFields(); bool createAccount(int fields, const RegistrationFields& values); void registerRegistrationHandler(RegistrationHandler* rh); }; class GLOOXWRAPPER_API Tag { NONCOPYABLE(Tag); private: gloox::Tag* m_Wrapped; bool m_Owned; Tag(const string& name); Tag(const string& name, const string& cdata); Tag(gloox::Tag* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) {} ~Tag(); public: // Internal use: gloox::Tag* getWrapped() { return m_Wrapped; } gloox::Tag* stealWrapped() { m_Owned = false; return m_Wrapped; } static Tag* allocate(gloox::Tag* wrapped, bool owned); // Instead of using new/delete, Tags must be allocated/freed with these functions static Tag* allocate(const string& name); static Tag* allocate(const string& name, const string& cdata); static void free(const Tag* tag); bool addAttribute(const string& name, const string& value); string findAttribute(const string& name) const; Tag* clone() const; string xmlns() const; bool setXmlns(const string& xmlns); string xml() const; void addChild(Tag* child); string name() const; string cdata() const; const Tag* findTag_clone(const string& expression) const; // like findTag but must be Tag::free()d ConstTagList findTagList_clone(const string& expression) const; // like findTagList but each tag must be Tag::free()d }; /** * See XEP-0166: Jingle and https://camaya.net/api/gloox/namespacegloox_1_1Jingle.html. */ namespace Jingle { class GLOOXWRAPPER_API Plugin { protected: const gloox::Jingle::Plugin* m_Wrapped; bool m_Owned; public: Plugin(const gloox::Jingle::Plugin* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) {} virtual ~Plugin(); const Plugin findPlugin(int type) const; const gloox::Jingle::Plugin* getWrapped() const { return m_Wrapped; } }; typedef list PluginList; /** * See XEP-0176: Jingle ICE-UDP Transport Method */ class GLOOXWRAPPER_API ICEUDP { public: struct Candidate { string ip; int port; }; private: // Class not implemented as it is not used. ICEUDP() = delete; }; class GLOOXWRAPPER_API Session { protected: gloox::Jingle::Session* m_Wrapped; bool m_Owned; public: class GLOOXWRAPPER_API Jingle { private: const gloox::Jingle::Session::Jingle* m_Wrapped; bool m_Owned; public: Jingle(const gloox::Jingle::Session::Jingle* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) {} ~Jingle() { if (m_Owned) delete m_Wrapped; } ICEUDP::Candidate getCandidate() const; }; Session(gloox::Jingle::Session* wrapped, bool owned); ~Session(); bool sessionInitiate(char* ipStr, uint16_t port); }; class GLOOXWRAPPER_API SessionHandler { public: virtual ~SessionHandler() {} virtual void handleSessionAction(gloox::Jingle::Action action, Session& session, const Session::Jingle& jingle) = 0; }; } class GLOOXWRAPPER_API SessionManager { private: gloox::Jingle::SessionManager* m_Wrapped; SessionHandlerWrapper* m_HandlerWrapper; public: SessionManager(Client* parent, Jingle::SessionHandler* sh); ~SessionManager(); void registerPlugins(); Jingle::Session createSession(const JID& callee); }; } #endif // INCLUDED_GLOOXWRAPPER_H Index: ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp =================================================================== --- ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp (revision 25406) +++ ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp (revision 25407) @@ -1,232 +1,233 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_Lobby.h" #include "gui/GUIManager.h" #include "lib/utf8.h" #include "lobby/IXmppClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Util.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "third_party/encryption/pkcs5_pbkdf2.h" #include namespace JSI_Lobby { bool HasXmppClient() { return g_XmppClient; } void SetRankedGame(bool isRanked) { g_rankedGame = isRanked; } #if CONFIG2_LOBBY void StartXmppClient(const ScriptRequest& rq, const std::wstring& username, const std::wstring& password, const std::wstring& room, const std::wstring& nick, int historyRequestSize) { if (g_XmppClient) { ScriptException::Raise(rq, "Cannot call StartXmppClient with an already initialized XmppClient!"); return; } g_XmppClient = IXmppClient::create( g_GUI->GetScriptInterface().get(), utf8_from_wstring(username), utf8_from_wstring(password), utf8_from_wstring(room), utf8_from_wstring(nick), historyRequestSize); g_rankedGame = true; } void StartRegisterXmppClient(const ScriptRequest& rq, const std::wstring& username, const std::wstring& password) { if (g_XmppClient) { ScriptException::Raise(rq, "Cannot call StartRegisterXmppClient with an already initialized XmppClient!"); return; } g_XmppClient = IXmppClient::create( g_GUI->GetScriptInterface().get(), utf8_from_wstring(username), utf8_from_wstring(password), std::string(), std::string(), 0, true); } void StopXmppClient(const ScriptRequest& rq) { if (!g_XmppClient) { ScriptException::Raise(rq, "Cannot call StopXmppClient without an initialized XmppClient!"); return; } SAFE_DELETE(g_XmppClient); g_rankedGame = false; } //////////////////////////////////////////////// //////////////////////////////////////////////// IXmppClient* XmppGetter(const ScriptRequest&, JS::CallArgs&) { if (!g_XmppClient) { LOGERROR("Cannot use XMPPClient functions without an initialized XmppClient!"); return nullptr; } return g_XmppClient; } void SendRegisterGame(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data) { if (!g_XmppClient) { ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Cannot call SendRegisterGame without an initialized XmppClient!"); return; } // Prevent JS mods to register matches in the lobby that were started with lobby authentication disabled if (!g_NetServer || !g_NetServer->UseLobbyAuth()) { LOGERROR("Registering games in the lobby requires lobby authentication to be enabled!"); return; } g_XmppClient->SendIqRegisterGame(*(pCmptPrivate->pScriptInterface), data); } // Unlike other functions, this one just returns Undefined if XmppClient isn't initialised. JS::Value GuiPollNewMessages(const ScriptInterface& scriptInterface) { if (!g_XmppClient) return JS::UndefinedValue(); return g_XmppClient->GuiPollNewMessages(scriptInterface); } // Non-public secure PBKDF2 hash function with salting and 1,337 iterations // // TODO: We should use libsodium's crypto_pwhash instead of this. The first reason is that // libsodium doesn't propose a bare PBKDF2 hash in its API and it's too bad to rely on custom // code when we have a fully-fledged library available; the second reason is that Argon2 (the // default algorithm for crypto_pwhash) is better than what we use (and it's the default one // in the lib for a reason). // However changing the hashing method should be planned carefully, by trying to login with a // password hashed the old way, and, if successful, updating the password in the database using // the new hashing method. Dropping the old hashing code can only be done either by giving users // a way to reset their password, or by keeping track of successful password updates and dropping // old unused accounts after some time. std::string EncryptPassword(const std::string& password, const std::string& username) { ENSURE(sodium_init() >= 0); const int DIGESTSIZE = crypto_hash_sha256_BYTES; const int ITERATIONS = 1337; cassert(DIGESTSIZE == 32); static const unsigned char salt_base[DIGESTSIZE] = { 244, 243, 249, 244, 32, 33, 34, 35, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 32, 33, 244, 224, 127, 129, 130, 140, 153, 133, 123, 234, 123 }; // initialize the salt buffer unsigned char salt_buffer[DIGESTSIZE] = {0}; crypto_hash_sha256_state state; crypto_hash_sha256_init(&state); crypto_hash_sha256_update(&state, salt_base, sizeof(salt_base)); crypto_hash_sha256_update(&state, (unsigned char*)username.c_str(), username.length()); crypto_hash_sha256_final(&state, salt_buffer); // PBKDF2 to create the buffer unsigned char encrypted[DIGESTSIZE]; pbkdf2(encrypted, (unsigned char*)password.c_str(), password.length(), salt_buffer, DIGESTSIZE, ITERATIONS); return CStr(Hexify(encrypted, DIGESTSIZE)).UpperCase(); } #endif void RegisterScriptFunctions(const ScriptRequest& rq) { // Lobby functions ScriptFunction::Register<&HasXmppClient>(rq, "HasXmppClient"); ScriptFunction::Register<&SetRankedGame>(rq, "SetRankedGame"); #if CONFIG2_LOBBY // Allow the lobby to be disabled ScriptFunction::Register<&StartXmppClient>(rq, "StartXmppClient"); ScriptFunction::Register<&StartRegisterXmppClient>(rq, "StartRegisterXmppClient"); ScriptFunction::Register<&StopXmppClient>(rq, "StopXmppClient"); #define REGISTER_XMPP(func, name) \ ScriptFunction::Register<&IXmppClient::func, &XmppGetter>(rq, name) REGISTER_XMPP(connect, "ConnectXmppClient"); REGISTER_XMPP(disconnect, "DisconnectXmppClient"); REGISTER_XMPP(isConnected, "IsXmppClientConnected"); REGISTER_XMPP(SendIqGetBoardList, "SendGetBoardList"); REGISTER_XMPP(SendIqGetProfile, "SendGetProfile"); REGISTER_XMPP(SendIqGameReport, "SendGameReport"); ScriptFunction::Register<&SendRegisterGame>(rq, "SendRegisterGame"); REGISTER_XMPP(SendIqUnregisterGame, "SendUnregisterGame"); REGISTER_XMPP(SendIqChangeStateGame, "SendChangeStateGame"); REGISTER_XMPP(GUIGetPlayerList, "GetPlayerList"); REGISTER_XMPP(GUIGetGameList, "GetGameList"); REGISTER_XMPP(GUIGetBoardList, "GetBoardList"); REGISTER_XMPP(GUIGetProfile, "GetProfile"); ScriptFunction::Register<&GuiPollNewMessages>(rq, "LobbyGuiPollNewMessages"); REGISTER_XMPP(GuiPollHistoricMessages, "LobbyGuiPollHistoricMessages"); REGISTER_XMPP(GuiPollHasPlayerListUpdate, "LobbyGuiPollHasPlayerListUpdate"); REGISTER_XMPP(SendMUCMessage, "LobbySendMessage"); REGISTER_XMPP(SetPresence, "LobbySetPlayerPresence"); REGISTER_XMPP(SetNick, "LobbySetNick"); REGISTER_XMPP(GetNick, "LobbyGetNick"); + REGISTER_XMPP(GetJID, "LobbyGetJID"); REGISTER_XMPP(kick, "LobbyKick"); REGISTER_XMPP(ban, "LobbyBan"); REGISTER_XMPP(GetPresence, "LobbyGetPlayerPresence"); REGISTER_XMPP(GetRole, "LobbyGetPlayerRole"); REGISTER_XMPP(GetRating, "LobbyGetPlayerRating"); REGISTER_XMPP(GetSubject, "LobbyGetRoomSubject"); #undef REGISTER_XMPP ScriptFunction::Register<&EncryptPassword>(rq, "EncryptPassword"); #endif // CONFIG2_LOBBY } } Index: ps/trunk/source/network/NetClient.cpp =================================================================== --- ps/trunk/source/network/NetClient.cpp (revision 25406) +++ ps/trunk/source/network/NetClient.cpp (revision 25407) @@ -1,970 +1,970 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "NetClient.h" #include "NetClientTurnManager.h" #include "NetMessage.h" #include "NetSession.h" #include "lib/byte_order.h" #include "lib/external_libraries/enet.h" #include "lib/external_libraries/libsdl.h" #include "lib/sysdep/sysdep.h" #include "lobby/IXmppClient.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/Compress.h" #include "ps/CStr.h" #include "ps/Game.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Threading.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "network/StunClient.h" /** * Once ping goes above turn length * command delay, * the game will start 'freezing' for other clients while we catch up. * Since commands are sent client -> server -> client, divide by 2. * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file) */ constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2; CNetClient *g_NetClient = NULL; /** * Async task for receiving the initial game state when rejoining an * in-progress network game. */ class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ClientRejoin); public: CNetFileReceiveTask_ClientRejoin(CNetClient& client, const CStr& initAttribs) : m_Client(client), m_InitAttributes(initAttribs) { } virtual void OnComplete() { // We've received the game state from the server // Save it so we can use it after the map has finished loading m_Client.m_JoinSyncBuffer = m_Buffer; // Pretend the server told us to start the game CGameStartMessage start; start.m_InitAttributes = m_InitAttributes; m_Client.HandleMessage(&start); } private: CNetClient& m_Client; CStr m_InitAttributes; }; CNetClient::CNetClient(CGame* game) : m_Session(NULL), m_UserName(L"anonymous"), m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game), m_LastConnectionCheck(0), m_ServerAddress(), m_ServerPort(0), m_Rejoin(false) { m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it void* context = this; JS_AddExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this); // Set up transitions for session AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, (void*)&OnConnect, context); AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, (void*)&OnHandshake, context); AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context); AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NCS_AUTHENTICATE, (void*)&OnAuthenticateRequest, context); AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_PREGAME, (void*)&OnAuthenticate, context); AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context); AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context); AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, (void*)&OnKicked, context); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context); AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_KICKED, NCS_JOIN_SYNCING, (void*)&OnKicked, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, (void*)&OnClientTimeout, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, (void*)&OnClientPerformance, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context); AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context); AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, (void*)&OnPlayerAssignment, context); AddTransition(NCS_LOADING, (uint)NMT_KICKED, NCS_LOADING, (void*)&OnKicked, context); AddTransition(NCS_LOADING, (uint)NMT_CLIENT_TIMEOUT, NCS_LOADING, (void*)&OnClientTimeout, context); AddTransition(NCS_LOADING, (uint)NMT_CLIENT_PERFORMANCE, NCS_LOADING, (void*)&OnClientPerformance, context); AddTransition(NCS_LOADING, (uint)NMT_CLIENTS_LOADING, NCS_LOADING, (void*)&OnClientsLoading, context); AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); AddTransition(NCS_INGAME, (uint)NMT_REJOINED, NCS_INGAME, (void*)&OnRejoined, context); AddTransition(NCS_INGAME, (uint)NMT_KICKED, NCS_INGAME, (void*)&OnKicked, context); AddTransition(NCS_INGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_INGAME, (void*)&OnClientTimeout, context); AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_INGAME, (void*)&OnClientPerformance, context); AddTransition(NCS_INGAME, (uint)NMT_CLIENTS_LOADING, NCS_INGAME, (void*)&OnClientsLoading, context); AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PAUSED, NCS_INGAME, (void*)&OnClientPaused, context); AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, (void*)&OnChat, context); AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, (void*)&OnGameSetup, context); AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, (void*)&OnPlayerAssignment, context); AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, context); AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, (void*)&OnInGame, context); AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, context); // Set first state SetFirstState(NCS_UNCONNECTED); } CNetClient::~CNetClient() { // Try to flush messages before dying (probably fails). if (m_ClientTurnManager) m_ClientTurnManager->OnDestroyConnection(); DestroyConnection(); JS_RemoveExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this); } void CNetClient::TraceMember(JSTracer *trc) { for (JS::Heap& guiMessage : m_GuiMessageQueue) JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue"); } void CNetClient::SetUserName(const CStrW& username) { ENSURE(!m_Session); // must be called before we start the connection m_UserName = username; } -void CNetClient::SetHostingPlayerName(const CStr& hostingPlayerName) +void CNetClient::SetHostJID(const CStr& jid) { - m_HostingPlayerName = hostingPlayerName; + m_HostJID = jid; } void CNetClient::SetGamePassword(const CStr& hashedPassword) { m_Password = hashedPassword; } void CNetClient::SetControllerSecret(const std::string& secret) { m_ControllerSecret = secret; } bool CNetClient::SetupConnection(ENetHost* enetClient) { CNetClientSession* session = new CNetClientSession(*this); bool ok = session->Connect(m_ServerAddress, m_ServerPort, enetClient); SetAndOwnSession(session); m_PollingThread = std::thread(Threading::HandleExceptions::Wrapper, m_Session); return ok; } void CNetClient::SetupServerData(CStr address, u16 port, bool stun) { ENSURE(!m_Session); m_ServerAddress = address; m_ServerPort = port; m_UseSTUN = stun; } void CNetClient::HandleGetServerDataFailed(const CStr& error) { if (m_Session) return; PushGuiMessage( "type", "serverdata", "status", "failed", "reason", error ); } bool CNetClient::TryToConnect(const CStr& hostJID) { if (m_Session) return false; if (m_ServerAddress.empty()) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_SERVER_REFUSED)); return false; } ENetHost* enetClient = nullptr; if (g_XmppClient && m_UseSTUN) { // Find an unused port for (int i = 0; i < 5 && !enetClient; ++i) { // Ports below 1024 are privileged on unix u16 port = 1024 + rand() % (UINT16_MAX - 1024); ENetAddress hostAddr{ ENET_HOST_ANY, port }; enetClient = enet_host_create(&hostAddr, 1, 1, 0, 0); ++hostAddr.port; } if (!enetClient) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_STUN_PORT_FAILED)); return false; } StunClient::StunEndpoint stunEndpoint; if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint)) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_STUN_ENDPOINT_FAILED)); return false; } g_XmppClient->SendStunEndpointToHost(stunEndpoint, hostJID); SDL_Delay(1000); StunClient::SendHolePunchingMessages(*enetClient, m_ServerAddress, m_ServerPort); } if (!g_NetClient->SetupConnection(enetClient)) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_UNKNOWN)); return false; } return true; } void CNetClient::SetAndOwnSession(CNetClientSession* session) { delete m_Session; m_Session = session; } void CNetClient::DestroyConnection() { if (m_Session) m_Session->Shutdown(); if (m_PollingThread.joinable()) // Use detach() over join() because we don't want to wait for the session // (which may be polling or trying to send messages). m_PollingThread.detach(); // The polling thread will cleanup the session on its own, // mark it as nullptr here so we know we're done using it. m_Session = nullptr; } void CNetClient::Poll() { if (!m_Session) return; PROFILE3("NetClient::poll"); CheckServerConnection(); m_Session->ProcessPolledMessages(); } void CNetClient::CheckServerConnection() { // Trigger local warnings if the connection to the server is bad. // At most once per second. std::time_t now = std::time(nullptr); if (now <= m_LastConnectionCheck) return; m_LastConnectionCheck = now; // Report if we are losing the connection to the server u32 lastReceived = m_Session->GetLastReceivedTime(); if (lastReceived > NETWORK_WARNING_TIMEOUT) { PushGuiMessage( "type", "netwarn", "warntype", "server-timeout", "lastReceivedTime", lastReceived); return; } // Report if we have a bad ping to the server. u32 meanRTT = m_Session->GetMeanRTT(); if (meanRTT > NETWORK_BAD_PING) { PushGuiMessage( "type", "netwarn", "warntype", "server-latency", "meanRTT", meanRTT); } } void CNetClient::GuiPoll(JS::MutableHandleValue ret) { if (m_GuiMessageQueue.empty()) { ret.setUndefined(); return; } ret.set(m_GuiMessageQueue.front()); m_GuiMessageQueue.pop_front(); } std::string CNetClient::TestReadGuiMessages() { ScriptRequest rq(GetScriptInterface()); std::string r; JS::RootedValue msg(rq.cx); while (true) { GuiPoll(&msg); if (msg.isUndefined()) break; r += GetScriptInterface().ToString(&msg) + "\n"; } return r; } const ScriptInterface& CNetClient::GetScriptInterface() { return m_Game->GetSimulation2()->GetScriptInterface(); } void CNetClient::PostPlayerAssignmentsToScript() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue newAssignments(rq.cx); ScriptInterface::CreateObject(rq, &newAssignments); for (const std::pair& p : m_PlayerAssignments) { JS::RootedValue assignment(rq.cx); ScriptInterface::CreateObject( rq, &assignment, "name", p.second.m_Name, "player", p.second.m_PlayerID, "status", p.second.m_Status); GetScriptInterface().SetProperty(newAssignments, p.first.c_str(), assignment); } PushGuiMessage( "type", "players", "newAssignments", newAssignments); } bool CNetClient::SendMessage(const CNetMessage* message) { if (!m_Session) return false; return m_Session->SendMessage(message); } void CNetClient::HandleConnect() { Update((uint)NMT_CONNECT_COMPLETE, NULL); } void CNetClient::HandleDisconnect(u32 reason) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", reason); DestroyConnection(); // Update the state immediately to UNCONNECTED (don't bother with FSM transitions since // we'd need one for every single state, and we don't need to use per-state actions) SetCurrState(NCS_UNCONNECTED); } void CNetClient::SendGameSetupMessage(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface) { CGameSetupMessage gameSetup(scriptInterface); gameSetup.m_Data = attrs; SendMessage(&gameSetup); } void CNetClient::SendAssignPlayerMessage(const int playerID, const CStr& guid) { CAssignPlayerMessage assignPlayer; assignPlayer.m_PlayerID = playerID; assignPlayer.m_GUID = guid; SendMessage(&assignPlayer); } void CNetClient::SendChatMessage(const std::wstring& text) { CChatMessage chat; chat.m_Message = text; SendMessage(&chat); } void CNetClient::SendReadyMessage(const int status) { CReadyMessage readyStatus; readyStatus.m_Status = status; SendMessage(&readyStatus); } void CNetClient::SendClearAllReadyMessage() { CClearAllReadyMessage clearAllReady; SendMessage(&clearAllReady); } void CNetClient::SendStartGameMessage(const CStr& initAttribs) { CGameStartMessage gameStart; gameStart.m_InitAttributes = initAttribs; SendMessage(&gameStart); } void CNetClient::SendRejoinedMessage() { CRejoinedMessage rejoinedMessage; SendMessage(&rejoinedMessage); } void CNetClient::SendKickPlayerMessage(const CStrW& playerName, bool ban) { CKickedMessage kickPlayer; kickPlayer.m_Name = playerName; kickPlayer.m_Ban = ban; SendMessage(&kickPlayer); } void CNetClient::SendPausedMessage(bool pause) { CClientPausedMessage pausedMessage; pausedMessage.m_Pause = pause; SendMessage(&pausedMessage); } bool CNetClient::HandleMessage(CNetMessage* message) { // Handle non-FSM messages first Status status = m_Session->GetFileTransferer().HandleMessageReceive(*message); if (status == INFO::OK) return true; if (status != INFO::SKIPPED) return false; if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) { CFileTransferRequestMessage* reqMessage = static_cast(message); // TODO: we should support different transfer request types, instead of assuming // it's always requesting the simulation state std::stringstream stream; LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn()); u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn()); stream.write((char*)&turn, sizeof(turn)); bool ok = m_Game->GetSimulation2()->SerializeState(stream); ENSURE(ok); // Compress the content with zlib to save bandwidth // (TODO: if this is still too large, compressing with e.g. LZMA works much better) std::string compressed; CompressZLib(stream.str(), compressed, true); m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed); return true; } // Update FSM bool ok = Update(message->GetType(), message); if (!ok) LOGERROR("Net client: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)GetCurrState()); return ok; } void CNetClient::LoadFinished() { if (!m_JoinSyncBuffer.empty()) { // We're rejoining a game, and just finished loading the initial map, // so deserialize the saved game state now std::string state; DecompressZLib(m_JoinSyncBuffer, state, true); std::stringstream stream(state); u32 turn; stream.read((char*)&turn, sizeof(turn)); turn = to_le32(turn); LOGMESSAGE("Rejoining client deserializing state at turn %u\n", turn); bool ok = m_Game->GetSimulation2()->DeserializeState(stream); ENSURE(ok); m_ClientTurnManager->ResetState(turn, turn); PushGuiMessage( "type", "netstatus", "status", "join_syncing"); } else { // Connecting at the start of a game, so we'll wait for other players to finish loading PushGuiMessage( "type", "netstatus", "status", "waiting_for_players"); } CLoadedGameMessage loaded; loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn(); SendMessage(&loaded); } void CNetClient::SendAuthenticateMessage() { CAuthenticateMessage authenticate; authenticate.m_Name = m_UserName; authenticate.m_Password = m_Password; authenticate.m_ControllerSecret = m_ControllerSecret; SendMessage(&authenticate); } bool CNetClient::OnConnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE); CNetClient* client = static_cast(context); client->PushGuiMessage( "type", "netstatus", "status", "connected"); return true; } bool CNetClient::OnHandshake(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE); CNetClient* client = static_cast(context); CCliHandshakeMessage handshake; handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; client->SendMessage(&handshake); return true; } bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE_RESPONSE); CNetClient* client = static_cast(context); CSrvHandshakeResponseMessage* message = static_cast(event->GetParamRef()); client->m_GUID = message->m_GUID; if (message->m_Flags & PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH) { - if (g_XmppClient && !client->m_HostingPlayerName.empty()) - g_XmppClient->SendIqLobbyAuth(client->m_HostingPlayerName, client->m_GUID); + if (g_XmppClient && !client->m_HostJID.empty()) + g_XmppClient->SendIqLobbyAuth(client->m_HostJID, client->m_GUID); else { client->PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_LOBBY_AUTH_FAILED)); LOGMESSAGE("Net client: Couldn't send lobby auth xmpp message"); } return true; } client->SendAuthenticateMessage(); return true; } bool CNetClient::OnAuthenticateRequest(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE); CNetClient* client = static_cast(context); client->SendAuthenticateMessage(); return true; } bool CNetClient::OnAuthenticate(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE_RESULT); CNetClient* client = static_cast(context); CAuthenticateResultMessage* message = static_cast(event->GetParamRef()); LOGMESSAGE("Net: Authentication result: host=%u, %s", message->m_HostID, utf8_from_wstring(message->m_Message)); client->m_HostID = message->m_HostID; client->m_Rejoin = message->m_Code == ARC_OK_REJOINING; client->m_IsController = message->m_IsController; client->PushGuiMessage( "type", "netstatus", "status", "authenticated", "rejoining", client->m_Rejoin); return true; } bool CNetClient::OnChat(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CHAT); CNetClient* client = static_cast(context); CChatMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "chat", "guid", message->m_GUID, "text", message->m_Message); return true; } bool CNetClient::OnReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_READY); CNetClient* client = static_cast(context); CReadyMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "ready", "guid", message->m_GUID, "status", message->m_Status); return true; } bool CNetClient::OnGameSetup(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_SETUP); CNetClient* client = static_cast(context); CGameSetupMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "gamesetup", "data", message->m_Data); return true; } bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_PLAYER_ASSIGNMENT); CNetClient* client = static_cast(context); CPlayerAssignmentMessage* message = static_cast(event->GetParamRef()); // Unpack the message PlayerAssignmentMap newPlayerAssignments; for (size_t i = 0; i < message->m_Hosts.size(); ++i) { PlayerAssignment assignment; assignment.m_Enabled = true; assignment.m_Name = message->m_Hosts[i].m_Name; assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID; assignment.m_Status = message->m_Hosts[i].m_Status; newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment; } client->m_PlayerAssignments.swap(newPlayerAssignments); client->PostPlayerAssignmentsToScript(); return true; } // This is called either when the host clicks the StartGame button or // if this client rejoins and finishes the download of the simstate. bool CNetClient::OnGameStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetClient* client = static_cast(context); CGameStartMessage* message = static_cast(event->GetParamRef()); // Find the player assigned to our GUID int player = -1; if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end()) player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID; client->m_ClientTurnManager = new CNetClientTurnManager( *client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger()); // Parse init attributes. const ScriptInterface& scriptInterface = client->m_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue initAttribs(rq.cx); scriptInterface.ParseJSON(message->m_InitAttributes, &initAttribs); client->m_Game->SetPlayerID(player); client->m_Game->StartGame(&initAttribs, ""); client->PushGuiMessage("type", "start", "initAttributes", initAttribs); return true; } bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START); CNetClient* client = static_cast(context); CJoinSyncStartMessage* joinSyncStartMessage = (CJoinSyncStartMessage*)event->GetParamRef(); // The server wants us to start downloading the game state from it, so do so client->m_Session->GetFileTransferer().StartTask( shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client, joinSyncStartMessage->m_InitAttributes)) ); return true; } bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); CNetClient* client = static_cast(context); CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef(); client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength); // Execute all the received commands for the latest turn client->m_ClientTurnManager->UpdateFastForward(); return true; } bool CNetClient::OnRejoined(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_REJOINED); CNetClient* client = static_cast(context); CRejoinedMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "rejoined", "guid", message->m_GUID); return true; } bool CNetClient::OnKicked(void *context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_KICKED); CNetClient* client = static_cast(context); CKickedMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "username", message->m_Name, "type", "kicked", "banned", message->m_Ban != 0); return true; } bool CNetClient::OnClientTimeout(void *context, CFsmEvent* event) { // Report the timeout of some other client ENSURE(event->GetType() == (uint)NMT_CLIENT_TIMEOUT); CNetClient* client = static_cast(context); CClientTimeoutMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "netwarn", "warntype", "client-timeout", "guid", message->m_GUID, "lastReceivedTime", message->m_LastReceivedTime); return true; } bool CNetClient::OnClientPerformance(void *context, CFsmEvent* event) { // Performance statistics for one or multiple clients ENSURE(event->GetType() == (uint)NMT_CLIENT_PERFORMANCE); CNetClient* client = static_cast(context); CClientPerformanceMessage* message = static_cast(event->GetParamRef()); // Display warnings for other clients with bad ping for (size_t i = 0; i < message->m_Clients.size(); ++i) { if (message->m_Clients[i].m_MeanRTT < NETWORK_BAD_PING || message->m_Clients[i].m_GUID == client->m_GUID) continue; client->PushGuiMessage( "type", "netwarn", "warntype", "client-latency", "guid", message->m_Clients[i].m_GUID, "meanRTT", message->m_Clients[i].m_MeanRTT); } return true; } bool CNetClient::OnClientsLoading(void *context, CFsmEvent *event) { ENSURE(event->GetType() == (uint)NMT_CLIENTS_LOADING); CNetClient* client = static_cast(context); CClientsLoadingMessage* message = static_cast(event->GetParamRef()); std::vector guids; guids.reserve(message->m_Clients.size()); for (const CClientsLoadingMessage::S_m_Clients& mClient : message->m_Clients) guids.push_back(mClient.m_GUID); client->PushGuiMessage( "type", "clients-loading", "guids", guids); return true; } bool CNetClient::OnClientPaused(void *context, CFsmEvent *event) { ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED); CNetClient* client = static_cast(context); CClientPausedMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "paused", "pause", message->m_Pause != 0, "guid", message->m_GUID); return true; } bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetClient* client = static_cast(context); // All players have loaded the game - start running the turn manager // so that the game begins client->m_Game->SetTurnManager(client->m_ClientTurnManager); client->PushGuiMessage( "type", "netstatus", "status", "active"); // If we have rejoined an in progress game, send the rejoined message to the server. if (client->m_Rejoin) client->SendRejoinedMessage(); return true; } bool CNetClient::OnInGame(void *context, CFsmEvent* event) { // TODO: should split each of these cases into a separate method CNetClient* client = static_cast(context); CNetMessage* message = static_cast(event->GetParamRef()); if (message) { if (message->GetType() == NMT_SIMULATION_COMMAND) { CSimulationMessage* simMessage = static_cast (message); client->m_ClientTurnManager->OnSimulationMessage(simMessage); } else if (message->GetType() == NMT_SYNC_ERROR) { CSyncErrorMessage* syncMessage = static_cast (message); client->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected, syncMessage->m_PlayerNames); } else if (message->GetType() == NMT_END_COMMAND_BATCH) { CEndCommandBatchMessage* endMessage = static_cast (message); client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength); } } return true; } Index: ps/trunk/source/network/NetClient.h =================================================================== --- ps/trunk/source/network/NetClient.h (revision 25406) +++ ps/trunk/source/network/NetClient.h (revision 25407) @@ -1,347 +1,348 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef NETCLIENT_H #define NETCLIENT_H #include "network/fsm.h" #include "network/NetFileTransfer.h" #include "network/NetHost.h" #include "scriptinterface/ScriptInterface.h" #include "ps/CStr.h" #include #include #include class CGame; class CNetClientSession; class CNetClientTurnManager; class CNetServer; class ScriptInterface; typedef struct _ENetHost ENetHost; // NetClient session FSM states enum { NCS_UNCONNECTED, NCS_CONNECT, NCS_HANDSHAKE, NCS_AUTHENTICATE, NCS_PREGAME, NCS_LOADING, NCS_JOIN_SYNCING, NCS_INGAME }; /** * Network client. * This code is run by every player (including the host, if they are not * a dedicated server). * It provides an interface between the GUI, the network (via CNetClientSession), * and the game (via CGame and CNetClientTurnManager). */ class CNetClient : public CFsm { NONCOPYABLE(CNetClient); friend class CNetFileReceiveTask_ClientRejoin; public: /** * Construct a client associated with the given game object. * The game must exist for the lifetime of this object. */ CNetClient(CGame* game); virtual ~CNetClient(); /** * We assume that adding a tracing function that's only called * during GC is better for performance than using a * PersistentRooted where each value needs to be added to * the root set. */ static void Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void TraceMember(JSTracer *trc); /** * Set the user's name that will be displayed to all players. * This must not be called after the connection setup. */ void SetUserName(const CStrW& username); /** - * Set the name of the hosting player. + * Store the JID of the host. * This is needed for the secure lobby authentication. */ - void SetHostingPlayerName(const CStr& hostingPlayerName); + void SetHostJID(const CStr& jid); void SetControllerSecret(const std::string& secret); bool IsController() const { return m_IsController; } /** * Set the game password. */ void SetGamePassword(const CStr& hashedPassword); /** * Returns the GUID of the local client. * Used for distinguishing observers. */ CStr GetGUID() const { return m_GUID; } /** * Set connection data to the remote networked server. * @param address IP address or host name to connect to */ void SetupServerData(CStr address, u16 port, bool stun); /** * Set up a connection to the remote networked server. * Must call SetupServerData first. * @return true on success, false on connection failure */ bool SetupConnection(ENetHost* enetClient); /** * Connect to the remote networked server using lobby. * Push netstatus messages on failure. * @return true on success, false on connection failure */ bool TryToConnect(const CStr& hostJID); /** * Destroy the connection to the server. * This client probably cannot be used again. */ void DestroyConnection(); /** * Poll the connection for messages from the server and process them, and send * any queued messages. * This must be called frequently (i.e. once per frame). */ void Poll(); /** * Locally triggers a GUI message if the connection to the server is being lost or has bad latency. */ void CheckServerConnection(); /** * Retrieves the next queued GUI message, and removes it from the queue. * The returned value is in the GetScriptInterface() JS context. * * This is the only mechanism for the networking code to send messages to * the GUI - it is pull-based (instead of push) so the engine code does not * need to know anything about the code structure of the GUI scripts. * * The structure of the messages is { "type": "...", ... }. * The exact types and associated data are not specified anywhere - the * implementation and GUI scripts must make the same assumptions. * * @return next message, or the value 'undefined' if the queue is empty */ void GuiPoll(JS::MutableHandleValue); /** * Add a message to the queue, to be read by GuiPoll. * The script value must be in the GetScriptInterface() JS context. */ template void PushGuiMessage(Args const&... args) { ScriptRequest rq(GetScriptInterface()); JS::RootedValue message(rq.cx); ScriptInterface::CreateObject(rq, &message, args...); m_GuiMessageQueue.push_back(JS::Heap(message)); } /** * Return a concatenation of all messages in the GUI queue, * for test cases to easily verify the queue contents. */ std::string TestReadGuiMessages(); /** * Get the script interface associated with this network client, * which is equivalent to the one used by the CGame in the constructor. */ const ScriptInterface& GetScriptInterface(); /** * Send a message to the server. * @param message message to send * @return true on success */ bool SendMessage(const CNetMessage* message); /** * Call when the network connection has been successfully initiated. */ void HandleConnect(); /** * Call when the network connection has been lost. */ void HandleDisconnect(u32 reason); /** * Call when a message has been received from the network. */ bool HandleMessage(CNetMessage* message); /** * Call when the game has started and all data files have been loaded, * to signal to the server that we are ready to begin the game. */ void LoadFinished(); void SendGameSetupMessage(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface); void SendAssignPlayerMessage(const int playerID, const CStr& guid); void SendChatMessage(const std::wstring& text); void SendReadyMessage(const int status); void SendClearAllReadyMessage(); void SendStartGameMessage(const CStr& initAttribs); /** * Call when the client has rejoined a running match and finished * the loading screen. */ void SendRejoinedMessage(); /** * Call to kick/ban a client */ void SendKickPlayerMessage(const CStrW& playerName, bool ban); /** * Call when the client has paused or unpaused the game. */ void SendPausedMessage(bool pause); /** * @return Whether the NetClient is shutting down. */ bool ShouldShutdown() const; /** * Called when fetching connection data from the host failed, to inform JS code. */ void HandleGetServerDataFailed(const CStr& error); private: void SendAuthenticateMessage(); // Net message / FSM transition handlers static bool OnConnect(void* context, CFsmEvent* event); static bool OnHandshake(void* context, CFsmEvent* event); static bool OnHandshakeResponse(void* context, CFsmEvent* event); static bool OnAuthenticateRequest(void* context, CFsmEvent* event); static bool OnAuthenticate(void* context, CFsmEvent* event); static bool OnChat(void* context, CFsmEvent* event); static bool OnReady(void* context, CFsmEvent* event); static bool OnGameSetup(void* context, CFsmEvent* event); static bool OnPlayerAssignment(void* context, CFsmEvent* event); static bool OnInGame(void* context, CFsmEvent* event); static bool OnGameStart(void* context, CFsmEvent* event); static bool OnJoinSyncStart(void* context, CFsmEvent* event); static bool OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event); static bool OnRejoined(void* context, CFsmEvent* event); static bool OnKicked(void* context, CFsmEvent* event); static bool OnClientTimeout(void* context, CFsmEvent* event); static bool OnClientPerformance(void* context, CFsmEvent* event); static bool OnClientsLoading(void* context, CFsmEvent* event); static bool OnClientPaused(void* context, CFsmEvent* event); static bool OnLoadedGame(void* context, CFsmEvent* event); /** * Take ownership of a session object, and use it for all network communication. */ void SetAndOwnSession(CNetClientSession* session); /** * Push a message onto the GUI queue listing the current player assignments. */ void PostPlayerAssignmentsToScript(); CGame *m_Game; CStrW m_UserName; - CStr m_HostingPlayerName; + + CStr m_HostJID; CStr m_ServerAddress; u16 m_ServerPort; bool m_UseSTUN; /** * Password to join the game. */ CStr m_Password; /// The 'secret' used to identify the controller of the game. std::string m_ControllerSecret; /// Note that this is just a "gui hint" with no actual impact on being controller. bool m_IsController = false; /// Current network session (or NULL if not connected) CNetClientSession* m_Session; std::thread m_PollingThread; /// Turn manager associated with the current game (or NULL if we haven't started the game yet) CNetClientTurnManager* m_ClientTurnManager; /// Unique-per-game identifier of this client, used to identify the sender of simulation commands u32 m_HostID; /// True if the player is currently rejoining or has already rejoined the game. bool m_Rejoin; /// Latest copy of player assignments heard from the server PlayerAssignmentMap m_PlayerAssignments; /// Globally unique identifier to distinguish users beyond the lifetime of a single network session CStr m_GUID; /// Queue of messages for GuiPoll std::deque> m_GuiMessageQueue; /// Serialized game state received when joining an in-progress game std::string m_JoinSyncBuffer; /// Time when the server was last checked for timeouts and bad latency std::time_t m_LastConnectionCheck; }; /// Global network client for the standard game extern CNetClient *g_NetClient; #endif // NETCLIENT_H Index: ps/trunk/source/network/scripting/JSInterface_Network.cpp =================================================================== --- ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 25406) +++ ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 25407) @@ -1,309 +1,310 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_Network.h" #include "lib/external_libraries/enet.h" #include "lib/external_libraries/libsdl.h" #include "lib/types.h" #include "lobby/IXmppClient.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetServer.h" #include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/GUID.h" #include "ps/Util.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "third_party/encryption/pkcs5_pbkdf2.h" namespace JSI_Network { u16 GetDefaultPort() { return PS_DEFAULT_PORT; } bool IsNetController() { return !!g_NetClient && g_NetClient->IsController(); } bool HasNetServer() { return !!g_NetServer; } bool HasNetClient() { return !!g_NetClient; } CStr HashPassword(const CStr& password) { if (password.empty()) return password; ENSURE(sodium_init() >= 0); const int DIGESTSIZE = crypto_hash_sha256_BYTES; constexpr int ITERATIONS = 1737; cassert(DIGESTSIZE == 32); static const unsigned char salt_base[DIGESTSIZE] = { 244, 243, 249, 244, 32, 33, 19, 35, 16, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 32, 33, 244, 224, 127, 129, 130, 140, 153, 88, 123, 234, 123 }; // initialize the salt buffer unsigned char salt_buffer[DIGESTSIZE] = { 0 }; crypto_hash_sha256_state state; crypto_hash_sha256_init(&state); crypto_hash_sha256_update(&state, salt_base, sizeof(salt_base)); crypto_hash_sha256_final(&state, salt_buffer); // PBKDF2 to create the buffer unsigned char encrypted[DIGESTSIZE]; pbkdf2(encrypted, (unsigned char*)password.c_str(), password.length(), salt_buffer, DIGESTSIZE, ITERATIONS); return CStr(Hexify(encrypted, DIGESTSIZE)).UpperCase(); } -void StartNetworkHost(const ScriptRequest& rq, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName, bool useSTUN, const CStr& password) + +void StartNetworkHost(const ScriptRequest& rq, const CStrW& playerName, const u16 serverPort, bool useSTUN, const CStr& password) { ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); // Always use lobby authentication for lobby matches to prevent impersonation and smurfing, in particular through mods that implemented an UI for arbitrary or other players nicknames. bool hasLobby = !!g_XmppClient; g_NetServer = new CNetServer(hasLobby); // In lobby, we send our public ip and port on request to the players, who want to connect. // In either case we need to know our public IP. If using STUN, we'll use that, // otherwise, the lobby's reponse to the game registration stanza will tell us our public IP. if (hasLobby) { CStr ip; if (!useSTUN) // Don't store IP - the lobby bot will send it later. // (if a client tries to connect before it's setup, they'll be disconnected) g_NetServer->SetConnectionData("", serverPort, false); else { u16 port = serverPort; // This is using port variable to store return value, do not pass serverPort itself. if (!StunClient::FindStunEndpointHost(ip, port)) { ScriptException::Raise(rq, "Failed to host via STUN."); SAFE_DELETE(g_NetServer); return; } g_NetServer->SetConnectionData(ip, port, true); } } if (!g_NetServer->SetupConnection(serverPort)) { ScriptException::Raise(rq, "Failed to start server"); SAFE_DELETE(g_NetServer); return; } // Generate a secret to identify the host client. std::string secret = ps_generate_guid(); // We will get hashed password from clients, so hash it once for server CStr hashedPass = HashPassword(password); g_NetServer->SetPassword(hashedPass); g_NetServer->SetControllerSecret(secret); g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); - g_NetClient->SetHostingPlayerName(hostLobbyName); + if (hasLobby) + g_NetClient->SetHostJID(g_XmppClient->GetJID()); g_NetClient->SetGamePassword(hashedPass); g_NetClient->SetupServerData("127.0.0.1", serverPort, false); g_NetClient->SetControllerSecret(secret); if (!g_NetClient->SetupConnection(nullptr)) { ScriptException::Raise(rq, "Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } -void StartNetworkJoin(const ScriptRequest& rq, const CStrW& playerName, const CStr& serverAddress, u16 serverPort, bool useSTUN, const CStr& hostJID) +void StartNetworkJoin(const ScriptRequest& rq, const CStrW& playerName, const CStr& serverAddress, u16 serverPort) { ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); - g_NetClient->SetHostingPlayerName(hostJID.substr(0, hostJID.find("@"))); - g_NetClient->SetupServerData(serverAddress, serverPort, useSTUN); + g_NetClient->SetupServerData(serverAddress, serverPort, false); if (!g_NetClient->SetupConnection(nullptr)) { ScriptException::Raise(rq, "Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } /** * Requires XmppClient to send iq request to the server to get server's ip and port based on passed password. * This is needed to not force server to share it's public ip with all potential clients in the lobby. * XmppClient will also handle logic after receiving the answer. */ void StartNetworkJoinLobby(const CStrW& playerName, const CStr& hostJID, const CStr& password) { ENSURE(!!g_XmppClient); ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); CStr hashedPass = HashPassword(password); g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); - g_NetClient->SetHostingPlayerName(hostJID.substr(0, hostJID.find("@"))); + g_NetClient->SetHostJID(hostJID); g_NetClient->SetGamePassword(hashedPass); g_XmppClient->SendIqGetConnectionData(hostJID, hashedPass.c_str()); } void DisconnectNetworkGame() { // TODO: we ought to do async reliable disconnections SAFE_DELETE(g_NetServer); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } CStr GetPlayerGUID() { if (!g_NetClient) return "local"; return g_NetClient->GetGUID(); } JS::Value PollNetworkClient(const ScriptInterface& scriptInterface) { if (!g_NetClient) return JS::UndefinedValue(); // Convert from net client context to GUI script context ScriptRequest rqNet(g_NetClient->GetScriptInterface()); JS::RootedValue pollNet(rqNet.cx); g_NetClient->GuiPoll(&pollNet); return scriptInterface.CloneValueFromOtherCompartment(g_NetClient->GetScriptInterface(), pollNet); } void SendGameSetupMessage(const ScriptInterface& scriptInterface, JS::HandleValue attribs1) { ENSURE(g_NetClient); // TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere (with no obvious reason). ScriptRequest rq(scriptInterface); JS::RootedValue attribs(rq.cx, attribs1); g_NetClient->SendGameSetupMessage(&attribs, scriptInterface); } void AssignNetworkPlayer(int playerID, const CStr& guid) { ENSURE(g_NetClient); g_NetClient->SendAssignPlayerMessage(playerID, guid); } void KickPlayer(const CStrW& playerName, bool ban) { ENSURE(g_NetClient); g_NetClient->SendKickPlayerMessage(playerName, ban); } void SendNetworkChat(const CStrW& message) { ENSURE(g_NetClient); g_NetClient->SendChatMessage(message); } void SendNetworkReady(int message) { ENSURE(g_NetClient); g_NetClient->SendReadyMessage(message); } void ClearAllPlayerReady () { ENSURE(g_NetClient); g_NetClient->SendClearAllReadyMessage(); } void StartNetworkGame(const ScriptInterface& scriptInterface, JS::HandleValue attribs1) { ENSURE(g_NetClient); // TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere (with no obvious reason). ScriptRequest rq(scriptInterface); JS::RootedValue attribs(rq.cx, attribs1); g_NetClient->SendStartGameMessage(scriptInterface.StringifyJSON(&attribs)); } void SetTurnLength(int length) { if (g_NetServer) g_NetServer->SetTurnLength(length); else LOGERROR("Only network host can change turn length"); } void RegisterScriptFunctions(const ScriptRequest& rq) { ScriptFunction::Register<&GetDefaultPort>(rq, "GetDefaultPort"); ScriptFunction::Register<&IsNetController>(rq, "IsNetController"); ScriptFunction::Register<&HasNetServer>(rq, "HasNetServer"); ScriptFunction::Register<&HasNetClient>(rq, "HasNetClient"); ScriptFunction::Register<&StartNetworkHost>(rq, "StartNetworkHost"); ScriptFunction::Register<&StartNetworkJoin>(rq, "StartNetworkJoin"); ScriptFunction::Register<&StartNetworkJoinLobby>(rq, "StartNetworkJoinLobby"); ScriptFunction::Register<&DisconnectNetworkGame>(rq, "DisconnectNetworkGame"); ScriptFunction::Register<&GetPlayerGUID>(rq, "GetPlayerGUID"); ScriptFunction::Register<&PollNetworkClient>(rq, "PollNetworkClient"); ScriptFunction::Register<&SendGameSetupMessage>(rq, "SendGameSetupMessage"); ScriptFunction::Register<&AssignNetworkPlayer>(rq, "AssignNetworkPlayer"); ScriptFunction::Register<&KickPlayer>(rq, "KickPlayer"); ScriptFunction::Register<&SendNetworkChat>(rq, "SendNetworkChat"); ScriptFunction::Register<&SendNetworkReady>(rq, "SendNetworkReady"); ScriptFunction::Register<&ClearAllPlayerReady>(rq, "ClearAllPlayerReady"); ScriptFunction::Register<&StartNetworkGame>(rq, "StartNetworkGame"); ScriptFunction::Register<&SetTurnLength>(rq, "SetTurnLength"); } }