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.RunGuiPage("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.RunGuiPage("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.RunGuiPage("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.RunGuiPage("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.RunGuiPage("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/modmod.js =================================================================== --- binaries/data/mods/mod/gui/modmod/modmod.js +++ binaries/data/mods/mod/gui/modmod/modmod.js @@ -84,7 +84,7 @@ initMods(); initGUIButtons(data); if (g_HasIncompatibleMods) - Engine.PushGuiPage("page_incompatible_mods.xml", {}); + Engine.RunGuiPage("page_incompatible_mods.xml"); } function initMods() Index: binaries/data/mods/mod/gui/modmod/modmod.xml =================================================================== --- binaries/data/mods/mod/gui/modmod/modmod.xml +++ binaries/data/mods/mod/gui/modmod/modmod.xml @@ -200,7 +200,7 @@ Help - Engine.PushGuiPage("page_modhelp.xml"); + Engine.RunGuiPage("page_modhelp.xml"); 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.RunGuiPage("page_modio.xml"); + initMods(); } Index: binaries/data/mods/public/autostart/entrypoint.js =================================================================== --- binaries/data/mods/public/autostart/entrypoint.js +++ binaries/data/mods/public/autostart/entrypoint.js @@ -7,7 +7,7 @@ Engine.HasXmppClient = () => false; Engine.SetRankedGame = () => {}; Engine.TextureExists = () => false; -Engine.PushGuiPage = () => {}; +Engine.RunGuiPage = () => {}; Engine.SwitchGuiPage = () => {}; var translateObjectKeys = () => {} 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.RunGuiPage('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/campaigns/setup/CampaignSetupPage.js =================================================================== --- binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.js +++ binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.js @@ -12,13 +12,14 @@ this.templates = CampaignTemplate.getAvailableTemplates(); Engine.GetGUIObjectByName("mainMenuButton").onPress = () => Engine.SwitchGuiPage("page_pregame.xml"); - Engine.GetGUIObjectByName("startCampButton").onPress = () => Engine.PushGuiPage("campaigns/new_modal/page.xml", this.selectedTemplate); + Engine.GetGUIObjectByName("startCampButton").onPress = () => Engine.RunGuiPage( + "campaigns/new_modal/page.xml", this.selectedTemplate); this.campaignSelection = Engine.GetGUIObjectByName("campaignSelection"); this.campaignSelection.onMouseLeftDoubleClickItem = () => { if (this.selectedIndex === -1) return; - Engine.PushGuiPage("campaigns/new_modal/page.xml", this.selectedTemplate); + Engine.RunGuiPage("campaigns/new_modal/page.xml", this.selectedTemplate); }; this.campaignSelection.onSelectionChange = () => { this.selectedIndex = this.campaignSelection.selected; Index: binaries/data/mods/public/gui/common/functions_global_object.js =================================================================== --- binaries/data/mods/public/gui/common/functions_global_object.js +++ binaries/data/mods/public/gui/common/functions_global_object.js @@ -46,7 +46,7 @@ Engine.SwitchGuiPage("page_pregame.xml"); if (msg) - Engine.PushGuiPage("page_msgbox.xml", { + Engine.RunGuiPage("page_msgbox.xml", { "width": 500, "height": 200, "message": '[font="sans-bold-18"]' + msg + '[/font]', 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.RunGuiPage(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.RunGuiPage(data.nextPage, { "civ": data.civ }); + + this.civInfo = data; } } Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LobbyButton.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LobbyButton.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LobbyButton.js @@ -11,7 +11,7 @@ onPress() { if (Engine.HasXmppClient()) - Engine.PushGuiPage("page_lobby.xml", { "dialog": true }); + Engine.RunGuiPage("page_lobby.xml", { "dialog": true }); } } Index: binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/HostButton.js =================================================================== --- binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/HostButton.js +++ binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/HostButton.js @@ -23,7 +23,7 @@ onPress() { - Engine.PushGuiPage("page_gamesetup_mp.xml", { + Engine.RunGuiPage("page_gamesetup_mp.xml", { "multiplayerGameType": "host", "name": g_Nickname, "rating": Engine.LobbyGetPlayerRating(g_Nickname) Index: binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js =================================================================== --- binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js +++ binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js @@ -70,7 +70,7 @@ return; let stanza = game.stanza; - Engine.PushGuiPage("page_gamesetup_mp.xml", { + Engine.RunGuiPage("page_gamesetup_mp.xml", { "multiplayerGameType": "join", "name": g_Nickname, "rating": this.getRejoinRating(stanza), 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.RunGuiPage("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 @@ -6,9 +6,7 @@ { "caption": translate("Manual"), "tooltip": translate("Open the 0 A.D. Game Manual."), - "onPress": () => { - Engine.PushGuiPage("page_manual.xml"); - } + "onPress": Engine.RunGuiPage.bind(Engine, "page_manual.xml") }, { "caption": translate("Tutorial"), @@ -36,39 +34,33 @@ "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.RunGuiPage("page_structree.xml"); + while (data.nextPage) + data = await Engine.RunGuiPage(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.RunGuiPage("page_civinfo.xml"); + while (data.nextPage) + data = await Engine.RunGuiPage(data.nextPage, { "civ": data.civ }); } }, { "caption": translate("Catafalque Overview"), "tooltip": translate("Compare the bonuses of catafalques featured in 0 A.D."), - "onPress": () => { - Engine.PushGuiPage("page_catafalque.xml"); - } + "onPress": Engine.RunGuiPage.bind(Engine, "page_catafalque.xml") }, { "caption": translate("Map Overview"), "tooltip": translate("View the different maps featured in 0 A.D."), - "onPress": () => { - Engine.PushGuiPage("page_mapbrowser.xml"); - }, + "onPress": Engine.RunGuiPage.bind(Engine, "page_mapbrowser.xml") } ] }, @@ -102,31 +94,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.RunGuiPage("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"), @@ -184,20 +178,18 @@ // Translation: Join a game by specifying the host's IP address. "caption": translate("Join Game"), "tooltip": translate("Joining an existing multiplayer game."), - "onPress": () => { - Engine.PushGuiPage("page_gamesetup_mp.xml", { + "onPress": Engine.RunGuiPage.bind(Engine, "page_gamesetup_mp.xml", + { "multiplayerGameType": "join" - }); - } + }) }, { "caption": translate("Host Game"), "tooltip": translate("Host a multiplayer game."), - "onPress": () => { - Engine.PushGuiPage("page_gamesetup_mp.xml", { + "onPress": Engine.RunGuiPage.bind(Engine, "page_gamesetup_mp.xml", + { "multiplayerGameType": "host" - }); - } + }) }, { "caption": translate("Game Lobby"), @@ -208,7 +200,7 @@ "hotkey": "lobby", "onPress": () => { if (Engine.StartXmppClient) - Engine.PushGuiPage("page_prelobby_entrance.xml"); + Engine.RunGuiPage("page_prelobby_entrance.xml"); } }, { @@ -233,26 +225,21 @@ { "caption": translate("Options"), "tooltip": translate("Adjust game settings."), - "onPress": () => { - Engine.PushGuiPage( - "page_options.xml", - {}, - fireConfigChangeHandlers); + "onPress": async function() + { + const changes = await Engine.RunGuiPage("page_options.xml"); + fireConfigChangeHandlers(changes); } }, { "caption": translate("Hotkeys"), "tooltip": translate("Adjust hotkeys."), - "onPress": () => { - Engine.PushGuiPage("hotkeys/page_hotkeys.xml"); - } + "onPress": Engine.RunGuiPage.bind(Engine, "hotkeys/page_hotkeys.xml") }, { "caption": translate("Language"), "tooltip": translate("Choose the language of the game."), - "onPress": () => { - Engine.PushGuiPage("page_locale.xml"); - } + "onPress": Engine.RunGuiPage.bind(Engine, "page_locale.xml") }, { "caption": translate("Mod Selection"), @@ -264,9 +251,7 @@ { "caption": translate("Welcome Screen"), "tooltip": translate("Show the Welcome Screen again. Useful if you hid it by mistake."), - "onPress": () => { - Engine.PushGuiPage("page_splashscreen.xml"); - } + "onPress": Engine.RunGuiPage.bind(Engine, "page_splashscreen.xml") } ] }, @@ -291,9 +276,7 @@ { "caption": translate("Credits"), "tooltip": translate("Show the 0 A.D. credits."), - "onPress": () => { - Engine.PushGuiPage("page_credits.xml"); - } + "onPress": Engine.RunGuiPage.bind(Engine, "page_credits.xml") }, { "caption": translate("Exit"), Index: binaries/data/mods/public/gui/pregame/SplashscreenHandler.js =================================================================== --- binaries/data/mods/public/gui/pregame/SplashscreenHandler.js +++ binaries/data/mods/public/gui/pregame/SplashscreenHandler.js @@ -32,6 +32,6 @@ if (Engine.ConfigDB_GetValue("user", "gui.splashscreen.enable") === "true" || Engine.ConfigDB_GetValue("user", "gui.splashscreen.version") < Engine.GetFileMTime("gui/splashscreen/splashscreen.txt")) - Engine.PushGuiPage("page_splashscreen.xml", {}); + Engine.RunGuiPage("page_splashscreen.xml"); } } Index: binaries/data/mods/public/gui/prelobby/entrance/entrance.js =================================================================== --- binaries/data/mods/public/gui/prelobby/entrance/entrance.js +++ binaries/data/mods/public/gui/prelobby/entrance/entrance.js @@ -6,12 +6,12 @@ function loginButton() { - Engine.PushGuiPage("page_prelobby_login.xml"); + Engine.RunGuiPage("page_prelobby_login.xml"); } function registerButton() { - Engine.PushGuiPage("page_prelobby_register.xml"); + Engine.RunGuiPage("page_prelobby_register.xml"); } function cancelButton() Index: binaries/data/mods/public/gui/prelobby/register/register.js =================================================================== --- binaries/data/mods/public/gui/prelobby/register/register.js +++ binaries/data/mods/public/gui/prelobby/register/register.js @@ -41,5 +41,5 @@ Engine.StopXmppClient(); Engine.PopGuiPage(); - Engine.PushGuiPage("page_prelobby_login.xml"); + Engine.RunGuiPage("page_prelobby_login.xml"); } Index: binaries/data/mods/public/gui/reference/structree/Boxes/EntityBox.js =================================================================== --- binaries/data/mods/public/gui/reference/structree/Boxes/EntityBox.js +++ binaries/data/mods/public/gui/reference/structree/Boxes/EntityBox.js @@ -10,12 +10,11 @@ static setViewerOnPress(guiObject, templateName, civCode) { - let viewerFunc = () => { - Engine.PushGuiPage("page_viewer.xml", { + const viewerFunc = Engine.RunGuiPage.bind(Engine, "page_viewer.xml", + { "templateName": templateName, "civ": civCode }); - }; guiObject.onPress = viewerFunc; guiObject.onPressRight = viewerFunc; } 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.RunGuiPage("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.RunGuiPage("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.RunGuiPage("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(); } }; @@ -149,7 +147,7 @@ if (!Engine.HasXmppClient()) return; closeOpenDialogs(); - Engine.PushGuiPage("page_lobby.xml", { "dialog": true }); + Engine.RunGuiPage("page_lobby.xml", { "dialog": true }); } }; @@ -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.RunGuiPage("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.RunGuiPage("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.RunGuiPage("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/campaigns/CampaignSession.js =================================================================== --- binaries/data/mods/public/gui/session/campaigns/CampaignSession.js +++ binaries/data/mods/public/gui/session/campaigns/CampaignSession.js @@ -23,7 +23,7 @@ this.endGameData.won = won; // Run the endgame script. - Engine.PushGuiPage(this.getEndGame(), this.endGameData); + Engine.RunGuiPage(this.getEndGame(), this.endGameData); Engine.PopGuiPage(); } 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.RunGuiPage( "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.RunGuiPage(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.RunGuiPage(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 @@ - + @@ -179,7 +179,7 @@ > if (Engine.HasXmppClient()) - Engine.PushGuiPage("page_lobby.xml", { "dialog": true }); + Engine.RunGuiPage("page_lobby.xml", { "dialog": true }); 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 this page is closed. */ - void PushPage(const CStrW& pageName, Script::StructuredClone initData, JS::HandleValue callbackFunc); + JS::Value RunPage(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); + RunPage(pageName, initDataClone); } -void CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone initData, JS::HandleValue callbackFunction) +JS::Value CGUIManager::RunPage(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(); + + pageStack.back().gui->GetScriptInterface()->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 RunGuiPage(const ScriptRequest& rq, const std::wstring& name, JS::HandleValue initData) { - g_GUI->PushPage(name, Script::WriteStructuredClone(rq, initData), callbackFunction); + return g_GUI->RunPage(name, Script::WriteStructuredClone(rq, initData)); } void SwitchGuiPage(const ScriptInterface& scriptInterface, const std::wstring& name, JS::HandleValue initData) @@ -76,7 +77,7 @@ void RegisterScriptFunctions(const ScriptRequest& rq) { - ScriptFunction::Register<&PushGuiPage>(rq, "PushGuiPage"); + ScriptFunction::Register<&RunGuiPage>(rq, "RunGuiPage"); ScriptFunction::Register<&SwitchGuiPage>(rq, "SwitchGuiPage"); ScriptFunction::Register<&PopGuiPage>(rq, "PopGuiPage"); ScriptFunction::Register<&SetCursor>(rq, "SetCursor"); 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->RunPage(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->RunPage(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->RunPage(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->RunPage(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->RunPage(L"regainFocus/page_pushWithPopOnInit.xml", data); // Pop the empty page and trigger the callback (effectively pops twice). g_GUI->PopPage(data); Index: source/scriptinterface/Promises.h =================================================================== --- /dev/null +++ source/scriptinterface/Promises.h @@ -0,0 +1,76 @@ +/* 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 "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/ScriptContext.h" +#include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/ScriptRequest.h" +#include "scriptinterface/StructuredClone.h" + +#include "ps/Filesystem.h" +#include "ps/TaskManager.h" + +#include + +class ScriptInterface; + +namespace Script +{ +/** + * Spidermonkey has to handle debugger interruptions to the job queue, which is a rather complex topic (see header). + * We aren't going to care about that, only queuing jobs & running them. + */ +class JobQueue : public JS::JobQueue { +public: + ~JobQueue() final = default; + + JSObject* getIncumbentGlobal(JSContext* cx) final + { + return JS::CurrentGlobalOrNull(cx); + } + + bool enqueuePromiseJob(JSContext* cx, JS::HandleObject UNUSED(promise), JS::HandleObject job, + JS::HandleObject UNUSED(allocationSite), JS::HandleObject UNUSED(incumbentGlobal)) final + { + ScriptRequest rq(cx); + const_cast(rq.GetScriptInterface()).AddJob(job); + return true; + } + + void runJobs(JSContext*) final + {}; + + bool empty() const final + { + return true; + }; + +private: + // This is used by the debugger-interruptible queue. + js::UniquePtr saveJobQueue(JSContext*) final + { + return nullptr; + }; +}; +} + +#endif // INCLUDED_SCRIPTINTERFACE_JOBQUEUE 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. * @@ -86,14 +90,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() Index: source/scriptinterface/ScriptInterface.h =================================================================== --- source/scriptinterface/ScriptInterface.h +++ source/scriptinterface/ScriptInterface.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 @@ -218,6 +218,9 @@ bool Eval(const char* code, JS::MutableHandleValue out) const; template bool Eval(const char* code, T& out) const; + void AddJob(JS::HandleObject job); + void RunJobs(); + /** * Calls the random number generator assigned to this ScriptInterface instance and returns the generated number. */ Index: source/scriptinterface/ScriptInterface.cpp =================================================================== --- source/scriptinterface/ScriptInterface.cpp +++ source/scriptinterface/ScriptInterface.cpp @@ -32,6 +32,7 @@ #include "scriptinterface/StructuredClone.h" #include +#include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION @@ -60,13 +61,15 @@ ScriptContext& m_context; friend ScriptRequest; - private: - JSContext* m_cx; - JS::PersistentRootedObject m_glob; // global scope object +private: + JSContext* m_cx; + JS::PersistentRootedObject m_glob; // global scope object - public: - boost::rand48* m_rng; - JS::PersistentRootedObject m_nativeScope; // native function scope object +public: + std::queue m_Jobs; + + boost::rand48* m_rng; + JS::PersistentRootedObject m_nativeScope; // native function scope object }; /** @@ -687,6 +690,25 @@ return LoadGlobalScript(path, code); } +void ScriptInterface::AddJob(JS::HandleObject job) +{ + ScriptRequest rq{this}; + m->m_Jobs.emplace(rq.cx, job); +} + +void ScriptInterface::RunJobs() +{ + ScriptRequest rq{this}; + JS::HandleValueArray args{JS::HandleValueArray::empty()}; + JS::RootedValue rval{rq.cx}; + JS::RootedValue globV{rq.cx, rq.globalValue()}; + while (!m->m_Jobs.empty()) + { + JS::Call(rq.cx, globV, m->m_Jobs.front(), args, &rval); + m->m_Jobs.pop(); + } +} + bool ScriptInterface::Eval(const char* code) const { ScriptRequest rq(this); Index: source/scriptinterface/tests/test_Promises.h =================================================================== --- /dev/null +++ source/scriptinterface/tests/test_Promises.h @@ -0,0 +1,54 @@ +/* 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 "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/JSON.h" +#include "scriptinterface/Object.h" +#include "scriptinterface/Promises.h" +#include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/StructuredClone.h" + +#include "ps/CLogger.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")); + script.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(); + } +};