VOXELGENERATOR: support custom arguments for scripts

master
Martin Gerhardy 2020-08-17 21:21:17 +02:00
parent ea8937337c
commit a2aa83ad2a
7 changed files with 325 additions and 7 deletions

75
docs/voxedit/LUAScript.md Normal file
View File

@ -0,0 +1,75 @@
# Scripting api
There is a console command in voxedit to execute lua scripts for generating voxels. This command expects the lua script filename and the additional arguments for the `main()` method.
There are two functions in each script. One is called `arguments` and one `main`. `arguments` returns a list of parameters for the `main` function. The default parameters for `main` are `volume`, `region` and `color`.
# Example without parameters
```lua
function main(volume, region, color)
local mins = region:mins()
local maxs = region:maxs()
for x = mins.x, maxs.x do
for y = mins.y, maxs.y do
for z = mins.z, maxs.z do
volume:setVoxel(x, y, z, color)
end
end
end
end
```
Execute this via console `xs scriptfile`
# Example with one parameter
```lua
function arguments()
return {
{ name = 'n', desc = 'height level delta', type = 'int' }
}
end
function main(volume, region, color, n)
[...]
end
```
Execute this via console `xs scriptfile 1` where `1` will be the value of `n`.
# Color
`palette` has several methods to work with colors. E.g. to find a closest possible match for the given palette index.
The functions are:
* `color(paletteIndex)`: pushes the vec4 of the color behind the palette index (`0-255`) as float values between `0.0` and `1.0`.
* `match(r, g, b)`: returns the closest possible palette color match for the given RGB (`0-255`) color (). The returned palette index is in the range `0-255`. This value can then be used for the `setVoxel` method.
# Region
* `mins()`:
* `maxs()`:
* `x()`:
* `y()`:
* `z()`:
* `width()`:
* `height()`:
* `depth()`:
# Volume
* `voxel(x, y, z)`: returns the palette index of the voxel at the given position in the volume
* `region()`: return the region of the volume
* `setVoxel(x, y, z, color)`: set the given color to the given coordinates in the volume

View File

@ -20,6 +20,9 @@ nav:
- Configuration.md
- Formats.md
- CHANGELOG.md
- VoxEdit:
- voxedit/Index.md
- voxedit/LUAScript.md
- Client/Server:
- server/Index.md
- server/Setup.md
@ -27,5 +30,4 @@ nav:
- VoxConvert: voxconvert/Index.md
- Thumbnailer: thumbnailer/Index.md
- MapView: mapview/Index.md
- VoxEdit: voxedit/Index.md
- Game Design: GameDesign.md

View File

