replace literally the entire lua engine

This commit is contained in:
Lion Kortlepel
2024-02-05 19:46:05 +01:00
parent 55fff0138b
commit a0241d1b36
14 changed files with 2097 additions and 133 deletions

101
src/FileWatcher.cpp Normal file
View File

@@ -0,0 +1,101 @@
#include "FileWatcher.h"
#include "Common.h"
#include <chrono>
#include <filesystem>
/// @file
/// This file holds the FileWatcher implementation.
FileWatcher::FileWatcher(unsigned int seconds)
: m_seconds(boost::posix_time::seconds(seconds))
, m_timer(boost::asio::deadline_timer(m_io, *m_seconds)) {
m_timer->async_wait([this](const auto& err) { on_tick(err); });
m_thread = boost::scoped_thread<> { &FileWatcher::thread_main, this };
}
FileWatcher::~FileWatcher() {
m_work_guard.reset();
*m_shutdown = true;
}
void FileWatcher::watch_file(const std::filesystem::path& path) {
m_files->insert(path);
}
void FileWatcher::watch_files_in(const std::filesystem::path& path) {
m_dirs->insert(path);
}
void FileWatcher::thread_main() {
while (!*m_shutdown) {
m_io.run_for(std::chrono::seconds(1));
}
}
void FileWatcher::on_tick(const boost::system::error_code& err) {
auto timer = m_timer.synchronize();
// set up timer so that the operations after this don't impact the accuracy of the
// timing
timer->expires_at(timer->expires_at() + *m_seconds);
if (err) {
l::error("FileWatcher encountered error: {}", err.message());
// TODO: Should any further action be taken?
} else {
try {
check_files();
} catch (const std::exception& e) {
l::error("FileWatcher exception while checking files: {}", e.what());
}
try {
check_directories();
} catch (const std::exception& e) {
l::error("FileWatcher exception while checking directories: {}", e.what());
}
}
// finally start the timer again, deadline has already been set at the beginning
// of this function
timer->async_wait([this](const auto& err) { on_tick(err); });
}
void FileWatcher::check_files() {
auto files = m_files.synchronize();
for (const auto& file : *files) {
check_file(file);
// TODO: add deleted/created watches
}
}
void FileWatcher::check_directories() {
auto directories = m_dirs.synchronize();
for (const auto& dir : *directories) {
if (std::filesystem::exists(dir)) {
for (const auto& entry : std::filesystem::recursive_directory_iterator(dir, std::filesystem::directory_options::follow_directory_symlink | std::filesystem::directory_options::skip_permission_denied)) {
if (entry.is_regular_file() || entry.is_symlink()) {
check_file(entry.path());
}
}
}
// TODO: add deleted/created watches
}
}
void FileWatcher::check_file(const std::filesystem::path& file) {
if (std::filesystem::exists(file)) {
auto real_file = file;
if (std::filesystem::is_symlink(file)) {
real_file = std::filesystem::read_symlink(file);
}
auto time = std::filesystem::last_write_time(real_file);
if (!m_file_mod_times.contains(file)) {
m_file_mod_times.insert_or_assign(file, time);
} else {
if (m_file_mod_times.at(file) != time) {
beammp_tracef("File changed: {}", file);
m_file_mod_times.at(file) = time;
}
}
}
}

View File

