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

View File

@@ -65,7 +65,7 @@ public:
std::string Resource { "Resources" };
std::string MapName { "/levels/gridmap_v2/info.json" };
std::string Key {};
std::string Password{};
std::string Password {};
std::string SSLKeyPath { "./.ssl/HttpServer/key.pem" };
std::string SSLCertPath { "./.ssl/HttpServer/cert.pem" };
bool HTTPServerEnabled { false };
@@ -218,6 +218,16 @@ void RegisterThread(const std::string& str);
do { \
Application::Console().Write(_this_location + std::string("[LUA WARN] ") + (x)); \
} while (false)
#define beammp_lua_info(x) \
do { \
Application::Console().Write(_this_location + std::string("[LUA INFO] ") + (x)); \
} while (false)
#define beammp_lua_debug(x) \
do { \
if (Application::Settings.DebugModeEnabled) { \
Application::Console().Write(_this_location + std::string("[LUA DEBUG] ") + (x)); \
} \
} while (false)
#define luaprint(x) Application::Console().Write(_this_location + std::string("[LUA] ") + (x))
#define beammp_debug(x) \
do { \
@@ -248,8 +258,6 @@ void RegisterThread(const std::string& str);
#define beammp_debugf(...) beammp_debug(fmt::format(__VA_ARGS__))
#define beammp_warnf(...) beammp_warn(fmt::format(__VA_ARGS__))
#define beammp_tracef(...) beammp_trace(fmt::format(__VA_ARGS__))
#define beammp_lua_errorf(...) beammp_lua_error(fmt::format(__VA_ARGS__))
#define beammp_lua_warnf(...) beammp_lua_warn(fmt::format(__VA_ARGS__))
#else // DOCTEST_CONFIG_DISABLE

120
include/Error.h Normal file
View File

@@ -0,0 +1,120 @@
#pragma once
#include <fmt/core.h>
#include <optional>
#include <string>
/// The Error class represents an error or the absence of an
/// error. It behaves like a bool, depending on context.
///
/// The idea is to use this class to pass around errors, together with
/// [[nodiscard]], in order to make errors displayable for the users and
/// to give errors some context. The only way to construct an error is to come
/// up with an error message with this class, so this is an attempt to enforce
/// this.
///
/// A default constructed Error means "no error" / "success", while
/// the only available non-default constructor is one which takes any format.
/// For example:
///
/// \code{.cpp}
/// Error myfunc() {
/// if (ok) {
/// return {}; // no error
/// } else {
/// return Error("Something went wrong: {}", 42); // error
/// }
/// }
///
/// // ... handling:
///
/// Error err = myfunc();
/// if (err) {
/// // error occurred
/// l::error("Error running myfunc: {}", err.error);
/// } else {
/// // ok
/// }
/// \endcode
struct Error {
/// Constructs a "non-error" / empty error, which is not considered
/// to be an error. Use this as the "no error occurred" return value.
Error() = default;
/// Constructs an error with a message. Accepts fmt::format() arguments.
///
/// Example:
///
/// \code{.cpp}
/// // format with fmt (automatically), all arguments are forwarded to fmt::format
/// return Error("failed to open '{}': {}", file, error);
/// // or just as a constexpr string
/// return Error("failed to open file");
/// \endcode
template<typename... Args>
Error(fmt::format_string<Args...> s, Args&&... args)
: is_error(true)
, error(fmt::format(s, std::forward<Args>(args)...)) { }
/// Whether this error represents an error (true) or success (false).
/// Use operator bool() instead of reading this if possible.
bool is_error { false };
/// The error message. Is a valid string even if is_error is false, but will
/// be "Success".
std::string error { "Success" };
/// Implicit conversion to boolean.
/// True if this Error contains an error, false if not.
operator bool() const { return is_error; }
};
// TODO: Add docs
template<typename T>
struct Result {
/// Constructs an error-value result.
/// Currently, you may have to force this by passing a second
/// empty string argument.
template<typename... Args>
Result(fmt::format_string<Args...> s, Args&&... args)
: is_error(true)
, error(fmt::format(s, std::forward<Args>(args)...)) { }
/// Constructs a value-result via an explicit type.
template<typename S>
Result(S&& value)
: result(std::move(value)) {
}
/// Constructs a value-result via an explicit type.
template<typename S>
explicit Result(const S& value)
: result(value) {
}
/// Constructs a value-result via an implicit conversion.
Result(T&& value)
: result(std::move(value)) {
}
/// Constructs a value-result via an implicit conversion.
explicit Result(const T& value)
: result(value) {
}
/// Converts to bool in context. If it has an error, its considered "not a result",
/// so it returns true only if a value is contained.
operator bool() const { return !is_error; }
/// Accesses the value contained by moving it out.
T&& move() { return std::move(result.value()); }
/// Accesses the value contained by const reference.
const T& value() const { return result.value(); }
/// Holds the optional result value. On error, is nullopt.
std::optional<T> result;
/// Whether this result holds an error.
bool is_error { false };
/// Error message.
std::string error { "Success" };
};

90
include/FileWatcher.h Normal file
View File

@@ -0,0 +1,90 @@
#pragma once
/// @file
/// This file holds the FileWatcher interface.
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#include <boost/system/detail/error_code.hpp>
#include <boost/thread/scoped_thread.hpp>
#include <boost/thread/synchronized_value.hpp>
#include <filesystem>
#include <functional>
#include <unordered_map>
#include <unordered_set>
/// The FileWatcher class watches a directory or a file for changes,
/// and then notifies the caller through a signal.
///
/// This is a pretty convoluted implementation, and you may find it difficult
/// to read. This is not intentional, but simplifying this would
/// cost more time than to write this excuse.
///
/// It operates as follows:
///
/// A boost::asio::deadline_timer is waited on asynchronously.
/// Once expired, this timer calls FileWatcher::on_tick.
/// That function then loops through all registered files and directories,
/// taking great care to follow symlinks, and tries to find a file which has changed.
/// It determines this by storing the last known modification time.
/// Once a file is found which has a new modification time, the FileWatcher::sig_file_changed
/// signal is fired, and all connected slots must take care to handle the signal.
class FileWatcher {
public:
/// Constructs the FileWatcher to watch the given files every few seconds, as
/// specified by the seconds argument.
FileWatcher(unsigned seconds);
/// Stops the thread via m_shutdown.
~FileWatcher();
/// Add a file to watch. If this file changes, FileWatcher::sig_file_changed is triggered
/// with the path to the file.
void watch_file(const std::filesystem::path& path);
/// Add a directory to watch. If any file in the directory, or any subdirectories, change,
/// FileWatcher::sig_file_changed is triggered.
void watch_files_in(const std::filesystem::path& dir);
private:
/// Entry point for the timer thread.
void thread_main();
/// Called every time the timer runs out, watches for file changes, then starts
/// a new timer.
void on_tick(const boost::system::error_code&);
/// Checks files for changes, calls FileWatcher::sig_file_changed on change.
void check_files();
/// Checks directories for files which changed, calls FileWatcher::sig_file_changed on change.
void check_directories();
/// Checks a single file for change.
void check_file(const std::filesystem::path& file);
/// Interval in seconds for the timer. Needed to be able to restart the timer over and over.
boost::synchronized_value<boost::posix_time::seconds> m_seconds;
/// If set, the thread handling the file watching will shut down. Set in the destructor.
boost::synchronized_value<bool> m_shutdown { false };
/// Io context handles the scheduling of timers on the thread.
boost::asio::io_context m_io {};
/// Holds all files that are to be checked.
///
/// It uses a boost::hash<> because in the original C++17
/// standard, std::hash of a filesystem path was not defined, and as such
/// some implementations still don't have it.
/// See https://cplusplus.github.io/LWG/issue3657
boost::synchronized_value<std::unordered_set<std::filesystem::path, boost::hash<std::filesystem::path>>> m_files {};
/// Holds all the directories that are to be searched for files to be checked.
///
/// See FileWatcher::m_files for an explanation for the boost::hash.
boost::synchronized_value<std::unordered_set<std::filesystem::path, boost::hash<std::filesystem::path>>> m_dirs {};
/// Holds the last known modification times of all found files.
std::unordered_map<std::filesystem::path, std::filesystem::file_time_type, boost::hash<std::filesystem::path>> m_file_mod_times {};
/// Timer used to time the checks. Restarted every FileWatcher::m_seconds seconds.
boost::synchronized_value<boost::asio::deadline_timer> m_timer;
/// Work guard helps the io_context "sleep" while there is no work to be done - must be reset in the
/// destructor in order to not cause work to be thrown away (though in this case we probably don't care).
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> m_work_guard = boost::asio::make_work_guard(m_io);
/// Thread on which all watching and timing work runs.
boost::scoped_thread<> m_thread;
};

22
include/HashMap.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
/// @file
/// HashMap holds hash map implementations and typedefs.
///
/// The idea is that we can easily swap out the implementation
/// in case there is a performance or memory usage concern.
#include <boost/container/flat_map.hpp>
#include <boost/thread/synchronized_value.hpp>
/// A hash map to be used for any kind of small number of key-value pairs.
/// Iterators and pointers may be invalidated on modification.
template<typename K, typename V>
using HashMap = boost::container::flat_map<K, V>;
/// A synchronized hash map is a hash map in which each
/// access is thread-safe. In this case, this is achieved by locking
/// each access with a mutex (which often ends up being a futex in the implementation).
template<typename K, typename V>
using SynchronizedHashMap = boost::synchronized_value<boost::container::flat_map<K, V>>;

View File

@@ -18,30 +18,33 @@
#pragma once
#include "TLuaEngine.h"
#include <sol/sol.hpp>
#include <string>
#include <tuple>
#include <utility>
namespace LuaAPI {
int PanicHandler(lua_State* State);
std::string LuaToString(const sol::object Value, size_t Indent = 1, bool QuoteStrings = false);
void Print(sol::variadic_args);
namespace MP {
extern TLuaEngine* Engine;
std::string GetOSName();
std::tuple<int, int, int> GetServerVersion();
std::pair<bool, std::string> TriggerClientEvent(int PlayerID, const std::string& EventName, const sol::object& Data);
std::pair<bool, std::string> TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data);
inline size_t GetPlayerCount() { return Engine->Network()->authenticated_client_count(); }
size_t GetPlayerCount();
std::pair<bool, std::string> DropPlayer(int ID, std::optional<std::string> MaybeReason);
std::pair<bool, std::string> SendChatMessage(int ID, const std::string& Message);
std::pair<bool, std::string> RemoveVehicle(int PlayerID, int VehicleID);
void Set(int ConfigID, sol::object NewValue);
bool IsPlayerGuest(int ID);
bool IsPlayerConnected(int ID);
void Sleep(size_t Ms);
void PrintRaw(sol::variadic_args);
std::string JsonEncode(const sol::table& object);
/// Returns the current time in millisecond accuracy.
size_t GetTimeMS();
/// Returns the current time in seconds, with millisecond accuracy (w/ decimal point).
double GetTimeS();
}
namespace Util {
std::string JsonEncode(const sol::object& object);
sol::table JsonDecode(sol::this_state s, const std::string& string);
std::string JsonDiff(const std::string& a, const std::string& b);
std::string JsonDiffApply(const std::string& data, const std::string& patch);
std::string JsonPrettify(const std::string& json);
@@ -62,5 +65,7 @@ namespace FS {
bool IsDirectory(const std::string& Path);
bool IsFile(const std::string& Path);
std::string ConcatPaths(sol::variadic_args Args);
sol::table ListFiles(sol::this_state s, const std::string& path);
sol::table ListDirectories(sol::this_state s, const std::string& path);
}
}

129
include/LuaPlugin.h Normal file
View File

@@ -0,0 +1,129 @@
#pragma once
#include "Common.h"
#include "FileWatcher.h"
#include "HashMap.h"
#include "Plugin.h"
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/io_service.hpp>
#include <boost/asio/post.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#include <boost/thread/scoped_thread.hpp>
#include <boost/thread/synchronized_value.hpp>
#include <sol/variadic_args.hpp>
#include <spdlog/logger.h>
#include <sol/sol.hpp>
struct Timer {
Timer(boost::asio::deadline_timer&& timer_, long interval_)
: timer(std::move(timer_))
, interval(interval_) { }
boost::asio::deadline_timer timer;
boost::posix_time::milliseconds interval;
};
class LuaPlugin : public Plugin {
public:
/// Declare a new plugin with the path.
/// Loading of any files only happens on LuaPlugin::initialize().
LuaPlugin(const std::string& path);
/// Shuts down lua thread, may hang if there is still work to be done.
~LuaPlugin();
template <typename FnT>
void register_function(const std::string& table, const std::string& identifier, const FnT& func) {
boost::asio::post(m_io, [this, table, identifier, func] {
if (!m_state.globals()[table].valid()) {
m_state.globals().create_named(table);
}
if (m_state.globals()[table][identifier].valid()) {
beammp_errorf("Global '{}.{}' already exists and could not be injected as function.", table, identifier);
} else {
m_state.globals()[table][identifier] = func;
}
});
}
private:
/// Initializes the error handlers for panic and exceptions.
Error initialize_error_handlers();
/// Initializes / loads base packages and libraries.
Error initialize_libraries();
/// Loads main file of the plugin.
Error load_files();
/// Overrides functions such as `print()`
Error initialize_overrides();
/// Fixes lua's package.path and package.cpath to understand our own file structure better.
Error fix_lua_paths();
/// Loads an extension. Call this from the lua thread.
/// This function cannot fail, as it reports errors to the user.
void load_extension(const std::filesystem::path& file, const std::string& ext_name);
/// Loads all extension from the folder, using the base as a prefix.
/// This function is recursive.
/// Returns the amount of extensions found. This function cannot fail.
size_t load_extensions(const std::filesystem::path& extensions_folder, const std::string& base = "");
/// Entry point for the lua plugin's thread.
void thread_main();
// Plugin interface
public:
/// Initializes the Lua Plugin, loads file(s), starts executing code.
virtual Error initialize() override;
// TODO cleanup
virtual Error cleanup() override;
// TODO reload
virtual Error reload() override;
/// Name of this lua plugin (the base name of the folder).
virtual std::string name() const override;
/// Path to the folder containing this lua plugin.
virtual std::filesystem::path path() const override;
/// Dispatches the event to the thread which runs all lua.
virtual std::shared_future<std::optional<Value>> handle_event(const std::string& event_name, const std::shared_ptr<Value>& args) override;
/// Returns the memory usage of this thread, updated at the slowest every 5 seconds.
virtual size_t memory_usage() const override;
private:
/// Path to the plugin's root folder.
std::filesystem::path m_path;
/// Thread where all lua work must happen. Started within the constructor but is blocked until LuaPlugin::initialize is called
boost::scoped_thread<> m_thread;
/// This asio context schedules all tasks. It's run in the m_thread thread.
boost::asio::io_context m_io;
/// Main (and usually only) lua state of this plugin.
/// ONLY access this from the m_thread thread.
sol::state m_state;
/// Whether the lua thread should shutdown. Triggered by the LuaPlugin::~LuaPlugin dtor.
boost::synchronized_value<bool> m_shutdown { false };
/// Current memory usage. Cached to avoid having to synchronize access to the lua state.
boost::synchronized_value<size_t> m_memory { 0 };
/// Hash map of all event handlers in this state.
// HashMap<std::string, sol::protected_function> m_event_handlers {};
SynchronizedHashMap<std::string, std::filesystem::path> m_known_extensions {};
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> m_work_guard = boost::asio::make_work_guard(m_io);
/// Iteration options to be used whenever iterating over a directory in this class.
static inline auto s_directory_iteration_options = std::filesystem::directory_options::follow_directory_symlink | std::filesystem::directory_options::skip_permission_denied;
FileWatcher m_extensions_watcher { 2 };
std::vector<std::shared_ptr<Timer>> m_timers {};
std::shared_ptr<Timer> make_timer(size_t ms);
void cancel_timer(const std::shared_ptr<Timer>& timer);
// Lua API
/// Override for lua's base.print().
/// Dumps tables, arrays, etc. properly.
void l_print(const sol::variadic_args&);
std::shared_ptr<Timer> l_mp_schedule_call_repeat(size_t ms, const sol::function& fn, sol::variadic_args args);
void 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);
void l_mp_schedule_call_once(size_t ms, const sol::function& fn, sol::variadic_args args);
std::string print_impl(const sol::variadic_args&);
};

