Load YAML files via Lua (feature 7590)

fix-osga-rotate-wildly
Andrei Kortunov 10 months ago
parent e340b06411
commit 715efe892f

@ -194,6 +194,7 @@
Feature #7546: Start the game on Fredas Feature #7546: Start the game on Fredas
Feature #7554: Controller binding for tab for menu navigation Feature #7554: Controller binding for tab for menu navigation
Feature #7568: Uninterruptable scripted music Feature #7568: Uninterruptable scripted music
Feature #7590: [Lua] Ability to deserialize YAML data from scripts
Feature #7606: Launcher: allow Shift-select in Archives tab Feature #7606: Launcher: allow Shift-select in Archives tab
Feature #7608: Make the missing dependencies warning when loading a savegame more helpful Feature #7608: Make the missing dependencies warning when loading a savegame more helpful
Feature #7618: Show the player character's health in the save details Feature #7618: Show the player character's health in the save details

@ -20,6 +20,7 @@ apps/openmw_test_suite/lua/test_storage.cpp
apps/openmw_test_suite/lua/test_ui_content.cpp apps/openmw_test_suite/lua/test_ui_content.cpp
apps/openmw_test_suite/lua/test_utilpackage.cpp apps/openmw_test_suite/lua/test_utilpackage.cpp
apps/openmw_test_suite/lua/test_inputactions.cpp apps/openmw_test_suite/lua/test_inputactions.cpp
apps/openmw_test_suite/lua/test_yaml.cpp
apps/openmw_test_suite/misc/test_endianness.cpp apps/openmw_test_suite/misc/test_endianness.cpp
apps/openmw_test_suite/misc/test_resourcehelpers.cpp apps/openmw_test_suite/misc/test_resourcehelpers.cpp
apps/openmw_test_suite/misc/test_stringops.cpp apps/openmw_test_suite/misc/test_stringops.cpp

@ -80,7 +80,7 @@ message(STATUS "Configuring OpenMW...")
set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MAJOR 0)
set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_MINOR 49)
set(OPENMW_VERSION_RELEASE 0) set(OPENMW_VERSION_RELEASE 0)
set(OPENMW_LUA_API_REVISION 56) set(OPENMW_LUA_API_REVISION 57)
set(OPENMW_POSTPROCESSING_API_REVISION 1) set(OPENMW_POSTPROCESSING_API_REVISION 1)
set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_COMMITHASH "")

@ -64,7 +64,7 @@ add_openmw_dir (mwlua
context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings
mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings
postprocessingbindings stats debugbindings corebindings worldbindings worker magicbindings factionbindings postprocessingbindings stats debugbindings corebindings worldbindings worker magicbindings factionbindings
classbindings itemdata inputprocessor animationbindings birthsignbindings racebindings classbindings itemdata inputprocessor animationbindings birthsignbindings racebindings markupbindings
types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc
types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus
types/potion types/ingredient types/misc types/repair types/armor types/light types/static types/potion types/ingredient types/misc types/repair types/armor types/light types/static

@ -14,6 +14,7 @@
#include "debugbindings.hpp" #include "debugbindings.hpp"
#include "inputbindings.hpp" #include "inputbindings.hpp"
#include "localscripts.hpp" #include "localscripts.hpp"
#include "markupbindings.hpp"
#include "menuscripts.hpp" #include "menuscripts.hpp"
#include "nearbybindings.hpp" #include "nearbybindings.hpp"
#include "objectbindings.hpp" #include "objectbindings.hpp"
@ -35,6 +36,7 @@ namespace MWLua
{ "openmw.async", { "openmw.async",
LuaUtil::getAsyncPackageInitializer( LuaUtil::getAsyncPackageInitializer(
lua, [tm] { return tm->getSimulationTime(); }, [tm] { return tm->getGameTime(); }) }, lua, [tm] { return tm->getSimulationTime(); }, [tm] { return tm->getGameTime(); }) },
{ "openmw.markup", initMarkupPackage(context) },
{ "openmw.util", LuaUtil::initUtilPackage(lua) }, { "openmw.util", LuaUtil::initUtilPackage(lua) },
{ "openmw.vfs", initVFSPackage(context) }, { "openmw.vfs", initVFSPackage(context) },
}; };

