mirror of
https://github.com/BeamMP/BeamMP-Server.git
synced 2025-07-03 00:05:34 +00:00
replace literally the entire lua engine
This commit is contained in:
parent
55fff0138b
commit
a0241d1b36
@ -22,7 +22,7 @@ add_subdirectory(deps/BeamMP-Protocol)
|
||||
### SETTINGS ###
|
||||
|
||||
# add all headers (.h, .hpp) to this
|
||||
set(PRJ_HEADERS
|
||||
set(PRJ_HEADERS
|
||||
include/ArgsParser.h
|
||||
include/BoostAliases.h
|
||||
include/Common.h
|
||||
@ -31,21 +31,22 @@ set(PRJ_HEADERS
|
||||
include/CustomAssert.h
|
||||
include/Defer.h
|
||||
include/Environment.h
|
||||
include/FileWatcher.h
|
||||
include/Http.h
|
||||
include/IThreaded.h
|
||||
include/Json.h
|
||||
include/LuaAPI.h
|
||||
include/LuaPlugin.h
|
||||
include/Plugin.h
|
||||
include/PluginManager.h
|
||||
include/RWMutex.h
|
||||
include/SignalHandling.h
|
||||
include/TConfig.h
|
||||
include/TConsole.h
|
||||
include/THeartbeatThread.h
|
||||
include/TLuaEngine.h
|
||||
include/TLuaPlugin.h
|
||||
include/TPluginMonitor.h
|
||||
include/TScopedTimer.h
|
||||
include/Value.h
|
||||
include/VehicleData.h
|
||||
include/Env.h
|
||||
include/Network.h
|
||||
)
|
||||
# add all source files (.cpp) to this, except the one with main()
|
||||
@ -53,18 +54,17 @@ set(PRJ_SOURCES
|
||||
src/ArgsParser.cpp
|
||||
src/Common.cpp
|
||||
src/Compat.cpp
|
||||
src/FileWatcher.cpp
|
||||
src/Http.cpp
|
||||
src/LuaAPI.cpp
|
||||
src/LuaPlugin.cpp
|
||||
src/SignalHandling.cpp
|
||||
src/TConfig.cpp
|
||||
src/TConsole.cpp
|
||||
src/THeartbeatThread.cpp
|
||||
src/TLuaEngine.cpp
|
||||
src/TLuaPlugin.cpp
|
||||
src/TPluginMonitor.cpp
|
||||
src/TScopedTimer.cpp
|
||||
src/Value.cpp
|
||||
src/VehicleData.cpp
|
||||
src/Env.cpp
|
||||
src/Network.cpp
|
||||
)
|
||||
|
||||
|
@ -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
120
include/Error.h
Normal 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
90
include/FileWatcher.h
Normal 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
22
include/HashMap.h
Normal 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>>;
|
||||
|
@ -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
129
include/LuaPlugin.h
Normal 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
68
include/Plugin.h
Normal 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
70
include/PluginManager.h
Normal 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
269
include/Value.h
Normal 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);
|
||||
|
101
src/FileWatcher.cpp
Normal file
101
src/FileWatcher.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
232
src/LuaAPI.cpp
232
src/LuaAPI.cpp
@ -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
602
src/LuaPlugin.cpp
Normal 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
470
src/Value.cpp
Normal 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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user