Index: ps/trunk/binaries/data/mods/public/gui/common/network.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/network.js (revision 24727)
+++ ps/trunk/binaries/data/mods/public/gui/common/network.js (revision 24728)
@@ -1,246 +1,249 @@
/**
* Number of milliseconds to display network warnings.
*/
var g_NetworkWarningTimeout = 3000;
/**
* Currently displayed network warnings. At most one message per user.
*/
var g_NetworkWarnings = {};
/**
* Message-types to be displayed.
*/
var g_NetworkWarningTexts = {
"server-timeout": (msg, username) =>
sprintf(translate("Losing connection to server (%(seconds)ss)"), {
"seconds": Math.ceil(msg.lastReceivedTime / 1000)
}),
"client-timeout": (msg, username) =>
sprintf(translate("%(player)s losing connection (%(seconds)ss)"), {
"player": username,
"seconds": Math.ceil(msg.lastReceivedTime / 1000)
}),
"server-latency": (msg, username) =>
sprintf(translate("Bad connection to server (%(milliseconds)sms)"), {
"milliseconds": msg.meanRTT
}),
"client-latency": (msg, username) =>
sprintf(translate("Bad connection to %(player)s (%(milliseconds)sms)"), {
"player": username,
"milliseconds": msg.meanRTT
})
};
var g_NetworkCommands = {
"/kick": argument => kickPlayer(argument, false),
"/ban": argument => kickPlayer(argument, true),
"/kickspecs": argument => kickObservers(false),
"/banspecs": argument => kickObservers(true),
"/list": argument => addChatMessage({ "type": "clientlist" }),
"/clear": argument => clearChatMessages()
};
var g_ValidPorts = { "min": 1, "max": 65535 };
function getValidPort(port)
{
if (isNaN(+port) || +port < g_ValidPorts.min || +port > g_ValidPorts.max)
return Engine.GetDefaultPort();
return +port;
}
/**
* Must be kept in sync with source/network/NetHost.h
*/
function getDisconnectReason(id, wasConnected)
{
switch (id)
{
case 0: return wasConnected ? "" :
translate("This is often caused by UDP port 20595 not being forwarded on the host side, by a firewall, or anti-virus software.");
case 1: return translate("The host has ended the game.");
case 2: return translate("Incorrect network protocol version.");
case 3: return translate("Game is loading, please try again later.");
case 4: return translate("Game has already started, no observers allowed.");
case 5: return translate("You have been kicked.");
case 6: return translate("You have been banned.");
case 7: return translate("Player name in use. If you were disconnected, retry in few seconds.");
case 8: return translate("Server full.");
case 9: return translate("Secure lobby authentication failed. Join via lobby.");
case 10: return translate("Error: Server failed to allocate a unique client identifier.");
case 11: return translate("Error: Client commands were ready for an unexpected game turn.");
case 12: return translate("Error: Client simulated an unexpected game turn.");
+ case 13: return translate("Password is invalid.");
+ case 14: return translate("Could not find an unused port for the enet STUN client.");
+ case 15: return translate("Could not find the STUN endpoint.");
default:
warn("Unknown disconnect-reason ID received: " + id);
return sprintf(translate("\\[Invalid value %(id)s]"), { "id": id });
}
}
/**
* Show the disconnect reason in a message box.
*
* @param {number} reason
*/
function reportDisconnect(reason, wasConnected)
{
messageBox(
400, 200,
(wasConnected ?
translate("Lost connection to the server.") :
translate("Failed to connect to the server.")
) + "\n\n" + getDisconnectReason(reason, wasConnected),
translate("Disconnected")
);
}
function kickError()
{
addChatMessage({
"type": "system",
"text": translate("Only the host can kick clients!")
});
}
function kickPlayer(username, ban)
{
if (g_IsController)
Engine.KickPlayer(username, ban);
else
kickError();
}
function kickObservers(ban)
{
if (!g_IsController)
{
kickError();
return;
}
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player == -1)
Engine.KickPlayer(g_PlayerAssignments[guid].name, ban);
}
/**
* Sort GUIDs of connected users sorted by playerindex, observers last.
* Requires g_PlayerAssignments.
*/
function sortGUIDsByPlayerID()
{
return Object.keys(g_PlayerAssignments).sort((guidA, guidB) => {
let playerIdA = g_PlayerAssignments[guidA].player;
let playerIdB = g_PlayerAssignments[guidB].player;
if (playerIdA == -1) return +1;
if (playerIdB == -1) return -1;
return playerIdA - playerIdB;
});
}
/**
* Get a colorized list of usernames sorted by player slot, observers last.
* Requires g_PlayerAssignments and colorizePlayernameByGUID.
*
* @returns {string}
*/
function getUsernameList()
{
let usernames = sortGUIDsByPlayerID().map(guid => colorizePlayernameByGUID(guid));
// Translation: Number of currently connected players/observers and their names
return sprintf(translate("Users (%(num)s): %(users)s"), {
"users": usernames.join(translate(", ")),
"num": usernames.length
});
}
/**
* Execute a command locally. Requires addChatMessage.
*
* @param {string} input
* @returns {boolean} whether a command was executed
*/
function executeNetworkCommand(input)
{
if (input.indexOf("/") != 0)
return false;
let command = input.split(" ", 1)[0];
let argument = input.substr(command.length + 1);
if (g_NetworkCommands[command])
g_NetworkCommands[command](argument);
return !!g_NetworkCommands[command];
}
/**
* Remember this warning for a few seconds.
* Overwrite previous warnings for this user.
*
* @param msg - GUI message sent by NetServer or NetClient
*/
function addNetworkWarning(msg)
{
if (!g_NetworkWarningTexts[msg.warntype])
{
warn("Unknown network warning type received: " + uneval(msg));
return;
}
if (Engine.ConfigDB_GetValue("user", "overlay.netwarnings") != "true")
return;
g_NetworkWarnings[msg.guid || "server"] = {
"added": Date.now(),
"msg": msg
};
}
/**
* Colorizes and concatenates all network warnings.
* Returns text and textWidth.
*/
function getNetworkWarnings()
{
// Remove outdated messages
for (let guid in g_NetworkWarnings)
if (Date.now() > g_NetworkWarnings[guid].added + g_NetworkWarningTimeout ||
guid != "server" && !g_PlayerAssignments[guid])
delete g_NetworkWarnings[guid];
// Show local messages first
let guids = Object.keys(g_NetworkWarnings).sort(guid => guid != "server");
let font = Engine.GetGUIObjectByName("gameStateNotifications").font;
let messages = [];
let maxTextWidth = 0;
for (let guid of guids)
{
let msg = g_NetworkWarnings[guid].msg;
// Add formatted text
messages.push(g_NetworkWarningTexts[msg.warntype](msg, colorizePlayernameByGUID(guid)));
// Add width of unformatted text
let username = guid != "server" && g_PlayerAssignments[guid].name;
let textWidth = Engine.GetTextWidth(font, g_NetworkWarningTexts[msg.warntype](msg, username));
maxTextWidth = Math.max(textWidth, maxTextWidth);
}
return {
"messages": messages,
"maxTextWidth": maxTextWidth
};
}
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js (revision 24727)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js (revision 24728)
@@ -1,142 +1,139 @@
/**
* 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 GameRegisterStanza
{
constructor(initData, setupWindow, netMessages, gameSettingsControl, mapCache)
{
this.mapCache = mapCache;
this.serverName = initData.serverName;
- this.serverPort = initData.serverPort;
- this.stunEndpoint = initData.stunEndpoint;
+ 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));
gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
netMessages.registerNetMessageHandler("start", this.onGameStart.bind(this));
}
onGameAttributesBatchChange()
{
if (this.lastStanza)
this.sendDelayed();
else
this.sendImmediately();
}
onGameStart()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
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;
if (this.timer !== undefined)
clearTimeout(this.timer);
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;
Engine.ProfileStart("sendRegisterGameStanza");
if (this.timer !== undefined)
{
clearTimeout(this.timer);
this.timer = undefined;
}
let clients = this.formatClientsForStanza();
let stanza = {
"name": this.serverName,
- "port": this.serverPort,
"hostUsername": Engine.LobbyGetNick(),
"mapName": g_GameAttributes.map,
"niceMapName": this.mapCache.getTranslatableMapName(g_GameAttributes.mapType, g_GameAttributes.map),
"mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default",
"mapType": g_GameAttributes.mapType,
"victoryConditions": g_GameAttributes.settings.VictoryConditions.join(","),
"nbp": clients.connectedPlayers,
"maxnbp": g_GameAttributes.settings.PlayerData.length,
"players": clients.list,
- "stunIP": this.stunEndpoint ? this.stunEndpoint.ip : "",
- "stunPort": this.stunEndpoint ? this.stunEndpoint.port : "",
- "mods": this.mods
+ "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.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
formatClientsForStanza()
{
let connectedPlayers = 0;
let playerData = [];
for (let guid in g_PlayerAssignments)
{
let pData = { "Name": g_PlayerAssignments[guid].name };
if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
++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.
*/
GameRegisterStanza.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 24727)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js (revision 24728)
@@ -1,378 +1,481 @@
/**
* 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 = "";
/**
- * Cached to pass it to the game setup of the controller to report the game to the lobby.
+ * Identifier if server is using password.
*/
-var g_ServerPort;
+var g_ServerHasPassword = false;
+
+var g_ServerId;
var g_IsRejoining = false;
var g_GameAttributes; // used when rejoining
var g_PlayerAssignments; // used when rejoining
var g_UserRating;
-/**
- * Object containing the IP address and port of the STUN server.
- */
-var g_StunEndpoint;
-
function init(attribs)
{
g_UserRating = attribs.rating;
switch (attribs.multiplayerGameType)
{
case "join":
{
- if (Engine.HasXmppClient())
+ if (!Engine.HasXmppClient())
{
- if (startJoin(attribs.name, attribs.ip, getValidPort(attribs.port), attribs.useSTUN, attribs.hostJID))
- switchSetupPage("pageConnecting");
- }
- else
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":
{
- Engine.GetGUIObjectByName("hostSTUNWrapper").hidden = !Engine.HasXmppClient();
- if (Engine.HasXmppClient())
+ 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, ""))
switchSetupPage("pageConnecting");
}
else if (!Engine.GetGUIObjectByName("pageHost").hidden)
{
- let hostPlayerName = Engine.GetGUIObjectByName("hostPlayerName").caption;
let hostServerName = Engine.GetGUIObjectByName("hostServerName").caption;
- let hostPort = Engine.GetGUIObjectByName("hostPort").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;
}
- if (startHost(hostPlayerName, hostServerName, getValidPort(hostPort)))
+ 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.");
+ default:
+ warn("Unknown connection failure reason: " + reason);
+ return sprintf(translate("\\[Invalid value %(reason)s]"), { "reason": id });
+ }
+}
+
+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 "gamesetup":
g_GameAttributes = message.data;
break;
case "players":
g_PlayerAssignments = message.newAssignments;
break;
case "start":
// Copy playernames from initial player assignment to the settings
for (let guid in g_PlayerAssignments)
{
let player = g_PlayerAssignments[guid];
if (player.player > 0) // not observer or GAIA
g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name;
}
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"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
-
+ // 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,
- "serverPort": g_ServerPort,
- "stunEndpoint": g_StunEndpoint
+ "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";
+ Engine.GetGUIObjectByName("continueButton").hidden = newPage == "pageConnecting" || newPage == "pagePassword";
}
-function startHost(playername, servername, port)
+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;
}
- if (Engine.HasXmppClient() && Engine.GetGUIObjectByName("useSTUN").checked)
- {
- g_StunEndpoint = Engine.FindStunEndpoint(port);
- if (!g_StunEndpoint)
- {
- cancelSetup();
- hostFeedback.caption = translate("Failed to host via STUN.");
- return false;
- }
- }
+ let useSTUN = Engine.HasXmppClient() && Engine.GetGUIObjectByName("useSTUN").checked;
try
{
- Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port, playername);
+ Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port, playername, 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_ServerPort = port;
+ g_ServerHasPassword = !!password;
if (Engine.HasXmppClient())
Engine.LobbySetPlayerPresence("playing");
return true;
}
/**
* Connects via STUN if the hostJID is given.
*/
function startJoin(playername, ip, port, useSTUN, hostJID)
{
try
{
Engine.StartNetworkJoin(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), ip, port, useSTUN, hostJID);
}
catch (e)
{
cancelSetup();
messageBox(
400, 200,
sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }),
translate("Error")
);
return false;
}
startConnectionStatus("client");
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");
}
return true;
}
+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/gamesetup_mp/gamesetup_mp.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml (revision 24727)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml (revision 24728)
@@ -1,137 +1,168 @@
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 24727)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js (revision 24728)
@@ -1,130 +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 ip;
- let port;
let stanza = game.stanza;
- if (stanza.stunIP)
- {
- ip = stanza.stunIP;
- port = stanza.stunPort;
- }
- else
- {
- ip = stanza.ip;
- port = stanza.port;
- }
-
- if (ip.split('.').length != 4)
- {
- messageBox(
- 400, 250,
- sprintf(
- translate("This game's address '%(ip)s' does not appear to be valid."),
- { "ip": escapeText(stanza.ip) }),
- translate("Error"));
- return;
- }
-
Engine.PushGuiPage("page_gamesetup_mp.xml", {
"multiplayerGameType": "join",
- "ip": ip,
- "port": port,
"name": g_Nickname,
"rating": this.getRejoinRating(stanza),
- "useSTUN": !!stanza.stunIP,
+ "hasPassword": !!stanza.hasPassword,
"hostJID": stanza.hostUsername + "@" + Engine.ConfigDB_GetValue("user", "lobby.server") + "/0ad"
});
}
openModSelectionPage()
{
Engine.StopXmppClient();
Engine.SwitchGuiPage("page_modmod.xml", {
"cancelbutton": true
});
}
/**
* Rejoin games with the original playername, even if the rating changed meanwhile.
*/
getRejoinRating(game)
{
for (let player of game.players)
{
let playerNickRating = splitRatingFromNick(player.Name);
if (playerNickRating.nick == g_Nickname)
return playerNickRating.rating;
}
return Engine.LobbyGetPlayerRating(g_Nickname);
}
}
// Translation: Join the game currently selected in the list.
JoinButton.prototype.Caption = translate("Join Game");
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js (revision 24727)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js (revision 24728)
@@ -1,341 +1,338 @@
/**
* This class represents a multiplayer match hosted by a player in the lobby.
* Having this represented as a class allows to leverage significant performance
* gains by caching computed, escaped, translated strings and sorting keys.
*
* Additionally class representation allows implementation of events such as
* a new match being hosted, a match having ended, or a buddy having joined a match.
*
* Ensure that escapeText is applied to player controlled data for display.
*
* Users of the properties of this class:
* GameList, GameDetails, MapFilters, JoinButton, any user of GameList.selectedGame()
*/
class Game
{
constructor(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");
displayData.mapName = escapeText(this.mapCache.translateMapName(newStanza.niceMapName));
displayData.mapDescription = this.mapCache.getTranslatedMapDescription(newStanza.mapType, newStanza.mapName);
Engine.ProfileStop();
}
if (oldStanza.mapName != newStanza.mapName)
{
Engine.ProfileStart("mapName");
sortValues.mapName = displayData.mapName;
Engine.ProfileStop();
}
if (oldStanza.mapType != newStanza.mapType)
{
Engine.ProfileStart("mapType");
displayData.mapType = g_MapTypes.Title[g_MapTypes.Name.indexOf(newStanza.mapType)] || "";
sortValues.mapType = newStanza.mapType;
Engine.ProfileStop();
}
if (oldStanza.mapSize != newStanza.mapSize)
{
Engine.ProfileStart("mapSize");
displayData.mapSize = translateMapSize(newStanza.mapSize);
sortValues.mapSize = newStanza.mapSize;
Engine.ProfileStop();
}
let playersChanged = oldStanza.players != newStanza.players;
if (playersChanged)
{
Engine.ProfileStart("playerData");
this.updatePlayers(newStanza);
Engine.ProfileStop();
}
if (oldStanza.nbp != newStanza.nbp ||
oldStanza.maxnbp != newStanza.maxnbp ||
playersChanged)
{
Engine.ProfileStart("playerCount");
displayData.playerCount = this.getTranslatedPlayerCount(newStanza);
sortValues.maxnbp = newStanza.maxnbp;
Engine.ProfileStop();
}
if (oldStanza.mods != newStanza.mods)
{
Engine.ProfileStart("mods");
this.updateMods(newStanza);
Engine.ProfileStop();
}
this.stanza = newStanza;
this.sortValue = this.sortValues[sortKey];
}
updatePlayers(newStanza)
{
let players;
{
Engine.ProfileStart("stringifiedTeamListToPlayerData");
players = stringifiedTeamListToPlayerData(newStanza.players);
this.players = players;
Engine.ProfileStop();
}
{
Engine.ProfileStart("parsePlayers");
let observerCount = 0;
let hasBuddies = 0;
let playerRatingTotal = 0;
for (let player of players)
{
let playerNickRating = splitRatingFromNick(player.Name);
if (player.Team == "observer")
++observerCount;
else
playerRatingTotal += playerNickRating.rating || g_DefaultLobbyRating;
// Sort games with playing buddies above games with spectating buddies
if (hasBuddies < 2 && g_Buddies.indexOf(playerNickRating.nick) != -1)
hasBuddies = player.Team == "observer" ? 1 : 2;
}
this.observerCount = observerCount;
this.hasBuddies = hasBuddies;
let displayData = this.displayData;
let sortValues = this.sortValues;
displayData.buddy = this.hasBuddies ? setStringTags(g_BuddySymbol, displayData.tags) : "";
sortValues.hasBuddyString = String(hasBuddies);
sortValues.buddy = sortValues.hasBuddyString + sortValues.gameName;
let playerCount = players.length - observerCount;
let gameRating =
playerCount ?
Math.round(playerRatingTotal / playerCount) :
g_DefaultLobbyRating;
this.gameRating = gameRating;
sortValues.gameRating = gameRating;
Engine.ProfileStop();
}
}
updateMods(newStanza)
{
{
Engine.ProfileStart("JSON.parse");
try
{
this.mods = JSON.parse(newStanza.mods);
}
catch (e)
{
this.mods = [];
}
Engine.ProfileStop();
}
{
Engine.ProfileStart("hasSameMods");
let isCompatible = this.mods && hasSameMods(this.mods, Engine.GetEngineInfo().mods);
if (this.isCompatible != isCompatible)
{
this.isCompatible = isCompatible;
this.updateGameTags(newStanza);
this.sortValues.compatibility = String(isCompatible);
}
Engine.ProfileStop();
}
}
updateGameTags(newStanza)
{
let displayData = this.displayData;
displayData.tags = this.isCompatible ? this.StateTags[newStanza.state] : this.IncompatibleTags;
displayData.buddy = this.hasBuddies ? setStringTags(g_BuddySymbol, displayData.tags) : "";
this.updateGameName(newStanza);
}
updateGameName(newStanza)
{
let displayData = this.displayData;
displayData.gameName = setStringTags(escapeText(newStanza.name), displayData.tags);
let sortValues = this.sortValues;
sortValues.gameName = sortValues.compatibility + sortValues.state + sortValues.gameName;
sortValues.buddy = sortValues.hasBuddyString + sortValues.gameName;
}
getTranslatedPlayerCount(newStanza)
{
let playerCountArgs = this.playerCountArgs;
playerCountArgs.current = setStringTags(escapeText(newStanza.nbp), this.PlayerCountTags.CurrentPlayers);
playerCountArgs.max = setStringTags(escapeText(newStanza.maxnbp), this.PlayerCountTags.MaxPlayers);
let txt;
if (this.observerCount)
{
playerCountArgs.observercount = setStringTags(this.observerCount, this.PlayerCountTags.Observers);
txt = this.PlayerCountObservers;
}
else
txt = this.PlayerCountNoObservers;
return sprintf(txt, playerCountArgs);
}
}
/**
* These are all keys that occur in a gamelist stanza sent by XPartaMupp.
*/
Game.prototype.StanzaKeys = [
"name",
- "ip",
- "port",
- "stunIP",
- "stunPort",
+ "hasPassword",
"hostUsername",
"state",
"nbp",
"maxnbp",
"players",
"mapName",
"niceMapName",
"mapSize",
"mapType",
"victoryConditions",
"startTime",
"mods"
];
/**
* Initial sorting order of the gamelist.
*/
Game.prototype.GameStatusOrder = [
"init",
"waiting",
"running"
];
// Translation: The number of players and observers in this game
Game.prototype.PlayerCountObservers = translate("%(current)s/%(max)s +%(observercount)s");
// Translation: The number of players in this game
Game.prototype.PlayerCountNoObservers = translate("%(current)s/%(max)s");
/**
* Compatible games will be listed in these colors.
*/
Game.prototype.StateTags = {
"init": {
"color": "0 219 0"
},
"waiting": {
"color": "255 127 0"
},
"running": {
"color": "219 0 0"
}
};
/**
* Games that require different mods than the ones launched by the current player are grayed out.
*/
Game.prototype.IncompatibleTags = {
"color": "gray"
};
/**
* Color for the player count number in the games list.
*/
Game.prototype.PlayerCountTags = {
"CurrentPlayers": {
"color": "0 160 160"
},
"MaxPlayers": {
"color": "0 160 160"
},
"Observers": {
"color": "0 128 128"
}
};
Index: ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js (revision 24727)
+++ ps/trunk/binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js (revision 24728)
@@ -1,223 +1,222 @@
/**
* 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_gameName = [];
this.list_mapName = [];
this.list_mapSize = [];
this.list_mapType = [];
this.list_maxnbp = [];
this.list_gameRating = [];
this.list = [];
this.filters = [];
for (let name in GameListFilters)
this.filters.push(new GameListFilters[name](this.onFilterChange.bind(this)));
xmppMessages.registerXmppMessageHandler("game", "gamelist", this.rebuildGameList.bind(this));
xmppMessages.registerXmppMessageHandler("system", "disconnected", this.rebuildGameList.bind(this));
buddyButton.registerBuddyChangeHandler(this.onBuddyChange.bind(this));
this.rebuildGameList();
}
registerSelectionChangeHandler(handler)
{
this.selectionChangeHandlers.add(handler);
}
onFilterChange()
{
this.rebuildGameList();
}
onBuddyChange()
{
for (let name in this.games)
this.games[name].onBuddyChange();
this.rebuildGameList();
}
onSelectionChange()
{
let game = this.selectedGame();
for (let handler of this.selectionChangeHandlers)
handler(game);
}
selectedGame()
{
return this.gameList[this.gamesBox.selected] || undefined;
}
rebuildGameList()
{
Engine.ProfileStart("rebuildGameList");
Engine.ProfileStart("getGameList");
let selectedGame = this.selectedGame();
let gameListData = Engine.GetGameList();
Engine.ProfileStop();
{
Engine.ProfileStart("updateGames");
let selectedColumn = this.gamesBox.selected_column;
let newGames = {};
for (let stanza of gameListData)
{
- let game = this.games[stanza.ip] || undefined;
+ let game = this.games[stanza.hostUsername] || undefined;
let exists = !!game;
if (!exists)
game = new Game(this.mapCache);
game.update(stanza, selectedColumn);
- newGames[stanza.ip] = game;
+ newGames[stanza.hostUsername] = game;
}
this.games = newGames;
Engine.ProfileStop();
}
{
Engine.ProfileStart("filterGameList");
this.gameList.length = 0;
for (let ip in this.games)
{
let game = this.games[ip];
if (this.filters.every(filter => filter.filter(game)))
this.gameList.push(game);
}
Engine.ProfileStop();
}
{
Engine.ProfileStart("sortGameList");
let sortOrder = this.gamesBox.selected_column_order;
this.gameList.sort((game1, game2) => {
if (game1.sortValue < game2.sortValue) return -sortOrder;
if (game1.sortValue > game2.sortValue) return +sortOrder;
return 0;
});
Engine.ProfileStop();
}
let selectedGameIndex = -1;
{
Engine.ProfileStart("setupGameList");
let length = this.gameList.length;
this.list_buddy.length = length;
this.list_gameName.length = length;
this.list_mapName.length = length;
this.list_mapSize.length = length;
this.list_mapType.length = length;
this.list_maxnbp.length = length;
this.list_gameRating.length = length;
this.list.length = length;
this.gameList.forEach((game, i) => {
let displayData = game.displayData;
this.list_buddy[i] = displayData.buddy;
this.list_gameName[i] = displayData.gameName;
this.list_mapName[i] = displayData.mapName;
this.list_mapSize[i] = displayData.mapSize;
this.list_mapType[i] = displayData.mapType;
this.list_maxnbp[i] = displayData.playerCount;
this.list_gameRating[i] = game.gameRating;
this.list[i] = "";
-
- if (selectedGame && game.stanza.ip == selectedGame.stanza.ip && game.stanza.port == selectedGame.stanza.port)
+ if (selectedGame && game.hostUsername == selectedGame.hostUsername && game.stanza.gameName == selectedGame.stanza.gameName)
selectedGameIndex = i;
});
Engine.ProfileStop();
}
{
Engine.ProfileStart("copyToGUI");
let gamesBox = this.gamesBox;
gamesBox.list_buddy = this.list_buddy;
gamesBox.list_gameName = this.list_gameName;
gamesBox.list_mapName = this.list_mapName;
gamesBox.list_mapSize = this.list_mapSize;
gamesBox.list_mapType = this.list_mapType;
gamesBox.list_maxnbp = this.list_maxnbp;
gamesBox.list_gameRating = this.list_gameRating;
// Change these last, otherwise crash
gamesBox.list = this.list;
gamesBox.list_data = this.list;
gamesBox.auto_scroll = false;
Engine.ProfileStop();
gamesBox.selected = selectedGameIndex;
}
Engine.ProfileStop();
}
/**
* Select the game where the selected player is currently playing, observing or offline.
* Selects in that order to account for players that occur in multiple games.
*/
selectGameFromPlayername(playerName)
{
if (!playerName)
return;
let foundAsObserver = false;
for (let i = 0; i < this.gameList.length; ++i)
for (let player of this.gameList[i].players)
{
if (playerName != splitRatingFromNick(player.Name).nick)
continue;
this.gamesBox.auto_scroll = true;
if (player.Team == "observer")
{
foundAsObserver = true;
this.gamesBox.selected = i;
}
else if (!player.Offline)
{
this.gamesBox.selected = i;
return;
}
if (!foundAsObserver)
this.gamesBox.selected = i;
}
}
}
Index: ps/trunk/build/premake/premake5.lua
===================================================================
--- ps/trunk/build/premake/premake5.lua (revision 24727)
+++ ps/trunk/build/premake/premake5.lua (revision 24728)
@@ -1,1468 +1,1469 @@
newoption { trigger = "android", description = "Use non-working Android cross-compiling mode" }
newoption { trigger = "atlas", description = "Include Atlas scenario editor projects" }
newoption { trigger = "coverage", description = "Enable code coverage data collection (GCC only)" }
newoption { trigger = "gles", description = "Use non-working OpenGL ES 2.0 mode" }
newoption { trigger = "icc", description = "Use Intel C++ Compiler (Linux only; should use either \"--cc icc\" or --without-pch too, and then set CXX=icpc before calling make)" }
newoption { trigger = "jenkins-tests", description = "Configure CxxTest to use the XmlPrinter runner which produces Jenkins-compatible output" }
newoption { trigger = "minimal-flags", description = "Only set compiler/linker flags that are really needed. Has no effect on Windows builds" }
newoption { trigger = "outpath", description = "Location for generated project files" }
newoption { trigger = "with-system-mozjs", description = "Search standard paths for libmozjs60, instead of using bundled copy" }
newoption { trigger = "with-system-nvtt", description = "Search standard paths for nvidia-texture-tools library, instead of using bundled copy" }
newoption { trigger = "without-audio", description = "Disable use of OpenAL/Ogg/Vorbis APIs" }
newoption { trigger = "without-lobby", description = "Disable the use of gloox and the multiplayer lobby" }
newoption { trigger = "without-miniupnpc", description = "Disable use of miniupnpc for port forwarding" }
newoption { trigger = "without-nvtt", description = "Disable use of NVTT" }
newoption { trigger = "without-pch", description = "Disable generation and usage of precompiled headers" }
newoption { trigger = "without-tests", description = "Disable generation of test projects" }
-- Linux/BSD specific options
newoption { trigger = "prefer-local-libs", description = "Prefer locally built libs. Any local libraries used must also be listed within a file within /etc/ld.so.conf.d so the dynamic linker can find them at runtime." }
-- OS X specific options
newoption { trigger = "macosx-version-min", description = "Set minimum required version of the OS X API, the build will possibly fail if an older SDK is used, while newer API functions will be weakly linked (i.e. resolved at runtime)" }
newoption { trigger = "sysroot", description = "Set compiler system root path, used for building against a non-system SDK. For example /usr/local becomes SYSROOT/user/local" }
-- Windows specific options
newoption { trigger = "build-shared-glooxwrapper", description = "Rebuild glooxwrapper DLL for Windows. Requires the same compiler version that gloox was built with" }
newoption { trigger = "use-shared-glooxwrapper", description = "Use prebuilt glooxwrapper DLL for Windows" }
newoption { trigger = "large-address-aware", description = "Make the executable large address aware. Do not use for development, in order to spot memory issues easily" }
-- Install options
newoption { trigger = "bindir", description = "Directory for executables (typically '/usr/games'); default is to be relocatable" }
newoption { trigger = "datadir", description = "Directory for data files (typically '/usr/share/games/0ad'); default is ../data/ relative to executable" }
newoption { trigger = "libdir", description = "Directory for libraries (typically '/usr/lib/games/0ad'); default is ./ relative to executable" }
-- Root directory of project checkout relative to this .lua file
rootdir = "../.."
dofile("extern_libs5.lua")
-- detect compiler for non-Windows
if os.istarget("macosx") then
cc = "clang"
elseif os.istarget("linux") and _OPTIONS["icc"] then
cc = "icc"
elseif os.istarget("bsd") and os.getversion().description == "FreeBSD" then
cc = "clang"
elseif not os.istarget("windows") then
cc = os.getenv("CC")
if cc == nil or cc == "" then
local hasgcc = os.execute("which gcc > .gccpath")
local f = io.open(".gccpath", "r")
local gccpath = f:read("*line")
f:close()
os.execute("rm .gccpath")
if gccpath == nil then
cc = "clang"
else
cc = "gcc"
end
end
end
-- detect CPU architecture (simplistic, currently only supports x86, amd64 and ARM)
arch = "x86"
if _OPTIONS["android"] then
arch = "arm"
elseif os.istarget("windows") then
if os.getenv("PROCESSOR_ARCHITECTURE") == "amd64" or os.getenv("PROCESSOR_ARCHITEW6432") == "amd64" then
arch = "amd64"
end
else
arch = os.getenv("HOSTTYPE")
if arch == "x86_64" or arch == "amd64" then
arch = "amd64"
else
os.execute(cc .. " -dumpmachine > .gccmachine.tmp")
local f = io.open(".gccmachine.tmp", "r")
local machine = f:read("*line")
f:close()
if string.find(machine, "x86_64") == 1 or string.find(machine, "amd64") == 1 then
arch = "amd64"
elseif string.find(machine, "i.86") == 1 then
arch = "x86"
elseif string.find(machine, "arm") == 1 then
arch = "arm"
elseif string.find(machine, "aarch64") == 1 then
arch = "aarch64"
elseif string.find(machine, "e2k") == 1 then
arch = "e2k"
else
print("WARNING: Cannot determine architecture from GCC, assuming x86")
end
end
end
-- Test whether we need to link libexecinfo.
-- This is mostly the case on musl systems, as well as on BSD systems : only glibc provides the
-- backtrace symbols we require in the libc, for other libcs we use the libexecinfo library.
local link_execinfo = false
if os.istarget("bsd") then
link_execinfo = true
elseif os.istarget("linux") then
local _, link_errorCode = os.outputof(cc .. " ./tests/execinfo.c -o /dev/null")
if link_errorCode ~= 0 then
link_execinfo = true
end
end
-- Set up the Workspace
workspace "pyrogenesis"
targetdir(rootdir.."/binaries/system")
libdirs(rootdir.."/binaries/system")
if not _OPTIONS["outpath"] then
error("You must specify the 'outpath' parameter")
end
location(_OPTIONS["outpath"])
configurations { "Release", "Debug" }
source_root = rootdir.."/source/" -- default for most projects - overridden by local in others
-- Rationale: projects should not have any additional include paths except for
-- those required by external libraries. Instead, we should always write the
-- full relative path, e.g. #include "maths/Vector3d.h". This avoids confusion
-- ("which file is meant?") and avoids enormous include path lists.
-- projects: engine static libs, main exe, atlas, atlas frontends, test.
--------------------------------------------------------------------------------
-- project helper functions
--------------------------------------------------------------------------------
function project_set_target(project_name)
-- Note: On Windows, ".exe" is added on the end, on unices the name is used directly
local obj_dir_prefix = _OPTIONS["outpath"].."/obj/"..project_name.."_"
filter "Debug"
objdir(obj_dir_prefix.."Debug")
targetsuffix("_dbg")
filter "Release"
objdir(obj_dir_prefix.."Release")
filter { }
end
function project_set_build_flags()
editandcontinue "Off"
if not _OPTIONS["minimal-flags"] then
symbols "On"
end
if cc ~= "icc" and (os.istarget("windows") or not _OPTIONS["minimal-flags"]) then
-- adds the -Wall compiler flag
warnings "Extra" -- this causes far too many warnings/remarks on ICC
end
-- disable Windows debug heap, since it makes malloc/free hugely slower when
-- running inside a debugger
if os.istarget("windows") then
debugenvs { "_NO_DEBUG_HEAP=1" }
end
filter "Debug"
defines { "DEBUG" }
filter "Release"
if os.istarget("windows") or not _OPTIONS["minimal-flags"] then
optimize "Speed"
end
defines { "NDEBUG", "CONFIG_FINAL=1" }
filter { }
if _OPTIONS["gles"] then
defines { "CONFIG2_GLES=1" }
end
if _OPTIONS["without-audio"] then
defines { "CONFIG2_AUDIO=0" }
end
if _OPTIONS["without-nvtt"] then
defines { "CONFIG2_NVTT=0" }
end
if _OPTIONS["without-lobby"] then
defines { "CONFIG2_LOBBY=0" }
end
if _OPTIONS["without-miniupnpc"] then
defines { "CONFIG2_MINIUPNPC=0" }
end
-- required for the lowlevel library. must be set from all projects that use it, otherwise it assumes it is
-- being used as a DLL (which is currently not the case in 0ad)
defines { "LIB_STATIC_LINK" }
-- Enable C++17 standard.
filter "action:vs*"
buildoptions { "/std:c++17" }
filter "action:not vs*"
buildoptions { "-std=c++17" }
filter {}
-- various platform-specific build flags
if os.istarget("windows") then
flags { "MultiProcessorCompile" }
-- Since KB4088875 Windows 7 has a soft requirement for SSE2.
-- Windows 8+ and Firefox ESR52 make it hard requirement.
-- Finally since VS2012 it's enabled implicitely when not set.
vectorextensions "SSE2"
-- use native wchar_t type (not typedef to unsigned short)
nativewchar "on"
else -- *nix
-- TODO, FIXME: This check is incorrect because it means that some additional flags will be added inside the "else" branch if the
-- compiler is ICC and minimal-flags is specified (ticket: #2994)
if cc == "icc" and not _OPTIONS["minimal-flags"] then
buildoptions {
"-w1",
-- "-Wabi",
-- "-Wp64", -- complains about OBJECT_TO_JSVAL which is annoying
"-Wpointer-arith",
"-Wreturn-type",
-- "-Wshadow",
"-Wuninitialized",
"-Wunknown-pragmas",
"-Wunused-function",
"-wd1292" -- avoid lots of 'attribute "__nonnull__" ignored'
}
filter "Debug"
buildoptions { "-O0" } -- ICC defaults to -O2
filter { }
if os.istarget("macosx") then
linkoptions { "-multiply_defined","suppress" }
end
else
-- exclude most non-essential build options for minimal-flags
if not _OPTIONS["minimal-flags"] then
buildoptions {
-- enable most of the standard warnings
"-Wno-switch", -- enumeration value not handled in switch (this is sometimes useful, but results in lots of noise)
"-Wno-reorder", -- order of initialization list in constructors (lots of noise)
"-Wno-invalid-offsetof", -- offsetof on non-POD types (see comment in renderer/PatchRData.cpp)
"-Wextra",
"-Wno-missing-field-initializers", -- (this is common in external headers we can't fix)
-- add some other useful warnings that need to be enabled explicitly
"-Wunused-parameter",
"-Wredundant-decls", -- (useful for finding some multiply-included header files)
-- "-Wformat=2", -- (useful sometimes, but a bit noisy, so skip it by default)
-- "-Wcast-qual", -- (useful for checking const-correctness, but a bit noisy, so skip it by default)
"-Wnon-virtual-dtor", -- (sometimes noisy but finds real bugs)
"-Wundef", -- (useful for finding macro name typos)
-- enable security features (stack checking etc) that shouldn't have
-- a significant effect on performance and can catch bugs
"-fstack-protector-all",
"-U_FORTIFY_SOURCE", -- (avoid redefinition warning if already defined)
"-D_FORTIFY_SOURCE=2",
-- always enable strict aliasing (useful in debug builds because of the warnings)
"-fstrict-aliasing",
-- don't omit frame pointers (for now), because performance will be impacted
-- negatively by the way this breaks profilers more than it will be impacted
-- positively by the optimisation
"-fno-omit-frame-pointer"
}
if not _OPTIONS["without-pch"] then
buildoptions {
-- do something (?) so that ccache can handle compilation with PCH enabled
-- (ccache 3.1+ also requires CCACHE_SLOPPINESS=time_macros for this to work)
"-fpch-preprocess"
}
end
if os.istarget("linux") or os.istarget("bsd") then
buildoptions { "-fPIC" }
linkoptions { "-Wl,--no-undefined", "-Wl,--as-needed", "-Wl,-z,relro" }
end
if arch == "x86" then
buildoptions {
-- To support intrinsics like __sync_bool_compare_and_swap on x86
-- we need to set -march to something that supports them (i686).
-- We use pentium3 to also enable other features like mmx and sse,
-- while tuning for generic to have good performance on every
-- supported CPU.
-- Note that all these features are already supported on amd64.
"-march=pentium3 -mtune=generic"
}
end
end
if arch == "arm" then
-- disable warnings about va_list ABI change and use
-- compile-time flags for futher configuration.
buildoptions { "-Wno-psabi" }
if _OPTIONS["android"] then
-- Android uses softfp, so we should too.
buildoptions { "-mfloat-abi=softfp" }
end
end
if _OPTIONS["coverage"] then
buildoptions { "-fprofile-arcs", "-ftest-coverage" }
links { "gcov" }
end
-- MacOS 10.12 only supports processors with SSE 4.1, so enable that.
if os.istarget("macosx") then
buildoptions { "-msse4.1" }
end
-- Check if SDK path should be used
if _OPTIONS["sysroot"] then
buildoptions { "-isysroot " .. _OPTIONS["sysroot"] }
linkoptions { "-Wl,-syslibroot," .. _OPTIONS["sysroot"] }
end
-- On OS X, sometimes we need to specify the minimum API version to use
if _OPTIONS["macosx-version-min"] then
buildoptions { "-mmacosx-version-min=" .. _OPTIONS["macosx-version-min"] }
-- clang and llvm-gcc look at mmacosx-version-min to determine link target
-- and CRT version, and use it to set the macosx_version_min linker flag
linkoptions { "-mmacosx-version-min=" .. _OPTIONS["macosx-version-min"] }
end
-- Only libc++ is supported on MacOS
if os.istarget("macosx") then
buildoptions { "-stdlib=libc++" }
linkoptions { "-stdlib=libc++" }
end
end
buildoptions {
-- Hide symbols in dynamic shared objects by default, for efficiency and for equivalence with
-- Windows - they should be exported explicitly with __attribute__ ((visibility ("default")))
"-fvisibility=hidden"
}
if _OPTIONS["bindir"] then
defines { "INSTALLED_BINDIR=" .. _OPTIONS["bindir"] }
end
if _OPTIONS["datadir"] then
defines { "INSTALLED_DATADIR=" .. _OPTIONS["datadir"] }
end
if _OPTIONS["libdir"] then
defines { "INSTALLED_LIBDIR=" .. _OPTIONS["libdir"] }
end
if os.istarget("linux") or os.istarget("bsd") then
if _OPTIONS["prefer-local-libs"] then
libdirs { "/usr/local/lib" }
end
-- To use our local shared libraries, they need to be found in the
-- runtime dynamic linker path. Add their path to -rpath.
if _OPTIONS["libdir"] then
linkoptions {"-Wl,-rpath," .. _OPTIONS["libdir"] }
else
-- On FreeBSD we need to allow use of $ORIGIN
if os.istarget("bsd") then
linkoptions { "-Wl,-z,origin" }
end
-- Adding the executable path and taking care of correct escaping
if _ACTION == "gmake" then
linkoptions { "-Wl,-rpath,'$$ORIGIN'" }
elseif _ACTION == "codeblocks" then
linkoptions { "-Wl,-R\\\\$$$ORIGIN" }
end
end
end
end
end
-- create a project and set the attributes that are common to all projects.
function project_create(project_name, target_type)
project(project_name)
language "C++"
kind(target_type)
filter "action:vs2017"
toolset "v141_xp"
filter {}
filter "action:vs*"
buildoptions "/utf-8"
filter {}
project_set_target(project_name)
project_set_build_flags()
end
-- OSX creates a .app bundle if the project type of the main application is set to "WindowedApp".
-- We don't want this because this bundle would be broken (it lacks all the resources and external dependencies, Info.plist etc...)
-- Windows opens a console in the background if it's set to ConsoleApp, which is not what we want.
-- I didn't check if this setting matters for linux, but WindowedApp works there.
function get_main_project_target_type()
if _OPTIONS["android"] then
return "SharedLib"
elseif os.istarget("macosx") then
return "ConsoleApp"
else
return "WindowedApp"
end
end
-- source_root: rel_source_dirs and rel_include_dirs are relative to this directory
-- rel_source_dirs: A table of subdirectories. All source files in these directories are added.
-- rel_include_dirs: A table of subdirectories to be included.
-- extra_params: table including zero or more of the following:
-- * no_pch: If specified, no precompiled headers are used for this project.
-- * pch_dir: If specified, this directory will be used for precompiled headers instead of the default
-- /pch//.
-- * extra_files: table of filenames (relative to source_root) to add to project
-- * extra_links: table of library names to add to link step
function project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params)
for i,v in pairs(rel_source_dirs) do
local prefix = source_root..v.."/"
files { prefix.."*.cpp", prefix.."*.h", prefix.."*.inl", prefix.."*.js", prefix.."*.asm", prefix.."*.mm" }
end
-- Put the project-specific PCH directory at the start of the
-- include path, so '#include "precompiled.h"' will look in
-- there first
local pch_dir
if not extra_params["pch_dir"] then
pch_dir = source_root .. "pch/" .. project().name .. "/"
else
pch_dir = extra_params["pch_dir"]
end
includedirs { pch_dir }
-- Precompiled Headers
-- rationale: we need one PCH per static lib, since one global header would
-- increase dependencies. To that end, we can either include them as
-- "projectdir/precompiled.h", or add "source/PCH/projectdir" to the
-- include path and put the PCH there. The latter is better because
-- many projects contain several dirs and it's unclear where there the
-- PCH should be stored. This way is also a bit easier to use in that
-- source files always include "precompiled.h".
-- Notes:
-- * Visual Assist manages to use the project include path and can
-- correctly open these files from the IDE.
-- * precompiled.cpp (needed to "Create" the PCH) also goes in
-- the abovementioned dir.
if (not _OPTIONS["without-pch"] and not extra_params["no_pch"]) then
filter "action:vs*"
pchheader("precompiled.h")
filter "action:xcode*"
pchheader("../"..pch_dir.."precompiled.h")
filter { "action:not vs*", "action:not xcode*" }
pchheader(pch_dir.."precompiled.h")
filter {}
pchsource(pch_dir.."precompiled.cpp")
defines { "CONFIG_ENABLE_PCH=1" }
files { pch_dir.."precompiled.h", pch_dir.."precompiled.cpp" }
else
defines { "CONFIG_ENABLE_PCH=0" }
flags { "NoPCH" }
end
-- next is source root dir, for absolute (nonrelative) includes
-- (e.g. "lib/precompiled.h")
includedirs { source_root }
for i,v in pairs(rel_include_dirs) do
includedirs { source_root .. v }
end
if extra_params["extra_files"] then
for i,v in pairs(extra_params["extra_files"]) do
-- .rc files are only needed on Windows
if path.getextension(v) ~= ".rc" or os.istarget("windows") then
files { source_root .. v }
end
end
end
if extra_params["extra_links"] then
links { extra_params["extra_links"] }
end
end
-- Add command-line options to set up the manifest dependencies for Windows
-- (See lib/sysdep/os/win/manifest.cpp)
function project_add_manifest()
linkoptions { "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='X86' publicKeyToken='6595b64144ccf1df'\"" }
end
--------------------------------------------------------------------------------
-- engine static libraries
--------------------------------------------------------------------------------
-- the engine is split up into several static libraries. this eases separate
-- distribution of those components, reduces dependencies a bit, and can
-- also speed up builds.
-- more to the point, it is necessary to efficiently support a separate
-- test executable that also includes much of the game code.
-- names of all static libs created. automatically added to the
-- main app project later (see explanation at end of this file)
static_lib_names = {}
static_lib_names_debug = {}
static_lib_names_release = {}
-- set up one of the static libraries into which the main engine code is split.
-- extra_params:
-- no_default_link: If specified, linking won't be done by default.
-- For the rest of extra_params, see project_add_contents().
-- note: rel_source_dirs and rel_include_dirs are relative to global source_root.
function setup_static_lib_project (project_name, rel_source_dirs, extern_libs, extra_params)
local target_type = "StaticLib"
project_create(project_name, target_type)
project_add_contents(source_root, rel_source_dirs, {}, extra_params)
project_add_extern_libs(extern_libs, target_type)
if not extra_params["no_default_link"] then
table.insert(static_lib_names, project_name)
end
-- Deactivate Run Time Type Information. Performance of dynamic_cast is very poor.
-- The exception to this principle is Atlas UI, which is not a static library.
rtti "off"
if os.istarget("macosx") and _OPTIONS["macosx-version-min"] then
xcodebuildsettings { MACOSX_DEPLOYMENT_TARGET = _OPTIONS["macosx-version-min"] }
end
end
function setup_third_party_static_lib_project (project_name, rel_source_dirs, extern_libs, extra_params)
setup_static_lib_project(project_name, rel_source_dirs, extern_libs, extra_params)
includedirs { source_root .. "third_party/" .. project_name .. "/include/" }
end
function setup_shared_lib_project (project_name, rel_source_dirs, extern_libs, extra_params)
local target_type = "SharedLib"
project_create(project_name, target_type)
project_add_contents(source_root, rel_source_dirs, {}, extra_params)
project_add_extern_libs(extern_libs, target_type)
if not extra_params["no_default_link"] then
table.insert(static_lib_names, project_name)
end
if os.istarget("windows") then
links { "delayimp" }
elseif os.istarget("macosx") and _OPTIONS["macosx-version-min"] then
xcodebuildsettings { MACOSX_DEPLOYMENT_TARGET = _OPTIONS["macosx-version-min"] }
end
end
-- this is where the source tree is chopped up into static libs.
-- can be changed very easily; just copy+paste a new setup_static_lib_project,
-- or remove existing ones. static libs are automagically added to
-- main_exe link step.
function setup_all_libs ()
-- relative to global source_root.
local source_dirs = {}
-- names of external libraries used (see libraries_dir comment)
local extern_libs = {}
source_dirs = {
"network",
}
extern_libs = {
"spidermonkey",
"enet",
+ "sdl",
"boost", -- dragged in via server->simulation.h->random and NetSession.h->lockfree
"fmt",
}
if not _OPTIONS["without-miniupnpc"] then
table.insert(extern_libs, "miniupnpc")
end
setup_static_lib_project("network", source_dirs, extern_libs, {})
source_dirs = {
"rlinterface",
}
extern_libs = {
"boost", -- dragged in via simulation.h and scriptinterface.h
"fmt",
"spidermonkey",
}
setup_static_lib_project("rlinterface", source_dirs, extern_libs, { no_pch = 1 })
source_dirs = {
"third_party/tinygettext/src",
}
extern_libs = {
"iconv",
"boost",
"fmt",
}
setup_third_party_static_lib_project("tinygettext", source_dirs, extern_libs, { } )
-- it's an external library and we don't want to modify its source to fix warnings, so we just disable them to avoid noise in the compile output
filter "action:vs*"
buildoptions {
"/wd4127",
"/wd4309",
"/wd4800",
"/wd4100",
"/wd4996",
"/wd4099",
"/wd4503"
}
filter {}
if not _OPTIONS["without-lobby"] then
source_dirs = {
"lobby",
"lobby/scripting",
"i18n",
"third_party/encryption"
}
extern_libs = {
"spidermonkey",
"boost",
"enet",
"gloox",
"icu",
"iconv",
"libsodium",
"tinygettext",
"fmt",
}
setup_static_lib_project("lobby", source_dirs, extern_libs, {})
if _OPTIONS["use-shared-glooxwrapper"] and not _OPTIONS["build-shared-glooxwrapper"] then
table.insert(static_lib_names_debug, "glooxwrapper_dbg")
table.insert(static_lib_names_release, "glooxwrapper")
else
source_dirs = {
"lobby/glooxwrapper",
}
extern_libs = {
"boost",
"gloox",
"fmt",
}
if _OPTIONS["build-shared-glooxwrapper"] then
setup_shared_lib_project("glooxwrapper", source_dirs, extern_libs, {})
else
setup_static_lib_project("glooxwrapper", source_dirs, extern_libs, {})
end
end
else
source_dirs = {
"lobby/scripting",
"third_party/encryption"
}
extern_libs = {
"spidermonkey",
"boost",
"libsodium",
"fmt",
}
setup_static_lib_project("lobby", source_dirs, extern_libs, {})
files { source_root.."lobby/Globals.cpp" }
end
source_dirs = {
"simulation2",
"simulation2/components",
"simulation2/helpers",
"simulation2/scripting",
"simulation2/serialization",
"simulation2/system",
"simulation2/testcomponents",
}
extern_libs = {
"boost",
"opengl",
"spidermonkey",
"fmt",
}
setup_static_lib_project("simulation2", source_dirs, extern_libs, {})
source_dirs = {
"scriptinterface",
"scriptinterface/third_party"
}
extern_libs = {
"boost",
"spidermonkey",
"valgrind",
"sdl",
"fmt",
}
setup_static_lib_project("scriptinterface", source_dirs, extern_libs, {})
source_dirs = {
"ps",
"ps/scripting",
"network/scripting",
"ps/GameSetup",
"ps/XML",
"soundmanager",
"soundmanager/data",
"soundmanager/items",
"soundmanager/scripting",
"maths",
"maths/scripting",
"i18n",
"i18n/scripting",
}
extern_libs = {
"spidermonkey",
"sdl", -- key definitions
"libxml2",
"opengl",
"zlib",
"boost",
"enet",
"libcurl",
"tinygettext",
"icu",
"iconv",
"libsodium",
"fmt",
}
if not _OPTIONS["without-audio"] then
table.insert(extern_libs, "openal")
table.insert(extern_libs, "vorbis")
end
setup_static_lib_project("engine", source_dirs, extern_libs, {})
source_dirs = {
"graphics",
"graphics/scripting",
"renderer",
"renderer/scripting",
"third_party/mikktspace",
"third_party/ogre3d_preprocessor"
}
extern_libs = {
"opengl",
"sdl", -- key definitions
"spidermonkey", -- for graphics/scripting
"boost",
"fmt",
}
if not _OPTIONS["without-nvtt"] then
table.insert(extern_libs, "nvtt")
end
setup_static_lib_project("graphics", source_dirs, extern_libs, {})
source_dirs = {
"tools/atlas/GameInterface",
"tools/atlas/GameInterface/Handlers"
}
extern_libs = {
"boost",
"sdl", -- key definitions
"opengl",
"spidermonkey",
"fmt",
}
setup_static_lib_project("atlas", source_dirs, extern_libs, {})
source_dirs = {
"gui",
"gui/ObjectTypes",
"gui/ObjectBases",
"gui/Scripting",
"gui/SettingTypes",
"i18n"
}
extern_libs = {
"spidermonkey",
"sdl", -- key definitions
"opengl",
"boost",
"enet",
"tinygettext",
"icu",
"iconv",
"fmt",
}
if not _OPTIONS["without-audio"] then
table.insert(extern_libs, "openal")
end
setup_static_lib_project("gui", source_dirs, extern_libs, {})
source_dirs = {
"lib",
"lib/adts",
"lib/allocators",
"lib/external_libraries",
"lib/file",
"lib/file/archive",
"lib/file/common",
"lib/file/io",
"lib/file/vfs",
"lib/pch",
"lib/posix",
"lib/res",
"lib/res/graphics",
"lib/sysdep",
"lib/tex"
}
extern_libs = {
"boost",
"sdl",
"openal",
"opengl",
"libpng",
"zlib",
"valgrind",
"cxxtest",
"fmt",
}
-- CPU architecture-specific
if arch == "amd64" then
table.insert(source_dirs, "lib/sysdep/arch/amd64");
table.insert(source_dirs, "lib/sysdep/arch/x86_x64");
elseif arch == "x86" then
table.insert(source_dirs, "lib/sysdep/arch/ia32");
table.insert(source_dirs, "lib/sysdep/arch/x86_x64");
elseif arch == "arm" then
table.insert(source_dirs, "lib/sysdep/arch/arm");
elseif arch == "aarch64" then
table.insert(source_dirs, "lib/sysdep/arch/aarch64");
elseif arch == "e2k" then
table.insert(source_dirs, "lib/sysdep/arch/e2k");
end
-- OS-specific
sysdep_dirs = {
linux = { "lib/sysdep/os/linux", "lib/sysdep/os/unix" },
-- note: RC file must be added to main_exe project.
-- note: don't add "lib/sysdep/os/win/aken.cpp" because that must be compiled with the DDK.
windows = { "lib/sysdep/os/win", "lib/sysdep/os/win/wposix", "lib/sysdep/os/win/whrt" },
macosx = { "lib/sysdep/os/osx", "lib/sysdep/os/unix" },
bsd = { "lib/sysdep/os/bsd", "lib/sysdep/os/unix", "lib/sysdep/os/unix/x" },
}
for i,v in pairs(sysdep_dirs[os.target()]) do
table.insert(source_dirs, v);
end
if os.istarget("linux") then
if _OPTIONS["android"] then
table.insert(source_dirs, "lib/sysdep/os/android")
else
table.insert(source_dirs, "lib/sysdep/os/unix/x")
end
end
-- On OSX, disable precompiled headers because C++ files and Objective-C++ files are
-- mixed in this project. To fix that, we would need per-file basis configuration which
-- is not yet supported by the gmake action in premake. We should look into using gmake2.
extra_params = {}
if os.istarget("macosx") then
extra_params = { no_pch = 1 }
end
-- runtime-library-specific
if _ACTION == "vs2017" then
table.insert(source_dirs, "lib/sysdep/rtl/msc");
else
table.insert(source_dirs, "lib/sysdep/rtl/gcc");
end
setup_static_lib_project("lowlevel", source_dirs, extern_libs, extra_params)
-- Third-party libraries that are built as part of the main project,
-- not built externally and then linked
source_dirs = {
"third_party/mongoose",
}
extern_libs = {
}
setup_static_lib_project("mongoose", source_dirs, extern_libs, { no_pch = 1 })
-- CxxTest mock function support
extern_libs = {
"boost",
"cxxtest",
}
-- 'real' implementations, to be linked against the main executable
-- (files are added manually and not with setup_static_lib_project
-- because not all files in the directory are included)
setup_static_lib_project("mocks_real", {}, extern_libs, { no_default_link = 1, no_pch = 1 })
files { "mocks/*.h", source_root.."mocks/*_real.cpp" }
-- 'test' implementations, to be linked against the test executable
setup_static_lib_project("mocks_test", {}, extern_libs, { no_default_link = 1, no_pch = 1 })
files { source_root.."mocks/*.h", source_root.."mocks/*_test.cpp" }
end
--------------------------------------------------------------------------------
-- main EXE
--------------------------------------------------------------------------------
-- used for main EXE as well as test
used_extern_libs = {
"opengl",
"sdl",
"libpng",
"zlib",
"spidermonkey",
"libxml2",
"boost",
"cxxtest",
"comsuppw",
"enet",
"libcurl",
"tinygettext",
"icu",
"iconv",
"libsodium",
"fmt",
"valgrind",
}
if not os.istarget("windows") and not _OPTIONS["android"] and not os.istarget("macosx") then
-- X11 should only be linked on *nix
table.insert(used_extern_libs, "x11")
end
if not _OPTIONS["without-audio"] then
table.insert(used_extern_libs, "openal")
table.insert(used_extern_libs, "vorbis")
end
if not _OPTIONS["without-nvtt"] then
table.insert(used_extern_libs, "nvtt")
end
if not _OPTIONS["without-lobby"] then
table.insert(used_extern_libs, "gloox")
end
if not _OPTIONS["without-miniupnpc"] then
table.insert(used_extern_libs, "miniupnpc")
end
-- Bundles static libs together with main.cpp and builds game executable.
function setup_main_exe ()
local target_type = get_main_project_target_type()
project_create("pyrogenesis", target_type)
filter "system:not macosx"
linkgroups 'On'
filter {}
links { "mocks_real" }
local extra_params = {
extra_files = { "main.cpp" },
no_pch = 1
}
project_add_contents(source_root, {}, {}, extra_params)
project_add_extern_libs(used_extern_libs, target_type)
dependson { "Collada" }
rtti "off"
-- Platform Specifics
if os.istarget("windows") then
files { source_root.."lib/sysdep/os/win/icon.rc" }
-- from "lowlevel" static lib; must be added here to be linked in
files { source_root.."lib/sysdep/os/win/error_dialog.rc" }
linkoptions {
-- wraps main thread in a __try block(see wseh.cpp). replace with mainCRTStartup if that's undesired.
"/ENTRY:wseh_EntryPoint",
-- see wstartup.h
"/INCLUDE:_wstartup_InitAndRegisterShutdown",
-- allow manual unload of delay-loaded DLLs
"/DELAY:UNLOAD",
}
-- allow the executable to use more than 2GB of RAM.
-- this should not be enabled during development, so that memory issues are easily spotted.
if _OPTIONS["large-address-aware"] then
linkoptions { "/LARGEADDRESSAWARE" }
end
-- see manifest.cpp
project_add_manifest()
elseif os.istarget("linux") or os.istarget("bsd") then
if not _OPTIONS["android"] and not (os.getversion().description == "OpenBSD") then
links { "rt" }
end
if _OPTIONS["android"] then
-- NDK's STANDALONE-TOOLCHAIN.html says this is required
linkoptions { "-Wl,--fix-cortex-a8" }
links { "log" }
end
if link_execinfo then
links {
"execinfo"
}
end
if os.istarget("linux") or os.getversion().description == "GNU/kFreeBSD" then
links {
-- Dynamic libraries (needed for linking for gold)
"dl",
}
end
-- Threading support
buildoptions { "-pthread" }
if not _OPTIONS["android"] then
linkoptions { "-pthread" }
end
-- For debug_resolve_symbol
filter "Debug"
linkoptions { "-rdynamic" }
filter { }
elseif os.istarget("macosx") then
links { "pthread" }
links { "ApplicationServices.framework", "Cocoa.framework", "CoreFoundation.framework" }
if _OPTIONS["macosx-version-min"] then
xcodebuildsettings { MACOSX_DEPLOYMENT_TARGET = _OPTIONS["macosx-version-min"] }
end
end
end
--------------------------------------------------------------------------------
-- atlas
--------------------------------------------------------------------------------
-- setup a typical Atlas component project
-- extra_params, rel_source_dirs and rel_include_dirs: as in project_add_contents;
function setup_atlas_project(project_name, target_type, rel_source_dirs, rel_include_dirs, extern_libs, extra_params)
local source_root = rootdir.."/source/tools/atlas/" .. project_name .. "/"
project_create(project_name, target_type)
-- if not specified, the default for atlas pch files is in the project root.
if not extra_params["pch_dir"] then
extra_params["pch_dir"] = source_root
end
project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params)
project_add_extern_libs(extern_libs, target_type)
-- Platform Specifics
if os.istarget("windows") then
-- Link to required libraries
links { "winmm", "delayimp" }
elseif os.istarget("linux") or os.istarget("bsd") then
if os.getversion().description == "FreeBSD" then
buildoptions { "-fPIC" }
linkoptions { "-fPIC" }
else
buildoptions { "-rdynamic", "-fPIC" }
linkoptions { "-fPIC", "-rdynamic" }
end
-- warnings triggered by wxWidgets
buildoptions { "-Wno-unused-local-typedefs" }
end
end
-- build all Atlas component projects
function setup_atlas_projects()
setup_atlas_project("AtlasObject", "StaticLib",
{ -- src
".",
"../../../third_party/jsonspirit"
},{ -- include
"../../../third_party/jsonspirit"
},{ -- extern_libs
"boost",
"iconv",
"libxml2"
},{ -- extra_params
no_pch = 1
})
atlas_src = {
"ActorEditor",
"CustomControls/Buttons",
"CustomControls/Canvas",
"CustomControls/ColorDialog",
"CustomControls/DraggableListCtrl",
"CustomControls/EditableListCtrl",
"CustomControls/FileHistory",
"CustomControls/HighResTimer",
"CustomControls/MapDialog",
"CustomControls/MapResizeDialog",
"CustomControls/SnapSplitterWindow",
"CustomControls/VirtualDirTreeCtrl",
"CustomControls/Windows",
"General",
"General/VideoRecorder",
"Misc",
"ScenarioEditor",
"ScenarioEditor/Sections/Common",
"ScenarioEditor/Sections/Cinema",
"ScenarioEditor/Sections/Environment",
"ScenarioEditor/Sections/Map",
"ScenarioEditor/Sections/Object",
"ScenarioEditor/Sections/Player",
"ScenarioEditor/Sections/Terrain",
"ScenarioEditor/Tools",
"ScenarioEditor/Tools/Common",
}
atlas_extra_links = {
"AtlasObject"
}
atlas_extern_libs = {
"boost",
"comsuppw",
"iconv",
"libxml2",
"sdl", -- key definitions
"wxwidgets",
"zlib",
}
if not os.istarget("windows") and not os.istarget("macosx") then
-- X11 should only be linked on *nix
table.insert(atlas_extern_libs, "x11")
end
setup_atlas_project("AtlasUI", "SharedLib", atlas_src,
{ -- include
"..",
"CustomControls",
"Misc",
"../../../third_party/jsonspirit"
},
atlas_extern_libs,
{ -- extra_params
pch_dir = rootdir.."/source/tools/atlas/AtlasUI/Misc/",
no_pch = false,
extra_links = atlas_extra_links,
extra_files = { "Misc/atlas.rc" }
})
end
-- Atlas 'frontend' tool-launching projects
function setup_atlas_frontend_project (project_name)
local target_type = get_main_project_target_type()
project_create(project_name, target_type)
local source_root = rootdir.."/source/tools/atlas/AtlasFrontends/"
files { source_root..project_name..".cpp" }
if os.istarget("windows") then
files { source_root..project_name..".rc" }
end
includedirs { source_root .. ".." }
-- Platform Specifics
if os.istarget("windows") then
-- see manifest.cpp
project_add_manifest()
else -- Non-Windows, = Unix
links { "AtlasObject" }
end
links { "AtlasUI" }
end
function setup_atlas_frontends()
setup_atlas_frontend_project("ActorEditor")
end
--------------------------------------------------------------------------------
-- collada
--------------------------------------------------------------------------------
function setup_collada_project(project_name, target_type, rel_source_dirs, rel_include_dirs, extern_libs, extra_params)
project_create(project_name, target_type)
local source_root = source_root.."collada/"
extra_params["pch_dir"] = source_root
project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params)
project_add_extern_libs(extern_libs, target_type)
-- Platform Specifics
if os.istarget("windows") then
characterset "MBCS"
elseif os.istarget("linux") then
defines { "LINUX" }
links {
"dl",
}
-- FCollada is not aliasing-safe, so disallow dangerous optimisations
-- (TODO: It'd be nice to fix FCollada, but that looks hard)
buildoptions { "-fno-strict-aliasing" }
if os.getversion().description ~= "FreeBSD" then
buildoptions { "-rdynamic" }
linkoptions { "-rdynamic" }
end
elseif os.istarget("bsd") then
if os.getversion().description == "OpenBSD" then
links { "c", }
end
if os.getversion().description == "GNU/kFreeBSD" then
links {
"dl",
}
end
buildoptions { "-fno-strict-aliasing" }
buildoptions { "-rdynamic" }
linkoptions { "-rdynamic" }
elseif os.istarget("macosx") then
-- define MACOS-something?
buildoptions { "-fno-strict-aliasing" }
-- On OSX, fcollada uses a few utility functions from coreservices
links { "CoreServices.framework" }
end
end
-- build all Collada component projects
function setup_collada_projects()
setup_collada_project("Collada", "SharedLib",
{ -- src
"."
},{ -- include
},{ -- extern_libs
"fcollada",
"iconv",
"libxml2"
},{ -- extra_params
})
end
--------------------------------------------------------------------------------
-- tests
--------------------------------------------------------------------------------
function setup_tests()
local cxxtest = require "cxxtest"
if os.istarget("windows") then
cxxtest.setpath(rootdir.."/build/bin/cxxtestgen.exe")
else
cxxtest.setpath(rootdir.."/libraries/source/cxxtest-4.4/bin/cxxtestgen")
end
local runner = "ErrorPrinter"
if _OPTIONS["jenkins-tests"] then
runner = "XmlPrinter"
end
local includefiles = {
-- Precompiled headers - the header is added to all generated .cpp files
-- note that the header isn't actually precompiled here, only #included
-- so that the build stage can use it as a precompiled header.
"precompiled.h",
-- This is required to build against SDL 2.0.4 on Windows.
"lib/external_libraries/libsdl.h",
}
cxxtest.init(source_root, true, runner, includefiles)
local target_type = get_main_project_target_type()
project_create("test", target_type)
-- Find header files in 'test' subdirectories
local all_files = os.matchfiles(source_root .. "**/tests/*.h")
local test_files = {}
for i,v in pairs(all_files) do
-- Don't include sysdep tests on the wrong sys
-- Don't include Atlas tests unless Atlas is being built
if not (string.find(v, "/sysdep/os/win/") and not os.istarget("windows")) and
not (string.find(v, "/tools/atlas/") and not _OPTIONS["atlas"]) and
not (string.find(v, "/sysdep/arch/x86_x64/") and ((arch ~= "amd64") or (arch ~= "x86")))
then
table.insert(test_files, v)
end
end
cxxtest.configure_project(test_files)
filter "system:not macosx"
linkgroups 'On'
filter {}
links { static_lib_names }
filter "Debug"
links { static_lib_names_debug }
filter "Release"
links { static_lib_names_release }
filter { }
links { "mocks_test" }
if _OPTIONS["atlas"] then
links { "AtlasObject" }
end
extra_params = {
extra_files = { "test_setup.cpp" },
}
project_add_contents(source_root, {}, {}, extra_params)
project_add_extern_libs(used_extern_libs, target_type)
dependson { "Collada" }
rtti "off"
-- TODO: should fix the duplication between this OS-specific linking
-- code, and the similar version in setup_main_exe
if os.istarget("windows") then
-- from "lowlevel" static lib; must be added here to be linked in
files { source_root.."lib/sysdep/os/win/error_dialog.rc" }
-- see wstartup.h
linkoptions { "/INCLUDE:_wstartup_InitAndRegisterShutdown" }
-- Enables console for the TEST project on Windows
linkoptions { "/SUBSYSTEM:CONSOLE" }
project_add_manifest()
elseif os.istarget("linux") or os.istarget("bsd") then
if link_execinfo then
links {
"execinfo"
}
end
if not _OPTIONS["android"] and not (os.getversion().description == "OpenBSD") then
links { "rt" }
end
if _OPTIONS["android"] then
-- NDK's STANDALONE-TOOLCHAIN.html says this is required
linkoptions { "-Wl,--fix-cortex-a8" }
end
if os.istarget("linux") or os.getversion().description == "GNU/kFreeBSD" then
links {
-- Dynamic libraries (needed for linking for gold)
"dl",
}
end
-- Threading support
buildoptions { "-pthread" }
if not _OPTIONS["android"] then
linkoptions { "-pthread" }
end
-- For debug_resolve_symbol
filter "Debug"
linkoptions { "-rdynamic" }
filter { }
includedirs { source_root .. "pch/test/" }
elseif os.istarget("macosx") and _OPTIONS["macosx-version-min"] then
xcodebuildsettings { MACOSX_DEPLOYMENT_TARGET = _OPTIONS["macosx-version-min"] }
end
end
-- must come first, so that VC sets it as the default project and therefore
-- allows running via F5 without the "where is the EXE" dialog.
setup_main_exe()
setup_all_libs()
-- add the static libs to the main EXE project. only now (after
-- setup_all_libs has run) are the lib names known. cannot move
-- setup_main_exe to run after setup_all_libs (see comment above).
-- we also don't want to hardcode the names - that would require more
-- work when changing the static lib breakdown.
project("pyrogenesis") -- Set the main project active
links { static_lib_names }
filter "Debug"
links { static_lib_names_debug }
filter "Release"
links { static_lib_names_release }
filter { }
if _OPTIONS["atlas"] then
setup_atlas_projects()
setup_atlas_frontends()
end
setup_collada_projects()
if not _OPTIONS["without-tests"] then
setup_tests()
end
Index: ps/trunk/source/lobby/IXmppClient.h
===================================================================
--- ps/trunk/source/lobby/IXmppClient.h (revision 24727)
+++ ps/trunk/source/lobby/IXmppClient.h (revision 24728)
@@ -1,70 +1,71 @@
-/* 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 .
*/
#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 void GetNick(std::string& nick) = 0;
virtual void kick(const std::string& nick, const std::string& reason) = 0;
virtual void ban(const std::string& nick, const std::string& reason) = 0;
virtual void SetPresence(const std::string& presence) = 0;
virtual const char* GetPresence(const std::string& nickname) = 0;
virtual const char* GetRole(const std::string& nickname) = 0;
virtual std::wstring GetRating(const std::string& nickname) = 0;
virtual const std::wstring& GetSubject() = 0;
virtual void GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0;
virtual void GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0;
virtual void GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0;
virtual void GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0;
virtual JS::Value 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/StanzaExtensions.cpp
===================================================================
--- ps/trunk/source/lobby/StanzaExtensions.cpp (revision 24727)
+++ ps/trunk/source/lobby/StanzaExtensions.cpp (revision 24728)
@@ -1,285 +1,353 @@
-/* Copyright (C) 2018 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 "StanzaExtensions.h"
/******************************************************
* GameReport, fairly generic custom stanza extension used
* to report game statistics.
*/
GameReport::GameReport(const glooxwrapper::Tag* tag)
: StanzaExtension(EXTGAMEREPORT)
{
if (!tag || tag->name() != "report" || tag->xmlns() != XMLNS_GAMEREPORT)
return;
// TODO if we want to handle receiving this stanza extension.
};
/**
* Required by gloox, used to serialize the GameReport into XML for sending.
*/
glooxwrapper::Tag* GameReport::tag() const
{
glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("report");
t->setXmlns(XMLNS_GAMEREPORT);
for (const glooxwrapper::Tag* const& tag : m_GameReport)
t->addChild(tag->clone());
return t;
}
/**
* Required by gloox, used to find the GameReport element in a recived IQ.
*/
const glooxwrapper::string& GameReport::filterString() const
{
static const glooxwrapper::string filter = "/iq/report[@xmlns='" XMLNS_GAMEREPORT "']";
return filter;
}
glooxwrapper::StanzaExtension* GameReport::clone() const
{
GameReport* q = new GameReport();
return q;
}
/******************************************************
* BoardListQuery, a flexible custom IQ Stanza useful for anything with ratings, used to
* request and receive leaderboard and rating data from server.
* Example stanza:
* 1200
*/
BoardListQuery::BoardListQuery(const glooxwrapper::Tag* tag)
: StanzaExtension(EXTBOARDLISTQUERY)
{
if (!tag || tag->name() != "query" || tag->xmlns() != XMLNS_BOARDLIST)
return;
const glooxwrapper::Tag* c = tag->findTag_clone("query/command");
if (c)
m_Command = c->cdata();
glooxwrapper::Tag::free(c);
for (const glooxwrapper::Tag* const& t : tag->findTagList_clone("query/board"))
m_StanzaBoardList.emplace_back(t);
}
/**
* Required by gloox, used to find the BoardList element in a received IQ.
*/
const glooxwrapper::string& BoardListQuery::filterString() const
{
static const glooxwrapper::string filter = "/iq/query[@xmlns='" XMLNS_BOARDLIST "']";
return filter;
}
/**
* Required by gloox, used to serialize the BoardList request into XML for sending.
*/
glooxwrapper::Tag* BoardListQuery::tag() const
{
glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("query");
t->setXmlns(XMLNS_BOARDLIST);
// Check for ratinglist or boardlist command
if (!m_Command.empty())
t->addChild(glooxwrapper::Tag::allocate("command", m_Command));
for (const glooxwrapper::Tag* const& tag : m_StanzaBoardList)
t->addChild(tag->clone());
return t;
}
glooxwrapper::StanzaExtension* BoardListQuery::clone() const
{
BoardListQuery* q = new BoardListQuery();
return q;
}
BoardListQuery::~BoardListQuery()
{
for (const glooxwrapper::Tag* const& t : m_StanzaBoardList)
glooxwrapper::Tag::free(t);
m_StanzaBoardList.clear();
}
/******************************************************
* GameListQuery, custom IQ Stanza, used to receive
* the listing of games from the server, and register/
* unregister/changestate games on the server.
*/
GameListQuery::GameListQuery(const glooxwrapper::Tag* tag)
: StanzaExtension(EXTGAMELISTQUERY)
{
if (!tag || tag->name() != "query" || tag->xmlns() != XMLNS_GAMELIST)
return;
const glooxwrapper::Tag* c = tag->findTag_clone("query/game");
if (c)
m_Command = c->cdata();
glooxwrapper::Tag::free(c);
for (const glooxwrapper::Tag* const& t : tag->findTagList_clone("query/game"))
m_GameList.emplace_back(t);
}
/**
* Required by gloox, used to find the GameList element in a received IQ.
*/
const glooxwrapper::string& GameListQuery::filterString() const
{
static const glooxwrapper::string filter = "/iq/query[@xmlns='" XMLNS_GAMELIST "']";
return filter;
}
/**
* Required by gloox, used to serialize the game object into XML for sending.
*/
glooxwrapper::Tag* GameListQuery::tag() const
{
glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("query");
t->setXmlns(XMLNS_GAMELIST);
// Check for register / unregister command
if (!m_Command.empty())
t->addChild(glooxwrapper::Tag::allocate("command", m_Command));
for (const glooxwrapper::Tag* const& tag : m_GameList)
t->addChild(tag->clone());
return t;
}
glooxwrapper::StanzaExtension* GameListQuery::clone() const
{
GameListQuery* q = new GameListQuery();
return q;
}
GameListQuery::~GameListQuery()
{
for (const glooxwrapper::Tag* const & t : m_GameList)
glooxwrapper::Tag::free(t);
m_GameList.clear();
}
/******************************************************
* ProfileQuery, a custom IQ Stanza useful for fetching
* user profiles
* Example stanza:
* foobar
*/
ProfileQuery::ProfileQuery(const glooxwrapper::Tag* tag)
: StanzaExtension(EXTPROFILEQUERY)
{
if (!tag || tag->name() != "query" || tag->xmlns() != XMLNS_PROFILE)
return;
const glooxwrapper::Tag* c = tag->findTag_clone("query/command");
if (c)
m_Command = c->cdata();
glooxwrapper::Tag::free(c);
for (const glooxwrapper::Tag* const& t : tag->findTagList_clone("query/profile"))
m_StanzaProfile.emplace_back(t);
}
/**
* Required by gloox, used to find the Profile element in a received IQ.
*/
const glooxwrapper::string& ProfileQuery::filterString() const
{
static const glooxwrapper::string filter = "/iq/query[@xmlns='" XMLNS_PROFILE "']";
return filter;
}
/**
* Required by gloox, used to serialize the Profile request into XML for sending.
*/
glooxwrapper::Tag* ProfileQuery::tag() const
{
glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("query");
t->setXmlns(XMLNS_PROFILE);
if (!m_Command.empty())
t->addChild(glooxwrapper::Tag::allocate("command", m_Command));
for (const glooxwrapper::Tag* const& tag : m_StanzaProfile)
t->addChild(tag->clone());
return t;
}
glooxwrapper::StanzaExtension* ProfileQuery::clone() const
{
ProfileQuery* q = new ProfileQuery();
return q;
}
ProfileQuery::~ProfileQuery()
{
for (const glooxwrapper::Tag* const& t : m_StanzaProfile)
glooxwrapper::Tag::free(t);
m_StanzaProfile.clear();
}
/******************************************************
* LobbyAuth, a custom IQ Stanza, used to send and
* receive a security token for hosting authentication.
*/
LobbyAuth::LobbyAuth(const glooxwrapper::Tag* tag)
: StanzaExtension(EXTLOBBYAUTH)
{
if (!tag || tag->name() != "auth" || tag->xmlns() != XMLNS_LOBBYAUTH)
return;
const glooxwrapper::Tag* c = tag->findTag_clone("auth/token");
if (c)
m_Token = c->cdata();
glooxwrapper::Tag::free(c);
}
/**
* Required by gloox, used to find the LobbyAuth element in a received IQ.
*/
const glooxwrapper::string& LobbyAuth::filterString() const
{
static const glooxwrapper::string filter = "/iq/auth[@xmlns='" XMLNS_LOBBYAUTH "']";
return filter;
}
/**
* Required by gloox, used to serialize the auth object into XML for sending.
*/
glooxwrapper::Tag* LobbyAuth::tag() const
{
glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("auth");
t->setXmlns(XMLNS_LOBBYAUTH);
// Check for the auth token
if (!m_Token.empty())
t->addChild(glooxwrapper::Tag::allocate("token", m_Token));
return t;
}
glooxwrapper::StanzaExtension* LobbyAuth::clone() const
{
return new LobbyAuth();
}
+
+/******************************************************
+ * ConnectionData, a custom IQ Stanza, used to send and
+ * receive a ip and port of the server.
+ */
+ConnectionData::ConnectionData(const glooxwrapper::Tag* tag)
+ : StanzaExtension(EXTCONNECTIONDATA)
+{
+ if (!tag || tag->name() != "connectiondata" || tag->xmlns() != XMLNS_CONNECTIONDATA)
+ return;
+
+ const glooxwrapper::Tag* c = tag->findTag_clone("connectiondata/ip");
+ if (c)
+ m_Ip = c->cdata();
+ const glooxwrapper::Tag* p= tag->findTag_clone("connectiondata/port");
+ if (p)
+ m_Port = p->cdata();
+ const glooxwrapper::Tag* s = tag->findTag_clone("connectiondata/useSTUN");
+ if (s)
+ m_UseSTUN = s->cdata();
+ const glooxwrapper::Tag* pw = tag->findTag_clone("connectiondata/password");
+ if (pw)
+ m_Password = pw->cdata();
+ const glooxwrapper::Tag* e = tag->findTag_clone("connectiondata/error");
+ if (e)
+ m_Error= e->cdata();
+
+ glooxwrapper::Tag::free(c);
+ glooxwrapper::Tag::free(p);
+ glooxwrapper::Tag::free(s);
+ glooxwrapper::Tag::free(pw);
+ glooxwrapper::Tag::free(e);
+}
+
+/**
+ * Required by gloox, used to find the LobbyAuth element in a received IQ.
+ */
+const glooxwrapper::string& ConnectionData::filterString() const
+{
+ static const glooxwrapper::string filter = "/iq/connectiondata[@xmlns='" XMLNS_CONNECTIONDATA "']";
+ return filter;
+}
+
+/**
+ * Required by gloox, used to serialize the auth object into XML for sending.
+ */
+glooxwrapper::Tag* ConnectionData::tag() const
+{
+ glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("connectiondata");
+ t->setXmlns(XMLNS_CONNECTIONDATA);
+
+ if (!m_Ip.empty())
+ t->addChild(glooxwrapper::Tag::allocate("ip", m_Ip));
+ if (!m_Port.empty())
+ t->addChild(glooxwrapper::Tag::allocate("port", m_Port));
+ if (!m_UseSTUN.empty())
+ t->addChild(glooxwrapper::Tag::allocate("useSTUN", m_UseSTUN));
+ if (!m_Password.empty())
+ t->addChild(glooxwrapper::Tag::allocate("password", m_Password));
+ if (!m_Error.empty())
+ t->addChild(glooxwrapper::Tag::allocate("error", m_Error));
+ return t;
+}
+
+glooxwrapper::StanzaExtension* ConnectionData::clone() const
+{
+ return new ConnectionData();
+}
Index: ps/trunk/source/lobby/StanzaExtensions.h
===================================================================
--- ps/trunk/source/lobby/StanzaExtensions.h (revision 24727)
+++ ps/trunk/source/lobby/StanzaExtensions.h (revision 24728)
@@ -1,137 +1,161 @@
/* Copyright (C) 2020 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 STANZAEXTENSIONS_H
#define STANZAEXTENSIONS_H
#include "glooxwrapper/glooxwrapper.h"
#include
/// Global Gamelist Extension
#define EXTGAMELISTQUERY 1403
#define XMLNS_GAMELIST "jabber:iq:gamelist"
/// Global Boardlist Extension
#define EXTBOARDLISTQUERY 1404
#define XMLNS_BOARDLIST "jabber:iq:boardlist"
/// Global Gamereport Extension
#define EXTGAMEREPORT 1405
#define XMLNS_GAMEREPORT "jabber:iq:gamereport"
/// Global Profile Extension
#define EXTPROFILEQUERY 1406
#define XMLNS_PROFILE "jabber:iq:profile"
/// Global Lobby Authentication Extension
#define EXTLOBBYAUTH 1407
#define XMLNS_LOBBYAUTH "jabber:iq:lobbyauth"
+#define EXTCONNECTIONDATA 1408
+#define XMLNS_CONNECTIONDATA "jabber:iq:connectiondata"
+
+class ConnectionData : public glooxwrapper::StanzaExtension
+{
+public:
+ ConnectionData(const glooxwrapper::Tag* tag = 0);
+
+ // Following four methods are all required by gloox
+ virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
+ {
+ return new ConnectionData(tag);
+ }
+ virtual const glooxwrapper::string& filterString() const;
+ virtual glooxwrapper::Tag* tag() const;
+ virtual glooxwrapper::StanzaExtension* clone() const;
+
+ glooxwrapper::string m_Ip;
+ glooxwrapper::string m_Port;
+ glooxwrapper::string m_UseSTUN;
+ glooxwrapper::string m_Password;
+ glooxwrapper::string m_Error;
+};
+
class GameReport : public glooxwrapper::StanzaExtension
{
public:
GameReport(const glooxwrapper::Tag* tag = 0);
// Following four methods are all required by gloox
virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
{
return new GameReport(tag);
}
virtual const glooxwrapper::string& filterString() const;
virtual glooxwrapper::Tag* tag() const;
virtual glooxwrapper::StanzaExtension* clone() const;
std::vector m_GameReport;
};
class GameListQuery : public glooxwrapper::StanzaExtension
{
public:
GameListQuery(const glooxwrapper::Tag* tag = 0);
// Following four methods are all required by gloox
virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
{
return new GameListQuery(tag);
}
virtual const glooxwrapper::string& filterString() const;
virtual glooxwrapper::Tag* tag() const;
virtual glooxwrapper::StanzaExtension* clone() const;
~GameListQuery();
glooxwrapper::string m_Command;
std::vector m_GameList;
};
class BoardListQuery : public glooxwrapper::StanzaExtension
{
public:
BoardListQuery(const glooxwrapper::Tag* tag = 0);
// Following four methods are all required by gloox
virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
{
return new BoardListQuery(tag);
}
virtual const glooxwrapper::string& filterString() const;
virtual glooxwrapper::Tag* tag() const;
virtual glooxwrapper::StanzaExtension* clone() const;
~BoardListQuery();
glooxwrapper::string m_Command;
std::vector m_StanzaBoardList;
};
class ProfileQuery : public glooxwrapper::StanzaExtension
{
public:
ProfileQuery(const glooxwrapper::Tag* tag = 0);
// Following four methods are all required by gloox
virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
{
return new ProfileQuery(tag);
}
virtual const glooxwrapper::string& filterString() const;
virtual glooxwrapper::Tag* tag() const;
virtual glooxwrapper::StanzaExtension* clone() const;
~ProfileQuery();
glooxwrapper::string m_Command;
std::vector m_StanzaProfile;
};
class LobbyAuth : public glooxwrapper::StanzaExtension
{
public:
LobbyAuth(const glooxwrapper::Tag* tag = 0);
// Following four methods are all required by gloox
virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
{
return new LobbyAuth(tag);
}
virtual const glooxwrapper::string& filterString() const;
virtual glooxwrapper::Tag* tag() const;
virtual glooxwrapper::StanzaExtension* clone() const;
glooxwrapper::string m_Token;
};
#endif // STANZAEXTENSIONS_H
Index: ps/trunk/source/lobby/XmppClient.cpp
===================================================================
--- ps/trunk/source/lobby/XmppClient.cpp (revision 24727)
+++ ps/trunk/source/lobby/XmppClient.cpp (revision 24728)
@@ -1,1379 +1,1464 @@
-/* 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 "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/Pyrogenesis.h"
#include "scriptinterface/ScriptExtraHeaders.h" // StructuredClone
#include "scriptinterface/ScriptInterface.h"
#include
#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_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");
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 = jid;
+ 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();
g->m_Command = "register";
glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game");
// Add a fake ip which will be overwritten by the ip stamp XMPP module on the server.
game->addAttribute("ip", "fake");
// Iterate through all the properties reported and add them to the stanza.
std::vector properties;
scriptInterface.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));
}
// Push the stanza onto the IQ
g->m_GameList.emplace_back(game);
// Send IQ
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqRegisterGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Send a request to unregister a game to the server.
*/
void XmppClient::SendIqUnregisterGame()
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Send IQ
GameListQuery* g = new GameListQuery();
g->m_Command = "unregister";
g->m_GameList.emplace_back(glooxwrapper::Tag::allocate("game"));
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqUnregisterGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Send a request to change the state of a registered game on the server.
*
* A game can either be in the 'running' or 'waiting' state - the server
* decides which - but we need to update the current players that are
* in-game so the server can make the calculation.
*/
void XmppClient::SendIqChangeStateGame(const std::string& nbp, const std::string& players)
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Send IQ
GameListQuery* g = new GameListQuery();
g->m_Command = "changestate";
glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game");
game->addAttribute("nbp", nbp);
game->addAttribute("players", players);
g->m_GameList.emplace_back(game);
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqChangeStateGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/*****************************************************
* iq to clients *
*****************************************************/
/**
* Send lobby authentication token.
*/
void XmppClient::SendIqLobbyAuth(const std::string& to, const std::string& token)
{
LobbyAuth* auth = new LobbyAuth();
auth->m_Token = token;
glooxwrapper::JID clientJid(to + "@" + m_server + "/0ad");
glooxwrapper::IQ iq(gloox::IQ::Set, clientJid, m_client->getID());
iq.addExtension(auth);
DbgXMPP("SendIqLobbyAuth [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/*****************************************************
* Account registration *
*****************************************************/
void XmppClient::handleRegistrationFields(const glooxwrapper::JID&, int fields, glooxwrapper::string)
{
glooxwrapper::RegistrationFields vals;
vals.username = m_username;
vals.password = m_password;
m_registration->createAccount(fields, vals);
}
void XmppClient::handleRegistrationResult(const glooxwrapper::JID&, gloox::RegistrationResult result)
{
if (result == gloox::RegistrationSuccess)
CreateGUIMessage("system", "registered", std::time(nullptr));
else
CreateGUIMessage("system", "error", std::time(nullptr), "text", result);
disconnect();
}
void XmppClient::handleAlreadyRegistered(const glooxwrapper::JID&)
{
DbgXMPP("the account already exists");
}
void XmppClient::handleDataForm(const glooxwrapper::JID&, const glooxwrapper::DataForm&)
{
DbgXMPP("dataForm received");
}
void XmppClient::handleOOB(const glooxwrapper::JID&, const glooxwrapper::OOB&)
{
DbgXMPP("OOB registration requested");
}
/*****************************************************
* Requests from GUI *
*****************************************************/
/**
* Handle requests from the GUI for the list of players.
*
* @return A JS array containing all known players and their presences
*/
void XmppClient::GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
ScriptRequest rq(scriptInterface);
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);
}
}
/**
* Handle requests from the GUI for the list of all active games.
*
* @return A JS array containing all known games
*/
void XmppClient::GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
ScriptRequest rq(scriptInterface);
ScriptInterface::CreateArray(rq, ret);
int j = 0;
- const char* stats[] = { "name", "ip", "port", "stunIP", "stunPort", "hostUsername", "state",
+ const char* stats[] = { "name", "hostUsername", "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);
}
}
/**
* Handle requests from the GUI for leaderboard data.
*
* @return A JS array containing all known leaderboard data
*/
void XmppClient::GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
ScriptRequest rq(scriptInterface);
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);
}
}
/**
* Handle requests from the GUI for profile data.
*
* @return A JS array containing the specific user's profile data
*/
void XmppClient::GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
ScriptRequest rq(scriptInterface);
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);
}
}
/*****************************************************
* 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)
+ return true;
+
+ if (!m_connectionDataIqId.empty() && m_connectionDataIqId.compare(iq.id().to_string()) != 0)
+ 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)
{
for (const glooxwrapper::Tag* const& t : m_GameList)
glooxwrapper::Tag::free(t);
m_GameList.clear();
for (const glooxwrapper::Tag* const& t : gq->m_GameList)
m_GameList.emplace_back(t->clone());
CreateGUIMessage("game", "gamelist", std::time(nullptr));
}
if (bq)
{
if (bq->m_Command == "boardlist")
{
for (const glooxwrapper::Tag* const& t : m_BoardList)
glooxwrapper::Tag::free(t);
m_BoardList.clear();
for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList)
m_BoardList.emplace_back(t->clone());
CreateGUIMessage("game", "leaderboard", std::time(nullptr));
}
else if (bq->m_Command == "ratinglist")
{
for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList)
{
const PlayerMap::iterator it = m_PlayerMap.find(t->findAttribute("name"));
if (it != m_PlayerMap.end())
{
it->second.m_Rating = t->findAttribute("rating");
m_PlayerMapUpdate = true;
}
}
CreateGUIMessage("game", "ratinglist", std::time(nullptr));
}
}
if (pq)
{
for (const glooxwrapper::Tag* const& t : m_Profile)
glooxwrapper::Tag::free(t);
m_Profile.clear();
for (const glooxwrapper::Tag* const& t : pq->m_StanzaProfile)
m_Profile.emplace_back(t->clone());
CreateGUIMessage("game", "profile", std::time(nullptr));
}
}
else if (iq.subtype() == gloox::IQ::Set)
{
const LobbyAuth* lobbyAuth = iq.findExtension(EXTLOBBYAUTH);
if (lobbyAuth)
{
LOGMESSAGE("XmppClient: Received lobby auth: %s from %s", lobbyAuth->m_Token.to_string(), iq.from().username());
glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id());
m_client->send(response);
if (g_NetServer)
g_NetServer->OnLobbyAuth(iq.from().username(), lobbyAuth->m_Token.to_string());
else
LOGERROR("Received lobby authentication request, but not hosting currently!");
}
}
+ else if (iq.subtype() == gloox::IQ::Get)
+ {
+ const ConnectionData* cd = iq.findExtension(EXTCONNECTIONDATA);
+ if (cd)
+ {
+ LOGMESSAGE("XmppClient: Recieved 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->CheckPassword(CStr(cd->m_Password.c_str())))
+ {
+ 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.
*
* @param nick Variable to store the nickname in.
*/
void XmppClient::GetNick(std::string& nick)
{
nick = m_mucRoom->nick().to_string();
}
/**
* Kick a player from the current room.
*
* @param nick Nickname to be kicked
* @param reason Reason the player was kicked
*/
void XmppClient::kick(const std::string& nick, const std::string& reason)
{
m_mucRoom->kick(nick, reason);
}
/**
* Ban a player from the current room.
*
* @param nick Nickname to be banned
* @param reason Reason the player was banned
*/
void XmppClient::ban(const std::string& nick, const std::string& reason)
{
m_mucRoom->ban(nick, reason);
}
/**
* Change the xmpp presence of the client.
*
* @param presence A string containing the desired presence
*/
void XmppClient::SetPresence(const std::string& presence)
{
#define IF(x,y) if (presence == x) m_mucRoom->setPresence(gloox::Presence::y)
IF("available", Available);
else IF("chat", Chat);
else IF("away", Away);
else IF("playing", DND);
else IF("offline", Unavailable);
// The others are not to be set
#undef IF
else LOGERROR("Unknown presence '%s'", presence.c_str());
}
/**
* Get the current xmpp presence of the given nick.
*/
const char* XmppClient::GetPresence(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return "offline";
return GetPresenceString(it->second.m_Presence);
}
/**
* Get the current xmpp role of the given nick.
*/
const char* XmppClient::GetRole(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return "";
return GetRoleString(it->second.m_Role);
}
/**
* Get the most recent received rating of the given nick.
* Notice that this doesn't request a rating profile if it hasn't been received yet.
*/
std::wstring XmppClient::GetRating(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return std::wstring();
return wstring_from_utf8(it->second.m_Rating.to_string());
}
/*****************************************************
* Utilities *
*****************************************************/
/**
* Parse and return the timestamp of a historic chat message and return the current time for new chat messages.
* Historic chat messages are implement as DelayedDelivers as specified in XEP-0203.
* Hence, their timestamp MUST be in UTC and conform to the DateTime format XEP-0082.
*
* @returns Seconds since the epoch.
*/
std::time_t XmppClient::ComputeTimestamp(const glooxwrapper::Message& msg)
{
// Only historic messages contain a timestamp!
if (!msg.when())
return std::time(nullptr);
// The locale is irrelevant, because the XMPP date format doesn't contain written month names
for (const std::string& format : std::vector{ "Y-M-d'T'H:m:sZ", "Y-M-d'T'H:m:s.SZ" })
{
UDate dateTime = g_L10n.ParseDateTime(msg.when()->stamp().to_string(), format, icu::Locale::getUS());
if (dateTime)
return dateTime / 1000.0;
}
return std::time(nullptr);
}
/**
* Convert a gloox presence type to an untranslated string literal to be used as an identifier by the scripts.
*/
const char* XmppClient::GetPresenceString(const gloox::Presence::PresenceType presenceType)
{
switch (presenceType)
{
#define CASE(X,Y) case gloox::Presence::X: return Y
CASE(Available, "available");
CASE(Chat, "chat");
CASE(Away, "away");
CASE(DND, "playing");
CASE(XA, "away");
CASE(Unavailable, "offline");
CASE(Probe, "probe");
CASE(Error, "error");
CASE(Invalid, "invalid");
default:
LOGERROR("Unknown presence type '%d'", static_cast(presenceType));
return "";
#undef CASE
}
}
/**
* Convert a gloox role type to an untranslated string literal to be used as an identifier by the scripts.
*/
const char* XmppClient::GetRoleString(const gloox::MUCRoomRole role)
{
switch (role)
{
#define CASE(X, Y) case gloox::X: return Y
CASE(RoleNone, "none");
CASE(RoleVisitor, "visitor");
CASE(RoleParticipant, "participant");
CASE(RoleModerator, "moderator");
CASE(RoleInvalid, "invalid");
default:
LOGERROR("Unknown role type '%d'", static_cast(role));
return "";
#undef CASE
}
}
/**
* Translates a gloox certificate error codes, i.e. gloox certificate statuses except CertOk.
* Keep in sync with specifications.
*/
std::string XmppClient::CertificateErrorToString(gloox::CertStatus status)
{
std::map certificateErrorStrings = {
{ gloox::CertInvalid, g_L10n.Translate("The certificate is not trusted.") },
{ gloox::CertSignerUnknown, g_L10n.Translate("The certificate hasn't got a known issuer.") },
{ gloox::CertRevoked, g_L10n.Translate("The certificate has been revoked.") },
{ gloox::CertExpired, g_L10n.Translate("The certificate has expired.") },
{ gloox::CertNotActive, g_L10n.Translate("The 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 24727)
+++ ps/trunk/source/lobby/XmppClient.h (revision 24728)
@@ -1,201 +1,206 @@
-/* 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 .
*/
#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);
void GetNick(std::string& nick);
void kick(const std::string& nick, const std::string& reason);
void ban(const std::string& nick, const std::string& reason);
void SetPresence(const std::string& presence);
const char* GetPresence(const std::string& nickname);
const char* GetRole(const std::string& nickname);
std::wstring GetRating(const std::string& nickname);
const std::wstring& GetSubject();
void GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
void GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
void GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
void GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
void SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJID);
/**
* Convert gloox values to string or time.
*/
static const char* GetPresenceString(const gloox::Presence::PresenceType presenceType);
static const char* GetRoleString(const gloox::MUCRoomRole role);
static std::string StanzaErrorToString(gloox::StanzaError err);
static std::string RegistrationResultToString(gloox::RegistrationResult res);
static std::string ConnectionErrorToString(gloox::ConnectionError err);
static std::string CertificateErrorToString(gloox::CertStatus status);
static std::time_t ComputeTimestamp(const glooxwrapper::Message& msg);
protected:
/* Xmpp handlers */
/* MUC handlers */
virtual void handleMUCParticipantPresence(glooxwrapper::MUCRoom& room, const glooxwrapper::MUCRoomParticipant, const glooxwrapper::Presence&);
virtual void handleMUCError(glooxwrapper::MUCRoom& room, gloox::StanzaError);
virtual void handleMUCMessage(glooxwrapper::MUCRoom& room, const glooxwrapper::Message& msg, bool priv);
virtual void handleMUCSubject(glooxwrapper::MUCRoom& room, const glooxwrapper::string& nick, const glooxwrapper::string& subject);
/* MUC handlers not supported by glooxwrapper */
// virtual bool handleMUCRoomCreation(glooxwrapper::MUCRoom*) {return false;}
// virtual void handleMUCInviteDecline(glooxwrapper::MUCRoom*, const glooxwrapper::JID&, const std::string&) {}
// virtual void handleMUCInfo(glooxwrapper::MUCRoom*, int, const std::string&, const glooxwrapper::DataForm*) {}
// virtual void handleMUCItems(glooxwrapper::MUCRoom*, const std::list >&) {}
/* Log handler */
virtual void handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message);
/* ConnectionListener handlers*/
virtual void onConnect();
virtual void onDisconnect(gloox::ConnectionError e);
virtual bool onTLSConnect(const glooxwrapper::CertInfo& info);
/* Iq Handlers */
virtual bool handleIq(const glooxwrapper::IQ& iq);
virtual void handleIqID(const glooxwrapper::IQ&, int) {}
/* Registration Handlers */
virtual void handleRegistrationFields(const glooxwrapper::JID& /*from*/, int fields, glooxwrapper::string instructions );
virtual void handleRegistrationResult(const glooxwrapper::JID& /*from*/, gloox::RegistrationResult result);
virtual void handleAlreadyRegistered(const glooxwrapper::JID& /*from*/);
virtual void handleDataForm(const glooxwrapper::JID& /*from*/, const glooxwrapper::DataForm& /*form*/);
virtual void handleOOB(const glooxwrapper::JID& /*from*/, const glooxwrapper::OOB& oob);
/* Message Handler */
virtual void handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession* session);
/* Session Handler */
virtual void handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle);
virtual void handleSessionInitiation(glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle);
public:
JS::Value 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/network/NetClient.cpp
===================================================================
--- ps/trunk/source/network/NetClient.cpp (revision 24727)
+++ ps/trunk/source/network/NetClient.cpp (revision 24728)
@@ -1,852 +1,943 @@
/* 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"
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)
: m_Client(client)
{
}
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;
m_Client.HandleMessage(&start);
}
private:
CNetClient& m_Client;
};
CNetClient::CNetClient(CGame* game, bool isLocalClient) :
m_Session(NULL),
m_UserName(L"anonymous"),
m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game),
m_GameAttributes(game->GetSimulation2()->GetScriptInterface().GetGeneralJSContext()),
m_IsLocalClient(isLocalClient),
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_INITIAL_GAMESETUP, (void*)&OnAuthenticate, context);
AddTransition(NCS_INITIAL_GAMESETUP, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, 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)
{
m_HostingPlayerName = hostingPlayerName;
}
-bool CNetClient::SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient)
+bool CNetClient::SetupConnection(ENetHost* enetClient)
{
CNetClientSession* session = new CNetClientSession(*this);
- bool ok = session->Connect(server, port, m_IsLocalClient, enetClient);
+ bool ok = session->Connect(m_ServerAddress, m_ServerPort, m_IsLocalClient, 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 > DEFAULT_TURN_LENGTH_MP)
{
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()
{
CGameStartMessage gameStart;
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 = L""; // TODO
authenticate.m_IsLocalClient = m_IsLocalClient;
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);
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->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->m_GameAttributes = message->m_Data;
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);
// 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());
client->m_Game->SetPlayerID(player);
client->m_Game->StartGame(&client->m_GameAttributes, "");
client->PushGuiMessage("type", "start");
return true;
}
bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START);
CNetClient* client = static_cast(context);
// 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))
);
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 < DEFAULT_TURN_LENGTH_MP || 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 24727)
+++ ps/trunk/source/network/NetClient.h (revision 24728)
@@ -1,314 +1,335 @@
-/* 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 .
*/
#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
#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_INITIAL_GAMESETUP,
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, bool isLocalClient);
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.
* This is needed for the secure lobby authentication.
*/
void SetHostingPlayerName(const CStr& hostingPlayerName);
/**
* 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.
- * @param server IP address or host name to connect to
+ * 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 SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient);
+ 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();
/**
* 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_ServerAddress;
+ u16 m_ServerPort;
+ bool m_UseSTUN;
/// 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;
/// Whether to prevent the client of the host from timing out
bool m_IsLocalClient;
/// Latest copy of game setup attributes heard from the server
JS::PersistentRootedValue m_GameAttributes;
/// 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/NetHost.h
===================================================================
--- ps/trunk/source/network/NetHost.h (revision 24727)
+++ ps/trunk/source/network/NetHost.h (revision 24728)
@@ -1,111 +1,114 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef NETHOST_H
#define NETHOST_H
#include "ps/CStr.h"
#include