Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -156,6 +156,11 @@ lobby = "Alt+L" ; Show the multiplayer lobby in a dialog window. structree = "Alt+Shift+T" ; Show structure tree civinfo = "Alt+Shift+H" ; Show civilization info +hostgame = "H" ; Open the dialog to host a match +joingame = "J" ; Join the selected lobby match or open the dialog to enter an IP address to join +networkstats = "N" ; Open the dialog that shows the network statistics of the connected clients +previousPage = "P" ; Presses the "return" button, which leads one back to the previous menu + ; This is different from the "close dialog" hotkey which doesn't change the root page but only closes a child page ; > CLIPBOARD CONTROLS copy = "Ctrl+C" ; Copy to clipboard @@ -453,6 +458,10 @@ observerlimit = 8 ; Prevent further observer joins in running games if this limit is reached gamestarttimeout = 60000 ; Don't disconnect clients timing out in the loading screen and rejoin process before exceeding this timeout. +[network.geolite2] +enabled = true; ; Whether or not to load the local GeoLite2 database, if it exists. +directory = "geolite2/" ; directory that contains the GeoLite2 databse. Obtainable at https://dev.maxmind.com/geoip/geoip2/ + [overlay] fps = "false" ; Show frames per second in top right corner realtime = "false" ; Show current system time in top right corner Index: binaries/data/mods/public/gui/common/NetworkDialogManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/common/NetworkDialogManager.js @@ -0,0 +1,28 @@ +function NetworkDialogManager() +{ + this.guiPage = undefined; +} + +NetworkDialogManager.prototype.open = function() +{ + this.guiPage = Engine.PushGuiPage("page_networkreport.xml", { + "gameAttributes": g_GameAttributes, + "playerAssignments": g_PlayerAssignments, + "callback": "closePageHack" // TODO: should use D1684 + }); +}; + +function closePageHack() +{ + if (typeof g_NetworkDialogManager != "undefined") + g_NetworkDialogManager.guiPage = undefined; +} + +NetworkDialogManager.prototype.refresh = function() +{ + if (this.guiPage) + this.guiPage.updatePage({ + "gameAttributes": g_GameAttributes, + "playerAssignments": g_PlayerAssignments, + }); +}; Index: binaries/data/mods/public/gui/common/color.js =================================================================== --- binaries/data/mods/public/gui/common/color.js +++ binaries/data/mods/public/gui/common/color.js @@ -75,6 +75,15 @@ } /** + * @param {Number} efficiency - between 0 and 1 + * @returns {String} GUI color representing efficiency, 1 yields green, 0.5 yellow and 0 red. + */ +function efficiencyToColor(efficiency) +{ + return hslToRgb(Math.min(1, Math.max(0, efficiency)) / 3, 1, 0.5).join(" "); +} + +/** * Convert color value from RGB to HSL space. * * @see {@link https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion} Index: binaries/data/mods/public/gui/common/network.js =================================================================== --- binaries/data/mods/public/gui/common/network.js +++ binaries/data/mods/public/gui/common/network.js @@ -1,40 +1,22 @@ /** - * Number of milliseconds to display network warnings. + * Latency at which the game performance becomes impaired. */ -var g_NetworkWarningTimeout = 3000; +const inefficientRTT = Engine.GetTurnLength(); /** - * Currently displayed network warnings. At most one message per user. + * Ratio at which the game performance becomes impaired. */ -var g_NetworkWarnings = {}; +const inefficientPacketLoss = 0.25; /** - * Message-types to be displayed. + * Number of milliseconds to display network warnings. */ -var g_NetworkWarningTexts = { +var g_NetworkWarningTimeout = 3000; - "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 - }) -}; +/** + * Currently displayed network reports. At most one message per user. + */ +var g_NetworkWarnings = {}; var g_NetworkCommands = { "/kick": argument => kickPlayer(argument, false), @@ -81,6 +63,42 @@ } /** + * @param {bool} isLocal - Whether this is a remote client or our connection. + * @param {Object} performance - The performance data of a remote client passed by the NetClient. + * @returns the String + */ +function getNetworkWarningString(isLocal, performance) +{ + if (performance.lastReceivedTime > 3000) + return isLocal ? + translate("Losing connection to server (%(seconds)ss)") : + translate("%(player)s losing connection (%(seconds)ss)"); + + if (performance.meanRTT > inefficientRTT) + return isLocal ? + translate("Bad connection to server (%(milliseconds)sms)") : + translate("Bad connection to %(player)s (%(milliseconds)sms)"); + + if (performance.packetLoss > inefficientPacketLoss) + return isLocal ? + translate("Bad connection to server (%(packetLossRatio)s%% packet loss)") : + translate("Bad connection to %(player)s (%(packetLossRatio)s%% packet loss)"); + + return ""; +} + +function getNetworkWarningText(string, performance, username) +{ + return sprintf(string, { + "player": username, + "seconds": Math.ceil(performance.lastReceivedTime / 1000), + "milliseconds": performance.meanRTT, + "packetLossRatio": (performance.packetLoss * 100).toFixed(1) + }) +} + + +/** * Show the disconnect reason in a message box. * * @param {number} reason @@ -110,7 +128,7 @@ function kickPlayer(username, ban) { - if (g_IsController) + if (Engine.HasNetServer()) Engine.KickPlayer(username, ban); else kickError(); @@ -118,7 +136,7 @@ function kickObservers(ban) { - if (!g_IsController) + if (!Engine.HasNetServer()) { kickError(); return; @@ -185,26 +203,28 @@ } /** - * Remember this warning for a few seconds. - * Overwrite previous warnings for this user. - * - * @param msg - GUI message sent by NetServer or NetClient + * Creates a report for each network client with a critically bad network connection to be displayed for some duration. */ -function addNetworkWarning(msg) +function pollNetworkWarnings() { - if (!g_NetworkWarningTexts[msg.warntype]) - { - warn("Unknown network warning type received: " + uneval(msg)); + if (Engine.ConfigDB_GetValue("user", "overlay.netwarnings") != "true") return; - } - if (Engine.ConfigDB_GetValue("user", "overlay.netwarnings") != "true") + let clientPerformance = Engine.GetNetworkClientPerformance(); + if (!clientPerformance) return; - g_NetworkWarnings[msg.guid || "server"] = { - "added": Date.now(), - "msg": msg - }; + for (let guid in clientPerformance) + { + let string = getNetworkWarningString(guid == Engine.GetPlayerGUID(), clientPerformance[guid]); + + if (string) + g_NetworkWarnings[guid] = { + "added": Date.now(), + "string": string, + "performance": clientPerformance[guid] + }; + } } /** @@ -213,14 +233,19 @@ */ function getNetworkWarnings() { + if (Engine.ConfigDB_GetValue("user", "overlay.netwarnings") != "true") + return { + "messages": [], + "maxTextWidth": 0 + }; + // Remove outdated messages for (let guid in g_NetworkWarnings) - if (Date.now() > g_NetworkWarnings[guid].added + g_NetworkWarningTimeout || - guid != "server" && !g_PlayerAssignments[guid]) + if (Date.now() > g_NetworkWarnings[guid].added + g_NetworkWarningTimeout || !g_PlayerAssignments[guid]) delete g_NetworkWarnings[guid]; // Show local messages first - let guids = Object.keys(g_NetworkWarnings).sort(guid => guid != "server"); + let guids = Object.keys(g_NetworkWarnings).sort(guid => guid != Engine.GetPlayerGUID()); let font = Engine.GetGUIObjectByName("gameStateNotifications").font; @@ -229,14 +254,11 @@ for (let guid of guids) { - let msg = g_NetworkWarnings[guid].msg; - // Add formatted text - messages.push(g_NetworkWarningTexts[msg.warntype](msg, colorizePlayernameByGUID(guid))); + messages.push(getNetworkWarningText(g_NetworkWarnings[guid].string, g_NetworkWarnings[guid].performance, 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)); + let textWidth = Engine.GetTextWidth(font, getNetworkWarningText(g_NetworkWarnings[guid].string, g_NetworkWarnings[guid].performance, g_PlayerAssignments[guid].name)); maxTextWidth = Math.max(textWidth, maxTextWidth); } Index: binaries/data/mods/public/gui/gamesetup/gamesetup.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/gamesetup.js +++ binaries/data/mods/public/gui/gamesetup/gamesetup.js @@ -12,6 +12,8 @@ var g_GameSpeeds = getGameSpeedChoices(false); +var g_NetworkDialogManager = new NetworkDialogManager(); + /** * Offer users to select playable civs only. * Load unselectable civs as they could appear in scenario maps. @@ -140,7 +142,6 @@ */ var g_NetMessageTypes = { "netstatus": msg => handleNetStatusMessage(msg), - "netwarn": msg => addNetworkWarning(msg), "gamesetup": msg => handleGamesetupMessage(msg), "players": msg => handlePlayerAssignmentMessage(msg), "ready": msg => handleReadyMessage(msg), @@ -1063,6 +1064,13 @@ }, "hidden": () => !Engine.HasXmppClient() }, + "networkButton": { + "onPress": () => function() { + if (g_IsNetworked) + g_NetworkDialogManager.open(); + }, + "hidden": () => !g_IsNetworked + }, "spTips": { "hidden": () => { let settingsPanel = Engine.GetGUIObjectByName("settingsPanel"); @@ -1405,7 +1413,7 @@ let settingTabButtons = Engine.GetGUIObjectByName("settingTabButtons"); let settingTabButtonsSize = settingTabButtons.size; settingTabButtonsSize.bottom = settingTabButtonsSize.top + g_SettingsTabsGUI.length * (g_TabButtonHeight + g_TabButtonDist); - settingTabButtonsSize.right = g_MiscControls.lobbyButton.hidden() ? + settingTabButtonsSize.right = (g_MiscControls.lobbyButton.hidden() && g_MiscControls.networkButton.hidden()) ? settingTabButtonsSize.right : Engine.GetGUIObjectByName("lobbyButton").size.left - g_LobbyButtonSpacing; settingTabButtons.size = settingTabButtonsSize; @@ -1572,6 +1580,8 @@ updateGUIObjects(); + g_NetworkDialogManager.refresh(); + hideLoadingWindow(); } @@ -1605,6 +1615,8 @@ sendRegisterGameStanzaImmediate(); else sendRegisterGameStanza(); + + g_NetworkDialogManager.refresh(); } function onClientJoin(newGUID, newAssignments) @@ -1996,6 +2008,7 @@ handleNetMessages(); updateTimers(); + pollNetworkWarnings(); let now = Date.now(); let tickLength = now - g_LastTickTime; Index: binaries/data/mods/public/gui/gamesetup/gamesetup.xml =================================================================== --- binaries/data/mods/public/gui/gamesetup/gamesetup.xml +++ binaries/data/mods/public/gui/gamesetup/gamesetup.xml @@ -202,6 +202,20 @@ Show the multiplayer lobby in a dialog window. + + + + Show the network dialog. + + @@ -256,6 +270,7 @@ size="100%-308 100%-52 100%-168 100%-24" tooltip_style="onscreenToolTip" z="21" + hotkey="previousPage" > Back cancelSetup(); Index: binaries/data/mods/public/gui/lobby/lobby_panels.xml =================================================================== --- binaries/data/mods/public/gui/lobby/lobby_panels.xml +++ binaries/data/mods/public/gui/lobby/lobby_panels.xml @@ -153,13 +153,13 @@ -