1
0
forked from 0ad/0ad

SessionMessageBox class, refs #5387.

Decouples events from event handling, removes implicit-pause duplication
per messagebox and allows mods to modify message box values.

Differential Revision: https://code.wildfiregames.com/D2398
Comments By: Stan, Freagarach
This was SVN commit r23114.
This commit is contained in:
elexis 2019-10-30 11:14:55 +00:00
parent 3617689230
commit bc6da5e3f2
15 changed files with 382 additions and 206 deletions

View File

@ -115,8 +115,7 @@ MenuButtons.prototype.Summary = class
},
"selectedData": this.selectedData
},
data =>
{
data => {
this.selectedData = data.summarySelectedData;
this.pauseControl.implicitResume();
});
@ -225,23 +224,7 @@ MenuButtons.prototype.Resign = class
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 +240,11 @@ MenuButtons.prototype.Exit = class
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();
}
}
};

View File

@ -0,0 +1,40 @@
/**
* 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()
{
this.onPageOpening();
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,
},
this.onPageClosed.bind(this));
}
onPageOpening()
{
closeOpenDialogs();
g_PauseControl.implicitPause();
}
onPageClosed(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;

View File

@ -23,10 +23,7 @@ class TimeWarp
this.enabled = enabled;
if (enabled)
messageBox(
500, 250,
translate(this.Description),
translate(this.Title));
(new TimeWarpMessageBox()).display();
Engine.EnableTimeWarpRecording(enabled ? this.NumberTurns : 0);
}
@ -60,7 +57,11 @@ TimeWarp.prototype.NumberTurns = 10;
*/
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.");

View File

@ -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(); }
}
];

View File

@ -0,0 +1,51 @@
class OutOfSyncNetwork extends SessionMessageBox
{
constructor()
{
super();
registerNetworkOutOfSyncHandler(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");

View File

@ -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.")
];

View File

@ -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
}
];

View File

@ -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?");

View File

@ -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"
});
}
}
];

View File

@ -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
}
];

View File

@ -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"
});
}
}
];

View File

@ -23,6 +23,11 @@ var g_PlayerAssignmentsChangeHandlers = new Set();
*/
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_NetworkOutOfSyncHandlers = new Set();
/**
* Handle all netmessage types that can occur.
*/
@ -34,7 +39,8 @@ var g_NetMessageTypes = {
addNetworkWarning(msg);
},
"out-of-sync": msg => {
onNetworkOutOfSync(msg);
for (let handler of g_NetworkOutOfSyncHandlers)
handler(msg);
},
"players": msg => {
handlePlayerAssignmentsMessage(msg);
@ -302,6 +308,11 @@ function registerCeasefireEndedHandler(handler)
g_CeasefireEndedHandlers.add(handler);
}
function registerNetworkOutOfSyncHandler(handler)
{
g_NetworkOutOfSyncHandlers.add(handler);
}
/**
* Loads all known cheat commands.
*/
@ -516,58 +527,6 @@ function handleClientsLoadingMessage(guids)
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)

View File

@ -18,10 +18,14 @@ var g_GameSpeedControl;
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;
@ -68,11 +72,6 @@ var g_IsObserver = false;
*/
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.
*/
@ -263,11 +262,11 @@ function init(initData, hotloadData)
}
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 @@ function init(initData, hotloadData)
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 @@ function playersFinished(players, victoryString, won)
global.music.states.VICTORY :
global.music.states.DEFEAT
);
g_ConfirmExit = won ? "won" : "defeated";
}
function resumeGame()
@ -625,42 +625,6 @@ function onSimulationUpdate()
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 updateGUIObjects()
}
}
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();

View File

@ -10,6 +10,7 @@
<script directory="gui/session/diplomacy/playercontrols/"/>
<script directory="gui/session/lobby/"/>
<script directory="gui/session/lobby/LobbyRatingReport/"/>
<script directory="gui/session/message_box/"/>
<script directory="gui/session/minimap/"/>
<script directory="gui/session/top_panel/"/>
<script directory="gui/session/top_panel/IconButtons/"/>
@ -30,14 +31,6 @@
onSimulationUpdate();
</action>
<action on="ReplayFinished">
onReplayFinished();
</action>
<action on="ReplayOutOfSync">
onReplayOutOfSync(arguments[0], arguments[1], arguments[2]);
</action>
<!-- Hotkeys won't work properly unless outside menu -->
<include directory="gui/session/hotkeys/"/>

View File

@ -1082,22 +1082,7 @@ var g_EntityCommands =
if (Engine.HotkeyIsPressed("session.noconfirmation"))
deleteSelection();
else
{
closeOpenDialogs();
g_PauseControl.implicitPause();
messageBox(
400, 200,
translate("Destroy everything currently selected?"),
translate("Delete"),
[translate("No"), translate("Yes")],
[
resumeGame,
() => {
deleteSelection();
resumeGame();
}
]);
};
(new DeleteSelectionConfirmation(deleteSelection)).display();
},
},