@@ -20,85 +20,14 @@
#include "Common.h"
#include "CustomAssert.h"
#include "TLuaEngine.h"
#include "Value.h"
#include <nlohmann/json.hpp>
#include <sol/types.hpp>
#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
std::string LuaAPI::LuaToString(const sol::object Value, size_t Indent, bool QuoteStrings) {
if (Indent > 80) {
return "[[possible recursion, refusing to keep printing]]";
}
switch (Value.get_type()) {
case sol::type::userdata: {
std::stringstream ss;
ss << "[[userdata: " << Value.as<sol::userdata>().pointer() << "]]";
return ss.str();
}
case sol::type::thread: {
std::stringstream ss;
ss << "[[thread: " << Value.as<sol::thread>().pointer() << "]] {"
<< "\n";
for (size_t i = 0; i < Indent; ++i) {
ss << "\t";
}
ss << "status: " << std::to_string(int(Value.as<sol::thread>().status())) << "\n}";
return ss.str();
}
case sol::type::lightuserdata: {
std::stringstream ss;
ss << "[[lightuserdata: " << Value.as<sol::lightuserdata>().pointer() << "]]";
return ss.str();
}
case sol::type::string:
if (QuoteStrings) {
return "\"" + Value.as<std::string>() + "\"";
} else {
return Value.as<std::string>();
}
case sol::type::number: {
std::stringstream ss;
ss << Value.as<float>();
return ss.str();
}
case sol::type::lua_nil:
case sol::type::none:
return "<nil>";
case sol::type::boolean:
return Value.as<bool>() ? "true" : "false";
case sol::type::table: {
std::stringstream Result;
auto Table = Value.as<sol::table>();
Result << "[[table: " << Table.pointer() << "]]: {";
if (!Table.empty()) {
for (const auto& Entry : Table) {
Result << "\n";
for (size_t i = 0; i < Indent; ++i) {
Result << "\t";
}
Result << LuaToString(Entry.first, Indent + 1) << ": " << LuaToString(Entry.second, Indent + 1, true) << ",";
}
Result << "\n";
}
for (size_t i = 0; i < Indent - 1; ++i) {
Result << "\t";
}
Result << "}";
return Result.str();
}
case sol::type::function: {
std::stringstream ss;
ss << "[[function: " << Value.as<sol::function>().pointer() << "]]";
return ss.str();
}
case sol::type::poly:
return "<poly>";
default:
return "<unprintable type>";
}
}
std::string LuaAPI::MP::GetOSName() {
#if WIN32
return "Windows";
@@ -113,15 +42,6 @@ std::tuple<int, int, int> LuaAPI::MP::GetServerVersion() {
return { Application::ServerVersion().major, Application::ServerVersion().minor, Application::ServerVersion().patch };
}
void LuaAPI::Print(sol::variadic_args Args) {
std::string ToPrint = "";
for (const auto& Arg : Args) {
ToPrint += LuaToString(static_cast<const sol::object>(Arg));
ToPrint += "\t";
}
luaprint(ToPrint);
}
TEST_CASE("LuaAPI::MP::GetServerVersion") {
const auto [ma, mi, pa] = LuaAPI::MP::GetServerVersion();
const auto real = Application::ServerVersion();
@@ -324,17 +244,6 @@ bool LuaAPI::MP::IsPlayerGuest(int ID) {
*/
}
void LuaAPI::MP::PrintRaw(sol::variadic_args Args) {
std::string ToPrint = "";
for (const auto& Arg : Args) {
ToPrint += LuaToString(static_cast<const sol::object>(Arg));
ToPrint += "\t";
}
#ifdef DOCTEST_CONFIG_DISABLE
Application::Console().WriteRaw(ToPrint);
#endif
}
int LuaAPI::PanicHandler(lua_State* State) {
beammp_lua_error("PANIC: " + sol::stack::get<std::string>(State, 1));
return 0;
@@ -633,22 +542,27 @@ static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, c
}
}
std::string LuaAPI::MP::JsonEncode(const sol::table& object) {
nlohmann::json json;
// table
bool is_array = true;
for (const auto& pair : object.as<sol::table>()) {
if (pair.first.get_type() != sol::type::number) {
is_array = false;
}
static std::string lua_to_json_impl(const sol::object& args) {
// used as the invalid value provider in sol_obj_to_value.
auto special_stringifier = [](const sol::object& object) -> Result<Value> {
beammp_lua_debugf("Cannot convert from type {} to json, ignoring (using null)", sol::to_string(object.get_type()));
return { Null };
};
auto maybe_val = sol_obj_to_value(obj, special_stringifier);
if (maybe_val) {
auto result = boost::apply_visitor(ValueToJsonVisitor(ValueToStringVisitor::Flag::NONE), maybe_val.move());
return result.dump();
} else {
beammp_lua_errorf("Failed to convert an argument to json: {}", maybe_val.error);
return "";
}
for (const auto& entry : object) {
JsonEncodeRecursive(json, entry.first, entry.second, is_array);
}
return json.dump();
}
std::string LuaAPI::MP::JsonDiff(const std::string& a, const std::string& b) {
std::string LuaAPI::Util::JsonEncode(const sol::object& object) {
return lua_to_json_impl(object);
}
std::string LuaAPI::Util::JsonDiff(const std::string& a, const std::string& b) {
if (!nlohmann::json::accept(a)) {
beammp_lua_error("JsonDiff first argument is not valid json: `" + a + "`");
return "";
@@ -662,7 +576,7 @@ std::string LuaAPI::MP::JsonDiff(const std::string& a, const std::string& b) {
return nlohmann::json::diff(a_json, b_json).dump();
}
std::string LuaAPI::MP::JsonDiffApply(const std::string& data, const std::string& patch) {
std::string LuaAPI::Util::JsonDiffApply(const std::string& data, const std::string& patch) {
if (!nlohmann::json::accept(data)) {
beammp_lua_error("JsonDiffApply first argument is not valid json: `" + data + "`");
return "";
@@ -677,7 +591,7 @@ std::string LuaAPI::MP::JsonDiffApply(const std::string& data, const std::string
return a_json.dump();
}
std::string LuaAPI::MP::JsonPrettify(const std::string& json) {
std::string LuaAPI::Util::JsonPrettify(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_lua_error("JsonPrettify argument is not valid json: `" + json + "`");
return "";
@@ -685,7 +599,7 @@ std::string LuaAPI::MP::JsonPrettify(const std::string& json) {
return nlohmann::json::parse(json).dump(4);
}
std::string LuaAPI::MP::JsonMinify(const std::string& json) {
std::string LuaAPI::Util::JsonMinify(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_lua_error("JsonMinify argument is not valid json: `" + json + "`");
return "";
@@ -693,7 +607,7 @@ std::string LuaAPI::MP::JsonMinify(const std::string& json) {
return nlohmann::json::parse(json).dump(-1);
}
std::string LuaAPI::MP::JsonFlatten(const std::string& json) {
std::string LuaAPI::Util::JsonFlatten(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_lua_error("JsonFlatten argument is not valid json: `" + json + "`");
return "";
@@ -701,7 +615,7 @@ std::string LuaAPI::MP::JsonFlatten(const std::string& json) {
return nlohmann::json::parse(json).flatten().dump(-1);
}
std::string LuaAPI::MP::JsonUnflatten(const std::string& json) {
std::string LuaAPI::Util::JsonUnflatten(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_lua_error("JsonUnflatten argument is not valid json: `" + json + "`");
return "";
@@ -712,3 +626,99 @@ std::string LuaAPI::MP::JsonUnflatten(const std::string& json) {
std::pair<bool, std::string> LuaAPI::MP::TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data) {
return InternalTriggerClientEvent(PlayerID, EventName, JsonEncode(Data));
}
size_t LuaAPI::MP::GetPlayerCount() { return Engine->Server().ClientCount(); }
static void JsonDecodeRecursive(sol::state_view& StateView, sol::table& table, const std::string& left, const nlohmann::json& right) {
switch (right.type()) {
case nlohmann::detail::value_t::null:
return;
case nlohmann::detail::value_t::object: {
auto value = table.create();
value.clear();
for (const auto& entry : right.items()) {
JsonDecodeRecursive(StateView, value, entry.key(), entry.value());
}
AddToTable(table, left, value);
break;
}
case nlohmann::detail::value_t::array: {
auto value = table.create();
value.clear();
for (const auto& entry : right.items()) {
JsonDecodeRecursive(StateView, value, "", entry.value());
}
AddToTable(table, left, value);
break;
}
case nlohmann::detail::value_t::string:
AddToTable(table, left, right.get<std::string>());
break;
case nlohmann::detail::value_t::boolean:
AddToTable(table, left, right.get<bool>());
break;
case nlohmann::detail::value_t::number_integer:
AddToTable(table, left, right.get<int64_t>());
break;
case nlohmann::detail::value_t::number_unsigned:
AddToTable(table, left, right.get<uint64_t>());
break;
case nlohmann::detail::value_t::number_float:
AddToTable(table, left, right.get<double>());
break;
case nlohmann::detail::value_t::binary:
beammp_lua_error("JsonDecode can't handle binary blob in json, ignoring");
return;
case nlohmann::detail::value_t::discarded:
return;
default:
beammp_assert_not_reachable();
}
}
sol::table LuaAPI::Util::JsonDecode(sol::this_state s, const std::string& string) {
sol::state_view StateView(s);
auto table = StateView.create_tab if (!nlohmann::json::accept(str)) {
beammp_lua_error("string given to JsonDecode is not valid json: `" + str + "`");
return sol::lua_nil;
}
nlohmann::json json = nlohmann::json::parse(str);
if (json.is_object()) {
for (const auto& entry : json.items()) {
JsonDecodeRecursive(StateView, table, entry.key(), entry.value());
}
} else if (json.is_array()) {
for (const auto& entry : json) {
JsonDecodeRecursive(StateView, table, "", entry);
}
} else {
beammp_lua_error("JsonDecode expected array or object json, instead got " + std::string(json.type_name()));
return sol::lua_nil;
}
return table;
}
sol::table LuaAPI::FS::ListDirectories(sol::this_state s, const std::string& path) {
if (!std::filesystem::exists(Path)) {
return sol::lua_nil;
}
auto table = s.create_table();
for (const auto& entry : std::filesystem::directory_iterator(Path)) {
if (entry.is_directory()) {
table[table.size() + 1] = entry.path().lexically_relative(Path).string();
}
}
return table;
}
sol::table LuaAPI::FS::ListFiles(sol::this_state s, const std::string& path) {
if (!std::filesystem::exists(Path)) {
return sol::lua_nil;
}
auto table = s.create_table();
for (const auto& entry : std::filesystem::directory_iterator(Path)) {
if (entry.is_regular_file() || entry.is_symlink()) {
table[table.size() + 1] = entry.path().lexically_relative(Path).string();
}
}
return table;
}

602
src/LuaPlugin.cpp Normal file
View File

@@ -0,0 +1,602 @@
#include "LuaPlugin.h"
#include "Common.h"
#include "LuaAPI.h"
#include "Value.h"
#include <boost/algorithm/string/replace.hpp>
#include <boost/asio/post.hpp>
#include <boost/date_time/microsec_time_clock.hpp>
#include <boost/date_time/posix_time/posix_time_config.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#include <boost/thread/exceptions.hpp>
#include <cctype>
#include <chrono>
#include <filesystem>
#include <fmt/chrono.h>
#include <fmt/ostream.h>
#include <fmt/ranges.h>
#include <fmt/std.h>
#include <functional>
#include <lauxlib.h>
#include <lua.h>
#include <optional>
#include <regex>
#include <sol/forward.hpp>
#include <sol/sol.hpp>
#include <sol/types.hpp>
#include <sol/variadic_args.hpp>
#include <spdlog/common.h>
#include <spdlog/spdlog.h>
#include <sstream>
#include <thread>
static int lua_panic_handler(lua_State* state) {
sol::state_view view(state);
sol::state new_state {};
luaL_traceback(state, new_state.lua_state(), nullptr, 1);
auto traceback = new_state.get<std::string>(-1);
beammp_errorf("Lua panic (unclear in which plugin): {}", traceback);
return 1;
}
#define beammp_lua_debugf(...) beammp_debugf("[lua:{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_infof(...) beammp_infof("[lua:{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_warnf(...) beammp_warnf("[lua:{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_errorf(...) beammp_errorf("[lua:{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_tracef(...) beammp_tracef("[lua:{}] {}", name(), fmt::format(__VA_ARGS__))
static constexpr const char* ERR_HANDLER = "__beammp_lua_error_handler";
/// Checks whether the supplied name is a valid lua identifier (mostly).
static inline bool check_name_validity(const std::string& name) {
if (name.empty()) {
return false;
}
if (std::isdigit(name.at(0))) {
return false;
}
for (const char c : name) {
if (!std::isalnum(c) && c != '_') {
return false;
}
}
return true;
}
LuaPlugin::LuaPlugin(const std::string& path)
: m_path(path) {
m_state = sol::state(lua_panic_handler);
m_thread = boost::scoped_thread<> { &LuaPlugin::thread_main, this };
}
LuaPlugin::~LuaPlugin() {
for (auto& timer : m_timers) {
timer->timer.cancel();
}
// work guard reset means that we allow all work to be finished before exit
m_work_guard.reset();
// setting this flag signals the thread to shut down
*m_shutdown = true;
}
Error LuaPlugin::initialize_error_handlers() {
m_state.set_exception_handler([](lua_State* state, sol::optional<const std::exception&>, auto err) -> int {
beammp_errorf("Error (unclear in which plugin): {}", err); // TODO: wtf?
return sol::stack::push(state, err);
});
m_state.globals()[ERR_HANDLER] = [this](const std::string& error) {
beammp_lua_errorf("Error: {}", error);
return error;
};
return {};
}
Error LuaPlugin::initialize_libraries() {
m_state.open_libraries(
sol::lib::base,
sol::lib::package,
sol::lib::coroutine,
sol::lib::string,
sol::lib::os,
sol::lib::math,
sol::lib::table,
sol::lib::debug,
sol::lib::bit32,
sol::lib::io);
auto& glob = m_state.globals();
glob.create_named("MP");
glob["MP"]["GetExtensions"] = [this]() -> sol::table {
auto table = m_state.create_table();
auto extensions = m_known_extensions.synchronize();
for (const auto& [ext, path] : *extensions) {
(void)path;
table[ext] = m_state.globals()[ext];
}
return table; };
glob["MP"]["GetStateMemoryUsage"] = [this]() { return size_t(m_state.memory_used()); };
glob["MP"]["GetPluginMemoryUsage"] = [this] { return memory_usage(); };
glob["MP"]["LogError"] = [this](const sol::variadic_args& args) {
auto result = print_impl(args);
beammp_lua_errorf("[out] {}", result);
};
glob["MP"]["LogWarn"] = [this](const sol::variadic_args& args) {
auto result = print_impl(args);
beammp_lua_warnf("[out] {}", result);
};
glob["MP"]["LogInfo"] = [this](const sol::variadic_args& args) {
auto result = print_impl(args);
beammp_lua_infof("[out] {}", result);
};
glob["MP"]["LogDebug"] = [this](const sol::variadic_args& args) {
auto result = print_impl(args);
beammp_lua_debugf("[out] {}", result);
};
glob["MP"]["GetPluginPath"] = [this] {
return std::filesystem::absolute(m_path).string();
};
glob["MP"]["ScheduleCallRepeat"] = [this](size_t ms, sol::function fn, sol::variadic_args args) {
return l_mp_schedule_call_repeat(ms, fn, args);
};
glob["MP"]["ScheduleCallOnce"] = [this](size_t ms, sol::function fn, sol::variadic_args args) {
l_mp_schedule_call_once(ms, fn, args);
};
glob["MP"]["CancelScheduledCall"] = [this](std::shared_ptr<Timer>& timer) {
// this has to be post()-ed, otherwise the call will not cancel (not sure why)
boost::asio::post(m_io, [this, timer] {
if (!timer) {
beammp_lua_errorf("uct.cancel_scheduled_call: timer already cancelled");
return;
}
beammp_lua_debugf("Cancelling timer");
cancel_timer(timer);
});
// release the lua's reference to this timer
timer.reset();
};
glob["MP"]["GetOSName"] = &LuaAPI::MP::GetOSName;
glob["MP"]["GetTimeMS"] = &LuaAPI::MP::GetTimeMS;
glob["MP"]["GetTimeS"] = &LuaAPI::MP::GetTimeS;
glob.create_named("Util");
glob["Util"]["JsonEncode"] = &LuaAPI::Util::JsonEncode;
glob["Util"]["JsonDiff"] = &LuaAPI::Util::JsonDiff;
glob["Util"]["JsonDiffApply"] = &LuaAPI::Util::JsonDiffApply;
glob["Util"]["JsonPrettify"] = &LuaAPI::Util::JsonPrettify;
glob["Util"]["JsonMinify"] = &LuaAPI::Util::JsonMinify;
glob["Util"]["JsonFlatten"] = &LuaAPI::Util::JsonFlatten;
glob["Util"]["JsonUnflatten"] = &LuaAPI::Util::JsonUnflatten;
glob["Util"]["JsonDecode"] = &LuaAPI::Util::JsonDecode;
glob.create_named("FS");
glob["FS"]["Exists"] = &LuaAPI::FS::Exists;
glob["FS"]["CreateDirectory"] = &LuaAPI::FS::CreateDirectory;
glob["FS"]["ConcatPaths"] = &LuaAPI::FS::ConcatPaths;
glob["FS"]["IsFile"] = &LuaAPI::FS::IsFile;
glob["FS"]["Remove"] = &LuaAPI::FS::Remove;
glob["FS"]["GetFilename"] = &LuaAPI::FS::GetFilename;
glob["FS"]["IsDirectory"] = &LuaAPI::FS::IsDirectory;
glob["FS"]["GetExtensinon"] = &LuaAPI::FS::GetExtension;
glob["FS"]["GetParentFolder"] = &LuaAPI::FS::GetParentFolder;
glob["FS"]["Copy"] = &LuaAPI::FS::Copy;
glob["FS"]["Rename"] = &LuaAPI::FS::Rename;
glob["FS"]["ListFiles"] = &LuaAPI::FS::ListFiles;
glob["FS"]["PathSep"] = fmt::format("{}", char(std::filesystem::path::preferred_separator));
return {};
}
Error LuaPlugin::load_files() {
// 1. look for main.lua, run that
// 2. look for extensions in extensions/, load those.
// make those globals based on the filename
// 3. call onInit by name (global)
auto extensions_folder = m_path / "extensions";
if (std::filesystem::exists(extensions_folder)
&& (std::filesystem::is_directory(extensions_folder)
|| std::filesystem::is_symlink(extensions_folder))) {
// TODO: Check that it points to a directory if its a symlink
beammp_lua_debugf("Found extensions/: {}", extensions_folder);
// load extensions from the folder, can't fail
auto n = load_extensions(extensions_folder);
beammp_lua_debugf("Loaded {} extensions.", n);
beammp_lua_debugf("Set up file watcher to watch extensions folder for changes");
// set up file watcher. this will watch for new extensions or for extensions which have
// changed (via modification time).
m_extensions_watcher.watch_files_in(extensions_folder);
// set up callback for when an extension changes.
// we simply reload the extension as if nothing happened :)
// TODO
/*
m_extensions_watch_conn = m_extensions_watcher.sig_file_changed.connect_scoped(
[this, extensions_folder](const std::filesystem::path& path) {
if (path.extension() != ".lua") {
return; // ignore
}
auto rel = std::filesystem::relative(path, extensions_folder).string();
rel = boost::algorithm::replace_all_copy(rel, "/", "_");
rel = boost::algorithm::replace_all_copy(rel, ".lua", "");
if (!check_name_validity(rel)) {
beammp_lua_errorf("Can't load/reload extension at path: {}. The resulting extension name would be invalid.", path);
} else {
load_extension(path, rel);
}
});
*/
} else {
beammp_lua_debugf("Plugin '{}' has no extensions.", name());
}
auto main_lua = m_path / "main.lua";
if (std::filesystem::exists(main_lua)) {
// TODO: Check that it's a regular file or symlink
beammp_lua_debugf("Found main.lua: {}", main_lua.string());
boost::asio::post(m_io, [this, main_lua] {
try {
m_state.safe_script_file(main_lua.string());
} catch (const std::exception& e) {
beammp_lua_errorf("Error running '{}': {}", main_lua.string(), e.what());
}
});
} else {
beammp_lua_warnf("No 'main.lua' found, a plugin should have a 'main.lua'.");
}
return {};
}
Error LuaPlugin::initialize_overrides() {
boost::asio::post(m_io, [this] {
m_state.globals()["print"] = [this](sol::variadic_args args) {
l_print(args);
};
});
Error err = fix_lua_paths();
if (err) {
return err;
}
return {};
}
Error LuaPlugin::fix_lua_paths() {
std::stringstream lua_paths;
std::stringstream lua_c_paths;
std::vector<std::filesystem::path> relevant_paths = {
m_path,
m_path / "extensions",
};
for (const auto& Path : relevant_paths) {
lua_paths << ";" << (Path / "?.lua").string();
lua_paths << ";" << (Path / "lua/?.lua").string();
#if WIN32
lua_c_paths << ";" << (Path / "?.dll").string();
lua_c_paths << ";" << (Path / "lib/?.dll").string();
#else // unix
lua_c_paths << ";" << (Path / "?.so").string();
lua_c_paths << ";" << (Path / "lib/?.so").string();
#endif
}
auto package_table = m_state.globals().get<sol::table>("package");
package_table["path"] = package_table.get<std::string>("path") + lua_paths.str();
package_table["cpath"] = package_table.get<std::string>("cpath") + lua_c_paths.str();
m_state.globals()["package"] = package_table;
return {};
}
void LuaPlugin::load_extension(const std::filesystem::path& file, const std::string& ext_name) {
// we have to assume that load_extension may be called at any time, even to reload an existing extension.
// thus, it cannot make assumptions about the plugin's status or state.
beammp_lua_debugf("Loading extension '{}' from {}", ext_name, file);
// save the extension in a list to make it queryable
m_known_extensions->insert_or_assign(ext_name, file);
// extension names, generated by the caller, must be valid lua identifiers
if (!check_name_validity(ext_name)) {
beammp_lua_errorf("Extension name '{}' is invalid. Please make sure the extension and it's folder(s) do not contain special characters, spaces, start with a digit, or similar.", ext_name);
return;
}
try {
auto result = m_state.safe_script_file(file.string());
if (!result.valid()) {
beammp_lua_errorf("Error loading extension '{}' from {}. Running file resulted in invalid state: {}. Please check for errors in the lines before this message", ext_name, file, sol::to_string(result.status()));
return;
} else if (result.get_type() != sol::type::table) {
beammp_lua_errorf("Error loading extension '{}' from {}: Expected extension to return a table, got {} instead.", ext_name, file, sol::type_name(m_state.lua_state(), result.get_type()));
return;
}
auto M = result.get<sol::table>();
m_state.globals()[ext_name] = M;
} catch (const std::exception& e) {
beammp_lua_errorf("Error loading extension '{}' from {}: {}", ext_name, file, e.what());
return;
}
beammp_lua_debugf("Extension '{}' loaded!", ext_name);
}
size_t LuaPlugin::load_extensions(const std::filesystem::path& extensions_folder, const std::string& base) {
std::filesystem::directory_iterator iter(extensions_folder, s_directory_iteration_options);
std::vector<std::filesystem::directory_entry> files;
std::vector<std::filesystem::directory_entry> directories;
for (const auto& entry : iter) {
if (entry.is_directory()) {
directories.push_back(entry);
} else if (entry.is_regular_file()) {
files.push_back(entry);
} else {
beammp_lua_tracef("{} is neither a file nor a directory, skipping", entry.path());
}
}
// sort files alphabetically
std::sort(files.begin(), files.end(), [&](const auto& a, const auto& b) {
auto as = a.path().filename().string();
auto bs = b.path().filename().string();
return std::lexicographical_compare(as.begin(), as.end(), bs.begin(), bs.end());
});
for (const auto& file : files) {
boost::asio::post(m_io, [this, base, file] {
std::string ext_name;
if (base.empty()) {
ext_name = file.path().stem().string();
} else {
ext_name = fmt::format("{}_{}", base, file.path().stem().string());
}
load_extension(file, ext_name);
});
}
std::sort(directories.begin(), directories.end(), [&](const auto& a, const auto& b) {
auto as = a.path().filename().string();
auto bs = b.path().filename().string();
return std::lexicographical_compare(as.begin(), as.end(), bs.begin(), bs.end());
});
size_t count = 0;
for (const auto& dir : directories) {
std::string ext_name = dir.path().filename().string();
std::filesystem::path path = dir.path();
count += load_extensions(path, ext_name);
}
return count + files.size();
}
void LuaPlugin::thread_main() {
RegisterThread(name());
beammp_lua_debugf("Waiting for initialization");
// wait for interruption
// we sleep for some time, which can be interrupted by a thread.interrupt(),
// which will cause the sleep to throw a boost::thread_interrupted exception.
// we (ab)use this to synchronize.
try {
boost::this_thread::sleep_for(boost::chrono::seconds(1));
} catch (boost::thread_interrupted) {
}
beammp_lua_debugf("Initialized!");
while (!*m_shutdown) {
auto ran = m_io.run_for(std::chrono::seconds(5));
// update the memory used by the Lua Plugin, then immediately resume execution of handlers
if (ran != 0) {
*m_memory = m_state.memory_used();
}
}
}
Error LuaPlugin::initialize() {
Error err = initialize_error_handlers();
if (err) {
return { "Failed to initialize error handlers: {}", err };
}
err = initialize_libraries();
if (err) {
return { "Failed to initialize libraries: {}", err };
}
err = initialize_overrides();
if (err) {
return { "Failed to initialize overrides: {}", err };
}
err = load_files();
if (err) {
return { "Failed to load initial files: {}", err };
}
// interrupt the thread, signalling it to start
m_thread.interrupt();
return {};
}
Error LuaPlugin::cleanup() {
// TODO
return {};
}
Error LuaPlugin::reload() {
// TODO
return {};
}
std::string LuaPlugin::name() const {
return m_path.stem().string();
}
std::filesystem::path LuaPlugin::path() const {
return m_path;
}
std::shared_future<std::optional<Value>> LuaPlugin::handle_event(const std::string& event_name, const std::shared_ptr<Value>& args) {
std::shared_ptr<std::promise<std::optional<Value>>> promise = std::make_shared<std::promise<std::optional<Value>>>();
std::shared_future<std::optional<Value>> future { promise->get_future() };
boost::asio::post(m_io, [this, event_name, args, promise, future] {
try {
if (m_state.globals()[event_name].valid() && m_state.globals()[event_name].get_type() == sol::type::function) {
auto fn = m_state.globals().get<sol::protected_function>(event_name);
fn.set_error_handler(m_state.globals()[ERR_HANDLER]);
auto lua_args = boost::apply_visitor(ValueToLuaVisitor(m_state), *args);
sol::protected_function_result res;
if (args->which() == VALUE_TYPE_IDX_TUPLE) {
res = fn(sol::as_args(lua_args.as<std::vector<sol::object>>()));
} else {
res = fn(lua_args);
}
if (res.valid()) {
auto maybe_res = sol_obj_to_value(res.get<sol::object>());
if (maybe_res) {
promise->set_value(maybe_res.move());
return;
} else {
beammp_lua_errorf("Error using return value from event handler '{}': {}", event_name, maybe_res.error);
}
}
} else { // TODO: CONTINUE HERE
beammp_lua_tracef("No handler for event '{}'", event_name);
}
} catch (const std::exception& e) {
beammp_lua_errorf("Error finding and running event handler for event '{}': {}. It was called with argument(s): {}", event_name, e.what(), boost::apply_visitor(ValueToStringVisitor(), *args));
}
promise->set_value(std::nullopt);
});
return future;
}
size_t LuaPlugin::memory_usage() const {
return *m_memory;
}
std::shared_ptr<Timer> LuaPlugin::make_timer(size_t ms) {
m_timers.push_back(std::make_shared<Timer>(boost::asio::deadline_timer(m_io), ms));
auto timer = m_timers.back();
timer->timer.expires_from_now(timer->interval);
std::sort(m_timers.begin(), m_timers.end());
return timer;
}
void LuaPlugin::cancel_timer(const std::shared_ptr<Timer>& timer) {
auto iter = std::find(m_timers.begin(), m_timers.end(), timer);
if (iter != m_timers.end()) {
m_timers.erase(iter);
timer->timer.cancel();
} else {
timer->timer.cancel();
beammp_lua_debugf("Failed to remove timer (already removed)");
}
}
void LuaPlugin::l_print(const sol::variadic_args& args) {
auto result = print_impl(args);
beammp_lua_infof("{}", result);
}
void LuaPlugin::l_mp_schedule_call_helper(const boost::system::error_code& err, std::shared_ptr<Timer> timer, const sol::function& fn, std::shared_ptr<ValueTuple> args) {
if (err) {
beammp_lua_debugf("uct.schedule_call_repeat: {}", err.what());
return;
}
timer->timer.expires_from_now(timer->interval);
sol::protected_function prot(fn);
prot.set_error_handler(m_state.globals()[ERR_HANDLER]);
std::vector<sol::object> objs;
objs.reserve(args->size());
for (const auto& val : *args) {
objs.push_back(boost::apply_visitor(ValueToLuaVisitor(m_state), val));
}
prot(sol::as_args(objs));
timer->timer.async_wait([this, timer, fn, args](const auto& err) {
l_mp_schedule_call_helper(err, timer, fn, args);
});
}
void LuaPlugin::l_mp_schedule_call_once(size_t ms, const sol::function& fn, sol::variadic_args args) {
auto timer = make_timer(ms);
std::vector<sol::object> args_vec(args.begin(), args.end());
std::shared_ptr<ValueTuple> tuple = std::make_shared<ValueTuple>();
tuple->reserve(args_vec.size());
for (const auto& obj : args_vec) {
auto res = sol_obj_to_value(obj);
if (res) [[likely]] {
tuple->emplace_back(res.move());
} else {
beammp_lua_errorf("Can't serialize an argument across boundaries (for passing to a uct.schedule_call_* later): ", res.error);
tuple->emplace_back(Null {});
}
}
timer->timer.async_wait([this, timer, fn, tuple](const auto& err) {
if (err) {
beammp_lua_debugf("uct.schedule_call_once: {}", err.what());
return;
}
sol::protected_function prot(fn);
prot.set_error_handler(m_state.globals()[ERR_HANDLER]);
std::vector<sol::object> objs;
objs.reserve(tuple->size());
for (const auto& val : *tuple) {
objs.push_back(boost::apply_visitor(ValueToLuaVisitor(m_state), val));
}
beammp_lua_debugf("Calling with {} args", objs.size());
prot(sol::as_args(objs));
cancel_timer(timer);
});
}
std::shared_ptr<Timer> LuaPlugin::l_mp_schedule_call_repeat(size_t ms, const sol::function& fn, sol::variadic_args args) {
auto timer = make_timer(ms);
// TODO: Cleanly transfer invalid objects
std::vector<sol::object> args_vec(args.begin(), args.end());
std::shared_ptr<ValueTuple> tuple = std::make_shared<ValueTuple>();
tuple->reserve(args_vec.size());
for (const auto& obj : args_vec) {
auto res = sol_obj_to_value(obj);
if (res) [[likely]] {
tuple->emplace_back(res.move());
} else {
beammp_lua_errorf("Can't serialize an argument across boundaries (for passing to a uct.schedule_call_* later): ", res.error);
tuple->emplace_back(Null {});
}
}
timer->timer.async_wait([this, timer, fn, tuple](const auto& err) {
l_mp_schedule_call_helper(err, timer, fn, tuple);
});
return timer;
}
std::string LuaPlugin::print_impl(const sol::variadic_args& args) {
auto obj_args = std::vector<sol::object>(args.begin(), args.end());
std::string result {};
result.reserve(500);
// used as the invalid value provider in sol_obj_to_value.
auto special_stringifier = [](const sol::object& object) -> Result<Value> {
switch (object.get_type()) {
case sol::type::none:
case sol::type::lua_nil:
case sol::type::string:
case sol::type::number:
case sol::type::boolean:
case sol::type::table:
// covered by value to string visitor
break;
case sol::type::thread:
return { fmt::format("<<lua thread: {:p}>>", object.pointer()) };
case sol::type::function:
return { fmt::format("<<lua function: {:p}>>", object.pointer()) };
case sol::type::userdata:
return { fmt::format("<<lua userdata: {:p}>>", object.pointer()) };
case sol::type::lightuserdata:
return { fmt::format("<<lua lightuserdata: {:p}>>", object.pointer()) };
case sol::type::poly:
return { fmt::format("<<lua poly: {:p}>>", object.pointer()) };
default:
break;
}
return { fmt::format("<<lua unknown type: {:p}>>", object.pointer()) };
};
for (const auto& obj : obj_args) {
auto maybe_val = sol_obj_to_value(obj, special_stringifier);
if (maybe_val) {
result += boost::apply_visitor(ValueToStringVisitor(ValueToStringVisitor::Flag::NONE), maybe_val.move());
result += " ";
} else {
beammp_lua_errorf("Failed to print() an argument: {}", maybe_val.error);
}
}
return result;
}

470
src/Value.cpp Normal file
View File

@@ -0,0 +1,470 @@
#include "Value.h"
#include "boost/json/value_from.hpp"
#include "sol/as_args.hpp"
#include "sol/types.hpp"
#include <doctest/doctest.h>
#include <fmt/format.h>
#include <sol/sol.hpp>
ValueToStringVisitor::ValueToStringVisitor(Flag flags, int depth)
: m_quote_strings(flags & QUOTE_STRINGS)
, m_depth(depth) {
}
std::string ValueToStringVisitor::operator()(const std::string& str) const {
if (m_quote_strings) {
return fmt::format("\"{}\"", str);
} else {
return str;
}
}
std::string ValueToStringVisitor::operator()(int64_t i) const {
return fmt::format("{}", i);
}
std::string ValueToStringVisitor::operator()(double d) const {
return fmt::format("{}", d);
}
std::string ValueToStringVisitor::operator()(Null) const {
return "null";
}
std::string ValueToStringVisitor::operator()(Bool b) const {
return b ? "true" : "false";
}
std::string ValueToStringVisitor::operator()(const ValueArray& array) const {
std::string res = "[ ";
size_t i = 0;
for (const auto& elem : array) {
res += fmt::format("\n{:>{}}{}", "", m_depth * 2, boost::apply_visitor(ValueToStringVisitor(QUOTE_STRINGS, m_depth + 1), elem));
if (i + 2 <= array.size()) {
res += ",";
} else {
res += "\n";
}
++i;
}
return res += fmt::format("{:>{}}]", "", (m_depth == 0 ? 0 : (m_depth - 1) * 2));
}
std::string ValueToStringVisitor::operator()(const ValueTuple& array) const {
std::string res = "( ";
size_t i = 0;
for (const auto& elem : array) {
res += fmt::format("\n{:>{}}{}", "", m_depth * 2, boost::apply_visitor(ValueToStringVisitor(QUOTE_STRINGS, m_depth + 1), elem));
if (i + 2 <= array.size()) {
res += ",";
} else {
res += "\n";
}
++i;
}
return res += fmt::format("{:>{}})", "", (m_depth == 0 ? 0 : (m_depth - 1) * 2));
}
std::string ValueToStringVisitor::operator()(const HashMap<std::string, Value>& map) const {
std::string res = "{ ";
size_t i = 0;
for (const auto& [key, value] : map) {
res += fmt::format("\n{:>{}}{}: {}", "", m_depth * 2, key, boost::apply_visitor(ValueToStringVisitor(QUOTE_STRINGS, m_depth + 1), value));
if (i + 2 <= map.size()) {
res += ",";
} else {
res += "\n";
}
++i;
}
return res += fmt::format("{:>{}}}}", "", (m_depth == 0 ? 0 : (m_depth - 1) * 2));
}
TEST_CASE("Value constructors") {
SUBCASE("string via const char*") {
Value value = "hello, world";
CHECK_EQ(value.which(), VALUE_TYPE_IDX_STRING);
}
SUBCASE("int via long literal") {
Value value = int64_t(1l);
CHECK_EQ(value.which(), VALUE_TYPE_IDX_INT);
}
SUBCASE("double via double literal") {
Value value = 5.432;
CHECK_EQ(value.which(), VALUE_TYPE_IDX_DOUBLE);
}
// other constructors must be explicit as far as we're concerned
}
TEST_CASE("ValueToStringVisitor") {
SUBCASE("string quoted") {
Value value = "hello, world";
// expected to implicitly be "ValueToStringVisitor::Flag::QUOTE_STRINGS"
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "\"hello, world\"");
}
SUBCASE("string not quoted") {
Value value = "hello, world";
std::string res = boost::apply_visitor(ValueToStringVisitor(ValueToStringVisitor::Flag::NONE), value);
CHECK_EQ(res, "hello, world");
}
SUBCASE("int") {
Value value = int64_t(123456789l);
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "123456789");
}
SUBCASE("negative int") {
Value value = int64_t(-123456789l);
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "-123456789");
}
SUBCASE("whole integer double") {
Value value = 123456789.0;
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "123456789");
}
SUBCASE("double") {
Value value = 1234.56789;
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "1234.56789");
}
SUBCASE("null") {
Value value = Null {};
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "null");
} /*
SUBCASE("array") {
Value value = ValueArray { 1l, 2l, "hello", 5.4 };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "[ 1, 2, \"hello\", 5.4 ]");
}
SUBCASE("tuple") {
Value value = ValueArray { 1l, 2l, "hello" };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "( 1, 2, \"hello\" )");
}
SUBCASE("array with array inside") {
Value value = ValueArray { 1l, 2l, "hello", ValueArray { -1l, -2l }, 5.4 };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "[ 1, 2, \"hello\", [ -1, -2 ], 5.4 ]");
}
SUBCASE("map") {
Value value = ValueHashMap { { "hello", "world" }, { "x", 53.5 } };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, R"({ hello="world", x=53.5 })");
}
SUBCASE("map with map inside") {
Value value = ValueHashMap { { "hello", "world" }, { "my map", ValueHashMap { { "value", 1l } } }, { "x", 53.5 } };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, R"({ hello="world", my map={ value=1 }, x=53.5 })");
}
*/
SUBCASE("empty array") {
Value value = ValueArray {};
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "[ ]");
}
SUBCASE("empty tuple") {
Value value = ValueTuple {};
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "( )");
}
SUBCASE("empty map") {
Value value = ValueHashMap {};
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "{ }");
}
SUBCASE("bool") {
Value value = Bool { false };
std::string res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "false");
value = Bool { true };
res = boost::apply_visitor(ValueToStringVisitor(), value);
CHECK_EQ(res, "true");
}
}
Result<Value> sol_obj_to_value(const sol::object& obj, const std::function<Result<Value>(const sol::object&)>& invalid_provider) {
switch (obj.get_type()) {
case sol::type::none:
case sol::type::lua_nil:
return { Null {} };
case sol::type::string:
return { obj.as<std::string>() };
case sol::type::number:
if (obj.is<int64_t>()) {
return { obj.as<int64_t>() };
} else {
return { obj.as<double>() };
}
case sol::type::thread: {
if (invalid_provider) {
return invalid_provider(obj);
} else {
return Result<Value>("Can't convert {} to Value", "lua thread");
}
}
case sol::type::boolean:
return { Bool { obj.as<bool>() } };
case sol::type::function:
if (invalid_provider) {
return invalid_provider(obj);
} else {
return Result<Value>("Can't convert {} to Value", "lua function");
}
case sol::type::userdata:
if (invalid_provider) {
return invalid_provider(obj);
} else {
return Result<Value>("Can't convert {} to Value", "lua userdata");
}
case sol::type::lightuserdata:
if (invalid_provider) {
return invalid_provider(obj);
} else {
return Result<Value>("Can't convert {} to Value", "lua lightuserdata");
}
case sol::type::table: {
bool is_map = false;
// look through all keys, if all are numbers, its not a map
for (const auto& [k, v] : obj.as<sol::table>()) {
// due to a quirk in lua+sol (sol issue #247), you have to
// both check that it's a number, but also that its an integer number.
// technically, lua allows float keys in an array (i guess?)
// but for us that counts as a type we need to use a hashmap for.
// k.is<double>() would be true for any number type, but
// k.is<int>() is only true for such numbers that are non-float.
if (k.get_type() == sol::type::number) {
if (!k.is<int64_t>()) {
is_map = true;
break;
}
} else if (k.get_type() == sol::type::string) {
is_map = true;
break;
} else {
return Result<Value>("Can't use non-string and non-number object as key for table (type id is {})", int(k.get_type())); // TODO: make a fucntion to fix htis messy way to enfore sending an error
}
}
if (is_map) {
ValueHashMap map;
for (const auto& [k, v] : obj.as<sol::table>()) {
std::string key;
if (k.get_type() == sol::type::number) {
if (k.is<int64_t>()) {
key = fmt::format("{}", k.as<int64_t>());
} else {
key = fmt::format("{:F}", k.as<double>());
}
} else if (k.get_type() == sol::type::string) {
key = k.as<std::string>();
} else {
return Result<Value>("Failed to construct hash-map: Can't use non-string and non-number object as key for table{}", "");
}
auto maybe_val = sol_obj_to_value(v, invalid_provider);
if (maybe_val) [[likely]] {
map.emplace(key, maybe_val.move());
} else {
return maybe_val; // error
}
}
return { std::move(map) };
} else {
ValueArray array;
for (const auto& [k, v] : obj.as<sol::table>()) {
auto i = k.as<int64_t>() - 1; // -1 due to lua arrays starting at 1
if (size_t(i) >= array.size()) {
array.resize(size_t(i) + 1, Null {});
}
auto maybe_val = sol_obj_to_value(v, invalid_provider);
if (maybe_val) [[likely]] {
array[size_t(i)] = maybe_val.move();
} else {
return maybe_val; // error
}
}
return { std::move(array) };
}
}
case sol::type::poly:
if (invalid_provider) {
return invalid_provider(obj);
} else {
return Result<Value>("Can't convert {} to Value", "lua poly");
}
default:
break;
}
return Result<Value>("Unknown type, can't convert to value.{}", "");
}
TEST_CASE("sol_obj_to_value") {
sol::state state {};
SUBCASE("nil") {
sol::table obj = state.create_table();
sol::object o = obj.get<sol::object>(0);
CHECK_EQ(sol_obj_to_value(o).value().which(), VALUE_TYPE_IDX_NULL);
}
SUBCASE("string") {
auto res = sol::make_object(state.lua_state(), "hello");
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_STRING);
auto val = sol_obj_to_value(res).value();
auto str = boost::get<std::string>(val);
CHECK_EQ(str, "hello");
}
SUBCASE("int") {
auto res = sol::make_object(state.lua_state(), 1234l);
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_INT);
auto val = sol_obj_to_value(res).value();
auto i = boost::get<int64_t>(val);
CHECK_EQ(i, 1234l);
}
SUBCASE("double") {
auto res = sol::make_object(state.lua_state(), 53.3);
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_DOUBLE);
auto val = sol_obj_to_value(res).value();
auto d = boost::get<double>(val);
CHECK_EQ(d, 53.3);
}
SUBCASE("bool") {
auto res = sol::make_object(state.lua_state(), true);
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_BOOL);
auto val = sol_obj_to_value(res).value();
auto d = boost::get<Bool>(val);
CHECK_EQ(bool(d), true);
}
SUBCASE("table (map)") {
auto res = state.create_table_with(
"hello", "world",
"x", 35l);
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_HASHMAP);
auto val = sol_obj_to_value(res).value();
auto m = boost::get<ValueHashMap>(val);
CHECK(m.contains("hello"));
CHECK(m.contains("x"));
CHECK_EQ(boost::get<std::string>(m["hello"]), "world");
CHECK_EQ(boost::get<int64_t>(m["x"]), 35l);
}
SUBCASE("table (array)") {
auto res = state.create_table_with(
1, 1,
2, 2,
3, 3,
6, 6);
CHECK_EQ(sol_obj_to_value(res).value().which(), VALUE_TYPE_IDX_ARRAY);
auto val = sol_obj_to_value(res).value();
auto m = boost::get<ValueArray>(val);
CHECK_EQ(boost::get<int64_t>(m[0]), 1);
CHECK_EQ(boost::get<int64_t>(m[1]), 2);
CHECK_EQ(boost::get<int64_t>(m[2]), 3);
CHECK_EQ(m[3].which(), VALUE_TYPE_IDX_NULL);
CHECK_EQ(m[4].which(), VALUE_TYPE_IDX_NULL);
CHECK_EQ(boost::get<int64_t>(m[5]), 6);
}
// TODO: add test for all the invalid types
}
std::ostream& operator<<(std::ostream& os, const Null&) {
return os << "null";
}
std::ostream& operator<<(std::ostream& os, const Bool& b) {
return os << (b ? "true" : "false");
}
boost::json::value ValueToJsonVisitor::operator()(const std::string& str) const {
return boost::json::value_from(str);
}
boost::json::value ValueToJsonVisitor::operator()(int64_t i) const {
return boost::json::value_from(i);
}
boost::json::value ValueToJsonVisitor::operator()(double d) const {
return boost::json::value_from(d);
}
boost::json::value ValueToJsonVisitor::operator()(Null) const {
return boost::json::value_from(nullptr);
}
boost::json::value ValueToJsonVisitor::operator()(Bool b) const {
return boost::json::value_from(b.b);
}
boost::json::value ValueToJsonVisitor::operator()(const ValueArray& array) const {
auto ja = boost::json::array();
for (const auto& val : array) {
auto obj = boost::apply_visitor(ValueToJsonVisitor(), val);
ja.push_back(obj);
}
return ja;
}
boost::json::value ValueToJsonVisitor::operator()(const ValueTuple& tuple) const {
auto ja = boost::json::array();
for (const auto& val : tuple) {
auto obj = boost::apply_visitor(ValueToJsonVisitor(), val);
ja.push_back(obj);
}
return ja;
}
boost::json::value ValueToJsonVisitor::operator()(const HashMap<std::string, Value>& map) const {
auto jo = boost::json::object();
for (const auto& [key, val] : map) {
auto json_val = boost::apply_visitor(ValueToJsonVisitor(), val);
jo.emplace(key, json_val);
}
return jo;
}
ValueToLuaVisitor::ValueToLuaVisitor(sol::state& state)
: m_state(state) {
}
sol::object ValueToLuaVisitor::operator()(const std::string& str) const {
return sol::make_object(m_state.lua_state(), str);
}
sol::object ValueToLuaVisitor::operator()(int64_t i) const {
return sol::make_object(m_state.lua_state(), i);
}
sol::object ValueToLuaVisitor::operator()(double d) const {
return sol::make_object(m_state.lua_state(), d);
}
sol::object ValueToLuaVisitor::operator()(Null) const {
return sol::lua_nil_t();
}
sol::object ValueToLuaVisitor::operator()(Bool b) const {
return sol::make_object(m_state.lua_state(), b.b);
}
sol::object ValueToLuaVisitor::operator()(const ValueArray& array) const {
auto table = m_state.create_table();
for (const auto& val : array) {
table.add(boost::apply_visitor(*this, val));
}
return table;
}
sol::object ValueToLuaVisitor::operator()(const ValueTuple& tuple) const {
auto table = m_state.create_table();
for (const auto& val : tuple) {
table.add(boost::apply_visitor(*this, val));
}
return table;
}
sol::object ValueToLuaVisitor::operator()(const HashMap<std::string, Value>& map) const {
auto table = m_state.create_table();
for (const auto& [key, val] : map) {
table.set(key, boost::apply_visitor(*this, val));
}
return table;
}