Index: binaries/data/mods/_test.gui/gui/regainFocus/pushWithPopOnInit.js =================================================================== --- binaries/data/mods/_test.gui/gui/regainFocus/pushWithPopOnInit.js +++ 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.bind(Engine)); Index: binaries/data/mods/_test.scriptinterface/promises/simple.js =================================================================== --- /dev/null +++ binaries/data/mods/_test.scriptinterface/promises/simple.js @@ -0,0 +1,33 @@ +var test = 0; + +function incrementTest() +{ + test += 1; +} + +async function waitAndIncrement(promise) +{ + await promise; + incrementTest(); +} + +function runTest() +{ + var rsv; + let prom = new Promise((resolve, reject) => { + incrementTest(); + rsv = resolve; + }); + waitAndIncrement(prom); + TS_ASSERT_EQUALS(test, 1); + rsv(); + // At this point, waitAndIncrement is still not run, but is now free to run. + TS_ASSERT_EQUALS(test, 1); +} + +runTest(); + +function endTest() +{ + TS_ASSERT_EQUALS(test, 2); +} Index: binaries/data/mods/mod/gui/common/functions_msgbox.js =================================================================== --- binaries/data/mods/mod/gui/common/functions_msgbox.js +++ binaries/data/mods/mod/gui/common/functions_msgbox.js @@ -1,24 +1,23 @@ -function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode, mbCallbackArgs) +async function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode, + mbCallbackArgs) { - Engine.PushGuiPage( - "page_msgbox.xml", + const btnCode = await Engine.PushGuiPage("page_msgbox.xml", { "width": mbWidth, "height": mbHeight, "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( - "page_timedconfirmation.xml", + const button = await Engine.PushGuiPage("page_timedconfirmation.xml", { "width": width, "height": height, @@ -27,22 +26,15 @@ "timeout": timeout, "title": title, "buttonCaptions": buttonCaptions - }, - button => { - if (btnCode !== undefined && btnCode[button]) - btnCode[button](callbackArgs ? callbackArgs[button] : undefined); }); + + if (btnCode !== undefined && btnCode[button]) + btnCode[button](callbackArgs ? callbackArgs[button] : undefined); } -function colorMixer(color, callback) +function colorMixer(color) { - Engine.PushGuiPage( - "page_colormixer.xml", - color, - result => { - callback(result); - } - ); + return Engine.PushGuiPage("page_colormixer.xml", color) } function openURL(url) Index: binaries/data/mods/mod/gui/common/terms.js =================================================================== --- binaries/data/mods/mod/gui/common/terms.js +++ binaries/data/mods/mod/gui/common/terms.js @@ -5,10 +5,9 @@ g_Terms = terms; } -function openTerms(page) +async function openTerms(page) { - Engine.PushGuiPage( - "page_termsdialog.xml", + const data = await Engine.PushGuiPage("page_termsdialog.xml", { "file": g_Terms[page].file, "title": g_Terms[page].title, @@ -16,19 +15,15 @@ "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"); - - if (g_Terms[data.page].callback) - g_Terms[data.page].callback(data); - } - ); + }); + + g_Terms[data.page].accepted = data.accepted; + + 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() Index: binaries/data/mods/mod/gui/modmod/modmodio.js =================================================================== --- binaries/data/mods/mod/gui/modmod/modmodio.js +++ binaries/data/mods/mod/gui/modmod/modmodio.js @@ -23,8 +23,11 @@ openTerms("Disclaimer"); } -function openModIo(data) +await function openModIo(data) { - if (data.accepted) - Engine.PushGuiPage("page_modio.xml", {}, initMods); + if (!data.accepted) + return; + + await Engine.PushGuiPage("page_modio.xml"); + initMods(); } Index: binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js =================================================================== --- binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js +++ binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js @@ -19,38 +19,40 @@ 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 = async function() + { + const gameId = await Engine.PushGuiPage('page_loadgame.xml', + { + "campaignRun": this.run.filename + }); - this.mapCache = new MapCache(); + if (!gameId) + return; - this._ready = true; - } - - loadSavegame(gameId) - { - if (!gameId) - return; + const metadata = Engine.StartSavedGame(gameId); + if (!metadata) + { + 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 - }); + this.mapCache = new MapCache(); + + this._ready = true; } goBackToMainMenu() Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CivInfoButton.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CivInfoButton.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CivInfoButton.js @@ -24,23 +24,14 @@ this.openPage(this.civInfo.page); } - openPage(page) + async openPage(page) { - Engine.PushGuiPage( - page, - { "civ": this.civInfo.civ }, - this.storeCivInfoPage.bind(this)); - } + let data = await Engine.PushGuiPage(page, { "civ": this.civInfo.civ }) - storeCivInfoPage(data) - { - if (data.nextPage) - Engine.PushGuiPage( - data.nextPage, - { "civ": data.civ }, - this.storeCivInfoPage.bind(this)); - else - this.civInfo = data; + while (data.nextPage) + data = await Engine.PushGuiPage(data.nextPage, { "civ": data.civ }); + + this.civInfo = data; } } Index: binaries/data/mods/public/gui/locale/locale.js =================================================================== --- binaries/data/mods/public/gui/locale/locale.js +++ binaries/data/mods/public/gui/locale/locale.js @@ -52,14 +52,11 @@ 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; Index: binaries/data/mods/public/gui/options/options.js =================================================================== --- binaries/data/mods/public/gui/options/options.js +++ binaries/data/mods/public/gui/options/options.js @@ -80,17 +80,15 @@ 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 function() + { + const color = await colorMixer(control.caption); + + if (color != control.caption) + { + control.caption = color; + control.onTextEdit(); + } }; }, "guiToValue": control => control.caption, Index: binaries/data/mods/public/gui/pregame/MainMenuItems.js =================================================================== --- binaries/data/mods/public/gui/pregame/MainMenuItems.js +++ binaries/data/mods/public/gui/pregame/MainMenuItems.js @@ -36,24 +36,22 @@ "caption": translate("Structure Tree"), "tooltip": colorizeHotkey(translate("%(hotkey)s: View the structure tree of civilizations featured in 0 A.D."), "structree"), "hotkey": "structree", - "onPress": () => { - let callback = data => { - if (data.nextPage) - Engine.PushGuiPage(data.nextPage, { "civ": data.civ }, callback); - }; - Engine.PushGuiPage("page_structree.xml", {}, callback); + "onPress": async function() + { + let data = await Engine.PushGuiPage("page_structree.xml"); + while (data.nextPage) + data = await Engine.PushGuiPage(data.nextPage, { "civ": data.civ }); }, }, { "caption": translate("Civilization Overview"), "tooltip": colorizeHotkey(translate("%(hotkey)s: Learn about the civilizations featured in 0 A.D."), "civinfo"), "hotkey": "civinfo", - "onPress": () => { - let callback = data => { - if (data.nextPage) - Engine.PushGuiPage(data.nextPage, { "civ": data.civ }, callback); - }; - Engine.PushGuiPage("page_civinfo.xml", {}, callback); + "onPress": async function() + { + let data = await Engine.PushGuiPage("page_civinfo.xml"); + while (data.nextPage) + data = await Engine.PushGuiPage(data.nextPage, { "civ": data.civ }); } }, { @@ -102,31 +100,33 @@ { "caption": translate("Load Game"), "tooltip": translate("Load a saved game."), - "onPress": Engine.PushGuiPage.bind(Engine, "page_loadgame.xml", {}, (gameId) => - { - if (!gameId) - return; + "onPress": async function() + { + const gameId = await Engine.PushGuiPage("page_loadgame.xml"); - const metadata = Engine.StartSavedGame(gameId); - if (!metadata) - { - error("Could not load saved game: " + gameId); - return; - } + if (!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 - }); - }) + 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 + }); + } }, { "caption": translate("Continue Campaign"), @@ -233,11 +233,10 @@ { "caption": translate("Options"), "tooltip": translate("Adjust game settings."), - "onPress": () => { - Engine.PushGuiPage( - "page_options.xml", - {}, - fireConfigChangeHandlers); + "onPress": async function() + { + const changes = await Engine.PushGuiPage("page_options.xml"); + fireConfigChangeHandlers(changes); } }, { Index: binaries/data/mods/public/gui/session/MenuButtons.js =================================================================== --- binaries/data/mods/public/gui/session/MenuButtons.js +++ binaries/data/mods/public/gui/session/MenuButtons.js @@ -16,11 +16,12 @@ 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,17 @@ this.pauseControl = pauseControl; } - onPress() + async onPress() { closeOpenDialogs(); this.pauseControl.implicitPause(); - Engine.PushGuiPage( - "page_loadgame.xml", + await Engine.PushGuiPage("page_loadgame.xml", { "savedGameData": getSavedGameData(), "campaignRun": g_CampaignSession ? g_CampaignSession.run.filename : null - }, - resumeGame); + }); + resumeGame(); } }; @@ -92,7 +92,7 @@ }); } - onPress() + async onPress() { if (Engine.IsAtlasRunning()) return; @@ -102,8 +102,7 @@ // 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( - "page_summary.xml", + const data = await Engine.PushGuiPage("page_summary.xml", { "sim": { "mapSettings": g_InitAttributes.settings, @@ -117,11 +116,10 @@ "isInGame": true, "summarySelection": this.summarySelection }, - }, - data => { - this.summarySelection = data.summarySelection; - this.pauseControl.implicitResume(); }); + + this.summarySelection = data.summarySelection; + this.pauseControl.implicitResume(); } }; @@ -162,18 +160,14 @@ this.pauseControl = pauseControl; } - onPress() + async onPress() { closeOpenDialogs(); this.pauseControl.implicitPause(); - Engine.PushGuiPage( - "page_options.xml", - {}, - changes => { - fireConfigChangeHandlers(changes); - resumeGame(); - }); + const changes = await Engine.PushGuiPage("page_options.xml"); + fireConfigChangeHandlers(changes); + resumeGame(); } }; @@ -186,15 +180,13 @@ 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(); } }; Index: binaries/data/mods/public/gui/session/SessionMessageBox.js =================================================================== --- binaries/data/mods/public/gui/session/SessionMessageBox.js +++ binaries/data/mods/public/gui/session/SessionMessageBox.js @@ -4,30 +4,20 @@ */ class SessionMessageBox { - display() + async display() { - this.onPageOpening(); + closeOpenDialogs(); + g_PauseControl.implicitPause(); - Engine.PushGuiPage( - "page_msgbox.xml", + const buttonId = await 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); Index: binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- binaries/data/mods/public/gui/session/selection_panels.js +++ binaries/data/mods/public/gui/session/selection_panels.js @@ -1262,19 +1262,19 @@ * * @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(); } /** Index: binaries/data/mods/public/gui/session/top_panel/CivIcon.js =================================================================== --- binaries/data/mods/public/gui/session/top_panel/CivIcon.js +++ binaries/data/mods/public/gui/session/top_panel/CivIcon.js @@ -27,35 +27,25 @@ this.openPage(this.dialogSelection.page); } - openPage(page) + async openPage(page) { closeOpenDialogs(); g_PauseControl.implicitPause(); - Engine.PushGuiPage( - page, + let data = await Engine.PushGuiPage(page, { // If an Observer triggers `openPage()` via hotkey, g_ViewedPlayer could be -1 or 0 // (depending on whether they're "viewing" no-one or gaia respectively) "civ": this.dialogSelection.civ || g_Players[Math.max(g_ViewedPlayer, 1)].civ, // TODO add info about researched techs and unlocked entities - }, - this.storePageSelection.bind(this)); - } + }); - storePageSelection(data) - { - if (data.nextPage) - Engine.PushGuiPage( - data.nextPage, - { "civ": data.civ }, - this.storePageSelection.bind(this)); - else - { - this.dialogSelection = data; - resumeGame(); - } + while (data.nextPage) + data = await Engine.PushGuiPage(data.nextPage, { "civ": data.civ }); + + this.dialogSelection = data; + resumeGame(); } rebuild() Index: binaries/data/mods/public/gui/summary/summary.xml =================================================================== --- binaries/data/mods/public/gui/summary/summary.xml +++ binaries/data/mods/public/gui/summary/summary.xml @@ -112,7 +112,7 @@ - + Index: source/gui/GUIManager.h =================================================================== --- source/gui/GUIManager.h +++ source/gui/GUIManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 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 @@ -68,9 +68,9 @@ * 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 std::wstring& pageName, Script::StructuredClone initData); /** * Unload the currently active GUI page, and make the previous page active. @@ -148,7 +148,7 @@ /** * Sets the callback handler when a new page is opened that will be performed when the page is closed. */ - void SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc); + void SetPromise(ScriptInterface& scriptInterface, JS::HandleObject promise); /** * Execute the stored callback function with the given arguments. @@ -164,7 +164,7 @@ * 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; Index: source/gui/GUIManager.cpp =================================================================== --- source/gui/GUIManager.cpp +++ source/gui/GUIManager.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 @@ -33,6 +33,8 @@ #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/StructuredClone.h" +#include "js/Promise.h" + namespace { @@ -111,15 +113,17 @@ 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 std::wstring& pageName, Script::StructuredClone initData) { + JSContext* generalContext{m_ScriptInterface->GetGeneralJSContext()}; + JS::RootedObject promise{generalContext, JS::NewPromiseObject(generalContext, nullptr)}; // Store the callback handler in the current GUI page before opening the new one - if (!m_PageStack.empty() && !callbackFunction.isUndefined()) + if (!m_PageStack.empty()) { - m_PageStack.back().SetCallbackFunction(*m_ScriptInterface, callbackFunction); + m_PageStack.back().SetPromise(*m_ScriptInterface, promise); // Make sure we unfocus anything on the current page. m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS); @@ -129,6 +133,8 @@ // 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 JS::ObjectValue(*promise); } void CGUIManager::PopPage(Script::StructuredClone args) @@ -244,23 +250,10 @@ LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(m_Name)); } -void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc) +void CGUIManager::SGUIPage::SetPromise(ScriptInterface& scriptInterface, JS::HandleObject promise) { - 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); + callbackFunction = + std::make_shared(scriptInterface.GetGeneralJSContext(), promise); } void CGUIManager::SGUIPage::PerformCallbackFunction(Script::StructuredClone args) @@ -273,7 +266,7 @@ 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(); @@ -282,13 +275,8 @@ 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) @@ -387,6 +375,8 @@ for (const SGUIPage& p : pageStack) p.gui->TickObjects(); + + m_ScriptInterface->GetContext().RunJobs(); } void CGUIManager::Draw(CCanvas2D& canvas) const Index: source/gui/Scripting/JSInterface_GUIManager.cpp =================================================================== --- source/gui/Scripting/JSInterface_GUIManager.cpp +++ 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 @@ { // 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) Index: source/gui/tests/test_GuiManager.h =================================================================== --- source/gui/tests/test_GuiManager.h +++ source/gui/tests/test_GuiManager.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 @@ -69,7 +69,7 @@ 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); @@ -133,7 +133,7 @@ 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,17 +208,17 @@ Script::CreateObject(rq, &val); 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); + g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data); g_GUI->PopPage(data); // 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); // Pop the empty page and trigger the callback (effectively pops twice). g_GUI->PopPage(data); Index: source/lib/alignment.h =================================================================== --- source/lib/alignment.h +++ source/lib/alignment.h @@ -26,6 +26,9 @@ #include "lib/sysdep/compiler.h" // MSC_VERSION #include "lib/sysdep/arch.h" // ARCH_AMD64 +#include +#include + template inline bool IsAligned(T t, uintptr_t multiple) { @@ -35,7 +38,7 @@ template inline size_t Align(size_t n) { - cassert(multiple != 0 && ((multiple & (multiple-1)) == 0)); // is power of 2 + static_assert(multiple != 0 && ((multiple & (multiple-1)) == 0)); // is power of 2 return (n + multiple-1) & ~(multiple-1); } Index: source/lib/status.h =================================================================== --- source/lib/status.h +++ source/lib/status.h @@ -161,6 +161,8 @@ #ifndef INCLUDED_STATUS #define INCLUDED_STATUS +#include "lib/types.h" + // an integral type allows defining error codes in separate headers, // but is not as type-safe as an enum. use Lint's 'strong type' checking // to catch errors such as Status Func() { return 1; }. Index: source/ps/Filesystem.h =================================================================== --- source/ps/Filesystem.h +++ source/ps/Filesystem.h @@ -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 @@ -21,6 +21,7 @@ #include "lib/file/file.h" #include "lib/file/io/write_buffer.h" #include "lib/file/vfs/vfs_util.h" +#include "lib/pch/pch_boost.h" #include "ps/CStrForward.h" #include "ps/Errors.h" Index: source/scriptinterface/Promises.h =================================================================== --- /dev/null +++ source/scriptinterface/Promises.h @@ -0,0 +1,50 @@ +/* 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 + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_SCRIPTINTERFACE_JOBQUEUE +#define INCLUDED_SCRIPTINTERFACE_JOBQUEUE + +#include "js/Promise.h" + +#include + +class ScriptInterface; + +namespace Script +{ +class JobQueue : public JS::JobQueue +{ +public: + ~JobQueue() final = default; + + void runJobs(JSContext*) final; + +private: + JSObject* getIncumbentGlobal(JSContext* cx) final; + bool enqueuePromiseJob(JSContext* cx, JS::HandleObject, JS::HandleObject job, JS::HandleObject, + JS::HandleObject) final; + bool empty() const final; + + using QueueType = std::queue>; + class SavedJobQueue; + js::UniquePtr saveJobQueue(JSContext*) final; + + QueueType m_Jobs; +}; +} + +#endif // INCLUDED_SCRIPTINTERFACE_JOBQUEUE Index: source/scriptinterface/Promises.cpp =================================================================== --- /dev/null +++ source/scriptinterface/Promises.cpp @@ -0,0 +1,85 @@ +/* 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 + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "lib/debug.h" +#include "scriptinterface/Promises.h" +#include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/ScriptRequest.h" + +namespace Script +{ +JSObject* JobQueue::getIncumbentGlobal(JSContext* cx) +{ + return JS::CurrentGlobalOrNull(cx); +} + +bool JobQueue::enqueuePromiseJob(JSContext* cx, JS::HandleObject, JS::HandleObject job, JS::HandleObject, + JS::HandleObject) +{ + m_Jobs.emplace(JS::PersistentRootedObject{cx, job}, ScriptRequest{cx}.GetScriptInterface()); + return true; +} + +void JobQueue::runJobs(JSContext*) +{ + while (!m_Jobs.empty()) + { + auto& job = m_Jobs.front(); + ScriptRequest rq{std::get<1>(job)}; + JS::RootedValue globV{rq.cx, rq.globalValue()}; + JS::RootedValue rval{rq.cx}; + JS::Call(rq.cx, globV, std::get<0>(job), JS::HandleValueArray::empty(), &rval); + m_Jobs.pop(); + } +} + +bool JobQueue::empty() const +{ + return m_Jobs.empty(); +} + + + +class JobQueue::SavedJobQueue : public JS::JobQueue::SavedJobQueue +{ +public: + SavedJobQueue(Script::JobQueue::QueueType& queue) : + externQueue{queue}, + internQueue{std::exchange(queue, {})} + {} + SavedJobQueue(const SavedJobQueue&) = delete; + SavedJobQueue(SavedJobQueue&&) = delete; + SavedJobQueue& operator=(const SavedJobQueue&) = delete; + SavedJobQueue& operator=(SavedJobQueue&&) = delete; + ~SavedJobQueue() final + { + ENSURE(!externQueue.empty()); + + externQueue = std::move(internQueue); + } +private: + QueueType& externQueue; + QueueType internQueue; +}; + +js::UniquePtr JobQueue::saveJobQueue(JSContext*) +{ + return js::MakeUnique(m_Jobs); +} +} Index: source/scriptinterface/ScriptContext.h =================================================================== --- source/scriptinterface/ScriptContext.h +++ 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,10 @@ 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 +78,11 @@ void RegisterRealm(JS::Realm* realm); void UnRegisterRealm(JS::Realm* realm); + /** + * Runs the promise continuation. + */ + 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 +95,15 @@ 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, Index: source/scriptinterface/ScriptContext.cpp =================================================================== --- source/scriptinterface/ScriptContext.cpp +++ 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 @@ -25,6 +25,7 @@ #include "scriptinterface/ScriptExtraHeaders.h" #include "scriptinterface/ScriptEngine.h" #include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/Promises.h" void GCSliceCallbackHook(JSContext* UNUSED(cx), JS::GCProgress progress, const JS::GCDescription& UNUSED(desc)) { @@ -83,10 +84,9 @@ } 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 @@ JS::ContextOptionsRef(m_cx).setStrictMode(true); ScriptEngine::GetSingleton().RegisterContext(m_cx); + + JS::SetJobQueue(m_cx, m_Jobqueue.get()); } ScriptContext::~ScriptContext() @@ -268,6 +270,11 @@ 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) Index: source/scriptinterface/ScriptInterface.cpp =================================================================== --- source/scriptinterface/ScriptInterface.cpp +++ source/scriptinterface/ScriptInterface.cpp @@ -384,6 +384,7 @@ ScriptInterface::~ScriptInterface() { + m->m_context.RunJobs(); if (Threading::IsMainThread()) { if (g_ScriptStatsTable) Index: source/scriptinterface/tests/test_Promises.h =================================================================== --- /dev/null +++ source/scriptinterface/tests/test_Promises.h @@ -0,0 +1,55 @@ +/* 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 + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "lib/self_test.h" + +#include "ps/CLogger.h" +#include "ps/Filesystem.h" +#include "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/JSON.h" +#include "scriptinterface/Object.h" +#include "scriptinterface/Promises.h" +#include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/ScriptContext.h" +#include "scriptinterface/StructuredClone.h" + +class TestPromises : public CxxTest::TestSuite +{ +public: + void test_simple_promises() + { + ScriptInterface script("Engine", "Test", g_ScriptContext); + ScriptTestSetup(script); + TS_ASSERT(script.LoadGlobalScriptFile(L"promises/simple.js")); + g_ScriptContext->RunJobs(); + + ScriptRequest rq(script); + JS::RootedValue global(rq.cx, rq.globalValue()); + ScriptFunction::CallVoid(rq, global, "endTest"); + } + + void setUp() + { + g_VFS = CreateVfs(); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.scriptinterface" / "", VFS_MOUNT_MUST_EXIST)); + } + + void tearDown() + { + g_VFS.reset(); + } +};