@ -0,0 +1,32 @@
#include "markupbindings.hpp"
#include <components/lua/luastate.hpp>
#include <components/lua/yamlloader.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/vfs/manager.hpp>
#include <components/vfs/pathutil.hpp>
#include "../mwbase/environment.hpp"
#include "context.hpp"
namespace MWLua
{
sol::table initMarkupPackage(const Context& context)
{
sol::table api(context.mLua->sol(), sol::create);
auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
api["loadYaml"] = [lua = context.mLua, vfs](std::string_view fileName) {
auto normalizedName = VFS::Path::normalizeFilename(fileName);
auto file = vfs->getNormalized(normalizedName);
return LuaUtil::YamlLoader::load(*file, lua->sol());
};
api["decodeYaml"] = [lua = context.mLua](std::string_view inputData) {
return LuaUtil::YamlLoader::load(std::string(inputData), lua->sol());
};
return LuaUtil::makeReadOnly(api);
}
}

@ -0,0 +1,13 @@
#ifndef MWLUA_MARKUPBINDINGS_H
#define MWLUA_MARKUPBINDINGS_H
#include <sol/forward.hpp>
#include "context.hpp"
namespace MWLua
{
sol::table initMarkupPackage(const Context&);
}
#endif // MWLUA_MARKUPBINDINGS_H

