Compare commits

..

18 Commits

Author SHA1 Message Date
Lion
e72c217e63 Remove all unused platforms from FUNDING.yml 2024-01-25 12:14:25 +01:00
Lion
75bae8ee5a Create FUNDING.yml 2024-01-25 12:14:25 +01:00
Lambdax
594c7881e5 Removing proprietary copyright notice from readme.md (#266) 2024-01-24 13:50:21 +01:00
Lion
b9c9f22feb Update README.md 2024-01-23 21:04:37 +01:00
Lion
a3ca9573a5 Update LICENSE 2024-01-23 21:00:11 +01:00
Lion Kortlepel
aff537afdf add AGPL-3.0 license header 2024-01-23 21:00:11 +01:00
Lucca Jiménez Könings
c77df3659a fix typos and phrasing in README.md
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings
fff9580f66 remove unecessary comment
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings
9948f2412d move undef macros into Compat.h
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings
6ca4acbbf8 update README.md
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings
df5766a935 document FreeBSD specific undefs
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings
2a54d448c3 fix typo
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings
3ce790a820 update README.md
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings
2374eba750 fix tests on FreeBSD
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lucca Jiménez Könings
f82572077f add FreeBSD build target
Signed-off-by: Lucca Jiménez Könings <development@jimkoen.com>
2024-01-19 14:29:05 +01:00
Lion Kortlepel
9915c83363 fix release arm64 workflow 2024-01-15 14:49:17 +01:00
Lion Kortlepel
9f5a30a871 fix windows build wrongly calling vcpg from the submodule 2024-01-15 14:49:17 +01:00
Lion Kortlepel
6832fd05d6 remove --build-id=none from build flags 2024-01-15 14:49:17 +01:00
44 changed files with 4262 additions and 3956 deletions

3
.gitmodules vendored
View File

@@ -4,6 +4,3 @@
[submodule "vcpkg"]
path = vcpkg
url = https://github.com/Microsoft/vcpkg.git
[submodule "deps/BeamMP-Protocol"]
path = deps/BeamMP-Protocol
url = https://github.com/BeamMP/BeamMP-Protocol

View File

@@ -15,58 +15,62 @@ include(cmake/StandardSettings.cmake)
include(cmake/StaticAnalyzers.cmake)
include(cmake/Git.cmake)
add_subdirectory(deps/BeamMP-Protocol)
# below are options which should be changed
### SETTINGS ###
# add all headers (.h, .hpp) to this
set(PRJ_HEADERS
set(PRJ_HEADERS
include/ArgsParser.h
include/BoostAliases.h
include/Client.h
include/Common.h
include/Compat.h
include/Cryptography.h
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/TNetwork.h
include/TPluginMonitor.h
include/TPPSMonitor.h
include/TResourceManager.h
include/TScopedTimer.h
include/Value.h
include/TServer.h
include/VehicleData.h
include/Network.h
include/Env.h
)
# add all source files (.cpp) to this, except the one with main()
set(PRJ_SOURCES
src/ArgsParser.cpp
src/Client.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/TNetwork.cpp
src/TPluginMonitor.cpp
src/TPPSMonitor.cpp
src/TResourceManager.cpp
src/TScopedTimer.cpp
src/Value.cpp
src/TServer.cpp
src/VehicleData.cpp
src/Network.cpp
src/Env.cpp
)
@@ -94,26 +98,19 @@ set(PRJ_LIBRARIES
httplib::httplib
libzip::zip
OpenSSL::SSL OpenSSL::Crypto
protocol
${LUA_LIBRARIES}
zstd::libzstd_static
Boost::thread
Boost::json
glm::glm
)
# add dependency find_package calls and similar here
find_package(fmt CONFIG REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(doctest CONFIG REQUIRED)
find_package(Boost REQUIRED COMPONENTS thread json)
find_package(Boost REQUIRED)
find_package(httplib CONFIG REQUIRED)
find_package(libzip CONFIG REQUIRED)
find_package(RapidJSON CONFIG REQUIRED)
find_package(sol2 CONFIG REQUIRED)
find_package(toml11 CONFIG REQUIRED)
find_package(zstd CONFIG REQUIRED)
find_package(glm CONFIG REQUIRED)
include_directories(include)

View File

@@ -71,8 +71,9 @@ function(set_project_warnings project_name)
-Werror=write-strings
-Werror=strict-aliasing -fstrict-aliasing
-Werror=missing-declarations
-Werror=missing-field-initializers
-Werror=ctor-dtor-privacy
-Wno-missing-field-initializers
-Werror=switch-enum
-Wswitch-default
-Werror=unused-result
-Werror=implicit-fallthrough

Submodule deps/BeamMP-Protocol deleted from 8eda5714c0

134
include/Client.h Normal file
View File

@@ -0,0 +1,134 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <chrono>
#include <memory>
#include <optional>
#include <queue>
#include <string>
#include <unordered_set>
#include "BoostAliases.h"
#include "Common.h"
#include "Compat.h"
#include "VehicleData.h"
class TServer;
#ifdef BEAMMP_WINDOWS
// for socklen_t
#include <WS2tcpip.h>
#endif // WINDOWS
struct TConnection final {
ip::tcp::socket Socket;
ip::tcp::endpoint SockAddr;
};
class TClient final {
public:
using TSetOfVehicleData = std::vector<TVehicleData>;
struct TVehicleDataLockPair {
TSetOfVehicleData* VehicleData;
std::unique_lock<std::mutex> Lock;
};
TClient(TServer& Server, ip::tcp::socket&& Socket);
TClient(const TClient&) = delete;
~TClient();
TClient& operator=(const TClient&) = delete;
void AddNewCar(int Ident, const std::string& Data);
void SetCarData(int Ident, const std::string& Data);
void SetCarPosition(int Ident, const std::string& Data);
TVehicleDataLockPair GetAllCars();
void SetName(const std::string& Name) { mName = Name; }
void SetRoles(const std::string& Role) { mRole = Role; }
void SetIdentifier(const std::string& key, const std::string& value) { mIdentifiers[key] = value; }
std::string GetCarData(int Ident);
std::string GetCarPositionRaw(int Ident);
void SetUDPAddr(const ip::udp::endpoint& Addr) { mUDPAddress = Addr; }
void SetDownSock(ip::tcp::socket&& CSock) { mDownSocket = std::move(CSock); }
void SetTCPSock(ip::tcp::socket&& CSock) { mSocket = std::move(CSock); }
void Disconnect(std::string_view Reason);
bool IsDisconnected() const { return !mSocket.is_open(); }
// locks
void DeleteCar(int Ident);
[[nodiscard]] const std::unordered_map<std::string, std::string>& GetIdentifiers() const { return mIdentifiers; }
[[nodiscard]] const ip::udp::endpoint& GetUDPAddr() const { return mUDPAddress; }
[[nodiscard]] ip::udp::endpoint& GetUDPAddr() { return mUDPAddress; }
[[nodiscard]] ip::tcp::socket& GetDownSock() { return mDownSocket; }
[[nodiscard]] const ip::tcp::socket& GetDownSock() const { return mDownSocket; }
[[nodiscard]] ip::tcp::socket& GetTCPSock() { return mSocket; }
[[nodiscard]] const ip::tcp::socket& GetTCPSock() const { return mSocket; }
[[nodiscard]] std::string GetRoles() const { return mRole; }
[[nodiscard]] std::string GetName() const { return mName; }
void SetUnicycleID(int ID) { mUnicycleID = ID; }
void SetID(int ID) { mID = ID; }
[[nodiscard]] int GetOpenCarID() const;
[[nodiscard]] int GetCarCount() const;
void ClearCars();
[[nodiscard]] int GetID() const { return mID; }
[[nodiscard]] int GetUnicycleID() const { return mUnicycleID; }
[[nodiscard]] bool IsConnected() const { return mIsConnected; }
[[nodiscard]] bool IsSynced() const { return mIsSynced; }
[[nodiscard]] bool IsSyncing() const { return mIsSyncing; }
[[nodiscard]] bool IsGuest() const { return mIsGuest; }
void SetIsGuest(bool NewIsGuest) { mIsGuest = NewIsGuest; }
void SetIsSynced(bool NewIsSynced) { mIsSynced = NewIsSynced; }
void SetIsSyncing(bool NewIsSyncing) { mIsSyncing = NewIsSyncing; }
void EnqueuePacket(const std::vector<uint8_t>& Packet);
[[nodiscard]] std::queue<std::vector<uint8_t>>& MissedPacketQueue() { return mPacketsSync; }
[[nodiscard]] const std::queue<std::vector<uint8_t>>& MissedPacketQueue() const { return mPacketsSync; }
[[nodiscard]] size_t MissedPacketQueueSize() const { return mPacketsSync.size(); }
[[nodiscard]] std::mutex& MissedPacketQueueMutex() const { return mMissedPacketsMutex; }
void SetIsConnected(bool NewIsConnected) { mIsConnected = NewIsConnected; }
[[nodiscard]] TServer& Server() const;
void UpdatePingTime();
int SecondsSinceLastPing();
private:
void InsertVehicle(int ID, const std::string& Data);
TServer& mServer;
bool mIsConnected = false;
bool mIsSynced = false;
bool mIsSyncing = false;
mutable std::mutex mMissedPacketsMutex;
std::queue<std::vector<uint8_t>> mPacketsSync;
std::unordered_map<std::string, std::string> mIdentifiers;
bool mIsGuest = false;
mutable std::mutex mVehicleDataMutex;
mutable std::mutex mVehiclePositionMutex;
TSetOfVehicleData mVehicleData;
SparseArray<std::string> mVehiclePosition;
std::string mName = "Unknown Client";
ip::tcp::socket mSocket;
ip::tcp::socket mDownSocket;
ip::udp::endpoint mUDPAddress {};
int mUnicycleID = -1;
std::string mRole;
std::string mDID;
int mID = -1;
std::chrono::time_point<std::chrono::high_resolution_clock> mLastPingTime;
};
std::optional<std::weak_ptr<TClient>> GetClient(class TServer& Server, int ID);

View File

@@ -23,11 +23,7 @@
#include <cstring>
#include <deque>
#include <filesystem>
#include <fmt/chrono.h>
#include <fmt/format.h>
#include <fmt/ostream.h>
#include <fmt/ranges.h>
#include <fmt/std.h>
#include <functional>
#include <memory>
#include <mutex>
@@ -69,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 };
@@ -222,16 +218,6 @@ 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 { \
@@ -262,6 +248,8 @@ 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
@@ -295,5 +283,57 @@ void RegisterThread(const std::string& str);
void LogChatMessage(const std::string& name, int id, const std::string& msg);
#define Biggest 30000
template <typename T>
inline T Comp(const T& Data) {
std::array<char, Biggest> C {};
// obsolete
C.fill(0);
z_stream defstream;
defstream.zalloc = nullptr;
defstream.zfree = nullptr;
defstream.opaque = nullptr;
defstream.avail_in = uInt(Data.size());
defstream.next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(&Data[0]));
defstream.avail_out = Biggest;
defstream.next_out = reinterpret_cast<Bytef*>(C.data());
deflateInit(&defstream, Z_BEST_COMPRESSION);
deflate(&defstream, Z_SYNC_FLUSH);
deflate(&defstream, Z_FINISH);
deflateEnd(&defstream);
size_t TotalOut = defstream.total_out;
T Ret;
Ret.resize(TotalOut);
std::fill(Ret.begin(), Ret.end(), 0);
std::copy_n(C.begin(), TotalOut, Ret.begin());
return Ret;
}
template <typename T>
inline T DeComp(const T& Compressed) {
std::array<char, Biggest> C {};
// not needed
C.fill(0);
z_stream infstream;
infstream.zalloc = nullptr;
infstream.zfree = nullptr;
infstream.opaque = nullptr;
infstream.avail_in = Biggest;
infstream.next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(&Compressed[0]));
infstream.avail_out = Biggest;
infstream.next_out = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(C.data()));
inflateInit(&infstream);
inflate(&infstream, Z_SYNC_FLUSH);
inflate(&infstream, Z_FINISH);
inflateEnd(&infstream);
size_t TotalOut = infstream.total_out;
T Ret;
Ret.resize(TotalOut);
std::fill(Ret.begin(), Ret.end(), 0);
std::copy_n(C.begin(), TotalOut, Ret.begin());
return Ret;
}
std::string GetPlatformAgnosticErrorString();
#define S_DSN SU_RAW

View File

@@ -1,120 +0,0 @@
#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" };
};

View File

@@ -1,92 +0,0 @@
#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/signals2.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);
boost::signals2::signal<void(const std::filesystem::path&)> sig_file_changed {};
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;
};

View File

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

View File

@@ -18,7 +18,6 @@
#pragma once
#include <boost/thread/scoped_thread.hpp>
#include <thread>
// pure virtual class to be inherited from by classes which intend to be threaded
@@ -28,14 +27,16 @@ public:
// invokes operator() on this object
: mThread() { }
virtual ~IThreaded() noexcept {
mThread.interrupt();
if (mThread.joinable()) {
mThread.join();
}
}
virtual void Start() final {
mThread = boost::scoped_thread<>([this] { (*this)(); });
mThread = std::thread([this] { (*this)(); });
}
virtual void operator()() = 0;
protected:
boost::scoped_thread<> mThread {};
std::thread mThread;
};

View File

@@ -18,33 +18,30 @@
#pragma once
#include <sol/sol.hpp>
#include <string>
#include "TLuaEngine.h"
#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);
size_t GetPlayerCount();
inline size_t GetPlayerCount() { return Engine->Server().ClientCount(); }
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);
/// 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);
void Sleep(size_t Ms);
void PrintRaw(sol::variadic_args);
std::string JsonEncode(const sol::table& object);
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);
@@ -65,7 +62,5 @@ 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);
}
}

View File

@@ -1,144 +0,0 @@
#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/signals2.hpp>
#include <boost/thread/scoped_thread.hpp>
#include <boost/thread/synchronized_value.hpp>
#include <sol/forward.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;
};
static constexpr const char* BEAMMP_MEMORY_STATE = ":console:";
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;
}
});
}
/// Dangerous: Only use this on a special memory state.
/// Results are *printed to stdout*.
void run_raw_lua(const std::string& raw);
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::vector<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;
/// Event handlers which are legacy-style (by name)
HashMap<std::string, std::vector<std::string>> m_event_handlers_named {};
/// Event handlers which are functions (v4 style)
HashMap<std::string, std::vector<sol::protected_function>> m_event_handlers {};
/// 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 };
boost::signals2::connection m_extensions_watch_conn;
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 std::vector<sol::object>&);
};

