Index: binaries/data/mods/public/gui/common/functions_global_object.js
===================================================================
--- binaries/data/mods/public/gui/common/functions_global_object.js (revision 18148)
+++ binaries/data/mods/public/gui/common/functions_global_object.js (working copy)
@@ -147,12 +147,10 @@ function updateCounters()
function displayGamestateNotifications()
{
let messages = [];
let maxTextWidth = 0;
- // TODO: Players who paused the game should be added here
-
// Add network warnings
if (Engine.ConfigDB_GetValue("user", "overlay.netwarnings") == "true")
{
let netwarnings = getNetworkWarnings();
messages = messages.concat(netwarnings.messages);
Index: binaries/data/mods/public/gui/credits/texts/programming.json
===================================================================
--- binaries/data/mods/public/gui/credits/texts/programming.json (revision 18148)
+++ binaries/data/mods/public/gui/credits/texts/programming.json (working copy)
@@ -54,10 +54,11 @@
{"nick": "Deiz"},
{"nick": "Dietger", "name": "Dietger van Antwerpen"},
{"nick": "dumbo"},
{"nick": "dvangennip", "name": "Doménique"},
{"nick": "Echelon9", "name": "Rhys Kidd"},
+ {"nick": "echotangoecho"},
{"nick": "eihrul", "name": "Lee Salzman"},
{"nick": "elexis", "name": "Alexander Heinsius"},
{"nick": "EmjeR", "name": "Matthijs de Rijk"},
{"nick": "EMontana"},
{"nick": "ericb"},
Index: binaries/data/mods/public/gui/session/menu.js
===================================================================
--- binaries/data/mods/public/gui/session/menu.js (revision 18148)
+++ binaries/data/mods/public/gui/session/menu.js (working copy)
@@ -668,42 +668,67 @@ function openStrucTree()
"callback": "resumeGame",
});
}
/**
- * Pause the game in single player mode.
+ * Pause or resume the game.
+ *
+ * @param avoidMultiplayerPause - Pause if a message box is opened in a singleplayer game, but don't pause a multiplayer match for that.
*/
-function pauseGame()
+function pauseGame(pause = true, avoidMultiplayerPause = true)
{
- if (g_IsNetworked)
+ if (g_IsNetworked && avoidMultiplayerPause)
return;
- Engine.GetGUIObjectByName("pauseButtonText").caption = translate("Resume");
- Engine.GetGUIObjectByName("pauseOverlay").hidden = false;
- Engine.SetPaused(true);
+ g_Paused = pause;
+ Engine.SetPaused(pause, true);
+ updatePauseOverlay();
}
function resumeGame()
{
- Engine.GetGUIObjectByName("pauseButtonText").caption = translate("Pause");
- Engine.GetGUIObjectByName("pauseOverlay").hidden = true;
- Engine.SetPaused(false);
+ resumeGame();
}
+/**
+ * Called when the current player explicitly wants to pause.
+ */
function togglePause()
{
if (!Engine.GetGUIObjectByName("pauseButton").enabled)
return;
closeOpenDialogs();
- let pauseOverlay = Engine.GetGUIObjectByName("pauseOverlay");
+ g_Paused = !g_Paused;
+
+ if (g_IsNetworked)
+ setClientPauseState(Engine.GetPlayerGUID(), g_Paused);
+
+ pauseGame(g_Paused, false);
+}
+
+function setClientPauseState(guid, paused)
+{
+ if (paused)
+ g_PausingClients.push(guid);
+ else
+ g_PausingClients.splice(g_PausingClients.indexOf(guid), 1);
- Engine.SetPaused(pauseOverlay.hidden);
- Engine.GetGUIObjectByName("pauseButtonText").caption = pauseOverlay.hidden ? translate("Resume") : translate("Pause");
+ updatePauseOverlay();
+ Engine.SetPaused(paused, false);
+}
+
+function updatePauseOverlay()
+{
+ Engine.GetGUIObjectByName("pauseButtonText").caption = g_Paused ? translate("Pause") : translate("Resume");
+ Engine.GetGUIObjectByName("multiplayerPauseText").caption = translate("Game paused by") + " " +
+ g_PausingClients.map(guid => colorizePlayernameByGUID(guid)).join(translate(", "));
- pauseOverlay.hidden = !pauseOverlay.hidden;
+ Engine.GetGUIObjectByName("multiplayerPauseOverlay").hidden = !g_PausingClients.length;
+ Engine.GetGUIObjectByName("multiplayerResumeMessage").hidden = !g_Paused;
+ Engine.GetGUIObjectByName("pauseOverlay").hidden = !g_Paused;
}
function openManual()
{
closeOpenDialogs();
Index: binaries/data/mods/public/gui/session/messages.js
===================================================================
--- binaries/data/mods/public/gui/session/messages.js (revision 18148)
+++ binaries/data/mods/public/gui/session/messages.js (working copy)
@@ -29,10 +29,11 @@ var g_ChatTimers = [];
*/
var g_NetMessageTypes = {
"netstatus": msg => handleNetStatusMessage(msg),
"netwarn": msg => addNetworkWarning(msg),
"players": msg => handlePlayerAssignmentsMessage(msg),
+ "paused": msg => setClientPauseState(message.guid, message.pause),
"rejoined": msg => addChatMessage({ "type": "rejoined", "guid": msg.guid }),
"kicked": msg => addChatMessage({ "type": "system", "text": sprintf(translate("%(username)s has been kicked"), { "username": msg.username }) }),
"banned": msg => addChatMessage({ "type": "system", "text": sprintf(translate("%(username)s has been banned"), { "username": msg.username }) }),
"chat": msg => addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text }),
"aichat": msg => addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text, "translate": true }),
@@ -459,10 +460,12 @@ function handlePlayerAssignmentsMessage(
for (let guid in g_PlayerAssignments)
{
if (message.hosts[guid])
continue;
+ setClientPauseState(guid, false);
+
addChatMessage({ "type": "disconnect", "guid": guid });
for (let id in g_Players)
if (g_Players[id].guid == guid)
g_Players[id].offline = true;
Index: binaries/data/mods/public/gui/session/session.js
===================================================================
--- binaries/data/mods/public/gui/session/session.js (revision 18148)
+++ binaries/data/mods/public/gui/session/session.js (working copy)
@@ -36,10 +36,20 @@ var g_IsObserver = false;
* True if the current user has rejoined (or joined the game after it started).
*/
var g_HasRejoined = false;
/**
+ * True if the current player has paused the game.
+ */
+var g_Paused = false;
+
+/**
+ * The list of GUIDs of players who have currently paused the game, if the game is networked.
+ */
+var g_PausingClients = [];
+
+/**
* The playerID selected in the change perspective tool.
*/
var g_ViewedPlayer = Engine.GetPlayerID();
/**
Index: binaries/data/mods/public/gui/session/session.xml
===================================================================
--- binaries/data/mods/public/gui/session/session.xml (revision 18148)
+++ binaries/data/mods/public/gui/session/session.xml (working copy)
@@ -190,10 +190,28 @@
togglePause();
+
+
+
+
+
Index: source/gui/scripting/ScriptFunctions.cpp
===================================================================
--- source/gui/scripting/ScriptFunctions.cpp (revision 18148)
+++ source/gui/scripting/ScriptFunctions.cpp (working copy)
@@ -770,11 +770,11 @@ bool IsPaused(ScriptInterface::CxPrivate
return g_Game->m_Paused;
}
// Pause/unpause the game
-void SetPaused(ScriptInterface::CxPrivate* pCxPrivate, bool pause)
+void SetPaused(ScriptInterface::CxPrivate* pCxPrivate, bool pause, bool sendMessage)
{
if (!g_Game)
{
JS_ReportError(pCxPrivate->pScriptInterface->GetContext(), "Game is not started");
return;
@@ -782,10 +782,13 @@ void SetPaused(ScriptInterface::CxPrivat
g_Game->m_Paused = pause;
#if CONFIG2_AUDIO
if (g_SoundManager)
g_SoundManager->Pause(pause);
#endif
+
+ if (g_NetClient && sendMessage)
+ g_NetClient->SendPausedMessage(pause);
}
// Return the global frames-per-second value.
// params:
// returns: FPS [int]
@@ -1085,11 +1088,11 @@ void GuiScriptingInit(ScriptInterface& s
scriptInterface.RegisterFunction("HotkeyIsPressed");
scriptInterface.RegisterFunction("DisplayErrorDialog");
scriptInterface.RegisterFunction("GetProfilerState");
scriptInterface.RegisterFunction("Exit");
scriptInterface.RegisterFunction("IsPaused");
- scriptInterface.RegisterFunction("SetPaused");
+ scriptInterface.RegisterFunction("SetPaused");
scriptInterface.RegisterFunction("GetFPS");
scriptInterface.RegisterFunction("GetBuildTimestamp");
scriptInterface.RegisterFunction("ReadJSONFile");
scriptInterface.RegisterFunction("WriteJSONFile");
scriptInterface.RegisterFunction("TemplateExists");
Index: source/network/NetClient.cpp
===================================================================
--- source/network/NetClient.cpp (revision 18148)
+++ source/network/NetClient.cpp (working copy)
@@ -120,10 +120,11 @@ CNetClient::CNetClient(CGame* game, bool
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_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);
@@ -339,10 +340,17 @@ void CNetClient::SendRejoinedMessage()
{
CRejoinedMessage rejoinedMessage;
SendMessage(&rejoinedMessage);
}
+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);
@@ -725,10 +733,27 @@ bool CNetClient::OnClientPerformance(voi
}
return true;
}
+bool CNetClient::OnClientPaused(void *context, CFsmEvent *event)
+{
+ ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
+
+ CNetClient* client = (CNetClient*)context;
+ JSContext* cx = client->GetScriptInterface().GetContext();
+ CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
+
+ JS::RootedValue msg(cx);
+ client->GetScriptInterface().Eval("({ 'type':'paused' })", &msg);
+ client->GetScriptInterface().SetProperty(msg, "pause", message->m_Pause != 0);
+ client->GetScriptInterface().SetProperty(msg, "guid", message->m_GUID);
+ client->PushGuiMessage(msg);
+
+ return true;
+}
+
bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetClient* client = (CNetClient*)context;
Index: source/network/NetClient.h
===================================================================
--- source/network/NetClient.h (revision 18148)
+++ source/network/NetClient.h (working copy)
@@ -195,10 +195,15 @@ public:
* Call when the client has rejoined a running match and finished
* the loading screen.
*/
void SendRejoinedMessage();
+ /**
+ * Call when the client has paused or unpaused the game.
+ */
+ void SendPausedMessage(bool pause);
+
private:
// 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);
@@ -213,10 +218,11 @@ private:
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 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.
*/
Index: source/network/NetMessage.cpp
===================================================================
--- source/network/NetMessage.cpp (revision 18148)
+++ source/network/NetMessage.cpp (working copy)
@@ -145,10 +145,14 @@ CNetMessage* CNetMessageFactory::CreateM
case NMT_CLIENT_PERFORMANCE:
pNewMessage = new CClientPerformanceMessage;
break;
+ case NMT_CLIENT_PAUSED:
+ pNewMessage = new CClientPausedMessage;
+ break;
+
case NMT_LOADED_GAME:
pNewMessage = new CLoadedGameMessage;
break;
case NMT_SERVER_HANDSHAKE:
Index: source/network/NetMessages.h
===================================================================
--- source/network/NetMessages.h (revision 18148)
+++ source/network/NetMessages.h (working copy)
@@ -26,11 +26,11 @@
#include "ps/CStr.h"
#include "scriptinterface/ScriptVal.h"
#define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?'
#define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!'
-#define PS_PROTOCOL_VERSION 0x01010012 // Arbitrary protocol
+#define PS_PROTOCOL_VERSION 0x01010013 // Arbitrary protocol
#define PS_DEFAULT_PORT 0x5073 // 'P', 's'
// Defines the list of message types. The order of the list must not change.
// The message types having a negative value are used internally and not sent
// over the network. The message types used for network communication have
@@ -60,10 +60,11 @@ enum NetMessageType
NMT_REJOINED,
NMT_KICKED,
NMT_CLIENT_TIMEOUT,
NMT_CLIENT_PERFORMANCE,
+ NMT_CLIENT_PAUSED,
NMT_LOADED_GAME,
NMT_GAME_START,
NMT_END_COMMAND_BATCH,
NMT_SYNC_CHECK, // OOS-detection hash checking
@@ -181,10 +182,15 @@ START_NMT_CLASS_(ClientPerformance, NMT_
NMT_FIELD(CStr8, m_GUID)
NMT_FIELD_INT(m_MeanRTT, u32, 4)
NMT_END_ARRAY()
END_NMT_CLASS()
+START_NMT_CLASS_(ClientPaused, NMT_CLIENT_PAUSED)
+ NMT_FIELD(CStr, m_GUID)
+ NMT_FIELD_INT(m_Pause, u8, 1)
+END_NMT_CLASS()
+
START_NMT_CLASS_(LoadedGame, NMT_LOADED_GAME)
NMT_FIELD_INT(m_CurrentTurn, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(GameStart, NMT_GAME_START)
Index: source/network/NetServer.cpp
===================================================================
--- source/network/NetServer.cpp (revision 18148)
+++ source/network/NetServer.cpp (working copy)
@@ -653,10 +653,11 @@ void CNetServerWorker::SetupSession(CNet
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context);
+ session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnInGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnInGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnInGame, context);
@@ -696,10 +697,14 @@ void CNetServerWorker::OnUserJoin(CNetSe
session->SendMessage(&assignMessage);
}
void CNetServerWorker::OnUserLeave(CNetServerSession* session)
{
+ std::vector::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
+ if (pausing != m_PausingPlayers.end())
+ m_PausingPlayers.erase(pausing);
+
RemovePlayer(session->GetGUID());
if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
@@ -1167,10 +1172,19 @@ bool CNetServerWorker::OnJoinSyncingLoad
// Tell the client that everything has finished loading and it should start now
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = readyTurn;
session->SendMessage(&loaded);
+ // Send all pausing players to the client.
+ for (const CStr& guid : server.m_PausingPlayers)
+ {
+ CClientPausedMessage pausedMessage;
+ pausedMessage.m_GUID = guid;
+ pausedMessage.m_Pause = true;
+ session->SendMessage(&pausedMessage);
+ }
+
return true;
}
bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
{
@@ -1199,10 +1213,49 @@ bool CNetServerWorker::OnDisconnect(void
server.OnUserLeave(session);
return true;
}
+bool CNetServerWorker::OnClientPaused(void *context, CFsmEvent *event)
+{
+ ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
+
+ CNetServerSession* session = (CNetServerSession*)context;
+ CNetServerWorker& server = session->GetServer();
+
+ CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
+
+ message->m_GUID = session->GetGUID();
+
+ // Update the list of pausing players.
+ std::vector::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
+
+ if (message->m_Pause)
+ {
+ if (player != server.m_PausingPlayers.end())
+ return true;
+
+ server.m_PausingPlayers.push_back(session->GetGUID());
+ }
+ else
+ {
+ if (player == server.m_PausingPlayers.end())
+ return true;
+
+ server.m_PausingPlayers.erase(player);
+ }
+
+ // Send messages to clients that are in game, and are not the client who paused.
+ for (CNetServerSession* session : server.m_Sessions)
+ {
+ if (session->GetCurrState() == NSS_INGAME && message->m_GUID != session->GetGUID())
+ session->SendMessage(message);
+ }
+
+ return true;
+}
+
void CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
{
for (const CNetServerSession* session : m_Sessions)
{
if (session != changedSession && session->GetCurrState() != NSS_INGAME)
Index: source/network/NetServer.h
===================================================================
--- source/network/NetServer.h (revision 18148)
+++ source/network/NetServer.h (working copy)
@@ -272,10 +272,11 @@ private:
static bool OnReady(void* context, CFsmEvent* event);
static bool OnLoadedGame(void* context, CFsmEvent* event);
static bool OnJoinSyncingLoadedGame(void* context, CFsmEvent* event);
static bool OnRejoined(void* context, CFsmEvent* event);
static bool OnDisconnect(void* context, CFsmEvent* event);
+ static bool OnClientPaused(void* context, CFsmEvent* event);
void CheckGameLoadStatus(CNetServerSession* changedSession);
void ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message);
@@ -314,10 +315,15 @@ private:
CStrW m_WelcomeMessage;
std::vector m_BannedIPs;
std::vector m_BannedPlayers;
+ /**
+ * Holds the GUIDs of all currently paused players.
+ */
+ std::vector m_PausingPlayers;
+
u32 m_NextHostID;
CNetServerTurnManager* m_ServerTurnManager;
CStr m_HostGUID;