Index: binaries/data/mods/public/gui/session/MenuButtons.js =================================================================== --- binaries/data/mods/public/gui/session/MenuButtons.js +++ binaries/data/mods/public/gui/session/MenuButtons.js @@ -225,23 +225,7 @@ onPress() { - closeOpenDialogs(); - this.pauseControl.implicitPause(); - - messageBox( - 400, 200, - translate("Are you sure you want to resign?"), - translate("Confirmation"), - [translate("No"), translate("Yes")], - [ - resumeGame, - () => { - Engine.PostNetworkCommand({ - "type": "resign" - }); - resumeGame(); - } - ]); + (new ResignConfirmation()).display(); } }; @@ -257,50 +241,11 @@ onPress() { - closeOpenDialogs(); - this.pauseControl.implicitPause(); - - let messageType = g_IsNetworked && g_IsController ? "host" : - (g_IsNetworked && !g_IsObserver ? "client" : "singleplayer"); - - messageBox( - 400, 200, - this.Confirmation[messageType].caption(), - translate("Confirmation"), - [translate("No"), translate("Yes")], - this.Confirmation[messageType].buttons()); - } -}; - -MenuButtons.prototype.Exit.prototype.Confirmation = { - "host": { - "caption": () => translate("Are you sure you want to quit? Leaving will disconnect all other players."), - "buttons": () => [resumeGame, endGame] - }, - "client": { - "caption": () => translate("Are you sure you want to quit?"), - "buttons": () => [ - resumeGame, - () => { - messageBox( - 400, 200, - translate("Do you want to resign or will you return soon?"), - translate("Confirmation"), - [translate("I will return"), translate("I resign")], - [ - endGame, - () => { - Engine.PostNetworkCommand({ - "type": "resign" - }); - resumeGame(); - } - ]); - } - ] - }, - "singleplayer": { - "caption": () => translate("Are you sure you want to quit?"), - "buttons": () => [resumeGame, endGame] + for (let name in QuitConfirmationMenu.prototype) + { + let quitConfirmation = new QuitConfirmationMenu.prototype[name](); + if (quitConfirmation.enabled()) + quitConfirmation.display(); + } } }; Index: binaries/data/mods/public/gui/session/SessionMessageBox.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/SessionMessageBox.js @@ -0,0 +1,31 @@ +/** + * This is the same as a regular MessageBox, but it pauses if it is + * a singleplayer game, until the player answered the dialog. + */ +class SessionMessageBox +{ + display() + { + closeOpenDialogs(); + g_PauseControl.implicitPause(); + Engine.PushGuiPage( + "page_msgbox.xml", + { + "width": this.Width, + "height": this.Height, + "title": this.Title, + "message": this.Caption, + "buttonCaptions": this.Buttons ? this.Buttons.map(button => button.caption) : undefined, + }, + buttonId => { + if (this.Buttons && this.Buttons[buttonId].onPress) + this.Buttons[buttonId].onPress.call(this); + + if (Engine.IsGameStarted()) + resumeGame(); + }); + } +} + +SessionMessageBox.prototype.Width = 400; +SessionMessageBox.prototype.Height = 200; Index: binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.js =================================================================== --- binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.js +++ binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.js @@ -22,13 +22,10 @@ this.enabled = enabled; - if (enabled) - messageBox( - 500, 250, - translate(this.Description), - translate(this.Title)); - Engine.EnableTimeWarpRecording(enabled ? this.NumberTurns : 0); + + if (enabled) + (new TimeWarpMessageBox()).display(); } rewind() @@ -60,7 +57,11 @@ */ TimeWarp.prototype.FastForwardSpeed = 20; -TimeWarp.prototype.Title = markForTranslation("Time warp mode"); - -TimeWarp.prototype.Description = markForTranslation( +class TimeWarpMessageBox extends SessionMessageBox +{ +} +TimeWarpMessageBox.prototype.Width = 500; +TimeWarpMessageBox.prototype.Height = 250; +TimeWarpMessageBox.prototype.Title = translate("Time warp mode"); +TimeWarpMessageBox.prototype.Caption = translate( "Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash."); Index: binaries/data/mods/public/gui/session/message_box/DeleteSelectionConfirmation.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/message_box/DeleteSelectionConfirmation.js @@ -0,0 +1,20 @@ +class DeleteSelectionConfirmation extends SessionMessageBox +{ + constructor(deleteSelection) + { + super(); + this.deleteSelection = deleteSelection; + } +} + +DeleteSelectionConfirmation.prototype.Title = translate("Delete"); +DeleteSelectionConfirmation.prototype.Caption = translate("Destroy everything currently selected?"); +DeleteSelectionConfirmation.prototype.Buttons = [ + { + "caption": translate("No") + }, + { + "caption": translate("Yes"), + "onPress": function() { this.deleteSelection(); } + } +]; Index: binaries/data/mods/public/gui/session/message_box/OutOfSyncNetwork.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/message_box/OutOfSyncNetwork.js @@ -0,0 +1,51 @@ +class OutOfSyncNetwork extends SessionMessageBox +{ + constructor() + { + super(); + registerOutOfSyncHandler(this.onNetworkOutOfSync.bind(this)); + } + + /** + * The message object is constructed in CNetClientTurnManager::OnSyncError. + */ + onNetworkOutOfSync(msg) + { + let txt = [ + sprintf(translate("Out-Of-Sync error on turn %(turn)s."), { + "turn": msg.turn + }), + + sprintf(translateWithContext("Out-Of-Sync", "Players: %(players)s"), { + "players": msg.players.join(translateWithContext("Separator for a list of players", ", ")) + }), + + msg.hash == msg.expectedHash ? + translateWithContext("Out-Of-Sync", "Your game state is identical to the hosts game state.") : + translateWithContext("Out-Of-Sync", "Your game state differs from the hosts game state."), + + "" + ]; + + if (msg.turn > 1 && g_GameAttributes.settings.PlayerData.some(pData => pData && pData.AI)) + txt.push(translateWithContext("Out-Of-Sync", "Rejoining Multiplayer games with AIs is not supported yet!")); + else + txt.push( + translateWithContext("Out-Of-Sync", "Ensure all players use the same mods."), + translateWithContext("Out-Of-Sync", 'Click on "Report a Bug" in the main menu to help fix this.'), + sprintf(translateWithContext("Out-Of-Sync", "Replay saved to %(filepath)s"), { + "filepath": escapeText(msg.path_replay) + }), + sprintf(translateWithContext("Out-Of-Sync", "Dumping current state to %(filepath)s"), { + "filepath": escapeText(msg.path_oos_dump) + })); + + this.Caption = txt.join("\n"); + this.display(); + } +} + +OutOfSyncNetwork.prototype.Width = 600; +OutOfSyncNetwork.prototype.Height = 280; + +OutOfSyncNetwork.prototype.Title = translate("Out of Sync"); Index: binaries/data/mods/public/gui/session/message_box/OutOfSyncReplay.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/message_box/OutOfSyncReplay.js @@ -0,0 +1,29 @@ +class OutOfSyncReplay extends SessionMessageBox +{ + constructor() + { + super(); + Engine.GetGUIObjectByName("session").onReplayOutOfSync = this.onReplayOutOfSync.bind(this); + } + + onReplayOutOfSync(turn, hash, expectedHash) + { + this.Caption = sprintf(this.Captions.join("\n"), { + "turn": turn, + "hash": hash, + "expectedHash": expectedHash + }); + this.display(); + } +} + +OutOfSyncReplay.prototype.Width = 500; +OutOfSyncReplay.prototype.Height = 140; + +OutOfSyncReplay.prototype.Title = translate("Out of Sync") + +OutOfSyncReplay.prototype.Captions = [ + translate("Out-Of-Sync error on turn %(turn)s."), + // Translation: This is shown if replay is out of sync + translate("Out-Of-Sync", "The current game state is different from the original game state.") +]; Index: binaries/data/mods/public/gui/session/message_box/QuitConfirmation.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/message_box/QuitConfirmation.js @@ -0,0 +1,20 @@ +class QuitConfirmation extends SessionMessageBox +{ +} + +QuitConfirmation.prototype.Title = + translate("Confirmation"); + +QuitConfirmation.prototype.Caption = + translate("Are you sure you want to quit?"); + +QuitConfirmation.prototype.Buttons = +[ + { + "caption": translate("No") + }, + { + "caption": translate("Yes"), + "onPress": endGame + } +]; Index: binaries/data/mods/public/gui/session/message_box/QuitConfirmationDefeat.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/message_box/QuitConfirmationDefeat.js @@ -0,0 +1,57 @@ +/** + * This class will spawn a dialog asking to exit the game in case the user was an assigned player who has been defeated. + */ +class QuitConfirmationDefeat extends QuitConfirmation +{ + constructor() + { + super(); + + if (Engine.IsAtlasRunning()) + return; + + this.confirmHandler = undefined; + registerPlayersFinishedHandler(this.onPlayersFinished.bind(this)); + } + + onPlayersFinished(players, won) + { + if (players.indexOf(Engine.GetPlayerID()) == -1) + return; + + // Defer simulation result until + // 1. the loading screen finished for all networked clients (g_IsNetworkedActive) + // 2. all messages modifying g_Players victory state were processed (next turn) + this.confirmHandler = this.confirmExit.bind(this, won); + registerSimulationUpdateHandler(this.confirmHandler); + } + + confirmExit(won) + { + if (g_IsNetworked && !g_IsNetworkedActive) + return; + + unregisterSimulationUpdateHandler(this.confirmHandler); + + // Don't ask for exit if other humans are still playing + let askExit = !Engine.HasNetServer() || g_Players.every((player, i) => + i == 0 || + player.state != "active" || + g_GameAttributes.settings.PlayerData[i].AI != ""); + + this.Title = won ? this.TitleVictory : this.TitleDefeated; + + this.Caption = + g_PlayerStateMessages[won ? "won" : "defeated"] + + (askExit ? "\n" + this.Question : ""); + + this.Buttons = askExit ? super.Buttons : undefined; + + this.display(); + } +} + +QuitConfirmationDefeat.prototype.TitleVictory = translate("VICTORIOUS!"); +QuitConfirmationDefeat.prototype.TitleDefeated = translate("DEFEATED!"); + +QuitConfirmationDefeat.prototype.Question = translate("Do you want to quit?"); Index: binaries/data/mods/public/gui/session/message_box/QuitConfirmationMenu.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/message_box/QuitConfirmationMenu.js @@ -0,0 +1,78 @@ +/** + * The class that is enabled() will be triggered when the user clicks on the Exit menu button. + */ +class QuitConfirmationMenu +{ +} + +/** + * In singleplayer mode, replaymode and for observers in multiplayermatches that + * aren't the host, exit the match instantly. + */ +QuitConfirmationMenu.prototype.Singleplayer = class extends QuitConfirmation +{ + enabled() + { + return !g_IsNetworked || (!g_IsController && g_IsObserver); + } +} + +/** + * If the current player is the host of a networked match, have the player + * confirm intent to end the game for the remote players as well. + */ +QuitConfirmationMenu.prototype.MultiplayerHost = class extends QuitConfirmation +{ + enabled() + { + return g_IsNetworked && g_IsController; + } +} + +QuitConfirmationMenu.prototype.MultiplayerHost.prototype.Caption = + translate("Are you sure you want to quit? Leaving will disconnect all other players."); + +/** + * Active players that aren't the host will be asked if they want to resign before leaving. + */ +QuitConfirmationMenu.prototype.MultiplayerClient = class extends QuitConfirmation +{ + enabled() + { + return g_IsNetworked && !g_IsController && !g_IsObserver; + } +} + +QuitConfirmationMenu.prototype.MultiplayerClient.prototype.Buttons = +[ + { + "caption": translate("No") + }, + { + "caption": translate("Yes"), + "onPress": () => { + (new ReturnQuestion()).display(); + } + } +]; + +class ReturnQuestion extends SessionMessageBox +{ +} + +ReturnQuestion.prototype.Title = translate("Confirmation"); +ReturnQuestion.prototype.Caption = translate("Do you want to resign or will you return soon?") +ReturnQuestion.prototype.Buttons = [ + { + "caption": translate("I will return"), + "onPress": endGame + }, + { + "caption": translate("I resign"), + "onPress": () => { + Engine.PostNetworkCommand({ + "type": "resign" + }); + } + } +]; Index: binaries/data/mods/public/gui/session/message_box/QuitConfirmationReplay.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/message_box/QuitConfirmationReplay.js @@ -0,0 +1,28 @@ +/** + * This class is concerned with opening a message box if the game is in replaymode and that replay ended. + */ +class QuitConfirmationReplay extends SessionMessageBox +{ + constructor() + { + super(); + Engine.GetGUIObjectByName("session").onReplayFinished = this.display.bind(this); + } +} + +QuitConfirmationReplay.prototype.Title = + translateWithContext("replayFinished", "Confirmation"); + +QuitConfirmationReplay.prototype.Caption = + translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"); + +QuitConfirmationReplay.prototype.Buttons = +[ + { + "caption": translateWithContext("replayFinished", "No") + }, + { + "caption": translateWithContext("replayFinished", "Yes"), + "onPress": endGame + } +]; Index: binaries/data/mods/public/gui/session/message_box/ResignConfirmation.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/message_box/ResignConfirmation.js @@ -0,0 +1,19 @@ +class ResignConfirmation extends SessionMessageBox +{ +} + +ResignConfirmation.prototype.Title = translate("Confirmation"); +ResignConfirmation.prototype.Caption = translate("Are you sure you want to resign?"); +ResignConfirmation.prototype.Buttons = [ + { + "caption": translate("No") + }, + { + "caption": translate("Yes"), + "onPress": () => { + Engine.PostNetworkCommand({ + "type": "resign" + }); + } + } +]; Index: binaries/data/mods/public/gui/session/messages.js =================================================================== --- binaries/data/mods/public/gui/session/messages.js +++ binaries/data/mods/public/gui/session/messages.js @@ -24,6 +24,11 @@ var g_CeasefireEndedHandlers = new Set(); /** + * These handlers are fired if the server informed the players that the networked game is out of sync. + */ +var g_OutOfSyncHandlers = new Set(); + +/** * Handle all netmessage types that can occur. */ var g_NetMessageTypes = { @@ -34,7 +39,8 @@ addNetworkWarning(msg); }, "out-of-sync": msg => { - onNetworkOutOfSync(msg); + for (let handler of g_OutOfSyncHandlers) + handler(msg); }, "players": msg => { handlePlayerAssignmentsMessage(msg); @@ -302,6 +308,11 @@ g_CeasefireEndedHandlers.add(handler); } +function registerOutOfSyncHandler(handler) +{ + g_OutOfSyncHandlers.add(handler); +} + /** * Loads all known cheat commands. */ @@ -516,58 +527,6 @@ loadingClientsText.caption = guids.map(guid => colorizePlayernameByGUID(guid)).join(translateWithContext("Separator for a list of client loading messages", ", ")); } -function onNetworkOutOfSync(msg) -{ - let txt = [ - sprintf(translate("Out-Of-Sync error on turn %(turn)s."), { - "turn": msg.turn - }), - - sprintf(translateWithContext("Out-Of-Sync", "Players: %(players)s"), { - "players": msg.players.join(translateWithContext("Separator for a list of players", ", ")) - }), - - msg.hash == msg.expectedHash ? - translateWithContext("Out-Of-Sync", "Your game state is identical to the hosts game state.") : - translateWithContext("Out-Of-Sync", "Your game state differs from the hosts game state."), - - "" - ]; - - if (msg.turn > 1 && g_GameAttributes.settings.PlayerData.some(pData => pData && pData.AI)) - txt.push(translateWithContext("Out-Of-Sync", "Rejoining Multiplayer games with AIs is not supported yet!")); - else - txt.push( - translateWithContext("Out-Of-Sync", "Ensure all players use the same mods."), - translateWithContext("Out-Of-Sync", 'Click on "Report a Bug" in the main menu to help fix this.'), - sprintf(translateWithContext("Out-Of-Sync", "Replay saved to %(filepath)s"), { - "filepath": escapeText(msg.path_replay) - }), - sprintf(translateWithContext("Out-Of-Sync", "Dumping current state to %(filepath)s"), { - "filepath": escapeText(msg.path_oos_dump) - }) - ); - - messageBox( - 600, 280, - txt.join("\n"), - translate("Out of Sync") - ); -} - -function onReplayOutOfSync(turn, hash, expectedHash) -{ - messageBox( - 500, 140, - sprintf(translate("Out-Of-Sync error on turn %(turn)s."), { - "turn": turn - }) + "\n" + - // Translation: This is shown if replay is out of sync - translateWithContext("Out-Of-Sync", "The current game state is different from the original game state."), - translate("Out of Sync") - ); -} - function handlePlayerAssignmentsMessage(message) { for (let guid in g_PlayerAssignments) Index: binaries/data/mods/public/gui/session/session.js =================================================================== --- binaries/data/mods/public/gui/session/session.js +++ binaries/data/mods/public/gui/session/session.js @@ -18,10 +18,14 @@ var g_Menu; var g_MiniMapPanel; var g_ObjectivesDialog; +var g_OutOfSyncNetwork; +var g_OutOfSyncReplay; var g_PanelEntityManager; var g_PauseControl; var g_PauseOverlay; var g_PlayerViewControl; +var g_QuitConfirmationDefeat; +var g_QuitConfirmationReplay; var g_RangeOverlayManager; var g_ResearchProgress; var g_TradeDialog; @@ -69,11 +73,6 @@ var g_HasRejoined = false; /** - * Shows a message box asking the user to leave if "won" or "defeated". - */ -var g_ConfirmExit = false; - -/** * The playerID selected in the change perspective tool. */ var g_ViewedPlayer = Engine.GetPlayerID(); @@ -263,11 +262,11 @@ } g_DiplomacyColors = new DiplomacyColors(); - g_PlayerViewControl = new PlayerViewControl(); g_PlayerViewControl.registerViewedPlayerChangeHandler(g_DiplomacyColors.updateDisplayedPlayerColors.bind(g_DiplomacyColors)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(g_PlayerViewControl.rebuild.bind(g_PlayerViewControl)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updateGUIObjects); + g_PauseControl = new PauseControl(); g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay); g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates); @@ -275,12 +274,15 @@ g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection); g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors); g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl); + g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat); g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes); g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl); + g_OutOfSyncNetwork = new OutOfSyncNetwork(); + g_OutOfSyncReplay = new OutOfSyncReplay(); g_PanelEntityManager = new PanelEntityManager(g_PlayerViewControl, g_Selection, g_PanelEntityOrder); - g_PauseControl = new PauseControl(); g_PauseOverlay = new PauseOverlay(g_PauseControl); - g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat); + g_QuitConfirmationDefeat = new QuitConfirmationDefeat(); + g_QuitConfirmationReplay = new QuitConfirmationReplay(); g_RangeOverlayManager = new RangeOverlayManager(g_Selection); g_ResearchProgress = new ResearchProgress(g_PlayerViewControl, g_Selection); g_TradeDialog = new TradeDialog(g_PlayerViewControl); @@ -479,8 +481,6 @@ global.music.states.VICTORY : global.music.states.DEFEAT ); - - g_ConfirmExit = won ? "won" : "defeated"; } function resumeGame() @@ -625,42 +625,6 @@ updateCinemaPath(); handleNotifications(); updateGUIObjects(); - - if (g_ConfirmExit) - confirmExit(); -} - -/** - * Don't show the message box before all playerstate changes are processed. - */ -function confirmExit() -{ - if (g_IsNetworked && !g_IsNetworkedActive) - return; - - closeOpenDialogs(); - g_PauseControl.implicitPause(); - - // Don't ask for exit if other humans are still playing - let askExit = !Engine.HasNetServer() || g_Players.every((player, i) => - i == 0 || - player.state != "active" || - g_GameAttributes.settings.PlayerData[i].AI != ""); - - let subject = g_PlayerStateMessages[g_ConfirmExit]; - if (askExit) - subject += "\n" + translate("Do you want to quit?"); - - messageBox( - 400, 200, - subject, - g_ConfirmExit == "won" ? - translate("VICTORIOUS!") : - translate("DEFEATED!"), - askExit ? [translate("No"), translate("Yes")] : [translate("OK")], - askExit ? [resumeGame, endGame] : [resumeGame]); - - g_ConfirmExit = false; } function toggleGUI() @@ -702,18 +666,6 @@ } } -function onReplayFinished() -{ - closeOpenDialogs(); - g_PauseControl.implicitPause(); - - messageBox(400, 200, - translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"), - translateWithContext("replayFinished", "Confirmation"), - [translateWithContext("replayFinished", "No"), translateWithContext("replayFinished", "Yes")], - [resumeGame, endGame]); -} - function updateGroups() { g_Groups.update(); Index: binaries/data/mods/public/gui/session/session.xml =================================================================== --- binaries/data/mods/public/gui/session/session.xml +++ binaries/data/mods/public/gui/session/session.xml @@ -10,6 +10,7 @@