View File

@@ -1,250 +0,0 @@
#pragma once
#include "Common.h"
#include "Packet.h"
#include "State.h"
#include "Sync.h"
#include "Transport.h"
#include "Util.h"
#include <boost/asio.hpp>
#include <boost/asio/execution_context.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/thread/scoped_thread.hpp>
#include <boost/thread/synchronized_value.hpp>
#include <cstdint>
#include <filesystem>
#include <glm/detail/qualifier.hpp>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <memory>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <unordered_map>
#include <vector>
using ClientID = uint32_t;
using VehicleID = uint16_t;
using namespace boost::asio;
struct Client : std::enable_shared_from_this<Client> {
using StrandPtr = std::shared_ptr<boost::asio::strand<ip::tcp::socket::executor_type>>;
using Ptr = std::shared_ptr<Client>;
ClientID id;
Sync<bmp::State> state { bmp::State::None };
Sync<std::string> name;
Sync<std::string> role;
Sync<bool> is_guest;
Sync<std::unordered_map<std::string /* identifier */, std::string /* value */>> identifiers;
/// Writes the packet to the TCP stream. Blocks all other writes.
void tcp_write(bmp::Packet& packet);
/// Writes the specified to the TCP stream without a header or any metadata - use in
/// conjunction with something else. Blocks other writes.
void tcp_write_file_raw(const std::filesystem::path& path);
/// Ensures no client is ever created as a non-shared-ptr, so that enable_shared_from_this works.
static Client::Ptr make_ptr(ClientID new_id, class Network& network, ip::tcp::socket&& tcp_socket);
~Client();
ip::tcp::socket& tcp_socket() { return m_tcp_socket; }
void start_tcp();
/// Used to associate the udp socket with this client.
/// This isn't very secure and still allows spoofing of the UDP connection (technically),
/// but better than simply using the ID like the old protocol.
const uint64_t udp_magic;
const ip::udp::endpoint& udp_endpoint() const { return m_udp_endpoint; }
void set_udp_endpoint(const ip::udp::endpoint& ep) { m_udp_endpoint = ep; }
private:
/// Ctor must be private to ensure all clients are constructed as shared_ptr to enable_shared_from_this.
Client(ClientID id, class Network& network, ip::tcp::socket&& tcp_socket);
/// Call this when the client seems to have timed out. Will send a ping and set a flag.
/// Returns true if try-again, false if the connection was closed.
[[nodiscard]] bool handle_timeout();
bool m_timed_out { false };
/// Timeout used for typical tcp reads.
boost::posix_time::milliseconds m_read_timeout { 5000 };
/// Timeout used for typical tcp writes. Specified in milliseconds per byte.
/// For example, 10 mbit/s works out to 1250 B/ms, so a value of 1250 here would
/// cause clients with >10 mbit/s download speed to usually not time out.
/// This is done because a write is considered completed when all data is written,
/// and worst-case this could mean that we're limited by their download speed.
/// We're setting it to 50, which will drop clients who are below a download speed + ping
/// combination of 0.4 mbit/s.
double m_write_byte_timeout { 0.01 };
/// Timeout used for mod download tcp writes.
/// This is typically orders of magnitude larger
/// to allow for slow downloads.
boost::posix_time::milliseconds m_download_write_timeout { 60000 };
std::mutex m_tcp_read_mtx;
std::mutex m_tcp_write_mtx;
std::mutex m_udp_read_mtx;
ip::tcp::socket m_tcp_socket;
boost::scoped_thread<> m_tcp_thread;
std::vector<uint8_t> m_header { bmp::Header::SERIALIZED_SIZE };
bmp::Packet m_packet {};
ip::udp::endpoint m_udp_endpoint;
class Network& m_network;
StrandPtr m_tcp_strand;
};
struct Vehicle {
using Ptr = std::shared_ptr<Vehicle>;
Sync<ClientID> owner;
Sync<std::vector<uint8_t>> data;
Vehicle(std::span<uint8_t> raw_data)
: data(std::vector<uint8_t>(raw_data.begin(), raw_data.end())) {
reset_status(data.get());
}
/// Resets all status fields to zero and reads any statuses present in the data into the fields.
void reset_status(std::span<const uint8_t> status_data);
struct Status {
glm::vec3 rvel {};
glm::vec4 rot {};
glm::vec3 vel {};
glm::vec3 pos {};
float time {};
};
Status get_status();
void update_status(std::span<const uint8_t> raw_packet);
const std::vector<uint8_t>& get_raw_status() const { return m_status_data; }
private:
std::recursive_mutex m_mtx;
/// Holds pos, rvel, vel, etc. raw, updated every time
/// such a packet arrives.
std::vector<uint8_t> m_status_data;
/// Parses the status_data on request sets needs_refresh = false.
void refresh_cache(std::unique_lock<std::recursive_mutex>& lock);
bool m_needs_refresh = false;
glm::vec3 m_rvel {};
glm::vec4 m_rot {};
glm::vec3 m_vel {};
glm::vec3 m_pos {};
float m_time {};
};
class Network {
public:
Network();
~Network();
friend Client;
void disconnect(ClientID id, const std::string& msg);
void send_to(ClientID id, bmp::Packet& packet);
/// Returns a map of <id, client> containing only clients which are
/// fully connected, i.e. who have mods downloaded and everything spawned in.
/// If you're unsure which to use, use this one.
std::unordered_map<ClientID, Client::Ptr> playing_clients() const;
/// Returns a map of <id, client> containing only clients who are authenticated.
std::unordered_map<ClientID, Client::Ptr> authenticated_clients() const;
/// Returns all clients, including non-authenticated clients. Use only for debugging,
/// information, stats, status.
std::unordered_map<ClientID, Client::Ptr> all_clients() const;
std::optional<Client::Ptr> get_client(ClientID id, bmp::State min_state) const;
std::unordered_map<VehicleID, Vehicle::Ptr> get_vehicles_owned_by(ClientID id);
std::optional<Vehicle::Ptr> get_vehicle(VehicleID id);
/// Builds the SessionSetup.PlayersInfo json which contains all player info and all vehicles.
nlohmann::json build_players_info();
size_t authenticated_client_count() const;
size_t clients_in_state_count(bmp::State state) const;
size_t guest_count() const;
size_t vehicle_count() const;
/// Creates a Playing state packet from uncompressed data.
bmp::Packet make_playing_packet(bmp::Purpose purpose, ClientID from_id, VehicleID veh_id, const std::vector<uint8_t>& data);
/// Sends a <System> or <Server> chat message to all or only one client(s).
void send_system_chat_message(const std::string& msg, ClientID to = 0xffffffff);
/// To be called by accept() async handler once an accept() is completed.
void accept();
/// Gets the async i/o context of the network - can be used to "schedule" tasks on it.
boost::asio::thread_pool& context() {
return m_threadpool;
}
private:
void handle_packet(ClientID id, const bmp::Packet& packet);
/// Reads a packet from the given UDP socket, returning the client's endpoint as an out-argument.
bmp::Packet udp_read(ip::udp::endpoint& out_ep);
/// Sends a packet to the specified UDP endpoint via the UDP socket.
void udp_write(bmp::Packet& packet, const ip::udp::endpoint& ep);
void udp_read_main();
void tcp_listen_main();
/// Handles all packets which are allowed during the Identification state.
void handle_identification(ClientID id, const bmp::Packet& packet, std::shared_ptr<Client>& client);
/// Handles all packets which are allowed during the Authentication state.
void handle_authentication(ClientID id, const bmp::Packet& packet, std::shared_ptr<Client>& client);
/// Handles all packets which are allowed during the ModDownload state.
void handle_mod_download(ClientID id, const bmp::Packet& packet, std::shared_ptr<Client>& client);
/// Handles all packets which are allowed during the SessionSetup state.
void handle_session_setup(ClientID id, const bmp::Packet& packet, std::shared_ptr<Client>& client);
/// Handles all packets which are allowed during the Playing state.
void handle_playing(ClientID id, const bmp::Packet& packet, std::shared_ptr<Client>& client);
/// On failure, throws an exception with the error for the client.
static void authenticate_user(const std::string& public_key, std::shared_ptr<Client>& client);
/// Called by accept() once completed (completion handler).
void handle_accept(const boost::system::error_code& ec);
Sync<std::unordered_map<ClientID, Client::Ptr>> m_clients {};
Sync<std::unordered_map<VehicleID, Vehicle::Ptr>> m_vehicles {};
Sync<std::unordered_map<uint64_t, ClientID>> m_client_magics {};
Sync<std::unordered_map<ip::udp::endpoint, ClientID>> m_udp_endpoints {};
ClientID new_client_id();
VehicleID new_vehicle_id();
thread_pool m_threadpool { std::thread::hardware_concurrency() };
Sync<bool> m_shutdown { false };
ip::udp::socket m_udp_socket { m_threadpool };
ip::tcp::socket m_tcp_listener { m_threadpool };
ip::tcp::acceptor m_tcp_acceptor { m_threadpool };
/// This socket gets accepted into, and is then moved.
ip::tcp::socket m_temp_socket { m_threadpool };
boost::scoped_thread<> m_tcp_listen_thread;
boost::scoped_thread<> m_udp_read_thread;
};

View File

@@ -1,68 +0,0 @@
#pragma once
#include "Error.h"
#include "Value.h"
#include <filesystem>
#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::shared_ptr<Plugin>;
/// Allocates a Plugin of the specific derived plugin type.
template <typename T, typename... Args>
static Pointer make_pointer(Args&&... args) {
return std::shared_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::vector<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;
};

View File

@@ -1,73 +0,0 @@
#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::vector<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::vector<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 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(std::move(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 {};
}
}
HashMap<std::string, Plugin::Pointer> get_plugins() {
return *m_plugins;
}
private:
/// All plugins as pointers to allow inheritance.
SynchronizedHashMap<std::string, Plugin::Pointer> m_plugins;
};

View File

@@ -1,10 +0,0 @@
#pragma once
#include <boost/thread/synchronized_value.hpp>
#include <mutex>
/// This header provides convenience aliases for synchronization primitives.
template<typename T>
using Sync = boost::synchronized_value<T, std::recursive_mutex>;

View File

@@ -19,8 +19,6 @@
#pragma once
#include "Cryptography.h"
#include "LuaPlugin.h"
#include "TScopedTimer.h"
#include "commandline.h"
#include <atomic>
#include <fstream>
@@ -31,6 +29,8 @@
#include <unordered_map>
#include <vector>
class TLuaEngine;
class TConsole {
public:
TConsole();
@@ -40,7 +40,7 @@ public:
void Write(const std::string& str);
void WriteRaw(const std::string& str);
// void InitializeLuaConsole(TLuaEngine& Engine);
void InitializeLuaConsole(TLuaEngine& Engine);
void BackupOldLog();
void StartLoggingToFile();
Commandline& Internal() { return *mCommandline; }
@@ -80,11 +80,11 @@ private:
std::unique_ptr<Commandline> mCommandline { nullptr };
std::vector<std::string> mCachedLuaHistory;
std::vector<std::string> mCachedRegularHistory;
TLuaEngine* mLuaEngine { nullptr };
bool mIsLuaConsole { false };
bool mFirstTime { true };
std::string mStateId;
const std::string mDefaultStateId = BEAMMP_MEMORY_STATE;
const std::string mDefaultStateId = "BEAMMP_SERVER_CONSOLE";
std::ofstream mLogFileStream;
std::mutex mLogFileStreamMtx;
TScopedTimer mUptimeTimer{};
};

View File

@@ -20,11 +20,12 @@
#include "Common.h"
#include "IThreaded.h"
#include "Network.h"
#include "TResourceManager.h"
#include "TServer.h"
class THeartbeatThread : public IThreaded {
public:
THeartbeatThread(std::shared_ptr<Network> network);
THeartbeatThread(TResourceManager& ResourceManager, TServer& Server);
//~THeartbeatThread();
void operator()() override;
@@ -32,5 +33,6 @@ private:
std::string GenerateCall();
std::string GetPlayers();
std::shared_ptr<Network> m_network;
TResourceManager& mResourceManager;
TServer& mServer;
};

298
include/TLuaEngine.h Normal file
View File

