diff --git a/.gitmodules b/.gitmodules index 5cfa6fa..968224c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "deps/commandline"] path = deps/commandline url = https://github.com/lionkor/commandline +[submodule "deps/fmt"] + path = deps/fmt + url = https://github.com/fmtlib/fmt diff --git a/CMakeLists.txt b/CMakeLists.txt index 680cd8b..cb1aeab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,5 @@ -cmake_minimum_required(VERSION 3.0) - -if (WIN32) - set(VCPKG_TARGET_TRIPLET "x64-windows-static") -elseif(UNIX) - set(VCPKG_TARGET_TRIPLET "x64-linux") -endif() - -option(USE_VCPKG "USE_VCPKG" ON) - -if(USE_VCPKG) - include(vcpkg.cmake) -endif() +# 3.4 is required for imported targets. +cmake_minimum_required(VERSION 3.4 FATAL_ERROR) message(STATUS "You can find build instructions and a list of dependencies in the README at \ https://github.com/BeamMP/BeamMP-Server") @@ -20,42 +9,57 @@ project(BeamMP-Server HOMEPAGE_URL https://beammp.com LANGUAGES CXX C) +find_package(Git REQUIRED) +# Update submodules as needed +option(GIT_SUBMODULE "Check submodules during build" ON) +if(GIT_SUBMODULE) + message(STATUS "Submodule update") + execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE GIT_SUBMOD_RESULT) + if(NOT GIT_SUBMOD_RESULT EQUAL "0") + message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") + endif() +endif() + + set(HTTPLIB_REQUIRE_OPENSSL ON) +set(SENTRY_BUILD_SHARED_LIBS OFF) + +include_directories("${PROJECT_SOURCE_DIR}/deps/asio/asio/include") +include_directories("${PROJECT_SOURCE_DIR}/deps/rapidjson/include") +include_directories("${PROJECT_SOURCE_DIR}/deps/websocketpp") +include_directories("${PROJECT_SOURCE_DIR}/deps/commandline") +include_directories("${PROJECT_SOURCE_DIR}/deps/sol2/include") +include_directories("${PROJECT_SOURCE_DIR}/deps/cpp-httplib") +include_directories("${PROJECT_SOURCE_DIR}/deps/json/single_include") +include_directories("${PROJECT_SOURCE_DIR}/deps") add_compile_definitions(CPPHTTPLIB_OPENSSL_SUPPORT) +# ------------------------ APPLE --------------------------------- if(APPLE) set(LUA_INCLUDE_DIR /usr/local/Cellar/lua@5.3/5.3.6/include/lua5.3) set(LUA_LIBRARIES lua) include_directories(/usr/local/opt/openssl@1.1/include) link_directories(/usr/local/Cellar/lua@5.3/5.3.6/lib) link_directories(/usr/local/opt/openssl@1.1/lib) -endif() - -if (WIN32) +# ------------------------ WINDOWS --------------------------------- +elseif (WIN32) # this has to happen before sentry, so that crashpad on windows links with these settings. message(STATUS "MSVC -> forcing use of statically-linked runtime.") STRING(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE}) STRING(REPLACE "/MDd" "/MTd" CMAKE_CXX_FLAGS_DEBUG ${CMAKE_CXX_FLAGS_DEBUG}) -endif() - -if (MSVC) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") -endif () - -message(STATUS "Adding local source dependencies") -# this has to happen before -DDEBUG since it wont compile properly with -DDEBUG -add_subdirectory(deps) - -message(STATUS "Setting compiler flags") -if (WIN32) - - #-DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static + if (MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") + set(SENTRY_BUILD_RUNTIMESTATIC ON) + endif () set(VcpkgRoot ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}) include_directories(${VcpkgRoot}/include) link_directories(${VcpkgRoot}/lib) +# ------------------------ LINUX --------------------------------- elseif (UNIX) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -static-libstdc++") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -static-libstdc++ -static-libgcc") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -g") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2 -fno-builtin") if (SANITIZE) @@ -64,6 +68,9 @@ elseif (UNIX) endif (SANITIZE) endif () +include_directories("include/sentry-native/include") + +# ------------------------ SENTRY --------------------------------- message(STATUS "Checking for Sentry URL") # this is set by the build system. # IMPORTANT: if you're building from source, just leave this empty @@ -73,14 +80,19 @@ if (NOT DEFINED BEAMMP_SECRET_SENTRY_URL) set(BEAMMP_SECRET_SENTRY_URL "") set(SENTRY_BACKEND none) else() - string(LENGTH ${BEAMMP_SECRET_SENTRY_URL} URL_LEN) - message(STATUS "Sentry URL is length ${URL_LEN}") set(SENTRY_BACKEND breakpad) endif() +add_subdirectory("deps/sentry-native") +# ------------------------ C++ SETUP --------------------------------- set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") +# ------------------------ DEPENDENCIES ------------------------------ +message(STATUS "Adding local source dependencies") +# this has to happen before -DDEBUG since it wont compile properly with -DDEBUG +add_subdirectory(deps) + +# ------------------------ BEAMMP SERVER ----------------------------- add_executable(BeamMP-Server src/main.cpp @@ -108,34 +120,33 @@ add_executable(BeamMP-Server target_compile_definitions(BeamMP-Server PRIVATE SECRET_SENTRY_URL="${BEAMMP_SECRET_SENTRY_URL}") include_directories(BeamMP-Server PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(BeamMP-Server PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/commandline") + +include(FindLua) +include(FindOpenSSL) include(FindThreads) +include(FindZLIB) -find_package(OpenSSL REQUIRED) -find_package(ZLIB REQUIRED) -find_package(CURL CONFIG REQUIRED) -find_package(Lua REQUIRED 5.3) -find_package(nlohmann_json CONFIG REQUIRED) -find_package(RapidJSON CONFIG REQUIRED) -find_package(sentry CONFIG REQUIRED) -find_package(sol2 CONFIG REQUIRED) -find_package(toml11 CONFIG REQUIRED) -find_path(CPP_HTTPLIB_INCLUDE_DIRS "httplib.h") +target_include_directories(BeamMP-Server PUBLIC + ${LUA_INCLUDE_DIR} + ${CURL_INCLUDE_DIRS} + "include/tomlplusplus" + "include/sentry-native/include" + "include/curl/include") -target_include_directories(BeamMP-Server PRIVATE - ${CPP_HTTPLIB_INCLUDE_DIRS} - ${LUA_INCLUDE_DIR} -) - -target_link_libraries(BeamMP-Server PRIVATE OpenSSL::SSL OpenSSL::Crypto - ZLIB::ZLIB - CURL::libcurl - ${LUA_LIBRARIES} - Threads::Threads - commandline - nlohmann_json::nlohmann_json - rapidjson - sentry::sentry +target_link_libraries(BeamMP-Server + OpenSSL::SSL + OpenSSL::Crypto sol2::sol2 - toml11::toml11 -) + fmt::fmt + Threads::Threads + ZLIB::ZLIB + ${LUA_LIBRARIES} + commandline + sentry) +if (WIN32) + target_link_libraries(BeamMP-Server wsock32 ws2_32) +endif () diff --git a/Changelog.md b/Changelog.md index 7ff6d04..da9eb18 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,11 @@ +# v3.1.0 + +- ADDED Tab autocomplete in console, smart tab autocomplete (understands lua tables and types) in the lua console +- ADDED lua debug facilities (type `:help` when attached to lua via `lua`) +- ADDED MP.JsonEncode() and MP.JsonDecode(), which turn lua tables into json and vice-versa +- ADDED FS.ListFiles and FS.ListDirectories +- FIXED issue with client->server events which contain ':' + # v3.0.2 - ADDED Periodic update message if a new server is released diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 6f87000..c4d4ce4 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -1,4 +1,6 @@ + include_directories("${PROJECT_SOURCE_DIR}/deps") include_directories("${PROJECT_SOURCE_DIR}/deps/commandline") add_subdirectory("${PROJECT_SOURCE_DIR}/deps/commandline") +add_subdirectory("${PROJECT_SOURCE_DIR}/deps/fmt") diff --git a/deps/fmt b/deps/fmt new file mode 160000 index 0000000..17dda58 --- /dev/null +++ b/deps/fmt @@ -0,0 +1 @@ +Subproject commit 17dda58391fba627a56482f5d2652a396619bc26 diff --git a/include/Common.h b/include/Common.h index afbafbf..afc935f 100644 --- a/include/Common.h +++ b/include/Common.h @@ -8,6 +8,7 @@ extern TSentry Sentry; #include #include #include +#include #include #include #include @@ -48,6 +49,7 @@ public: bool DebugModeEnabled { false }; int Port { 30814 }; std::string CustomIP {}; + bool LogChat { true }; bool SendErrors { true }; bool SendErrorsMessageEnabled { true }; int HTTPServerPort { 8080 }; @@ -120,7 +122,7 @@ private: static inline std::mutex mShutdownHandlersMutex {}; static inline std::deque mShutdownHandlers {}; - static inline Version mVersion { 3, 0, 2 }; + static inline Version mVersion { 3, 1, 0 }; }; std::string ThreadName(bool DebugModeOverride = false); @@ -146,6 +148,10 @@ void RegisterThread(const std::string& str); #define _function_name std::string(__func__) #endif +#ifndef NDEBUG +#define DEBUG +#endif + #if defined(DEBUG) // if this is defined, we will show the full function signature infront of @@ -207,6 +213,13 @@ void RegisterThread(const std::string& str); #define beammp_trace(x) #endif // defined(DEBUG) +#define beammp_errorf(...) beammp_error(fmt::format(__VA_ARGS__)) +#define beammp_infof(...) beammp_info(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__)) + void LogChatMessage(const std::string& name, int id, const std::string& msg); #define Biggest 30000 diff --git a/include/Compat.h b/include/Compat.h index e1e906d..1a3cc63 100644 --- a/include/Compat.h +++ b/include/Compat.h @@ -6,10 +6,10 @@ #ifdef BEAMMP_LINUX #include +#include #include #include #include -#include using SOCKET = int; using DWORD = unsigned long; using PDWORD = unsigned long*; @@ -25,10 +25,10 @@ inline void CloseSocketProper(int TheSocket) { #ifdef BEAMMP_APPLE #include +#include #include #include #include -#include using SOCKET = int; using DWORD = unsigned long; using PDWORD = unsigned long*; @@ -48,6 +48,11 @@ inline void CloseSocketProper(int TheSocket) { inline void CloseSocketProper(SOCKET TheSocket) { shutdown(TheSocket, 2); // 2 == SD_BOTH closesocket(TheSocket); - } #endif // WIN32 + +#ifdef INVALID_SOCKET +static inline constexpr int BEAMMP_INVALID_SOCKET = INVALID_SOCKET; +#else +static inline constexpr int BEAMMP_INVALID_SOCKET = -1; +#endif diff --git a/include/LuaAPI.h b/include/LuaAPI.h index 0cd0e1f..4871a0b 100644 --- a/include/LuaAPI.h +++ b/include/LuaAPI.h @@ -12,7 +12,8 @@ namespace MP { std::string GetOSName(); std::tuple GetServerVersion(); - bool TriggerClientEvent(int PlayerID, const std::string& EventName, const std::string& Data); + bool TriggerClientEvent(int PlayerID, const std::string& EventName, const sol::object& Data); + bool TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data); inline size_t GetPlayerCount() { return Engine->Server().ClientCount(); } void DropPlayer(int ID, std::optional MaybeReason); void SendChatMessage(int ID, const std::string& Message); @@ -22,7 +23,15 @@ namespace MP { bool IsPlayerConnected(int ID); 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); + std::string JsonMinify(const std::string& json); + std::string JsonFlatten(const std::string& json); + std::string JsonUnflatten(const std::string& json); } + namespace FS { std::pair CreateDirectory(const std::string& Path); std::pair Remove(const std::string& Path); diff --git a/include/TConfig.h b/include/TConfig.h index b05f422..2349cc8 100644 --- a/include/TConfig.h +++ b/include/TConfig.h @@ -3,6 +3,7 @@ #include "Common.h" #include +#include #define TOML11_PRESERVE_COMMENTS_BY_DEFAULT #include // header-only version of TOML++ diff --git a/include/TConsole.h b/include/TConsole.h index 839f862..1f990b8 100644 --- a/include/TConsole.h +++ b/include/TConsole.h @@ -4,6 +4,11 @@ #include "commandline.h" #include #include +#include +#include +#include +#include +#include class TLuaEngine; @@ -22,13 +27,31 @@ private: void RunAsCommand(const std::string& cmd, bool IgnoreNotACommand = false); void ChangeToLuaConsole(const std::string& LuaStateId); void ChangeToRegularConsole(); + void HandleLuaInternalCommand(const std::string& cmd); - void Command_Lua(const std::string& cmd); - void Command_Help(const std::string& cmd); - void Command_Kick(const std::string& cmd); - void Command_Say(const std::string& cmd); - void Command_List(const std::string& cmd); - void Command_Status(const std::string& cmd); + void Command_Lua(const std::string& cmd, const std::vector& args); + void Command_Help(const std::string& cmd, const std::vector& args); + void Command_Kick(const std::string& cmd, const std::vector& args); + void Command_List(const std::string& cmd, const std::vector& args); + void Command_Status(const std::string& cmd, const std::vector& args); + void Command_Settings(const std::string& cmd, const std::vector& args); + + void Command_Say(const std::string& FullCommand); + bool EnsureArgsCount(const std::vector& args, size_t n); + bool EnsureArgsCount(const std::vector& args, size_t min, size_t max); + + static std::tuple> ParseCommand(const std::string& cmd); + static std::string ConcatArgs(const std::vector& args, char space = ' '); + + std::unordered_map&)>> mCommandMap = { + { "lua", [this](const auto& a, const auto& b) { Command_Lua(a, b); } }, + { "help", [this](const auto& a, const auto& b) { Command_Help(a, b); } }, + { "kick", [this](const auto& a, const auto& b) { Command_Kick(a, b); } }, + { "list", [this](const auto& a, const auto& b) { Command_List(a, b); } }, + { "status", [this](const auto& a, const auto& b) { Command_Status(a, b); } }, + { "settings", [this](const auto& a, const auto& b) { Command_Settings(a, b); } }, + { "say", [this](const auto&, const auto&) { Command_Say(""); } }, // shouldn't actually be called + }; Commandline mCommandline; std::vector mCachedLuaHistory; diff --git a/include/TLuaEngine.h b/include/TLuaEngine.h index 876d910..1689b22 100644 --- a/include/TLuaEngine.h +++ b/include/TLuaEngine.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -33,8 +34,8 @@ static constexpr size_t TLuaArgTypes_Bool = 3; class TLuaPlugin; struct TLuaResult { - std::atomic_bool Ready; - std::atomic_bool Error; + bool Ready; + bool Error; std::string ErrorMessage; sol::object Result { sol::lua_nil }; TLuaStateId StateId; @@ -102,6 +103,7 @@ public: std::unique_lock Lock(mResultsToCheckMutex); return mResultsToCheck.size(); } + size_t GetLuaStateCount() { std::unique_lock Lock(mLuaStatesMutex); return mLuaStates.size(); @@ -166,6 +168,14 @@ public: static constexpr const char* BeamMPFnNotFoundError = "BEAMMP_FN_NOT_FOUND"; + std::vector GetStateGlobalKeysForState(TLuaStateId StateId); + + // Debugging functions (slow) + std::unordered_map /* handlers */> Debug_GetEventsForState(TLuaStateId StateId); + std::queue>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId); + std::vector Debug_GetStateFunctionQueueForState(TLuaStateId StateId); + std::vector Debug_GetResultsToCheckForState(TLuaStateId StateId); + private: void CollectAndInitPlugins(); void InitializePlugin(const fs::path& Folder, const TLuaPluginConfig& Config); @@ -185,6 +195,12 @@ private: void operator()() override; sol::state_view State() { return sol::state_view(mState); } + std::vector GetStateGlobalKeys(); + + // Debug functions, slow + std::queue>> Debug_GetStateExecuteQueue(); + std::vector 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); @@ -193,7 +209,10 @@ private: std::string Lua_GetPlayerName(int ID); sol::table Lua_GetPlayerVehicles(int ID); 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; std::atomic_bool& mShutdown; @@ -209,6 +228,8 @@ private: sol::state_view mStateView { mState }; std::queue mPaths; std::recursive_mutex mPathsMutex; + std::mt19937 mMersenneTwister; + std::uniform_real_distribution mUniformRealDistribution01; }; struct TimedEvent { diff --git a/src/ArgsParser.cpp b/src/ArgsParser.cpp index 8440661..f00548f 100644 --- a/src/ArgsParser.cpp +++ b/src/ArgsParser.cpp @@ -12,7 +12,7 @@ void ArgsParser::Parse(const std::vector& ArgList) { ConsumeLongFlag(std::string(Arg)); } } else { - beammp_error("Error parsing commandline arguments: Supplied argument '" + std::string(Arg) + "' is not a valid argument and was ignored."); + beammp_errorf("Error parsing commandline arguments: Supplied argument '{}' is not a valid argument and was ignored.", Arg); } } } @@ -21,7 +21,7 @@ bool ArgsParser::Verify() { bool Ok = true; for (const auto& RegisteredArg : mRegisteredArguments) { if (RegisteredArg.Flags & Flags::REQUIRED && !FoundArgument(RegisteredArg.Names)) { - beammp_error("Error in commandline arguments: Argument '" + std::string(RegisteredArg.Names.at(0)) + "' is required but wasn't found."); + beammp_errorf("Error in commandline arguments: Argument '{}' is required but wasn't found.", RegisteredArg.Names.at(0)); Ok = false; continue; } else if (FoundArgument(RegisteredArg.Names)) { diff --git a/src/Client.cpp b/src/Client.cpp index 4edbbc7..410d966 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -5,8 +5,6 @@ #include #include -// FIXME: add debug prints - void TClient::DeleteCar(int Ident) { std::unique_lock lock(mVehicleDataMutex); auto iter = std::find_if(mVehicleData.begin(), mVehicleData.end(), [&](auto& elem) { diff --git a/src/Common.cpp b/src/Common.cpp index 34b28ea..23c483d 100644 --- a/src/Common.cpp +++ b/src/Common.cpp @@ -43,6 +43,7 @@ void Application::GracefullyShutdown() { beammp_info("Subsystem " + std::to_string(i + 1) + "/" + std::to_string(mShutdownHandlers.size()) + " shutting down"); mShutdownHandlers[i](); } + // std::exit(-1); } std::string Application::ServerVersionString() { @@ -60,7 +61,6 @@ std::array Application::VersionStrToInts(const std::string& str) { return Version; } -// FIXME: This should be used by operator< on Version bool Application::IsOutdated(const Version& Current, const Version& Newest) { if (Newest.major > Current.major) { return true; @@ -162,7 +162,7 @@ void RegisterThread(const std::string& str) { ThreadId = std::to_string(gettid()); #endif if (Application::Settings.DebugModeEnabled) { - std::ofstream ThreadFile("Threads.log", std::ios::app); + std::ofstream ThreadFile(".Threads.log", std::ios::app); ThreadFile << ("Thread \"" + str + "\" is TID " + ThreadId) << std::endl; } auto Lock = std::unique_lock(ThreadNameMapMutex); @@ -179,22 +179,22 @@ Version::Version(const std::array& v) } std::string Version::AsString() { - std::stringstream ss {}; - ss << int(major) << "." << int(minor) << "." << int(patch); - return ss.str(); + return fmt::format("{:d}.{:d}.{:d}", major, minor, patch); } void LogChatMessage(const std::string& name, int id, const std::string& msg) { - std::stringstream ss; - ss << ThreadName(); - ss << "[CHAT] "; - if (id != -1) { - ss << "(" << id << ") <" << name << "> "; - } else { - ss << name << ""; + if (Application::Settings.LogChat) { + std::stringstream ss; + ss << ThreadName(); + ss << "[CHAT] "; + if (id != -1) { + ss << "(" << id << ") <" << name << "> "; + } else { + ss << name << ""; + } + ss << msg; + Application::Console().Write(ss.str()); } - ss << msg; - Application::Console().Write(ss.str()); } std::string GetPlatformAgnosticErrorString() { diff --git a/src/LuaAPI.cpp b/src/LuaAPI.cpp index 4a76cdc..4a5df29 100644 --- a/src/LuaAPI.cpp +++ b/src/LuaAPI.cpp @@ -3,6 +3,8 @@ #include "Common.h" #include "TLuaEngine.h" +#include + #define SOL_ALL_SAFETIES_ON 1 #include @@ -100,26 +102,31 @@ void LuaAPI::Print(sol::variadic_args Args) { luaprint(ToPrint); } -bool LuaAPI::MP::TriggerClientEvent(int PlayerID, const std::string& EventName, const std::string& Data) { +static inline bool InternalTriggerClientEvent(int PlayerID, const std::string& EventName, const std::string& Data) { std::string Packet = "E:" + EventName + ":" + Data; if (PlayerID == -1) - Engine->Network().SendToAll(nullptr, Packet, true, true); + LuaAPI::MP::Engine->Network().SendToAll(nullptr, Packet, true, true); else { - auto MaybeClient = GetClient(Engine->Server(), PlayerID); + auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID); if (!MaybeClient || MaybeClient.value().expired()) { beammp_lua_error("TriggerClientEvent invalid Player ID"); return false; } auto c = MaybeClient.value().lock(); - if (!Engine->Network().Respond(*c, Packet, true)) { + if (!LuaAPI::MP::Engine->Network().Respond(*c, Packet, true)) { beammp_lua_error("Respond failed, dropping client " + std::to_string(PlayerID)); - Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets"); + LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets"); return false; } } return true; } +bool LuaAPI::MP::TriggerClientEvent(int PlayerID, const std::string& EventName, const sol::object& DataObj) { + std::string Data = DataObj.as(); + return InternalTriggerClientEvent(PlayerID, EventName, Data); +} + void LuaAPI::MP::DropPlayer(int ID, std::optional MaybeReason) { auto MaybeClient = GetClient(Engine->Server(), ID); if (!MaybeClient || MaybeClient.value().expired()) { @@ -352,3 +359,157 @@ std::string LuaAPI::FS::ConcatPaths(sol::variadic_args Args) { auto Result = Path.lexically_normal().string(); return Result; } + +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_lua_error("json serialize will not go deeper than 100 nested tables, internal references assumed, aborted this path"); + return; + } + std::string key; + switch (left.get_type()) { + case sol::type::lua_nil: + case sol::type::none: + case sol::type::poly: + case sol::type::boolean: + case sol::type::lightuserdata: + case sol::type::userdata: + case sol::type::thread: + case sol::type::function: + case sol::type::table: + beammp_lua_error("JsonEncode: left side of table field is unexpected type"); + return; + case sol::type::string: + key = left.as(); + break; + case sol::type::number: + key = std::to_string(left.as()); + break; + } + nlohmann::json value; + switch (right.get_type()) { + case sol::type::lua_nil: + case sol::type::none: + return; + case sol::type::poly: + beammp_lua_warn("unsure what to do with poly type in JsonEncode, ignoring"); + return; + case sol::type::boolean: + value = right.as(); + break; + case sol::type::lightuserdata: + beammp_lua_warn("unsure what to do with lightuserdata in JsonEncode, ignoring"); + return; + case sol::type::userdata: + beammp_lua_warn("unsure what to do with userdata in JsonEncode, ignoring"); + return; + case sol::type::thread: + beammp_lua_warn("unsure what to do with thread in JsonEncode, ignoring"); + return; + case sol::type::string: + value = right.as(); + break; + case sol::type::number: + value = right.as(); + break; + case sol::type::function: + beammp_lua_warn("unsure what to do with function in JsonEncode, ignoring"); + return; + case sol::type::table: { + bool local_is_array = true; + for (const auto& pair : right.as()) { + if (pair.first.get_type() != sol::type::number) { + local_is_array = false; + } + } + for (const auto& pair : right.as()) { + JsonEncodeRecursive(value, pair.first, pair.second, local_is_array, depth + 1); + } + break; + } + } + if (is_array) { + json.push_back(value); + } else { + json[key] = value; + } +} + +std::string LuaAPI::MP::JsonEncode(const sol::table& object) { + nlohmann::json json; + // table + bool is_array = true; + for (const auto& pair : object.as()) { + 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::MP::JsonDiff(const std::string& a, const std::string& b) { + if (!nlohmann::json::accept(a)) { + beammp_lua_error("JsonDiff first argument is not valid json: `" + a + "`"); + return ""; + } + if (!nlohmann::json::accept(b)) { + beammp_lua_error("JsonDiff second argument is not valid json: `" + b + "`"); + return ""; + } + auto a_json = nlohmann::json::parse(a); + auto b_json = nlohmann::json::parse(b); + return nlohmann::json::diff(a_json, b_json).dump(); +} + +std::string LuaAPI::MP::JsonDiffApply(const std::string& data, const std::string& patch) { + if (!nlohmann::json::accept(data)) { + beammp_lua_error("JsonDiffApply first argument is not valid json: `" + data + "`"); + return ""; + } + if (!nlohmann::json::accept(patch)) { + beammp_lua_error("JsonDiffApply second argument is not valid json: `" + patch + "`"); + return ""; + } + auto a_json = nlohmann::json::parse(data); + auto b_json = nlohmann::json::parse(patch); + a_json.patch(b_json); + return a_json.dump(); +} + +std::string LuaAPI::MP::JsonPrettify(const std::string& json) { + if (!nlohmann::json::accept(json)) { + beammp_lua_error("JsonPrettify argument is not valid json: `" + json + "`"); + return ""; + } + return nlohmann::json::parse(json).dump(4); +} + +std::string LuaAPI::MP::JsonMinify(const std::string& json) { + if (!nlohmann::json::accept(json)) { + beammp_lua_error("JsonMinify argument is not valid json: `" + json + "`"); + return ""; + } + return nlohmann::json::parse(json).dump(-1); +} + +std::string LuaAPI::MP::JsonFlatten(const std::string& json) { + if (!nlohmann::json::accept(json)) { + beammp_lua_error("JsonFlatten argument is not valid json: `" + json + "`"); + return ""; + } + return nlohmann::json::parse(json).flatten().dump(-1); +} + +std::string LuaAPI::MP::JsonUnflatten(const std::string& json) { + if (!nlohmann::json::accept(json)) { + beammp_lua_error("JsonUnflatten argument is not valid json: `" + json + "`"); + return ""; + } + return nlohmann::json::parse(json).unflatten().dump(-1); +} + +bool LuaAPI::MP::TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data) { + return InternalTriggerClientEvent(PlayerID, EventName, JsonEncode(Data)); +} diff --git a/src/TConfig.cpp b/src/TConfig.cpp index da99cc1..c27e5d2 100644 --- a/src/TConfig.cpp +++ b/src/TConfig.cpp @@ -17,6 +17,7 @@ static constexpr std::string_view StrName = "Name"; static constexpr std::string_view StrDescription = "Description"; static constexpr std::string_view StrResourceFolder = "ResourceFolder"; static constexpr std::string_view StrAuthKey = "AuthKey"; +static constexpr std::string_view StrLogChat = "LogChat"; // Misc static constexpr std::string_view StrSendErrors = "SendErrors"; @@ -63,6 +64,8 @@ void TConfig::FlushToFile() { auto data = toml::parse(mConfigFileName); data["General"][StrAuthKey.data()] = Application::Settings.Key; SetComment(data["General"][StrAuthKey.data()].comments(), " AuthKey has to be filled out in order to run the server"); + data["General"][StrLogChat.data()] = Application::Settings.LogChat; + SetComment(data["General"][StrLogChat.data()].comments(), " Whether to log chat messages in the console / log"); data["General"][StrDebug.data()] = Application::Settings.DebugModeEnabled; data["General"][StrPrivate.data()] = Application::Settings.Private; data["General"][StrPort.data()] = Application::Settings.Port; @@ -161,6 +164,7 @@ void TConfig::ParseFromFile(std::string_view name) { TryReadValue(data, "General", StrDescription, Application::Settings.ServerDesc); TryReadValue(data, "General", StrResourceFolder, Application::Settings.Resource); TryReadValue(data, "General", StrAuthKey, Application::Settings.Key); + TryReadValue(data, "General", StrLogChat, Application::Settings.LogChat); // Misc TryReadValue(data, "Misc", StrSendErrors, Application::Settings.SendErrors); TryReadValue(data, "Misc", StrHideUpdateMessages, Application::Settings.HideUpdateMessages); @@ -201,6 +205,7 @@ void TConfig::PrintDebug() { beammp_debug(std::string(StrMap) + ": \"" + Application::Settings.MapName + "\""); beammp_debug(std::string(StrName) + ": \"" + Application::Settings.ServerName + "\""); beammp_debug(std::string(StrDescription) + ": \"" + Application::Settings.ServerDesc + "\""); + beammp_debug(std::string(StrLogChat) + ": \"" + (Application::Settings.LogChat ? "true" : "false") + "\""); beammp_debug(std::string(StrResourceFolder) + ": \"" + Application::Settings.Resource + "\""); beammp_debug(std::string(StrHTTPServerPort) + ": \"" + std::to_string(Application::Settings.HTTPServerPort) + "\""); beammp_debug(std::string(StrHTTPServerIP) + ": \"" + Application::Settings.HTTPServerIP + "\""); diff --git a/src/TConsole.cpp b/src/TConsole.cpp index f6eb1fa..8bc98c6 100644 --- a/src/TConsole.cpp +++ b/src/TConsole.cpp @@ -118,10 +118,10 @@ void TConsole::ChangeToLuaConsole(const std::string& LuaStateId) { mStateId = LuaStateId; mIsLuaConsole = true; if (mStateId != mDefaultStateId) { - Application::Console().WriteRaw("Entered Lua console for state '" + mStateId + "'. To exit, type `exit()`"); + Application::Console().WriteRaw("Attached to Lua state '" + mStateId + "'. For help, type `:help`. To detach, type `:detach`"); mCommandline.set_prompt("lua @" + LuaStateId + "> "); } else { - Application::Console().WriteRaw("Entered Lua console. To exit, type `exit()`"); + Application::Console().WriteRaw("Attached to Lua. For help, type `:help`. To detach, type `:detach`"); mCommandline.set_prompt("lua> "); } mCachedRegularHistory = mCommandline.history(); @@ -133,9 +133,9 @@ void TConsole::ChangeToRegularConsole() { if (mIsLuaConsole) { mIsLuaConsole = false; if (mStateId != mDefaultStateId) { - Application::Console().WriteRaw("Left Lua console for state '" + mStateId + "'."); + Application::Console().WriteRaw("Detached from Lua state '" + mStateId + "'."); } else { - Application::Console().WriteRaw("Left Lua console."); + Application::Console().WriteRaw("Detached from Lua."); } mCachedLuaHistory = mCommandline.history(); mCommandline.set_history(mCachedRegularHistory); @@ -144,21 +144,54 @@ void TConsole::ChangeToRegularConsole() { } } -void TConsole::Command_Lua(const std::string& cmd) { - if (cmd.size() > 3) { - auto NewStateId = cmd.substr(4); +bool TConsole::EnsureArgsCount(const std::vector& args, size_t n) { + if (n == 0 && args.size() != 0) { + Application::Console().WriteRaw("This command expects no arguments."); + return false; + } else if (args.size() != n) { + Application::Console().WriteRaw("Expected " + std::to_string(n) + " argument(s), instead got " + std::to_string(args.size())); + return false; + } else { + return true; + } +} + +bool TConsole::EnsureArgsCount(const std::vector& args, size_t min, size_t max) { + if (min == max) { + return EnsureArgsCount(args, min); + } else { + if (args.size() > max) { + Application::Console().WriteRaw("Too many arguments. At most " + std::to_string(max) + " arguments expected, got " + std::to_string(args.size()) + " instead."); + return false; + } else if (args.size() < min) { + Application::Console().WriteRaw("Too few arguments. At least " + std::to_string(max) + " arguments expected, got " + std::to_string(args.size()) + " instead."); + return false; + } + } + return true; +} + +void TConsole::Command_Lua(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0, 1)) { + return; + } + if (args.size() == 1) { + auto NewStateId = args.at(0); beammp_assert(!NewStateId.empty()); if (mLuaEngine->HasState(NewStateId)) { ChangeToLuaConsole(NewStateId); } else { Application::Console().WriteRaw("Lua state '" + NewStateId + "' is not a known state. Didn't switch to Lua."); } - } else { + } else if (args.size() == 0) { ChangeToLuaConsole(mDefaultStateId); } } -void TConsole::Command_Help(const std::string&) { +void TConsole::Command_Help(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0)) { + return; + } static constexpr const char* sHelpString = R"( Commands: help displays this help @@ -167,53 +200,127 @@ void TConsole::Command_Help(const std::string&) { list lists all players and info about them say sends the message to all players in chat lua [state id] switches to lua, optionally into a specific state id's lua + settings [command] sets or gets settings for the server, run `settings help` for more info status how the server is doing and what it's up to)"; Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString)); } -void TConsole::Command_Kick(const std::string& cmd) { - if (cmd.size() > 4) { - auto Name = cmd.substr(5); - std::string Reason = "Kicked by server console"; - auto SpacePos = Name.find(' '); - if (SpacePos != Name.npos) { - Reason = Name.substr(SpacePos + 1); - Name = cmd.substr(5, cmd.size() - Reason.size() - 5 - 1); +std::string TConsole::ConcatArgs(const std::vector& args, char space) { + std::string Result; + for (const auto& arg : args) { + Result += arg + space; + } + Result = Result.substr(0, Result.size() - 1); // strip trailing space + return Result; +} + +void TConsole::Command_Kick(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 1, size_t(-1))) { + return; + } + auto Name = args.at(0); + std::string Reason = "Kicked by server console"; + if (args.size() > 1) { + Reason = ConcatArgs({ args.begin() + 1, args.end() }); + } + beammp_trace("attempt to kick '" + Name + "' for '" + Reason + "'"); + bool Kicked = false; + auto NameCompare = [](std::string Name1, std::string Name2) -> bool { + std::for_each(Name1.begin(), Name1.end(), [](char& c) { c = tolower(c); }); + std::for_each(Name2.begin(), Name2.end(), [](char& c) { c = tolower(c); }); + return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1); + }; + mLuaEngine->Server().ForEachClient([&](std::weak_ptr Client) -> bool { + if (!Client.expired()) { + auto locked = Client.lock(); + if (NameCompare(locked->GetName(), Name)) { + mLuaEngine->Network().ClientKick(*locked, Reason); + Kicked = true; + return false; + } } - beammp_trace("attempt to kick '" + Name + "' for '" + Reason + "'"); - bool Kicked = false; - auto NameCompare = [](std::string Name1, std::string Name2) -> bool { - std::for_each(Name1.begin(), Name1.end(), [](char& c) { c = tolower(c); }); - std::for_each(Name2.begin(), Name2.end(), [](char& c) { c = tolower(c); }); - return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1); - }; - mLuaEngine->Server().ForEachClient([&](std::weak_ptr 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 { + Application::Console().WriteRaw("Kicked player '" + Name + "' for reason: '" + Reason + "'."); + } +} + +std::tuple> TConsole::ParseCommand(const std::string& CommandWithArgs) { + // Algorithm designed and implemented by Lion Kortlepel (c) 2022 + // It correctly splits arguments, including respecting single and double quotes, as well as backticks + auto End_i = CommandWithArgs.find_first_of(' '); + std::string Command = CommandWithArgs.substr(0, End_i); + std::string ArgsStr {}; + if (End_i != std::string::npos) { + ArgsStr = CommandWithArgs.substr(End_i); + } + std::vector Args; + char* PrevPtr = ArgsStr.data(); + char* Ptr = ArgsStr.data(); + const char* End = ArgsStr.data() + ArgsStr.size(); + while (Ptr != End) { + std::string Arg = ""; + // advance while space + while (Ptr != End && std::isspace(*Ptr)) + ++Ptr; + PrevPtr = Ptr; + // advance while NOT space, also handle quotes + while (Ptr != End && !std::isspace(*Ptr)) { + // TODO: backslash escaping quotes + for (char Quote : { '"', '\'', '`' }) { + if (*Ptr == Quote) { + // seek if there's a closing quote + // if there is, go there and continue, otherwise ignore + char* Seeker = Ptr + 1; + while (Seeker != End && *Seeker != Quote) + ++Seeker; + if (Seeker != End) { + // found closing quote + Ptr = Seeker; + } + break; // exit for loop } } - return true; - }); - if (!Kicked) { - Application::Console().WriteRaw("Error: No player with name matching '" + Name + "' was found."); - } else { - Application::Console().WriteRaw("Kicked player '" + Name + "' for reason: '" + Reason + "'."); + ++Ptr; + } + Arg = std::string(PrevPtr, Ptr - PrevPtr); + // remove quotes if enclosed in quotes + for (char Quote : { '"', '\'', '`' }) { + if (!Arg.empty() && Arg.at(0) == Quote && Arg.at(Arg.size() - 1) == Quote) { + Arg = Arg.substr(1, Arg.size() - 2); + break; + } + } + if (!Arg.empty()) { + Args.push_back(Arg); + } + } + return { Command, Args }; +} + +void TConsole::Command_Settings(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0)) { + return; + } +} + +void TConsole::Command_Say(const std::string& FullCmd) { + if (FullCmd.size() > 3) { + auto Message = FullCmd.substr(4); + LuaAPI::MP::SendChatMessage(-1, Message); + if (!Application::Settings.LogChat) { + Application::Console().WriteRaw("Chat message sent!"); } } } -void TConsole::Command_Say(const std::string& cmd) { - if (cmd.size() > 3) { - auto Message = cmd.substr(4); - LuaAPI::MP::SendChatMessage(-1, Message); +void TConsole::Command_List(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0)) { + return; } -} - -void TConsole::Command_List(const std::string&) { if (mLuaEngine->Server().ClientCount() == 0) { Application::Console().WriteRaw("No players online."); } else { @@ -233,7 +340,10 @@ void TConsole::Command_List(const std::string&) { } } -void TConsole::Command_Status(const std::string&) { +void TConsole::Command_Status(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0)) { + return; + } std::stringstream Status; size_t CarCount = 0; @@ -318,12 +428,12 @@ void TConsole::Command_Status(const std::string&) { << "\t\tEvent handlers: " << mLuaEngine->GetRegisteredEventHandlerCount() << "\n" << "\tSubsystems:\n" << "\t\tGood/Starting/Bad: " << SystemsGood << "/" << SystemsStarting << "/" << SystemsBad << "\n" - << "\t\tShutting down/Shutdown: " << SystemsShuttingDown << "/" << SystemsShutdown << "\n" + << "\t\tShutting down/Shut down: " << SystemsShuttingDown << "/" << SystemsShutdown << "\n" << "\t\tGood: [ " << SystemsGoodList << " ]\n" << "\t\tStarting: [ " << SystemsStartingList << " ]\n" << "\t\tBad: [ " << SystemsBadList << " ]\n" << "\t\tShutting down: [ " << SystemsShuttingDownList << " ]\n" - << "\t\tShutdown: [ " << SystemsShutdownList << " ]\n" + << "\t\tShut down: [ " << SystemsShutdownList << " ]\n" << ""; Application::Console().WriteRaw(Status.str()); @@ -375,6 +485,58 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) { } } +void TConsole::HandleLuaInternalCommand(const std::string& cmd) { + if (cmd == "exit") { + ChangeToRegularConsole(); + } else if (cmd == "queued") { + auto QueuedFunctions = LuaAPI::MP::Engine->Debug_GetStateFunctionQueueForState(mStateId); + Application::Console().WriteRaw("Pending functions in State '" + mStateId + "'"); + std::unordered_map FunctionsCount; + std::vector FunctionsInOrder; + while (!QueuedFunctions.empty()) { + auto Tuple = QueuedFunctions.front(); + QueuedFunctions.erase(QueuedFunctions.begin()); + FunctionsInOrder.push_back(Tuple.FunctionName); + FunctionsCount[Tuple.FunctionName] += 1; + } + std::set Uniques; + for (const auto& Function : FunctionsInOrder) { + if (Uniques.count(Function) == 0) { + Uniques.insert(Function); + if (FunctionsCount.at(Function) > 1) { + Application::Console().WriteRaw(" " + Function + " (" + std::to_string(FunctionsCount.at(Function)) + "x)"); + } else { + Application::Console().WriteRaw(" " + Function); + } + } + } + Application::Console().WriteRaw("Executed functions waiting to be checked in State '" + mStateId + "'"); + for (const auto& Function : LuaAPI::MP::Engine->Debug_GetResultsToCheckForState(mStateId)) { + Application::Console().WriteRaw(" '" + Function.Function + "' (Ready? " + (Function.Ready ? "Yes" : "No") + ", Error? " + (Function.Error ? "Yes: '" + Function.ErrorMessage + "'" : "No") + ")"); + } + } else if (cmd == "events") { + auto Events = LuaAPI::MP::Engine->Debug_GetEventsForState(mStateId); + Application::Console().WriteRaw("Registered Events + Handlers for State '" + mStateId + "'"); + for (const auto& EventHandlerPair : Events) { + Application::Console().WriteRaw(" Event '" + EventHandlerPair.first + "'"); + for (const auto& Handler : EventHandlerPair.second) { + Application::Console().WriteRaw(" " + Handler); + } + } + } else if (cmd == "help") { + Application::Console().WriteRaw(R"(BeamMP Lua Debugger + All commands must be prefixed with a `:`. Non-prefixed commands are interpreted as Lua. + +Commands + :exit detaches (exits) from this Lua console + :help displays this help + :events shows a list of currently registered events + :queued shows a list of all pending and queued functions)"); + } else { + beammp_error("internal command '" + cmd + "' is not known"); + } +} + TConsole::TConsole() { mCommandline.enable_history(); mCommandline.set_history_limit(20); @@ -382,21 +544,22 @@ TConsole::TConsole() { BackupOldLog(); mCommandline.on_command = [this](Commandline& c) { try { - auto cmd = c.get_command(); - cmd = TrimString(cmd); - mCommandline.write(mCommandline.prompt() + cmd); + auto TrimmedCmd = c.get_command(); + TrimmedCmd = TrimString(TrimmedCmd); + 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 == "exit()") { - ChangeToRegularConsole(); + } else if (!cmd.empty() && cmd.at(0) == ':') { + HandleLuaInternalCommand(cmd.substr(1)); } else { - auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared(cmd), "", "" }); + auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared(TrimmedCmd), "", "" }); while (!Future->Ready) { std::this_thread::yield(); // TODO: Add a timeout } if (Future->Error) { - beammp_lua_error(Future->ErrorMessage); + beammp_lua_error("error in " + mStateId + ": " + Future->ErrorMessage); } } } else { @@ -405,31 +568,63 @@ TConsole::TConsole() { } else if (cmd == "exit") { beammp_info("gracefully shutting down"); Application::GracefullyShutdown(); - } else if (StringStartsWith(cmd, "lua")) { - Command_Lua(cmd); - } else if (StringStartsWith(cmd, "help")) { - RunAsCommand(cmd, true); - Command_Help(cmd); - } else if (StringStartsWith(cmd, "kick")) { - RunAsCommand(cmd, true); - Command_Kick(cmd); - } else if (StringStartsWith(cmd, "say")) { - RunAsCommand(cmd, true); - Command_Say(cmd); - } else if (StringStartsWith(cmd, "list")) { - RunAsCommand(cmd, true); - Command_List(cmd); - } else if (StringStartsWith(cmd, "status")) { - RunAsCommand(cmd, true); - Command_Status(cmd); - } else if (!cmd.empty()) { - RunAsCommand(cmd); + } else if (cmd == "say") { + RunAsCommand(TrimmedCmd, true); + Command_Say(TrimmedCmd); + } else { + if (mCommandMap.find(cmd) != mCommandMap.end()) { + mCommandMap.at(cmd)(cmd, args); + RunAsCommand(TrimmedCmd, true); + } 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."); } }; + mCommandline.on_autocomplete = [this](Commandline&, std::string stub, int) { + std::vector suggestions; + try { + auto cmd = TrimString(stub); + // beammp_error("yes 1"); + // beammp_error(stub); + if (mIsLuaConsole) { // if lua + if (!mLuaEngine) { + beammp_info("Lua not started yet, please try again in a second"); + } else { + std::string prefix {}; + for (size_t i = stub.length(); i > 0; i--) { + if (!std::isalnum(stub[i - 1]) && stub[i - 1] != '_') { + prefix = stub.substr(0, i); + stub = stub.substr(i); + break; + } + } + auto keys = mLuaEngine->GetStateGlobalKeysForState(mStateId); + for (const auto& key : keys) { + std::string::size_type n = key.find(stub); + if (n == 0) { + suggestions.push_back(prefix + key); + // beammp_warn(cmd_name); + } + } + } + } else { // if not lua + for (const auto& [cmd_name, cmd_fn] : mCommandMap) { + std::string::size_type n = cmd_name.find(stub); + if (n == 0) { + suggestions.push_back(cmd_name); + // beammp_warn(cmd_name); + } + } + } + } 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."); + } + return suggestions; + }; } void TConsole::Write(const std::string& str) { diff --git a/src/THeartbeatThread.cpp b/src/THeartbeatThread.cpp index d65380e..5d3d437 100644 --- a/src/THeartbeatThread.cpp +++ b/src/THeartbeatThread.cpp @@ -64,8 +64,10 @@ void THeartbeatThread::operator()() { beammp_trace(T); Doc.Parse(T.data(), T.size()); if (Doc.HasParseError() || !Doc.IsObject()) { - beammp_debug("Failed to contact backend at " + Url + " (this is not an error)."); - beammp_trace("Response was: " + T); + if (!Application::Settings.Private) { + beammp_error("Backend response failed to parse as valid json"); + beammp_trace("Response was: `" + T + "`"); + } Sentry.SetContext("JSON Response", { { "reponse", T } }); SentryReportError(Url + Target, ResponseCode); } else if (ResponseCode != 200) { @@ -113,21 +115,21 @@ void THeartbeatThread::operator()() { } } - if (Ok && !isAuth) { + if (Ok && !isAuth && !Application::Settings.Private) { if (Status == "2000") { - beammp_info(("Authenticated!")); + beammp_info(("Authenticated! " + Message)); isAuth = true; } else if (Status == "200") { - beammp_info(("Resumed authenticated session!")); + beammp_info(("Resumed authenticated session! " + Message)); isAuth = true; } else { if (Message.empty()) { - Message = "Backend didn't provide a reason"; + Message = "Backend didn't provide a reason."; } - beammp_error("Backend REFUSED the auth key. " + Message); + beammp_error("Backend REFUSED the auth key. Reason: " + Message); } } - if (isAuth) { + if (isAuth || Application::Settings.Private) { Application::SetSubsystemStatus("Heartbeat", Application::Status::Good); } if (!Application::Settings.HideUpdateMessages && UpdateReminderCounter % 5) { diff --git a/src/TLuaEngine.cpp b/src/TLuaEngine.cpp index 1d684d8..e5885ec 100644 --- a/src/TLuaEngine.cpp +++ b/src/TLuaEngine.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -154,10 +155,70 @@ void TLuaEngine::AddResultToCheck(const std::shared_ptr& Result) { mResultsToCheckCond.notify_one(); } +std::unordered_map /* handlers */> TLuaEngine::Debug_GetEventsForState(TLuaStateId StateId) { + std::unordered_map> Result; + std::unique_lock Lock(mLuaEventsMutex); + for (const auto& EventNameToEventMap : mLuaEvents) { + for (const auto& IdSetOfHandlersPair : EventNameToEventMap.second) { + if (IdSetOfHandlersPair.first == StateId) { + for (const auto& Handler : IdSetOfHandlersPair.second) { + Result[EventNameToEventMap.first].push_back(Handler); + } + } + } + } + return Result; +} + +std::queue>> TLuaEngine::Debug_GetStateExecuteQueueForState(TLuaStateId StateId) { + std::queue>> Result; + std::unique_lock Lock(mLuaStatesMutex); + Result = mLuaStates.at(StateId)->Debug_GetStateExecuteQueue(); + return Result; +} + +std::vector TLuaEngine::Debug_GetStateFunctionQueueForState(TLuaStateId StateId) { + std::vector Result; + std::unique_lock Lock(mLuaStatesMutex); + Result = mLuaStates.at(StateId)->Debug_GetStateFunctionQueue(); + return Result; +} + +std::vector TLuaEngine::Debug_GetResultsToCheckForState(TLuaStateId StateId) { + std::unique_lock Lock(mResultsToCheckMutex); + auto ResultsToCheckCopy = mResultsToCheck; + Lock.unlock(); + std::vector Result; + while (!ResultsToCheckCopy.empty()) { + auto ResultToCheck = std::move(ResultsToCheckCopy.front()); + ResultsToCheckCopy.pop_front(); + if (ResultToCheck->StateId == StateId) { + Result.push_back(*ResultToCheck); + } + } + return Result; +} + +std::vector TLuaEngine::GetStateGlobalKeysForState(TLuaStateId StateId) { + std::unique_lock Lock(mLuaStatesMutex); + auto Result = mLuaStates.at(StateId)->GetStateGlobalKeys(); + return Result; +} + +std::vector TLuaEngine::StateThreadData::GetStateGlobalKeys() { + auto globals = mStateView.globals(); + std::vector Result; + for (const auto& [key, value] : globals) { + Result.push_back(key.as()); + } + return Result; +} + void TLuaEngine::WaitForAll(std::vector>& Results, const std::optional& Max) { for (const auto& Result : Results) { bool Cancelled = false; size_t ms = 0; + std::set WarnedResults; while (!Result->Ready && !Cancelled) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); ms += 10; @@ -165,7 +226,11 @@ void TLuaEngine::WaitForAll(std::vector>& Results, c beammp_trace("'" + Result->Function + "' in '" + Result->StateId + "' did not finish executing in time (took: " + std::to_string(ms) + "ms)."); Cancelled = true; } else if (ms > 1000 * 60) { - beammp_lua_warn("'" + Result->Function + "' in '" + Result->StateId + "' is taking very long. The event it's handling is too important to discard the result of this handler, but may block this event and possibly the whole lua state."); + auto ResultId = Result->StateId + "_" + Result->Function; + if (WarnedResults.count(ResultId) == 0) { + WarnedResults.insert(ResultId); + beammp_lua_warn("'" + Result->Function + "' in '" + Result->StateId + "' is taking very long. The event it's handling is too important to discard the result of this handler, but may block this event and possibly the whole lua state."); + } } } if (Cancelled) { @@ -277,7 +342,6 @@ std::set TLuaEngine::GetEventHandlersForState(const std::string& Ev sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string& EventName, sol::variadic_args EventArgs) { auto Return = mEngine->TriggerEvent(EventName, mStateId, EventArgs); - // TODO Synchronous call to the event handlers auto MyHandlers = mEngine->GetEventHandlersForState(EventName, mStateId); for (const auto& Handler : MyHandlers) { auto Fn = mStateView[Handler]; @@ -335,7 +399,7 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerLocalEvent(const std::string& Result.add(FnRet); } else { sol::error Err = FnRet; - beammp_lua_error(Err.what()); + beammp_lua_error(std::string("TriggerLocalEvent: ") + Err.what()); } } } @@ -386,6 +450,32 @@ int TLuaEngine::StateThreadData::Lua_GetPlayerIDByName(const std::string& Name) return Id; } +sol::table TLuaEngine::StateThreadData::Lua_FS_ListFiles(const std::string& Path) { + if (!std::filesystem::exists(Path)) { + return sol::lua_nil; + } + auto table = mStateView.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; +} + +sol::table TLuaEngine::StateThreadData::Lua_FS_ListDirectories(const std::string& Path) { + if (!std::filesystem::exists(Path)) { + return sol::lua_nil; + } + auto table = mStateView.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; +} + std::string TLuaEngine::StateThreadData::Lua_GetPlayerName(int ID) { auto MaybeClient = GetClient(mEngine->Server(), ID); if (MaybeClient && !MaybeClient.value().expired()) { @@ -439,6 +529,83 @@ sol::table TLuaEngine::StateThreadData::Lua_HttpCreateConnection(const std::stri return table; } +template +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()); + break; + case nlohmann::detail::value_t::boolean: + AddToTable(table, left, right.get()); + break; + case nlohmann::detail::value_t::number_integer: + AddToTable(table, left, right.get()); + break; + case nlohmann::detail::value_t::number_unsigned: + AddToTable(table, left, right.get()); + break; + case nlohmann::detail::value_t::number_float: + AddToTable(table, left, right.get()); + break; + case nlohmann::detail::value_t::binary: + beammp_lua_error("JsonDecode can't handle binary blob in json, ignoring"); + return; + case nlohmann::detail::value_t::discarded: + return; + } +} + +sol::table TLuaEngine::StateThreadData::Lua_JsonDecode(const std::string& str) { + sol::state_view StateView(mState); + auto table = StateView.create_table(); + if (!nlohmann::json::accept(str)) { + beammp_lua_error("string given to JsonDecode is not valid json: `" + str + "`"); + return sol::lua_nil; + } + nlohmann::json json = nlohmann::json::parse(str); + if (json.is_object()) { + for (const auto& entry : json.items()) { + JsonDecodeRecursive(StateView, table, entry.key(), entry.value()); + } + } else if (json.is_array()) { + for (const auto& entry : json) { + JsonDecodeRecursive(StateView, table, "", entry); + } + } else { + beammp_lua_error("JsonDecode expected array or object json, instead got " + std::string(json.type_name())); + return sol::lua_nil; + } + return table; +} + TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, std::atomic_bool& Shutdown, TLuaStateId StateId, TLuaEngine& Engine) : mName(Name) , mShutdown(Shutdown) @@ -484,6 +651,7 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, std::atomi return Lua_TriggerLocalEvent(EventName, EventArgs); }); MPTable.set_function("TriggerClientEvent", &LuaAPI::MP::TriggerClientEvent); + MPTable.set_function("TriggerClientEventJson", &LuaAPI::MP::TriggerClientEventJson); MPTable.set_function("GetPlayerCount", &LuaAPI::MP::GetPlayerCount); MPTable.set_function("IsPlayerConnected", &LuaAPI::MP::IsPlayerConnected); MPTable.set_function("GetPlayerIDByName", [&](const std::string& Name) -> int { @@ -538,6 +706,27 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, std::atomi mEngine->CancelEventTimers(EventName, mStateId); }); MPTable.set_function("Set", &LuaAPI::MP::Set); + + auto UtilTable = StateView.create_named_table("Util"); + UtilTable.set_function("JsonEncode", &LuaAPI::MP::JsonEncode); + UtilTable.set_function("JsonDecode", [this](const std::string& str) { + return Lua_JsonDecode(str); + }); + UtilTable.set_function("JsonDiff", &LuaAPI::MP::JsonDiff); + UtilTable.set_function("JsonFlatten", &LuaAPI::MP::JsonFlatten); + UtilTable.set_function("JsonUnflatten", &LuaAPI::MP::JsonUnflatten); + UtilTable.set_function("JsonPrettify", &LuaAPI::MP::JsonPrettify); + UtilTable.set_function("JsonMinify", &LuaAPI::MP::JsonMinify); + UtilTable.set_function("Random", [this] { + return mUniformRealDistribution01(mMersenneTwister); + }); + UtilTable.set_function("RandomRange", [this](double min, double max) -> double { + return std::uniform_real_distribution(min, max)(mMersenneTwister); + }); + UtilTable.set_function("RandomIntRange", [this](int64_t min, int64_t max) -> int64_t { + return std::uniform_int_distribution(min, max)(mMersenneTwister); + }); + auto HttpTable = StateView.create_named_table("Http"); HttpTable.set_function("CreateConnection", [this](const std::string& host, uint16_t port) { return Lua_HttpCreateConnection(host, port); @@ -568,6 +757,12 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, std::atomi FSTable.set_function("IsDirectory", &LuaAPI::FS::IsDirectory); FSTable.set_function("IsFile", &LuaAPI::FS::IsFile); FSTable.set_function("ConcatPaths", &LuaAPI::FS::ConcatPaths); + FSTable.set_function("ListFiles", [this](const std::string& Path) { + return Lua_FS_ListFiles(Path); + }); + FSTable.set_function("ListDirectories", [this](const std::string& Path) { + return Lua_FS_ListDirectories(Path); + }); Start(); } @@ -722,6 +917,16 @@ void TLuaEngine::StateThreadData::operator()() { } } +std::queue>> TLuaEngine::StateThreadData::Debug_GetStateExecuteQueue() { + std::unique_lock Lock(mStateExecuteQueueMutex); + return mStateExecuteQueue; +} + +std::vector TLuaEngine::StateThreadData::Debug_GetStateFunctionQueue() { + std::unique_lock Lock(mStateFunctionQueueMutex); + return mStateFunctionQueue; +} + void TLuaEngine::CreateEventTimer(const std::string& EventName, TLuaStateId StateId, size_t IntervalMS, CallStrategy Strategy) { std::unique_lock Lock(mTimedEventsMutex); TimedEvent Event { @@ -798,38 +1003,47 @@ void TPluginMonitor::operator()() { beammp_info("PluginMonitor started"); while (!mShutdown) { std::this_thread::sleep_for(std::chrono::seconds(3)); + std::vector ToRemove; for (const auto& Pair : mFileTimes) { - 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_info("File \"" + Pair.first + "\" changed, reloading"); - // 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(); - 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); - // TODO: call onInit - mEngine.AddResultToCheck(Res); - } else { - // TODO: trigger onFileChanged event - beammp_trace("Change detected in file \"" + Pair.first + "\", event trigger not implemented yet"); - /* - // is in subfolder, dont reload, just trigger an event - auto Results = mEngine.TriggerEvent("onFileChanged", "", Pair.first); - mEngine.WaitForAll(Results); - for (const auto& Result : Results) { - if (Result->Error) { - beammp_lua_error(Result->ErrorMessage); - } - }*/ + 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_info("File \"" + Pair.first + "\" changed, reloading"); + // 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(); + 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); + // TODO: call onInit + mEngine.AddResultToCheck(Res); + } else { + // TODO: trigger onFileChanged event + beammp_trace("Change detected in file \"" + Pair.first + "\", event trigger not implemented yet"); + /* + // is in subfolder, dont reload, just trigger an event + auto Results = mEngine.TriggerEvent("onFileChanged", "", Pair.first); + mEngine.WaitForAll(Results); + for (const auto& Result : Results) { + if (Result->Error) { + beammp_lua_error(Result->ErrorMessage); + } + }*/ + } } + } catch (const std::exception& e) { + ToRemove.push_back(Pair.first); } } + for (const auto& File : ToRemove) { + mFileTimes.erase(File); + beammp_warn("file '" + File + "' couldn't be accessed, so it was removed from plugin hot reload monitor (probably got deleted)"); + } } } diff --git a/src/TNetwork.cpp b/src/TNetwork.cpp index 03c90f1..dfa8776 100644 --- a/src/TNetwork.cpp +++ b/src/TNetwork.cpp @@ -108,40 +108,48 @@ void TNetwork::TCPServerMain() { #if defined(BEAMMP_WINDOWS) WSADATA wsaData; if (WSAStartup(514, &wsaData)) { - beammp_error("Can't start Winsock!"); - return; + beammp_error("Can't start Winsock! Shutting down"); + Application::GracefullyShutdown(); } #endif // WINDOWS TConnection client {}; SOCKET Listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - int optval = 1; + if (Listener == BEAMMP_INVALID_SOCKET) { + beammp_error("Failed to create socket: " + GetPlatformAgnosticErrorString() + + ". This is a fatal error, as a socket is needed for the server to operate. Shutting down."); + Application::GracefullyShutdown(); + } #if defined(BEAMMP_WINDOWS) - const char* optval_ptr = reinterpret_cast(&optval); + const char optval = 0; + int ret = ::setsockopt(Listener, SOL_SOCKET, SO_DONTLINGER, &optval, sizeof(optval)); #elif defined(BEAMMP_LINUX) || defined(BEAMMP_APPLE) - void* optval_ptr = reinterpret_cast(&optval); + int optval = true; + int ret = ::setsockopt(Listener, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&optval), sizeof(optval)); #endif - setsockopt(Listener, SOL_SOCKET, SO_REUSEADDR, optval_ptr, sizeof(optval)); - // TODO: check optval or return value idk + // not a fatal error + if (ret < 0) { + beammp_error("Failed to set up listening socket to not linger / reuse address. " + "This may cause the socket to refuse to bind(). Error: " + + GetPlatformAgnosticErrorString()); + } sockaddr_in addr {}; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_family = AF_INET; addr.sin_port = htons(uint16_t(Application::Settings.Port)); - if (bind(Listener, (sockaddr*)&addr, sizeof(addr)) != 0) { - beammp_error("bind() failed: " + GetPlatformAgnosticErrorString()); - std::this_thread::sleep_for(std::chrono::seconds(5)); - exit(-1); // TODO: Wtf. + if (bind(Listener, (sockaddr*)&addr, sizeof(addr)) < 0) { + beammp_error("bind() failed, the server cannot operate and will shut down now. " + "Error: " + + GetPlatformAgnosticErrorString()); + Application::GracefullyShutdown(); } - if (Listener == -1) { - beammp_error("Invalid listening socket"); - return; - } - if (listen(Listener, SOMAXCONN)) { - beammp_error("listen() failed: " + GetPlatformAgnosticErrorString()); - // FIXME leak Listener - return; + if (listen(Listener, SOMAXCONN) < 0) { + beammp_error("listen() failed, which is needed for the server to operate. " + "Shutting down. Error: " + + GetPlatformAgnosticErrorString()); + Application::GracefullyShutdown(); } Application::SetSubsystemStatus("TCPNetwork", Application::Status::Good); - beammp_info(("Vehicle event network online")); + beammp_info("Vehicle event network online"); do { try { if (mShutdown) { @@ -154,15 +162,15 @@ void TNetwork::TCPServerMain() { beammp_warn(("Got an invalid client socket on connect! Skipping...")); continue; } - // set timeout (DWORD, aka uint32_t) - uint32_t SendTimeoutMS = 30 * 1000; + // set timeout + size_t SendTimeoutMS = 30 * 1000; #if defined(BEAMMP_WINDOWS) int ret = ::setsockopt(client.Socket, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&SendTimeoutMS), sizeof(SendTimeoutMS)); #else // POSIX struct timeval optval; optval.tv_sec = (int)(SendTimeoutMS / 1000); optval.tv_usec = (SendTimeoutMS % 1000) * 1000; - int ret = ::setsockopt(client.Socket, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&optval), sizeof(optval)); + ret = ::setsockopt(client.Socket, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&optval), sizeof(optval)); #endif if (ret < 0) { throw std::runtime_error("setsockopt recv timeout: " + GetPlatformAgnosticErrorString()); @@ -170,9 +178,9 @@ void TNetwork::TCPServerMain() { std::thread ID(&TNetwork::Identify, this, client); ID.detach(); // TODO: Add to a queue and attempt to join periodically } catch (const std::exception& e) { - beammp_error(("fatal: ") + std::string(e.what())); + beammp_error("fatal: " + std::string(e.what())); } - } while (client.Socket); + } while (client.Socket != BEAMMP_INVALID_SOCKET); beammp_debug("all ok, arrived at " + std::string(__func__) + ":" + std::to_string(__LINE__)); @@ -477,7 +485,7 @@ std::string TNetwork::TCPRcv(TClient& c) { void TNetwork::ClientKick(TClient& c, const std::string& R) { beammp_info("Client kicked: " + R); if (!TCPSend(c, "K" + R)) { - // TODO handle + beammp_warn("tried to kick player '" + c.GetName() + "' (id " + std::to_string(c.GetID()) + "), but was already disconnected"); } c.SetStatus(-2); @@ -755,7 +763,7 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) { void TNetwork::SplitLoad(TClient& c, size_t Sent, size_t Size, bool D, const std::string& Name) { std::ifstream f(Name.c_str(), std::ios::binary); - uint32_t Split = 0x7735940; // 125MB + uint32_t Split = 125 * MB; char* Data; if (Size > Split) Data = new char[Split]; diff --git a/src/TServer.cpp b/src/TServer.cpp index cee40b3..48b2bbb 100644 --- a/src/TServer.cpp +++ b/src/TServer.cpp @@ -125,7 +125,7 @@ void TServer::GlobalParser(const std::weak_ptr& Client, std::string Pac break; auto Futures = LuaAPI::MP::Engine->TriggerEvent("onChatMessage", "", LockedClient->GetID(), LockedClient->GetName(), Packet.substr(Packet.find(':', 3) + 2)); TLuaEngine::WaitForAll(Futures); - LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), Packet.substr(Packet.find(':', 3) + 1)); // FIXME: this needs to be adjusted once lua is merged + LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), Packet.substr(Packet.find(':', 3) + 1)); if (std::any_of(Futures.begin(), Futures.end(), [](const std::shared_ptr& Elem) { return !Elem->Error @@ -150,26 +150,18 @@ void TServer::GlobalParser(const std::weak_ptr& Client, std::string Pac } } -void TServer::HandleEvent(TClient& c, const std::string& Data) { - std::stringstream ss(Data); - std::string t, Name; - int a = 0; - while (std::getline(ss, t, ':')) { - switch (a) { - case 1: - Name = t; - break; - case 2: - LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent(Name, "", c.GetID(), t)); - break; - default: - break; - } - if (a == 2) - break; - a++; +void TServer::HandleEvent(TClient& c, const std::string& RawData) { + // E:Name:Data + // Data is allowed to have ':' + 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); diff --git a/src/main.cpp b/src/main.cpp index f867ccd..76ef7ca 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -113,7 +113,7 @@ int BeamMPServerMain(MainArguments Arguments) { try { fs::current_path(fs::path(MaybeWorkingDirectory.value())); } catch (const std::exception& e) { - beammp_error("Could not set working directory to '" + MaybeWorkingDirectory.value() + "': " + e.what()); + beammp_errorf("Could not set working directory to '{}': {}", MaybeWorkingDirectory.value(), e.what()); } } }