mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-07-29 13:12:07 +00:00
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:
@@ -479,14 +479,14 @@ RandomMap.prototype.exportTerrainTextures = function()
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
RandomMap.prototype.ExportMap = function()
|
RandomMap.prototype.MakeExportable = function()
|
||||||
{
|
{
|
||||||
if (g_Environment.Water.WaterBody.Height === undefined)
|
if (g_Environment.Water.WaterBody.Height === undefined)
|
||||||
g_Environment.Water.WaterBody.Height = SEA_LEVEL - 0.1;
|
g_Environment.Water.WaterBody.Height = SEA_LEVEL - 0.1;
|
||||||
|
|
||||||
this.logger.close();
|
this.logger.close();
|
||||||
|
|
||||||
Engine.ExportMap({
|
return {
|
||||||
"entities": this.exportEntityList(),
|
"entities": this.exportEntityList(),
|
||||||
"height": this.exportHeightData(),
|
"height": this.exportHeightData(),
|
||||||
"seaLevel": SEA_LEVEL,
|
"seaLevel": SEA_LEVEL,
|
||||||
@@ -495,5 +495,10 @@ RandomMap.prototype.ExportMap = function()
|
|||||||
"tileData": this.exportTerrainTextures(),
|
"tileData": this.exportTerrainTextures(),
|
||||||
"Camera": g_Camera,
|
"Camera": g_Camera,
|
||||||
"Environment": g_Environment
|
"Environment": g_Environment
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
RandomMap.prototype.ExportMap = function()
|
||||||
|
{
|
||||||
|
Engine.ExportMap(this.MakeExportable());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
|
}
|
||||||
@@ -32,6 +32,12 @@ global.TS_ASSERT_EQUALS = function(x, y)
|
|||||||
fail("Expected equal, got " + uneval(x) + " !== " + uneval(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)
|
global.TS_ASSERT_EQUALS_APPROX = function(x, y, maxDifference)
|
||||||
{
|
{
|
||||||
TS_ASSERT_NUMBER(maxDifference);
|
TS_ASSERT_NUMBER(maxDifference);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023 Wildfire Games.
|
/* Copyright (C) 2024 Wildfire Games.
|
||||||
* This file is part of 0 A.D.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
@@ -47,6 +47,8 @@ extern bool IsQuitRequested();
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
constexpr const char* GENERATOR_NAME{"GenerateMap"};
|
||||||
|
|
||||||
bool MapGenerationInterruptCallback(JSContext* UNUSED(cx))
|
bool MapGenerationInterruptCallback(JSContext* UNUSED(cx))
|
||||||
{
|
{
|
||||||
// This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents
|
// 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 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023 Wildfire Games.
|
/* Copyright (C) 2024 Wildfire Games.
|
||||||
* This file is part of 0 A.D.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
@@ -51,19 +51,31 @@ public:
|
|||||||
|
|
||||||
for (const VfsPath& path : paths)
|
for (const VfsPath& path : paths)
|
||||||
{
|
{
|
||||||
|
TestLogger logger;
|
||||||
ScriptInterface scriptInterface("Engine", "MapGenerator", g_ScriptContext);
|
ScriptInterface scriptInterface("Engine", "MapGenerator", g_ScriptContext);
|
||||||
ScriptTestSetup(scriptInterface);
|
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};
|
std::atomic<int> progress{1};
|
||||||
|
|
||||||
const Script::StructuredClone result{RunMapGenerationScript(progress, scriptInterface,
|
const Script::StructuredClone result{RunMapGenerationScript(progress, scriptInterface,
|
||||||
path, "{\"Seed\": 0}", JSPROP_ENUMERATE | JSPROP_PERMANENT)};
|
path, "{\"Seed\": 0}", JSPROP_ENUMERATE | JSPROP_PERMANENT)};
|
||||||
|
|
||||||
// The test scripts don't call `ExportMap` so `RunMapGenerationScript` allways returns
|
if (path == "maps/random/tests/test_Generator.js" ||
|
||||||
// `nullptr`.
|
path == "maps/random/tests/test_RecoverableError.js")
|
||||||
TS_ASSERT_EQUALS(result, nullptr);
|
{
|
||||||
|
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`.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023 Wildfire Games.
|
/* Copyright (C) 2024 Wildfire Games.
|
||||||
* This file is part of 0 A.D.
|
* This file is part of 0 A.D.
|
||||||
*
|
*
|
||||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||||
@@ -22,8 +22,10 @@
|
|||||||
#include "ScriptExceptions.h"
|
#include "ScriptExceptions.h"
|
||||||
#include "ScriptRequest.h"
|
#include "ScriptRequest.h"
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
#include <stdexcept>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
class ScriptInterface;
|
class ScriptInterface;
|
||||||
@@ -80,6 +82,17 @@ private:
|
|||||||
template<typename C, typename R, typename ...Types>
|
template<typename C, typename R, typename ...Types>
|
||||||
struct args_info<R(C::*)(Types ...) const> : public args_info<R(C::*)(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)...);
|
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.
|
* Return a function spec from a C++ function.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user