@@ -0,0 +1,298 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "TNetwork.h"
#include "TServer.h"
#include <any>
#include <condition_variable>
#include <filesystem>
#include <initializer_list>
#include <list>
#include <lua.hpp>
#include <memory>
#include <mutex>
#include <queue>
#include <random>
#include <set>
#include <toml.hpp>
#include <unordered_map>
#include <vector>
#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
using TLuaStateId = std::string;
namespace fs = std::filesystem;
/**
* std::variant means, that TLuaArgTypes may be one of the Types listed as template args
*/
using TLuaArgTypes = std::variant<std::string, int, sol::variadic_args, bool, std::unordered_map<std::string, std::string>>;
static constexpr size_t TLuaArgTypes_String = 0;
static constexpr size_t TLuaArgTypes_Int = 1;
static constexpr size_t TLuaArgTypes_VariadicArgs = 2;
static constexpr size_t TLuaArgTypes_Bool = 3;
static constexpr size_t TLuaArgTypes_StringStringMap = 4;
class TLuaPlugin;
struct TLuaResult {
bool Ready;
bool Error;
std::string ErrorMessage;
sol::object Result { sol::lua_nil };
TLuaStateId StateId;
std::string Function;
std::shared_ptr<std::mutex> ReadyMutex {
std::make_shared<std::mutex>()
};
std::shared_ptr<std::condition_variable> ReadyCondition {
std::make_shared<std::condition_variable>()
};
void MarkAsReady();
void WaitUntilReady();
};
struct TLuaPluginConfig {
static inline const std::string FileName = "PluginConfig.toml";
TLuaStateId StateId;
// TODO: Add execute list
// TODO: Build a better toml serializer, or some way to do this in an easier way
};
struct TLuaChunk {
TLuaChunk(std::shared_ptr<std::string> Content,
std::string FileName,
std::string PluginPath);
std::shared_ptr<std::string> Content;
std::string FileName;
std::string PluginPath;
};
class TLuaEngine : public std::enable_shared_from_this<TLuaEngine>, IThreaded {
public:
enum CallStrategy : int {
BestEffort,
Precise,
};
struct QueuedFunction {
std::string FunctionName;
std::shared_ptr<TLuaResult> Result;
std::vector<TLuaArgTypes> Args;
std::string EventName; // optional, may be empty
};
TLuaEngine();
virtual ~TLuaEngine() noexcept {
beammp_debug("Lua Engine terminated");
}
void operator()() override;
TNetwork& Network() { return *mNetwork; }
TServer& Server() { return *mServer; }
void SetNetwork(TNetwork* Network) { mNetwork = Network; }
void SetServer(TServer* Server) { mServer = Server; }
size_t GetResultsToCheckSize() {
std::unique_lock Lock(mResultsToCheckMutex);
return mResultsToCheck.size();
}
size_t GetLuaStateCount() {
std::unique_lock Lock(mLuaStatesMutex);
return mLuaStates.size();
}
std::vector<std::string> GetLuaStateNames() {
std::vector<std::string> names {};
for (auto const& [stateId, _] : mLuaStates) {
names.push_back(stateId);
}
return names;
}
size_t GetTimedEventsCount() {
std::unique_lock Lock(mTimedEventsMutex);
return mTimedEvents.size();
}
size_t GetRegisteredEventHandlerCount() {
std::unique_lock Lock(mLuaEventsMutex);
size_t LuaEventsCount = 0;
for (const auto& State : mLuaEvents) {
for (const auto& Events : State.second) {
LuaEventsCount += Events.second.size();
}
}
return LuaEventsCount - GetLuaStateCount();
}
static void WaitForAll(std::vector<std::shared_ptr<TLuaResult>>& Results,
const std::optional<std::chrono::high_resolution_clock::duration>& Max = std::nullopt);
void ReportErrors(const std::vector<std::shared_ptr<TLuaResult>>& Results);
bool HasState(TLuaStateId StateId);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(TLuaStateId StateID, const std::string& FunctionName, const std::vector<TLuaArgTypes>& Args);
void EnsureStateExists(TLuaStateId StateId, const std::string& Name, bool DontCallOnInit = false);
void RegisterEvent(const std::string& EventName, TLuaStateId StateId, const std::string& FunctionName);
/**
*
* @tparam ArgsT Template Arguments for the event (Metadata) todo: figure out what this means
* @param EventName Name of the event
* @param IgnoreId
* @param Args
* @return
*/
template <typename... ArgsT>
[[nodiscard]] std::vector<std::shared_ptr<TLuaResult>> TriggerEvent(const std::string& EventName, TLuaStateId IgnoreId, ArgsT&&... Args) {
std::unique_lock Lock(mLuaEventsMutex);
beammp_event(EventName);
if (mLuaEvents.find(EventName) == mLuaEvents.end()) { // if no event handler is defined for 'EventName', return immediately
return {};
}
std::vector<std::shared_ptr<TLuaResult>> Results;
std::vector<TLuaArgTypes> Arguments { TLuaArgTypes { std::forward<ArgsT>(Args) }... };
for (const auto& Event : mLuaEvents.at(EventName)) {
for (const auto& Function : Event.second) {
if (Event.first != IgnoreId) {
Results.push_back(EnqueueFunctionCall(Event.first, Function, Arguments));
}
}
}
return Results; //
}
template <typename... ArgsT>
[[nodiscard]] std::vector<std::shared_ptr<TLuaResult>> TriggerLocalEvent(const TLuaStateId& StateId, const std::string& EventName, ArgsT&&... Args) {
std::unique_lock Lock(mLuaEventsMutex);
beammp_event(EventName + " in '" + StateId + "'");
if (mLuaEvents.find(EventName) == mLuaEvents.end()) { // if no event handler is defined for 'EventName', return immediately
return {};
}
std::vector<std::shared_ptr<TLuaResult>> Results;
std::vector<TLuaArgTypes> Arguments { TLuaArgTypes { std::forward<ArgsT>(Args) }... };
const auto Handlers = GetEventHandlersForState(EventName, StateId);
for (const auto& Handler : Handlers) {
Results.push_back(EnqueueFunctionCall(StateId, Handler, Arguments));
}
return Results;
}
std::set<std::string> GetEventHandlersForState(const std::string& EventName, TLuaStateId StateId);
void CreateEventTimer(const std::string& EventName, TLuaStateId StateId, size_t IntervalMS, CallStrategy Strategy);
void CancelEventTimers(const std::string& EventName, TLuaStateId StateId);
sol::state_view GetStateForPlugin(const fs::path& PluginPath);
TLuaStateId GetStateIDForPlugin(const fs::path& PluginPath);
void AddResultToCheck(const std::shared_ptr<TLuaResult>& Result);
static constexpr const char* BeamMPFnNotFoundError = "BEAMMP_FN_NOT_FOUND";
std::vector<std::string> GetStateGlobalKeysForState(TLuaStateId StateId);
std::vector<std::string> GetStateTableKeysForState(TLuaStateId StateId, std::vector<std::string> keys);
// Debugging functions (slow)
std::unordered_map<std::string /*event name */, std::vector<std::string> /* handlers */> Debug_GetEventsForState(TLuaStateId StateId);
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId);
std::vector<QueuedFunction> Debug_GetStateFunctionQueueForState(TLuaStateId StateId);
std::vector<TLuaResult> Debug_GetResultsToCheckForState(TLuaStateId StateId);
private:
void CollectAndInitPlugins();
void InitializePlugin(const fs::path& Folder, const TLuaPluginConfig& Config);
void FindAndParseConfig(const fs::path& Folder, TLuaPluginConfig& Config);
size_t CalculateMemoryUsage();
class StateThreadData : IThreaded {
public:
StateThreadData(const std::string& Name, TLuaStateId StateId, TLuaEngine& Engine);
StateThreadData(const StateThreadData&) = delete;
virtual ~StateThreadData() noexcept { beammp_debug("\"" + mStateId + "\" destroyed"); }
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueScript(const TLuaChunk& Script);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(const std::string& FunctionName, const std::vector<TLuaArgTypes>& Args);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector<TLuaArgTypes>& Args, const std::string& EventName, CallStrategy Strategy);
void RegisterEvent(const std::string& EventName, const std::string& FunctionName);
void AddPath(const fs::path& Path); // to be added to path and cpath
void operator()() override;
sol::state_view State() { return sol::state_view(mState); }
std::vector<std::string> GetStateGlobalKeys();
std::vector<std::string> GetStateTableKeys(const std::vector<std::string>& keys);
// Debug functions, slow
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Debug_GetStateExecuteQueue();
std::vector<TLuaEngine::QueuedFunction> Debug_GetStateFunctionQueue();
private:
sol::table Lua_TriggerGlobalEvent(const std::string& EventName, sol::variadic_args EventArgs);
sol::table Lua_TriggerLocalEvent(const std::string& EventName, sol::variadic_args EventArgs);
sol::table Lua_GetPlayerIdentifiers(int ID);
sol::table Lua_GetPlayers();
std::string Lua_GetPlayerName(int ID);
sol::table Lua_GetPlayerVehicles(int ID);
std::pair<sol::table, std::string> Lua_GetPositionRaw(int PID, int VID);
sol::table Lua_HttpCreateConnection(const std::string& host, uint16_t port);
sol::table Lua_JsonDecode(const std::string& str);
int Lua_GetPlayerIDByName(const std::string& Name);
sol::table Lua_FS_ListFiles(const std::string& Path);
sol::table Lua_FS_ListDirectories(const std::string& Path);
std::string mName;
TLuaStateId mStateId;
lua_State* mState;
std::thread mThread;
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> mStateExecuteQueue;
std::recursive_mutex mStateExecuteQueueMutex;
std::vector<QueuedFunction> mStateFunctionQueue;
std::mutex mStateFunctionQueueMutex;
std::condition_variable mStateFunctionQueueCond;
TLuaEngine* mEngine;
sol::state_view mStateView { mState };
std::queue<fs::path> mPaths;
std::recursive_mutex mPathsMutex;
std::mt19937 mMersenneTwister;
std::uniform_real_distribution<double> mUniformRealDistribution01;
};
struct TimedEvent {
std::chrono::high_resolution_clock::duration Duration {};
std::chrono::high_resolution_clock::time_point LastCompletion {};
std::string EventName;
TLuaStateId StateId;
CallStrategy Strategy;
bool Expired();
void Reset();
};
TNetwork* mNetwork;
TServer* mServer;
const fs::path mResourceServerPath;
std::vector<std::shared_ptr<TLuaPlugin>> mLuaPlugins;
std::unordered_map<TLuaStateId, std::unique_ptr<StateThreadData>> mLuaStates;
std::recursive_mutex mLuaStatesMutex;
std::unordered_map<std::string /* event name */, std::unordered_map<TLuaStateId, std::set<std::string>>> mLuaEvents;
std::recursive_mutex mLuaEventsMutex;
std::vector<TimedEvent> mTimedEvents;
std::recursive_mutex mTimedEventsMutex;
std::list<std::shared_ptr<TLuaResult>> mResultsToCheck;
std::mutex mResultsToCheckMutex;
std::condition_variable mResultsToCheckCond;
};
// std::any TriggerLuaEvent(const std::string& Event, bool local, TLuaPlugin* Caller, std::shared_ptr<TLuaArg> arg, bool Wait);

37
include/TLuaPlugin.h Normal file
View File

@@ -0,0 +1,37 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TLuaEngine.h"
class TLuaPlugin {
public:
TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const fs::path& MainFolder);
TLuaPlugin(const TLuaPlugin&) = delete;
TLuaPlugin& operator=(const TLuaPlugin&) = delete;
~TLuaPlugin() noexcept = default;
const TLuaPluginConfig& GetConfig() const { return mConfig; }
fs::path GetFolder() const { return mFolder; }
private:
TLuaPluginConfig mConfig;
TLuaEngine& mEngine;
fs::path mFolder;
std::string mPluginName;
std::unordered_map<std::string, std::shared_ptr<std::string>> mFileContents;
};

74
include/TNetwork.h Normal file
View File

@@ -0,0 +1,74 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "BoostAliases.h"
#include "Compat.h"
#include "TResourceManager.h"
#include "TServer.h"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/udp.hpp>
struct TConnection;
class TNetwork {
public:
TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& ResourceManager);
[[nodiscard]] bool TCPSend(TClient& c, const std::vector<uint8_t>& Data, bool IsSync = false);
[[nodiscard]] bool SendLarge(TClient& c, std::vector<uint8_t> Data, bool isSync = false);
[[nodiscard]] bool Respond(TClient& c, const std::vector<uint8_t>& MSG, bool Rel, bool isSync = false);
std::shared_ptr<TClient> CreateClient(ip::tcp::socket&& TCPSock);
std::vector<uint8_t> TCPRcv(TClient& c);
void ClientKick(TClient& c, const std::string& R);
[[nodiscard]] bool SyncClient(const std::weak_ptr<TClient>& c);
void Identify(TConnection&& client);
std::shared_ptr<TClient> Authentication(TConnection&& ClientConnection);
void SyncResources(TClient& c);
[[nodiscard]] bool UDPSend(TClient& Client, std::vector<uint8_t> Data);
void SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self, bool Rel);
void UpdatePlayer(TClient& Client);
private:
void UDPServerMain();
void TCPServerMain();
TServer& mServer;
TPPSMonitor& mPPSMonitor;
ip::udp::socket mUDPSock;
TResourceManager& mResourceManager;
std::thread mUDPThread;
std::thread mTCPThread;
std::vector<uint8_t> UDPRcvFromClient(ip::udp::endpoint& ClientEndpoint);
void HandleDownload(TConnection&& TCPSock);
void OnConnect(const std::weak_ptr<TClient>& c);
void TCPClient(const std::weak_ptr<TClient>& c);
void Looper(const std::weak_ptr<TClient>& c);
int OpenID();
void OnDisconnect(const std::weak_ptr<TClient>& ClientPtr);
void Parse(TClient& c, const std::vector<uint8_t>& Packet);
void SendFile(TClient& c, const std::string& Name);
static bool TCPSendRaw(TClient& C, ip::tcp::socket& socket, const uint8_t* Data, size_t Size);
static void SplitLoad(TClient& c, size_t Sent, size_t Size, bool D, const std::string& Name);
static const uint8_t* SendSplit(TClient& c, ip::tcp::socket& Socket, const uint8_t* DataPtr, size_t Size);
};
std::string HashPassword(const std::string& str);
std::vector<uint8_t> StringToVector(const std::string& Str);

45
include/TPPSMonitor.h Normal file
View File

@@ -0,0 +1,45 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "Common.h"
#include "TServer.h"
#include <optional>
class TNetwork;
class TPPSMonitor : public IThreaded {
public:
explicit TPPSMonitor(TServer& Server);
virtual ~TPPSMonitor() {}
void operator()() override;
void SetInternalPPS(int NewPPS) { mInternalPPS = NewPPS; }
void IncrementInternalPPS() { ++mInternalPPS; }
[[nodiscard]] int InternalPPS() const { return mInternalPPS; }
void SetNetwork(TNetwork& Server) { mNetwork = std::ref(Server); }
private:
TNetwork& Network() { return mNetwork->get(); }
TServer& mServer;
std::optional<std::reference_wrapper<TNetwork>> mNetwork { std::nullopt };
int mInternalPPS { 0 };
};

40
include/TPluginMonitor.h Normal file
View File

@@ -0,0 +1,40 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "Common.h"
#include "IThreaded.h"
#include <atomic>
#include <memory>
#include <unordered_map>
class TLuaEngine;
class TPluginMonitor : IThreaded, public std::enable_shared_from_this<TPluginMonitor> {
public:
TPluginMonitor(const fs::path& Path, std::shared_ptr<TLuaEngine> Engine);
void operator()();
private:
std::shared_ptr<TLuaEngine> mEngine;
fs::path mPath;
std::unordered_map<std::string, fs::file_time_type> mFileTimes;
};

View File

@@ -0,0 +1,39 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "Common.h"
class TResourceManager {
public:
TResourceManager();
[[nodiscard]] size_t MaxModSize() const { return mMaxModSize; }
[[nodiscard]] std::string FileList() const { return mFileList; }
[[nodiscard]] std::string TrimmedList() const { return mTrimmedList; }
[[nodiscard]] std::string FileSizes() const { return mFileSizes; }
[[nodiscard]] int ModsLoaded() const { return mModsLoaded; }
private:
size_t mMaxModSize = 0;
std::string mFileSizes;
std::string mFileList;
std::string mTrimmedList;
int mModsLoaded = 0;
};

