Compare commits

..
2 Commits
Author SHA1 Message Date
Tixx 43e86a1e22 Change to luajit 2026-02-19 22:08:06 +01:00
Tixx 70fc8436ed Remove lua version lock 2026-01-18 23:32:54 +01:00
42 changed files with 561 additions and 2189 deletions

No files matched your search

+5 -8
View File
@@ -3,8 +3,8 @@ name: Linux
on: on:
push: push:
branches: branches:
- "develop" - 'develop'
- "minor" - 'minor'
pull_request: pull_request:
env: env:
@@ -21,8 +21,6 @@ jobs:
include: include:
- distro: debian - distro: debian
version: 12 version: 12
- distro: debian
version: 13
- distro: ubuntu - distro: ubuntu
version: 22.04 version: 22.04
- distro: ubuntu - distro: ubuntu
@@ -47,7 +45,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
submodules: "recursive" submodules: 'recursive'
fetch-depth: 0 fetch-depth: 0
- name: Git config safe directory - name: Git config safe directory
@@ -93,8 +91,6 @@ jobs:
include: include:
- distro: debian - distro: debian
version: 12 version: 12
- distro: debian
version: 13
- distro: ubuntu - distro: ubuntu
version: 22.04 version: 22.04
- distro: ubuntu - distro: ubuntu
@@ -119,7 +115,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
submodules: "recursive" submodules: 'recursive'
fetch-depth: 0 fetch-depth: 0
- name: Git config safe directory - name: Git config safe directory
@@ -155,3 +151,4 @@ jobs:
- name: Test - name: Test
run: ./bin/BeamMP-Server-tests run: ./bin/BeamMP-Server-tests
+4 -8
View File
@@ -3,7 +3,7 @@ on:
push: push:
# Sequence of patterns matched against refs/tags # Sequence of patterns matched against refs/tags
tags: tags:
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
env: env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
@@ -40,8 +40,6 @@ jobs:
include: include:
- distro: debian - distro: debian
version: 12 version: 12
- distro: debian
version: 13
- distro: ubuntu - distro: ubuntu
version: 22.04 version: 22.04
- distro: ubuntu - distro: ubuntu
@@ -66,7 +64,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
submodules: "recursive" submodules: 'recursive'
fetch-depth: 0 fetch-depth: 0
- name: Git config safe directory - name: Git config safe directory
@@ -112,8 +110,6 @@ jobs:
include: include:
- distro: debian - distro: debian
version: 12 version: 12
- distro: debian
version: 13
- distro: ubuntu - distro: ubuntu
version: 22.04 version: 22.04
- distro: ubuntu - distro: ubuntu
@@ -141,7 +137,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
submodules: "recursive" submodules: 'recursive'
fetch-depth: 0 fetch-depth: 0
- name: Git config safe directory - name: Git config safe directory
@@ -195,7 +191,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
submodules: "recursive" submodules: 'recursive'
fetch-depth: 0 fetch-depth: 0
- name: Create Build Environment - name: Create Build Environment
+7 -10
View File
@@ -8,7 +8,7 @@ include(cmake/Vcpkg.cmake) # needs to happen before project()
project( project(
"BeamMP-Server" # replace this "BeamMP-Server" # replace this
VERSION 3.9.3 VERSION 3.3.0
) )
include(cmake/StandardSettings.cmake) include(cmake/StandardSettings.cmake)
@@ -39,7 +39,6 @@ set(PRJ_HEADERS
include/TConfig.h include/TConfig.h
include/TConsole.h include/TConsole.h
include/THeartbeatThread.h include/THeartbeatThread.h
include/TIoPollThread.h
include/TLuaEngine.h include/TLuaEngine.h
include/TLuaPlugin.h include/TLuaPlugin.h
include/TNetwork.h include/TNetwork.h
@@ -53,8 +52,6 @@ set(PRJ_HEADERS
include/Settings.h include/Settings.h
include/Profiling.h include/Profiling.h
include/ChronoWrapper.h include/ChronoWrapper.h
include/TConnectionLimiter.h
include/TLuaResult.h
) )
# add all source files (.cpp) to this, except the one with main() # add all source files (.cpp) to this, except the one with main()
set(PRJ_SOURCES set(PRJ_SOURCES
@@ -68,7 +65,6 @@ set(PRJ_SOURCES
src/TConfig.cpp src/TConfig.cpp
src/TConsole.cpp src/TConsole.cpp
src/THeartbeatThread.cpp src/THeartbeatThread.cpp
src/TIoPollThread.cpp
src/TLuaEngine.cpp src/TLuaEngine.cpp
src/TLuaPlugin.cpp src/TLuaPlugin.cpp
src/TNetwork.cpp src/TNetwork.cpp
@@ -82,18 +78,17 @@ set(PRJ_SOURCES
src/Settings.cpp src/Settings.cpp
src/Profiling.cpp src/Profiling.cpp
src/ChronoWrapper.cpp src/ChronoWrapper.cpp
src/TConnectionLimiter.cpp
src/TLuaResult.cpp
) )
find_package(Lua REQUIRED) # fix for sol2
find_path(LUAJIT_INCLUDE_DIR lua.hpp PATH_SUFFIXES luajit)
# set the source file containing main() # set the source file containing main()
set(PRJ_MAIN src/main.cpp) set(PRJ_MAIN src/main.cpp)
# set the source file containing the test's main # set the source file containing the test's main
set(PRJ_TEST_MAIN test/test_main.cpp) set(PRJ_TEST_MAIN test/test_main.cpp)
# set include paths not part of libraries # set include paths not part of libraries
set(PRJ_INCLUDE_DIRS ${LUA_INCLUDE_DIR}) set(PRJ_INCLUDE_DIRS ${LUAJIT_INCLUDE_DIR})
# set compile features (e.g. standard version) # set compile features (e.g. standard version)
set(PRJ_COMPILE_FEATURES cxx_std_20) set(PRJ_COMPILE_FEATURES cxx_std_20)
# set #defines (test enable/disable not included here) # set #defines (test enable/disable not included here)
@@ -111,9 +106,11 @@ set(PRJ_LIBRARIES
libzip::zip libzip::zip
OpenSSL::SSL OpenSSL::Crypto OpenSSL::SSL OpenSSL::Crypto
CURL::libcurl CURL::libcurl
${LUA_LIBRARIES} lua51
) )
link_directories(${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib)
# add dependency find_package calls and similar here # add dependency find_package calls and similar here
find_package(fmt CONFIG REQUIRED) find_package(fmt CONFIG REQUIRED)
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
-2
View File
@@ -15,5 +15,3 @@ if(NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake) set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake)
endif() endif()
# ensure SOL2 safeties are all ON
add_compile_definitions(SOL_ALL_SAFETIES_ON=1)
+2 -17
View File
@@ -18,7 +18,6 @@
#pragma once #pragma once
#include <atomic>
#include <chrono> #include <chrono>
#include <memory> #include <memory>
#include <optional> #include <optional>
@@ -69,11 +68,8 @@ public:
std::string GetCarPositionRaw(int Ident); std::string GetCarPositionRaw(int Ident);
void SetUDPAddr(const ip::udp::endpoint& Addr) { mUDPAddress = Addr; } void SetUDPAddr(const ip::udp::endpoint& Addr) { mUDPAddress = Addr; }
void SetTCPSock(ip::tcp::socket&& CSock) { mSocket = std::move(CSock); } void SetTCPSock(ip::tcp::socket&& CSock) { mSocket = std::move(CSock); }
// Returns true only for the thread that actually performs socket shutdown/close. void Disconnect(std::string_view Reason);
[[nodiscard]] bool Disconnect(std::string_view Reason); bool IsDisconnected() const { return !mSocket.is_open(); }
bool IsDisconnected() const {
return mDisconnectState.load(std::memory_order_acquire) != EDisconnectState::Connected;
}
// locks // locks
void DeleteCar(int Ident); void DeleteCar(int Ident);
[[nodiscard]] const std::unordered_map<std::string, std::string>& GetIdentifiers() const { return mIdentifiers; } [[nodiscard]] const std::unordered_map<std::string, std::string>& GetIdentifiers() const { return mIdentifiers; }
@@ -85,12 +81,10 @@ public:
[[nodiscard]] std::string GetName() const { return mName; } [[nodiscard]] std::string GetName() const { return mName; }
void SetUnicycleID(int ID) { mUnicycleID = ID; } void SetUnicycleID(int ID) { mUnicycleID = ID; }
void SetID(int ID) { mID = ID; } void SetID(int ID) { mID = ID; }
void SetSelfPing(uint16_t ping ) { mSelfPing = ping; }
[[nodiscard]] int GetOpenCarID() const; [[nodiscard]] int GetOpenCarID() const;
[[nodiscard]] int GetCarCount() const; [[nodiscard]] int GetCarCount() const;
void ClearCars(); void ClearCars();
[[nodiscard]] int GetID() const { return mID; } [[nodiscard]] int GetID() const { return mID; }
[[nodiscard]] uint16_t GetSelfPing() const { return mSelfPing; }
[[nodiscard]] int GetUnicycleID() const { return mUnicycleID; } [[nodiscard]] int GetUnicycleID() const { return mUnicycleID; }
[[nodiscard]] bool IsUDPConnected() const { return mIsUDPConnected; } [[nodiscard]] bool IsUDPConnected() const { return mIsUDPConnected; }
[[nodiscard]] bool IsSynced() const { return mIsSynced; } [[nodiscard]] bool IsSynced() const { return mIsSynced; }
@@ -112,12 +106,6 @@ public:
[[nodiscard]] const std::vector<uint8_t>& GetMagic() const { return mMagic; } [[nodiscard]] const std::vector<uint8_t>& GetMagic() const { return mMagic; }
private: private:
enum class EDisconnectState {
Connected,
Disconnecting,
Disconnected
};
void InsertVehicle(int ID, const std::string& Data); void InsertVehicle(int ID, const std::string& Data);
TServer& mServer; TServer& mServer;
@@ -133,15 +121,12 @@ private:
TSetOfVehicleData mVehicleData; TSetOfVehicleData mVehicleData;
SparseArray<std::string> mVehiclePosition; SparseArray<std::string> mVehiclePosition;
std::string mName = "Unknown Client"; std::string mName = "Unknown Client";
// Once disconnect starts, this client is terminal and its socket must be treated as dead.
std::atomic<EDisconnectState> mDisconnectState { EDisconnectState::Connected };
ip::tcp::socket mSocket; ip::tcp::socket mSocket;
ip::udp::endpoint mUDPAddress {}; ip::udp::endpoint mUDPAddress {};
int mUnicycleID = -1; int mUnicycleID = -1;
std::string mRole; std::string mRole;
std::string mDID; std::string mDID;
int mID = -1; int mID = -1;
uint16_t mSelfPing = 0;
std::chrono::time_point<std::chrono::high_resolution_clock> mLastPingTime = std::chrono::high_resolution_clock::now(); std::chrono::time_point<std::chrono::high_resolution_clock> mLastPingTime = std::chrono::high_resolution_clock::now();
std::vector<uint8_t> mMagic; std::vector<uint8_t> mMagic;
}; };
+1 -5
View File
@@ -129,13 +129,9 @@ private:
static inline std::mutex mShutdownHandlersMutex {}; static inline std::mutex mShutdownHandlersMutex {};
static inline std::deque<TShutdownHandler> mShutdownHandlers {}; static inline std::deque<TShutdownHandler> mShutdownHandlers {};
static inline Version mVersion { 3, 9, 4 }; static inline Version mVersion { 3, 9, 0 };
}; };
/// Used to static_assert in std::visit
template<class>
inline constexpr bool AlwaysFalseV = false;
void SplitString(std::string const& str, const char delim, std::vector<std::string>& out); void SplitString(std::string const& str, const char delim, std::vector<std::string>& out);
std::string LowerString(std::string str); std::string LowerString(std::string str);
-2
View File
@@ -23,11 +23,9 @@
namespace Env { namespace Env {
enum class Key { enum class Key {
MAX_CONCURRENT_CONNECTIONS,
// provider settings // provider settings
PROVIDER_UPDATE_MESSAGE, PROVIDER_UPDATE_MESSAGE,
PROVIDER_DISABLE_CONFIG, PROVIDER_DISABLE_CONFIG,
PROVIDER_DISABLE_MP_SET,
PROVIDER_PORT_ENV, PROVIDER_PORT_ENV,
PROVIDER_IP_ENV PROVIDER_IP_ENV
}; };
+1 -1
View File
@@ -34,7 +34,7 @@ namespace MP {
std::pair<bool, std::string> TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data); std::pair<bool, std::string> TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data);
inline size_t GetPlayerCount() { return Engine->Server().ClientCount(); } 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> DropPlayer(int ID, std::optional<std::string> MaybeReason);
std::pair<bool, std::string> SendChatMessage(int ID, const std::string& Message, const bool& LogChat = true); std::pair<bool, std::string> SendChatMessage(int ID, const std::string& Message);
std::pair<bool, std::string> SendNotification(int ID, const std::string& Message, const std::string& Icon, const std::string& Category); std::pair<bool, std::string> SendNotification(int ID, const std::string& Message, const std::string& Icon, const std::string& Category);
std::pair<bool, std::string> ConfirmationDialog(int ID, const std::string& Title, const std::string& Body, const sol::table& buttons, const std::string& InteractionID, const bool& warning = false, const bool& reportToServer = true, const bool& reportToExtensions = true); std::pair<bool, std::string> ConfirmationDialog(int ID, const std::string& Title, const std::string& Body, const sol::table& buttons, const std::string& InteractionID, const bool& warning = false, const bool& reportToServer = true, const bool& reportToExtensions = true);
std::pair<bool, std::string> RemoveVehicle(int PlayerID, int VehicleID); std::pair<bool, std::string> RemoveVehicle(int PlayerID, int VehicleID);
-90
View File
@@ -1,90 +0,0 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2026 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 <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
class TConnectionLimiter {
public:
struct TStats {
size_t CurrentGlobal = 0;
size_t MaxGlobal = 0;
size_t ActiveIpBuckets = 0;
size_t CurrentMaxPerIp = 0;
size_t MaxPerIp = 0;
size_t SaturatedIpBuckets = 0;
};
class TGuard {
public:
TGuard() = default;
TGuard(TConnectionLimiter* owner, std::string ip);
TGuard(const TGuard&) = delete;
TGuard& operator=(const TGuard&) = delete;
~TGuard() { Release(); }
// not threadsafe
TGuard(TGuard&& other) noexcept { *this = std::move(other); }
// not threadsafe
TGuard& operator=(TGuard&& other) noexcept;
private:
friend class TConnectionLimiter;
// not threadsafe
void Release();
TConnectionLimiter* mOwner { nullptr };
std::string mIp;
};
TConnectionLimiter(size_t maxPerIp, size_t maxGlobal);
[[nodiscard]] std::optional<TGuard> TryAcquire(const std::string& ip);
[[nodiscard]] TStats GetStats();
private:
void Release(const std::string& ip) {
std::unique_lock Lock { mMutex };
auto It = mPerIp.find(ip);
if (It != mPerIp.end()) {
// this guard exists to avoid underflow in case something goes wrong and this gets called too many times
if (It->second > 0)
--It->second;
if (It->second == 0)
mPerIp.erase(It);
}
// this guard exists to avoid underflow in case something goes wrong and this gets called too many times
if (mGlobal > 0)
--mGlobal;
}
const size_t mMaxPerIp;
const size_t mMaxGlobal;
std::mutex mMutex { };
std::unordered_map<std::string, size_t> mPerIp { };
size_t mGlobal = 0;
};
-36
View File
@@ -1,36 +0,0 @@
// 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 <boost/asio/io_context.hpp>
#include <boost/asio/executor_work_guard.hpp>
#include <thread>
class TIoPollThread {
public:
TIoPollThread();
~TIoPollThread();
boost::asio::io_context& IoCtx() noexcept { return mIoCtx; }
private:
boost::asio::io_context mIoCtx;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> mWorkGuard;
std::jthread mThread;
};
+35 -10
View File
@@ -19,12 +19,13 @@
#pragma once #pragma once
#include "Profiling.h" #include "Profiling.h"
#include "TLuaResult.h"
#include "TNetwork.h" #include "TNetwork.h"
#include "TServer.h" #include "TServer.h"
#include <any>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
#include <filesystem> #include <filesystem>
#include <initializer_list>
#include <list> #include <list>
#include <lua.hpp> #include <lua.hpp>
#include <memory> #include <memory>
@@ -33,8 +34,6 @@
#include <queue> #include <queue>
#include <random> #include <random>
#include <set> #include <set>
#include <sol/forward.hpp>
#include <sol/protected_function_result.hpp>
#include <toml.hpp> #include <toml.hpp>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
@@ -59,10 +58,36 @@ namespace fs = std::filesystem;
/** /**
* std::variant means, that TLuaArgTypes may be one of the Types listed as template args * std::variant means, that TLuaArgTypes may be one of the Types listed as template args
*/ */
using TLuaValue = std::variant<std::monostate, std::string, int, JsonString, bool, std::unordered_map<std::string, std::string>, float>; using TLuaValue = std::variant<std::string, int, JsonString, bool, std::unordered_map<std::string, std::string>, float>;
enum TLuaType {
String = 0,
Int = 1,
Json = 2,
Bool = 3,
StringStringMap = 4,
Float = 5,
};
class TLuaPlugin; 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 { struct TLuaPluginConfig {
static inline const std::string FileName = "PluginConfig.toml"; static inline const std::string FileName = "PluginConfig.toml";
TLuaStateId StateId; TLuaStateId StateId;
@@ -141,7 +166,7 @@ public:
const std::optional<std::chrono::high_resolution_clock::duration>& Max = std::nullopt); const std::optional<std::chrono::high_resolution_clock::duration>& Max = std::nullopt);
void ReportErrors(const std::vector<std::shared_ptr<TLuaResult>>& Results); void ReportErrors(const std::vector<std::shared_ptr<TLuaResult>>& Results);
bool HasState(TLuaStateId StateId); bool HasState(TLuaStateId StateId);
[[nodiscard]] std::shared_ptr<TLuaVoidResult> EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script); [[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<TLuaValue>& Args, const std::string& EventName); [[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(TLuaStateId StateID, const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName);
void EnsureStateExists(TLuaStateId StateId, const std::string& Name, bool DontCallOnInit = false); void EnsureStateExists(TLuaStateId StateId, const std::string& Name, bool DontCallOnInit = false);
void RegisterEvent(const std::string& EventName, TLuaStateId StateId, const std::string& FunctionName); void RegisterEvent(const std::string& EventName, TLuaStateId StateId, const std::string& FunctionName);
@@ -202,9 +227,9 @@ public:
// Debugging functions (slow) // Debugging functions (slow)
std::unordered_map<std::string /*event name */, std::vector<std::string> /* handlers */> Debug_GetEventsForState(TLuaStateId StateId); std::unordered_map<std::string /*event name */, std::vector<std::string> /* handlers */> Debug_GetEventsForState(TLuaStateId StateId);
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId); std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId);
std::vector<QueuedFunction> Debug_GetStateFunctionQueueForState(TLuaStateId StateId); std::vector<QueuedFunction> Debug_GetStateFunctionQueueForState(TLuaStateId StateId);
std::vector<TLuaResult::DetachedSnapshot> Debug_GetResultsToCheckForState(TLuaStateId StateId); std::vector<TLuaResult> Debug_GetResultsToCheckForState(TLuaStateId StateId);
private: private:
void CollectAndInitPlugins(); void CollectAndInitPlugins();
@@ -217,7 +242,7 @@ private:
StateThreadData(const std::string& Name, TLuaStateId StateId, TLuaEngine& Engine); StateThreadData(const std::string& Name, TLuaStateId StateId, TLuaEngine& Engine);
StateThreadData(const StateThreadData&) = delete; StateThreadData(const StateThreadData&) = delete;
virtual ~StateThreadData() noexcept { beammp_debug("\"" + mStateId + "\" destroyed"); } virtual ~StateThreadData() noexcept { beammp_debug("\"" + mStateId + "\" destroyed"); }
[[nodiscard]] std::shared_ptr<TLuaVoidResult> EnqueueScript(const TLuaChunk& Script); [[nodiscard]] std::shared_ptr<TLuaResult> EnqueueScript(const TLuaChunk& Script);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName); [[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName, CallStrategy Strategy); [[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName, CallStrategy Strategy);
void RegisterEvent(const std::string& EventName, const std::string& FunctionName); void RegisterEvent(const std::string& EventName, const std::string& FunctionName);
@@ -229,7 +254,7 @@ private:
std::vector<std::string> GetStateTableKeys(const std::vector<std::string>& keys); std::vector<std::string> GetStateTableKeys(const std::vector<std::string>& keys);
// Debug functions, slow // Debug functions, slow
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> Debug_GetStateExecuteQueue(); std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Debug_GetStateExecuteQueue();
std::vector<TLuaEngine::QueuedFunction> Debug_GetStateFunctionQueue(); std::vector<TLuaEngine::QueuedFunction> Debug_GetStateFunctionQueue();
private: private:
@@ -254,7 +279,7 @@ private:
TLuaStateId mStateId; TLuaStateId mStateId;
lua_State* mState; lua_State* mState;
std::thread mThread; std::thread mThread;
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> mStateExecuteQueue; std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> mStateExecuteQueue;
std::recursive_mutex mStateExecuteQueueMutex; std::recursive_mutex mStateExecuteQueueMutex;
std::vector<QueuedFunction> mStateFunctionQueue; std::vector<QueuedFunction> mStateFunctionQueue;
std::mutex mStateFunctionQueueMutex; std::mutex mStateFunctionQueueMutex;
-146
View File
@@ -1,146 +0,0 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2026 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 <condition_variable>
#include <string>
#include <memory>
#include <sol/sol.hpp>
using TLuaStateId = std::string;
struct TDetachedLuaValue {
using Ptr = std::shared_ptr<TDetachedLuaValue>;
using Array = std::vector<TDetachedLuaValue>;
// This weird Ptr indirection is needed because some implementations of libstc++,
// like the GCC 11 one shipped with ubuntu 22.04, need to know the size of the second
// member of the pairs that make up the elements of such a map. It's fine in vector.
using Object = std::unordered_map<std::string, TDetachedLuaValue::Ptr>;
std::variant<std::monostate, bool, double, int, std::string, Array, Object> V;
};
std::ostream& operator<<(std::ostream& os, const TDetachedLuaValue& value);
/// Result for operations that stay within one Lua state (e.g. script loads).
/// This type intentionally does not store any Lua references or serialized Lua values.
struct TLuaVoidResult {
struct Snapshot {
bool Error;
bool Ready;
std::string ErrorMessage;
TLuaStateId StateId;
};
explicit TLuaVoidResult(TLuaStateId StateId)
: mStateId(std::move(StateId)) {}
/// Marks this result as success & sets it as ready, waking all waiting threads.
void MarkReadySuccess();
/// Marks this result as erroneous & sets it as ready, waking all waiting threads.
void MarkReadyError(sol::protected_function_result Res);
/// Marks this result as erroneous & sets it as ready, waking all waiting threads.
void MarkReadyError(std::string Res);
/// Wait (suspend) until this result is ready.
void WaitUntilReady();
bool IsReady() const;
bool IsError() const;
Snapshot GetSnapshot() const;
private:
mutable std::mutex mMutex {};
bool mReady { false };
bool mError { false };
std::string mErrorMessage;
TLuaStateId mStateId;
std::condition_variable mReadyCondition {};
void SetErrorMessageFromResult(sol::protected_function_result& Res) {
if (Res.valid()) {
beammp_lua_errorf("Error was not an error");
}
if (Res.get_type() == sol::type::string) {
mErrorMessage = Res.get<sol::error>().what();
} else {
mErrorMessage = "(unknown error; error object is not inspectable)";
}
}
void MarkAsReady();
};
struct TLuaResult {
struct DetachedSnapshot {
bool Error;
bool Ready;
std::string ErrorMessage;
/// Serialized Lua result. Has no reference to the Lua state, and is safe to use
/// from any thread that acquires it.
TDetachedLuaValue Result;
TLuaStateId StateId;
std::string Function;
};
TLuaResult(TLuaStateId StateId, std::string FunctionName) : mStateId(StateId), mFunction(std::move(FunctionName)) {}
/// Marks this result as success & sets it as ready, waking all threads waiting on it.
void MarkReadySuccess(sol::object Res);
/// Marks this result as successful and ready without serializing a Lua value.
void MarkReadySuccessNoResult();
/// Marks this result as erroneous & sets it as ready, waking all threads waiting on it.
void MarkReadyError(sol::protected_function_result Res);
/// Marks this result as erroneous & sets it as ready, waking all threads waiting on it.
void MarkReadyError(std::string Res);
/// Wait (suspend) until this result is ready. Use `GetDetachedSnapshot` to get the result.
void WaitUntilReady();
bool IsReady() const;
bool IsError() const;
TLuaStateId OwnerState() const;
void SetOwnerState(TLuaStateId StateId);
/// Returns a snapshot with, if not an error, a serialized version of the result
/// object. This never stores or returns raw Lua references and is safe to use
/// across threads.
DetachedSnapshot GetDetachedSnapshot() const;
private:
mutable std::mutex mMutex {};
bool mReady { false };
bool mError { false };
std::string mErrorMessage;
TDetachedLuaValue mDetachedResult;
TLuaStateId mStateId;
std::string mFunction;
std::condition_variable mReadyCondition {};
TDetachedLuaValue Freeze(const sol::object& o, int depth = 0);
void SetErrorMessageFromResult(sol::protected_function_result& Res) {
if (Res.valid()) {
beammp_lua_errorf("Error was not an error");
}
if (Res.get_type() == sol::type::string) {
mErrorMessage = Res.get<sol::error>().what();
} else {
mErrorMessage = "(unknown error; error object is not inspectable)";
}
}
void MarkAsReady();
};
+9 -15
View File
@@ -20,9 +20,9 @@
#include "BoostAliases.h" #include "BoostAliases.h"
#include "Compat.h" #include "Compat.h"
#include "TConnectionLimiter.h"
#include "TResourceManager.h" #include "TResourceManager.h"
#include "TServer.h" #include "TServer.h"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/udp.hpp> #include <boost/asio/ip/udp.hpp>
struct TConnection; struct TConnection;
@@ -34,21 +34,16 @@ public:
[[nodiscard]] bool TCPSend(TClient& c, const std::vector<uint8_t>& Data, bool IsSync = false); [[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 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); [[nodiscard]] bool Respond(TClient& c, const std::vector<uint8_t>& MSG, bool Rel, bool isSync = false);
std::shared_ptr<TClient> CreateClient(boost::asio::ip::tcp::socket&& TCPSock); std::shared_ptr<TClient> CreateClient(ip::tcp::socket&& TCPSock);
std::vector<uint8_t> TCPRcv(TClient& c, bool WithTimeout = false); std::vector<uint8_t> TCPRcv(TClient& c);
void ClientKick(TClient& c, const std::string& R); void ClientKick(TClient& c, const std::string& R);
void DisconnectClient(const std::weak_ptr<TClient>& c, const std::string& R);
void DisconnectClient(TClient& c, const std::string& R);
[[nodiscard]] bool SyncClient(const std::weak_ptr<TClient>& c); [[nodiscard]] bool SyncClient(const std::weak_ptr<TClient>& c);
void Identify(TConnection&& client, TConnectionLimiter::TGuard&&); void Identify(TConnection&& client);
std::shared_ptr<TClient> Authentication(TConnection&& ClientConnection); std::shared_ptr<TClient> Authentication(TConnection&& ClientConnection);
void SyncResources(TClient& c); void SyncResources(TClient& c);
[[nodiscard]] bool UDPSend(TClient& Client, std::vector<uint8_t> Data); [[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 SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self, bool Rel);
void UpdatePlayer(TClient& Client); void UpdatePlayer(TClient& Client);
boost::system::error_code ReadWithTimeout(boost::asio::ip::tcp::socket& Socket, void* Buf, size_t Len, std::chrono::steady_clock::duration Timeout);
boost::system::error_code ReadWithTimeout(TConnection& Connection, void* Buf, size_t Len, std::chrono::steady_clock::duration Timeout);
[[nodiscard]] TConnectionLimiter::TStats GetConnectionLimiterStats() { return mConnectionLimiter.GetStats(); }
TResourceManager& ResourceManager() const { return mResourceManager; } TResourceManager& ResourceManager() const { return mResourceManager; }
@@ -58,14 +53,13 @@ private:
TServer& mServer; TServer& mServer;
TPPSMonitor& mPPSMonitor; TPPSMonitor& mPPSMonitor;
boost::asio::ip::udp::socket mUDPSock; ip::udp::socket mUDPSock;
TResourceManager& mResourceManager; TResourceManager& mResourceManager;
std::thread mUDPThread; std::thread mUDPThread;
std::thread mTCPThread; std::thread mTCPThread;
std::mutex mOpenIDMutex; std::mutex mOpenIDMutex;
TConnectionLimiter mConnectionLimiter;
std::vector<uint8_t> UDPRcvFromClient(boost::asio::ip::udp::endpoint& ClientEndpoint); std::vector<uint8_t> UDPRcvFromClient(ip::udp::endpoint& ClientEndpoint);
void OnConnect(const std::weak_ptr<TClient>& c); void OnConnect(const std::weak_ptr<TClient>& c);
void TCPClient(const std::weak_ptr<TClient>& c); void TCPClient(const std::weak_ptr<TClient>& c);
void Looper(const std::weak_ptr<TClient>& c); void Looper(const std::weak_ptr<TClient>& c);
@@ -73,9 +67,9 @@ private:
void OnDisconnect(const std::weak_ptr<TClient>& ClientPtr); void OnDisconnect(const std::weak_ptr<TClient>& ClientPtr);
void Parse(TClient& c, const std::vector<uint8_t>& Packet); void Parse(TClient& c, const std::vector<uint8_t>& Packet);
void SendFile(TClient& c, const std::string& Name); void SendFile(TClient& c, const std::string& Name);
static bool TCPSendRaw(TClient& C, boost::asio::ip::tcp::socket& socket, const uint8_t* Data, size_t Size); static bool TCPSendRaw(TClient& C, ip::tcp::socket& socket, const uint8_t* Data, size_t Size);
void SendFileToClient(TClient& c, size_t Size, const std::string& Name); static void SendFileToClient(TClient& c, size_t Size, const std::string& Name);
static const uint8_t* SendSplit(TClient& c, boost::asio::ip::tcp::socket& Socket, const uint8_t* DataPtr, size_t Size); 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::string HashPassword(const std::string& str);
+3 -6
View File
@@ -20,7 +20,6 @@
#include "IThreaded.h" #include "IThreaded.h"
#include "RWMutex.h" #include "RWMutex.h"
#include "TIoPollThread.h"
#include "TScopedTimer.h" #include "TScopedTimer.h"
#include <functional> #include <functional>
#include <memory> #include <memory>
@@ -51,13 +50,11 @@ public:
const TScopedTimer UptimeTimer; const TScopedTimer UptimeTimer;
io_context& IoCtx() { return mIoCtxPoller.IoCtx(); } // asio io context
io_context& IoCtx() { return mIoCtx; }
uint64_t GetServerTimeMS() const { return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - TimeSyncStart).count();}
double GetServerTime() const { return std::chrono::duration<double>(std::chrono::steady_clock::now() - TimeSyncStart).count(); }
private: private:
std::chrono::time_point<std::chrono::steady_clock> TimeSyncStart; io_context mIoCtx {};
TIoPollThread mIoCtxPoller;
TClientSet mClients; TClientSet mClients;
mutable RWMutex mClientsMutex; mutable RWMutex mClientsMutex;
static void ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network); static void ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network);
+1 -1
View File
@@ -4,4 +4,4 @@ set -ex
./vcpkg/bootstrap-vcpkg.sh ./vcpkg/bootstrap-vcpkg.sh
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections" -DBeamMP-Server_ENABLE_LTO=ON
+1 -1
View File
@@ -4,4 +4,4 @@ set -ex
./vcpkg/bootstrap-vcpkg.sh ./vcpkg/bootstrap-vcpkg.sh
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections" -DBeamMP-Server_ENABLE_LTO=ON
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
set -ex
apt-get update -y
apt-get install -y liblua5.3-0 liblua5.3-dev curl zip unzip tar cmake make git g++ ninja-build
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
set -ex
git config --global --add safe.directory $(pwd)
git config --global --add safe.directory $(pwd)/vcpkg
git config --global --add safe.directory $(pwd)/deps/commandline
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
set -ex
./vcpkg/bootstrap-vcpkg.sh
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
set -ex
cmake --build bin --parallel -t BeamMP-Server-tests
-10
View File
@@ -1,10 +0,0 @@
#!/bin/bash
set -ex
cmake --build bin --parallel -t BeamMP-Server
objcopy --only-keep-debug bin/BeamMP-Server bin/BeamMP-Server.debug
objcopy --add-gnu-debuglink bin/BeamMP-Server bin/BeamMP-Server.debug
strip -s bin/BeamMP-Server
@@ -1,8 +0,0 @@
#!/bin/bash
set -ex
apt-get update -y
apt-get install -y liblua5.3-0 curl
+1 -1
View File
@@ -4,4 +4,4 @@ set -ex
./vcpkg/bootstrap-vcpkg.sh ./vcpkg/bootstrap-vcpkg.sh
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections" -DBeamMP-Server_ENABLE_LTO=ON
+1 -1
View File
@@ -4,4 +4,4 @@ set -ex
./vcpkg/bootstrap-vcpkg.sh ./vcpkg/bootstrap-vcpkg.sh
cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON cmake . -B bin $1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -g -Wl,-z,norelro -Wl,--hash-style=gnu -Wl,-z,noseparate-code -ffunction-sections -fdata-sections -Wl,--gc-sections" -DBeamMP-Server_ENABLE_LTO=ON
+3 -11
View File
@@ -76,13 +76,7 @@ std::string TClient::GetCarPositionRaw(int Ident) {
} }
} }
bool TClient::Disconnect(std::string_view Reason) { void TClient::Disconnect(std::string_view Reason) {
// Do not remove this guard: concurrent close() on the same socket can crash in Asio internals.
EDisconnectState Expected = EDisconnectState::Connected;
if (!mDisconnectState.compare_exchange_strong(Expected, EDisconnectState::Disconnecting, std::memory_order_acq_rel, std::memory_order_acquire)) {
return false;
}
beammp_debugf("Disconnecting client {} for reason: {}", GetID(), Reason); beammp_debugf("Disconnecting client {} for reason: {}", GetID(), Reason);
boost::system::error_code ec; boost::system::error_code ec;
if (mSocket.is_open()) { if (mSocket.is_open()) {
@@ -97,9 +91,6 @@ bool TClient::Disconnect(std::string_view Reason) {
} else { } else {
beammp_debug("Socket is already closed."); beammp_debug("Socket is already closed.");
} }
// Terminal state: this client must not perform TCP IO after this point.
mDisconnectState.store(EDisconnectState::Disconnected, std::memory_order_release);
return true;
} }
void TClient::SetCarPosition(int Ident, const std::string& Data) { void TClient::SetCarPosition(int Ident, const std::string& Data) {
@@ -178,7 +169,8 @@ std::optional<std::weak_ptr<TClient>> GetClient(TServer& Server, int ID) {
std::optional<std::weak_ptr<TClient>> MaybeClient { std::nullopt }; std::optional<std::weak_ptr<TClient>> MaybeClient { std::nullopt };
Server.ForEachClient([&](std::weak_ptr<TClient> CPtr) -> bool { Server.ForEachClient([&](std::weak_ptr<TClient> CPtr) -> bool {
ReadLock Lock(Server.GetClientMutex()); ReadLock Lock(Server.GetClientMutex());
if (auto C = CPtr.lock()) { if (!CPtr.expired()) {
auto C = CPtr.lock();
if (C->GetID() == ID) { if (C->GetID() == ID) {
MaybeClient = CPtr; MaybeClient = CPtr;
return false; return false;
-1
View File
@@ -31,7 +31,6 @@
#include <sstream> #include <sstream>
#include <thread> #include <thread>
#include "TLuaResult.h"
#include "Compat.h" #include "Compat.h"
#include "CustomAssert.h" #include "CustomAssert.h"
#include "Http.h" #include "Http.h"
-6
View File
@@ -36,18 +36,12 @@ std::string_view Env::ToString(Env::Key key) {
case Key::PROVIDER_DISABLE_CONFIG: case Key::PROVIDER_DISABLE_CONFIG:
return "BEAMMP_PROVIDER_DISABLE_CONFIG"; return "BEAMMP_PROVIDER_DISABLE_CONFIG";
break; break;
case Key::PROVIDER_DISABLE_MP_SET:
return "BEAMMP_PROVIDER_DISABLE_MP_SET";
break;
case Key::PROVIDER_PORT_ENV: case Key::PROVIDER_PORT_ENV:
return "BEAMMP_PROVIDER_PORT_ENV"; return "BEAMMP_PROVIDER_PORT_ENV";
break; break;
case Key::PROVIDER_IP_ENV: case Key::PROVIDER_IP_ENV:
return "BEAMMP_PROVIDER_IP_ENV"; return "BEAMMP_PROVIDER_IP_ENV";
break; break;
case Key::MAX_CONCURRENT_CONNECTIONS:
return "BEAMMP_MAX_CONCURRENT_CONNECTIONS";
break;
} }
return ""; return "";
} }
+11 -107
View File
@@ -18,12 +18,15 @@
#include "Http.h" #include "Http.h"
#include "Client.h"
#include "Common.h" #include "Common.h"
#include "CustomAssert.h" #include "CustomAssert.h"
#include "LuaAPI.h"
#include <curl/easy.h>
#include <map> #include <map>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <random>
#include <stdexcept>
using json = nlohmann::json; using json = nlohmann::json;
@@ -34,107 +37,17 @@ static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void*
return size * nmemb; return size * nmemb;
} }
struct CurlDeleter {
void operator()(CURL* c) const noexcept {
if (c)
curl_easy_cleanup(c);
}
};
static std::mutex gCurlPoolMutex;
static std::map<CURL*, bool> gCurlPool; // false = free, true = in use
constexpr size_t MAX_CURL_POOL_SIZE = 128;
static CURL* AcquireCurl() {
std::unique_lock Lock(gCurlPoolMutex);
for (auto& [Curl, InUse] : gCurlPool) {
if (!InUse) {
InUse = true;
curl_easy_reset(Curl);
return Curl;
}
}
// none found, if we're under the limit add one!
if (gCurlPool.size() >= MAX_CURL_POOL_SIZE) {
beammp_debugf("Ran out of curl handles for network requests, skipping this request");
return nullptr;
}
CURL* Curl = curl_easy_init();
if (!Curl) {
// failed, ignore
return nullptr;
}
gCurlPool[Curl] = true;
return Curl;
}
static void ReleaseCurl(CURL* curl) {
if (!curl) {
return;
}
std::unique_lock Lock(gCurlPoolMutex);
auto It = gCurlPool.find(curl);
if (It != gCurlPool.end()) {
It->second = false;
}
}
/// RAII container for curl handles to ensure correct release
class CurlLease {
private:
CURL* mHandle = nullptr;
public:
CurlLease() : mHandle(AcquireCurl()) {}
~CurlLease() {
ReleaseCurl(mHandle);
}
CurlLease(const CurlLease&) = delete;
CurlLease& operator=(const CurlLease&) = delete;
CurlLease(CurlLease&& other) noexcept
: mHandle(other.mHandle) {
other.mHandle = nullptr;
}
CurlLease& operator=(CurlLease&& other) noexcept {
if (this != &other) {
ReleaseCurl(mHandle);
mHandle = other.mHandle;
other.mHandle = nullptr;
}
return *this;
}
CURL* GetHandle() const noexcept {
return mHandle;
}
};
std::string Http::GET(const std::string& url, unsigned int* status) { std::string Http::GET(const std::string& url, unsigned int* status) {
std::string Ret; std::string Ret;
CurlLease Lease{}; static thread_local CURL* curl = curl_easy_init();
CURL* curl = Lease.GetHandle();
if (curl) { if (curl) {
CURLcode res; CURLcode res;
char errbuf[CURL_ERROR_SIZE]; char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast<void*>(&Ret)); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds
// ensure we dont keep connections open for long
curl_easy_setopt(curl, CURLOPT_MAXCONNECTS, 8L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_MAXAGE_CONN, 60L);
curl_easy_setopt(curl, CURLOPT_MAXLIFETIME_CONN, 300L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
errbuf[0] = 0; errbuf[0] = 0;
@@ -158,16 +71,15 @@ std::string Http::GET(const std::string& url, unsigned int* status) {
std::string Http::POST(const std::string& url, const std::string& body, const std::string& ContentType, unsigned int* status, const std::map<std::string, std::string>& headers) { std::string Http::POST(const std::string& url, const std::string& body, const std::string& ContentType, unsigned int* status, const std::map<std::string, std::string>& headers) {
std::string Ret; std::string Ret;
CurlLease Lease{}; static thread_local CURL* curl = curl_easy_init();
CURL* curl = Lease.GetHandle();
if (curl) { if (curl) {
CURLcode res; CURLcode res;
char errbuf[CURL_ERROR_SIZE]; char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast<void*>(&Ret)); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size());
struct curl_slist* list = nullptr; struct curl_slist* list = nullptr;
@@ -178,15 +90,7 @@ std::string Http::POST(const std::string& url, const std::string& body, const st
} }
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds
// ensure we dont keep connections open for long
curl_easy_setopt(curl, CURLOPT_MAXCONNECTS, 8L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_MAXAGE_CONN, 60L);
curl_easy_setopt(curl, CURLOPT_MAXLIFETIME_CONN, 300L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
errbuf[0] = 0; errbuf[0] = 0;
+86 -130
View File
@@ -143,23 +143,22 @@ static inline std::pair<bool, std::string> InternalTriggerClientEvent(int Player
return { true, "" }; return { true, "" };
} else { } else {
auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID); auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID);
if (MaybeClient) { if (!MaybeClient || MaybeClient.value().expired()) {
if (auto c = MaybeClient.value().lock()) { beammp_lua_errorf("TriggerClientEvent invalid Player ID '{}'", PlayerID);
if (!c->IsSyncing() && !c->IsSynced()) { return { false, "Invalid Player ID" };
return { false, "Player hasn't joined yet" }; }
} auto c = MaybeClient.value().lock();
if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) { if (!c->IsSyncing() && !c->IsSynced()) {
beammp_lua_errorf("Respond failed, dropping client {}", PlayerID); return { false, "Player hasn't joined yet" };
LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets");
return { false, "Respond failed, dropping client" };
}
return { true, "" };
}
} }
beammp_lua_errorf("TriggerClientEvent invalid Player ID '{}'", PlayerID); if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
return { false, "Invalid Player ID" }; 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, "" };
} }
} }
@@ -170,48 +169,37 @@ 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) { std::pair<bool, std::string> LuaAPI::MP::DropPlayer(int ID, std::optional<std::string> MaybeReason) {
auto MaybeClient = GetClient(Engine->Server(), ID); auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) { if (!MaybeClient || MaybeClient.value().expired()) {
if (auto c = MaybeClient.value().lock()) { beammp_lua_errorf("Tried to drop client with id {}, who doesn't exist", ID);
LuaAPI::MP::Engine->Network().ClientKick(*c, MaybeReason.value_or("No reason")); return { false, "Player does not exist" };
return { true, "" };
}
} }
auto c = MaybeClient.value().lock();
beammp_lua_errorf("Tried to drop client with id {}, who doesn't exist", ID); LuaAPI::MP::Engine->Network().ClientKick(*c, MaybeReason.value_or("No reason"));
return { false, "Player does not exist" }; return { true, "" };
} }
std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::string& Message, const bool& LogChat) { std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::string& Message) {
std::pair<bool, std::string> Result; std::pair<bool, std::string> Result;
std::string Packet = "C:Server: " + Message; std::string Packet = "C:Server: " + Message;
if (ID == -1) { if (ID == -1) {
if (LogChat) { LogChatMessage("<Server> (to everyone) ", -1, Message);
LogChatMessage("<Server> (to everyone) ", -1, Message);
}
Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true); Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
Result.first = true; Result.first = true;
} else { } else {
auto MaybeClient = GetClient(Engine->Server(), ID); auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) { if (MaybeClient && !MaybeClient.value().expired()) {
if (auto c = MaybeClient.value().lock()) { auto c = MaybeClient.value().lock();
if (!c->IsSynced()) { if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player still syncing data";
return Result;
}
if (LogChat) {
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);
// TODO: should we return an error here?
}
Result.first = true;
} else {
beammp_lua_error("SendChatMessage invalid argument [1] invalid ID");
Result.first = false; Result.first = false;
Result.second = "Invalid Player ID"; Result.second = "Player still syncing data";
return Result;
} }
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);
// TODO: should we return an error here?
}
Result.first = true;
} else { } else {
beammp_lua_error("SendChatMessage invalid argument [1] invalid ID"); beammp_lua_error("SendChatMessage invalid argument [1] invalid ID");
Result.first = false; Result.first = false;
@@ -231,23 +219,18 @@ std::pair<bool, std::string> LuaAPI::MP::SendNotification(int ID, const std::str
} else { } else {
auto MaybeClient = GetClient(Engine->Server(), ID); auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) { if (MaybeClient) {
if (auto c = MaybeClient.value().lock()) { auto c = MaybeClient.value().lock();
if (!c->IsSynced()) { if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player is not synced yet";
return Result;
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send notification to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else {
beammp_lua_error("SendNotification invalid argument [1] invalid ID");
Result.first = false; Result.first = false;
Result.second = "Invalid Player ID"; Result.second = "Player is not synced yet";
return Result;
} }
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send notification to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else { } else {
beammp_lua_error("SendNotification invalid argument [1] invalid ID"); beammp_lua_error("SendNotification invalid argument [1] invalid ID");
Result.first = false; Result.first = false;
@@ -278,23 +261,18 @@ std::pair<bool, std::string> LuaAPI::MP::ConfirmationDialog(int ID, const std::s
} else { } else {
auto MaybeClient = GetClient(Engine->Server(), ID); auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) { if (MaybeClient) {
if (auto c = MaybeClient.value().lock()) { auto c = MaybeClient.value().lock();
if (!c->IsSynced()) { if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player is not synced yet";
return Result;
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send confirmation dialog to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else {
beammp_lua_error("ConfirmationDialog invalid argument [1] invalid ID");
Result.first = false; Result.first = false;
Result.second = "Invalid Player ID"; Result.second = "Player is not synced yet";
return Result;
} }
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send confirmation dialog to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else { } else {
beammp_lua_error("ConfirmationDialog invalid argument [1] invalid ID"); beammp_lua_error("ConfirmationDialog invalid argument [1] invalid ID");
Result.first = false; Result.first = false;
@@ -308,27 +286,22 @@ std::pair<bool, std::string> LuaAPI::MP::ConfirmationDialog(int ID, const std::s
std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) { std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) {
std::pair<bool, std::string> Result; std::pair<bool, std::string> Result;
auto MaybeClient = GetClient(Engine->Server(), PID); auto MaybeClient = GetClient(Engine->Server(), PID);
if (MaybeClient) { if (!MaybeClient || MaybeClient.value().expired()) {
if (auto c = MaybeClient.value().lock()) {
if (c->GetCarData(VID) != nlohmann::detail::value_t::null) {
std::string Destroy = "Od:" + std::to_string(PID) + "-" + std::to_string(VID);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", PID, VID));
Engine->Network().SendToAll(nullptr, StringToVector(Destroy), true, true);
c->DeleteCar(VID);
Result.first = true;
} else {
Result.first = false;
Result.second = "Vehicle does not exist";
}
} else {
beammp_lua_error("RemoveVehicle invalid Player ID");
Result.first = false;
Result.second = "Invalid Player ID";
}
} else {
beammp_lua_error("RemoveVehicle invalid Player ID"); beammp_lua_error("RemoveVehicle invalid Player ID");
Result.first = false; Result.first = false;
Result.second = "Invalid Player ID"; Result.second = "Invalid Player ID";
return Result;
}
auto c = MaybeClient.value().lock();
if (c->GetCarData(VID) != nlohmann::detail::value_t::null) {
std::string Destroy = "Od:" + std::to_string(PID) + "-" + std::to_string(VID);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", PID, VID));
Engine->Network().SendToAll(nullptr, StringToVector(Destroy), true, true);
c->DeleteCar(VID);
Result.first = true;
} else {
Result.first = false;
Result.second = "Vehicle does not exist";
} }
return Result; return Result;
} }
@@ -435,22 +408,20 @@ void LuaAPI::MP::Sleep(size_t Ms) {
bool LuaAPI::MP::IsPlayerConnected(int ID) { bool LuaAPI::MP::IsPlayerConnected(int ID) {
auto MaybeClient = GetClient(Engine->Server(), ID); auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) { if (MaybeClient && !MaybeClient.value().expired()) {
if (auto c = MaybeClient.value().lock()) { return MaybeClient.value().lock()->IsUDPConnected();
return c->IsUDPConnected(); } else {
} return false;
} }
return false;
} }
bool LuaAPI::MP::IsPlayerGuest(int ID) { bool LuaAPI::MP::IsPlayerGuest(int ID) {
auto MaybeClient = GetClient(Engine->Server(), ID); auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) { if (MaybeClient && !MaybeClient.value().expired()) {
if (auto c = MaybeClient.value().lock()) { return MaybeClient.value().lock()->IsGuest();
return c->IsGuest(); } else {
} return false;
} }
return false;
} }
void LuaAPI::MP::PrintRaw(sol::variadic_args Args) { void LuaAPI::MP::PrintRaw(sol::variadic_args Args) {
@@ -465,16 +436,7 @@ void LuaAPI::MP::PrintRaw(sol::variadic_args Args) {
} }
int LuaAPI::PanicHandler(lua_State* State) { int LuaAPI::PanicHandler(lua_State* State) {
// panic path: use raw lua c api only; sol2 conversions can raise lua_error beammp_lua_error("PANIC: " + sol::stack::get<std::string>(State, 1));
// and re-enter panic recursively.
const int ErrorType = lua_type(State, -1);
if (ErrorType == LUA_TSTRING) {
const char* Message = lua_tostring(State, -1);
beammp_lua_error(std::string("PANIC: ") + (Message ? Message : "(null)"));
} else {
const char* ErrorTypeName = lua_typename(State, ErrorType);
beammp_lua_error(std::string("PANIC: non-string error object (") + (ErrorTypeName ? ErrorTypeName : "unknown") + ")");
}
return 0; return 0;
} }
@@ -693,22 +655,6 @@ std::string LuaAPI::FS::ConcatPaths(sol::variadic_args Args) {
return Result; return Result;
} }
static bool IsLuaArray(const sol::table& table) {
for (const auto& pair : table) {
if (pair.first.get_type() != sol::type::number) {
return false;
}
if (!pair.first.is<int>()) {
return false;
}
int key = pair.first.as<int>();
if (key < 1) {
return false;
}
}
return true;
}
static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, const sol::object& right, bool is_array, size_t depth = 0) { static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, const sol::object& right, bool is_array, size_t depth = 0) {
if (depth > 100) { if (depth > 100) {
beammp_lua_error("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");
@@ -778,7 +724,12 @@ static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, c
if (right.as<sol::table>().empty()) { if (right.as<sol::table>().empty()) {
value = nlohmann::json::object(); value = nlohmann::json::object();
} else { } else {
bool local_is_array = IsLuaArray(right.as<sol::table>()); bool local_is_array = true;
for (const auto& pair : right.as<sol::table>()) {
if (pair.first.get_type() != sol::type::number) {
local_is_array = false;
}
}
for (const auto& pair : right.as<sol::table>()) { for (const auto& pair : right.as<sol::table>()) {
JsonEncodeRecursive(value, pair.first, pair.second, local_is_array, depth + 1); JsonEncodeRecursive(value, pair.first, pair.second, local_is_array, depth + 1);
} }
@@ -801,7 +752,12 @@ std::string LuaAPI::MP::JsonEncode(const sol::table& object) {
if (object.as<sol::table>().empty()) { if (object.as<sol::table>().empty()) {
json = nlohmann::json::object(); json = nlohmann::json::object();
} else { } else {
bool is_array = IsLuaArray(object.as<sol::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) { for (const auto& entry : object) {
JsonEncodeRecursive(json, entry.first, entry.second, is_array); JsonEncodeRecursive(json, entry.first, entry.second, is_array);
} }
-96
View File
@@ -1,96 +0,0 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2026 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 "TConnectionLimiter.h"
#include <mutex>
#include <optional>
#include "Common.h"
TConnectionLimiter::TConnectionLimiter(size_t maxPerIp, size_t maxGlobal)
: mMaxPerIp(maxPerIp)
, mMaxGlobal(maxGlobal) {
if (maxPerIp == 0) {
beammp_errorf("Max connections count per IP is set to zero; the server would reject ALL connections");
throw std::runtime_error("Invalid maximum connections per IP setting");
}
if (maxGlobal == 0) {
beammp_errorf("Max connection count is set to zero; the server would reject ALL connections");
throw std::runtime_error("Invalid maximum connections setting");
}
}
std::optional<TConnectionLimiter::TGuard> TConnectionLimiter::TryAcquire(const std::string& ip) {
std::unique_lock Lock { mMutex };
if (mGlobal >= mMaxGlobal) return std::nullopt;
// `It` is the inserted element (so if insertion worked, its 0), or its the element that
// was already there, in which case we must check the ip connection limit
auto [It, _] = mPerIp.try_emplace(ip, 0);
if (It->second >= mMaxPerIp) {
return std::nullopt;
}
// now increment the counter finally
++It->second;
++mGlobal;
// RAII guard will drop the count once destructed
return TGuard(this, ip);
}
TConnectionLimiter::TStats TConnectionLimiter::GetStats() {
std::unique_lock Lock { mMutex };
TStats Stats;
Stats.CurrentGlobal = mGlobal;
Stats.MaxGlobal = mMaxGlobal;
Stats.ActiveIpBuckets = mPerIp.size();
Stats.MaxPerIp = mMaxPerIp;
for (const auto& [_, Count] : mPerIp) {
if (Count > Stats.CurrentMaxPerIp) {
Stats.CurrentMaxPerIp = Count;
}
if (Count >= mMaxPerIp) {
++Stats.SaturatedIpBuckets;
}
}
return Stats;
}
TConnectionLimiter::TGuard::TGuard(TConnectionLimiter* owner, std::string ip)
: mOwner(owner)
, mIp(std::move(ip)) {
beammp_debugf("Acquired connection guard for {}", mIp);
}
TConnectionLimiter::TGuard& TConnectionLimiter::TGuard::operator=(TGuard&& other) noexcept {
// identity check
if (this != &other) {
Release();
mOwner = other.mOwner;
mIp = std::move(other.mIp);
other.mOwner = nullptr;
// setting this so its obvious when this happens, instead of being UB or empty string
other.mIp = "<moved-from>";
}
return *this;
}
void TConnectionLimiter::TGuard::Release() {
if (mOwner) {
mOwner->Release(mIp);
mOwner = nullptr;
beammp_debugf("Released connection guard for {}", mIp);
}
}
+31 -41
View File
@@ -22,9 +22,9 @@
#include "Client.h" #include "Client.h"
#include "CustomAssert.h" #include "CustomAssert.h"
#include "Http.h"
#include "LuaAPI.h" #include "LuaAPI.h"
#include "TLuaEngine.h" #include "TLuaEngine.h"
#include "Http.h"
#include <ctime> #include <ctime>
#include <lua.hpp> #include <lua.hpp>
@@ -260,8 +260,7 @@ void TConsole::Command_Version(const std::string& cmd, const std::vector<std::st
Application::Console().WriteRaw("Platform: " + platform); Application::Console().WriteRaw("Platform: " + platform);
Application::Console().WriteRaw("Server: v" + Application::ServerVersionString()); Application::Console().WriteRaw("Server: v" + Application::ServerVersionString());
std::string lua_version = fmt::format("Lua: v{}.{}.{}", LUA_VERSION_MAJOR, LUA_VERSION_MINOR, LUA_VERSION_RELEASE); Application::Console().WriteRaw(LUA_RELEASE);
Application::Console().WriteRaw(lua_version);
std::string openssl_version = fmt::format("OpenSSL: v{}.{}.{}", OPENSSL_VERSION_MAJOR, OPENSSL_VERSION_MINOR, OPENSSL_VERSION_PATCH); std::string openssl_version = fmt::format("OpenSSL: v{}.{}.{}", OPENSSL_VERSION_MAJOR, OPENSSL_VERSION_MINOR, OPENSSL_VERSION_PATCH);
Application::Console().WriteRaw(openssl_version); Application::Console().WriteRaw(openssl_version);
} }
@@ -296,7 +295,8 @@ void TConsole::Command_NetTest(const std::string& cmd, const std::vector<std::st
unsigned int status = 0; unsigned int status = 0;
std::string T = Http::GET( std::string T = Http::GET(
Application::GetServerCheckUrl() + "/api/v2/beammp/" + std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)), &status); Application::GetServerCheckUrl() + "/api/v2/beammp/" + std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)), &status
);
beammp_debugf("Status and response from Server Check API: {0}, {1}", status, T); beammp_debugf("Status and response from Server Check API: {0}, {1}", status, T);
@@ -333,9 +333,10 @@ void TConsole::Command_Kick(const std::string&, const std::vector<std::string>&
return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1); return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1);
}; };
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool { mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (auto Locked = Client.lock()) { if (!Client.expired()) {
if (NameCompare(Locked->GetName(), Name)) { auto locked = Client.lock();
mLuaEngine->Network().ClientKick(*Locked, Reason); if (NameCompare(locked->GetName(), Name)) {
mLuaEngine->Network().ClientKick(*locked, Reason);
Kicked = true; Kicked = true;
return false; return false;
} }
@@ -354,7 +355,7 @@ std::tuple<std::string, std::vector<std::string>> TConsole::ParseCommand(const s
// It correctly splits arguments, including respecting single and double quotes, as well as backticks // It correctly splits arguments, including respecting single and double quotes, as well as backticks
auto End_i = CommandWithArgs.find_first_of(' '); auto End_i = CommandWithArgs.find_first_of(' ');
std::string Command = CommandWithArgs.substr(0, End_i); std::string Command = CommandWithArgs.substr(0, End_i);
std::string ArgsStr { }; std::string ArgsStr {};
if (End_i != std::string::npos) { if (End_i != std::string::npos) {
ArgsStr = CommandWithArgs.substr(End_i); ArgsStr = CommandWithArgs.substr(End_i);
} }
@@ -564,10 +565,11 @@ void TConsole::Command_List(const std::string&, const std::vector<std::string>&
std::stringstream ss; std::stringstream ss;
ss << std::left << std::setw(25) << "Name" << std::setw(6) << "ID" << std::setw(6) << "Cars" << std::endl; ss << std::left << std::setw(25) << "Name" << std::setw(6) << "ID" << std::setw(6) << "Cars" << std::endl;
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool { mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (auto Locked = Client.lock()) { if (!Client.expired()) {
ss << std::left << std::setw(25) << Locked->GetName() auto locked = Client.lock();
<< std::setw(6) << Locked->GetID() ss << std::left << std::setw(25) << locked->GetName()
<< std::setw(6) << Locked->GetCarCount() << "\n"; << std::setw(6) << locked->GetID()
<< std::setw(6) << locked->GetCarCount() << "\n";
} }
return true; return true;
}); });
@@ -590,7 +592,8 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
size_t MissedPacketQueueSum = 0; size_t MissedPacketQueueSum = 0;
int LargestSecondsSinceLastPing = 0; int LargestSecondsSinceLastPing = 0;
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool { mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (auto Locked = Client.lock()) { if (!Client.expired()) {
auto Locked = Client.lock();
CarCount += Locked->GetCarCount(); CarCount += Locked->GetCarCount();
ConnectedCount += Locked->IsUDPConnected() ? 1 : 0; ConnectedCount += Locked->IsUDPConnected() ? 1 : 0;
GuestCount += Locked->IsGuest() ? 1 : 0; GuestCount += Locked->IsGuest() ? 1 : 0;
@@ -609,11 +612,11 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
size_t SystemsBad = 0; size_t SystemsBad = 0;
size_t SystemsShuttingDown = 0; size_t SystemsShuttingDown = 0;
size_t SystemsShutdown = 0; size_t SystemsShutdown = 0;
std::string SystemsBadList { }; std::string SystemsBadList {};
std::string SystemsGoodList { }; std::string SystemsGoodList {};
std::string SystemsStartingList { }; std::string SystemsStartingList {};
std::string SystemsShuttingDownList { }; std::string SystemsShuttingDownList {};
std::string SystemsShutdownList { }; std::string SystemsShutdownList {};
auto Statuses = Application::GetSubsystemStatuses(); auto Statuses = Application::GetSubsystemStatuses();
for (const auto& NameStatusPair : Statuses) { for (const auto& NameStatusPair : Statuses) {
switch (NameStatusPair.second) { switch (NameStatusPair.second) {
@@ -649,7 +652,6 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
SystemsShutdownList = SystemsShutdownList.substr(0, SystemsShutdownList.size() - 2); SystemsShutdownList = SystemsShutdownList.substr(0, SystemsShutdownList.size() - 2);
auto ElapsedTime = mLuaEngine->Server().UptimeTimer.GetElapsedTime(); auto ElapsedTime = mLuaEngine->Server().UptimeTimer.GetElapsedTime();
auto ConnectionLimiterStats = mLuaEngine->Network().GetConnectionLimiterStats();
Status << "BeamMP-Server Status:\n" Status << "BeamMP-Server Status:\n"
<< "\tTotal Players: " << mLuaEngine->Server().ClientCount() << "\n" << "\tTotal Players: " << mLuaEngine->Server().ClientCount() << "\n"
@@ -664,11 +666,6 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
<< "\t\tStates: " << mLuaEngine->GetLuaStateCount() << "\n" << "\t\tStates: " << mLuaEngine->GetLuaStateCount() << "\n"
<< "\t\tEvent timers: " << mLuaEngine->GetTimedEventsCount() << "\n" << "\t\tEvent timers: " << mLuaEngine->GetTimedEventsCount() << "\n"
<< "\t\tEvent handlers: " << mLuaEngine->GetRegisteredEventHandlerCount() << "\n" << "\t\tEvent handlers: " << mLuaEngine->GetRegisteredEventHandlerCount() << "\n"
<< "\tConnection limiter:\n"
<< "\t\tActive/Max global: " << ConnectionLimiterStats.CurrentGlobal << "/" << ConnectionLimiterStats.MaxGlobal << "\n"
<< "\t\tActive IP buckets: " << ConnectionLimiterStats.ActiveIpBuckets << "\n"
<< "\t\tHighest single IP load: " << ConnectionLimiterStats.CurrentMaxPerIp << "/" << ConnectionLimiterStats.MaxPerIp << "\n"
<< "\t\tSaturated IP buckets: " << ConnectionLimiterStats.SaturatedIpBuckets << "\n"
<< "\tSubsystems:\n" << "\tSubsystems:\n"
<< "\t\tGood/Starting/Bad: " << SystemsGood << "/" << SystemsStarting << "/" << SystemsBad << "\n" << "\t\tGood/Starting/Bad: " << SystemsGood << "/" << SystemsStarting << "/" << SystemsBad << "\n"
<< "\t\tShutting down/Shut down: " << SystemsShuttingDown << "/" << SystemsShutdown << "\n" << "\t\tShutting down/Shut down: " << SystemsShuttingDown << "/" << SystemsShutdown << "\n"
@@ -685,13 +682,9 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) { void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
auto FutureIsNonNil = auto FutureIsNonNil =
[](const std::shared_ptr<TLuaResult>& Future) { [](const std::shared_ptr<TLuaResult>& Future) {
if (!Future->IsError()) { if (!Future->Error && Future->Result.valid()) {
auto Snapshot = Future->GetDetachedSnapshot(); auto Type = Future->Result.get_type();
if (Snapshot.Result.V.valueless_by_exception() || std::get_if<std::monostate>(&Snapshot.Result.V) != nullptr) { return Type != sol::type::lua_nil && Type != sol::type::none;
// no value contained
return false;
}
return true;
} }
return false; return false;
}; };
@@ -701,7 +694,7 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5)); TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5));
size_t Count = 0; size_t Count = 0;
for (auto& Future : Futures) { for (auto& Future : Futures) {
if (!Future->IsError()) { if (!Future->Error) {
++Count; ++Count;
} }
} }
@@ -719,16 +712,14 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
std::stringstream Reply; std::stringstream Reply;
if (NonNilFutures.size() > 1) { if (NonNilFutures.size() > 1) {
for (size_t i = 0; i < NonNilFutures.size(); ++i) { for (size_t i = 0; i < NonNilFutures.size(); ++i) {
auto Snapshot = NonNilFutures[i]->GetDetachedSnapshot(); Reply << NonNilFutures[i]->StateId << ": \n"
Reply << Snapshot.StateId << ": \n" << LuaAPI::LuaToString(NonNilFutures[i]->Result);
<< Snapshot.Result;
if (i < NonNilFutures.size() - 1) { if (i < NonNilFutures.size() - 1) {
Reply << "\n"; Reply << "\n";
} }
} }
} else { } else {
auto Snapshot = NonNilFutures[0]->GetDetachedSnapshot(); Reply << LuaAPI::LuaToString(NonNilFutures[0]->Result);
Reply << Snapshot.Result;
} }
Application::Console().WriteRaw(Reply.str()); Application::Console().WriteRaw(Reply.str());
} }
@@ -809,9 +800,8 @@ void TConsole::InitializeCommandline() {
} else { } else {
auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared<std::string>(TrimmedCmd), "", "" }); auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared<std::string>(TrimmedCmd), "", "" });
Future->WaitUntilReady(); Future->WaitUntilReady();
if (Future->IsError()) { if (Future->Error) {
auto Snapshot = Future->GetSnapshot(); beammp_lua_error("error in " + mStateId + ": " + Future->ErrorMessage);
beammp_lua_error("error in " + mStateId + ": " + Snapshot.ErrorMessage);
} }
} }
} else { } else {
@@ -843,7 +833,7 @@ void TConsole::InitializeCommandline() {
if (!mLuaEngine) { if (!mLuaEngine) {
beammp_info("Lua not started yet, please try again in a second"); beammp_info("Lua not started yet, please try again in a second");
} else { } else {
std::string prefix { }; // stores non-table part of input std::string prefix {}; // stores non-table part of input
for (size_t i = stub.length(); i > 0; i--) { // separate table from input for (size_t i = stub.length(); i > 0; i--) { // separate table from input
if (!std::isalnum(stub[i - 1]) && stub[i - 1] != '_' && stub[i - 1] != '.') { if (!std::isalnum(stub[i - 1]) && stub[i - 1] != '_' && stub[i - 1] != '.') {
prefix = stub.substr(0, i); prefix = stub.substr(0, i);
+2 -2
View File
@@ -181,8 +181,8 @@ std::string THeartbeatThread::GetPlayers() {
std::string Return; std::string Return;
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool { mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
ReadLock Lock(mServer.GetClientMutex()); ReadLock Lock(mServer.GetClientMutex());
if (auto Client = ClientPtr.lock()) { if (!ClientPtr.expired()) {
Return += Client->GetName() + ";"; Return += ClientPtr.lock()->GetName() + ";";
} }
return true; return true;
}); });
-37
View File
@@ -1,37 +0,0 @@
// 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 "TIoPollThread.h"
TIoPollThread::TIoPollThread()
: mWorkGuard(boost::asio::make_work_guard(mIoCtx))
, mThread([this](std::stop_token StopToken) {
while (!StopToken.stop_requested()) {
try {
mIoCtx.run();
break;
} catch (...) {
mIoCtx.restart();
}
}
}) { }
TIoPollThread::~TIoPollThread() {
mWorkGuard.reset();
mIoCtx.stop();
}
+187 -370
View File
@@ -20,26 +20,19 @@
#include "Client.h" #include "Client.h"
#include "Common.h" #include "Common.h"
#include "CustomAssert.h" #include "CustomAssert.h"
#include "Env.h"
#include "Http.h" #include "Http.h"
#include "LuaAPI.h" #include "LuaAPI.h"
#include "Profiling.h" #include "Profiling.h"
#include "TLuaPlugin.h" #include "TLuaPlugin.h"
#include "TLuaResult.h"
#include "sol/object.hpp" #include "sol/object.hpp"
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
#include <fmt/core.h> #include <fmt/core.h>
#include <mutex>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <random> #include <random>
#include <sol/types.hpp>
#include <thread> #include <thread>
#include <tuple> #include <tuple>
#include <type_traits>
#include <unordered_map>
#include <variant>
TLuaEngine* LuaAPI::MP::Engine; TLuaEngine* LuaAPI::MP::Engine;
@@ -63,10 +56,16 @@ TLuaEngine::TLuaEngine()
IThreaded::Start(); IThreaded::Start();
} }
TEST_CASE("TLuaEngine ctor & dtor") {
Application::Settings.set(Settings::Key::General_ResourceFolder, "beammp_server_test_resources");
TLuaEngine engine;
Application::GracefullyShutdown();
}
void TLuaEngine::operator()() { void TLuaEngine::operator()() {
RegisterThread("LuaEngine"); RegisterThread("LuaEngine");
// lua engine main thread // lua engine main thread
beammp_infof("Lua v{}.{}.{}", LUA_VERSION_MAJOR, LUA_VERSION_MINOR, LUA_VERSION_RELEASE); beammp_infof(LUA_RELEASE);
CollectAndInitPlugins(); CollectAndInitPlugins();
Application::SetSubsystemStatus("LuaEngine", Application::Status::Good); Application::SetSubsystemStatus("LuaEngine", Application::Status::Good);
@@ -74,9 +73,8 @@ void TLuaEngine::operator()() {
auto Futures = TriggerEvent("onInit", ""); auto Futures = TriggerEvent("onInit", "");
WaitForAll(Futures, std::chrono::seconds(5)); WaitForAll(Futures, std::chrono::seconds(5));
for (const auto& Future : Futures) { for (const auto& Future : Futures) {
auto Snapshot = Future->GetDetachedSnapshot(); if (Future->Error && Future->ErrorMessage != BeamMPFnNotFoundError) {
if (Snapshot.Error && Snapshot.ErrorMessage != BeamMPFnNotFoundError) { beammp_lua_error("Calling \"onInit\" on \"" + Future->StateId + "\" failed: " + Future->ErrorMessage);
beammp_lua_error("Calling \"onInit\" on \"" + Snapshot.StateId + "\" failed: " + Snapshot.ErrorMessage);
} }
} }
@@ -86,11 +84,10 @@ void TLuaEngine::operator()() {
std::unique_lock Lock(mResultsToCheckMutex); std::unique_lock Lock(mResultsToCheckMutex);
if (!mResultsToCheck.empty()) { if (!mResultsToCheck.empty()) {
mResultsToCheck.remove_if([](const std::shared_ptr<TLuaResult>& Ptr) -> bool { mResultsToCheck.remove_if([](const std::shared_ptr<TLuaResult>& Ptr) -> bool {
if (Ptr->IsReady()) { if (Ptr->Ready) {
auto Snapshot = Ptr->GetDetachedSnapshot(); if (Ptr->Error) {
if (Snapshot.Error) { if (Ptr->ErrorMessage != BeamMPFnNotFoundError) {
if (Snapshot.ErrorMessage != BeamMPFnNotFoundError) { beammp_lua_error(Ptr->Function + ": " + Ptr->ErrorMessage);
beammp_lua_error(Snapshot.Function + ": " + Snapshot.ErrorMessage);
} }
} }
return true; return true;
@@ -115,7 +112,7 @@ void TLuaEngine::operator()() {
std::unique_lock StateLock(mLuaStatesMutex); std::unique_lock StateLock(mLuaStatesMutex);
std::unique_lock Lock2(mResultsToCheckMutex); std::unique_lock Lock2(mResultsToCheckMutex);
for (auto& Handler : Handlers) { for (auto& Handler : Handlers) {
auto Res = mLuaStates[Timer.StateId]->EnqueueFunctionCallFromCustomEvent(Handler, { }, Timer.EventName, Timer.Strategy); auto Res = mLuaStates[Timer.StateId]->EnqueueFunctionCallFromCustomEvent(Handler, {}, Timer.EventName, Timer.Strategy);
if (Res) { if (Res) {
mResultsToCheck.push_back(Res); mResultsToCheck.push_back(Res);
mResultsToCheckCond.notify_one(); mResultsToCheckCond.notify_one();
@@ -130,13 +127,7 @@ void TLuaEngine::operator()() {
} }
} }
} }
bool StatesEmpty = false; if (mLuaStates.empty()) {
{
std::unique_lock Lock(mLuaStatesMutex);
StatesEmpty = mLuaStates.empty();
}
if (StatesEmpty) {
beammp_trace("No Lua states, event loop running extremely sparsely"); beammp_trace("No Lua states, event loop running extremely sparsely");
Application::SleepSafeSeconds(10); Application::SleepSafeSeconds(10);
} else { } else {
@@ -209,8 +200,8 @@ std::unordered_map<std::string /* event name */, std::vector<std::string> /* han
return Result; return Result;
} }
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> TLuaEngine::Debug_GetStateExecuteQueueForState(TLuaStateId StateId) { std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> TLuaEngine::Debug_GetStateExecuteQueueForState(TLuaStateId StateId) {
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> Result; std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Result;
std::unique_lock Lock(mLuaStatesMutex); std::unique_lock Lock(mLuaStatesMutex);
Result = mLuaStates.at(StateId)->Debug_GetStateExecuteQueue(); Result = mLuaStates.at(StateId)->Debug_GetStateExecuteQueue();
return Result; return Result;
@@ -223,16 +214,16 @@ std::vector<TLuaEngine::QueuedFunction> TLuaEngine::Debug_GetStateFunctionQueueF
return Result; return Result;
} }
std::vector<TLuaResult::DetachedSnapshot> TLuaEngine::Debug_GetResultsToCheckForState(TLuaStateId StateId) { std::vector<TLuaResult> TLuaEngine::Debug_GetResultsToCheckForState(TLuaStateId StateId) {
std::unique_lock Lock(mResultsToCheckMutex); std::unique_lock Lock(mResultsToCheckMutex);
auto ResultsToCheckCopy = mResultsToCheck; auto ResultsToCheckCopy = mResultsToCheck;
Lock.unlock(); Lock.unlock();
std::vector<TLuaResult::DetachedSnapshot> Result; std::vector<TLuaResult> Result;
while (!ResultsToCheckCopy.empty()) { while (!ResultsToCheckCopy.empty()) {
auto ResultToCheck = std::move(ResultsToCheckCopy.front()); auto ResultToCheck = std::move(ResultsToCheckCopy.front());
ResultsToCheckCopy.pop_front(); ResultsToCheckCopy.pop_front();
if (ResultToCheck->OwnerState() == StateId) { if (ResultToCheck->StateId == StateId) {
Result.push_back(ResultToCheck->GetDetachedSnapshot()); Result.push_back(*ResultToCheck);
} }
} }
return Result; return Result;
@@ -263,7 +254,7 @@ std::vector<std::string> TLuaEngine::StateThreadData::GetStateTableKeys(const st
auto globals = mStateView.globals(); auto globals = mStateView.globals();
sol::table current = globals; sol::table current = globals;
std::vector<std::string> Result { }; std::vector<std::string> Result {};
for (const auto& [key, value] : current) { for (const auto& [key, value] : current) {
std::string s = key.as<std::string>(); std::string s = key.as<std::string>();
@@ -319,30 +310,27 @@ void TLuaEngine::WaitForAll(std::vector<std::shared_ptr<TLuaResult>>& Results, c
size_t ms = 0; size_t ms = 0;
std::set<std::string> WarnedResults; std::set<std::string> WarnedResults;
while (!Result->IsReady() && !Cancelled) { while (!Result->Ready && !Cancelled) {
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
ms += 10; ms += 10;
if (Max.has_value() && std::chrono::milliseconds(ms) > Max.value()) { if (Max.has_value() && std::chrono::milliseconds(ms) > Max.value()) {
auto Snapshot = Result->GetDetachedSnapshot(); beammp_trace("'" + Result->Function + "' in '" + Result->StateId + "' did not finish executing in time (took: " + std::to_string(ms) + "ms).");
beammp_trace("'" + Snapshot.Function + "' in '" + Snapshot.StateId + "' did not finish executing in time (took: " + std::to_string(ms) + "ms).");
Cancelled = true; Cancelled = true;
} else if (ms > 1000 * 60) { } else if (ms > 1000 * 60) {
auto Snapshot = Result->GetDetachedSnapshot(); auto ResultId = Result->StateId + "_" + Result->Function;
auto ResultId = Snapshot.StateId + "_" + Snapshot.Function;
if (WarnedResults.count(ResultId) == 0) { if (WarnedResults.count(ResultId) == 0) {
WarnedResults.insert(ResultId); WarnedResults.insert(ResultId);
beammp_lua_warn("'" + Snapshot.Function + "' in '" + Snapshot.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."); 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 Snapshot = Result->GetDetachedSnapshot();
if (Cancelled) { if (Cancelled) {
beammp_lua_warn("'" + Snapshot.Function + "' in '" + Snapshot.StateId + "' failed to execute in time and was not waited for. It may still finish executing at a later time."); beammp_lua_warn("'" + Result->Function + "' in '" + Result->StateId + "' failed to execute in time and was not waited for. It may still finish executing at a later time.");
LuaAPI::MP::Engine->ReportErrors({ Result }); LuaAPI::MP::Engine->ReportErrors({ Result });
} else if (Snapshot.Error) { } else if (Result->Error) {
if (Snapshot.ErrorMessage != BeamMPFnNotFoundError) { if (Result->ErrorMessage != BeamMPFnNotFoundError) {
beammp_lua_error(Snapshot.Function + ": " + Snapshot.ErrorMessage); beammp_lua_error(Result->Function + ": " + Result->ErrorMessage);
} }
} }
} }
@@ -362,7 +350,7 @@ bool TLuaEngine::HasState(TLuaStateId StateId) {
return mLuaStates.find(StateId) != mLuaStates.end(); return mLuaStates.find(StateId) != mLuaStates.end();
} }
std::shared_ptr<TLuaVoidResult> TLuaEngine::EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script) { std::shared_ptr<TLuaResult> TLuaEngine::EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script) {
std::unique_lock Lock(mLuaStatesMutex); std::unique_lock Lock(mLuaStatesMutex);
return mLuaStates.at(StateID)->EnqueueScript(Script); return mLuaStates.at(StateID)->EnqueueScript(Script);
} }
@@ -442,11 +430,10 @@ void TLuaEngine::EnsureStateExists(TLuaStateId StateId, const std::string& Name,
mLuaStates[StateId] = std::move(DataPtr); mLuaStates[StateId] = std::move(DataPtr);
RegisterEvent("onInit", StateId, "onInit"); RegisterEvent("onInit", StateId, "onInit");
if (!DontCallOnInit) { if (!DontCallOnInit) {
auto Res = EnqueueFunctionCall(StateId, "onInit", { }, "onInit"); auto Res = EnqueueFunctionCall(StateId, "onInit", {}, "onInit");
Res->WaitUntilReady(); Res->WaitUntilReady();
auto Snapshot = Res->GetDetachedSnapshot(); if (Res->Error && Res->ErrorMessage != TLuaEngine::BeamMPFnNotFoundError) {
if (Snapshot.Error && Snapshot.ErrorMessage != TLuaEngine::BeamMPFnNotFoundError) { beammp_lua_error("Calling \"onInit\" on \"" + StateId + "\" failed: " + Res->ErrorMessage);
beammp_lua_error("Calling \"onInit\" on \"" + StateId + "\" failed: " + Snapshot.ErrorMessage);
} }
} }
} }
@@ -458,7 +445,6 @@ void TLuaEngine::RegisterEvent(const std::string& EventName, TLuaStateId StateId
} }
std::set<std::string> TLuaEngine::GetEventHandlersForState(const std::string& EventName, TLuaStateId StateId) { std::set<std::string> TLuaEngine::GetEventHandlersForState(const std::string& EventName, TLuaStateId StateId) {
std::unique_lock Lock(mLuaEventsMutex);
return mLuaEvents[EventName][StateId]; return mLuaEvents[EventName][StateId];
} }
@@ -466,7 +452,7 @@ std::vector<sol::object> TLuaEngine::StateThreadData::JsonStringToArray(JsonStri
auto LocalTable = Lua_JsonDecode(Str.value).as<std::vector<sol::object>>(); auto LocalTable = Lua_JsonDecode(Str.value).as<std::vector<sol::object>>();
for (auto& value : LocalTable) { for (auto& value : LocalTable) {
if (value.is<std::string>() && value.as<std::string>() == BEAMMP_INTERNAL_NIL) { if (value.is<std::string>() && value.as<std::string>() == BEAMMP_INTERNAL_NIL) {
value = sol::object { }; value = sol::object {};
} }
} }
return LocalTable; return LocalTable;
@@ -508,16 +494,15 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string
auto Fn = mStateView[Handler]; auto Fn = mStateView[Handler];
if (Fn.valid()) { if (Fn.valid()) {
auto LuaResult = Fn(LocalArgs); auto LuaResult = Fn(LocalArgs);
auto Result = std::make_shared<TLuaResult>(mStateId, Handler); auto Result = std::make_shared<TLuaResult>();
if (LuaResult.valid()) { if (LuaResult.valid()) {
try { Result->Error = false;
Result->MarkReadySuccess(LuaResult); Result->Result = LuaResult;
} catch (const std::exception& e) {
Result->MarkReadyError(fmt::format("Call was successful, but result could not be serialized"));
}
} else { } else {
Result->MarkReadyError("Function result in TriggerGlobalEvent was invalid"); Result->Error = true;
Result->ErrorMessage = "Function result in TriggerGlobalEvent was invalid";
} }
Result->MarkAsReady();
Return.push_back(Result); Return.push_back(Result);
} }
} }
@@ -525,60 +510,26 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string
sol::table AsyncEventReturn = StateView.create_table(); sol::table AsyncEventReturn = StateView.create_table();
AsyncEventReturn["ReturnValueImpl"] = Return; AsyncEventReturn["ReturnValueImpl"] = Return;
AsyncEventReturn.set_function("IsDone", AsyncEventReturn.set_function("IsDone",
[](const sol::table& Self) -> bool { [&](const sol::table& Self) -> bool {
auto Vector = Self.get<std::vector<std::shared_ptr<TLuaResult>>>("ReturnValueImpl"); auto Vector = Self.get<std::vector<std::shared_ptr<TLuaResult>>>("ReturnValueImpl");
for (const auto& Value : Vector) { for (const auto& Value : Vector) {
if (!Value->IsReady()) { if (!Value->Ready) {
return false; return false;
} }
} }
return true; return true;
}); });
AsyncEventReturn.set_function("GetResults", AsyncEventReturn.set_function("GetResults",
[](const sol::table& Self, sol::this_state State) -> sol::table { [&](const sol::table& Self) -> sol::table {
sol::state_view StateView(State); sol::state_view StateView(mState);
sol::table Result = StateView.create_table(); sol::table Result = StateView.create_table();
auto Vector = Self.get<std::vector<std::shared_ptr<TLuaResult>>>("ReturnValueImpl"); auto Vector = Self.get<std::vector<std::shared_ptr<TLuaResult>>>("ReturnValueImpl");
auto DetachedToLuaObject = [&StateView](const auto& SelfConvert, const TDetachedLuaValue& value) -> sol::object {
return std::visit([&StateView, &SelfConvert](auto&& arg) -> sol::object {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, TDetachedLuaValue::Array>) {
sol::table Table = StateView.create_table(static_cast<int>(arg.size()), 0);
size_t i = 1;
for (const auto& Elem : arg) {
Table.set(i, SelfConvert(SelfConvert, Elem));
++i;
}
return sol::make_object(StateView, Table);
} else if constexpr (std::is_same_v<T, TDetachedLuaValue::Object>) {
sol::table Table = StateView.create_table();
for (const auto& [Key, Elem] : arg) {
Table.set(Key, SelfConvert(SelfConvert, *Elem));
}
return sol::make_object(StateView, Table);
}
else if constexpr (std::is_same_v<T, bool>)
return sol::make_object(StateView, arg);
else if constexpr (std::is_same_v<T, double>)
return sol::make_object(StateView, arg);
else if constexpr (std::is_same_v<T, int>)
return sol::make_object(StateView, arg);
else if constexpr (std::is_same_v<T, std::string>)
return sol::make_object(StateView, arg);
else if constexpr (std::is_same_v<T, std::monostate>)
return sol::make_object(StateView, sol::lua_nil_t());
else
static_assert(AlwaysFalseV<T>, "non-exhaustive visitor!");
}, value.V);
};
int i = 1; int i = 1;
for (const auto& Value : Vector) { for (const auto& Value : Vector) {
if (!Value->IsReady()) { if (!Value->Ready) {
return sol::lua_nil; return sol::lua_nil;
} }
auto Snapshot = Value->GetDetachedSnapshot(); Result.set(i, Value->Result);
Result.set(i, DetachedToLuaObject(DetachedToLuaObject, Snapshot.Result));
++i; ++i;
} }
return Result; return Result;
@@ -598,13 +549,8 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerLocalEvent(const std::string&
Result.set(i, FnRet); Result.set(i, FnRet);
++i; ++i;
} else { } else {
std::string ErrStr; sol::error Err = FnRet;
if (FnRet.get_type() == sol::type::string) { beammp_lua_error(std::string("TriggerLocalEvent: ") + Err.what());
ErrStr = FnRet.get<sol::error>().what();
} else {
ErrStr = "(unknown error; error object is not inspectable)";
}
beammp_lua_errorf("TriggerLocalEvent: {}", ErrStr);
} }
} }
} }
@@ -613,37 +559,37 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerLocalEvent(const std::string&
sol::table TLuaEngine::StateThreadData::Lua_GetPlayerIdentifiers(int ID) { sol::table TLuaEngine::StateThreadData::Lua_GetPlayerIdentifiers(int ID) {
auto MaybeClient = GetClient(mEngine->Server(), ID); auto MaybeClient = GetClient(mEngine->Server(), ID);
if (MaybeClient) { if (MaybeClient && !MaybeClient.value().expired()) {
if (std::shared_ptr<TClient> Locked = MaybeClient.value().lock()) { auto IDs = MaybeClient.value().lock()->GetIdentifiers();
auto IDs = Locked->GetIdentifiers(); if (IDs.empty()) {
if (IDs.empty()) { return sol::lua_nil;
return sol::lua_nil;
}
sol::table Result = mStateView.create_table();
for (const auto& Pair : IDs) {
Result.set(Pair.first, Pair.second);
}
return Result;
} }
sol::table Result = mStateView.create_table();
for (const auto& Pair : IDs) {
Result[Pair.first] = Pair.second;
}
return Result;
} else {
return sol::lua_nil;
} }
return sol::lua_nil;
} }
std::variant<std::string, sol::nil_t> TLuaEngine::StateThreadData::Lua_GetPlayerRole(int ID) { std::variant<std::string, sol::nil_t> TLuaEngine::StateThreadData::Lua_GetPlayerRole(int ID) {
auto MaybeClient = GetClient(mEngine->Server(), ID); auto MaybeClient = GetClient(mEngine->Server(), ID);
if (MaybeClient) { if (MaybeClient) {
if (auto Locked = MaybeClient.value().lock()) { return MaybeClient.value().lock()->GetRoles();
return Locked->GetRoles(); } else {
} return sol::nil;
} }
return sol::nil;
} }
sol::table TLuaEngine::StateThreadData::Lua_GetPlayers() { sol::table TLuaEngine::StateThreadData::Lua_GetPlayers() {
sol::table Result = mStateView.create_table(); sol::table Result = mStateView.create_table();
mEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool { mEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (auto Locked = Client.lock()) { if (!Client.expired()) {
Result[Locked->GetID()] = Locked->GetName(); auto locked = Client.lock();
Result[locked->GetID()] = locked->GetName();
} }
return true; return true;
}); });
@@ -653,9 +599,10 @@ sol::table TLuaEngine::StateThreadData::Lua_GetPlayers() {
int TLuaEngine::StateThreadData::Lua_GetPlayerIDByName(const std::string& Name) { int TLuaEngine::StateThreadData::Lua_GetPlayerIDByName(const std::string& Name) {
int Id = -1; int Id = -1;
mEngine->mServer->ForEachClient([&Id, &Name](std::weak_ptr<TClient> Client) -> bool { mEngine->mServer->ForEachClient([&Id, &Name](std::weak_ptr<TClient> Client) -> bool {
if (auto Locked = Client.lock()) { if (!Client.expired()) {
if (Locked->GetName() == Name) { auto locked = Client.lock();
Id = Locked->GetID(); if (locked->GetName() == Name) {
Id = locked->GetID();
return false; return false;
} }
} }
@@ -692,59 +639,60 @@ sol::table TLuaEngine::StateThreadData::Lua_FS_ListDirectories(const std::string
std::string TLuaEngine::StateThreadData::Lua_GetPlayerName(int ID) { std::string TLuaEngine::StateThreadData::Lua_GetPlayerName(int ID) {
auto MaybeClient = GetClient(mEngine->Server(), ID); auto MaybeClient = GetClient(mEngine->Server(), ID);
if (MaybeClient) { if (MaybeClient && !MaybeClient.value().expired()) {
if (auto Locked = MaybeClient.value().lock()) { return MaybeClient.value().lock()->GetName();
return Locked->GetName(); } else {
} return "";
} }
return "";
} }
sol::table TLuaEngine::StateThreadData::Lua_GetPlayerVehicles(int ID) { sol::table TLuaEngine::StateThreadData::Lua_GetPlayerVehicles(int ID) {
auto MaybeClient = GetClient(mEngine->Server(), ID); auto MaybeClient = GetClient(mEngine->Server(), ID);
if (MaybeClient) { if (MaybeClient && !MaybeClient.value().expired()) {
if (auto Client = MaybeClient.value().lock()) { auto Client = MaybeClient.value().lock();
TClient::TSetOfVehicleData VehicleData; TClient::TSetOfVehicleData VehicleData;
{ // Vehicle Data Lock Scope { // Vehicle Data Lock Scope
auto LockedData = Client->GetAllCars(); auto LockedData = Client->GetAllCars();
VehicleData = *LockedData.VehicleData; VehicleData = *LockedData.VehicleData;
} // End Vehicle Data Lock Scope } // End Vehicle Data Lock Scope
if (VehicleData.empty()) { if (VehicleData.empty()) {
return sol::lua_nil; return sol::lua_nil;
}
sol::state_view StateView(mState);
sol::table Result = StateView.create_table();
for (const auto& v : VehicleData) {
Result[v.ID()] = v.DataAsPacket(Client->GetRoles(), Client->GetName(), Client->GetID()).substr(3);
}
return Result;
} }
} sol::state_view StateView(mState);
return sol::lua_nil; sol::table Result = StateView.create_table();
for (const auto& v : VehicleData) {
Result[v.ID()] = v.DataAsPacket(Client->GetRoles(), Client->GetName(), Client->GetID()).substr(3);
}
return Result;
} else
return sol::lua_nil;
} }
std::pair<sol::table, std::string> TLuaEngine::StateThreadData::Lua_GetPositionRaw(int PID, int VID) { std::pair<sol::table, std::string> TLuaEngine::StateThreadData::Lua_GetPositionRaw(int PID, int VID) {
std::pair<sol::table, std::string> Result; std::pair<sol::table, std::string> Result;
auto MaybeClient = GetClient(mEngine->Server(), PID); auto MaybeClient = GetClient(mEngine->Server(), PID);
if (MaybeClient) { if (MaybeClient && !MaybeClient.value().expired()) {
if (auto Client = MaybeClient.value().lock()) { auto Client = MaybeClient.value().lock();
std::string VehiclePos = Client->GetCarPositionRaw(VID); std::string VehiclePos = Client->GetCarPositionRaw(VID);
if (VehiclePos.empty()) { if (VehiclePos.empty()) {
Result.second = "Vehicle not found"; // return std::make_tuple(sol::lua_nil, sol::make_object(StateView, "Vehicle not found"));
return Result; Result.second = "Vehicle not found";
}
sol::table t = Lua_JsonDecode(VehiclePos);
if (t == sol::lua_nil) {
Result.second = "Packet decode failed";
}
Result.first = t;
return Result; return Result;
} }
sol::table t = Lua_JsonDecode(VehiclePos);
if (t == sol::lua_nil) {
Result.second = "Packet decode failed";
}
// return std::make_tuple(Result, sol::make_object(StateView, sol::lua_nil));
Result.first = t;
return Result;
} else {
// return std::make_tuple(sol::lua_nil, sol::make_object(StateView, "Client expired"));
Result.second = "No such player";
return Result;
} }
Result.second = "No such player";
return Result;
} }
sol::table TLuaEngine::StateThreadData::Lua_HttpCreateConnection(const std::string& host, uint16_t port) { sol::table TLuaEngine::StateThreadData::Lua_HttpCreateConnection(const std::string& host, uint16_t port) {
@@ -778,38 +726,10 @@ static void AddToTable(sol::table& table, const std::string& left, const T& valu
} }
} }
static bool mDisableMPSet = [] {
auto DisableMPSet = Env::Get(Env::Key::PROVIDER_DISABLE_MP_SET).value_or("false");
return DisableMPSet == "true" || DisableMPSet == "1";
}();
static auto GetSettingName = [](int id) -> const char* {
switch (id) {
case 0:
return "Debug";
case 1:
return "Private";
case 2:
return "MaxCars";
case 3:
return "MaxPlayers";
case 4:
return "Map";
case 5:
return "Name";
case 6:
return "Description";
case 7:
return "InformationPacket";
default:
return "Unknown";
}
};
static void JsonDecodeRecursive(sol::state_view& StateView, sol::table& table, const std::string& left, const nlohmann::json& right) { static void JsonDecodeRecursive(sol::state_view& StateView, sol::table& table, const std::string& left, const nlohmann::json& right) {
switch (right.type()) { switch (right.type()) {
case nlohmann::detail::value_t::null: case nlohmann::detail::value_t::null:
AddToTable(table, left, sol::lua_nil_t { }); AddToTable(table, left, sol::lua_nil_t {});
return; return;
case nlohmann::detail::value_t::object: { case nlohmann::detail::value_t::object: {
auto value = table.create(); auto value = table.create();
@@ -937,15 +857,7 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
MPTable.set_function("GetPositionRaw", [&](int PID, int VID) -> std::pair<sol::table, std::string> { MPTable.set_function("GetPositionRaw", [&](int PID, int VID) -> std::pair<sol::table, std::string> {
return Lua_GetPositionRaw(PID, VID); return Lua_GetPositionRaw(PID, VID);
}); });
MPTable.set_function("SendChatMessage", [&](sol::variadic_args Args) { MPTable.set_function("SendChatMessage", &LuaAPI::MP::SendChatMessage);
if (Args.size() == 2) {
LuaAPI::MP::SendChatMessage(Args.get<int>(0), Args.get<std::string>(1));
} else if (Args.size() == 3) {
LuaAPI::MP::SendChatMessage(Args.get<int>(0), Args.get<std::string>(1), Args.get<bool>(2));
} else {
beammp_lua_error("SendChatMessage expects 2 or 3 arguments.");
}
});
MPTable.set_function("SendNotification", [&](sol::variadic_args Args) { MPTable.set_function("SendNotification", [&](sol::variadic_args Args) {
if (Args.size() == 2) { if (Args.size() == 2) {
LuaAPI::MP::SendNotification(Args.get<int>(0), Args.get<std::string>(1), "", Args.get<std::string>(1)); LuaAPI::MP::SendNotification(Args.get<int>(0), Args.get<std::string>(1), "", Args.get<std::string>(1));
@@ -957,9 +869,12 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
beammp_lua_error("SendNotification expects 2, 3 or 4 arguments."); beammp_lua_error("SendNotification expects 2, 3 or 4 arguments.");
} }
}); });
MPTable.set_function("ConfirmationDialog", sol::overload(&LuaAPI::MP::ConfirmationDialog, [&](const int& ID, const std::string& Title, const std::string& Body, const sol::table& Buttons, const std::string& InteractionID) { MPTable.set_function("ConfirmationDialog", sol::overload(
LuaAPI::MP::ConfirmationDialog(ID, Title, Body, Buttons, InteractionID); &LuaAPI::MP::ConfirmationDialog,
})); [&](const int& ID, const std::string& Title, const std::string& Body, const sol::table& Buttons, const std::string& InteractionID) {
LuaAPI::MP::ConfirmationDialog(ID, Title, Body, Buttons, InteractionID);
}
));
MPTable.set_function("GetPlayers", [&]() -> sol::table { MPTable.set_function("GetPlayers", [&]() -> sol::table {
return Lua_GetPlayers(); return Lua_GetPlayers();
}); });
@@ -1003,23 +918,9 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
MPTable.set_function("CancelEventTimer", [&](const std::string& EventName) { MPTable.set_function("CancelEventTimer", [&](const std::string& EventName) {
mEngine->CancelEventTimers(EventName, mStateId); mEngine->CancelEventTimers(EventName, mStateId);
}); });
if (mDisableMPSet) { MPTable.set_function("Set", &LuaAPI::MP::Set);
MPTable.set_function("Set", [this](int ConfigID, sol::object NewValue) {
beammp_lua_errorf("A script ({}) tried to call MP.Set to change setting '{}' but this was blocked by your server provider.", mStateId, GetSettingName(ConfigID));
});
} else {
MPTable.set_function("Set", &LuaAPI::MP::Set);
}
MPTable.set_function("Get", &LuaAPI::MP::Get); MPTable.set_function("Get", &LuaAPI::MP::Get);
MPTable.set_function("GetServerTimeMS", [this](const sol::this_state ts) {
return make_object(sol::state_view(ts), this->mEngine->Server().GetServerTimeMS());
});
MPTable.set_function("GetServerTime", [this](const sol::this_state ts) {
return make_object(sol::state_view(ts), this->mEngine->Server().GetServerTime());
});
auto UtilTable = StateView.create_named_table("Util"); auto UtilTable = StateView.create_named_table("Util");
UtilTable.set_function("LogDebug", [this](sol::variadic_args args) { UtilTable.set_function("LogDebug", [this](sol::variadic_args args) {
std::string ToPrint = ""; std::string ToPrint = "";
@@ -1138,16 +1039,15 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
Start(); Start();
} }
std::shared_ptr<TLuaVoidResult> TLuaEngine::StateThreadData::EnqueueScript(const TLuaChunk& Script) { std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueScript(const TLuaChunk& Script) {
std::unique_lock Lock(mStateExecuteQueueMutex); std::unique_lock Lock(mStateExecuteQueueMutex);
auto Result = std::make_shared<TLuaVoidResult>(mStateId); auto Result = std::make_shared<TLuaResult>();
mStateExecuteQueue.push({ Script, Result }); mStateExecuteQueue.push({ Script, Result });
return Result; return Result;
} }
std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName, CallStrategy Strategy) { std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName, CallStrategy Strategy) {
// TODO: Document all this // TODO: Document all this
std::unique_lock Lock(mStateFunctionQueueMutex);
decltype(mStateFunctionQueue)::iterator Iter = mStateFunctionQueue.end(); decltype(mStateFunctionQueue)::iterator Iter = mStateFunctionQueue.end();
if (Strategy == CallStrategy::BestEffort) { if (Strategy == CallStrategy::BestEffort) {
Iter = std::find_if(mStateFunctionQueue.begin(), mStateFunctionQueue.end(), Iter = std::find_if(mStateFunctionQueue.begin(), mStateFunctionQueue.end(),
@@ -1156,7 +1056,10 @@ std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCallFrom
}); });
} }
if (Iter == mStateFunctionQueue.end()) { if (Iter == mStateFunctionQueue.end()) {
auto Result = std::make_shared<TLuaResult>(mStateId, FunctionName); auto Result = std::make_shared<TLuaResult>();
Result->StateId = mStateId;
Result->Function = FunctionName;
std::unique_lock Lock(mStateFunctionQueueMutex);
mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName }); mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName });
mStateFunctionQueueCond.notify_all(); mStateFunctionQueueCond.notify_all();
return Result; return Result;
@@ -1166,7 +1069,9 @@ std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCallFrom
} }
std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCall(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName) { std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCall(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName) {
auto Result = std::make_shared<TLuaResult>(mStateId, FunctionName); auto Result = std::make_shared<TLuaResult>();
Result->StateId = mStateId;
Result->Function = FunctionName;
std::unique_lock Lock(mStateFunctionQueueMutex); std::unique_lock Lock(mStateFunctionQueueMutex);
mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName }); mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName });
mStateFunctionQueueCond.notify_all(); mStateFunctionQueueCond.notify_all();
@@ -1215,14 +1120,14 @@ void TLuaEngine::StateThreadData::operator()() {
sol::state_view StateView(mState); sol::state_view StateView(mState);
auto Res = StateView.safe_script(*S.first.Content, sol::script_pass_on_error, S.first.FileName); auto Res = StateView.safe_script(*S.first.Content, sol::script_pass_on_error, S.first.FileName);
if (Res.valid()) { if (Res.valid()) {
// Script-load completion should not serialize the script's return value. S.second->Error = false;
// A loaded chunk may legally return non-serializable Lua values such as S.second->Result = std::move(Res);
// functions or function tables. For this reason, we don't pass anything
// to the result here.
S.second->MarkReadySuccess();
} else { } else {
S.second->MarkReadyError(std::move(Res)); S.second->Error = true;
sol::error Err = Res;
S.second->ErrorMessage = Err.what();
} }
S.second->MarkAsReady();
} }
} }
{ // StateFunctionQueue Scope { // StateFunctionQueue Scope
@@ -1239,7 +1144,7 @@ void TLuaEngine::StateThreadData::operator()() {
auto& Result = TheQueuedFunction.Result; auto& Result = TheQueuedFunction.Result;
auto Args = TheQueuedFunction.Args; auto Args = TheQueuedFunction.Args;
// TODO: Use TheQueuedFunction.EventName for errors, warnings, etc // TODO: Use TheQueuedFunction.EventName for errors, warnings, etc
Result->SetOwnerState(mStateId); Result->StateId = mStateId;
sol::state_view StateView(mState); sol::state_view StateView(mState);
auto Fn = StateView[FnName]; auto Fn = StateView[FnName];
if (Fn.valid() && Fn.get_type() == sol::type::function) { if (Fn.valid() && Fn.get_type() == sol::type::function) {
@@ -1248,45 +1153,49 @@ void TLuaEngine::StateThreadData::operator()() {
if (Arg.valueless_by_exception()) { if (Arg.valueless_by_exception()) {
continue; continue;
} }
std::visit([&LuaArgs, &StateView, this](const auto& arg) { switch (Arg.index()) {
using T = std::decay_t<decltype(arg)>; case TLuaType::String:
if constexpr (std::is_same_v<T, std::string>) { LuaArgs.push_back(sol::make_object(StateView, std::get<std::string>(Arg)));
LuaArgs.push_back(sol::make_object(StateView, arg)); break;
} else if constexpr (std::is_same_v<T, int>) { case TLuaType::Int:
LuaArgs.push_back(sol::make_object(StateView, arg)); LuaArgs.push_back(sol::make_object(StateView, std::get<int>(Arg)));
} else if constexpr (std::is_same_v<T, bool>) { break;
LuaArgs.push_back(sol::make_object(StateView, arg)); case TLuaType::Json: {
} else if constexpr (std::is_same_v<T, JsonString>) { auto LocalArgs = JsonStringToArray(std::get<JsonString>(Arg));
auto LocalArgs = JsonStringToArray(arg); LuaArgs.insert(LuaArgs.end(), LocalArgs.begin(), LocalArgs.end());
LuaArgs.insert(LuaArgs.end(), LocalArgs.begin(), LocalArgs.end()); break;
} else if constexpr (std::is_same_v<T, std::monostate>) { }
beammp_lua_error("Unknown argument type, passed as nil"); case TLuaType::Bool:
LuaArgs.push_back(sol::lua_nil_t()); LuaArgs.push_back(sol::make_object(StateView, std::get<bool>(Arg)));
} else if constexpr (std::is_same_v<T, std::unordered_map<std::string, std::string>>) { break;
auto Table = StateView.create_table(); case TLuaType::StringStringMap: {
for (const auto& [k, v] : arg) { auto Map = std::get<std::unordered_map<std::string, std::string>>(Arg);
Table[k] = v; auto Table = StateView.create_table();
} for (const auto& [k, v] : Map) {
LuaArgs.push_back(Table); Table[k] = v;
} else if constexpr (std::is_same_v<T, float>) {
LuaArgs.push_back(sol::make_object(StateView, arg));
} else {
static_assert(AlwaysFalseV<T>, "unhandled variant");
} }
}, Arg); LuaArgs.push_back(sol::make_object(StateView, Table));
break;
}
default:
beammp_error("Unknown argument type, passed as nil");
break;
}
} }
auto Res = Fn(sol::as_args(LuaArgs)); auto Res = Fn(sol::as_args(LuaArgs));
if (Res.valid()) { if (Res.valid()) {
try { Result->Error = false;
Result->MarkReadySuccess(std::move(Res)); Result->Result = std::move(Res);
} catch (const std::exception& e) {
Result->MarkReadyError(fmt::format("Call was successful, but result could not be serialized"));
}
} else { } else {
Result->MarkReadyError(std::move(Res)); Result->Error = true;
sol::error Err = Res;
Result->ErrorMessage = Err.what();
} }
Result->MarkAsReady();
} else { } else {
Result->MarkReadyError(BeamMPFnNotFoundError); Result->Error = true;
Result->ErrorMessage = BeamMPFnNotFoundError; // special error kind that we can ignore later
Result->MarkAsReady();
} }
auto ProfEnd = prof::now(); auto ProfEnd = prof::now();
auto ProfDuration = prof::duration(ProfStart, ProfEnd); auto ProfDuration = prof::duration(ProfStart, ProfEnd);
@@ -1296,7 +1205,7 @@ void TLuaEngine::StateThreadData::operator()() {
} }
} }
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> TLuaEngine::StateThreadData::Debug_GetStateExecuteQueue() { std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> TLuaEngine::StateThreadData::Debug_GetStateExecuteQueue() {
std::unique_lock Lock(mStateExecuteQueueMutex); std::unique_lock Lock(mStateExecuteQueueMutex);
return mStateExecuteQueue; return mStateExecuteQueue;
} }
@@ -1339,6 +1248,21 @@ void TLuaEngine::StateThreadData::AddPath(const fs::path& Path) {
mPaths.push(Path); mPaths.push(Path);
} }
void TLuaResult::MarkAsReady() {
{
std::lock_guard<std::mutex> readyLock(*this->ReadyMutex);
this->Ready = true;
}
this->ReadyCondition->notify_all();
}
void TLuaResult::WaitUntilReady() {
std::unique_lock readyLock(*this->ReadyMutex);
// wait if not ready yet
if (!this->Ready)
this->ReadyCondition->wait(readyLock);
}
TLuaChunk::TLuaChunk(std::shared_ptr<std::string> Content, std::string FileName, std::string PluginPath) TLuaChunk::TLuaChunk(std::shared_ptr<std::string> Content, std::string FileName, std::string PluginPath)
: Content(Content) : Content(Content)
, FileName(FileName) , FileName(FileName)
@@ -1353,110 +1277,3 @@ bool TLuaEngine::TimedEvent::Expired() {
void TLuaEngine::TimedEvent::Reset() { void TLuaEngine::TimedEvent::Reset() {
LastCompletion = std::chrono::high_resolution_clock::now(); LastCompletion = std::chrono::high_resolution_clock::now();
} }
TEST_CASE("TLuaEngine ctor & dtor") {
Application::Settings.set(Settings::Key::General_ResourceFolder, "beammp_server_test_resources");
TLuaEngine engine;
const TLuaStateId StateId = "lua_event_contract_test";
engine.EnsureStateExists(StateId, "LuaEventContractTest", true);
// LLM generated test code
auto Script = std::make_shared<std::string>(R"(
function onPlayerAuth(playerName, playerRole, isGuest, identifiers)
if type(playerName) ~= "string" then return "on:bad-playerName-type:" .. type(playerName) end
if type(playerRole) ~= "string" then return "on:bad-playerRole-type:" .. type(playerRole) end
if type(isGuest) ~= "boolean" then return "on:bad-isGuest-type:" .. type(isGuest) end
if type(identifiers) ~= "table" then return "on:bad-identifiers-type:" .. type(identifiers) end
return "on:" .. playerName .. ":" .. playerRole .. ":" .. tostring(isGuest) .. ":" .. tostring(identifiers.ip) .. ":" .. tostring(identifiers.beammp)
end
function postPlayerAuth(isDenied, reason, playerName, playerRole, isGuest, identifiers)
if type(isDenied) ~= "boolean" then return "post:bad-isDenied-type:" .. type(isDenied) end
if type(reason) ~= "string" then return "post:bad-reason-type:" .. type(reason) end
if type(playerName) ~= "string" then return "post:bad-playerName-type:" .. type(playerName) end
if type(playerRole) ~= "string" then return "post:bad-playerRole-type:" .. type(playerRole) end
if type(isGuest) ~= "boolean" then return "post:bad-isGuest-type:" .. type(isGuest) end
if type(identifiers) ~= "table" then return "post:bad-identifiers-type:" .. type(identifiers) end
return "post:" .. tostring(isDenied) .. ":" .. reason .. ":" .. playerName .. ":" .. playerRole .. ":" .. tostring(isGuest) .. ":" .. tostring(identifiers.ip) .. ":" .. tostring(identifiers.beammp)
end
function arrayBoundaryHandler()
return { "first", "second", [4] = true }
end
function verifyArrayBoundaryRoundtrip()
local pending = MP.TriggerGlobalEvent("arrayBoundaryEvent")
if not pending:IsDone() then
return "not_done"
end
local results = pending:GetResults()
if type(results) ~= "table" then
return "bad_results_type:" .. type(results)
end
if type(results[1]) ~= "table" then
return "bad_item_type:" .. type(results[1])
end
local arr = results[1]
return tostring(arr[1]) .. "|" .. tostring(arr[2]) .. "|" .. tostring(arr[4]) .. "|" .. tostring(arr[3] == nil)
end
MP.RegisterEvent("onPlayerAuth", "onPlayerAuth")
MP.RegisterEvent("postPlayerAuth", "postPlayerAuth")
MP.RegisterEvent("arrayBoundaryEvent", "arrayBoundaryHandler")
)");
auto LoadResult = engine.EnqueueScript(StateId, TLuaChunk(Script, "event_contract.lua", "beammp_server_test_resources/Server/LuaEventContractTest"));
LoadResult->WaitUntilReady();
auto LoadSnapshot = LoadResult->GetSnapshot();
CHECK(!LoadSnapshot.Error);
const std::unordered_map<std::string, std::string> Identifiers {
{"ip", "410.0.24.1"},
{"beammp", "123456"},
};
auto OnPlayerAuthResults = engine.TriggerEvent(
"onPlayerAuth", "",
std::string("guest8133569"),
std::string("USER"),
true,
Identifiers);
REQUIRE(OnPlayerAuthResults.size() == 1);
TLuaEngine::WaitForAll(OnPlayerAuthResults);
auto OnPlayerAuthSnapshot = OnPlayerAuthResults.front()->GetDetachedSnapshot();
CHECK(!OnPlayerAuthSnapshot.Error);
const auto* OnPlayerAuthValue = std::get_if<std::string>(&OnPlayerAuthSnapshot.Result.V);
REQUIRE(OnPlayerAuthValue != nullptr);
CHECK(*OnPlayerAuthValue == "on:guest8133569:USER:true:410.0.24.1:123456");
auto PostPlayerAuthResults = engine.TriggerEvent(
"postPlayerAuth", "",
false,
std::string(""),
std::string("guest8133569"),
std::string("USER"),
true,
Identifiers);
REQUIRE(PostPlayerAuthResults.size() == 1);
TLuaEngine::WaitForAll(PostPlayerAuthResults);
auto PostPlayerAuthSnapshot = PostPlayerAuthResults.front()->GetDetachedSnapshot();
CHECK(!PostPlayerAuthSnapshot.Error);
const auto* PostPlayerAuthValue = std::get_if<std::string>(&PostPlayerAuthSnapshot.Result.V);
REQUIRE(PostPlayerAuthValue != nullptr);
CHECK(*PostPlayerAuthValue == "post:false::guest8133569:USER:true:410.0.24.1:123456");
auto ArrayRoundtrip = engine.EnqueueFunctionCall(StateId, "verifyArrayBoundaryRoundtrip", {}, "verifyArrayBoundaryRoundtrip");
ArrayRoundtrip->WaitUntilReady();
auto ArrayRoundtripSnapshot = ArrayRoundtrip->GetDetachedSnapshot();
CHECK(!ArrayRoundtripSnapshot.Error);
const auto* ArrayRoundtripValue = std::get_if<std::string>(&ArrayRoundtripSnapshot.Result.V);
REQUIRE(ArrayRoundtripValue != nullptr);
CHECK(*ArrayRoundtripValue == "first|second|true|true");
Application::GracefullyShutdown();
}
+3 -4
View File
@@ -44,7 +44,7 @@ TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const
std::transform(secondStr.begin(), secondStr.end(), secondStr.begin(), ::tolower); std::transform(secondStr.begin(), secondStr.end(), secondStr.begin(), ::tolower);
return firstStr < secondStr; return firstStr < secondStr;
}); });
std::vector<std::pair<fs::path, std::shared_ptr<TLuaVoidResult>>> ResultsToCheck; std::vector<std::pair<fs::path, std::shared_ptr<TLuaResult>>> ResultsToCheck;
for (const auto& Entry : Entries) { for (const auto& Entry : Entries) {
// read in entire file // read in entire file
try { try {
@@ -63,9 +63,8 @@ TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const
} }
for (auto& Result : ResultsToCheck) { for (auto& Result : ResultsToCheck) {
Result.second->WaitUntilReady(); Result.second->WaitUntilReady();
auto Snapshot = Result.second->GetSnapshot(); if (Result.second->Error) {
if (Snapshot.Error) { beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Result.second->ErrorMessage);
beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Snapshot.ErrorMessage);
} }
} }
} }
-425
View File
@@ -1,425 +0,0 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2026 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 "TLuaResult.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <limits>
#include <sol/unsafe_function_result.hpp>
#include <stdexcept>
#include <thread>
std::ostream& operator<<(std::ostream& os, const TDetachedLuaValue& value) {
std::visit([&os](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, TDetachedLuaValue::Array>) {
size_t i = 0;
for (const auto& val : arg) {
if (i > 0) {
os << ", ";
}
os << val;
++i;
}
} else if constexpr (std::is_same_v<T, TDetachedLuaValue::Object>) {
size_t i = 0;
for (const auto& [key, val] : arg) {
if (i > 0) {
os << ", ";
}
os << key << "=" << val;
++i;
}
} else if constexpr (std::is_same_v<T, bool>)
os << (arg ? "true" : "false");
else if constexpr (std::is_same_v<T, double>)
os << arg;
else if constexpr (std::is_same_v<T, int>)
os << arg;
else if constexpr (std::is_same_v<T, std::string>)
os << arg;
else if constexpr (std::is_same_v<T, std::monostate>)
// monostate means no result value
os << "";
else
static_assert(AlwaysFalseV<T>, "non-exhaustive visitor!");
},
value.V);
return os;
}
void TLuaVoidResult::MarkReadySuccess() {
std::unique_lock Lock(mMutex);
mError = false;
mErrorMessage.clear();
MarkAsReady();
}
void TLuaVoidResult::MarkReadyError(sol::protected_function_result Res) {
std::unique_lock Lock(mMutex);
mError = true;
SetErrorMessageFromResult(Res);
MarkAsReady();
}
void TLuaVoidResult::MarkReadyError(std::string Res) {
std::unique_lock Lock(mMutex);
mError = true;
mErrorMessage = std::move(Res);
MarkAsReady();
}
bool TLuaVoidResult::IsReady() const {
std::unique_lock Lock(mMutex);
return mReady;
}
bool TLuaVoidResult::IsError() const {
std::unique_lock Lock(mMutex);
return mError;
}
TLuaVoidResult::Snapshot TLuaVoidResult::GetSnapshot() const {
std::unique_lock Lock(mMutex);
Snapshot snapshot {
.Error = mError,
.Ready = mReady,
.ErrorMessage = mErrorMessage,
.StateId = mStateId,
};
return snapshot;
}
void TLuaVoidResult::MarkAsReady() {
mReady = true;
mReadyCondition.notify_all();
}
void TLuaVoidResult::WaitUntilReady() {
std::unique_lock Lock(mMutex);
while (!mReady)
mReadyCondition.wait_for(Lock, std::chrono::milliseconds(50),
[this] {
return mReady;
});
}
void TLuaResult::MarkReadySuccess(sol::object Res) {
std::unique_lock Lock(mMutex);
mError = false;
mDetachedResult = Freeze(Res);
MarkAsReady();
}
void TLuaResult::MarkReadySuccessNoResult() {
std::unique_lock Lock(mMutex);
mError = false;
mDetachedResult = { { std::monostate { } } };
MarkAsReady();
}
void TLuaResult::MarkReadyError(sol::protected_function_result Res) {
std::unique_lock Lock(mMutex);
mError = true;
SetErrorMessageFromResult(Res);
MarkAsReady();
}
void TLuaResult::MarkReadyError(std::string Res) {
std::unique_lock Lock(mMutex);
mError = true;
mErrorMessage = std::move(Res);
MarkAsReady();
}
bool TLuaResult::IsReady() const {
std::unique_lock Lock(mMutex);
return mReady;
}
bool TLuaResult::IsError() const {
std::unique_lock Lock(mMutex);
return mError;
}
TLuaStateId TLuaResult::OwnerState() const {
std::unique_lock Lock(mMutex);
// copy
return mStateId;
}
void TLuaResult::SetOwnerState(TLuaStateId StateId) {
std::unique_lock Lock(mMutex);
mStateId = std::move(StateId);
}
TLuaResult::DetachedSnapshot TLuaResult::GetDetachedSnapshot() const {
std::unique_lock Lock(mMutex);
DetachedSnapshot snapshot {
.Error = mError,
.Ready = mReady,
.ErrorMessage = mErrorMessage,
.Result = mDetachedResult,
.StateId = mStateId,
.Function = mFunction,
};
return snapshot;
}
TDetachedLuaValue TLuaResult::Freeze(const sol::object& o, int depth) {
if (depth > 64)
throw std::runtime_error("max depth (64) reached");
switch (o.get_type()) {
case sol::type::lua_nil:
return { { std::monostate { } } };
case sol::type::boolean:
return { { o.as<bool>() } };
case sol::type::number: {
if (o.is<int>()) {
return { { o.as<int>() } };
} else {
return { { o.as<double>() } };
}
}
case sol::type::string:
return { { o.as<std::string>() } };
case sol::type::table: {
TDetachedLuaValue::Object ObjectOut;
// numeric stuff is for arrays
std::vector<std::pair<size_t, TDetachedLuaValue>> NumericEntries;
bool HasNonStringOrNumericKey = false;
size_t MaxNumericIndex = 0;
for (auto&& [k, v] : o.as<sol::table>()) {
if (k.is<std::string>()) {
ObjectOut.emplace(k.as<std::string>(), std::make_shared<TDetachedLuaValue>(std::move(Freeze(v, depth + 1))));
continue;
}
// Thanks to lua handling arrays in a weird way, and because they can also be sparse, this weird code is needed.
if (k.get_type() == sol::type::number) {
const double NumericKey = k.as<double>();
const size_t CandidateIndex = static_cast<size_t>(NumericKey);
// unsure if we need to do this, or if we can do the same int check we do in other places, but this works well
const bool IsPositiveInteger = std::isfinite(NumericKey)
&& NumericKey >= 1.0
&& std::fabs(NumericKey - static_cast<double>(CandidateIndex)) <= std::numeric_limits<double>::epsilon();
if (IsPositiveInteger) {
const auto Index = CandidateIndex;
MaxNumericIndex = std::max(MaxNumericIndex, Index);
NumericEntries.emplace_back(Index, Freeze(v, depth + 1));
continue;
}
}
HasNonStringOrNumericKey = true;
}
// Pure numeric-keyed tables are reconstructed as Lua arrays (1-based indexes).
// This preserves array semantics across this serialization boundary :^)
if (!NumericEntries.empty() && ObjectOut.empty() && !HasNonStringOrNumericKey) {
TDetachedLuaValue::Array ArrayOut(MaxNumericIndex);
for (const auto& [Index, Value] : NumericEntries) {
if (Index > 0 && Index <= ArrayOut.size()) {
ArrayOut[Index - 1] = Value;
}
}
return { { std::move(ArrayOut) } };
}
return { { std::move(ObjectOut) } };
}
default:
throw std::runtime_error("unsupported Lua type for cross-thread snapshot");
}
}
void TLuaResult::MarkAsReady() {
mReady = true;
mReadyCondition.notify_all();
}
void TLuaResult::WaitUntilReady() {
std::unique_lock Lock(mMutex);
while (!mReady)
mReadyCondition.wait_for(Lock, std::chrono::milliseconds(50),
[this] {
return mReady;
});
}
TEST_CASE("TLuaInStateResult MarkReadyError(string) marks ready and wakes waiters") {
TLuaVoidResult result("state_local");
std::atomic<bool> waiterDone { false };
auto waiter = std::thread([&] {
result.WaitUntilReady();
waiterDone.store(true, std::memory_order_release);
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
CHECK_FALSE(waiterDone.load(std::memory_order_acquire));
result.MarkReadyError(std::string("boom"));
waiter.join();
CHECK(result.IsReady());
const auto snapshot = result.GetSnapshot();
CHECK(snapshot.Ready);
CHECK(snapshot.Error);
CHECK(snapshot.ErrorMessage == "boom");
CHECK(snapshot.StateId == "state_local");
}
TEST_CASE("TLuaInStateResult MarkReadySuccess clears error state") {
TLuaVoidResult result("state_local_success");
result.MarkReadySuccess();
CHECK(result.IsReady());
CHECK_FALSE(result.IsError());
const auto snapshot = result.GetSnapshot();
CHECK(snapshot.Ready);
CHECK_FALSE(snapshot.Error);
CHECK(snapshot.ErrorMessage.empty());
}
TEST_CASE("TLuaResult MarkReadyError(string) marks ready and wakes waiters") {
TLuaResult result("state_a", "fn_a");
std::atomic<bool> waiterDone { false };
auto waiter = std::thread([&] {
result.WaitUntilReady();
waiterDone.store(true, std::memory_order_release);
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
CHECK_FALSE(waiterDone.load(std::memory_order_acquire));
result.MarkReadyError(std::string("boom"));
waiter.join();
CHECK(result.IsReady());
const auto snapshot = result.GetDetachedSnapshot();
CHECK(snapshot.Ready);
CHECK(snapshot.Error);
CHECK(snapshot.ErrorMessage == "boom");
CHECK(snapshot.StateId == "state_a");
CHECK(snapshot.Function == "fn_a");
}
TEST_CASE("TLuaResult detached snapshot freezes nested string-keyed tables") {
sol::state lua;
TLuaResult result("state_table", "fn_table");
lua.open_libraries(sol::lib::base);
auto outer = lua.create_table();
auto inner = lua.create_table();
inner["k"] = std::string("v");
outer["flag"] = true;
outer["msg"] = std::string("hello");
outer["inner"] = inner;
outer[1] = std::string("ignored_numeric_key");
result.MarkReadySuccess(sol::make_object(lua.lua_state(), outer));
const auto detached = result.GetDetachedSnapshot();
CHECK(detached.Ready);
CHECK_FALSE(detached.Error);
const auto* object = std::get_if<TDetachedLuaValue::Object>(&detached.Result.V);
REQUIRE(object != nullptr);
CHECK(object->contains("flag"));
CHECK(object->contains("msg"));
CHECK(object->contains("inner"));
CHECK_FALSE(object->contains("1"));
const auto* flag = std::get_if<bool>(&object->at("flag")->V);
REQUIRE(flag != nullptr);
CHECK(*flag);
const auto* msg = std::get_if<std::string>(&object->at("msg")->V);
REQUIRE(msg != nullptr);
CHECK(*msg == "hello");
const auto* innerObj = std::get_if<TDetachedLuaValue::Object>(&object->at("inner")->V);
REQUIRE(innerObj != nullptr);
REQUIRE(innerObj->contains("k"));
const auto* innerValue = std::get_if<std::string>(&innerObj->at("k")->V);
REQUIRE(innerValue != nullptr);
CHECK(*innerValue == "v");
}
TEST_CASE("TLuaResult detached snapshot preserves numeric array tables") {
sol::state lua;
TLuaResult result("state_array", "fn_array");
lua.open_libraries(sol::lib::base);
auto arr = lua.create_table();
arr[1] = std::string("a");
arr[2] = 42;
arr[4] = true; // keep sparse indexes
result.MarkReadySuccess(sol::make_object(lua.lua_state(), arr));
const auto detached = result.GetDetachedSnapshot();
CHECK(detached.Ready);
CHECK_FALSE(detached.Error);
const auto* array = std::get_if<TDetachedLuaValue::Array>(&detached.Result.V);
REQUIRE(array != nullptr);
REQUIRE(array->size() == 4);
const auto* v1 = std::get_if<std::string>(&(*array)[0].V);
REQUIRE(v1 != nullptr);
CHECK(*v1 == "a");
const auto* v2 = std::get_if<int>(&(*array)[1].V);
REQUIRE(v2 != nullptr);
CHECK(*v2 == 42);
CHECK(std::holds_alternative<std::monostate>((*array)[2].V));
const auto* v4 = std::get_if<bool>(&(*array)[3].V);
REQUIRE(v4 != nullptr);
CHECK(*v4);
}
TEST_CASE("TLuaResult MarkReadySuccess throws on unsupported Lua function value") {
sol::state lua;
TLuaResult result("state_fn", "fn_fn");
lua.open_libraries(sol::lib::base);
lua["f"] = [] { return 1; };
const sol::table globals = lua.globals();
const sol::protected_function fn = globals.get<sol::protected_function>("f");
const sol::object fnObj = sol::make_object(lua.lua_state(), fn);
CHECK_THROWS_AS(result.MarkReadySuccess(fnObj), std::runtime_error);
CHECK_FALSE(result.IsReady());
}
TEST_CASE("TLuaResult MarkReadySuccessNoResult stores monostate") {
TLuaResult result("state_empty", "fn_empty");
result.MarkReadySuccessNoResult();
CHECK(result.IsReady());
const auto snapshot = result.GetDetachedSnapshot();
CHECK_FALSE(snapshot.Error);
CHECK(std::holds_alternative<std::monostate>(snapshot.Result.V));
}
+132 -442
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -55,8 +55,8 @@ void TPPSMonitor::operator()() {
std::shared_ptr<TClient> c; std::shared_ptr<TClient> c;
{ {
ReadLock Lock(mServer.GetClientMutex()); ReadLock Lock(mServer.GetClientMutex());
if (auto Locked = ClientPtr.lock()) { if (!ClientPtr.expired()) {
c = std::move(Locked); c = ClientPtr.lock();
} else } else
return true; return true;
} }
@@ -76,7 +76,7 @@ void TPPSMonitor::operator()() {
return true; return true;
}); });
for (auto& ClientToKick : TimedOutClients) { for (auto& ClientToKick : TimedOutClients) {
Network().DisconnectClient(*ClientToKick, "Timeout"); ClientToKick->Disconnect("Timeout");
} }
TimedOutClients.clear(); TimedOutClients.clear();
if (C == 0 || mInternalPPS == 0) { if (C == 0 || mInternalPPS == 0) {
+2 -3
View File
@@ -69,9 +69,8 @@ void TPluginMonitor::operator()() {
auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path()); auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path());
auto Res = mEngine->EnqueueScript(StateID, Chunk); auto Res = mEngine->EnqueueScript(StateID, Chunk);
Res->WaitUntilReady(); Res->WaitUntilReady();
if (Res->IsError()) { if (Res->Error) {
auto Snapshot = Res->GetSnapshot(); beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Res->ErrorMessage);
beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Snapshot.ErrorMessage);
} else { } else {
mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit")); mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit"));
mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first)); mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first));
+25 -87
View File
@@ -28,7 +28,6 @@
#include <any> #include <any>
#include <optional> #include <optional>
#include <sstream> #include <sstream>
#include <utility>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@@ -54,6 +53,7 @@ static std::optional<std::pair<int, int>> GetPidVid(const std::string& str) {
} }
return std::nullopt; return std::nullopt;
} }
TEST_CASE("GetPidVid") { TEST_CASE("GetPidVid") {
SUBCASE("Valid singledigit") { SUBCASE("Valid singledigit") {
const auto MaybePidVid = GetPidVid("0-1"); const auto MaybePidVid = GetPidVid("0-1");
@@ -120,28 +120,27 @@ TEST_CASE("GetPidVid") {
CHECK(!MaybePidVid); CHECK(!MaybePidVid);
} }
} }
TServer::TServer(const std::vector<std::string_view>& Arguments) { TServer::TServer(const std::vector<std::string_view>& Arguments) {
beammp_info("BeamMP Server v" + Application::ServerVersionString()); beammp_info("BeamMP Server v" + Application::ServerVersionString());
Application::SetSubsystemStatus("Server", Application::Status::Starting); Application::SetSubsystemStatus("Server", Application::Status::Starting);
std::mt19937_64 TimeSyncRandomDevice(std::random_device{}());
TimeSyncStart = std::chrono::steady_clock::now()
- std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>(std::uniform_real_distribution<double>(0.0, 2592000.0)(TimeSyncRandomDevice))
);
Application::SetSubsystemStatus("Server", Application::Status::Good); Application::SetSubsystemStatus("Server", Application::Status::Good);
} }
void TServer::RemoveClient(const std::weak_ptr<TClient>& WeakClientPtr) { void TServer::RemoveClient(const std::weak_ptr<TClient>& WeakClientPtr) {
auto LockedClientPtr = WeakClientPtr.lock(); std::shared_ptr<TClient> LockedClientPtr { nullptr };
if (!LockedClientPtr) { try {
LockedClientPtr = WeakClientPtr.lock();
} catch (const std::exception&) {
// silently fail, as there's nothing to do
return; return;
} }
beammp_assert(LockedClientPtr != nullptr);
TClient& Client = *LockedClientPtr; TClient& Client = *LockedClientPtr;
beammp_debug("removing client " + Client.GetName() + " (" + std::to_string(ClientCount()) + ")"); beammp_debug("removing client " + Client.GetName() + " (" + std::to_string(ClientCount()) + ")");
Client.ClearCars(); Client.ClearCars();
WriteLock Lock(mClientsMutex); WriteLock Lock(mClientsMutex);
mClients.erase(LockedClientPtr); mClients.erase(WeakClientPtr.lock());
} }
void TServer::ForEachClient(const std::function<bool(std::weak_ptr<TClient>)>& Fn) { void TServer::ForEachClient(const std::function<bool(std::weak_ptr<TClient>)>& Fn) {
@@ -169,16 +168,14 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
try { try {
Packet = DeComp(Packet); Packet = DeComp(Packet);
} catch (const InvalidDataError& ) { } catch (const InvalidDataError& ) {
if (auto LockedClient = Client.lock()) { auto LockedClient = Client.lock();
beammp_errorf("Failed to decompress packet from client {}. The client sent invalid data and will now be disconnected.", LockedClient->GetID()); beammp_errorf("Failed to decompress packet from client {}. The client sent invalid data and will now be disconnected.", LockedClient->GetID());
Network.ClientKick(*LockedClient, "Sent invalid compressed packet (this is likely a bug on your end)"); Network.ClientKick(*LockedClient, "Sent invalid compressed packet (this is likely a bug on your end)");
}
return; return;
} catch (const std::runtime_error& e) { } catch (const std::runtime_error& e) {
if (auto LockedClient = Client.lock()) { auto LockedClient = Client.lock();
beammp_errorf("Failed to decompress packet from client {}: {}. The server might be out of RAM! The client will now be disconnected.", LockedClient->GetID(), e.what()); beammp_errorf("Failed to decompress packet from client {}: {}. The server might be out of RAM! The client will now be disconnected.", LockedClient->GetID(), e.what());
Network.ClientKick(*LockedClient, "Decompression failed (likely a server-side problem)"); Network.ClientKick(*LockedClient, "Decompression failed (likely a server-side problem)");
}
return; return;
} }
} }
@@ -186,10 +183,10 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
return; return;
} }
auto LockedClient = Client.lock(); if (Client.expired()) {
if (!LockedClient) {
return; return;
} }
auto LockedClient = Client.lock();
std::any Res; std::any Res;
char Code = Packet.at(0); char Code = Packet.at(0);
@@ -216,30 +213,6 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
return; return;
} }
switch (Code) { switch (Code) {
case 't':
if (!udp && Packet.size() == 11) {
uint64_t Time = GetServerTimeMS();
std::vector<uint8_t> ServerTime(sizeof(uint64_t));
std::memcpy(ServerTime.data(), &Time, sizeof(uint64_t));
uint16_t selfPing = 0;
std::memcpy(&selfPing, &Packet[Packet.size() - 2], sizeof(uint16_t));
LockedClient->SetSelfPing(selfPing);
Packet.erase(Packet.end() - 2,Packet.end());
auto Data = Packet;
beammp_debugf("Sending server time {} to client '{}' ({}), client time: {}", Time, LockedClient->GetName(), LockedClient->GetID(), reinterpret_cast<uint64_t>(Data.data()));
Data.insert(Data.end(), ServerTime.begin(), ServerTime.end());
if (!Network.Respond(*LockedClient, Data, true)) {
Network.DisconnectClient(*LockedClient, "Failed to send time");
}
}
return;
case 'H': // initial connection case 'H': // initial connection
if (udp) { if (udp) {
beammp_debugf("Received 'H' packet over UDP from client '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID()); beammp_debugf("Received 'H' packet over UDP from client '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
@@ -252,7 +225,7 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
case 'p': case 'p':
if (!Network.Respond(*LockedClient, StringToVector("p"), false)) { if (!Network.Respond(*LockedClient, StringToVector("p"), false)) {
// failed to send // failed to send
Network.DisconnectClient(*LockedClient, "Failed to send ping"); LockedClient->Disconnect("Failed to send ping");
} else { } else {
Network.UpdatePlayer(*LockedClient); Network.UpdatePlayer(*LockedClient);
} }
@@ -293,15 +266,9 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), PacketAsString.substr(PacketAsString.find(':', 3) + 1)); LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), PacketAsString.substr(PacketAsString.find(':', 3) + 1));
bool Rejected = std::any_of(Futures.begin(), Futures.end(), bool Rejected = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Elem) { [](const std::shared_ptr<TLuaResult>& Elem) {
auto Snapshot = Elem->GetDetachedSnapshot(); return !Elem->Error
if (Snapshot.Error) { && Elem->Result.is<int>()
return false; && bool(Elem->Result.as<int>());
}
const int* MaybeInt = std::get_if<int>(&Snapshot.Result.V);
if (MaybeInt == nullptr) {
return false;
}
return bool(*MaybeInt);
}); });
if (!Rejected) { if (!Rejected) {
std::string SanitizedPacket = fmt::format("C:{}: {}", LockedClient->GetName(), Message); std::string SanitizedPacket = fmt::format("C:{}: {}", LockedClient->GetName(), Message);
@@ -414,15 +381,7 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
TLuaEngine::WaitForAll(Futures); TLuaEngine::WaitForAll(Futures);
bool ShouldntSpawn = std::any_of(Futures.begin(), Futures.end(), bool ShouldntSpawn = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Result) { [](const std::shared_ptr<TLuaResult>& Result) {
auto Snapshot = Result->GetDetachedSnapshot(); return !Result->Error && Result->Result.is<int>() && Result->Result.as<int>() != 0;
if (Snapshot.Error) {
return false;
}
const int* MaybeInt = std::get_if<int>(&Snapshot.Result.V);
if (MaybeInt == nullptr) {
return false;
}
return *MaybeInt != 0;
}); });
bool SpawnConfirmed = false; bool SpawnConfirmed = false;
@@ -459,15 +418,7 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
TLuaEngine::WaitForAll(Futures); TLuaEngine::WaitForAll(Futures);
bool ShouldntAllow = std::any_of(Futures.begin(), Futures.end(), bool ShouldntAllow = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Result) { [](const std::shared_ptr<TLuaResult>& Result) {
auto Snapshot = Result->GetDetachedSnapshot(); return !Result->Error && Result->Result.is<int>() && Result->Result.as<int>() != 0;
if (Snapshot.Error) {
return false;
}
const int* MaybeInt = std::get_if<int>(&Snapshot.Result.V);
if (MaybeInt == nullptr) {
return false;
}
return *MaybeInt != 0;
}); });
auto FoundPos = Packet.find('{'); auto FoundPos = Packet.find('{');
@@ -521,13 +472,7 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
} }
if (PID != -1 && VID != -1 && PID == c.GetID()) { if (PID != -1 && VID != -1 && PID == c.GetID()) {
auto BracketPos = Data.find('{'); Data = Data.substr(Data.find('{'));
if (BracketPos == std::string::npos) {
beammp_debugf("Invalid 'Or' packet body from client {}", c.GetID());
return;
}
Data = Data.substr(BracketPos);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleReset", "", c.GetID(), VID, Data)); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleReset", "", c.GetID(), VID, Data));
Network.SendToAll(&c, StringToVector(Packet), false, true); Network.SendToAll(&c, StringToVector(Packet), false, true);
} }
@@ -556,14 +501,7 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
} }
if (PID != -1 && VID != -1 && PID == c.GetID()) { if (PID != -1 && VID != -1 && PID == c.GetID()) {
auto BracketPos = Data.find('['); Data = Data.substr(Data.find('['));
if (BracketPos == std::string::npos) {
beammp_debugf("Invalid 'Op' packet body from client {}", c.GetID());
return;
}
Data = Data.substr(BracketPos);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehiclePaintChanged", "", c.GetID(), VID, Data)); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehiclePaintChanged", "", c.GetID(), VID, Data));
Network.SendToAll(&c, StringToVector(Packet), false, true); Network.SendToAll(&c, StringToVector(Packet), false, true);
+4 -15
View File
@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "ArgsParser.h" #include "ArgsParser.h"
#include "Client.h"
#include "Common.h" #include "Common.h"
#include "Http.h" #include "Http.h"
#include "LuaAPI.h" #include "LuaAPI.h"
@@ -176,6 +175,10 @@ int BeamMPServerMain(MainArguments Arguments) {
Application::SetSubsystemStatus("Main", Application::Status::ShuttingDown); Application::SetSubsystemStatus("Main", Application::Status::ShuttingDown);
Shutdown = true; Shutdown = true;
}); });
Application::RegisterShutdownHandler([] {
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onShutdown", "");
TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5));
});
TServer Server(Arguments.List); TServer Server(Arguments.List);
@@ -197,20 +200,6 @@ int BeamMPServerMain(MainArguments Arguments) {
TPluginMonitor PluginMonitor(fs::path(Application::Settings.getAsString(Settings::Key::General_ResourceFolder)) / "Server", LuaEngine); TPluginMonitor PluginMonitor(fs::path(Application::Settings.getAsString(Settings::Key::General_ResourceFolder)) / "Server", LuaEngine);
Application::RegisterShutdownHandler([] {
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onShutdown", "");
TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5));
});
Application::RegisterShutdownHandler([&Server, &Network] {
beammp_debug("Kicking all players due to shutdown");
Server.ForEachClient([&Network](std::weak_ptr<TClient> client) -> bool {
if (!client.expired()) {
Network.ClientKick(*client.lock(), "Server shutdown");
}
return true;
});
});
RegisterThread("Main(Waiting)"); RegisterThread("Main(Waiting)");
std::set<std::string> IgnoreSubsystems { std::set<std::string> IgnoreSubsystems {
+1 -5
View File
@@ -15,16 +15,12 @@
"rapidjson", "rapidjson",
"sol2", "sol2",
"curl", "curl",
"lua" "luajit"
], ],
"overrides": [ "overrides": [
{ {
"name": "sol2", "name": "sol2",
"version": "3.3.1" "version": "3.3.1"
},
{
"name": "lua",
"version": "5.3.5#6"
} }
], ],
"builtin-baseline": "5bf0c55239da398b8c6f450818c9e28d36bf9966" "builtin-baseline": "5bf0c55239da398b8c6f450818c9e28d36bf9966"