Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/RandomMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/RandomMap.js +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/RandomMap.js @@ -479,14 +479,14 @@ }; }; -RandomMap.prototype.ExportMap = function() +RandomMap.prototype.MakeExportable = function() { if (g_Environment.Water.WaterBody.Height === undefined) g_Environment.Water.WaterBody.Height = SEA_LEVEL - 0.1; this.logger.close(); - Engine.ExportMap({ + return { "entities": this.exportEntityList(), "height": this.exportHeightData(), "seaLevel": SEA_LEVEL, @@ -495,5 +495,10 @@ "tileData": this.exportTerrainTextures(), "Camera": g_Camera, "Environment": g_Environment - }); + }; +}; + +RandomMap.prototype.ExportMap = function() +{ + Engine.ExportMap(this.MakeExportable()); }; Index: ps/trunk/binaries/data/mods/public/maps/random/tests/test_Generator.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/tests/test_Generator.js +++ ps/trunk/binaries/data/mods/public/maps/random/tests/test_Generator.js @@ -0,0 +1,24 @@ +Engine.GetTemplate = path => ( + { + "Identity": { + "GenericName": null, + "Icon": null, + "History": null + } + }); + +Engine.LoadLibrary("rmgen"); + +RandomMapLogger.prototype.printDirectly = function(string) +{ + log(string); + // print(string); +}; + +function* GenerateMap(mapSettings) +{ + TS_ASSERT_DIFFER(mapSettings.Seed, undefined); + // Phew... that assertion took a while. ;) Let's update the progress bar. + yield 50; + return new RandomMap(0, "blackness"); +} Index: ps/trunk/binaries/data/mods/public/maps/random/tests/test_RecoverableError.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/tests/test_RecoverableError.js +++ ps/trunk/binaries/data/mods/public/maps/random/tests/test_RecoverableError.js @@ -0,0 +1,16 @@ +function* GenerateMap() +{ + try + { + yield; + } + catch (error) + { + TS_ASSERT(error instanceof Error); + TS_ASSERT_EQUALS(error.message, "Failed to convert the yielded value to an integer."); + yield 50; + return; + } + + TS_FAIL("The yield statement didn't throw."); +} Index: ps/trunk/binaries/data/tests/test_setup.js =================================================================== --- ps/trunk/binaries/data/tests/test_setup.js +++ ps/trunk/binaries/data/tests/test_setup.js @@ -32,6 +32,12 @@ fail("Expected equal, got " + uneval(x) + " !== " + uneval(y)); }; +global.TS_ASSERT_DIFFER = function(x, y) +{ + if (x === y) + fail("Expected differ, got " + uneval(x) + " === " + uneval(y)); +}; + global.TS_ASSERT_EQUALS_APPROX = function(x, y, maxDifference) { TS_ASSERT_NUMBER(maxDifference); Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp +++ ps/trunk/source/graphics/MapGenerator.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 @@ -47,6 +47,8 @@ namespace { +constexpr const char* GENERATOR_NAME{"GenerateMap"}; + bool MapGenerationInterruptCallback(JSContext* UNUSED(cx)) { // This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents @@ -423,5 +425,49 @@ return nullptr; } - return mapData; + LOGMESSAGE("Run RMS generator"); + bool hasGenerator; + JS::RootedObject globalAsObject{rq.cx, &JS::HandleValue{global}.toObject()}; + if (!JS_HasProperty(rq.cx, globalAsObject, GENERATOR_NAME, &hasGenerator)) + { + LOGERROR("RunMapGenerationScript: failed to search `%s`.", GENERATOR_NAME); + return nullptr; + } + + if (mapData != nullptr) + { + LOGWARNING("The map generation script called `Engine.ExportMap` that's deprecated. The " + "generator based interface should be used."); + if (hasGenerator) + LOGWARNING("The map generation script contains a `%s` but `Engine.ExportMap` was already " + "called. `%s` isn't called, preserving the old behavior.", GENERATOR_NAME, + GENERATOR_NAME); + return mapData; + } + + try + { + JS::RootedValue map{rq.cx, ScriptFunction::RunGenerator(rq, global, GENERATOR_NAME, settingsVal, + [&](const JS::HandleValue value) + { + int tempProgress; + if (!Script::FromJSVal(rq, value, tempProgress)) + throw std::runtime_error{"Failed to convert the yielded value to an " + "integer."}; + progress.store(tempProgress); + })}; + + JS::RootedValue exportedMap{rq.cx}; + const bool exportSuccess{ScriptFunction::Call(rq, map, "MakeExportable", &exportedMap)}; + return Script::WriteStructuredClone(rq, exportSuccess ? exportedMap : map); + } + catch(const std::exception& e) + { + LOGERROR("%s", e.what()); + return nullptr; + } + catch(...) + { + return nullptr; + } } Index: ps/trunk/source/graphics/tests/test_MapGenerator.h =================================================================== --- ps/trunk/source/graphics/tests/test_MapGenerator.h +++ ps/trunk/source/graphics/tests/test_MapGenerator.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 @@ -51,19 +51,31 @@ for (const VfsPath& path : paths) { + TestLogger logger; ScriptInterface scriptInterface("Engine", "MapGenerator", g_ScriptContext); ScriptTestSetup(scriptInterface); - // It's never read in the test so it doesn't matter to what value it's initialized. For - // good practice it's initialized to 1. std::atomic progress{1}; const Script::StructuredClone result{RunMapGenerationScript(progress, scriptInterface, path, "{\"Seed\": 0}", JSPROP_ENUMERATE | JSPROP_PERMANENT)}; - // The test scripts don't call `ExportMap` so `RunMapGenerationScript` allways returns - // `nullptr`. - TS_ASSERT_EQUALS(result, nullptr); + if (path == "maps/random/tests/test_Generator.js" || + path == "maps/random/tests/test_RecoverableError.js") + { + TS_ASSERT_EQUALS(progress.load(), 50); + TS_ASSERT_DIFFERS(result, nullptr); + } + else + { + // The test scripts don't call `ExportMap` so `RunMapGenerationScript` allways + // returns `nullptr`. + TS_ASSERT_EQUALS(result, nullptr); + // Because the test scripts don't call `ExportMap`, `GenerateMap` is searched, which + // doesn't exist. + TS_ASSERT_STR_CONTAINS(logger.GetOutput(), + "Failed to call the generator `GenerateMap`."); + } } } }; Index: ps/trunk/source/scriptinterface/FunctionWrapper.h =================================================================== --- ps/trunk/source/scriptinterface/FunctionWrapper.h +++ ps/trunk/source/scriptinterface/FunctionWrapper.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 @@ -22,8 +22,10 @@ #include "ScriptExceptions.h" #include "ScriptRequest.h" +#include #include #include +#include #include class ScriptInterface; @@ -80,6 +82,17 @@ template struct args_info : public args_info {}; + struct IteratorResultError : std::runtime_error + { + IteratorResultError(const std::string& property) : + IteratorResultError{property.c_str()} + {} + IteratorResultError(const char* property) : + std::runtime_error{fmt::format("Failed to get `{}` from an `IteratorResult`.", property)} + {} + using std::runtime_error::runtime_error; + }; + /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// @@ -347,6 +360,59 @@ } /** + * Call a JS function @a name, property of object @a val, with the argument @a args. Repeatetly + * invokes @a yieldCallback with the yielded value. + * @return the final value of the generator. + */ + template + static JS::Value RunGenerator(const ScriptRequest& rq, JS::HandleValue val, const char* name, + JS::HandleValue arg, Callback yieldCallback) + { + JS::RootedValue generator{rq.cx}; + if (!ScriptFunction::Call(rq, val, name, &generator, arg)) + throw std::runtime_error{fmt::format("Failed to call the generator `{}`.", name)}; + + const auto continueGenerator = [&](const char* property, auto... args) -> JS::Value + { + JS::RootedValue iteratorResult{rq.cx}; + if (!ScriptFunction::Call(rq, generator, property, &iteratorResult, args...)) + throw std::runtime_error{fmt::format("Failed to call `{}`.", name)}; + return iteratorResult; + }; + + JS::PersistentRootedValue error{rq.cx, JS::UndefinedValue()}; + while (true) + { + JS::RootedValue iteratorResult{rq.cx, error.isUndefined() ? continueGenerator("next") : + continueGenerator("throw", std::exchange(error, JS::UndefinedValue()))}; + + try + { + JS::RootedObject iteratorResultObject{rq.cx, &iteratorResult.toObject()}; + + bool done; + if (!Script::FromJSProperty(rq, iteratorResult, "done", done, true)) + throw IteratorResultError{"done"}; + + JS::RootedValue value{rq.cx}; + if (!JS_GetProperty(rq.cx, iteratorResultObject, "value", &value)) + throw IteratorResultError{"value"}; + + if (done) + return value; + + yieldCallback(value); + } + catch (const std::exception& e) + { + JS::RootedValue global{rq.cx, rq.globalValue()}; + if (!ScriptFunction::Call(rq, global, "Error", &error, e.what())) + throw std::runtime_error{"Failed to construct `Error`."}; + } + } + } + + /** * Return a function spec from a C++ function. */ template thisGetter = nullptr>