From f9114a87f2eedabd99611adbaa32ee1cbd3cf64c Mon Sep 17 00:00:00 2001 From: phosit Date: Mon, 8 Jul 2024 19:07:04 +0000 Subject: [PATCH] Get a promise when starting a GUIpage When calling `Engine.PushGuiPage` a promise is returned. The promise is settled when the "child" page is closed. That allows to `await` it inside `async` functions. Previously the callback was run right inside the call to `Engine.PopGuiPage`. Now the continuation of the promise is called at the end of the "tick". This won't help performance. It will more likely make things worse. Since gui pages aren't opened or closed that frequently, it doesn't matter that much. Refs: 86c151ebaa For the engine side: The promise is stored in the `CGUIManager::SGUIPage` (like previously the callback). When the promise is fulfilled it enqueues a callback in the `JobQueue` of the `JSContext`. Original patch by: @wraitii Comments by: @wraitii, @Stan, @Polakrity, @lyv, @elexis, @vladislavbelov Differential Revision: https://code.wildfiregames.com/D3807 This was SVN commit r28145. --- .../gui/regainFocus/pushWithPopOnInit.js | 2 +- .../mods/mod/gui/common/functions_msgbox.js | 33 ++++------- binaries/data/mods/mod/gui/common/terms.js | 24 ++++---- binaries/data/mods/mod/gui/modmod/modmodio.js | 9 ++- .../campaigns/default_menu/CampaignMenu.js | 11 +++- .../public/gui/common/functions_utility.js | 18 +++--- .../Panels/Buttons/CivInfoButton.js | 4 +- .../data/mods/public/gui/locale/locale.js | 8 +-- .../data/mods/public/gui/options/options.js | 19 +++--- .../mods/public/gui/pregame/MainMenuItems.js | 54 ++++++++--------- .../mods/public/gui/session/MenuButtons.js | 43 ++++++-------- .../public/gui/session/SessionMessageBox.js | 19 ++---- .../public/gui/session/selection_panels.js | 8 +-- .../public/gui/session/top_panel/CivIcon.js | 10 +--- source/gui/GUIManager.cpp | 59 ++++++++----------- source/gui/GUIManager.h | 31 +++++++--- .../gui/Scripting/JSInterface_GUIManager.cpp | 7 ++- source/gui/tests/test_GuiManager.h | 21 +++++-- source/scriptinterface/ScriptContext.cpp | 17 ++++-- source/scriptinterface/ScriptContext.h | 20 ++++++- source/scriptinterface/ScriptInterface.cpp | 3 +- source/simulation2/Simulation2.cpp | 2 + 22 files changed, 212 insertions(+), 210 deletions(-) diff --git a/binaries/data/mods/_test.gui/gui/regainFocus/pushWithPopOnInit.js b/binaries/data/mods/_test.gui/gui/regainFocus/pushWithPopOnInit.js index 9c01c3d9af..8181d15209 100644 --- a/binaries/data/mods/_test.gui/gui/regainFocus/pushWithPopOnInit.js +++ b/binaries/data/mods/_test.gui/gui/regainFocus/pushWithPopOnInit.js @@ -1 +1 @@ -Engine.PushGuiPage("regainFocus/page_emptyPage.xml", {}, () => Engine.PopGuiPage()); +Engine.PushGuiPage("regainFocus/page_emptyPage.xml").then(Engine.PopGuiPage); diff --git a/binaries/data/mods/mod/gui/common/functions_msgbox.js b/binaries/data/mods/mod/gui/common/functions_msgbox.js index ab02276108..e9e9224590 100644 --- a/binaries/data/mods/mod/gui/common/functions_msgbox.js +++ b/binaries/data/mods/mod/gui/common/functions_msgbox.js @@ -1,6 +1,7 @@ -function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode, mbCallbackArgs) +async function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode, + mbCallbackArgs) { - Engine.PushGuiPage( + const btnCode = await Engine.PushGuiPage( "page_msgbox.xml", { "width": mbWidth, @@ -8,16 +9,16 @@ function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbB "message": mbMessage, "title": mbTitle, "buttonCaptions": mbButtonCaptions - }, - btnCode => { - if (mbBtnCode !== undefined && mbBtnCode[btnCode]) - mbBtnCode[btnCode](mbCallbackArgs ? mbCallbackArgs[btnCode] : undefined); }); + + if (mbBtnCode !== undefined && mbBtnCode[btnCode]) + mbBtnCode[btnCode](mbCallbackArgs ? mbCallbackArgs[btnCode] : undefined); } -function timedConfirmation(width, height, message, timeParameter, timeout, title, buttonCaptions, btnCode, callbackArgs) +async function timedConfirmation(width, height, message, timeParameter, timeout, title, buttonCaptions, + btnCode, callbackArgs) { - Engine.PushGuiPage( + const button = await Engine.PushGuiPage( "page_timedconfirmation.xml", { "width": width, @@ -27,22 +28,10 @@ function timedConfirmation(width, height, message, timeParameter, timeout, title "timeout": timeout, "title": title, "buttonCaptions": buttonCaptions - }, - button => { - if (btnCode !== undefined && btnCode[button]) - btnCode[button](callbackArgs ? callbackArgs[button] : undefined); }); -} -function colorMixer(color, callback) -{ - Engine.PushGuiPage( - "page_colormixer.xml", - color, - result => { - callback(result); - } - ); + if (btnCode !== undefined && btnCode[button]) + btnCode[button](callbackArgs ? callbackArgs[button] : undefined); } function openURL(url) diff --git a/binaries/data/mods/mod/gui/common/terms.js b/binaries/data/mods/mod/gui/common/terms.js index 893e2dd333..b05ad037f4 100644 --- a/binaries/data/mods/mod/gui/common/terms.js +++ b/binaries/data/mods/mod/gui/common/terms.js @@ -5,9 +5,9 @@ function initTerms(terms) g_Terms = terms; } -function openTerms(page) +async function openTerms(page) { - Engine.PushGuiPage( + const data = await Engine.PushGuiPage( "page_termsdialog.xml", { "file": g_Terms[page].file, @@ -16,19 +16,17 @@ function openTerms(page) "urlButtons": g_Terms[page].urlButtons || [], "termsURL": g_Terms[page].termsURL || undefined, "page": page - }, - data => { - g_Terms[data.page].accepted = data.accepted; + }); - Engine.ConfigDB_CreateAndSaveValue( - "user", - g_Terms[data.page].config, - data.accepted ? getTermsHash(data.page) : "0"); + g_Terms[data.page].accepted = data.accepted; - if (g_Terms[data.page].callback) - g_Terms[data.page].callback(data); - } - ); + Engine.ConfigDB_CreateAndSaveValue( + "user", + g_Terms[data.page].config, + data.accepted ? getTermsHash(data.page) : "0"); + + if (g_Terms[data.page].callback) + g_Terms[data.page].callback(data); } function checkTerms() diff --git a/binaries/data/mods/mod/gui/modmod/modmodio.js b/binaries/data/mods/mod/gui/modmod/modmodio.js index b3e69f1044..e27b325184 100644 --- a/binaries/data/mods/mod/gui/modmod/modmodio.js +++ b/binaries/data/mods/mod/gui/modmod/modmodio.js @@ -23,8 +23,11 @@ function downloadModsButton() openTerms("Disclaimer"); } -function openModIo(data) +async function openModIo(data) { - if (data.accepted) - Engine.PushGuiPage("page_modio.xml", {}, initMods); + if (!data.accepted) + return; + + await Engine.PushGuiPage("page_modio.xml"); + initMods(); } diff --git a/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js b/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js index b5ac43ac64..dd51f7636b 100644 --- a/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js +++ b/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js @@ -19,16 +19,21 @@ class CampaignMenu extends AutoWatcher this.levelSelection.onMouseLeftDoubleClickItem = () => this.startScenario(); Engine.GetGUIObjectByName('startButton').onPress = () => this.startScenario(); Engine.GetGUIObjectByName('backToMain').onPress = () => this.goBackToMainMenu(); - Engine.GetGUIObjectByName('savedGamesButton').onPress = Engine.PushGuiPage.bind(Engine, - 'page_loadgame.xml', { "campaignRun": this.run.filename }, this.loadSavegame.bind(this)); + Engine.GetGUIObjectByName('savedGamesButton').onPress = this.loadSavegame.bind(this); this.mapCache = new MapCache(); this._ready = true; } - loadSavegame(gameId) + async loadSavegame() { + const gameId = await Engine.PushGuiPage( + 'page_loadgame.xml', + { + "campaignRun": this.run.filename + }); + if (!gameId) return; diff --git a/binaries/data/mods/public/gui/common/functions_utility.js b/binaries/data/mods/public/gui/common/functions_utility.js index f3c42fb211..5c95b242ac 100644 --- a/binaries/data/mods/public/gui/common/functions_utility.js +++ b/binaries/data/mods/public/gui/common/functions_utility.js @@ -288,19 +288,15 @@ function getBuildString() * property that page is opened with the @a args property of that object. * That continues untill there is no @a nextPage property in the completion * value. If there is no @a nextPage in the completion value the - * @a continuation is called with the completion value. + * @a completionValue is returned. * @param {String} page - The page first opened. * @param args - passed to the first page opened. - * @param continuation {function | undefined} - Completion callback, called when - * there is no @a nextPage property in the completion value. */ -function pageLoop(page, args, continuation) +async function pageLoop(page, args) { - (function recursiveFunction(completionValue) - { - if (completionValue?.nextPage != null) - Engine.PushGuiPage(completionValue.nextPage, completionValue.args, recursiveFunction); - else - continuation?.(completionValue); - })({ "nextPage": page, "args": args }); + let completionValue = { "nextPage": page, "args": args }; + while (completionValue?.nextPage != null) + completionValue = await Engine.PushGuiPage(completionValue.nextPage, completionValue.args); + + return completionValue; } diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CivInfoButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CivInfoButton.js index fda58c1983..6a2a63f7cd 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CivInfoButton.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CivInfoButton.js @@ -23,9 +23,9 @@ class CivInfoButton this.openPage(this.civInfo.page); } - openPage(page) + async openPage(page) { - pageLoop(page, this.civInfo.args, data => this.civInfo = data); + this.civInfo = await pageLoop(page, this.civInfo.args); } } diff --git a/binaries/data/mods/public/gui/locale/locale.js b/binaries/data/mods/public/gui/locale/locale.js index db67978bcc..f03094c2e9 100644 --- a/binaries/data/mods/public/gui/locale/locale.js +++ b/binaries/data/mods/public/gui/locale/locale.js @@ -52,14 +52,11 @@ function languageSelectionChanged() localeText.caption = locale; } -function openAdvancedMenu() +async function openAdvancedMenu() { let localeText = Engine.GetGUIObjectByName("localeText"); - Engine.PushGuiPage("page_locale_advanced.xml", { "locale": localeText.caption }, applyFromAdvancedMenu); -} + const locale = await Engine.PushGuiPage("page_locale_advanced.xml", { "locale": localeText.caption }); -function applyFromAdvancedMenu(locale) -{ if (!locale) return; @@ -72,6 +69,5 @@ function applyFromAdvancedMenu(locale) if (index != -1) languageList.selected = index; - var localeText = Engine.GetGUIObjectByName("localeText"); localeText.caption = locale; } diff --git a/binaries/data/mods/public/gui/options/options.js b/binaries/data/mods/public/gui/options/options.js index cf918b7370..9970ad7bbb 100644 --- a/binaries/data/mods/public/gui/options/options.js +++ b/binaries/data/mods/public/gui/options/options.js @@ -80,17 +80,14 @@ var g_OptionType = { control.caption = value; }, "initGUI": (option, control) => { - control.children[2].onPress = () => { - colorMixer( - control.caption, - (color) => { - if (color != control.caption) - { - control.caption = color; - control.onTextEdit(); - } - } - ); + control.children[2].onPress = async() => { + const color = await Engine.PushGuiPage("page_colormixer.xml", control.caption); + + if (color != control.caption) + { + control.caption = color; + control.onTextEdit(); + } }; }, "guiToValue": control => control.caption, diff --git a/binaries/data/mods/public/gui/pregame/MainMenuItems.js b/binaries/data/mods/public/gui/pregame/MainMenuItems.js index 8102bbdd57..4ba5d654f4 100644 --- a/binaries/data/mods/public/gui/pregame/MainMenuItems.js +++ b/binaries/data/mods/public/gui/pregame/MainMenuItems.js @@ -90,31 +90,32 @@ var g_MainMenuItems = [ { "caption": translate("Load Game"), "tooltip": translate("Load a saved game."), - "onPress": Engine.PushGuiPage.bind(Engine, "page_loadgame.xml", {}, (gameId) => + "onPress": async() => { + const gameId = await Engine.PushGuiPage("page_loadgame.xml"); + + if (!gameId) + return; + + const metadata = Engine.StartSavedGame(gameId); + if (!metadata) { - if (!gameId) - return; + error("Could not load saved game: " + gameId); + return; + } - const metadata = Engine.StartSavedGame(gameId); - if (!metadata) - { - error("Could not load saved game: " + gameId); - return; - } - - Engine.SwitchGuiPage("page_loading.xml", { - "attribs": metadata.initAttributes, - "playerAssignments": { - "local": { - "name": metadata.initAttributes.settings. - PlayerData[metadata.playerID]?.Name ?? - singleplayerName(), - "player": metadata.playerID - } - }, - "savedGUIData": metadata.gui - }); - }) + Engine.SwitchGuiPage("page_loading.xml", { + "attribs": metadata.initAttributes, + "playerAssignments": { + "local": { + "name": metadata.initAttributes.settings. + PlayerData[metadata.playerID]?.Name ?? + singleplayerName(), + "player": metadata.playerID + } + }, + "savedGUIData": metadata.gui + }); + } }, { "caption": translate("Continue Campaign"), @@ -221,11 +222,8 @@ var g_MainMenuItems = [ { "caption": translate("Options"), "tooltip": translate("Adjust game settings."), - "onPress": () => { - Engine.PushGuiPage( - "page_options.xml", - {}, - fireConfigChangeHandlers); + "onPress": async() => { + fireConfigChangeHandlers(await Engine.PushGuiPage("page_options.xml")); } }, { diff --git a/binaries/data/mods/public/gui/session/MenuButtons.js b/binaries/data/mods/public/gui/session/MenuButtons.js index e3194215b5..4fda15821f 100644 --- a/binaries/data/mods/public/gui/session/MenuButtons.js +++ b/binaries/data/mods/public/gui/session/MenuButtons.js @@ -16,11 +16,12 @@ MenuButtons.prototype.Manual = class this.pauseControl = pauseControl; } - onPress() + async onPress() { closeOpenDialogs(); this.pauseControl.implicitPause(); - Engine.PushGuiPage("page_manual.xml", {}, resumeGame); + await Engine.PushGuiPage("page_manual.xml"); + resumeGame(); } }; @@ -54,18 +55,18 @@ MenuButtons.prototype.Save = class this.pauseControl = pauseControl; } - onPress() + async onPress() { closeOpenDialogs(); this.pauseControl.implicitPause(); - Engine.PushGuiPage( + await Engine.PushGuiPage( "page_loadgame.xml", { "savedGameData": getSavedGameData(), "campaignRun": g_CampaignSession ? g_CampaignSession.run.filename : null - }, - resumeGame); + }); + resumeGame(); } }; @@ -92,7 +93,7 @@ MenuButtons.prototype.Summary = class }); } - onPress() + async onPress() { if (Engine.IsAtlasRunning()) return; @@ -102,7 +103,7 @@ MenuButtons.prototype.Summary = class // Allows players to see their own summary. // If they have shared ally vision researched, they are able to see the summary of there allies too. let simState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); - Engine.PushGuiPage( + const data = await Engine.PushGuiPage( "page_summary.xml", { "sim": { @@ -117,11 +118,10 @@ MenuButtons.prototype.Summary = class "isInGame": true, "summarySelection": this.summarySelection }, - }, - data => { - this.summarySelection = data.summarySelection; - this.pauseControl.implicitResume(); }); + + this.summarySelection = data.summarySelection; + this.pauseControl.implicitResume(); } }; @@ -162,18 +162,13 @@ MenuButtons.prototype.Options = class this.pauseControl = pauseControl; } - onPress() + async onPress() { closeOpenDialogs(); this.pauseControl.implicitPause(); - Engine.PushGuiPage( - "page_options.xml", - {}, - changes => { - fireConfigChangeHandlers(changes); - resumeGame(); - }); + fireConfigChangeHandlers(await Engine.PushGuiPage("page_options.xml")); + resumeGame(); } }; @@ -186,15 +181,13 @@ MenuButtons.prototype.Hotkeys = class this.pauseControl = pauseControl; } - onPress() + async onPress() { closeOpenDialogs(); this.pauseControl.implicitPause(); - Engine.PushGuiPage( - "hotkeys/page_hotkeys.xml", - {}, - () => { resumeGame(); }); + await Engine.PushGuiPage("hotkeys/page_hotkeys.xml"); + resumeGame(); } }; diff --git a/binaries/data/mods/public/gui/session/SessionMessageBox.js b/binaries/data/mods/public/gui/session/SessionMessageBox.js index 3b6170497d..b3bcb5d0c0 100644 --- a/binaries/data/mods/public/gui/session/SessionMessageBox.js +++ b/binaries/data/mods/public/gui/session/SessionMessageBox.js @@ -4,11 +4,12 @@ */ class SessionMessageBox { - display() + async display() { - this.onPageOpening(); + closeOpenDialogs(); + g_PauseControl.implicitPause(); - Engine.PushGuiPage( + const buttonId = await Engine.PushGuiPage( "page_msgbox.xml", { "width": this.Width, @@ -16,18 +17,8 @@ class SessionMessageBox "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); diff --git a/binaries/data/mods/public/gui/session/selection_panels.js b/binaries/data/mods/public/gui/session/selection_panels.js index cd3d2a9183..a0a063f5e6 100644 --- a/binaries/data/mods/public/gui/session/selection_panels.js +++ b/binaries/data/mods/public/gui/session/selection_panels.js @@ -1262,19 +1262,19 @@ function initSelectionPanels() * * @param {string} [civCode] - The template name of the entity that researches the selected technology. */ -function showTemplateDetails(templateName, civCode) +async function showTemplateDetails(templateName, civCode) { if (inputState != INPUT_NORMAL) return; g_PauseControl.implicitPause(); - Engine.PushGuiPage( + await Engine.PushGuiPage( "page_viewer.xml", { "templateName": templateName, "civ": civCode - }, - resumeGame); + }); + resumeGame(); } /** diff --git a/binaries/data/mods/public/gui/session/top_panel/CivIcon.js b/binaries/data/mods/public/gui/session/top_panel/CivIcon.js index f207d50a6c..e1d1ea1a5c 100644 --- a/binaries/data/mods/public/gui/session/top_panel/CivIcon.js +++ b/binaries/data/mods/public/gui/session/top_panel/CivIcon.js @@ -29,12 +29,12 @@ class CivIcon this.openPage(this.dialogSelection.page); } - openPage(page) + async openPage(page) { closeOpenDialogs(); g_PauseControl.implicitPause(); - pageLoop( + this.dialogSelection = await pageLoop( page, { // If an Observer triggers `openPage()` via hotkey, g_ViewedPlayer could be -1 or 0 @@ -42,12 +42,8 @@ class CivIcon "civ": this.dialogSelection.args.civ ?? g_Players[Math.max(g_ViewedPlayer, 1)].civ // TODO add info about researched techs and unlocked entities - }, - data => - { - this.dialogSelection = data; - resumeGame(); }); + resumeGame(); } rebuild() diff --git a/source/gui/GUIManager.cpp b/source/gui/GUIManager.cpp index b6815d2235..497d02f3d2 100644 --- a/source/gui/GUIManager.cpp +++ b/source/gui/GUIManager.cpp @@ -30,6 +30,7 @@ #include "ps/XML/Xeromyces.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/Object.h" +#include "scriptinterface/Promises.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/StructuredClone.h" @@ -112,24 +113,28 @@ void CGUIManager::SwitchPage(const CStrW& pageName, const ScriptInterface* srcSc m_PageStack.clear(); } - PushPage(pageName, initDataClone, JS::UndefinedHandleValue); + PushPage(pageName, initDataClone); } -void CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone initData, JS::HandleValue callbackFunction) +JS::Value CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone initData) { // Store the callback handler in the current GUI page before opening the new one - if (!m_PageStack.empty() && !callbackFunction.isUndefined()) - { - m_PageStack.back().SetCallbackFunction(m_ScriptInterface, callbackFunction); + JS::RootedValue promise{m_ScriptInterface.GetGeneralJSContext(), [&] + { + if (m_PageStack.empty()) + return JS::UndefinedValue(); - // Make sure we unfocus anything on the current page. - m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS); - } + // Make sure we unfocus anything on the current page. + m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS); + return m_PageStack.back().ReplacePromise(m_ScriptInterface); + }()}; // Push the page prior to loading its contents, because that may push // another GUI page on init which should be pushed on top of this new page. m_PageStack.emplace_back(pageName, initData); m_PageStack.back().LoadPage(m_ScriptContext); + + return promise; } void CGUIManager::PopPage(Script::StructuredClone args) @@ -144,7 +149,7 @@ void CGUIManager::PopPage(Script::StructuredClone args) m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS); m_PageStack.pop_back(); - m_PageStack.back().PerformCallbackFunction(args); + m_PageStack.back().ResolvePromise(args); // We return to a page where some object might have been focused. m_PageStack.back().gui->SendFocusMessage(GUIM_GOT_FOCUS); @@ -245,26 +250,15 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext) LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(m_Name)); } -void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc) +JS::Value CGUIManager::SGUIPage::ReplacePromise(ScriptInterface& scriptInterface) { - if (!callbackFunc.isObject()) - { - LOGERROR("Given callback handler is not an object!"); - return; - } - - ScriptRequest rq(scriptInterface); - - if (!JS_ObjectIsFunction(&callbackFunc.toObject())) - { - LOGERROR("Given callback handler is not a function!"); - return; - } - - callbackFunction = std::make_shared(scriptInterface.GetGeneralJSContext(), callbackFunc); + JSContext* generalContext{scriptInterface.GetGeneralJSContext()}; + callbackFunction = std::make_shared(generalContext, + JS::NewPromiseObject(generalContext, nullptr)); + return JS::ObjectValue(**callbackFunction); } -void CGUIManager::SGUIPage::PerformCallbackFunction(Script::StructuredClone args) +void CGUIManager::SGUIPage::ResolvePromise(Script::StructuredClone args) { if (!callbackFunction) return; @@ -274,7 +268,7 @@ void CGUIManager::SGUIPage::PerformCallbackFunction(Script::StructuredClone args JS::RootedObject globalObj(rq.cx, rq.glob); - JS::RootedValue funcVal(rq.cx, *callbackFunction); + JS::RootedObject funcVal(rq.cx, *callbackFunction); // Delete the callback function, so that it is not called again callbackFunction.reset(); @@ -283,13 +277,8 @@ void CGUIManager::SGUIPage::PerformCallbackFunction(Script::StructuredClone args if (args) Script::ReadStructuredClone(rq, args, &argVal); - JS::RootedValueVector paramData(rq.cx); - ignore_result(paramData.append(argVal)); - - JS::RootedValue result(rq.cx); - - if(!JS_CallFunctionValue(rq.cx, globalObj, funcVal, paramData, &result)) - ScriptException::CatchPending(rq); + // This only resolves the promise, it doesn't call the continuation. + JS::ResolvePromise(rq.cx, funcVal, argVal); } Status CGUIManager::ReloadChangedFile(const VfsPath& path) @@ -388,6 +377,8 @@ void CGUIManager::TickObjects() for (const SGUIPage& p : pageStack) p.gui->TickObjects(); + + m_ScriptContext.RunJobs(); } void CGUIManager::Draw(CCanvas2D& canvas) const diff --git a/source/gui/GUIManager.h b/source/gui/GUIManager.h index bcfd3f7ebc..318c6f232c 100644 --- a/source/gui/GUIManager.h +++ b/source/gui/GUIManager.h @@ -68,9 +68,9 @@ public: * Load a new GUI page and make it active. All current pages will be retained, * and will still be drawn and receive tick events, but will not receive * user inputs. - * If given, the callbackHandler function will be executed once this page is closed. + * The returned promise will be fulfilled once the pushed page is closed. */ - void PushPage(const CStrW& pageName, Script::StructuredClone initData, JS::HandleValue callbackFunc); + JS::Value PushPage(const CStrW& pageName, Script::StructuredClone initData); /** * Unload the currently active GUI page, and make the previous page active. @@ -146,16 +146,17 @@ private: void LoadPage(ScriptContext& scriptContext); /** - * Sets the callback handler when a new page is opened that will be performed when the page is closed. + * A new promise gets set. A reference to that promise is returned. The promise will settle when + * the page is closed. */ - void SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc); + JS::Value ReplacePromise(ScriptInterface& scriptInterface); /** * Execute the stored callback function with the given arguments. */ - void PerformCallbackFunction(Script::StructuredClone args); + void ResolvePromise(Script::StructuredClone args); - CStrW m_Name; + std::wstring m_Name; std::unordered_set inputs; // for hotloading Script::StructuredClone initData; // data to be passed to the init() function std::shared_ptr gui; // the actual GUI page @@ -164,7 +165,7 @@ private: * Function executed by this parent GUI page when the child GUI page it pushed is popped. * Notice that storing it in the SGUIPage instead of CGUI means that it will survive the hotloading CGUI reset. */ - std::shared_ptr callbackFunction; + std::shared_ptr callbackFunction; }; std::shared_ptr top() const; @@ -176,8 +177,22 @@ private: * The page stack must not move pointers on push/pop, or pushing a page in a page's init method * may crash (as the pusher page will suddenly have moved, and the stack will be confused). * Therefore use std::deque over std::vector. + * Also the elements have to be destructed back to front. */ - using PageStackType = std::deque; + class PageStackType : public std::deque + { + public: + ~PageStackType() + { + clear(); + } + + void clear() + { + while (!std::deque::empty()) + std::deque::pop_back(); + } + }; PageStackType m_PageStack; CTemplateLoader m_TemplateLoader; diff --git a/source/gui/Scripting/JSInterface_GUIManager.cpp b/source/gui/Scripting/JSInterface_GUIManager.cpp index c66b3dfedf..18d6da7f8a 100644 --- a/source/gui/Scripting/JSInterface_GUIManager.cpp +++ b/source/gui/Scripting/JSInterface_GUIManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -32,9 +32,10 @@ namespace JSI_GUIManager { // Note that the initData argument may only contain clonable data. // Functions aren't supported for example! -void PushGuiPage(const ScriptRequest& rq, const std::wstring& name, JS::HandleValue initData, JS::HandleValue callbackFunction) +// It returns a promise. +JS::Value PushGuiPage(const ScriptRequest& rq, const std::wstring& name, JS::HandleValue initData) { - g_GUI->PushPage(name, Script::WriteStructuredClone(rq, initData), callbackFunction); + return g_GUI->PushPage(name, Script::WriteStructuredClone(rq, initData)); } void SwitchGuiPage(const ScriptInterface& scriptInterface, const std::wstring& name, JS::HandleValue initData) diff --git a/source/gui/tests/test_GuiManager.h b/source/gui/tests/test_GuiManager.h index 268115a071..9a51c3c577 100644 --- a/source/gui/tests/test_GuiManager.h +++ b/source/gui/tests/test_GuiManager.h @@ -26,6 +26,7 @@ #include "ps/GameSetup/GameSetup.h" #include "ps/Hotkey.h" #include "ps/XML/Xeromyces.h" +#include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptRequest.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/StructuredClone.h" @@ -72,7 +73,7 @@ public: Script::CreateObject(rq, &val); Script::StructuredClone data = Script::WriteStructuredClone(rq, JS::NullHandleValue); - g_GUI->PushPage(L"event/page_event.xml", data, JS::UndefinedHandleValue); + g_GUI->PushPage(L"event/page_event.xml", data); const ScriptInterface& pageScriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); ScriptRequest prq(pageScriptInterface); @@ -135,7 +136,7 @@ public: Script::CreateObject(rq, &val); Script::StructuredClone data = Script::WriteStructuredClone(rq, JS::NullHandleValue); - g_GUI->PushPage(L"hotkey/page_hotkey.xml", data, JS::UndefinedHandleValue); + g_GUI->PushPage(L"hotkey/page_hotkey.xml", data); // Press 'a'. SDL_Event_ hotkeyNotification; @@ -208,20 +209,28 @@ public: JS::RootedValue val(rq.cx); Script::CreateObject(rq, &val); + TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 0); Script::StructuredClone data = Script::WriteStructuredClone(rq, JS::NullHandleValue); - g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data, JS::UndefinedHandleValue); + g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data); const ScriptInterface& pageScriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); ScriptRequest prq(pageScriptInterface); JS::RootedValue global(prq.cx, prq.globalValue()); - g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data, JS::UndefinedHandleValue); + TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1); + g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data); + TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 2); g_GUI->PopPage(data); + TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1); // This page instantly pushes an empty page with a callback that pops another page again. - g_GUI->PushPage(L"regainFocus/page_pushWithPopOnInit.xml", data, JS::UndefinedHandleValue); + g_GUI->PushPage(L"regainFocus/page_pushWithPopOnInit.xml", data); + TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 3); - // Pop the empty page and trigger the callback (effectively pops twice). + // Pop the empty page g_GUI->PopPage(data); + TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 2); + scriptInterface->GetContext().RunJobs(); + TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1); } }; diff --git a/source/scriptinterface/ScriptContext.cpp b/source/scriptinterface/ScriptContext.cpp index 9e27489ba7..eb1916a1d4 100644 --- a/source/scriptinterface/ScriptContext.cpp +++ b/source/scriptinterface/ScriptContext.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -22,6 +22,7 @@ #include "lib/alignment.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" +#include "scriptinterface/Promises.h" #include "scriptinterface/ScriptExtraHeaders.h" #include "scriptinterface/ScriptEngine.h" #include "scriptinterface/ScriptInterface.h" @@ -83,10 +84,9 @@ std::shared_ptr ScriptContext::CreateContext(int contextSize, int } ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger): - m_LastGCBytes(0), - m_LastGCCheck(0.0f), - m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger), - m_ContextSize(contextSize) + m_JobQueue{std::make_unique()}, + m_ContextSize{contextSize}, + m_HeapGrowthBytesGCTrigger{heapGrowthBytesGCTrigger} { ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be initialized before constructing any ScriptContexts!"); @@ -130,6 +130,8 @@ ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger): JS::ContextOptionsRef(m_cx).setStrictMode(true); ScriptEngine::GetSingleton().RegisterContext(m_cx); + + JS::SetJobQueue(m_cx, m_JobQueue.get()); } ScriptContext::~ScriptContext() @@ -268,6 +270,11 @@ void ScriptContext::ShrinkingGC() JS_SetGCParameter(m_cx, JSGC_PER_ZONE_GC_ENABLED, false); } +void ScriptContext::RunJobs() +{ + m_JobQueue->runJobs(m_cx); +} + void ScriptContext::PrepareZonesForIncrementalGC() const { for (JS::Realm* const& realm : m_Realms) diff --git a/source/scriptinterface/ScriptContext.h b/source/scriptinterface/ScriptContext.h index 93abb94345..c1b08058f2 100644 --- a/source/scriptinterface/ScriptContext.h +++ b/source/scriptinterface/ScriptContext.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -27,6 +27,11 @@ constexpr int DEFAULT_CONTEXT_SIZE = 16 * 1024 * 1024; constexpr int DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER = 2 * 1024 * 1024; +namespace Script +{ +class JobQueue; +} + /** * Abstraction around a SpiderMonkey JSContext. * @@ -74,6 +79,14 @@ public: void RegisterRealm(JS::Realm* realm); void UnRegisterRealm(JS::Realm* realm); + /** + * Runs the promise continuation. + * On contexts where promises can be used this function has to be + * called. + * This function has to be called frequently. + */ + void RunJobs(); + /** * GetGeneralJSContext returns the context without starting a GC request and without * entering any compartment. It should only be used in specific situations, such as @@ -86,14 +99,15 @@ public: private: JSContext* m_cx; + const std::unique_ptr m_JobQueue; void PrepareZonesForIncrementalGC() const; std::list m_Realms; int m_ContextSize; int m_HeapGrowthBytesGCTrigger; - int m_LastGCBytes; - double m_LastGCCheck; + int m_LastGCBytes{0}; + double m_LastGCCheck{0.0}; }; // Using a global object for the context is a workaround until Simulation, AI, etc, diff --git a/source/scriptinterface/ScriptInterface.cpp b/source/scriptinterface/ScriptInterface.cpp index e4057ac6a9..0df8477046 100644 --- a/source/scriptinterface/ScriptInterface.cpp +++ b/source/scriptinterface/ScriptInterface.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -384,6 +384,7 @@ ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugN ScriptInterface::~ScriptInterface() { + m->m_context.RunJobs(); if (Threading::IsMainThread()) { if (g_ScriptStatsTable) diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp index af4964506f..925b135e85 100644 --- a/source/simulation2/Simulation2.cpp +++ b/source/simulation2/Simulation2.cpp @@ -585,6 +585,8 @@ void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengt cmpPathfinder->UpdateGrid(); cmpPathfinder->StartProcessingMoves(false); } + + componentManager.GetScriptInterface().GetContext().RunJobs(); } void CSimulation2Impl::Interpolate(float simFrameLength, float frameOffset, float realFrameLength)