73
include/TServer.h Normal file
View File

@@ -0,0 +1,73 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "IThreaded.h"
#include "RWMutex.h"
#include "TScopedTimer.h"
#include <functional>
#include <memory>
#include <mutex>
#include <unordered_set>
#include "BoostAliases.h"
class TClient;
class TNetwork;
class TPPSMonitor;
class TServer final {
public:
using TClientSet = std::unordered_set<std::shared_ptr<TClient>>;
TServer(const std::vector<std::string_view>& Arguments);
void InsertClient(const std::shared_ptr<TClient>& Ptr);
void RemoveClient(const std::weak_ptr<TClient>&);
// in Fn, return true to continue, return false to break
void ForEachClient(const std::function<bool(std::weak_ptr<TClient>)>& Fn);
size_t ClientCount() const;
void GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uint8_t>&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network);
static void HandleEvent(TClient& c, const std::string& Data);
RWMutex& GetClientMutex() const { return mClientsMutex; }
const TScopedTimer UptimeTimer;
// asio io context
io_context& IoCtx() { return mIoCtx; }
private:
io_context mIoCtx {};
TClientSet mClients;
mutable RWMutex mClientsMutex;
static void ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network);
static bool ShouldSpawn(TClient& c, const std::string& CarJson, int ID);
static bool IsUnicycle(TClient& c, const std::string& CarJson);
static void Apply(TClient& c, int VID, const std::string& pckt);
void HandlePosition(TClient& c, const std::string& Packet);
};
struct BufferView {
uint8_t* Data { nullptr };
size_t Size { 0 };
const uint8_t* data() const { return Data; }
uint8_t* data() { return Data; }
size_t size() const { return Size; }
};

View File

@@ -1,269 +0,0 @@
#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, size_t max_depth = 50);

173
src/Client.cpp Normal file
View File

@@ -0,0 +1,173 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "Client.h"
#include "CustomAssert.h"
#include "TServer.h"
#include <memory>
#include <optional>
void TClient::DeleteCar(int Ident) {
// TODO: Send delete packets
std::unique_lock lock(mVehicleDataMutex);
auto iter = std::find_if(mVehicleData.begin(), mVehicleData.end(), [&](auto& elem) {
return Ident == elem.ID();
});
if (iter != mVehicleData.end()) {
mVehicleData.erase(iter);
} else {
beammp_debug("tried to erase a vehicle that doesn't exist (not an error)");
}
}
void TClient::ClearCars() {
std::unique_lock lock(mVehicleDataMutex);
mVehicleData.clear();
}
int TClient::GetOpenCarID() const {
int OpenID = 0;
bool found;
std::unique_lock lock(mVehicleDataMutex);
do {
found = true;
for (auto& v : mVehicleData) {
if (v.ID() == OpenID) {
OpenID++;
found = false;
}
}
} while (!found);
return OpenID;
}
void TClient::AddNewCar(int Ident, const std::string& Data) {
std::unique_lock lock(mVehicleDataMutex);
mVehicleData.emplace_back(Ident, Data);
}
TClient::TVehicleDataLockPair TClient::GetAllCars() {
return { &mVehicleData, std::unique_lock(mVehicleDataMutex) };
}
std::string TClient::GetCarPositionRaw(int Ident) {
std::unique_lock lock(mVehiclePositionMutex);
try {
return mVehiclePosition.at(size_t(Ident));
} catch (const std::out_of_range& oor) {
beammp_debugf("Failed to get vehicle position for {}: {}", Ident, oor.what());
return "";
}
}
void TClient::Disconnect(std::string_view Reason) {
beammp_debugf("Disconnecting client {} for reason: {}", GetID(), Reason);
boost::system::error_code ec;
mSocket.shutdown(socket_base::shutdown_both, ec);
if (ec) {
beammp_debugf("Failed to shutdown client socket: {}", ec.message());
}
mSocket.close(ec);
if (ec) {
beammp_debugf("Failed to close client socket: {}", ec.message());
}
}
void TClient::SetCarPosition(int Ident, const std::string& Data) {
std::unique_lock lock(mVehiclePositionMutex);
mVehiclePosition[size_t(Ident)] = Data;
}
std::string TClient::GetCarData(int Ident) {
{ // lock
std::unique_lock lock(mVehicleDataMutex);
for (auto& v : mVehicleData) {
if (v.ID() == Ident) {
return v.Data();
}
}
} // unlock
DeleteCar(Ident);
return "";
}
void TClient::SetCarData(int Ident, const std::string& Data) {
{ // lock
std::unique_lock lock(mVehicleDataMutex);
for (auto& v : mVehicleData) {
if (v.ID() == Ident) {
v.SetData(Data);
return;
}
}
} // unlock
DeleteCar(Ident);
}
int TClient::GetCarCount() const {
return int(mVehicleData.size());
}
TServer& TClient::Server() const {
return mServer;
}
void TClient::EnqueuePacket(const std::vector<uint8_t>& Packet) {
std::unique_lock Lock(mMissedPacketsMutex);
mPacketsSync.push(Packet);
}
TClient::TClient(TServer& Server, ip::tcp::socket&& Socket)
: mServer(Server)
, mSocket(std::move(Socket))
, mDownSocket(ip::tcp::socket(Server.IoCtx()))
, mLastPingTime(std::chrono::high_resolution_clock::now()) {
}
TClient::~TClient() {
beammp_debugf("client destroyed: {} ('{}')", this->GetID(), this->GetName());
}
void TClient::UpdatePingTime() {
mLastPingTime = std::chrono::high_resolution_clock::now();
}
int TClient::SecondsSinceLastPing() {
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::high_resolution_clock::now() - mLastPingTime)
.count();
return int(seconds);
}
std::optional<std::weak_ptr<TClient>> GetClient(TServer& Server, int ID) {
std::optional<std::weak_ptr<TClient>> MaybeClient { std::nullopt };
Server.ForEachClient([&](std::weak_ptr<TClient> CPtr) -> bool {
ReadLock Lock(Server.GetClientMutex());
if (!CPtr.expired()) {
auto C = CPtr.lock();
if (C->GetID() == ID) {
MaybeClient = CPtr;
return false;
}
} else {
beammp_debugf("Found an expired client while looking for id {}", ID);
}
return true;
});
return MaybeClient;
}

View File

@@ -1,101 +0,0 @@
#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) {
beammp_errorf("FileWatcher encountered error: {}", err.message());
// TODO: Should any further action be taken?
} else {
try {
check_files();
} catch (const std::exception& e) {
beammp_errorf("FileWatcher exception while checking files: {}", e.what());
}
try {
check_directories();
} catch (const std::exception& e) {
beammp_errorf("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);
sig_file_changed(file);;
m_file_mod_times.at(file) = time;
}
}
}
}

View File