@ -29,6 +29,7 @@ file(GLOB UNITTEST_SRC_FILES
lua/test_storage.cpp lua/test_storage.cpp
lua/test_async.cpp lua/test_async.cpp
lua/test_inputactions.cpp lua/test_inputactions.cpp
lua/test_yaml.cpp
lua/test_ui_content.cpp lua/test_ui_content.cpp

@ -0,0 +1,354 @@
#include "gmock/gmock.h"
#include <gtest/gtest.h>
#include <components/lua/yamlloader.hpp>
#include "../testing_util.hpp"
namespace
{
template <typename T>
bool checkNumber(sol::state_view& lua, const std::string& inputData, T requiredValue)
{
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
if (result.get_type() != sol::type::number)
return false;
return result.as<T>() == requiredValue;
}
bool checkBool(sol::state_view& lua, const std::string& inputData, bool requiredValue)
{
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
if (result.get_type() != sol::type::boolean)
return false;
return result.as<bool>() == requiredValue;
}
bool checkNil(sol::state_view& lua, const std::string& inputData)
{
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
return result == sol::nil;
}
bool checkNan(sol::state_view& lua, const std::string& inputData)
{
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
if (result.get_type() != sol::type::number)
return false;
return std::isnan(result.as<double>());
}
bool checkString(sol::state_view& lua, const std::string& inputData, const std::string& requiredValue)
{
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
if (result.get_type() != sol::type::string)
return false;
return result.as<std::string>() == requiredValue;
}
bool checkString(sol::state_view& lua, const std::string& inputData)
{
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
if (result.get_type() != sol::type::string)
return false;
return result.as<std::string>() == inputData;
}
TEST(LuaUtilYamlLoader, ScalarTypeDeduction)
{
sol::state lua;
ASSERT_TRUE(checkNil(lua, "null"));
ASSERT_TRUE(checkNil(lua, "Null"));
ASSERT_TRUE(checkNil(lua, "NULL"));
ASSERT_TRUE(checkNil(lua, "~"));
ASSERT_TRUE(checkNil(lua, ""));
ASSERT_FALSE(checkNil(lua, "NUll"));
ASSERT_TRUE(checkString(lua, "NUll"));
ASSERT_TRUE(checkString(lua, "'null'", "null"));
ASSERT_TRUE(checkNumber(lua, "017", 17));
ASSERT_TRUE(checkNumber(lua, "-017", -17));
ASSERT_TRUE(checkNumber(lua, "+017", 17));
ASSERT_TRUE(checkNumber(lua, "17", 17));
ASSERT_TRUE(checkNumber(lua, "-17", -17));
ASSERT_TRUE(checkNumber(lua, "+17", 17));
ASSERT_TRUE(checkNumber(lua, "0o17", 15));
ASSERT_TRUE(checkString(lua, "-0o17"));
ASSERT_TRUE(checkString(lua, "+0o17"));
ASSERT_TRUE(checkString(lua, "0b1"));
ASSERT_TRUE(checkString(lua, "1:00"));
ASSERT_TRUE(checkString(lua, "'17'", "17"));
ASSERT_TRUE(checkNumber(lua, "0x17", 23));
ASSERT_TRUE(checkString(lua, "'-0x17'", "-0x17"));
ASSERT_TRUE(checkString(lua, "'+0x17'", "+0x17"));
ASSERT_TRUE(checkNumber(lua, "2.1e-05", 2.1e-5));
ASSERT_TRUE(checkNumber(lua, "-2.1e-05", -2.1e-5));
ASSERT_TRUE(checkNumber(lua, "+2.1e-05", 2.1e-5));
ASSERT_TRUE(checkNumber(lua, "2.1e+5", 210000));
ASSERT_TRUE(checkNumber(lua, "-2.1e+5", -210000));
ASSERT_TRUE(checkNumber(lua, "+2.1e+5", 210000));
ASSERT_TRUE(checkNumber(lua, "0.27", 0.27));
ASSERT_TRUE(checkNumber(lua, "-0.27", -0.27));
ASSERT_TRUE(checkNumber(lua, "+0.27", 0.27));
ASSERT_TRUE(checkNumber(lua, "2.7", 2.7));
ASSERT_TRUE(checkNumber(lua, "-2.7", -2.7));
ASSERT_TRUE(checkNumber(lua, "+2.7", 2.7));
ASSERT_TRUE(checkNumber(lua, ".27", 0.27));
ASSERT_TRUE(checkNumber(lua, "-.27", -0.27));
ASSERT_TRUE(checkNumber(lua, "+.27", 0.27));
ASSERT_TRUE(checkNumber(lua, "27.", 27.0));
ASSERT_TRUE(checkNumber(lua, "-27.", -27.0));
ASSERT_TRUE(checkNumber(lua, "+27.", 27.0));
ASSERT_TRUE(checkNan(lua, ".nan"));
ASSERT_TRUE(checkNan(lua, ".NaN"));
ASSERT_TRUE(checkNan(lua, ".NAN"));
ASSERT_FALSE(checkNan(lua, "nan"));
ASSERT_FALSE(checkNan(lua, ".nAn"));
ASSERT_TRUE(checkString(lua, "'.nan'", ".nan"));
ASSERT_TRUE(checkString(lua, ".nAn"));
ASSERT_TRUE(checkNumber(lua, "1.7976931348623157E+308", std::numeric_limits<double>::max()));
ASSERT_TRUE(checkNumber(lua, "-1.7976931348623157E+308", std::numeric_limits<double>::lowest()));
ASSERT_TRUE(checkNumber(lua, "2.2250738585072014e-308", std::numeric_limits<double>::min()));
ASSERT_TRUE(checkNumber(lua, ".inf", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "+.inf", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "-.inf", -std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, ".Inf", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "+.Inf", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "-.Inf", -std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, ".INF", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "+.INF", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "-.INF", -std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkString(lua, ".INf"));
ASSERT_TRUE(checkString(lua, "-.INf"));
ASSERT_TRUE(checkString(lua, "+.INf"));
ASSERT_TRUE(checkBool(lua, "true", true));
ASSERT_TRUE(checkBool(lua, "false", false));
ASSERT_TRUE(checkBool(lua, "True", true));
ASSERT_TRUE(checkBool(lua, "False", false));
ASSERT_TRUE(checkBool(lua, "TRUE", true));
ASSERT_TRUE(checkBool(lua, "FALSE", false));
ASSERT_TRUE(checkString(lua, "y"));
ASSERT_TRUE(checkString(lua, "n"));
ASSERT_TRUE(checkString(lua, "On"));
ASSERT_TRUE(checkString(lua, "Off"));
ASSERT_TRUE(checkString(lua, "YES"));
ASSERT_TRUE(checkString(lua, "NO"));
ASSERT_TRUE(checkString(lua, "TrUe"));
ASSERT_TRUE(checkString(lua, "FaLsE"));
ASSERT_TRUE(checkString(lua, "'true'", "true"));
}
TEST(LuaUtilYamlLoader, DepthLimit)
{
sol::state lua;
const std::string input = R"(
array1: &array1_alias
[
<: *array1_alias,
foo
]
)";
bool depthExceptionThrown = false;
try
{
YAML::Node root = YAML::Load(input);
sol::object result = LuaUtil::YamlLoader::load(input, lua);
}
catch (const std::runtime_error& e)
{
ASSERT_EQ(std::string(e.what()), "Maximum layers depth exceeded, probably caused by a circular reference");
depthExceptionThrown = true;
}
ASSERT_TRUE(depthExceptionThrown);
}
TEST(LuaUtilYamlLoader, Collections)
{
sol::state lua;
sol::object map = LuaUtil::YamlLoader::load("{ x: , y: 2, 4: 5 }", lua);
ASSERT_EQ(map.as<sol::table>()["x"], sol::nil);
ASSERT_EQ(map.as<sol::table>()["y"], 2);
ASSERT_EQ(map.as<sol::table>()[4], 5);
sol::object array = LuaUtil::YamlLoader::load("[ 3, 4 ]", lua);
ASSERT_EQ(array.as<sol::table>()[1], 3);
sol::object emptyTable = LuaUtil::YamlLoader::load("{}", lua);
ASSERT_TRUE(emptyTable.as<sol::table>().empty());
sol::object emptyArray = LuaUtil::YamlLoader::load("[]", lua);
ASSERT_TRUE(emptyArray.as<sol::table>().empty());
ASSERT_THROW(LuaUtil::YamlLoader::load("{ null: 1 }", lua), std::runtime_error);
ASSERT_THROW(LuaUtil::YamlLoader::load("{ .nan: 1 }", lua), std::runtime_error);
const std::string scalarArrayInput = R"(
- First Scalar
- 1
- true)";
sol::object scalarArray = LuaUtil::YamlLoader::load(scalarArrayInput, lua);
ASSERT_EQ(scalarArray.as<sol::table>()[1], std::string("First Scalar"));
ASSERT_EQ(scalarArray.as<sol::table>()[2], 1);
ASSERT_EQ(scalarArray.as<sol::table>()[3], true);
const std::string scalarMapWithCommentsInput = R"(
string: 'str' # String value
integer: 65 # Integer value
float: 0.278 # Float value
bool: false # Boolean value)";
sol::object scalarMapWithComments = LuaUtil::YamlLoader::load(scalarMapWithCommentsInput, lua);
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["string"], std::string("str"));
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["integer"], 65);
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["float"], 0.278);
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["bool"], false);
const std::string mapOfArraysInput = R"(
x:
- 2
- 7
- true
y:
- aaa
- false
- 1)";
sol::object mapOfArrays = LuaUtil::YamlLoader::load(mapOfArraysInput, lua);
ASSERT_EQ(mapOfArrays.as<sol::table>()["x"][3], true);
ASSERT_EQ(mapOfArrays.as<sol::table>()["y"][1], std::string("aaa"));
const std::string arrayOfMapsInput = R"(
-
name: Name1
hr: 65
avg: 0.278
-
name: Name2
hr: 63
avg: 0.288)";
sol::object arrayOfMaps = LuaUtil::YamlLoader::load(arrayOfMapsInput, lua);
ASSERT_EQ(arrayOfMaps.as<sol::table>()[1]["avg"], 0.278);
ASSERT_EQ(arrayOfMaps.as<sol::table>()[2]["name"], std::string("Name2"));
const std::string arrayOfArraysInput = R"(
- [Name1, 65, 0.278]
- [Name2 , 63, 0.288])";
sol::object arrayOfArrays = LuaUtil::YamlLoader::load(arrayOfArraysInput, lua);
ASSERT_EQ(arrayOfArrays.as<sol::table>()[1][2], 65);
ASSERT_EQ(arrayOfArrays.as<sol::table>()[2][1], std::string("Name2"));
const std::string mapOfMapsInput = R"(
Name1: {hr: 65, avg: 0.278}
Name2 : {
hr: 63,
avg: 0.288,
})";
sol::object mapOfMaps = LuaUtil::YamlLoader::load(mapOfMapsInput, lua);
ASSERT_EQ(mapOfMaps.as<sol::table>()["Name1"]["hr"], 65);
ASSERT_EQ(mapOfMaps.as<sol::table>()["Name2"]["avg"], 0.288);
}
TEST(LuaUtilYamlLoader, Structures)
{
sol::state lua;
const std::string twoDocumentsInput
= "---\n"
" - First Scalar\n"
" - 2\n"
" - true\n"
"\n"
"---\n"
" - Second Scalar\n"
" - 3\n"
" - false";
sol::object twoDocuments = LuaUtil::YamlLoader::load(twoDocumentsInput, lua);
ASSERT_EQ(twoDocuments.as<sol::table>()[1][1], std::string("First Scalar"));
ASSERT_EQ(twoDocuments.as<sol::table>()[2][3], false);
const std::string anchorInput = R"(---
x:
- Name1
# Following node labeled as "a"
- &a Value1
y:
- *a # Subsequent occurrence
- Name2)";
sol::object anchor = LuaUtil::YamlLoader::load(anchorInput, lua);
ASSERT_EQ(anchor.as<sol::table>()["y"][1], std::string("Value1"));
const std::string compoundKeyInput = R"(
? - String1
- String2
: - 1
? [ String3,
String4 ]
: [ 2, 3, 4 ])";
ASSERT_THROW(LuaUtil::YamlLoader::load(compoundKeyInput, lua), std::runtime_error);
const std::string compactNestedMappingInput = R"(
- item : Item1
quantity: 2
- item : Item2
quantity: 4
- item : Item3
quantity: 11)";
sol::object compactNestedMapping = LuaUtil::YamlLoader::load(compactNestedMappingInput, lua);
ASSERT_EQ(compactNestedMapping.as<sol::table>()[2]["quantity"], 4);
}
TEST(LuaUtilYamlLoader, Scalars)
{
sol::state lua;
const std::string literalScalarInput = R"(--- |
a
b
c)";
ASSERT_TRUE(checkString(lua, literalScalarInput, "a\nb\nc"));
const std::string foldedScalarInput = R"(--- >
a
b
c)";
ASSERT_TRUE(checkString(lua, foldedScalarInput, "a b c"));
const std::string multiLinePlanarScalarsInput = R"(
plain:
This unquoted scalar
spans many lines.
quoted: "So does this
quoted scalar.\n")";
sol::object multiLinePlanarScalars = LuaUtil::YamlLoader::load(multiLinePlanarScalarsInput, lua);
ASSERT_TRUE(
multiLinePlanarScalars.as<sol::table>()["plain"] == std::string("This unquoted scalar spans many lines."));
ASSERT_TRUE(multiLinePlanarScalars.as<sol::table>()["quoted"] == std::string("So does this quoted scalar.\n"));
}
}

