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)