@@ -18,6 +18,7 @@
#include "Http.h"
#include "Client.h"
#include "Common.h"
#include "CustomAssert.h"
#include "LuaAPI.h"
@@ -32,7 +33,7 @@
using json = nlohmann::json;
std::string Http::GET(const std::string& host, int port, const std::string& target, unsigned int* status) {
static thread_local httplib::SSLClient client(host, port);
httplib::SSLClient client(host, port);
client.enable_server_certificate_verification(false);
client.set_address_family(AF_INET);
auto res = client.Get(target.c_str());
@@ -47,7 +48,7 @@ std::string Http::GET(const std::string& host, int port, const std::string& targ
}
std::string Http::POST(const std::string& host, int port, const std::string& target, const std::string& body, const std::string& ContentType, unsigned int* status, const httplib::Headers& headers) {
static thread_local httplib::SSLClient client(host, port);
httplib::SSLClient client(host, port);
client.set_read_timeout(std::chrono::seconds(10));
beammp_assert(client.is_valid());
client.enable_server_certificate_verification(false);
@@ -198,7 +199,7 @@ void Http::Server::THttpServerInstance::operator()() try {
}
}
res.set_content(
nlohmann::json {
json {
{ "ok", SystemsBad == 0 },
}
.dump(),

View File

@@ -17,17 +17,89 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "LuaAPI.h"
#include "Client.h"
#include "Common.h"
#include "CustomAssert.h"
#include "Value.h"
#include "TLuaEngine.h"
#include <boost/json/serialize.hpp>
#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";
@@ -42,6 +114,15 @@ 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();
@@ -51,26 +132,24 @@ TEST_CASE("LuaAPI::MP::GetServerVersion") {
}
static inline std::pair<bool, std::string> InternalTriggerClientEvent(int PlayerID, const std::string& EventName, const std::string& Data) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
std::string Packet = "E:" + EventName + ":" + Data;
if (PlayerID == -1) {
LuaAPI::MP::Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
return { true, "" };
} else {
auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID);
if (!MaybeClient) {
beammp_errorff("TriggerClientEvent invalid Player ID '{}'", PlayerID);
return { false, "Invalid Player ID" };
}
auto c = MaybeClient.value();
if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorff("Respond failed, dropping client {}", PlayerID);
LuaAPI::MP::Engine->Network().Disconnect(*c);
return { false, "Respond failed, dropping client" };
}
return { true, "" };
}*/
std::string Packet = "E:" + EventName + ":" + Data;
if (PlayerID == -1) {
LuaAPI::MP::Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
return { true, "" };
} else {
auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID);
if (!MaybeClient || MaybeClient.value().expired()) {
beammp_lua_errorf("TriggerClientEvent invalid Player ID '{}'", PlayerID);
return { false, "Invalid Player ID" };
}
auto c = MaybeClient.value().lock();
if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_lua_errorf("Respond failed, dropping client {}", PlayerID);
LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets");
return { false, "Respond failed, dropping client" };
}
return { true, "" };
}
}
std::pair<bool, std::string> LuaAPI::MP::TriggerClientEvent(int PlayerID, const std::string& EventName, const sol::object& DataObj) {
@@ -79,22 +158,17 @@ std::pair<bool, std::string> LuaAPI::MP::TriggerClientEvent(int PlayerID, const
}
std::pair<bool, std::string> LuaAPI::MP::DropPlayer(int ID, std::optional<std::string> MaybeReason) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
auto MaybeClient = GetClient(Engine->Server(), ID);
if (!MaybeClient) {
beammp_errorff("Tried to drop client with id {}, who doesn't exist", ID);
if (!MaybeClient || MaybeClient.value().expired()) {
beammp_lua_errorf("Tried to drop client with id {}, who doesn't exist", ID);
return { false, "Player does not exist" };
}
auto c = MaybeClient.value();
auto c = MaybeClient.value().lock();
LuaAPI::MP::Engine->Network().ClientKick(*c, MaybeReason.value_or("No reason"));
return { true, "" };
*/
}
std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::string& Message) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
std::pair<bool, std::string> Result;
std::string Packet = "C:Server: " + Message;
if (ID == -1) {
@@ -103,43 +177,39 @@ std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::stri
Result.first = true;
} else {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
auto c = MaybeClient.value();
if (!c->IsSynced) {
if (MaybeClient && !MaybeClient.value().expired()) {
auto c = MaybeClient.value().lock();
if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player still syncing data";
return Result;
}
LogChatMessage("<Server> (to \"" + c->Name.get() + "\")", -1, Message);
LogChatMessage("<Server> (to \"" + c->GetName() + "\")", -1, Message);
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send chat message back to sender (id {}) - did the sender disconnect?", ID);
beammp_infof("Disconnecting client {} for failure to receive a chat message (TCP disconnect)", c->Name.get());
Engine->Network().Disconnect(c);
// TODO: should we return an error here?
}
Result.first = true;
} else {
beammp_errorf("SendChatMessage invalid argument [1] invalid ID");
beammp_lua_error("SendChatMessage invalid argument [1] invalid ID");
Result.first = false;
Result.second = "Invalid Player ID";
}
return Result;
}
return Result;
*/
}
std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
std::pair<bool, std::string> Result;
auto MaybeClient = GetClient(Engine->Server(), PID);
if (!MaybeClient) {
beammp_errorf("RemoveVehicle invalid Player ID");
if (!MaybeClient || MaybeClient.value().expired()) {
beammp_lua_error("RemoveVehicle invalid Player ID");
Result.first = false;
Result.second = "Invalid Player ID";
return Result;
}
auto c = MaybeClient.value();
auto c = MaybeClient.value().lock();
if (!c->GetCarData(VID).empty()) {
std::string Destroy = "Od:" + std::to_string(PID) + "-" + std::to_string(VID);
Engine->Network().SendToAll(nullptr, StringToVector(Destroy), true, true);
@@ -150,7 +220,6 @@ std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) {
Result.second = "Vehicle does not exist";
}
return Result;
*/
}
void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
@@ -160,7 +229,7 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
Application::Settings.DebugModeEnabled = NewValue.as<bool>();
beammp_info(std::string("Set `Debug` to ") + (Application::Settings.DebugModeEnabled ? "true" : "false"));
} else {
beammp_errorf("set invalid argument [2] expected boolean");
beammp_lua_error("set invalid argument [2] expected boolean");
}
break;
case 1: // private
@@ -168,7 +237,7 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
Application::Settings.Private = NewValue.as<bool>();
beammp_info(std::string("Set `Private` to ") + (Application::Settings.Private ? "true" : "false"));
} else {
beammp_errorf("set invalid argument [2] expected boolean");
beammp_lua_error("set invalid argument [2] expected boolean");
}
break;
case 2: // max cars
@@ -176,7 +245,7 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
Application::Settings.MaxCars = NewValue.as<int>();
beammp_info(std::string("Set `MaxCars` to ") + std::to_string(Application::Settings.MaxCars));
} else {
beammp_errorf("set invalid argument [2] expected integer");
beammp_lua_error("set invalid argument [2] expected integer");
}
break;
case 3: // max players
@@ -184,7 +253,7 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
Application::Settings.MaxPlayers = NewValue.as<int>();
beammp_info(std::string("Set `MaxPlayers` to ") + std::to_string(Application::Settings.MaxPlayers));
} else {
beammp_errorf("set invalid argument [2] expected integer");
beammp_lua_error("set invalid argument [2] expected integer");
}
break;
case 4: // Map
@@ -192,7 +261,7 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
Application::Settings.MapName = NewValue.as<std::string>();
beammp_info(std::string("Set `Map` to ") + Application::Settings.MapName);
} else {
beammp_errorf("set invalid argument [2] expected string");
beammp_lua_error("set invalid argument [2] expected string");
}
break;
case 5: // Name
@@ -200,7 +269,7 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
Application::Settings.ServerName = NewValue.as<std::string>();
beammp_info(std::string("Set `Name` to ") + Application::Settings.ServerName);
} else {
beammp_errorf("set invalid argument [2] expected string");
beammp_lua_error("set invalid argument [2] expected string");
}
break;
case 6: // Desc
@@ -208,7 +277,7 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
Application::Settings.ServerDesc = NewValue.as<std::string>();
beammp_info(std::string("Set `Description` to ") + Application::Settings.ServerDesc);
} else {
beammp_errorf("set invalid argument [2] expected string");
beammp_lua_error("set invalid argument [2] expected string");
}
break;
default:
@@ -217,27 +286,42 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
}
}
bool LuaAPI::MP::IsPlayerConnected(int ID) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
return MaybeClient.value()->IsConnected.get();
} else {
return false;
}*/
void LuaAPI::MP::Sleep(size_t Ms) {
std::this_thread::sleep_for(std::chrono::milliseconds(Ms));
}
bool LuaAPI::MP::IsPlayerGuest(int ID) {
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
bool LuaAPI::MP::IsPlayerConnected(int ID) {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
return MaybeClient.value()->IsGuest.get();
if (MaybeClient && !MaybeClient.value().expired()) {
return MaybeClient.value().lock()->IsConnected();
} else {
return false;
}
*/
}
bool LuaAPI::MP::IsPlayerGuest(int ID) {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient && !MaybeClient.value().expired()) {
return MaybeClient.value().lock()->IsGuest();
} else {
return false;
}
}
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;
}
template <typename FnT, typename... ArgsT>
@@ -443,7 +527,7 @@ std::string LuaAPI::FS::ConcatPaths(sol::variadic_args Args) {
for (size_t i = 0; i < Args.size(); ++i) {
auto Obj = Args[i];
if (!Obj.is<std::string>()) {
beammp_errorf("FS.Concat called with non-string argument");
beammp_lua_error("FS.Concat called with non-string argument");
return "";
}
Path += Obj.as<std::string>();
@@ -457,7 +541,7 @@ std::string LuaAPI::FS::ConcatPaths(sol::variadic_args Args) {
static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, const sol::object& right, bool is_array, size_t depth = 0) {
if (depth > 100) {
beammp_errorf("json serialize will not go deeper than 100 nested tables, internal references assumed, aborted this path");
beammp_lua_error("json serialize will not go deeper than 100 nested tables, internal references assumed, aborted this path");
return;
}
std::string key {};
@@ -471,7 +555,7 @@ static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, c
case sol::type::thread:
case sol::type::function:
case sol::type::table:
beammp_errorf("JsonEncode: left side of table field is unexpected type");
beammp_lua_error("JsonEncode: left side of table field is unexpected type");
return;
case sol::type::string:
key = left.as<std::string>();
@@ -533,33 +617,28 @@ static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, c
}
}
static std::string lua_to_json_impl(const sol::object& obj) {
// used as the invalid value provider in sol_obj_to_value.
auto special_stringifier = [](const sol::object& object) -> Result<Value> {
beammp_debugf("Cannot convert from type {} to json, ignoring (using null)", int(object.get_type()));
return { Null {} };
};
auto maybe_val = sol_obj_to_value(obj, special_stringifier);
if (maybe_val) {
auto result = boost::apply_visitor(ValueToJsonVisitor {}, maybe_val.move());
return boost::json::serialize(result);
} else {
beammp_errorf("Failed to convert an argument to json: {}", maybe_val.error);
return "";
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;
}
}
for (const auto& entry : object) {
JsonEncodeRecursive(json, entry.first, entry.second, is_array);
}
return json.dump();
}
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) {
std::string LuaAPI::MP::JsonDiff(const std::string& a, const std::string& b) {
if (!nlohmann::json::accept(a)) {
beammp_error("JsonDiff first argument is not valid json: `" + a + "`");
beammp_lua_error("JsonDiff first argument is not valid json: `" + a + "`");
return "";
}
if (!nlohmann::json::accept(b)) {
beammp_error("JsonDiff second argument is not valid json: `" + b + "`");
beammp_lua_error("JsonDiff second argument is not valid json: `" + b + "`");
return "";
}
auto a_json = nlohmann::json::parse(a);
@@ -567,13 +646,13 @@ std::string LuaAPI::Util::JsonDiff(const std::string& a, const std::string& b) {
return nlohmann::json::diff(a_json, b_json).dump();
}
std::string LuaAPI::Util::JsonDiffApply(const std::string& data, const std::string& patch) {
std::string LuaAPI::MP::JsonDiffApply(const std::string& data, const std::string& patch) {
if (!nlohmann::json::accept(data)) {
beammp_error("JsonDiffApply first argument is not valid json: `" + data + "`");
beammp_lua_error("JsonDiffApply first argument is not valid json: `" + data + "`");
return "";
}
if (!nlohmann::json::accept(patch)) {
beammp_error("JsonDiffApply second argument is not valid json: `" + patch + "`");
beammp_lua_error("JsonDiffApply second argument is not valid json: `" + patch + "`");
return "";
}
auto a_json = nlohmann::json::parse(data);
@@ -582,145 +661,38 @@ std::string LuaAPI::Util::JsonDiffApply(const std::string& data, const std::stri
return a_json.dump();
}
std::string LuaAPI::Util::JsonPrettify(const std::string& json) {
std::string LuaAPI::MP::JsonPrettify(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_error("JsonPrettify argument is not valid json: `" + json + "`");
beammp_lua_error("JsonPrettify argument is not valid json: `" + json + "`");
return "";
}
return nlohmann::json::parse(json).dump(4);
}
std::string LuaAPI::Util::JsonMinify(const std::string& json) {
std::string LuaAPI::MP::JsonMinify(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_error("JsonMinify argument is not valid json: `" + json + "`");
beammp_lua_error("JsonMinify argument is not valid json: `" + json + "`");
return "";
}
return nlohmann::json::parse(json).dump(-1);
}
std::string LuaAPI::Util::JsonFlatten(const std::string& json) {
std::string LuaAPI::MP::JsonFlatten(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_error("JsonFlatten argument is not valid json: `" + json + "`");
beammp_lua_error("JsonFlatten argument is not valid json: `" + json + "`");
return "";
}
return nlohmann::json::parse(json).flatten().dump(-1);
}
std::string LuaAPI::Util::JsonUnflatten(const std::string& json) {
std::string LuaAPI::MP::JsonUnflatten(const std::string& json) {
if (!nlohmann::json::accept(json)) {
beammp_error("JsonUnflatten argument is not valid json: `" + json + "`");
beammp_lua_error("JsonUnflatten argument is not valid json: `" + json + "`");
return "";
}
return nlohmann::json::parse(json).unflatten().dump(-1);
}
std::pair<bool, std::string> LuaAPI::MP::TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data) {
return InternalTriggerClientEvent(PlayerID, EventName, Util::JsonEncode(Data));
}
size_t LuaAPI::MP::GetPlayerCount() { return 0; }
template <typename T>
static void AddToTable(sol::table& table, const std::string& left, const T& value) {
if (left.empty()) {
table[table.size() + 1] = value;
} else {
table[left] = value;
}
}
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_errorf("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& str) {
sol::state_view StateView(s.lua_state());
auto table = StateView.create_table(StateView);
if (!nlohmann::json::accept(str)) {
beammp_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_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 = sol::state_view(s.lua_state()).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 = sol::state_view(s.lua_state()).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;
return InternalTriggerClientEvent(PlayerID, EventName, JsonEncode(Data));
}

View File

@@ -1,703 +0,0 @@
#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 <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("[{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_infof(...) beammp_infof("[{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_warnf(...) beammp_warnf("[{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_errorf(...) beammp_errorf("[{}] {}", name(), fmt::format(__VA_ARGS__))
#define beammp_lua_tracef(...) beammp_tracef("[{}] {}", 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.begin(), args.end() });
beammp_lua_errorf("[out] {}", result);
};
glob["MP"]["LogWarn"] = [this](const sol::variadic_args& args) {
auto result = print_impl({ args.begin(), args.end() });
beammp_lua_warnf("[out] {}", result);
};
glob["MP"]["LogInfo"] = [this](const sol::variadic_args& args) {
auto result = print_impl({ args.begin(), args.end() });
beammp_lua_infof("[out] {}", result);
};
glob["MP"]["LogDebug"] = [this](const sol::variadic_args& args) {
auto result = print_impl({ args.begin(), args.end() });
beammp_lua_debugf("[out] {}", result);
};
glob["MP"]["GetPluginPath"] = [this] {
return std::filesystem::absolute(m_path).string();
};
glob["MP"]["RegisterEvent"] = [this](const std::string& event_name, const sol::object& handler) {
if (handler.get_type() == sol::type::string) {
m_event_handlers_named[event_name].push_back(handler.as<std::string>());
} else if (handler.get_type() == sol::type::function) {
auto fn = handler.as<sol::protected_function>();
fn.set_error_handler(m_state.globals()[ERR_HANDLER]);
m_event_handlers[event_name].push_back(fn);
} else {
beammp_lua_errorf("Invalid call to MP.RegisterEvent for event '{}': Expected string or function as second argument", event_name);
}
};
glob["MP"]["Post"] = [this](const sol::protected_function& fn) {
boost::asio::post(m_io, [fn] {
fn();
});
};
glob["MP"]["TriggerLocalEvent"] = [this](const std::string& event_name, sol::variadic_args args) -> sol::table {
std::vector<sol::object> args_vec(args.begin(), args.end());
ValueTuple values {};
values.reserve(args_vec.size());
for (const auto& obj : args_vec) {
auto res = sol_obj_to_value(obj);
if (res) [[likely]] {
values.emplace_back(res.move());
} else {
beammp_lua_errorf("Can't serialize an argument across boundaries (for passing to a MP.TriggerLocalEvent): {}", res.error);
values.emplace_back(Null {});
}
}
auto results = handle_event(event_name, std::make_shared<Value>(std::move(values)));
auto result = m_state.create_table();
result["__INTERNAL"] = results;
// waits for a specific number of milliseconds for a result to be available, returns false if timed out, true if ready.
result["WaitForMS"] = [](const sol::object& self, int milliseconds) {
std::shared_future<std::vector<Value>> future = self.as<sol::table>()["__INTERNAL"];
auto status = future.wait_for(std::chrono::milliseconds(milliseconds));
return status == std::future_status::ready;
};
// waits indefinitely for all results to be available
result["Wait"] = [](const sol::table& self) {
std::shared_future<std::vector<Value>> future = self["__INTERNAL"];
future.wait();
};
// waits indefinitely for all results to be available, then returns them
result["Results"] = [this](const sol::table& self) -> sol::table {
std::shared_future<std::vector<Value>> future = self["__INTERNAL"];
future.wait();
Value results = future.get();
auto lua_res = boost::apply_visitor(ValueToLuaVisitor(m_state), results);
return lua_res;
};
return result;
};
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("MP.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 :)
m_extensions_watch_conn = m_extensions_watcher.sig_file_changed.connect(
[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());
}
if (m_path != BEAMMP_MEMORY_STATE) {
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::vector<Value>> LuaPlugin::handle_event(const std::string& event_name, const std::shared_ptr<Value>& args) {
std::shared_ptr<std::promise<std::vector<Value>>> promise = std::make_shared<std::promise<std::vector<Value>>>();
std::shared_future<std::vector<Value>> futures { promise->get_future() };
boost::asio::post(m_io, [this, event_name, args, promise, futures] {
std::vector<Value> results {};
if (m_event_handlers_named.contains(event_name)) {
auto handler_names = m_event_handlers_named.at(event_name);
for (const auto& handler_name : handler_names) {
try {
if (m_state.globals()[handler_name].valid() && m_state.globals()[handler_name].get_type() == sol::type::function) {
auto fn = m_state.globals().get<sol::protected_function>(handler_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) {
results.emplace_back(maybe_res.move());
} else {
beammp_lua_errorf("Error using return value from event handler '{}' for event '{}': {}", handler_name, event_name, maybe_res.error);
}
}
} else {
beammp_lua_errorf("Invalid event handler '{}' for event '{}': Handler either doesn't exist or isn't a global function", handler_name, 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));
}
}
}
if (m_event_handlers.contains(event_name)) {
auto handlers = m_event_handlers.at(event_name);
for (const auto& fn : handlers) {
try {
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) {
results.emplace_back(maybe_res.move());
} else {
beammp_lua_errorf("Error using return value from event handler '<<lua function {:p}>>' for event '{}': {}", fn.pointer(), event_name, maybe_res.error);
}
}
} 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::move(results));
});
return futures;
}
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.begin(), args.end() });
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("MP.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 MP.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("MP.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 MP.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 std::vector<sol::object>& obj_args) {
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;
}
void LuaPlugin::run_raw_lua(const std::string& raw) {
if (raw.empty()) {
return;
}
std::string id = "";
if (raw.size() < 5) {
id = raw;
} else {
id = raw.substr(0, 5) + "...";
}
boost::asio::post(m_io, [this, raw, id] {
try {
beammp_debugf("Running '{}'", raw);
auto res = m_state.safe_script(raw, id);
if (res.valid()) {
std::vector<sol::object> args;
args.push_back(res.get<sol::object>());
auto str = print_impl(args);
Application::Console().WriteRaw(fmt::format("=> {}", str));
}
} catch (const sol::error& err) {
beammp_lua_errorf("Error: {}", err.what());
}
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,8 +20,10 @@
#include "Common.h"
#include "Compat.h"
#include "Client.h"
#include "CustomAssert.h"
#include "LuaAPI.h"
#include "TLuaEngine.h"
#include <ctime>
#include <mutex>
@@ -153,7 +155,6 @@ void TConsole::StartLoggingToFile() {
void TConsole::ChangeToLuaConsole(const std::string& LuaStateId) {
if (!mIsLuaConsole) {
/*
if (!mLuaEngine) {
beammp_error("Lua engine not initialized yet, please wait and try again");
return;
@@ -169,7 +170,7 @@ void TConsole::ChangeToLuaConsole(const std::string& LuaStateId) {
mCommandline->set_prompt("lua> ");
}
mCachedRegularHistory = mCommandline->history();
mCommandline->set_history(mCachedLuaHistory);*/
mCommandline->set_history(mCachedLuaHistory);
}
}
@@ -222,17 +223,13 @@ void TConsole::Command_Lua(const std::string&, const std::vector<std::string>& a
if (args.size() == 1) {
auto NewStateId = args.at(0);
beammp_assert(!NewStateId.empty());
// TODO: This must be implemented
/*
if (mLuaEngine->HasState(NewStateId)) {
ChangeToLuaConsole(NewStateId);
} else {
Application::Console().WriteRaw("Lua state '" + NewStateId + "' is not a known state. Didn't switch to Lua.");
}
*/
} else if (args.size() == 0) {
ChangeToLuaConsole(mDefaultStateId);
// CONTINUE HERE
}
}
@@ -287,15 +284,17 @@ void TConsole::Command_Kick(const std::string&, const std::vector<std::string>&
std::for_each(Name2.begin(), Name2.end(), [](char& c) { c = char(std::tolower(char(c))); });
return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1);
};
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*mLuaEngine->Server().ForEachClient([&](const std::shared_ptr<TClient>& Client) -> bool {
if (NameCompare(Client->Name.get(), Name)) {
mLuaEngine->Network().ClientKick(*Client, Reason);
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto locked = Client.lock();
if (NameCompare(locked->GetName(), Name)) {
mLuaEngine->Network().ClientKick(*locked, Reason);
Kicked = true;
return false;
}
}
return true;
});*/
});
if (!Kicked) {
Application::Console().WriteRaw("Error: No player with name matching '" + Name + "' was found.");
} else {
@@ -378,24 +377,23 @@ void TConsole::Command_List(const std::string&, const std::vector<std::string>&
if (!EnsureArgsCount(args, 0)) {
return;
}
// Implement once running
throw std::runtime_error(fmt::format("NOT IMPLEMENTED: {}", __func__));
/*
if (mLuaEngine->Server().ClientCount() == 0) {
Application::Console().WriteRaw("No players online.");
} else {
std::stringstream ss;
ss << std::left << std::setw(25) << "Name" << std::setw(6) << "ID" << std::setw(6) << "Cars" << std::endl;
mLuaEngine->Server().ForEachClient([&](const std::shared_ptr<TClient>& Client) -> bool {
ss << std::left << std::setw(25) << Client->Name.get()
<< std::setw(6) << Client->ID.get()
<< std::setw(6) << Client->GetCarCount() << "\n";
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto locked = Client.lock();
ss << std::left << std::setw(25) << locked->GetName()
<< std::setw(6) << locked->GetID()
<< std::setw(6) << locked->GetCarCount() << "\n";
}
return true;
});
auto Str = ss.str();
Application::Console().WriteRaw(Str.substr(0, Str.size() - 1));
}
*/
}
void TConsole::Command_Status(const std::string&, const std::vector<std::string>& args) {
@@ -404,6 +402,29 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
}
std::stringstream Status;
size_t CarCount = 0;
size_t ConnectedCount = 0;
size_t GuestCount = 0;
size_t SyncedCount = 0;
size_t SyncingCount = 0;
size_t MissedPacketQueueSum = 0;
int LargestSecondsSinceLastPing = 0;
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto Locked = Client.lock();
CarCount += Locked->GetCarCount();
ConnectedCount += Locked->IsConnected() ? 1 : 0;
GuestCount += Locked->IsGuest() ? 1 : 0;
SyncedCount += Locked->IsSynced() ? 1 : 0;
SyncingCount += Locked->IsSyncing() ? 1 : 0;
MissedPacketQueueSum += Locked->MissedPacketQueueSize();
if (Locked->SecondsSinceLastPing() < LargestSecondsSinceLastPing) {
LargestSecondsSinceLastPing = Locked->SecondsSinceLastPing();
}
}
return true;
});
size_t SystemsStarting = 0;
size_t SystemsGood = 0;
size_t SystemsBad = 0;
@@ -448,22 +469,15 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
SystemsShuttingDownList = SystemsShuttingDownList.substr(0, SystemsShuttingDownList.size() - 2);
SystemsShutdownList = SystemsShutdownList.substr(0, SystemsShutdownList.size() - 2);
auto ElapsedTime = mUptimeTimer.GetElapsedTime();
/*
auto network = mLuaEngine->Network();
auto clients = network->all_clients();
auto ElapsedTime = mLuaEngine->Server().UptimeTimer.GetElapsedTime();
Status << "BeamMP-Server Status:\n"
<< "\tTotal Players: " << clients.size() << "\n"
<< "\tPlayers identifying: " << network->clients_in_state_count(bmp::State::Identification) << "\n"
<< "\tPlayers authenticating: " << network->clients_in_state_count(bmp::State::Authentication) << "\n"
<< "\tPlayers downloading mods: " << network->clients_in_state_count(bmp::State::ModDownload) << "\n"
<< "\tPlayers spawning in: " << network->clients_in_state_count(bmp::State::SessionSetup) << "\n"
<< "\tPlayers playing: " << network->clients_in_state_count(bmp::State::Playing) << "\n"
<< "\tPlayers leaving: " << network->clients_in_state_count(bmp::State::Leaving) << "\n"
<< "\tGuests: " << network->guest_count() << "\n"
<< "\tVehicles: " << network->vehicle_count() << "\n"
<< "\tTotal Players: " << mLuaEngine->Server().ClientCount() << "\n"
<< "\tSyncing Players: " << SyncingCount << "\n"
<< "\tSynced Players: " << SyncedCount << "\n"
<< "\tConnected Players: " << ConnectedCount << "\n"
<< "\tGuests: " << GuestCount << "\n"
<< "\tCars: " << CarCount << "\n"
<< "\tUptime: " << ElapsedTime << "ms (~" << size_t(double(ElapsedTime) / 1000.0 / 60.0 / 60.0) << "h) \n"
<< "\tLua:\n"
<< "\t\tQueued results to check: " << mLuaEngine->GetResultsToCheckSize() << "\n"
@@ -478,13 +492,12 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
<< "\t\tBad: [ " << SystemsBadList << " ]\n"
<< "\t\tShutting down: [ " << SystemsShuttingDownList << " ]\n"
<< "\t\tShut down: [ " << SystemsShutdownList << " ]\n"
<< "";*/
<< "";
Application::Console().WriteRaw(Status.str());
}
void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
/*
auto FutureIsNonNil =
[](const std::shared_ptr<TLuaResult>& Future) {
if (!Future->Error && Future->Result.valid()) {
@@ -528,11 +541,9 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
}
Application::Console().WriteRaw(Reply.str());
}
*/
}
void TConsole::HandleLuaInternalCommand(const std::string& cmd) {
/*
if (cmd == "exit") {
ChangeToRegularConsole();
} else if (cmd == "queued") {
@@ -582,7 +593,6 @@ Commands
} else {
beammp_error("internal command '" + cmd + "' is not known");
}
*/
}
TConsole::TConsole() {
@@ -601,7 +611,6 @@ void TConsole::InitializeCommandline() {
auto [cmd, args] = ParseCommand(TrimmedCmd);
mCommandline->write(mCommandline->prompt() + TrimmedCmd);
if (mIsLuaConsole) {
/*
if (!mLuaEngine) {
beammp_info("Lua not started yet, please try again in a second");
} else if (!cmd.empty() && cmd.at(0) == ':') {
@@ -613,9 +622,7 @@ void TConsole::InitializeCommandline() {
beammp_lua_error("error in " + mStateId + ": " + Future->ErrorMessage);
}
}
*/
} else {
/*
if (!mLuaEngine) {
beammp_error("Attempted to run a command before Lua engine started. Please wait and try again.");
} else if (cmd == "exit") {
@@ -631,7 +638,7 @@ void TConsole::InitializeCommandline() {
} else {
RunAsCommand(TrimmedCmd);
}
}*/
}
}
} catch (const std::exception& e) {
beammp_error("Console died with: " + std::string(e.what()) + ". This could be a fatal error and could cause the server to terminate.");
@@ -641,7 +648,6 @@ void TConsole::InitializeCommandline() {
std::vector<std::string> suggestions;
try {
if (mIsLuaConsole) { // if lua
/*
if (!mLuaEngine) {
beammp_info("Lua not started yet, please try again in a second");
} else {
@@ -663,6 +669,7 @@ void TConsole::InitializeCommandline() {
if (stub.rfind('.') != stub.size() - 1 && !tablekeys.empty()) {
tablekeys.pop_back();
}
auto keys = mLuaEngine->GetStateTableKeysForState(mStateId, tablekeys);
for (const auto& key : keys) { // go through each bottom-level key
@@ -678,18 +685,17 @@ void TConsole::InitializeCommandline() {
suggestions.push_back(prefix + before_last_atom + key);
}
}
}*/
}
} else { // if not lua
if (stub.find("lua") == 0) { // starts with "lua" means we should suggest state names
std::string after_prefix = TrimString(stub.substr(3));
/*auto stateNames = mLuaEngine->GetLuaStateNames();
auto stateNames = mLuaEngine->GetLuaStateNames();
for (const auto& name : stateNames) {
if (name.find(after_prefix) == 0) {
suggestions.push_back("lua " + name);
}
}
*/
} else {
for (const auto& [cmd_name, cmd_fn] : mCommandMap) {
if (cmd_name.find(stub) == 0) {
@@ -725,8 +731,6 @@ void TConsole::WriteRaw(const std::string& str) {
}
}
/*
void TConsole::InitializeLuaConsole(TLuaEngine& Engine) {
mLuaEngine = &Engine;
}
*/

View File

@@ -18,8 +18,9 @@
#include "THeartbeatThread.h"
#include "Client.h"
#include "Http.h"
// #include "SocketIO.h"
//#include "SocketIO.h"
#include <rapidjson/document.h>
#include <rapidjson/rapidjson.h>
#include <sstream>
@@ -138,7 +139,7 @@ std::string THeartbeatThread::GenerateCall() {
std::stringstream Ret;
Ret << "uuid=" << Application::Settings.Key
<< "&players=" << m_network->authenticated_client_count()
<< "&players=" << mServer.ClientCount()
<< "&maxplayers=" << Application::Settings.MaxPlayers
<< "&port=" << Application::Settings.Port
<< "&map=" << Application::Settings.MapName
@@ -147,16 +148,17 @@ std::string THeartbeatThread::GenerateCall() {
<< "&clientversion=" << std::to_string(Application::ClientMajorVersion()) + ".0" // FIXME: Wtf.
<< "&name=" << Application::Settings.ServerName
<< "&tags=" << Application::Settings.ServerTags
<< "&modlist=" << "-"//mResourceManager.TrimmedList()
<< "&modstotalsize=" << 0 //mResourceManager.MaxModSize()
<< "&modstotal=" << 0 // mResourceManager.ModsLoaded()
<< "&modlist=" << mResourceManager.TrimmedList()
<< "&modstotalsize=" << mResourceManager.MaxModSize()
<< "&modstotal=" << mResourceManager.ModsLoaded()
<< "&playerslist=" << GetPlayers()
<< "&desc=" << Application::Settings.ServerDesc
<< "&pass=" << (Application::Settings.Password.empty() ? "false" : "true");
return Ret.str();
}
THeartbeatThread::THeartbeatThread(std::shared_ptr<Network> network)
: m_network(std::move(network)) {
THeartbeatThread::THeartbeatThread(TResourceManager& ResourceManager, TServer& Server)
: mResourceManager(ResourceManager)
, mServer(Server) {
Application::SetSubsystemStatus("Heartbeat", Application::Status::Starting);
Application::RegisterShutdownHandler([&] {
Application::SetSubsystemStatus("Heartbeat", Application::Status::ShuttingDown);
@@ -168,10 +170,15 @@ THeartbeatThread::THeartbeatThread(std::shared_ptr<Network> network)
Start();
}
std::string THeartbeatThread::GetPlayers() {
std::string players;
for (const auto& [id, client] : m_network->authenticated_clients()) {
players += client->name.get() + ";";
}
return players;
std::string Return;
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
Return += ClientPtr.lock()->GetName() + ";";
}
return true;
});
return Return;
}
/*THeartbeatThread::~THeartbeatThread() {
}*/

1121
src/TLuaEngine.cpp Normal file

File diff suppressed because it is too large Load Diff

70
src/TLuaPlugin.cpp Normal file
View File

@@ -0,0 +1,70 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TLuaPlugin.h"
#include <chrono>
#include <functional>
#include <random>
#include <utility>
TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const fs::path& MainFolder)
: mConfig(Config)
, mEngine(Engine)
, mFolder(MainFolder)
, mPluginName(MainFolder.stem().string())
, mFileContents(0) {
beammp_debug("Lua plugin \"" + mPluginName + "\" starting in \"" + mFolder.string() + "\"");
std::vector<fs::path> Entries;
for (const auto& Entry : fs::directory_iterator(mFolder)) {
if (Entry.is_regular_file() && Entry.path().extension() == ".lua") {
Entries.push_back(Entry);
}
}
// sort alphabetically (not needed if config is used to determine call order)
// TODO: Use config to figure out what to run in which order
std::sort(Entries.begin(), Entries.end(), [](const fs::path& first, const fs::path& second) {
auto firstStr = first.string();
auto secondStr = second.string();
std::transform(firstStr.begin(), firstStr.end(), firstStr.begin(), ::tolower);
std::transform(secondStr.begin(), secondStr.end(), secondStr.begin(), ::tolower);
return firstStr < secondStr;
});
std::vector<std::pair<fs::path, std::shared_ptr<TLuaResult>>> ResultsToCheck;
for (const auto& Entry : Entries) {
// read in entire file
try {
std::ifstream FileStream(Entry.string(), std::ios::in | std::ios::binary);
auto Size = std::filesystem::file_size(Entry);
auto Contents = std::make_shared<std::string>();
Contents->resize(Size);
FileStream.read(Contents->data(), Contents->size());
mFileContents[fs::relative(Entry).string()] = Contents;
// Execute first time
auto Result = mEngine.EnqueueScript(mConfig.StateId, TLuaChunk(Contents, Entry.string(), MainFolder.string()));
ResultsToCheck.emplace_back(Entry.string(), std::move(Result));
} catch (const std::exception& e) {
beammp_error("Error loading file \"" + Entry.string() + "\": " + e.what());
}
}
for (auto& Result : ResultsToCheck) {
Result.second->WaitUntilReady();
if (Result.second->Error) {
beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Result.second->ErrorMessage);
}
}
}

1017
src/TNetwork.cpp Normal file

File diff suppressed because it is too large Load Diff

87
src/TPPSMonitor.cpp Normal file
View File

@@ -0,0 +1,87 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TPPSMonitor.h"
#include "Client.h"
#include "TNetwork.h"
TPPSMonitor::TPPSMonitor(TServer& Server)
: mServer(Server) {
Application::SetSubsystemStatus("PPSMonitor", Application::Status::Starting);
Application::SetPPS("-");
Application::RegisterShutdownHandler([&] {
Application::SetSubsystemStatus("PPSMonitor", Application::Status::ShuttingDown);
if (mThread.joinable()) {
beammp_debug("shutting down PPSMonitor");
mThread.join();
beammp_debug("shut down PPSMonitor");
}
Application::SetSubsystemStatus("PPSMonitor", Application::Status::Shutdown);
});
Start();
}
void TPPSMonitor::operator()() {
RegisterThread("PPSMonitor");
while (!mNetwork) {
// hard(-ish) spin
std::this_thread::yield();
}
beammp_debug("PPSMonitor starting");
Application::SetSubsystemStatus("PPSMonitor", Application::Status::Good);
std::vector<std::shared_ptr<TClient>> TimedOutClients;
while (!Application::IsShuttingDown()) {
std::this_thread::sleep_for(std::chrono::seconds(1));
int C = 0, V = 0;
if (mServer.ClientCount() == 0) {
Application::SetPPS("-");
continue;
}
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
std::shared_ptr<TClient> c;
{
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
c = ClientPtr.lock();
} else
return true;
}
if (c->GetCarCount() > 0) {
C++;
V += c->GetCarCount();
}
// kick on "no ping"
if (c->SecondsSinceLastPing() > (20 * 60)) {
beammp_debug("client " + std::string("(") + std::to_string(c->GetID()) + ")" + c->GetName() + " timing out: " + std::to_string(c->SecondsSinceLastPing()) + ", pps: " + Application::PPS());
TimedOutClients.push_back(c);
}
return true;
});
for (auto& ClientToKick : TimedOutClients) {
Network().ClientKick(*ClientToKick, "Timeout (no ping for way too long)");
}
TimedOutClients.clear();
if (C == 0 || mInternalPPS == 0) {
Application::SetPPS("-");
} else {
int R = (mInternalPPS / C) / V;
Application::SetPPS(std::to_string(R));
}
mInternalPPS = 0;
}
}

94
src/TPluginMonitor.cpp Normal file
View File

@@ -0,0 +1,94 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TPluginMonitor.h"
#include "TLuaEngine.h"
#include <filesystem>
TPluginMonitor::TPluginMonitor(const fs::path& Path, std::shared_ptr<TLuaEngine> Engine)
: mEngine(Engine)
, mPath(Path) {
Application::SetSubsystemStatus("PluginMonitor", Application::Status::Starting);
if (!fs::exists(mPath)) {
fs::create_directories(mPath);
}
for (const auto& Entry : fs::recursive_directory_iterator(mPath, fs::directory_options::follow_directory_symlink)) {
// TODO: trigger an event when a subfolder file changes
if (Entry.is_regular_file()) {
mFileTimes[Entry.path().string()] = fs::last_write_time(Entry.path());
}
}
Application::RegisterShutdownHandler([this] {
if (mThread.joinable()) {
mThread.join();
}
});
Start();
}
void TPluginMonitor::operator()() {
RegisterThread("PluginMonitor");
beammp_info("PluginMonitor started");
Application::SetSubsystemStatus("PluginMonitor", Application::Status::Good);
while (!Application::IsShuttingDown()) {
std::vector<std::string> ToRemove;
for (const auto& Pair : mFileTimes) {
try {
auto CurrentTime = fs::last_write_time(Pair.first);
if (CurrentTime > Pair.second) {
mFileTimes[Pair.first] = CurrentTime;
// grandparent of the path should be Resources/Server
if (fs::equivalent(fs::path(Pair.first).parent_path().parent_path(), mPath)) {
beammp_infof("File \"{}\" changed, reloading", Pair.first);
// is in root folder, so reload
std::ifstream FileStream(Pair.first, std::ios::in | std::ios::binary);
auto Size = std::filesystem::file_size(Pair.first);
auto Contents = std::make_shared<std::string>();
Contents->resize(Size);
FileStream.read(Contents->data(), Contents->size());
TLuaChunk Chunk(Contents, Pair.first, fs::path(Pair.first).parent_path().string());
auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path());
auto Res = mEngine->EnqueueScript(StateID, Chunk);
Res->WaitUntilReady();
if (Res->Error) {
beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Res->ErrorMessage);
} else {
mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit"));
mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first));
}
} else {
// is in subfolder, dont reload, just trigger an event
beammp_debugf("File \"{}\" changed, not reloading because it's in a subdirectory. Triggering 'onFileChanged' event instead", Pair.first);
mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first));
}
}
} catch (const std::exception& e) {
ToRemove.push_back(Pair.first);
}
}
Application::SleepSafeSeconds(3);
for (const auto& File : ToRemove) {
mFileTimes.erase(File);
beammp_warnf("File \"{}\" couldn't be accessed, so it was removed from plugin hot reload monitor (probably got deleted)", File);
}
}
Application::SetSubsystemStatus("PluginMonitor", Application::Status::Shutdown);
}