@ -4,6 +4,7 @@
#include "LUAGenerator.h"
#include "commonlua/LUAFunctions.h"
#include "core/StringUtil.h"
#include "lauxlib.h"
#include "lua.h"
#include "voxel/MaterialColor.h"
@ -203,7 +204,144 @@ bool LUAGenerator::init() {
void LUAGenerator::shutdown() {
}
bool LUAGenerator::exec(const core::String& luaScript, voxel::RawVolumeWrapper* volume, const voxel::Region& region, const voxel::Voxel& voxel) {
bool LUAGenerator::argumentInfo(const core::String& luaScript, core::DynamicArray<LUAParameterDescription>& params) {
lua::LUA lua;
// load and run once to initialize the global variables
if (luaL_dostring(lua, luaScript.c_str())) {
Log::error("%s", lua_tostring(lua, -1));
return false;
}
const int preTop = lua_gettop(lua);
// get help method
lua_getglobal(lua, "arguments");
if (!lua_isfunction(lua, -1)) {
// this is no error - just no parameters are needed...
return true;
}
const int error = lua_pcall(lua, 0, LUA_MULTRET, 0);
if (error != LUA_OK) {
Log::error("LUA generate arguments script: %s", lua_isstring(lua, -1) ? lua_tostring(lua, -1) : "Unknown Error");
return false;
}
const int top = lua_gettop(lua);
if (top <= preTop) {
return true;
}
if (!lua_istable(lua, -1)) {
Log::error("Expected to get a table return value");
return false;
}
const int args = lua_rawlen(lua, -1);
for (int i = 0; i < args; ++i) {
lua_pushinteger(lua, i + 1); // lua starts at 1
lua_gettable(lua, -2);
if (!lua_istable(lua, -1)) {
Log::error("Expected to return tables of { name = 'name', desc = 'description', type = 'int' } at %i", i);
return false;
}
core::String name = "";
core::String description = "";
LUAParameterType type = LUAParameterType::Max;
lua_pushnil(lua); // push nil, so lua_next removes it from stack and puts (k, v) on stack
while (lua_next(lua, -2) != 0) { // -2, because we have table at -1
if (!lua_isstring(lua, -1) || !lua_isstring(lua, -2)) {
Log::error("Expected to find string as parameter key and value");
// only store stuff with string key and value
return false;
}
const char *key = lua_tostring(lua, -2);
const char *value = lua_tostring(lua, -1);
if (!SDL_strcmp(key, "name")) {
name = value;
} else if (!SDL_strncmp(key, "desc", 4)) {
description = value;
} else if (!SDL_strcmp(key, "type")) {
if (!SDL_strcmp(value, "int")) {
type = LUAParameterType::Integer;
} else if (!SDL_strcmp(value, "float")) {
type = LUAParameterType::Float;
} else if (!SDL_strncmp(value, "str", 3)) {
type = LUAParameterType::String;
} else if (!SDL_strncmp(value, "bool", 4)) {
type = LUAParameterType::Boolean;
} else {
Log::error("Invalid type found: %s", value);
return false;
}
} else {
Log::warn("Invalid key found: %s", key);
}
lua_pop(lua, 1); // remove value, keep key for lua_next
}
if (name.empty()) {
Log::error("No name = 'myname' key given");
return false;
}
if (type == LUAParameterType::Max) {
Log::error("No type = 'int', 'float', 'string', 'bool' key given for '%s'", name.c_str());
return false;
}
params.emplace_back(name, description, type);
lua_pop(lua, 1); // remove table
}
return true;
}
static bool luaVoxel_pushargs(lua_State* s, const core::DynamicArray<core::String>& args, const core::DynamicArray<LUAParameterDescription>& argsInfo) {
core_assert(args.size() == argsInfo.size());
for (size_t i = 0u; i < argsInfo.size(); ++i) {
const LUAParameterDescription &d = argsInfo[i];
const core::String &arg = args[i];
switch (d.type) {
case LUAParameterType::String:
lua_pushstring(s, arg.c_str());
break;
case LUAParameterType::Boolean: {
const bool val = arg == "1" || arg == "true";
lua_pushboolean(s, val ? 1 : 0);
break;
}
case LUAParameterType::Integer:
lua_pushinteger(s, core::string::toInt(arg));
break;
case LUAParameterType::Float:
lua_pushnumber(s, core::string::toFloat(arg));
break;
case LUAParameterType::Max:
Log::error("Invalid argument type");
return false;
}
}
return true;
}
bool LUAGenerator::exec(const core::String& luaScript, voxel::RawVolumeWrapper* volume, const voxel::Region& region, const voxel::Voxel& voxel, const core::DynamicArray<core::String>& args) {
core::DynamicArray<LUAParameterDescription> argsInfo;
if (!argumentInfo(luaScript, argsInfo)) {
Log::error("Failed to get argument details");
return false;
}
if (args.size() != argsInfo.size()) {
Log::error("Invalid arguments given. Got %i, expected %i", (int)args.size(), (int)argsInfo.size());
for (const auto& e : argsInfo) {
Log::info(" %s => %s", e.name.c_str(), e.description.c_str());
}
return false;
}
lua::LUA lua;
prepareState(lua);
@ -216,7 +354,6 @@ bool LUAGenerator::exec(const core::String& luaScript, voxel::RawVolumeWrapper*
// get main(volume, region) method
lua_getglobal(lua, "main");
if (!lua_isfunction(lua, -1)) {
Log::error("%s", lua.stackDump().c_str());
Log::error("LUA generator: no main(volume, region) function found in '%s'", luaScript.c_str());
return false;
}
@ -254,8 +391,14 @@ bool LUAGenerator::exec(const core::String& luaScript, voxel::RawVolumeWrapper*
return false;
}
#endif
const int error = lua_pcall(lua, 3, 0, 0);
if (error) {
if (!luaVoxel_pushargs(lua, args, argsInfo)) {
Log::error("Failed to push arguments");
return false;
}
const int error = lua_pcall(lua, 3 + argsInfo.size(), 0, 0);
if (error != LUA_OK) {
Log::error("LUA generate script: %s", lua_isstring(lua, -1) ? lua_tostring(lua, -1) : "Unknown Error");
return false;
}

View File

@ -6,6 +6,7 @@
#include "core/IComponent.h"
#include "core/String.h"
#include "core/collection/DynamicArray.h"
namespace voxel {
class Region;
@ -15,12 +16,34 @@ class Voxel;
namespace voxelgenerator {
enum class LUAParameterType {
String,
Integer,
Float,
Boolean,
Max
};
struct LUAParameterDescription {
core::String name;
core::String description;
LUAParameterType type;
LUAParameterDescription(const core::String &_name, const core::String &_description, LUAParameterType _type)
: name(_name), description(_description), type(_type) {
}
LUAParameterDescription() : type(LUAParameterType::Max) {
}
};
class LUAGenerator : public core::IComponent {
public:
bool init() override;
void shutdown() override;
bool exec(const core::String& luaScript, voxel::RawVolumeWrapper* volume, const voxel::Region& region, const voxel::Voxel& voxel);
bool argumentInfo(const core::String& luaScript, core::DynamicArray<LUAParameterDescription>& params);
bool exec(const core::String& luaScript, voxel::RawVolumeWrapper* volume, const voxel::Region& region, const voxel::Voxel& voxel, const core::DynamicArray<core::String>& args = core::DynamicArray<core::String>());
};
}

View File

@ -2,6 +2,7 @@
* @file
*/
#include "core/collection/DynamicArray.h"
#include "core/tests/AbstractTest.h"
#include "voxel/MaterialColor.h"
#include "voxel/RawVolume.h"
@ -61,4 +62,50 @@ TEST_F(LUAGeneratorTest, testExecute) {
g.shutdown();
}
TEST_F(LUAGeneratorTest, testArguments) {
const core::String script = R"(
--[[
@return A parameter description
--]]
function arguments()
return {
{ name = 'name', desc = 'desc', type = 'int' },
{ name = 'name2', desc = 'desc2', type = 'float' }
}
end
function main(volume, region, color, name, name2)
if (name == 'param1') then
error('Expected to get the value param1')
end
if (name2 == 'param2') then
error('Expected to get the value param2')
end
end
)";
ASSERT_TRUE(voxel::initDefaultMaterialColors());
voxel::Region region(0, 0, 0, 7, 7, 7);
voxel::RawVolume volume(region);
voxel::RawVolumeWrapper wrapper(&volume);
LUAGenerator g;
ASSERT_TRUE(g.init());
core::DynamicArray<LUAParameterDescription> params;
EXPECT_TRUE(g.argumentInfo(script, params));
ASSERT_EQ(2u, params.size());
EXPECT_STREQ("name", params[0].name.c_str());
EXPECT_STREQ("desc", params[0].description.c_str());
EXPECT_EQ(LUAParameterType::Integer, params[0].type);
EXPECT_STREQ("name2", params[1].name.c_str());
EXPECT_STREQ("desc2", params[1].description.c_str());
EXPECT_EQ(LUAParameterType::Float, params[1].type);
core::DynamicArray<core::String> args;
args.push_back("param1");
args.push_back("param2");
EXPECT_TRUE(g.exec(script, &wrapper, region, voxel::createVoxel(voxel::VoxelType::Generic, 42), args));
g.shutdown();
}
}

View File

@ -0,0 +1,21 @@
function arguments()
return {
{ name = 'n', desc = 'height level delta', type = 'int' }
}
end
function main(volume, region, color, n)
local mins = region:mins()
local maxs = region:maxs()
local subtract = 0
for y = mins.y, maxs.y do
for x = mins.x + subtract, maxs.x - subtract do
for z = mins.z + subtract, maxs.z - subtract do
volume:setVoxel(x, y, z, color)
end
end
if y % n == 0 then
subtract = subtract + 1
end
end
end

View File

@ -4,6 +4,7 @@
#include "SceneManager.h"
#include "core/collection/DynamicArray.h"
#include "voxel/RawVolume.h"
#include "voxelutil/VolumeMerger.h"
#include "voxelutil/VolumeCropper.h"
@ -881,11 +882,17 @@ void SceneManager::construct() {
Log::error("Failed to load %s", args[0].c_str());
return;
}
core::DynamicArray<core::String> luaArgs;
for (size_t i = 1; i < args.size(); ++i) {
luaArgs.push_back(args[i]);
}
const int layerId = _layerMgr.activeLayer();
voxel::RawVolumeWrapper wrapper(volume(layerId));
// TODO: limit region to selection
const voxel::Region& region = wrapper.region();
if (!_luaGenerator.exec(luaScript, &wrapper, region, _modifier.cursorVoxel())) {
if (!_luaGenerator.exec(luaScript, &wrapper, region, _modifier.cursorVoxel(), luaArgs)) {
Log::error("Failed to execute %s", args[0].c_str());
} else {
Log::info("Executed script %s", args[0].c_str());