@ -65,7 +65,7 @@ list(APPEND COMPONENT_FILES "${OpenMW_BINARY_DIR}/${OSG_PLUGIN_CHECKER_CPP_FILE}
add_component_dir (lua add_component_dir (lua
luastate scriptscontainer asyncpackage utilpackage serialization configuration l10n storage utf8 luastate scriptscontainer asyncpackage utilpackage serialization configuration l10n storage utf8
shapes/box inputactions shapes/box inputactions yamlloader
) )
add_component_dir (l10n add_component_dir (l10n

@ -0,0 +1,241 @@
#include "yamlloader.hpp"
#include <charconv>
#include <regex>
#include <components/misc/strings/format.hpp>
#include <components/misc/strings/lower.hpp>
namespace LuaUtil
{
namespace
{
constexpr uint64_t maxDepth = 250;
}
sol::object YamlLoader::load(const std::string& input, const sol::state_view& lua)
{
std::vector<YAML::Node> rootNodes = YAML::LoadAll(input);
return LuaUtil::YamlLoader::load(rootNodes, lua);
}
sol::object YamlLoader::load(std::istream& input, const sol::state_view& lua)
{
std::vector<YAML::Node> rootNodes = YAML::LoadAll(input);
return load(rootNodes, lua);
}
sol::object YamlLoader::load(const std::vector<YAML::Node> rootNodes, const sol::state_view& lua)
{
if (rootNodes.empty())
return sol::nil;
if (rootNodes.size() == 1)
return getNode(rootNodes[0], lua, 0);
sol::table documentsTable(lua, sol::create);
for (const auto& root : rootNodes)
{
documentsTable.add(getNode(root, lua, 1));
}
return documentsTable;
}
sol::object YamlLoader::getNode(const YAML::Node& node, const sol::state_view& lua, uint64_t depth)
{
if (depth >= maxDepth)
throw std::runtime_error("Maximum layers depth exceeded, probably caused by a circular reference");
++depth;
if (node.IsMap())
return getMap(node, lua, depth);
else if (node.IsSequence())
return getArray(node, lua, depth);
else if (node.IsScalar())
return getScalar(node, lua);
else if (node.IsNull())
return sol::nil;
nodeError(node, "An unknown YAML node encountered");
}
sol::table YamlLoader::getMap(const YAML::Node& node, const sol::state_view& lua, uint64_t depth)
{
sol::table childTable(lua, sol::create);
for (const auto& pair : node)
{
if (pair.first.IsMap())
nodeError(pair.first, "Only scalar nodes can be used as keys, encountered map instead");
if (pair.first.IsSequence())
nodeError(pair.first, "Only scalar nodes can be used as keys, encountered array instead");
if (pair.first.IsNull())
nodeError(pair.first, "Only scalar nodes can be used as keys, encountered null instead");
auto key = getNode(pair.first, lua, depth);
if (key.get_type() == sol::type::number && std::isnan(key.as<double>()))
nodeError(pair.first, "Only scalar nodes can be used as keys, encountered nan instead");
childTable[key] = getNode(pair.second, lua, depth);
}
return childTable;
}
sol::table YamlLoader::getArray(const YAML::Node& node, const sol::state_view& lua, uint64_t depth)
{
sol::table childTable(lua, sol::create);
for (const auto& child : node)
{
childTable.add(getNode(child, lua, depth));
}
return childTable;
}
YamlLoader::ScalarType YamlLoader::getScalarType(const YAML::Node& node)
{
const auto& tag = node.Tag();
const auto& value = node.Scalar();
if (tag == "!")
return ScalarType::String;
// Note that YAML allows to explicitely specify a scalar type via tag (e.g. "!!bool"), but it makes no
// sense in Lua:
// 1. Both integers and floats use the "number" type prior to Lua 5.3
// 2. Strings can be quoted, which is more readable than "!!str"
// 3. Most of possible conversions are invalid or their result is unclear
// So ignore this feature for now.
if (tag != "?")
nodeError(node, "An invalid tag'" + tag + "' encountered");
if (value.empty())
return ScalarType::Null;
// Resolve type according to YAML 1.2 Core Schema (see https://yaml.org/spec/1.2.2/#103-core-schema)
static const std::regex boolRegex("true|True|TRUE|false|False|FALSE", std::regex_constants::extended);
if (std::regex_match(node.Scalar(), boolRegex))
return ScalarType::Boolean;
static const std::regex decimalRegex("[-+]?[0-9]+", std::regex_constants::extended);
if (std::regex_match(node.Scalar(), decimalRegex))
return ScalarType::Decimal;
static const std::regex floatRegex(
"[-+]?([.][0-9]+|[0-9]+([.][0-9]*)?)([eE][-+]?[0-9]+)?", std::regex_constants::extended);
if (std::regex_match(node.Scalar(), floatRegex))
return ScalarType::Float;
static const std::regex octalRegex("0o[0-7]+", std::regex_constants::extended);
if (std::regex_match(node.Scalar(), octalRegex))
return ScalarType::Octal;
static const std::regex hexdecimalRegex("0x[0-9a-fA-F]+", std::regex_constants::extended);
if (std::regex_match(node.Scalar(), hexdecimalRegex))
return ScalarType::Hexadecimal;
static const std::regex infinityRegex("[-+]?([.]inf|[.]Inf|[.]INF)", std::regex_constants::extended);
if (std::regex_match(node.Scalar(), infinityRegex))
return ScalarType::Infinity;
static const std::regex nanRegex("[.]nan|[.]NaN|[.]NAN", std::regex_constants::extended);
if (std::regex_match(node.Scalar(), nanRegex))
return ScalarType::NotNumber;
static const std::regex nullRegex("null|Null|NULL|~", std::regex_constants::extended);
if (std::regex_match(node.Scalar(), nullRegex))
return ScalarType::Null;
return ScalarType::String;
}
sol::object YamlLoader::getScalar(const YAML::Node& node, const sol::state_view& lua)
{
auto type = getScalarType(node);
const auto& value = node.Scalar();
switch (type)
{
case ScalarType::Null:
return sol::nil;
case ScalarType::String:
return sol::make_object<std::string>(lua, value);
case ScalarType::NotNumber:
return sol::make_object<double>(lua, std::nan(""));
case ScalarType::Infinity:
{
if (!value.empty() && value[0] == '-')
return sol::make_object<double>(lua, -std::numeric_limits<double>::infinity());
return sol::make_object<double>(lua, std::numeric_limits<double>::infinity());
}
case ScalarType::Boolean:
{
if (Misc::StringUtils::lowerCase(value) == "true")
return sol::make_object<bool>(lua, true);
if (Misc::StringUtils::lowerCase(value) == "false")
return sol::make_object<bool>(lua, false);
nodeError(node, "Can not read a boolean value '" + value + "'");
}
case ScalarType::Decimal:
{
int offset = 0;
// std::from_chars does not support "+" sign
if (!value.empty() && value[0] == '+')
++offset;
int result = 0;
const auto status = std::from_chars(value.data() + offset, value.data() + value.size(), result);
if (status.ec == std::errc())
return sol::make_object<int>(lua, result);
nodeError(node, "Can not read a decimal value '" + value + "'");
}
case ScalarType::Float:
{
// Not all compilers support std::from_chars for floats
double result = 0.0;
bool success = YAML::convert<double>::decode(node, result);
if (success)
return sol::make_object<double>(lua, result);
nodeError(node, "Can not read a float value '" + value + "'");
}
case ScalarType::Hexadecimal:
{
int result = 0;
const auto status = std::from_chars(value.data() + 2, value.data() + value.size(), result, 16);
if (status.ec == std::errc())
return sol::make_object<int>(lua, result);
nodeError(node, "Can not read a hexadecimal value '" + value + "'");
}
case ScalarType::Octal:
{
int result = 0;
const auto status = std::from_chars(value.data() + 2, value.data() + value.size(), result, 8);
if (status.ec == std::errc())
return sol::make_object<int>(lua, result);
nodeError(node, "Can not read an octal value '" + value + "'");
}
default:
nodeError(node, "An unknown scalar '" + value + "' encountered");
}
}
[[noreturn]] void YamlLoader::nodeError(const YAML::Node& node, const std::string& message)
{
const auto& mark = node.Mark();
std::string error = Misc::StringUtils::format(
" at line=%d column=%d position=%d", mark.line + 1, mark.column + 1, mark.pos + 1);
throw std::runtime_error(message + error);
}
}

@ -0,0 +1,50 @@
#ifndef COMPONENTS_LUA_YAMLLOADER_H
#define COMPONENTS_LUA_YAMLLOADER_H
#include <map>
#include <sol/sol.hpp>
#include <stdexcept>
#include <yaml-cpp/yaml.h>
namespace LuaUtil
{
class YamlLoader
{
public:
static sol::object load(const std::string& input, const sol::state_view& lua);
static sol::object load(std::istream& input, const sol::state_view& lua);
private:
enum class ScalarType
{
Boolean,
Decimal,
Float,
Hexadecimal,
Infinity,
NotNumber,
Null,
Octal,
String
};
static sol::object load(const std::vector<YAML::Node> rootNodes, const sol::state_view& lua);
static sol::object getNode(const YAML::Node& node, const sol::state_view& lua, uint64_t depth);
static sol::table getMap(const YAML::Node& node, const sol::state_view& lua, uint64_t depth);
static sol::table getArray(const YAML::Node& node, const sol::state_view& lua, uint64_t depth);
static ScalarType getScalarType(const YAML::Node& node);
static sol::object getScalar(const YAML::Node& node, const sol::state_view& lua);
[[noreturn]] static void nodeError(const YAML::Node& node, const std::string& message);
};
}
#endif // COMPONENTS_LUA_YAMLLOADER_H

@ -19,6 +19,7 @@ Lua API reference
openmw_animation openmw_animation
openmw_async openmw_async
openmw_vfs openmw_vfs
openmw_markup
openmw_world openmw_world
openmw_self openmw_self
openmw_nearby openmw_nearby

@ -0,0 +1,7 @@
Package openmw.markup
=====================
.. include:: version.rst
.. raw:: html
:file: generated_html/openmw_markup.html

@ -19,6 +19,8 @@
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ +------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.vfs <Package openmw.vfs>` | everywhere | | Read-only access to data directories via VFS. | |:ref:`openmw.vfs <Package openmw.vfs>` | everywhere | | Read-only access to data directories via VFS. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ +------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.markup <Package openmw.markup>` | everywhere | | API to work with markup languages. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.world <Package openmw.world>` | by global scripts | | Read-write access to the game world. | |:ref:`openmw.world <Package openmw.world>` | by global scripts | | Read-write access to the game world. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ +------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.self <Package openmw.self>` | by local scripts | | Full access to the object the script is attached to. | |:ref:`openmw.self <Package openmw.self>` | by local scripts | | Full access to the object the script is attached to. |

@ -23,6 +23,7 @@ local env = {
core = require('openmw.core'), core = require('openmw.core'),
types = require('openmw.types'), types = require('openmw.types'),
vfs = require('openmw.vfs'), vfs = require('openmw.vfs'),
markup = require('openmw.markup'),
async = require('openmw.async'), async = require('openmw.async'),
world = require('openmw.world'), world = require('openmw.world'),
aux_util = require('openmw_aux.util'), aux_util = require('openmw_aux.util'),

@ -25,6 +25,7 @@ local env = {
core = require('openmw.core'), core = require('openmw.core'),
types = require('openmw.types'), types = require('openmw.types'),
vfs = require('openmw.vfs'), vfs = require('openmw.vfs'),
markup = require('openmw.markup'),
async = require('openmw.async'), async = require('openmw.async'),
nearby = require('openmw.nearby'), nearby = require('openmw.nearby'),
self = require('openmw.self'), self = require('openmw.self'),

@ -47,6 +47,7 @@ local env = {
core = require('openmw.core'), core = require('openmw.core'),
storage = require('openmw.storage'), storage = require('openmw.storage'),
vfs = require('openmw.vfs'), vfs = require('openmw.vfs'),
markup = require('openmw.markup'),
ambient = require('openmw.ambient'), ambient = require('openmw.ambient'),
async = require('openmw.async'), async = require('openmw.async'),
ui = require('openmw.ui'), ui = require('openmw.ui'),

@ -72,6 +72,7 @@ local env = {
core = require('openmw.core'), core = require('openmw.core'),
types = require('openmw.types'), types = require('openmw.types'),
vfs = require('openmw.vfs'), vfs = require('openmw.vfs'),
markup = require('openmw.markup'),
ambient = require('openmw.ambient'), ambient = require('openmw.ambient'),
async = require('openmw.async'), async = require('openmw.async'),
nearby = require('openmw.nearby'), nearby = require('openmw.nearby'),

@ -17,6 +17,7 @@ set(LUA_API_FILES
openmw/debug.lua openmw/debug.lua
openmw/input.lua openmw/input.lua
openmw/interfaces.lua openmw/interfaces.lua
openmw/markup.lua
openmw/menu.lua openmw/menu.lua
openmw/nearby.lua openmw/nearby.lua
openmw/postprocessing.lua openmw/postprocessing.lua

@ -0,0 +1,37 @@
---
-- `openmw.markup` allows to work with markup languages.
-- @module markup
-- @usage local markup = require('openmw.markup')
---
-- Convert YAML data to Lua object
-- @function [parent=#markup] decodeYaml
-- @param #string inputData Data to decode. It has such limitations:
--
-- 1. YAML format of [version 1.2](https://yaml.org/spec/1.2.2) is used.
-- 2. Map keys should be scalar values (strings, booleans, numbers).
-- 3. YAML tag system is not supported.
-- 4. If scalar is quoted, it is treated like a string.
-- Othewise type deduction works according to YAML 1.2 [Core Schema](https://yaml.org/spec/1.2.2/#103-core-schema).
-- 5. Circular dependencies between YAML nodes are not allowed.
-- 6. Lua 5.1 does not have integer numbers - all numeric scalars use a #number type (which use a floating point).
-- 7. Integer scalars numbers values are limited by the "int" range. Use floating point notation for larger number in YAML files.
-- @return #any Lua object (can be table or scalar value).
-- @usage local result = markup.decodeYaml('{ "x": 1 }');
-- -- prints 1
-- print(result["x"])
---
-- Load YAML file from VFS to Lua object. Conventions are the same as in @{#markup.decodeYaml}.
-- @function [parent=#markup] loadYaml
-- @param #string fileName YAML file path in VFS.
-- @return #any Lua object (can be table or scalar value).
-- @usage -- file contains '{ "x": 1 }' data
-- local result = markup.loadYaml('test.yaml');
-- -- prints 1
-- print(result["x"])
return nil
Loading…
Cancel
Save