68
include/Plugin.h Normal file
View File

@@ -0,0 +1,68 @@
#pragma once
#include "Error.h"
#include "Value.h"
#include <future>
#include <utility>
#include <variant>
/// The Plugin class is an abstract interface for any plugin.
///
/// A plugin must itself figure out how to deal with events, load itself,
/// and must react quickly and appropriately to any incoming events or function calls.
/// A plugin may *not* ever block a calling thread unless explicitly marked with
/// "this may block the caller" or similar.
class Plugin {
public:
/// Self-managing pointer type of this plugin.
using Pointer = std::unique_ptr<Plugin>;
/// Allocates a Plugin of the specific derived plugin type.
template<typename T, typename... Args>
static Pointer make_pointer(Args&&... args) {
return std::unique_ptr<Plugin>(new T(std::forward<Args>(args)...));
}
/// Default constructor to enable derived classes to default-construct.
Plugin() = default;
/// Plugin is not copyable.
Plugin(const Plugin&) = delete;
/// Plugin is not copyable.
Plugin& operator=(const Plugin&) = delete;
/// Plugin is movable.
Plugin(Plugin&&) = default;
/// Plugin is movable.
Plugin& operator=(Plugin&&) = default;
/// Default destructor but virtual, to make the compiler happy.
virtual ~Plugin() = default;
/// Called when the plugin should initialize its state.
/// This may block the caller.
virtual Error initialize() = 0;
/// Called when the plugin should tear down and clean up its state.
/// This may block the caller.
virtual Error cleanup() = 0;
/// Called when the plugin should be reloaded. Usually it's a good idea
/// to notify the plugin's code, call cleanup(), etc. internally.
virtual Error reload() = 0;
/// Returns the name of the plugin.
virtual std::string name() const = 0;
/// Returns the path to the plugin - this can either be the folder in which
/// the plugin's files live, or the plugin itself if it's a single file.
/// The exact format of what this returns (directory/file) is implementation defined.
virtual std::filesystem::path path() const = 0;
/// Instructs the plugin to handle the given event, with the given arguments.
/// Returns a future with a result if this event will be handled by the plugin, otherwise must return
/// std::nullopt.
virtual std::shared_future<std::optional<Value>> handle_event(const std::string& event_name, const std::shared_ptr<Value>& args) = 0;
/// Returns how much memory this state thinks it uses.
///
/// This value is difficult to calculate for some use-cases, but a rough ballpark
/// should be returned regardless.
virtual size_t memory_usage() const = 0;
};