54
src/TResourceManager.cpp Normal file
View File

@@ -0,0 +1,54 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TResourceManager.h"
#include <algorithm>
#include <filesystem>
namespace fs = std::filesystem;
TResourceManager::TResourceManager() {
Application::SetSubsystemStatus("ResourceManager", Application::Status::Starting);
std::string Path = Application::Settings.Resource + "/Client";
if (!fs::exists(Path))
fs::create_directories(Path);
for (const auto& entry : fs::directory_iterator(Path)) {
std::string File(entry.path().string());
if (auto pos = File.find(".zip"); pos != std::string::npos) {
if (File.length() - pos == 4) {
std::replace(File.begin(), File.end(), '\\', '/');
mFileList += File + ';';
if (auto i = File.find_last_of('/'); i != std::string::npos) {
++i;
File = File.substr(i, pos - i);
}
mTrimmedList += "/" + fs::path(File).filename().string() + ';';
mFileSizes += std::to_string(size_t(fs::file_size(entry.path()))) + ';';
mMaxModSize += size_t(fs::file_size(entry.path()));
mModsLoaded++;
}
}
}
if (mModsLoaded) {
beammp_info("Loaded " + std::to_string(mModsLoaded) + " Mods");
}
Application::SetSubsystemStatus("ResourceManager", Application::Status::Good);
}

