1
0
forked from 0ad/0ad

Allow to use a generator as MapGenerator

This way it's clear what's the input and what's the output of the
computation.
All map generation scripts should reman working. They are adopted in a
future commit.

`Engine.SetProgress` and `Engine.ExportMap` can be removed in a future
commit.

Comments by: @marder, @sera, @Stan
Differential Revision: https://code.wildfiregames.com/D5220
This was SVN commit r28093.
This commit is contained in:
phosit 2024-05-22 15:52:12 +00:00
parent 585e821274
commit 6ce2fc53ea
7 changed files with 187 additions and 12 deletions

View File

@ -479,14 +479,14 @@ RandomMap.prototype.exportTerrainTextures = function()
};
};
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 @@ RandomMap.prototype.ExportMap = function()
"tileData": this.exportTerrainTextures(),
"Camera": g_Camera,
"Environment": g_Environment
});
};
};
RandomMap.prototype.ExportMap = function()
{
Engine.ExportMap(this.MakeExportable());
};

View File

@ -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");
}

View File

@ -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.");
}

View File

@ -32,6 +32,12 @@ global.TS_ASSERT_EQUALS = function(x, y)
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);

View File

@ -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 @@ extern bool IsQuitRequested();
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 @@ Script::StructuredClone RunMapGenerationScript(std::atomic<int>& progress, Scrip
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;
}
}

View File

@ -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 @@ public:
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<int> 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`.");
}
}
}
};

View File

@ -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 <fmt/format.h>
#include <tuple>
#include <type_traits>
#include <stdexcept>
#include <utility>
class ScriptInterface;
@ -80,6 +82,17 @@ private:
template<typename C, typename R, typename ...Types>
struct args_info<R(C::*)(Types ...) const> : public args_info<R(C::*)(Types ...)> {};
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;
};
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
@ -346,6 +359,59 @@ public:
return Call(rq, val, name, IgnoreResult, std::forward<const Args>(args)...);
}
/**
* 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<typename Callback>
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.
*/