70
include/PluginManager.h Normal file
View File

@@ -0,0 +1,70 @@
#pragma once
#include "HashMap.h"
#include "Plugin.h"
/// The PluginManager class manages all plugins, specifically their lifetime,
/// events and memory.
class PluginManager {
public:
/// Iterates through all plugins, ask them for their usage, take the sum.
size_t memory_usage() const {
size_t total = 0;
auto plugins = m_plugins.synchronize();
for (const auto& [name, plugin] : *plugins) {
(void)name; // name ignored
total += plugin->memory_usage();
}
return total;
}
/// Triggers (sends) the event to all plugins and gathers the results as futures.
///
/// PLEASE be aware that some plugins' handlers may take a while to handle the event,
/// so try not to wait on these futures without a timeout.
///
/// This function should not block.
std::vector<std::shared_future<std::optional<Value>>> trigger_event(const std::string& event_name, const std::shared_ptr<Value>& args) {
// results will go in here
std::vector<std::shared_future<std::optional<Value>>> results;
// synchronize practically grabs a lock to the mutex, this is (as the name suggests)
// a synchronization point. technically, it could dead-lock if something that is called
// in this locked context tries to lock the m_plugins mutex.
// Plugin::handle_event should NEVER do much else than dispatch the event to the
// plugin's main thread, so this really cannot happen.
// that said, if you end up here with gdb, make sure it doesn't ;)
auto plugins = m_plugins.synchronize();
// allocate as many as we could possibly have, to avoid realloc()'s
results.reserve(plugins->size());
for (const auto& [name, plugin] : *plugins) {
(void)name; // ignore the name
// propagates the event to the plugin, this returns a future
// we assume that at this point no plugin-specific code has executed
auto maybe_result = plugin->handle_event(event_name, args);
// if the plugin had no handler, this result has no value, and we can ignore it
results.push_back(maybe_result);
}
return results;
}
/// Adds the plugin, calls Plugin::initialize(), and so on
[[nodiscard]] Error add_plugin(Plugin::Pointer&& plugin) {
auto plugins = m_plugins.synchronize();
if (plugins->contains(plugin->name())) {
return Error("Plugin with the name '{}' already exists, refusing to replace it.", plugin->name());
} else {
auto [iter, b] = plugins->insert({ plugin->name(), std::move(plugin) });
(void)b; // ignore b
auto err = iter->second->initialize();
if (err) {
return err;
}
return {};
}
}
private:
/// All plugins as pointers to allow inheritance.
SynchronizedHashMap<std::string, Plugin::Pointer> m_plugins;
};