528
src/TServer.cpp Normal file
View File

@@ -0,0 +1,528 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
//
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TServer.h"
#include "Client.h"
#include "Common.h"
#include "CustomAssert.h"
#include "TNetwork.h"
#include "TPPSMonitor.h"
#include <TLuaPlugin.h>
#include <algorithm>
#include <any>
#include <optional>
#include <sstream>
#include <nlohmann/json.hpp>
#include "LuaAPI.h"
#undef GetObject // Fixes Windows
#include "Json.h"
static std::optional<std::pair<int, int>> GetPidVid(const std::string& str) {
auto IDSep = str.find('-');
std::string pid = str.substr(0, IDSep);
std::string vid = str.substr(IDSep + 1);
if (pid.find_first_not_of("0123456789") == std::string::npos && vid.find_first_not_of("0123456789") == std::string::npos) {
try {
int PID = stoi(pid);
int VID = stoi(vid);
return { { PID, VID } };
} catch (const std::exception&) {
return std::nullopt;
}
}
return std::nullopt;
}
TEST_CASE("GetPidVid") {
SUBCASE("Valid singledigit") {
const auto MaybePidVid = GetPidVid("0-1");
CHECK(MaybePidVid);
auto [pid, vid] = MaybePidVid.value();
CHECK_EQ(pid, 0);
CHECK_EQ(vid, 1);
}
SUBCASE("Valid doubledigit") {
const auto MaybePidVid = GetPidVid("10-12");
CHECK(MaybePidVid);
auto [pid, vid] = MaybePidVid.value();
CHECK_EQ(pid, 10);
CHECK_EQ(vid, 12);
}
SUBCASE("Valid doubledigit 2") {
const auto MaybePidVid = GetPidVid("10-2");
CHECK(MaybePidVid);
auto [pid, vid] = MaybePidVid.value();
CHECK_EQ(pid, 10);
CHECK_EQ(vid, 2);
}
SUBCASE("Valid doubledigit 3") {
const auto MaybePidVid = GetPidVid("33-23");
CHECK(MaybePidVid);
auto [pid, vid] = MaybePidVid.value();
CHECK_EQ(pid, 33);
CHECK_EQ(vid, 23);
}
SUBCASE("Valid doubledigit 4") {
const auto MaybePidVid = GetPidVid("3-23");
CHECK(MaybePidVid);
auto [pid, vid] = MaybePidVid.value();
CHECK_EQ(pid, 3);
CHECK_EQ(vid, 23);
}
SUBCASE("Empty string") {
const auto MaybePidVid = GetPidVid("");
CHECK(!MaybePidVid);
}
SUBCASE("Invalid separator") {
const auto MaybePidVid = GetPidVid("0x0");
CHECK(!MaybePidVid);
}
SUBCASE("Missing pid") {
const auto MaybePidVid = GetPidVid("-0");
CHECK(!MaybePidVid);
}
SUBCASE("Missing vid") {
const auto MaybePidVid = GetPidVid("0-");
CHECK(!MaybePidVid);
}
SUBCASE("Invalid pid") {
const auto MaybePidVid = GetPidVid("x-0");
CHECK(!MaybePidVid);
}
SUBCASE("Invalid vid") {
const auto MaybePidVid = GetPidVid("0-x");
CHECK(!MaybePidVid);
}
}
TServer::TServer(const std::vector<std::string_view>& Arguments) {
beammp_info("BeamMP Server v" + Application::ServerVersionString());
Application::SetSubsystemStatus("Server", Application::Status::Starting);
if (Arguments.size() > 1) {
Application::Settings.CustomIP = Arguments[0];
size_t n = std::count(Application::Settings.CustomIP.begin(), Application::Settings.CustomIP.end(), '.');
auto p = Application::Settings.CustomIP.find_first_not_of(".0123456789");
if (p != std::string::npos || n != 3 || Application::Settings.CustomIP.substr(0, 3) == "127") {
Application::Settings.CustomIP.clear();
beammp_warn("IP Specified is invalid! Ignoring");
} else {
beammp_info("server started with custom IP");
}
}
Application::SetSubsystemStatus("Server", Application::Status::Good);
}
void TServer::RemoveClient(const std::weak_ptr<TClient>& WeakClientPtr) {
std::shared_ptr<TClient> LockedClientPtr { nullptr };
try {
LockedClientPtr = WeakClientPtr.lock();
} catch (const std::exception&) {
// silently fail, as there's nothing to do
return;
}
beammp_assert(LockedClientPtr != nullptr);
TClient& Client = *LockedClientPtr;
beammp_debug("removing client " + Client.GetName() + " (" + std::to_string(ClientCount()) + ")");
// TODO: Send delete packets for all cars
Client.ClearCars();
WriteLock Lock(mClientsMutex);
mClients.erase(WeakClientPtr.lock());
}
void TServer::ForEachClient(const std::function<bool(std::weak_ptr<TClient>)>& Fn) {
decltype(mClients) Clients;
{
ReadLock lock(mClientsMutex);
Clients = mClients;
}
for (auto& Client : Clients) {
if (!Fn(Client)) {
break;
}
}
}
size_t TServer::ClientCount() const {
ReadLock Lock(mClientsMutex);
return mClients.size();
}
void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uint8_t>&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network) {
constexpr std::string_view ABG = "ABG:";
if (Packet.size() >= ABG.size() && std::equal(Packet.begin(), Packet.begin() + ABG.size(), ABG.begin(), ABG.end())) {
Packet.erase(Packet.begin(), Packet.begin() + ABG.size());
Packet = DeComp(Packet);
}
if (Packet.empty()) {
return;
}
if (Client.expired()) {
return;
}
auto LockedClient = Client.lock();
std::any Res;
char Code = Packet.at(0);
std::string StringPacket(reinterpret_cast<const char*>(Packet.data()), Packet.size());
// V to Y
if (Code <= 89 && Code >= 86) {
PPSMonitor.IncrementInternalPPS();
Network.SendToAll(LockedClient.get(), Packet, false, false);
return;
}
switch (Code) {
case 'H': // initial connection
if (!Network.SyncClient(Client)) {
// TODO handle
}
return;
case 'p':
if (!Network.Respond(*LockedClient, StringToVector("p"), false)) {
// failed to send
LockedClient->Disconnect("Failed to send ping");
} else {
Network.UpdatePlayer(*LockedClient);
}
return;
case 'O':
if (Packet.size() > 1000) {
beammp_debug(("Received data from: ") + LockedClient->GetName() + (" Size: ") + std::to_string(Packet.size()));
}
ParseVehicle(*LockedClient, StringPacket, Network);
return;
case 'C': {
if (Packet.size() < 4 || std::find(Packet.begin() + 3, Packet.end(), ':') == Packet.end())
break;
const auto PacketAsString = std::string(reinterpret_cast<const char*>(Packet.data()), Packet.size());
std::string Message = "";
const auto ColonPos = PacketAsString.find(':', 3);
if (ColonPos != std::string::npos && ColonPos + 2 < PacketAsString.size()) {
Message = PacketAsString.substr(ColonPos + 2);
}
if (Message.empty()) {
beammp_debugf("Empty chat message received from '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
return;
}
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onChatMessage", "", LockedClient->GetID(), LockedClient->GetName(), Message);
TLuaEngine::WaitForAll(Futures);
LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), PacketAsString.substr(PacketAsString.find(':', 3) + 1));
if (std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Elem) {
return !Elem->Error
&& Elem->Result.is<int>()
&& bool(Elem->Result.as<int>());
})) {
break;
}
std::string SanitizedPacket = fmt::format("C:{}: {}", LockedClient->GetName(), Message);
Network.SendToAll(nullptr, StringToVector(SanitizedPacket), true, true);
return;
}
case 'E':
HandleEvent(*LockedClient, StringPacket);
return;
case 'N':
beammp_trace("got 'N' packet (" + std::to_string(Packet.size()) + ")");
Network.SendToAll(LockedClient.get(), Packet, false, true);
return;
case 'Z': // position packet
PPSMonitor.IncrementInternalPPS();
Network.SendToAll(LockedClient.get(), Packet, false, false);
HandlePosition(*LockedClient, StringPacket);
return;
default:
return;
}
}
void TServer::HandleEvent(TClient& c, const std::string& RawData) {
// E:Name:Data
// Data is allowed to have ':'
if (RawData.size() < 2) {
beammp_debugf("Client '{}' ({}) tried to send an empty event, ignoring", c.GetName(), c.GetID());
return;
}
auto NameDataSep = RawData.find(':', 2);
if (NameDataSep == std::string::npos) {
beammp_warn("received event in invalid format (missing ':'), got: '" + RawData + "'");
}
std::string Name = RawData.substr(2, NameDataSep - 2);
std::string Data = RawData.substr(NameDataSep + 1);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent(Name, "", c.GetID(), Data));
}
bool TServer::IsUnicycle(TClient& c, const std::string& CarJson) {
try {
auto Car = nlohmann::json::parse(CarJson);
const std::string jbm = "jbm";
if (Car.contains(jbm) && Car[jbm].is_string() && Car[jbm] == "unicycle") {
return true;
}
} catch (const std::exception& e) {
beammp_warn("Failed to parse vehicle data as json for client " + std::to_string(c.GetID()) + ": '" + CarJson + "'.");
}
return false;
}
bool TServer::ShouldSpawn(TClient& c, const std::string& CarJson, int ID) {
if (IsUnicycle(c, CarJson) && c.GetUnicycleID() < 0) {
c.SetUnicycleID(ID);
return true;
} else {
return c.GetCarCount() < Application::Settings.MaxCars;
}
}
void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network) {
if (Pckt.length() < 6)
return;
std::string Packet = Pckt;
char Code = Packet.at(1);
int PID = -1;
int VID = -1;
std::string Data = Packet.substr(3), pid, vid;
switch (Code) { // Spawned Destroyed Switched/Moved NotFound Reset
case 's':
beammp_tracef("got 'Os' packet: '{}' ({})", Packet, Packet.size());
if (Data.at(0) == '0') {
int CarID = c.GetOpenCarID();
beammp_debugf("'{}' created a car with ID {}", c.GetName(), CarID);
std::string CarJson = Packet.substr(5);
Packet = "Os:" + c.GetRoles() + ":" + c.GetName() + ":" + std::to_string(c.GetID()) + "-" + std::to_string(CarID) + ":" + CarJson;
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onVehicleSpawn", "", c.GetID(), CarID, Packet.substr(3));
TLuaEngine::WaitForAll(Futures);
bool ShouldntSpawn = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Result) {
return !Result->Error && Result->Result.is<int>() && Result->Result.as<int>() != 0;
});
if (ShouldSpawn(c, CarJson, CarID) && !ShouldntSpawn) {
c.AddNewCar(CarID, Packet);
Network.SendToAll(nullptr, StringToVector(Packet), true, true);
} else {
if (!Network.Respond(c, StringToVector(Packet), true)) {
// TODO: handle
}
std::string Destroy = "Od:" + std::to_string(c.GetID()) + "-" + std::to_string(CarID);
if (!Network.Respond(c, StringToVector(Destroy), true)) {
// TODO: handle
}
beammp_debugf("{} (force : car limit/lua) removed ID {}", c.GetName(), CarID);
}
}
return;
case 'c': {
beammp_trace(std::string(("got 'Oc' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")"));
auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1)));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID != -1 && VID != -1 && PID == c.GetID()) {
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onVehicleEdited", "", c.GetID(), VID, Packet.substr(3));
TLuaEngine::WaitForAll(Futures);
bool ShouldntAllow = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Result) {
return !Result->Error && Result->Result.is<int>() && Result->Result.as<int>() != 0;
});
auto FoundPos = Packet.find('{');
FoundPos = FoundPos == std::string::npos ? 0 : FoundPos; // attempt at sanitizing this
if ((c.GetUnicycleID() != VID || IsUnicycle(c, Packet.substr(FoundPos)))
&& !ShouldntAllow) {
Network.SendToAll(&c, StringToVector(Packet), false, true);
Apply(c, VID, Packet);
} else {
if (c.GetUnicycleID() == VID) {
c.SetUnicycleID(-1);
}
std::string Destroy = "Od:" + std::to_string(c.GetID()) + "-" + std::to_string(VID);
Network.SendToAll(nullptr, StringToVector(Destroy), true, true);
c.DeleteCar(VID);
}
}
return;
}
case 'd': {
beammp_trace(std::string(("got 'Od' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")"));
auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1)));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID != -1 && VID != -1 && PID == c.GetID()) {
if (c.GetUnicycleID() == VID) {
c.SetUnicycleID(-1);
}
Network.SendToAll(nullptr, StringToVector(Packet), true, true);
// TODO: should this trigger on all vehicle deletions?
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", c.GetID(), VID));
c.DeleteCar(VID);
beammp_debug(c.GetName() + (" deleted car with ID ") + std::to_string(VID));
}
return;
}
case 'r': {
beammp_trace(std::string(("got 'Or' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")"));
auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1)));
if (MaybePidVid) {
std::tie(PID, VID) = MaybePidVid.value();
}
if (PID != -1 && VID != -1 && PID == c.GetID()) {
Data = Data.substr(Data.find('{'));
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleReset", "", c.GetID(), VID, Data));
Network.SendToAll(&c, StringToVector(Packet), false, true);
}
return;
}
case 't':
beammp_trace(std::string(("got 'Ot' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")"));
Network.SendToAll(&c, StringToVector(Packet), false, true);
return;
case 'm':
Network.SendToAll(&c, StringToVector(Packet), true, true);
return;
default:
beammp_trace(std::string(("possibly not implemented: '") + Packet + ("' (") + std::to_string(Packet.size()) + (")")));
return;
}
}
void TServer::Apply(TClient& c, int VID, const std::string& pckt) {
auto FoundPos = pckt.find('{');
if (FoundPos == std::string::npos) {
beammp_error("Malformed packet received, no '{' found");
return;
}
std::string Packet = pckt.substr(FoundPos);
std::string VD = c.GetCarData(VID);
if (VD.empty()) {
beammp_error("Tried to apply change to vehicle that does not exist");
return;
}
std::string Header = VD.substr(0, VD.find('{'));
FoundPos = VD.find('{');
if (FoundPos == std::string::npos) {
return;
}
VD = VD.substr(FoundPos);
rapidjson::Document Veh, Pack;
Veh.Parse(VD.c_str());
if (Veh.HasParseError()) {
beammp_error("Could not get vehicle config!");
return;
}
Pack.Parse(Packet.c_str());
if (Pack.HasParseError() || Pack.IsNull()) {
beammp_error("Could not get active vehicle config!");
return;
}
for (auto& M : Pack.GetObject()) {
if (Veh[M.name].IsNull()) {
Veh.AddMember(M.name, M.value, Veh.GetAllocator());
} else {
Veh[M.name] = Pack[M.name];
}
}
rapidjson::StringBuffer Buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(Buffer);
Veh.Accept(writer);
c.SetCarData(VID, Header + Buffer.GetString());
}
void TServer::InsertClient(const std::shared_ptr<TClient>& NewClient) {
beammp_debug("inserting client (" + std::to_string(ClientCount()) + ")");
WriteLock Lock(mClientsMutex); // TODO why is there 30+ threads locked here
(void)mClients.insert(NewClient);
}
struct PidVidData {
int PID;
int VID;
std::string Data;
};
static std::optional<PidVidData> ParsePositionPacket(const std::string& Packet) {
if (Packet.size() < 3) {
// invalid packet
return std::nullopt;
}
// Zp:PID-VID:DATA
std::string withoutCode = Packet.substr(3);
// parse veh ID
if (auto DataBeginPos = withoutCode.find('{'); DataBeginPos != std::string::npos && DataBeginPos != 0) {
// separator is :{, so position of { minus one
auto PidVidOnly = withoutCode.substr(0, DataBeginPos - 1);
auto MaybePidVid = GetPidVid(PidVidOnly);
if (MaybePidVid) {
int PID = -1;
int VID = -1;
// FIXME: check that the VID and PID are valid, so that we don't waste memory
std::tie(PID, VID) = MaybePidVid.value();
std::string Data = withoutCode.substr(DataBeginPos);
return PidVidData {
.PID = PID,
.VID = VID,
.Data = Data,
};
} else {
// invalid packet
return std::nullopt;
}
}
// invalid packet
return std::nullopt;
}
TEST_CASE("ParsePositionPacket") {
const auto TestData = R"({"tim":10.428000331623,"vel":[-2.4171722121385e-05,-9.7184734153252e-06,-7.6420763232237e-06],"rot":[-0.0001296154171915,0.0031575385950029,0.98994906610295,0.14138903660382],"rvel":[5.3640324636461e-05,-9.9824529946024e-05,5.1664064641372e-05],"pos":[-0.27281248907838,-0.20515357944633,0.49695488960431],"ping":0.032999999821186})";
SUBCASE("All the pids and vids") {
for (int pid = 0; pid < 100; ++pid) {
for (int vid = 0; vid < 100; ++vid) {
std::optional<PidVidData> MaybeRes = ParsePositionPacket(fmt::format("Zp:{}-{}:{}", pid, vid, TestData));
CHECK(MaybeRes.has_value());
CHECK_EQ(MaybeRes.value().PID, pid);
CHECK_EQ(MaybeRes.value().VID, vid);
CHECK_EQ(MaybeRes.value().Data, TestData);
}
}
}
}
void TServer::HandlePosition(TClient& c, const std::string& Packet) {
if (auto Parsed = ParsePositionPacket(Packet); Parsed.has_value()) {
c.SetCarPosition(Parsed.value().VID, Parsed.value().Data);
}
}

