Only call Engine.EndGame right before closing

Since #7786, the GUI tick completed normally after calling
`closePageCallback` instead of being cancelled like previously. Since
`Engine.EndGame` deletes the internal `g_Game` pointer, every Engine call
after it that tried to access `g_Game` caused a segfault.
This patch solves it by moving `Engine.EndGame` to the very end, right
before closing the session page.
In order to follow the convention that only functions directly closing
the page are called `closePageCallback`, that function is in turn
renamed to `closeSession`. Additionally, this allows only resolving
the promise with a bool (`showSummary`) and then only calling
`getNextPageOpenRequest` at the end of `init` for reasons of simplicity.
This commit is contained in:
Vantha 2026-06-11 21:06:33 +02:00 committed by Vantha
parent 801d21671e
commit 2520c45220
10 changed files with 58 additions and 64 deletions

View file

@ -3,7 +3,7 @@
*/ */
class Menu class Menu
{ {
constructor(pauseControl, playerViewControl, chat, closePageCallback) constructor(pauseControl, playerViewControl, chat, closeSession)
{ {
this.menuButton = Engine.GetGUIObjectByName("menuButton"); this.menuButton = Engine.GetGUIObjectByName("menuButton");
this.menuButton.onPress = this.toggle.bind(this); this.menuButton.onPress = this.toggle.bind(this);
@ -26,7 +26,7 @@ class Menu
this.buttons = handlerNames.map((handlerName, i) => this.buttons = handlerNames.map((handlerName, i) =>
{ {
const handler = new MenuButtons.prototype[handlerName](menuButtons[i], pauseControl, playerViewControl, chat); const handler = new MenuButtons.prototype[handlerName](menuButtons[i], pauseControl, playerViewControl, chat);
this.initButton(handler, menuButtons[i], i, closePageCallback); this.initButton(handler, menuButtons[i], i, closeSession);
return handler; return handler;
}); });
@ -62,12 +62,12 @@ class Menu
this.startAnimation(); this.startAnimation();
} }
initButton(handler, button, i, closePageCallback) initButton(handler, button, i, closeSession)
{ {
button.onPress = () => button.onPress = () =>
{ {
this.close(); this.close();
handler.onPress(closePageCallback); handler.onPress(closeSession);
}; };
button.size.top = this.buttonHeight * (i + 1) + this.margin; button.size.top = this.buttonHeight * (i + 1) + this.margin;
button.size.bottom = this.buttonHeight * (i + 2); button.size.bottom = this.buttonHeight * (i + 2);

View file

@ -283,13 +283,13 @@ MenuButtons.prototype.Exit = class
this.pauseControl = pauseControl; this.pauseControl = pauseControl;
} }
onPress(closePageCallback) onPress(closeSession)
{ {
for (const name in QuitConfirmationMenu.prototype) for (const name in QuitConfirmationMenu.prototype)
{ {
const quitConfirmation = new QuitConfirmationMenu.prototype[name](); const quitConfirmation = new QuitConfirmationMenu.prototype[name]();
if (quitConfirmation.enabled()) if (quitConfirmation.enabled())
quitConfirmation.display(closePageCallback); quitConfirmation.display(closeSession);
} }
} }
}; };

View file