269
include/Value.h Normal file
View File

@@ -0,0 +1,269 @@
#pragma once
/// @file
/// The Value.h file describes a collection of wrapper types for use in
/// cross-plugin communication and similar. These wrapper types are
/// typically not zero-cost, so be careful and use these sparigly.
///
/// Base visitors, such as ValueToStringVisitor, should be declared
/// here also.
#include "Error.h"
#include "HashMap.h"
#include "boost/variant/variant_fwd.hpp"
#include <boost/json.hpp>
#include <boost/variant.hpp>
#include <boost/variant/variant.hpp>
#include <ostream>
#include <sol/forward.hpp>
#include <string>
#include <vector>
/// Dynamic array, can resize.
template<typename T>
using Array = std::vector<T>;
/// Null value, for use in Value.
struct Null {
/// Makes a null value. It's an identity value,
/// so its existance is the value.
explicit Null() { }
};
/// Formats "null".
std::ostream& operator<<(std::ostream& os, const Null&);
/// Contains a boolean value, for use in Value,
/// as booleans will be implicitly converted to int.
struct Bool {
/// Construct a bool from a boolean.
explicit Bool(bool b_)
: b(b_) { }
/// Contained value.
bool b;
/// Implicit conversion to bool, because it's expected to work this way.
operator bool() const { return b; }
};
template<typename T>
struct Tuple final : public Array<T> {
using Array<T>::Array;
};
/// Formats to "true" or "false".
std::ostream& operator<<(std::ostream& os, const Bool&);
/// The Value type is a recursively defined variant, which allows
/// passing a single value with any of a selection of types, including
/// the possibility to pass hashmaps of hashmaps of hashmaps of types (and so on).
///
/// In common pseudo-C++, this would be written as:
///
/// \code{.cpp}
/// using Value = variant<string, int, double, HashMap<string, Value>;
/// // ^^^^^
/// \endcode
/// Note the `^^^` annotated recursion. This isn't directly possible in C++,
/// so we use boost's recursive variants for this. Documentation is here
/// https://www.boost.org/doc/libs/1_82_0/doc/html/variant/tutorial.html#variant.tutorial.recursive
///
/// The use-case of a Value is to represent almost any primitive-ish type we may get from, or
/// may want to pass to, a Plugin.
///
/// For example, a table of key-value pairs, or a table of tables, or just a string, or a float, could all
/// be represented by this.
///
/// See the abstract template class ValueVisitor for how to access this with the
/// visitor pattern.
using Value = boost::make_recursive_variant<
std::string,
int64_t,
double,
Null,
Array<boost::recursive_variant_>,
HashMap<std::string, boost::recursive_variant_>,
Bool,
Tuple<boost::recursive_variant_>>::type;
// the following VALUE_TYPE_* variables are used mostly for
// unit-tests and code that can't use visitors.
/// Index of string in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_STRING = 0;
/// Index of int in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_INT = 1;
/// Index of double in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_DOUBLE = 2;
/// Index of null in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_NULL = 3;
/// Index of array in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_ARRAY = 4;
/// Index of hashmap in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_HASHMAP = 5;
/// Index of bool in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_BOOL = 6;
/// Index of tuple in Value
[[maybe_unused]] constexpr int VALUE_TYPE_IDX_TUPLE = 7;
/// A handy typedef for the recursive HashMap type inside a Value.
/// You may have to use this in order to make the compiler understand
/// what kind of value (a hash map) you are constructing.
using ValueHashMap = HashMap<std::string, Value>;
/// A handy typedef for the recursive Array type inside a Value.
/// You may have to use this in order to make the compiler understand
/// what kind of value (an array) you are constructing.
using ValueArray = Array<Value>;
/// A handy dandy typedef for using a tuple of values.
using ValueTuple = Tuple<Value>;
/// The ValueVisitor class is an abstract interface which allows the implementation
/// to easily construct a visitor for a Value object.
///
/// A Value object is a recursive variant class, and as such it's not simple to access
/// (no variants are really trivial to access). The visitor pattern gives us a type-safe
/// way to access such a variant, and the boost::static_visitor pattern does so in a
/// pretty concise way.
///
/// An example use is the ValueToStringVisitor.
template<typename ResultT>
class ValueVisitor : public boost::static_visitor<ResultT> {
public:
/// Needs to be default-constructible for the standard use case (see example above).
ValueVisitor() = default;
/// Cannot be copied.
ValueVisitor(const ValueVisitor&) = delete;
/// Cannot be copied.
ValueVisitor& operator=(const ValueVisitor&) = delete;
/// Virtual destructor is needed for virtual classes.
virtual ~ValueVisitor() = default;
/// ResultT from string.
virtual ResultT operator()(const std::string& str) const = 0;
/// ResultT from integer.
virtual ResultT operator()(int64_t i) const = 0;
/// ResultT from float.
virtual ResultT operator()(double d) const = 0;
/// ResultT from null.
virtual ResultT operator()(Null null) const = 0;
/// ResultT from boolean.
virtual ResultT operator()(Bool b) const = 0;
/// ResultT from array of values (must recursively visit).
virtual ResultT operator()(const ValueArray& array) const = 0;
/// ResultT from tuple of values (must recursively visit).
virtual ResultT operator()(const ValueTuple& array) const = 0;
/// ResultT from hashmap of values (must recursively visit).
virtual ResultT operator()(const HashMap<std::string, Value>& map) const = 0;
};
/// The ValueToStringVisitor class implements a visitor for a Value which
/// turns it into a human-readable string.
///
/// Example
/// \code{.cpp}
/// #include <boost/variant.hpp>
///
/// Value value = ...;
///
/// std::string str = boost::apply_visitor(ValueToStringVisitor(), value);
/// // ^--------------------^ ^---^
/// // default ctor |
/// // value to visit
/// \endcode
class ValueToStringVisitor : public ValueVisitor<std::string> {
public:
/// Flag used to specify behavior of ValueToStringVisitor.
enum Flag {
/// No options
NONE = 0,
/// Quote strings, `value` becomes `"value"`.
QUOTE_STRINGS = 0b1,
};
/// Constructs a ValueToStringVisitor with options.
/// With flags you can change, for example, whether strings should be quoted
/// when they standalone.
/// Depth is used by recursion, ignore it.
explicit ValueToStringVisitor(Flag flags = QUOTE_STRINGS, int depth = 1);
/// Returns the same string, possibly quoted (depends on flags).
std::string operator()(const std::string& str) const;
/// Uses fmt::format() to stringify the integer.
std::string operator()(int64_t i) const;
/// Uses fmt::format() to stringify the double.
std::string operator()(double d) const;
/// Returns "null".
std::string operator()(Null null) const;
/// Returns "true" or "false".
std::string operator()(Bool b) const;
/// Returns an object of format [ value, value, value ].
/// Recursively visits the elements of the array.
std::string operator()(const ValueArray& array) const;
/// Returns a tuple of format ( value, value, value ).
/// Recursively visits the elements of the array.
std::string operator()(const ValueTuple& array) const;
/// Returns an object of format { key: value, key: value }.
/// Recursively visits the elements of the map.
std::string operator()(const HashMap<std::string, Value>& map) const;
private:
/// Whether to quote strings before output.
bool m_quote_strings;
/// How many 2-space "tabs" to use - used by recursion.
int m_depth;
};
/// The ValueToJsonVisitor class is used to convert a Value into
/// a boost::json object.
class ValueToJsonVisitor : public ValueVisitor<boost::json::value> {
public:
/// Converts to json string.
boost::json::value operator()(const std::string& str) const;
/// Converts to json integer.
boost::json::value operator()(int64_t i) const;
/// Converts to json float.
boost::json::value operator()(double d) const;
/// Converts to empty json value.
boost::json::value operator()(Null null) const;
/// Converts to json boolean.
boost::json::value operator()(Bool b) const;
/// Converts to json array.
boost::json::value operator()(const ValueArray& array) const;
/// Converts to json array (because tuples don't exist).
boost::json::value operator()(const ValueTuple& array) const;
/// Converts to json object.
boost::json::value operator()(const HashMap<std::string, Value>& map) const;
};
/// The ValueToLuaVisitor class is used to convert a Value into a
/// sol object.
class ValueToLuaVisitor : public ValueVisitor<sol::object> {
public:
/// ValueToLuaVisitor needs a sol state in order to construct objects.
ValueToLuaVisitor(sol::state& state);
sol::object operator()(const std::string& str) const;
sol::object operator()(int64_t i) const;
sol::object operator()(double d) const;
sol::object operator()(Null null) const;
sol::object operator()(Bool b) const;
sol::object operator()(const ValueArray& array) const;
sol::object operator()(const ValueTuple& array) const;
sol::object operator()(const HashMap<std::string, Value>& map) const;
private:
sol::state& m_state;
};
/// This function converts from a lua (sol) wrapped value into a beammp value, for use in C++.
///
/// Value is a type which can be passed around between threads, and has no external dependencies.
/// Sol values are not like that, as they are references to stack indices in lua, and similar.
///
/// This function is also used to print values, by first converting them to a Value, then using a
/// ValueToStringVisitor.
///
/// The second argument is a provider for values which the function can't convert.
/// "invalid provider" means "provider of values for invalid sol values". If nullptr, then
/// any invalid value (such as a function) will be resolved to an error instead and the function will
/// fail.
Result<Value> sol_obj_to_value(const sol::object&, const std::function<Result<Value>(const sol::object&)>& invalid_provider = nullptr);