View File

@@ -1,478 +0,0 @@
#include "Value.h"
#include "Common.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");
}
}
static Result<Value> sol_obj_to_value_impl(const sol::object& obj, const std::function<Result<Value>(const sol::object&)>& invalid_provider, size_t max_depth, size_t depth) {
if (depth == max_depth) {
return { Null {} };
}
++depth;
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_impl(v, invalid_provider, max_depth, depth);
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_impl(v, invalid_provider, max_depth, depth);
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.{}", "");
}
Result<Value> sol_obj_to_value(const sol::object& obj, const std::function<Result<Value>(const sol::object&)>& invalid_provider, size_t max_depth) {
return sol_obj_to_value_impl(obj, invalid_provider, max_depth, 0);
}
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(std::string(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;
}

View File

@@ -20,13 +20,16 @@
#include "Common.h"
#include "Http.h"
#include "LuaAPI.h"
#include "LuaPlugin.h"
#include "Plugin.h"
#include "PluginManager.h"
#include "SignalHandling.h"
#include "TConfig.h"
#include "THeartbeatThread.h"
#include <cstdlib>
#include "TLuaEngine.h"
#include "TNetwork.h"
#include "TPPSMonitor.h"
#include "TPluginMonitor.h"
#include "TResourceManager.h"
#include "TServer.h"
#include <iostream>
#include <thread>
@@ -102,9 +105,6 @@ int BeamMPServerMain(MainArguments Arguments) {
return 0;
}
// badly seed C's rng - this is only because rand() is used here and there for unimportant stuff
std::srand(std::time(0));
std::string ConfigPath = "ServerConfig.toml";
if (Parser.FoundArgument({ "config" })) {
auto MaybeConfigPath = Parser.GetValueOfArgument({ "config" });
@@ -124,7 +124,7 @@ int BeamMPServerMain(MainArguments Arguments) {
}
}
}
TConfig Config(ConfigPath);
if (Config.Failed()) {
@@ -148,36 +148,29 @@ int BeamMPServerMain(MainArguments Arguments) {
Application::SetSubsystemStatus("Main", Application::Status::ShuttingDown);
Shutdown = true;
});
/*
Application::RegisterShutdownHandler([] {
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onShutdown", "");
TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5));
});
Application::Console().InitializeLuaConsole(*LuaEngine);
TServer Server(Arguments.List);
*/
auto LuaEngine = std::make_shared<TLuaEngine>();
LuaEngine->SetServer(&Server);
Application::Console().InitializeLuaConsole(*LuaEngine);
RegisterThread("Main");
beammp_trace("Running in debug mode on a debug build");
std::shared_ptr<Network> network = std::make_shared<Network>();
THeartbeatThread Heartbeat(network);
// LuaEngine->SetNetwork(network);
TResourceManager ResourceManager;
TPPSMonitor PPSMonitor(Server);
THeartbeatThread Heartbeat(ResourceManager, Server);
TNetwork Network(Server, PPSMonitor, ResourceManager);
LuaEngine->SetNetwork(&Network);
PPSMonitor.SetNetwork(Network);
Application::CheckForUpdates();
// TPluginMonitor PluginMonitor(fs::path(Application::Settings.Resource) / "Server", LuaEngine);
PluginManager PluginManager;
(void)PluginManager.add_plugin(Plugin::make_pointer<LuaPlugin>("Resources/Server/Test"));
auto console = Plugin::make_pointer<LuaPlugin>(BEAMMP_MEMORY_STATE);
(void)PluginManager.add_plugin(console);
dynamic_cast<LuaPlugin&>(*console).run_raw_lua(R"(local n = 12; return n)");
PluginManager.trigger_event("onInit", std::make_shared<Value>(HashMap<std::string, Value> {
{ "big", "balls" },
{ "longer", "falls" },
}));
TPluginMonitor PluginMonitor(fs::path(Application::Settings.Resource) / "Server", LuaEngine);
if (Application::Settings.HTTPServerEnabled) {
Http::Server::THttpServerInstance HttpServerInstance {};

View File

@@ -6,17 +6,14 @@
"boost-spirit",
"boost-uuid",
"boost-variant",
"boost-iostreams",
"boost-json",
"cpp-httplib",
"doctest",
"fmt",
"libzip",
"nlohmann-json",
"openssl",
"rapidjson",
"sol2",
"toml11",
"zstd",
"glm"
"toml11"
]
}