@ -4,15 +4,12 @@
*/ */
class NetworkStatusOverlay class NetworkStatusOverlay
{ {
constructor(closePageCallback) constructor(closeSession)
{ {
this.netStatus = Engine.GetGUIObjectByName("netStatus"); this.netStatus = Engine.GetGUIObjectByName("netStatus");
this.loadingClientsText = Engine.GetGUIObjectByName("loadingClientsText"); this.loadingClientsText = Engine.GetGUIObjectByName("loadingClientsText");
Engine.GetGUIObjectByName("disconnectedExitButton").onPress = () => Engine.GetGUIObjectByName("disconnectedExitButton").onPress = () => { closeSession(true); };
{
closePageCallback({ [Engine.openRequest]: endGame(true) });
};
registerNetworkStatusChangeHandler(this.onNetStatusMessage.bind(this)); registerNetworkStatusChangeHandler(this.onNetStatusMessage.bind(this));
registerClientsLoadingHandler(this.onClientsLoadingMessage.bind(this)); registerClientsLoadingHandler(this.onClientsLoadingMessage.bind(this));

View file

@ -4,7 +4,7 @@
*/ */
class SessionMessageBox class SessionMessageBox
{ {
async display(closePageCallback) async display(closeSession)
{ {
closeOpenDialogs(); closeOpenDialogs();
g_PauseControl.implicitPause(); g_PauseControl.implicitPause();
@ -19,12 +19,9 @@ class SessionMessageBox
"buttonCaptions": this.Buttons ? this.Buttons.map(button => button.caption) : undefined, "buttonCaptions": this.Buttons ? this.Buttons.map(button => button.caption) : undefined,
}); });
this.closeSession = closeSession;
if (this.Buttons && this.Buttons[buttonId].onPress) if (this.Buttons && this.Buttons[buttonId].onPress)
{ this.Buttons[buttonId].onPress.call(this);
const ret = this.Buttons[buttonId].onPress.call(this);
if (ret !== undefined)
closePageCallback({ [Engine.openRequest]: ret });
}
if (this.ResumeOnClose) if (this.ResumeOnClose)
resumeGame(); resumeGame();

View file

@ -18,12 +18,12 @@ QuitConfirmation.prototype.Buttons =
{ {
// Translation: Shown in the Dialog that shows up when the game finishes // Translation: Shown in the Dialog that shows up when the game finishes
"caption": translate("Quit and View Summary"), "caption": translate("Quit and View Summary"),
"onPress": () => endGame(true) "onPress": function() { this.closeSession(true); }
}, },
{ {
// Translation: Shown in the Dialog that shows up when the game finishes // Translation: Shown in the Dialog that shows up when the game finishes
"caption": translate("Quit"), "caption": translate("Quit"),
"onPress": () => endGame(false) "onPress": function() { this.closeSession(false); }
} }
]; ];

View file

@ -3,7 +3,7 @@
*/ */
class QuitConfirmationDefeat extends QuitConfirmation class QuitConfirmationDefeat extends QuitConfirmation
{ {
constructor(closePageCallback) constructor(closeSession)
{ {
super(); super();
@ -11,10 +11,10 @@ class QuitConfirmationDefeat extends QuitConfirmation
return; return;
this.confirmHandler = undefined; this.confirmHandler = undefined;
registerPlayersFinishedHandler(this.onPlayersFinished.bind(this, closePageCallback)); registerPlayersFinishedHandler(this.onPlayersFinished.bind(this, closeSession));
} }
onPlayersFinished(closePageCallback, players, won) onPlayersFinished(closeSession, players, won)
{ {
if (players.indexOf(Engine.GetPlayerID()) == -1) if (players.indexOf(Engine.GetPlayerID()) == -1)
return; return;
@ -22,11 +22,11 @@ class QuitConfirmationDefeat extends QuitConfirmation
// Defer simulation result until // Defer simulation result until
// 1. the loading screen finished for all networked clients (g_IsNetworkedActive) // 1. the loading screen finished for all networked clients (g_IsNetworkedActive)
// 2. all messages modifying g_Players victory state were processed (next turn) // 2. all messages modifying g_Players victory state were processed (next turn)
this.confirmHandler = this.confirmExit.bind(this, won, closePageCallback); this.confirmHandler = this.confirmExit.bind(this, won, closeSession);
registerSimulationUpdateHandler(this.confirmHandler); registerSimulationUpdateHandler(this.confirmHandler);
} }
confirmExit(won, closePageCallback) confirmExit(won, closeSession)
{ {
if (g_IsNetworked && !g_IsNetworkedActive) if (g_IsNetworked && !g_IsNetworkedActive)
return; return;
@ -47,7 +47,7 @@ class QuitConfirmationDefeat extends QuitConfirmation
this.Buttons = askExit ? super.Buttons : undefined; this.Buttons = askExit ? super.Buttons : undefined;
this.display(closePageCallback); this.display(closeSession);
} }
} }

View file

@ -6,7 +6,7 @@ ReturnQuestion.prototype.Caption = translate("Do you want to resign or will you
ReturnQuestion.prototype.Buttons = [ ReturnQuestion.prototype.Buttons = [
{ {
"caption": translate("I will return"), "caption": translate("I will return"),
"onPress": () => endGame(false) "onPress": function() { this.closeSession(false); }
}, },
{ {
"caption": translate("I resign"), "caption": translate("I resign"),
@ -74,9 +74,9 @@ QuitConfirmationMenu.prototype.MultiplayerClient.prototype.Buttons =
}, },
{ {
"caption": translate("Yes"), "caption": translate("Yes"),
"onPress": closePageCallback => "onPress": closeSession =>
{ {
(new ReturnQuestion()).display(closePageCallback); (new ReturnQuestion()).display(closeSession);
} }
} }
]; ];

View file

@ -3,10 +3,10 @@
*/ */
class QuitConfirmationReplay extends QuitConfirmation class QuitConfirmationReplay extends QuitConfirmation
{ {
constructor(closePageCallback) constructor(closeSession)
{ {
super(); super();
Engine.GetGUIObjectByName("session").onReplayFinished = this.display.bind(this, closePageCallback); Engine.GetGUIObjectByName("session").onReplayFinished = this.display.bind(this, closeSession);
} }
} }
@ -26,11 +26,11 @@ QuitConfirmationReplay.prototype.Buttons =
{ {
// Translation: Shown in the Dialog that shows up when a replay finishes // Translation: Shown in the Dialog that shows up when a replay finishes
"caption": translate("Quit and View Summary"), "caption": translate("Quit and View Summary"),
"onPress": () => endGame(true) "onPress": function() { this.closeSession(true); }
}, },
{ {
// Translation: Shown in the Dialog that shows up when a replay finishes // Translation: Shown in the Dialog that shows up when a replay finishes
"caption": translate("Quit"), "caption": translate("Quit"),
"onPress": () => endGame(false) "onPress": function() { this.closeSession(false); }
} }
]; ];

View file

@ -338,13 +338,13 @@ async function init(initData, hotloadData)
{ {
if (g_IsNetworked) if (g_IsNetworked)
handleNetMessages().catch(reject); handleNetMessages().catch(reject);
}), new Promise(closePageCallback => }), new Promise(closeSession =>
{ {
g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat, closePageCallback); g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat, closeSession);
g_NetworkStatusOverlay = new NetworkStatusOverlay(closePageCallback); g_NetworkStatusOverlay = new NetworkStatusOverlay(closeSession);
g_QuitConfirmationDefeat = new QuitConfirmationDefeat(closePageCallback); g_QuitConfirmationDefeat = new QuitConfirmationDefeat(closeSession);
g_QuitConfirmationReplay = new QuitConfirmationReplay(closePageCallback); g_QuitConfirmationReplay = new QuitConfirmationReplay(closeSession);
g_TutorialManager = new TutorialManager(closePageCallback, hotloadData?.tutorial); g_TutorialManager = new TutorialManager(closeSession, hotloadData?.tutorial);
})]); })]);
// TODO: use event instead // TODO: use event instead
@ -370,7 +370,19 @@ async function init(initData, hotloadData)
setTimeout(displayGamestateNotifications, 1000); setTimeout(displayGamestateNotifications, 1000);
return promise; const showSummary = await promise;
// Depends on the simulation, so it has to be called before EndGame.
const openRequest = getNextPageOpenRequest(showSummary);
Engine.EndGame();
// After the replay file was closed in EndGame
if (!g_IsReplay)
Engine.AddReplayToCache(Engine.GetCurrentReplayDirectory());
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
return { [Engine.openRequest]: openRequest };
} }
function registerPlayersInitHandler(handler) function registerPlayersInitHandler(handler)
@ -530,31 +542,20 @@ function closeOpenDialogs()
g_TradeDialog.close(); g_TradeDialog.close();
} }
function endGame(showSummary) /**
* Put together an open request for the next GUI page to show after closing the session.
* This could be, depending on the situation, the the summary screen, the main menu, the lobby, etc.
*/
function getNextPageOpenRequest(showSummary)
{ {
// Before ending the game
const replayDirectory = Engine.GetCurrentReplayDirectory();
const simData = Engine.GuiInterfaceCall("GetReplayMetadata");
const playerID = Engine.GetPlayerID();
Engine.EndGame();
// After the replay file was closed in EndGame
// Done here to keep EndGame small
if (!g_IsReplay)
Engine.AddReplayToCache(replayDirectory);
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
const summaryData = { const summaryData = {
"sim": simData, "sim": Engine.GuiInterfaceCall("GetReplayMetadata"),
"gui": { "gui": {
"dialog": false, "dialog": false,
"assignedPlayer": playerID, "assignedPlayer": Engine.GetPlayerID(),
"disconnected": g_Disconnected, "disconnected": g_Disconnected,
"isReplay": g_IsReplay, "isReplay": g_IsReplay,
"replayDirectory": !g_HasRejoined && replayDirectory, "replayDirectory": !g_HasRejoined && Engine.GetCurrentReplayDirectory(),
"replaySelectionData": g_ReplaySelectionData "replaySelectionData": g_ReplaySelectionData
} }
}; };
@ -563,9 +564,8 @@ function endGame(showSummary)
{ {
const menu = g_CampaignSession.getMenu(); const menu = g_CampaignSession.getMenu();
if (g_InitAttributes.campaignData.skipSummary) if (g_InitAttributes.campaignData.skipSummary)
{
return { "page": menu }; return { "page": menu };
}
summaryData.campaignData = { "filename": g_InitAttributes.campaignData.run }; summaryData.campaignData = { "filename": g_InitAttributes.campaignData.run };
summaryData.nextPage = menu; summaryData.nextPage = menu;
} }

View file

@ -25,11 +25,11 @@ class TutorialManager
activePanel; activePanel;
currentWarning = ""; currentWarning = "";
isFinished = false; isFinished = false;
closePageCallback; closeSession;
constructor(closePageCallback, hotloadData) constructor(closeSession, hotloadData)
{ {
this.closePageCallback = closePageCallback; this.closeSession = closeSession;
this.parentObj.hidden = true; this.parentObj.hidden = true;
@ -140,7 +140,7 @@ class TutorialManager
continue() continue()
{ {
if (this.isFinished) if (this.isFinished)
this.closePageCallback({ [Engine.openRequest]: endGame(true) }); this.closeSession(true);
else if (this.pendingSteps.length) else if (this.pendingSteps.length)
this.displayNextPendingStep(); this.displayNextPendingStep();
else else