mirror of
https://github.com/BeamMP/BeamMP-Server.git
synced 2026-07-13 10:13:42 +00:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54f49d81e2 | |||
| 9bc22de9b0 | |||
| 24c83bfb40 | |||
| a008e1edc6 | |||
| 4104dc1f18 | |||
| 57fe7cb055 | |||
| 7c6acfdc86 | |||
| 06bd719dbf | |||
| 4045727c8b | |||
| c97d7b1b73 | |||
| dd6d90a986 | |||
| 8f2b8b25e8 | |||
| f820d75851 | |||
| 9ca12fc7a6 | |||
| 98fb12bbcc | |||
| 3e12e487ab | |||
| b3e8d86cef | |||
| 0c864665bb | |||
| 4bb7232a41 | |||
| 58d9f2e980 | |||
| 66f5f2b8b6 | |||
| e260de55af | |||
| 7096fe058a | |||
| 7089e5da9a | |||
| e9ce71d39a | |||
| d26e53a975 | |||
| 4da5a7440a | |||
| b1946ef1d9 | |||
| c08eefdf69 | |||
| be0d8d5334 | |||
| 1f55c35f2b | |||
| a3cfe47e78 | |||
| 58e5317687 | |||
| be6f3a2fd1 | |||
| 0ac5da2366 | |||
| b5642247a6 | |||
| a46f2d3204 | |||
| a59f7296d0 | |||
| 87e9db5382 | |||
| 7c7b7477eb | |||
| 6fca901aa2 | |||
| 7925caf1a2 | |||
| 8cdee376c6 | |||
| d56d9892c4 | |||
| 82102fcd4b | |||
| 66884920db | |||
| 5c2a0db7ef | |||
| b139a6aaad | |||
| 75e9c52ce8 | |||
| b4b262196b | |||
| 4af0e4ccac | |||
| 5d2b198fb8 | |||
| 89392a67c2 | |||
| 86a1c50c8b | |||
| d97010c8ca | |||
| 8a613e0b16 | |||
| 48d721f602 | |||
| f38797a9ab | |||
| da0089fb2e | |||
| 3befc84f00 | |||
| 595247d51d | |||
| 48a77e9ea6 | |||
| f9c85a1f35 | |||
| 168a410129 | |||
| 4926e1aebd | |||
| 1e2d8688a8 | |||
| 6f196aca64 | |||
| 99476a2c77 | |||
| 9fa9974159 | |||
| 3ef816845d | |||
| 6ec4106ec7 | |||
| b094e35f8c | |||
| 83afafc0c3 | |||
| 31122abe10 | |||
| 0615b57a37 | |||
| 4a8378427a | |||
| b74f0c7ca8 | |||
| 420c64f6cf | |||
| add45c085b | |||
| 372076a4ef | |||
| eb2deb73c1 | |||
| 21874afb87 | |||
| 184d50bf8c |
+13
-12
@@ -3,11 +3,11 @@ name: Linux
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'minor'
|
||||
- "develop"
|
||||
- "minor"
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
env:
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VCPKG_FORCE_SYSTEM_BINARIES: 1
|
||||
CMAKE_BUILD_TYPE: "Release"
|
||||
@@ -19,10 +19,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- distro: debian
|
||||
version: 11
|
||||
- distro: debian
|
||||
version: 12
|
||||
- distro: debian
|
||||
version: 13
|
||||
- distro: ubuntu
|
||||
version: 22.04
|
||||
- distro: ubuntu
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
steps:
|
||||
- name: get-cmake
|
||||
uses: lukka/get-cmake@v3.28.1
|
||||
|
||||
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
@@ -47,7 +47,8 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Git config safe directory
|
||||
shell: bash
|
||||
@@ -90,10 +91,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- distro: debian
|
||||
version: 11
|
||||
- distro: debian
|
||||
version: 12
|
||||
- distro: debian
|
||||
version: 13
|
||||
- distro: ubuntu
|
||||
version: 22.04
|
||||
- distro: ubuntu
|
||||
@@ -103,7 +104,7 @@ jobs:
|
||||
steps:
|
||||
- name: get-cmake
|
||||
uses: lukka/get-cmake@v3.28.1
|
||||
|
||||
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
@@ -118,7 +119,8 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Git config safe directory
|
||||
shell: bash
|
||||
@@ -153,4 +155,3 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: ./bin/BeamMP-Server-tests
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/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:
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
@@ -14,8 +14,8 @@ jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
name: Create Release
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
@@ -38,10 +38,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- distro: debian
|
||||
version: 11
|
||||
- distro: debian
|
||||
version: 12
|
||||
- distro: debian
|
||||
version: 13
|
||||
- distro: ubuntu
|
||||
version: 22.04
|
||||
- distro: ubuntu
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
steps:
|
||||
- name: get-cmake
|
||||
uses: lukka/get-cmake@v3.28.1
|
||||
|
||||
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
@@ -66,7 +66,8 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Git config safe directory
|
||||
shell: bash
|
||||
@@ -82,7 +83,7 @@ jobs:
|
||||
run: bash ./scripts/${{ matrix.distro }}-${{ matrix.version }}/3-build.sh
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -109,10 +110,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- distro: debian
|
||||
version: 11
|
||||
- distro: debian
|
||||
version: 12
|
||||
- distro: debian
|
||||
version: 13
|
||||
- distro: ubuntu
|
||||
version: 22.04
|
||||
- distro: ubuntu
|
||||
@@ -125,7 +126,7 @@ jobs:
|
||||
steps:
|
||||
- name: get-cmake
|
||||
uses: lukka/get-cmake@v3.28.1
|
||||
|
||||
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
@@ -140,7 +141,8 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Git config safe directory
|
||||
shell: bash
|
||||
@@ -156,7 +158,7 @@ jobs:
|
||||
run: bash ./scripts/${{ matrix.distro }}-${{ matrix.version }}/3-build.sh
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -193,7 +195,8 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create Build Environment
|
||||
shell: bash
|
||||
@@ -204,7 +207,7 @@ jobs:
|
||||
run: bash ./scripts/windows/2-build.sh
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -26,6 +26,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
|
||||
+9
-2
@@ -8,7 +8,7 @@ include(cmake/Vcpkg.cmake) # needs to happen before project()
|
||||
|
||||
project(
|
||||
"BeamMP-Server" # replace this
|
||||
VERSION 3.3.0
|
||||
VERSION 3.9.3
|
||||
)
|
||||
|
||||
include(cmake/StandardSettings.cmake)
|
||||
@@ -39,6 +39,7 @@ set(PRJ_HEADERS
|
||||
include/TConfig.h
|
||||
include/TConsole.h
|
||||
include/THeartbeatThread.h
|
||||
include/TIoPollThread.h
|
||||
include/TLuaEngine.h
|
||||
include/TLuaPlugin.h
|
||||
include/TNetwork.h
|
||||
@@ -52,6 +53,8 @@ set(PRJ_HEADERS
|
||||
include/Settings.h
|
||||
include/Profiling.h
|
||||
include/ChronoWrapper.h
|
||||
include/TConnectionLimiter.h
|
||||
include/TLuaResult.h
|
||||
)
|
||||
# add all source files (.cpp) to this, except the one with main()
|
||||
set(PRJ_SOURCES
|
||||
@@ -65,6 +68,7 @@ set(PRJ_SOURCES
|
||||
src/TConfig.cpp
|
||||
src/TConsole.cpp
|
||||
src/THeartbeatThread.cpp
|
||||
src/TIoPollThread.cpp
|
||||
src/TLuaEngine.cpp
|
||||
src/TLuaPlugin.cpp
|
||||
src/TNetwork.cpp
|
||||
@@ -78,6 +82,8 @@ set(PRJ_SOURCES
|
||||
src/Settings.cpp
|
||||
src/Profiling.cpp
|
||||
src/ChronoWrapper.cpp
|
||||
src/TConnectionLimiter.cpp
|
||||
src/TLuaResult.cpp
|
||||
)
|
||||
|
||||
find_package(Lua REQUIRED)
|
||||
@@ -148,7 +154,8 @@ if(UNIX)
|
||||
endif(UNIX)
|
||||
|
||||
if (WIN32)
|
||||
add_compile_options("-D_WIN32_WINNT=0x0601")
|
||||
add_compile_definitions(_WIN32_WINNT=0x0A00)
|
||||
|
||||
add_compile_options("/bigobj")
|
||||
endif(WIN32)
|
||||
|
||||
|
||||
@@ -15,3 +15,5 @@ if(NOT DEFINED CMAKE_TOOLCHAIN_FILE)
|
||||
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake)
|
||||
endif()
|
||||
|
||||
# ensure SOL2 safeties are all ON
|
||||
add_compile_definitions(SOL_ALL_SAFETIES_ON=1)
|
||||
|
||||
+14
-2
@@ -18,6 +18,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -68,8 +69,11 @@ public:
|
||||
std::string GetCarPositionRaw(int Ident);
|
||||
void SetUDPAddr(const ip::udp::endpoint& Addr) { mUDPAddress = Addr; }
|
||||
void SetTCPSock(ip::tcp::socket&& CSock) { mSocket = std::move(CSock); }
|
||||
void Disconnect(std::string_view Reason);
|
||||
bool IsDisconnected() const { return !mSocket.is_open(); }
|
||||
// Returns true only for the thread that actually performs socket shutdown/close.
|
||||
[[nodiscard]] bool Disconnect(std::string_view Reason);
|
||||
bool IsDisconnected() const {
|
||||
return mDisconnectState.load(std::memory_order_acquire) != EDisconnectState::Connected;
|
||||
}
|
||||
// locks
|
||||
void DeleteCar(int Ident);
|
||||
[[nodiscard]] const std::unordered_map<std::string, std::string>& GetIdentifiers() const { return mIdentifiers; }
|
||||
@@ -106,6 +110,12 @@ public:
|
||||
[[nodiscard]] const std::vector<uint8_t>& GetMagic() const { return mMagic; }
|
||||
|
||||
private:
|
||||
enum class EDisconnectState {
|
||||
Connected,
|
||||
Disconnecting,
|
||||
Disconnected
|
||||
};
|
||||
|
||||
void InsertVehicle(int ID, const std::string& Data);
|
||||
|
||||
TServer& mServer;
|
||||
@@ -121,6 +131,8 @@ private:
|
||||
TSetOfVehicleData mVehicleData;
|
||||
SparseArray<std::string> mVehiclePosition;
|
||||
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::udp::endpoint mUDPAddress {};
|
||||
int mUnicycleID = -1;
|
||||
|
||||
+5
-1
@@ -129,9 +129,13 @@ private:
|
||||
static inline std::mutex mShutdownHandlersMutex {};
|
||||
static inline std::deque<TShutdownHandler> mShutdownHandlers {};
|
||||
|
||||
static inline Version mVersion { 3, 9, 0 };
|
||||
static inline Version mVersion { 3, 9, 3 };
|
||||
};
|
||||
|
||||
/// 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);
|
||||
std::string LowerString(std::string str);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ enum class Key {
|
||||
// provider settings
|
||||
PROVIDER_UPDATE_MESSAGE,
|
||||
PROVIDER_DISABLE_CONFIG,
|
||||
PROVIDER_DISABLE_MP_SET,
|
||||
PROVIDER_PORT_ENV,
|
||||
PROVIDER_IP_ENV
|
||||
};
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ namespace MP {
|
||||
std::pair<bool, std::string> TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data);
|
||||
inline size_t GetPlayerCount() { return Engine->Server().ClientCount(); }
|
||||
std::pair<bool, std::string> DropPlayer(int ID, std::optional<std::string> MaybeReason);
|
||||
std::pair<bool, std::string> SendChatMessage(int ID, const std::string& Message);
|
||||
std::pair<bool, std::string> SendChatMessage(int ID, const std::string& Message, const bool& LogChat = true);
|
||||
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> RemoveVehicle(int PlayerID, int VehicleID);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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;
|
||||
};
|
||||
+11
-38
@@ -19,13 +19,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "Profiling.h"
|
||||
#include "TLuaResult.h"
|
||||
#include "TNetwork.h"
|
||||
#include "TServer.h"
|
||||
#include <any>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <initializer_list>
|
||||
#include <list>
|
||||
#include <lua.hpp>
|
||||
#include <memory>
|
||||
@@ -34,6 +33,8 @@
|
||||
#include <queue>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sol/forward.hpp>
|
||||
#include <sol/protected_function_result.hpp>
|
||||
#include <toml.hpp>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -58,36 +59,10 @@ namespace fs = std::filesystem;
|
||||
/**
|
||||
* std::variant means, that TLuaArgTypes may be one of the Types listed as template args
|
||||
*/
|
||||
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,
|
||||
};
|
||||
using TLuaValue = std::variant<std::monostate, std::string, int, JsonString, bool, std::unordered_map<std::string, std::string>, float>;
|
||||
|
||||
class TLuaPlugin;
|
||||
|
||||
struct TLuaResult {
|
||||
bool Ready;
|
||||
bool Error;
|
||||
std::string ErrorMessage;
|
||||
sol::object Result { sol::lua_nil };
|
||||
TLuaStateId StateId;
|
||||
std::string Function;
|
||||
std::shared_ptr<std::mutex> ReadyMutex {
|
||||
std::make_shared<std::mutex>()
|
||||
};
|
||||
std::shared_ptr<std::condition_variable> ReadyCondition {
|
||||
std::make_shared<std::condition_variable>()
|
||||
};
|
||||
|
||||
void MarkAsReady();
|
||||
void WaitUntilReady();
|
||||
};
|
||||
|
||||
struct TLuaPluginConfig {
|
||||
static inline const std::string FileName = "PluginConfig.toml";
|
||||
TLuaStateId StateId;
|
||||
@@ -166,7 +141,7 @@ public:
|
||||
const std::optional<std::chrono::high_resolution_clock::duration>& Max = std::nullopt);
|
||||
void ReportErrors(const std::vector<std::shared_ptr<TLuaResult>>& Results);
|
||||
bool HasState(TLuaStateId StateId);
|
||||
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script);
|
||||
[[nodiscard]] std::shared_ptr<TLuaVoidResult> 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);
|
||||
void EnsureStateExists(TLuaStateId StateId, const std::string& Name, bool DontCallOnInit = false);
|
||||
void RegisterEvent(const std::string& EventName, TLuaStateId StateId, const std::string& FunctionName);
|
||||
@@ -192,9 +167,7 @@ public:
|
||||
for (const auto& Event : mLuaEvents.at(EventName)) {
|
||||
for (const auto& Function : Event.second) {
|
||||
if (Event.first != IgnoreId) {
|
||||
auto Result = EnqueueFunctionCall(Event.first, Function, Arguments, EventName);
|
||||
Results.push_back(Result);
|
||||
AddResultToCheck(Result);
|
||||
Results.push_back(EnqueueFunctionCall(Event.first, Function, Arguments, EventName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,9 +202,9 @@ public:
|
||||
|
||||
// Debugging functions (slow)
|
||||
std::unordered_map<std::string /*event name */, std::vector<std::string> /* handlers */> Debug_GetEventsForState(TLuaStateId StateId);
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId);
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId);
|
||||
std::vector<QueuedFunction> Debug_GetStateFunctionQueueForState(TLuaStateId StateId);
|
||||
std::vector<TLuaResult> Debug_GetResultsToCheckForState(TLuaStateId StateId);
|
||||
std::vector<TLuaResult::DetachedSnapshot> Debug_GetResultsToCheckForState(TLuaStateId StateId);
|
||||
|
||||
private:
|
||||
void CollectAndInitPlugins();
|
||||
@@ -244,7 +217,7 @@ private:
|
||||
StateThreadData(const std::string& Name, TLuaStateId StateId, TLuaEngine& Engine);
|
||||
StateThreadData(const StateThreadData&) = delete;
|
||||
virtual ~StateThreadData() noexcept { beammp_debug("\"" + mStateId + "\" destroyed"); }
|
||||
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueScript(const TLuaChunk& Script);
|
||||
[[nodiscard]] std::shared_ptr<TLuaVoidResult> 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> 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);
|
||||
@@ -256,7 +229,7 @@ private:
|
||||
std::vector<std::string> GetStateTableKeys(const std::vector<std::string>& keys);
|
||||
|
||||
// Debug functions, slow
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Debug_GetStateExecuteQueue();
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> Debug_GetStateExecuteQueue();
|
||||
std::vector<TLuaEngine::QueuedFunction> Debug_GetStateFunctionQueue();
|
||||
|
||||
private:
|
||||
@@ -281,7 +254,7 @@ private:
|
||||
TLuaStateId mStateId;
|
||||
lua_State* mState;
|
||||
std::thread mThread;
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> mStateExecuteQueue;
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> mStateExecuteQueue;
|
||||
std::recursive_mutex mStateExecuteQueueMutex;
|
||||
std::vector<QueuedFunction> mStateFunctionQueue;
|
||||
std::mutex mStateFunctionQueueMutex;
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// 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();
|
||||
};
|
||||
+15
-9
@@ -20,9 +20,9 @@
|
||||
|
||||
#include "BoostAliases.h"
|
||||
#include "Compat.h"
|
||||
#include "TConnectionLimiter.h"
|
||||
#include "TResourceManager.h"
|
||||
#include "TServer.h"
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/udp.hpp>
|
||||
|
||||
struct TConnection;
|
||||
@@ -34,16 +34,21 @@ public:
|
||||
[[nodiscard]] bool TCPSend(TClient& c, const std::vector<uint8_t>& Data, bool IsSync = false);
|
||||
[[nodiscard]] bool SendLarge(TClient& c, std::vector<uint8_t> Data, bool isSync = false);
|
||||
[[nodiscard]] bool Respond(TClient& c, const std::vector<uint8_t>& MSG, bool Rel, bool isSync = false);
|
||||
std::shared_ptr<TClient> CreateClient(ip::tcp::socket&& TCPSock);
|
||||
std::vector<uint8_t> TCPRcv(TClient& c);
|
||||
std::shared_ptr<TClient> CreateClient(boost::asio::ip::tcp::socket&& TCPSock);
|
||||
std::vector<uint8_t> TCPRcv(TClient& c, bool WithTimeout = false);
|
||||
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);
|
||||
void Identify(TConnection&& client);
|
||||
void Identify(TConnection&& client, TConnectionLimiter::TGuard&&);
|
||||
std::shared_ptr<TClient> Authentication(TConnection&& ClientConnection);
|
||||
void SyncResources(TClient& c);
|
||||
[[nodiscard]] bool UDPSend(TClient& Client, std::vector<uint8_t> Data);
|
||||
void SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self, bool Rel);
|
||||
void UpdatePlayer(TClient& Client);
|
||||
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; }
|
||||
|
||||
@@ -53,13 +58,14 @@ private:
|
||||
|
||||
TServer& mServer;
|
||||
TPPSMonitor& mPPSMonitor;
|
||||
ip::udp::socket mUDPSock;
|
||||
boost::asio::ip::udp::socket mUDPSock;
|
||||
TResourceManager& mResourceManager;
|
||||
std::thread mUDPThread;
|
||||
std::thread mTCPThread;
|
||||
std::mutex mOpenIDMutex;
|
||||
TConnectionLimiter mConnectionLimiter;
|
||||
|
||||
std::vector<uint8_t> UDPRcvFromClient(ip::udp::endpoint& ClientEndpoint);
|
||||
std::vector<uint8_t> UDPRcvFromClient(boost::asio::ip::udp::endpoint& ClientEndpoint);
|
||||
void OnConnect(const std::weak_ptr<TClient>& c);
|
||||
void TCPClient(const std::weak_ptr<TClient>& c);
|
||||
void Looper(const std::weak_ptr<TClient>& c);
|
||||
@@ -67,9 +73,9 @@ private:
|
||||
void OnDisconnect(const std::weak_ptr<TClient>& ClientPtr);
|
||||
void Parse(TClient& c, const std::vector<uint8_t>& Packet);
|
||||
void SendFile(TClient& c, const std::string& Name);
|
||||
static bool TCPSendRaw(TClient& C, ip::tcp::socket& socket, const uint8_t* Data, size_t Size);
|
||||
static void SendFileToClient(TClient& c, size_t Size, const std::string& Name);
|
||||
static const uint8_t* SendSplit(TClient& c, ip::tcp::socket& Socket, const uint8_t* DataPtr, size_t Size);
|
||||
static bool TCPSendRaw(TClient& C, boost::asio::ip::tcp::socket& socket, const uint8_t* Data, size_t Size);
|
||||
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);
|
||||
};
|
||||
|
||||
std::string HashPassword(const std::string& str);
|
||||
|
||||
+3
-3
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "IThreaded.h"
|
||||
#include "RWMutex.h"
|
||||
#include "TIoPollThread.h"
|
||||
#include "TScopedTimer.h"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -50,11 +51,10 @@ public:
|
||||
|
||||
const TScopedTimer UptimeTimer;
|
||||
|
||||
// asio io context
|
||||
io_context& IoCtx() { return mIoCtx; }
|
||||
io_context& IoCtx() { return mIoCtxPoller.IoCtx(); }
|
||||
|
||||
private:
|
||||
io_context mIoCtx {};
|
||||
TIoPollThread mIoCtxPoller;
|
||||
TClientSet mClients;
|
||||
mutable RWMutex mClientsMutex;
|
||||
static void ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network);
|
||||
|
||||
@@ -4,4 +4,4 @@ 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" -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 -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON
|
||||
|
||||
@@ -4,4 +4,4 @@ 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" -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 -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
cmake --build bin --parallel -t BeamMP-Server-tests
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
apt-get update -y
|
||||
|
||||
apt-get install -y liblua5.3-0 curl
|
||||
|
||||
@@ -4,4 +4,4 @@ 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" -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 -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON
|
||||
|
||||
@@ -4,4 +4,4 @@ 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" -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 -Wl,--export-dynamic" -DBeamMP-Server_ENABLE_LTO=ON
|
||||
|
||||
+11
-3
@@ -76,7 +76,13 @@ std::string TClient::GetCarPositionRaw(int Ident) {
|
||||
}
|
||||
}
|
||||
|
||||
void TClient::Disconnect(std::string_view Reason) {
|
||||
bool 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);
|
||||
boost::system::error_code ec;
|
||||
if (mSocket.is_open()) {
|
||||
@@ -91,6 +97,9 @@ void TClient::Disconnect(std::string_view Reason) {
|
||||
} else {
|
||||
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) {
|
||||
@@ -169,8 +178,7 @@ std::optional<std::weak_ptr<TClient>> GetClient(TServer& Server, int ID) {
|
||||
std::optional<std::weak_ptr<TClient>> MaybeClient { std::nullopt };
|
||||
Server.ForEachClient([&](std::weak_ptr<TClient> CPtr) -> bool {
|
||||
ReadLock Lock(Server.GetClientMutex());
|
||||
if (!CPtr.expired()) {
|
||||
auto C = CPtr.lock();
|
||||
if (auto C = CPtr.lock()) {
|
||||
if (C->GetID() == ID) {
|
||||
MaybeClient = CPtr;
|
||||
return false;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
#include "TLuaResult.h"
|
||||
#include "Compat.h"
|
||||
#include "CustomAssert.h"
|
||||
#include "Http.h"
|
||||
|
||||
@@ -36,6 +36,9 @@ std::string_view Env::ToString(Env::Key key) {
|
||||
case Key::PROVIDER_DISABLE_CONFIG:
|
||||
return "BEAMMP_PROVIDER_DISABLE_CONFIG";
|
||||
break;
|
||||
case Key::PROVIDER_DISABLE_MP_SET:
|
||||
return "BEAMMP_PROVIDER_DISABLE_MP_SET";
|
||||
break;
|
||||
case Key::PROVIDER_PORT_ENV:
|
||||
return "BEAMMP_PROVIDER_PORT_ENV";
|
||||
break;
|
||||
|
||||
+107
-11
@@ -18,15 +18,12 @@
|
||||
|
||||
#include "Http.h"
|
||||
|
||||
#include "Client.h"
|
||||
#include "Common.h"
|
||||
#include "CustomAssert.h"
|
||||
#include "LuaAPI.h"
|
||||
|
||||
#include <curl/easy.h>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -37,17 +34,107 @@ static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void*
|
||||
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 Ret;
|
||||
static thread_local CURL* curl = curl_easy_init();
|
||||
CurlLease Lease{};
|
||||
CURL* curl = Lease.GetHandle();
|
||||
if (curl) {
|
||||
CURLcode res;
|
||||
char errbuf[CURL_ERROR_SIZE];
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast<void*>(&Ret));
|
||||
|
||||
// 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_ERRORBUFFER, errbuf);
|
||||
errbuf[0] = 0;
|
||||
@@ -71,15 +158,16 @@ 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 Ret;
|
||||
static thread_local CURL* curl = curl_easy_init();
|
||||
CurlLease Lease{};
|
||||
CURL* curl = Lease.GetHandle();
|
||||
if (curl) {
|
||||
CURLcode res;
|
||||
char errbuf[CURL_ERROR_SIZE];
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast<void*>(&Ret));
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size());
|
||||
struct curl_slist* list = nullptr;
|
||||
@@ -90,7 +178,15 @@ 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_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_ERRORBUFFER, errbuf);
|
||||
errbuf[0] = 0;
|
||||
|
||||
+112
-74
@@ -143,22 +143,23 @@ static inline std::pair<bool, std::string> InternalTriggerClientEvent(int Player
|
||||
return { true, "" };
|
||||
} else {
|
||||
auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID);
|
||||
if (!MaybeClient || MaybeClient.value().expired()) {
|
||||
beammp_lua_errorf("TriggerClientEvent invalid Player ID '{}'", PlayerID);
|
||||
return { false, "Invalid Player ID" };
|
||||
}
|
||||
auto c = MaybeClient.value().lock();
|
||||
if (MaybeClient) {
|
||||
if (auto c = MaybeClient.value().lock()) {
|
||||
if (!c->IsSyncing() && !c->IsSynced()) {
|
||||
return { false, "Player hasn't joined yet" };
|
||||
}
|
||||
|
||||
if (!c->IsSyncing() && !c->IsSynced()) {
|
||||
return { false, "Player hasn't joined yet" };
|
||||
if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
|
||||
beammp_lua_errorf("Respond failed, dropping client {}", PlayerID);
|
||||
LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets");
|
||||
return { false, "Respond failed, dropping client" };
|
||||
}
|
||||
return { true, "" };
|
||||
}
|
||||
}
|
||||
|
||||
if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
|
||||
beammp_lua_errorf("Respond failed, dropping client {}", PlayerID);
|
||||
LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets");
|
||||
return { false, "Respond failed, dropping client" };
|
||||
}
|
||||
return { true, "" };
|
||||
beammp_lua_errorf("TriggerClientEvent invalid Player ID '{}'", PlayerID);
|
||||
return { false, "Invalid Player ID" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,37 +170,48 @@ 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) {
|
||||
auto MaybeClient = GetClient(Engine->Server(), ID);
|
||||
if (!MaybeClient || MaybeClient.value().expired()) {
|
||||
beammp_lua_errorf("Tried to drop client with id {}, who doesn't exist", ID);
|
||||
return { false, "Player does not exist" };
|
||||
if (MaybeClient) {
|
||||
if (auto c = MaybeClient.value().lock()) {
|
||||
LuaAPI::MP::Engine->Network().ClientKick(*c, MaybeReason.value_or("No reason"));
|
||||
return { true, "" };
|
||||
}
|
||||
}
|
||||
auto c = MaybeClient.value().lock();
|
||||
LuaAPI::MP::Engine->Network().ClientKick(*c, MaybeReason.value_or("No reason"));
|
||||
return { true, "" };
|
||||
|
||||
beammp_lua_errorf("Tried to drop client with id {}, who doesn't exist", ID);
|
||||
return { false, "Player does not exist" };
|
||||
}
|
||||
|
||||
std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::string& Message) {
|
||||
std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::string& Message, const bool& LogChat) {
|
||||
std::pair<bool, std::string> Result;
|
||||
std::string Packet = "C:Server: " + Message;
|
||||
if (ID == -1) {
|
||||
LogChatMessage("<Server> (to everyone) ", -1, Message);
|
||||
if (LogChat) {
|
||||
LogChatMessage("<Server> (to everyone) ", -1, Message);
|
||||
}
|
||||
Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
|
||||
Result.first = true;
|
||||
} else {
|
||||
auto MaybeClient = GetClient(Engine->Server(), ID);
|
||||
if (MaybeClient && !MaybeClient.value().expired()) {
|
||||
auto c = MaybeClient.value().lock();
|
||||
if (!c->IsSynced()) {
|
||||
if (MaybeClient) {
|
||||
if (auto c = MaybeClient.value().lock()) {
|
||||
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.second = "Player still syncing data";
|
||||
return Result;
|
||||
Result.second = "Invalid Player ID";
|
||||
}
|
||||
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;
|
||||
@@ -219,18 +231,23 @@ std::pair<bool, std::string> LuaAPI::MP::SendNotification(int ID, const std::str
|
||||
} else {
|
||||
auto MaybeClient = GetClient(Engine->Server(), ID);
|
||||
if (MaybeClient) {
|
||||
auto c = MaybeClient.value().lock();
|
||||
if (!c->IsSynced()) {
|
||||
if (auto c = MaybeClient.value().lock()) {
|
||||
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.second = "Player is not synced yet";
|
||||
return Result;
|
||||
Result.second = "Invalid Player ID";
|
||||
}
|
||||
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;
|
||||
@@ -261,18 +278,23 @@ std::pair<bool, std::string> LuaAPI::MP::ConfirmationDialog(int ID, const std::s
|
||||
} else {
|
||||
auto MaybeClient = GetClient(Engine->Server(), ID);
|
||||
if (MaybeClient) {
|
||||
auto c = MaybeClient.value().lock();
|
||||
if (!c->IsSynced()) {
|
||||
if (auto c = MaybeClient.value().lock()) {
|
||||
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.second = "Player is not synced yet";
|
||||
return Result;
|
||||
Result.second = "Invalid Player ID";
|
||||
}
|
||||
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;
|
||||
@@ -286,22 +308,27 @@ 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> Result;
|
||||
auto MaybeClient = GetClient(Engine->Server(), PID);
|
||||
if (!MaybeClient || MaybeClient.value().expired()) {
|
||||
if (MaybeClient) {
|
||||
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");
|
||||
Result.first = false;
|
||||
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;
|
||||
}
|
||||
@@ -408,20 +435,22 @@ void LuaAPI::MP::Sleep(size_t Ms) {
|
||||
|
||||
bool LuaAPI::MP::IsPlayerConnected(int ID) {
|
||||
auto MaybeClient = GetClient(Engine->Server(), ID);
|
||||
if (MaybeClient && !MaybeClient.value().expired()) {
|
||||
return MaybeClient.value().lock()->IsUDPConnected();
|
||||
} else {
|
||||
return false;
|
||||
if (MaybeClient) {
|
||||
if (auto c = MaybeClient.value().lock()) {
|
||||
return c->IsUDPConnected();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LuaAPI::MP::IsPlayerGuest(int ID) {
|
||||
auto MaybeClient = GetClient(Engine->Server(), ID);
|
||||
if (MaybeClient && !MaybeClient.value().expired()) {
|
||||
return MaybeClient.value().lock()->IsGuest();
|
||||
} else {
|
||||
return false;
|
||||
if (MaybeClient) {
|
||||
if (auto c = MaybeClient.value().lock()) {
|
||||
return c->IsGuest();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void LuaAPI::MP::PrintRaw(sol::variadic_args Args) {
|
||||
@@ -436,7 +465,16 @@ void LuaAPI::MP::PrintRaw(sol::variadic_args Args) {
|
||||
}
|
||||
|
||||
int LuaAPI::PanicHandler(lua_State* State) {
|
||||
beammp_lua_error("PANIC: " + sol::stack::get<std::string>(State, 1));
|
||||
// panic path: use raw lua c api only; sol2 conversions can raise lua_error
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
+43
-34
@@ -22,9 +22,9 @@
|
||||
|
||||
#include "Client.h"
|
||||
#include "CustomAssert.h"
|
||||
#include "Http.h"
|
||||
#include "LuaAPI.h"
|
||||
#include "TLuaEngine.h"
|
||||
#include "Http.h"
|
||||
|
||||
#include <ctime>
|
||||
#include <lua.hpp>
|
||||
@@ -160,7 +160,7 @@ void TConsole::ChangeToRegularConsole() {
|
||||
}
|
||||
|
||||
bool TConsole::EnsureArgsCount(const std::vector<std::string>& args, size_t n) {
|
||||
if (n == 0 && args.size() != 0) {
|
||||
if (n == 0 && !args.empty()) {
|
||||
Application::Console().WriteRaw("This command expects no arguments.");
|
||||
return false;
|
||||
} else if (args.size() != n) {
|
||||
@@ -198,7 +198,7 @@ void TConsole::Command_Lua(const std::string&, const std::vector<std::string>& a
|
||||
} else {
|
||||
Application::Console().WriteRaw("Lua state '" + NewStateId + "' is not a known state. Didn't switch to Lua.");
|
||||
}
|
||||
} else if (args.size() == 0) {
|
||||
} else if (args.empty()) {
|
||||
ChangeToLuaConsole(mDefaultStateId);
|
||||
}
|
||||
}
|
||||
@@ -296,8 +296,7 @@ void TConsole::Command_NetTest(const std::string& cmd, const std::vector<std::st
|
||||
unsigned int status = 0;
|
||||
|
||||
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);
|
||||
|
||||
@@ -334,10 +333,9 @@ void TConsole::Command_Kick(const std::string&, const std::vector<std::string>&
|
||||
return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1);
|
||||
};
|
||||
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
|
||||
if (!Client.expired()) {
|
||||
auto locked = Client.lock();
|
||||
if (NameCompare(locked->GetName(), Name)) {
|
||||
mLuaEngine->Network().ClientKick(*locked, Reason);
|
||||
if (auto Locked = Client.lock()) {
|
||||
if (NameCompare(Locked->GetName(), Name)) {
|
||||
mLuaEngine->Network().ClientKick(*Locked, Reason);
|
||||
Kicked = true;
|
||||
return false;
|
||||
}
|
||||
@@ -356,7 +354,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
|
||||
auto End_i = CommandWithArgs.find_first_of(' ');
|
||||
std::string Command = CommandWithArgs.substr(0, End_i);
|
||||
std::string ArgsStr {};
|
||||
std::string ArgsStr { };
|
||||
if (End_i != std::string::npos) {
|
||||
ArgsStr = CommandWithArgs.substr(End_i);
|
||||
}
|
||||
@@ -423,7 +421,7 @@ void TConsole::Command_Settings(const std::string&, const std::vector<std::strin
|
||||
settings set <category> <setting> <value> sets specified setting to value
|
||||
)";
|
||||
|
||||
if (args.size() == 0) {
|
||||
if (args.empty()) {
|
||||
beammp_errorf("No arguments specified for command 'settings'!");
|
||||
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
|
||||
return;
|
||||
@@ -566,11 +564,10 @@ void TConsole::Command_List(const std::string&, const std::vector<std::string>&
|
||||
std::stringstream ss;
|
||||
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 {
|
||||
if (!Client.expired()) {
|
||||
auto locked = Client.lock();
|
||||
ss << std::left << std::setw(25) << locked->GetName()
|
||||
<< std::setw(6) << locked->GetID()
|
||||
<< std::setw(6) << locked->GetCarCount() << "\n";
|
||||
if (auto Locked = Client.lock()) {
|
||||
ss << std::left << std::setw(25) << Locked->GetName()
|
||||
<< std::setw(6) << Locked->GetID()
|
||||
<< std::setw(6) << Locked->GetCarCount() << "\n";
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -593,8 +590,7 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
|
||||
size_t MissedPacketQueueSum = 0;
|
||||
int LargestSecondsSinceLastPing = 0;
|
||||
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
|
||||
if (!Client.expired()) {
|
||||
auto Locked = Client.lock();
|
||||
if (auto Locked = Client.lock()) {
|
||||
CarCount += Locked->GetCarCount();
|
||||
ConnectedCount += Locked->IsUDPConnected() ? 1 : 0;
|
||||
GuestCount += Locked->IsGuest() ? 1 : 0;
|
||||
@@ -613,11 +609,11 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
|
||||
size_t SystemsBad = 0;
|
||||
size_t SystemsShuttingDown = 0;
|
||||
size_t SystemsShutdown = 0;
|
||||
std::string SystemsBadList {};
|
||||
std::string SystemsGoodList {};
|
||||
std::string SystemsStartingList {};
|
||||
std::string SystemsShuttingDownList {};
|
||||
std::string SystemsShutdownList {};
|
||||
std::string SystemsBadList { };
|
||||
std::string SystemsGoodList { };
|
||||
std::string SystemsStartingList { };
|
||||
std::string SystemsShuttingDownList { };
|
||||
std::string SystemsShutdownList { };
|
||||
auto Statuses = Application::GetSubsystemStatuses();
|
||||
for (const auto& NameStatusPair : Statuses) {
|
||||
switch (NameStatusPair.second) {
|
||||
@@ -653,6 +649,7 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
|
||||
SystemsShutdownList = SystemsShutdownList.substr(0, SystemsShutdownList.size() - 2);
|
||||
|
||||
auto ElapsedTime = mLuaEngine->Server().UptimeTimer.GetElapsedTime();
|
||||
auto ConnectionLimiterStats = mLuaEngine->Network().GetConnectionLimiterStats();
|
||||
|
||||
Status << "BeamMP-Server Status:\n"
|
||||
<< "\tTotal Players: " << mLuaEngine->Server().ClientCount() << "\n"
|
||||
@@ -667,6 +664,11 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
|
||||
<< "\t\tStates: " << mLuaEngine->GetLuaStateCount() << "\n"
|
||||
<< "\t\tEvent timers: " << mLuaEngine->GetTimedEventsCount() << "\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"
|
||||
<< "\t\tGood/Starting/Bad: " << SystemsGood << "/" << SystemsStarting << "/" << SystemsBad << "\n"
|
||||
<< "\t\tShutting down/Shut down: " << SystemsShuttingDown << "/" << SystemsShutdown << "\n"
|
||||
@@ -683,9 +685,13 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
|
||||
void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
|
||||
auto FutureIsNonNil =
|
||||
[](const std::shared_ptr<TLuaResult>& Future) {
|
||||
if (!Future->Error && Future->Result.valid()) {
|
||||
auto Type = Future->Result.get_type();
|
||||
return Type != sol::type::lua_nil && Type != sol::type::none;
|
||||
if (!Future->IsError()) {
|
||||
auto Snapshot = Future->GetDetachedSnapshot();
|
||||
if (Snapshot.Result.V.valueless_by_exception() || std::get_if<std::monostate>(&Snapshot.Result.V) != nullptr) {
|
||||
// no value contained
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -695,7 +701,7 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
|
||||
TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5));
|
||||
size_t Count = 0;
|
||||
for (auto& Future : Futures) {
|
||||
if (!Future->Error) {
|
||||
if (!Future->IsError()) {
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
@@ -705,7 +711,7 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (NonNilFutures.size() == 0) {
|
||||
if (NonNilFutures.empty()) {
|
||||
if (!IgnoreNotACommand) {
|
||||
Application::Console().WriteRaw("Error: Unknown command: '" + cmd + "'. Type 'help' to see a list of valid commands.");
|
||||
}
|
||||
@@ -713,14 +719,16 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
|
||||
std::stringstream Reply;
|
||||
if (NonNilFutures.size() > 1) {
|
||||
for (size_t i = 0; i < NonNilFutures.size(); ++i) {
|
||||
Reply << NonNilFutures[i]->StateId << ": \n"
|
||||
<< LuaAPI::LuaToString(NonNilFutures[i]->Result);
|
||||
auto Snapshot = NonNilFutures[i]->GetDetachedSnapshot();
|
||||
Reply << Snapshot.StateId << ": \n"
|
||||
<< Snapshot.Result;
|
||||
if (i < NonNilFutures.size() - 1) {
|
||||
Reply << "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Reply << LuaAPI::LuaToString(NonNilFutures[0]->Result);
|
||||
auto Snapshot = NonNilFutures[0]->GetDetachedSnapshot();
|
||||
Reply << Snapshot.Result;
|
||||
}
|
||||
Application::Console().WriteRaw(Reply.str());
|
||||
}
|
||||
@@ -801,8 +809,9 @@ void TConsole::InitializeCommandline() {
|
||||
} else {
|
||||
auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared<std::string>(TrimmedCmd), "", "" });
|
||||
Future->WaitUntilReady();
|
||||
if (Future->Error) {
|
||||
beammp_lua_error("error in " + mStateId + ": " + Future->ErrorMessage);
|
||||
if (Future->IsError()) {
|
||||
auto Snapshot = Future->GetSnapshot();
|
||||
beammp_lua_error("error in " + mStateId + ": " + Snapshot.ErrorMessage);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -834,7 +843,7 @@ void TConsole::InitializeCommandline() {
|
||||
if (!mLuaEngine) {
|
||||
beammp_info("Lua not started yet, please try again in a second");
|
||||
} 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
|
||||
if (!std::isalnum(stub[i - 1]) && stub[i - 1] != '_' && stub[i - 1] != '.') {
|
||||
prefix = stub.substr(0, i);
|
||||
|
||||
@@ -181,8 +181,8 @@ std::string THeartbeatThread::GetPlayers() {
|
||||
std::string Return;
|
||||
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
|
||||
ReadLock Lock(mServer.GetClientMutex());
|
||||
if (!ClientPtr.expired()) {
|
||||
Return += ClientPtr.lock()->GetName() + ";";
|
||||
if (auto Client = ClientPtr.lock()) {
|
||||
Return += Client->GetName() + ";";
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// BeamMP, the BeamNG.drive multiplayer mod.
|
||||
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
|
||||
//
|
||||
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "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();
|
||||
}
|
||||
+363
-211
@@ -20,26 +20,29 @@
|
||||
#include "Client.h"
|
||||
#include "Common.h"
|
||||
#include "CustomAssert.h"
|
||||
#include "Env.h"
|
||||
#include "Http.h"
|
||||
#include "LuaAPI.h"
|
||||
#include "Profiling.h"
|
||||
#include "TLuaPlugin.h"
|
||||
#include "TLuaResult.h"
|
||||
#include "sol/object.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <fmt/core.h>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <sol/stack_core.hpp>
|
||||
#include <sol/types.hpp>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
|
||||
TLuaEngine* LuaAPI::MP::Engine;
|
||||
|
||||
static sol::protected_function AddTraceback(sol::state_view StateView, sol::protected_function RawFn);
|
||||
|
||||
TLuaEngine::TLuaEngine()
|
||||
: mResourceServerPath(fs::path(Application::Settings.getAsString(Settings::Key::General_ResourceFolder)) / "Server") {
|
||||
Application::SetSubsystemStatus("LuaEngine", Application::Status::Starting);
|
||||
@@ -60,12 +63,6 @@ TLuaEngine::TLuaEngine()
|
||||
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()() {
|
||||
RegisterThread("LuaEngine");
|
||||
// lua engine main thread
|
||||
@@ -77,8 +74,9 @@ void TLuaEngine::operator()() {
|
||||
auto Futures = TriggerEvent("onInit", "");
|
||||
WaitForAll(Futures, std::chrono::seconds(5));
|
||||
for (const auto& Future : Futures) {
|
||||
if (Future->Error && Future->ErrorMessage != BeamMPFnNotFoundError) {
|
||||
beammp_lua_error("Calling \"onInit\" on \"" + Future->StateId + "\" failed: " + Future->ErrorMessage);
|
||||
auto Snapshot = Future->GetDetachedSnapshot();
|
||||
if (Snapshot.Error && Snapshot.ErrorMessage != BeamMPFnNotFoundError) {
|
||||
beammp_lua_error("Calling \"onInit\" on \"" + Snapshot.StateId + "\" failed: " + Snapshot.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,10 +86,11 @@ void TLuaEngine::operator()() {
|
||||
std::unique_lock Lock(mResultsToCheckMutex);
|
||||
if (!mResultsToCheck.empty()) {
|
||||
mResultsToCheck.remove_if([](const std::shared_ptr<TLuaResult>& Ptr) -> bool {
|
||||
if (Ptr->Ready) {
|
||||
if (Ptr->Error) {
|
||||
if (Ptr->ErrorMessage != BeamMPFnNotFoundError) {
|
||||
beammp_lua_error(Ptr->Function + ": " + Ptr->ErrorMessage);
|
||||
if (Ptr->IsReady()) {
|
||||
auto Snapshot = Ptr->GetDetachedSnapshot();
|
||||
if (Snapshot.Error) {
|
||||
if (Snapshot.ErrorMessage != BeamMPFnNotFoundError) {
|
||||
beammp_lua_error(Snapshot.Function + ": " + Snapshot.ErrorMessage);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -116,7 +115,7 @@ void TLuaEngine::operator()() {
|
||||
std::unique_lock StateLock(mLuaStatesMutex);
|
||||
std::unique_lock Lock2(mResultsToCheckMutex);
|
||||
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) {
|
||||
mResultsToCheck.push_back(Res);
|
||||
mResultsToCheckCond.notify_one();
|
||||
@@ -131,7 +130,13 @@ void TLuaEngine::operator()() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mLuaStates.size() == 0) {
|
||||
bool StatesEmpty = false;
|
||||
{
|
||||
std::unique_lock Lock(mLuaStatesMutex);
|
||||
StatesEmpty = mLuaStates.empty();
|
||||
}
|
||||
|
||||
if (StatesEmpty) {
|
||||
beammp_trace("No Lua states, event loop running extremely sparsely");
|
||||
Application::SleepSafeSeconds(10);
|
||||
} else {
|
||||
@@ -204,8 +209,8 @@ std::unordered_map<std::string /* event name */, std::vector<std::string> /* han
|
||||
return Result;
|
||||
}
|
||||
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> TLuaEngine::Debug_GetStateExecuteQueueForState(TLuaStateId StateId) {
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Result;
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> TLuaEngine::Debug_GetStateExecuteQueueForState(TLuaStateId StateId) {
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> Result;
|
||||
std::unique_lock Lock(mLuaStatesMutex);
|
||||
Result = mLuaStates.at(StateId)->Debug_GetStateExecuteQueue();
|
||||
return Result;
|
||||
@@ -218,16 +223,16 @@ std::vector<TLuaEngine::QueuedFunction> TLuaEngine::Debug_GetStateFunctionQueueF
|
||||
return Result;
|
||||
}
|
||||
|
||||
std::vector<TLuaResult> TLuaEngine::Debug_GetResultsToCheckForState(TLuaStateId StateId) {
|
||||
std::vector<TLuaResult::DetachedSnapshot> TLuaEngine::Debug_GetResultsToCheckForState(TLuaStateId StateId) {
|
||||
std::unique_lock Lock(mResultsToCheckMutex);
|
||||
auto ResultsToCheckCopy = mResultsToCheck;
|
||||
Lock.unlock();
|
||||
std::vector<TLuaResult> Result;
|
||||
std::vector<TLuaResult::DetachedSnapshot> Result;
|
||||
while (!ResultsToCheckCopy.empty()) {
|
||||
auto ResultToCheck = std::move(ResultsToCheckCopy.front());
|
||||
ResultsToCheckCopy.pop_front();
|
||||
if (ResultToCheck->StateId == StateId) {
|
||||
Result.push_back(*ResultToCheck);
|
||||
if (ResultToCheck->OwnerState() == StateId) {
|
||||
Result.push_back(ResultToCheck->GetDetachedSnapshot());
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
@@ -258,7 +263,7 @@ std::vector<std::string> TLuaEngine::StateThreadData::GetStateTableKeys(const st
|
||||
auto globals = mStateView.globals();
|
||||
|
||||
sol::table current = globals;
|
||||
std::vector<std::string> Result {};
|
||||
std::vector<std::string> Result { };
|
||||
|
||||
for (const auto& [key, value] : current) {
|
||||
std::string s = key.as<std::string>();
|
||||
@@ -314,27 +319,30 @@ void TLuaEngine::WaitForAll(std::vector<std::shared_ptr<TLuaResult>>& Results, c
|
||||
size_t ms = 0;
|
||||
std::set<std::string> WarnedResults;
|
||||
|
||||
while (!Result->Ready && !Cancelled) {
|
||||
while (!Result->IsReady() && !Cancelled) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
ms += 10;
|
||||
if (Max.has_value() && std::chrono::milliseconds(ms) > Max.value()) {
|
||||
beammp_trace("'" + Result->Function + "' in '" + Result->StateId + "' did not finish executing in time (took: " + std::to_string(ms) + "ms).");
|
||||
auto Snapshot = Result->GetDetachedSnapshot();
|
||||
beammp_trace("'" + Snapshot.Function + "' in '" + Snapshot.StateId + "' did not finish executing in time (took: " + std::to_string(ms) + "ms).");
|
||||
Cancelled = true;
|
||||
} else if (ms > 1000 * 60) {
|
||||
auto ResultId = Result->StateId + "_" + Result->Function;
|
||||
auto Snapshot = Result->GetDetachedSnapshot();
|
||||
auto ResultId = Snapshot.StateId + "_" + Snapshot.Function;
|
||||
if (WarnedResults.count(ResultId) == 0) {
|
||||
WarnedResults.insert(ResultId);
|
||||
beammp_lua_warn("'" + Result->Function + "' in '" + Result->StateId + "' is taking very long. The event it's handling is too important to discard the result of this handler, but may block this event and possibly the whole lua state.");
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Snapshot = Result->GetDetachedSnapshot();
|
||||
if (Cancelled) {
|
||||
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.");
|
||||
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.");
|
||||
LuaAPI::MP::Engine->ReportErrors({ Result });
|
||||
} else if (Result->Error) {
|
||||
if (Result->ErrorMessage != BeamMPFnNotFoundError) {
|
||||
beammp_lua_error(Result->Function + ": " + Result->ErrorMessage);
|
||||
} else if (Snapshot.Error) {
|
||||
if (Snapshot.ErrorMessage != BeamMPFnNotFoundError) {
|
||||
beammp_lua_error(Snapshot.Function + ": " + Snapshot.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,7 +362,7 @@ bool TLuaEngine::HasState(TLuaStateId StateId) {
|
||||
return mLuaStates.find(StateId) != mLuaStates.end();
|
||||
}
|
||||
|
||||
std::shared_ptr<TLuaResult> TLuaEngine::EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script) {
|
||||
std::shared_ptr<TLuaVoidResult> TLuaEngine::EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script) {
|
||||
std::unique_lock Lock(mLuaStatesMutex);
|
||||
return mLuaStates.at(StateID)->EnqueueScript(Script);
|
||||
}
|
||||
@@ -434,10 +442,11 @@ void TLuaEngine::EnsureStateExists(TLuaStateId StateId, const std::string& Name,
|
||||
mLuaStates[StateId] = std::move(DataPtr);
|
||||
RegisterEvent("onInit", StateId, "onInit");
|
||||
if (!DontCallOnInit) {
|
||||
auto Res = EnqueueFunctionCall(StateId, "onInit", {}, "onInit");
|
||||
auto Res = EnqueueFunctionCall(StateId, "onInit", { }, "onInit");
|
||||
Res->WaitUntilReady();
|
||||
if (Res->Error && Res->ErrorMessage != TLuaEngine::BeamMPFnNotFoundError) {
|
||||
beammp_lua_error("Calling \"onInit\" on \"" + StateId + "\" failed: " + Res->ErrorMessage);
|
||||
auto Snapshot = Res->GetDetachedSnapshot();
|
||||
if (Snapshot.Error && Snapshot.ErrorMessage != TLuaEngine::BeamMPFnNotFoundError) {
|
||||
beammp_lua_error("Calling \"onInit\" on \"" + StateId + "\" failed: " + Snapshot.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,6 +458,7 @@ void TLuaEngine::RegisterEvent(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];
|
||||
}
|
||||
|
||||
@@ -456,7 +466,7 @@ std::vector<sol::object> TLuaEngine::StateThreadData::JsonStringToArray(JsonStri
|
||||
auto LocalTable = Lua_JsonDecode(Str.value).as<std::vector<sol::object>>();
|
||||
for (auto& value : LocalTable) {
|
||||
if (value.is<std::string>() && value.as<std::string>() == BEAMMP_INTERNAL_NIL) {
|
||||
value = sol::object {};
|
||||
value = sol::object { };
|
||||
}
|
||||
}
|
||||
return LocalTable;
|
||||
@@ -496,20 +506,18 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string
|
||||
sol::variadic_results LocalArgs = JsonStringToArray(Str);
|
||||
for (const auto& Handler : MyHandlers) {
|
||||
auto Fn = mStateView[Handler];
|
||||
Fn = AddTraceback(mStateView, Fn);
|
||||
if (Fn.valid()) {
|
||||
auto LuaResult = Fn(LocalArgs);
|
||||
auto Result = std::make_shared<TLuaResult>();
|
||||
auto Result = std::make_shared<TLuaResult>(mStateId, Handler);
|
||||
if (LuaResult.valid()) {
|
||||
Result->Error = false;
|
||||
Result->Result = LuaResult;
|
||||
try {
|
||||
Result->MarkReadySuccess(LuaResult);
|
||||
} catch (const std::exception& e) {
|
||||
Result->MarkReadyError(fmt::format("Call was successful, but result could not be serialized"));
|
||||
}
|
||||
} else {
|
||||
Result->Error = true;
|
||||
sol::error Err = LuaResult;
|
||||
Result->ErrorMessage = Err.what();
|
||||
beammp_errorf("An error occured while executing local event handler \"{}\" for event \"{}\": {}", Handler, EventName, Result->ErrorMessage);
|
||||
Result->MarkReadyError("Function result in TriggerGlobalEvent was invalid");
|
||||
}
|
||||
Result->MarkAsReady();
|
||||
Return.push_back(Result);
|
||||
}
|
||||
}
|
||||
@@ -517,26 +525,60 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string
|
||||
sol::table AsyncEventReturn = StateView.create_table();
|
||||
AsyncEventReturn["ReturnValueImpl"] = Return;
|
||||
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");
|
||||
for (const auto& Value : Vector) {
|
||||
if (!Value->Ready) {
|
||||
if (!Value->IsReady()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
AsyncEventReturn.set_function("GetResults",
|
||||
[&](const sol::table& Self) -> sol::table {
|
||||
sol::state_view StateView(mState);
|
||||
[](const sol::table& Self, sol::this_state State) -> sol::table {
|
||||
sol::state_view StateView(State);
|
||||
sol::table Result = StateView.create_table();
|
||||
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;
|
||||
for (const auto& Value : Vector) {
|
||||
if (!Value->Ready) {
|
||||
if (!Value->IsReady()) {
|
||||
return sol::lua_nil;
|
||||
}
|
||||
Result.set(i, Value->Result);
|
||||
auto Snapshot = Value->GetDetachedSnapshot();
|
||||
Result.set(i, DetachedToLuaObject(DetachedToLuaObject, Snapshot.Result));
|
||||
|
||||
++i;
|
||||
}
|
||||
return Result;
|
||||
@@ -556,8 +598,13 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerLocalEvent(const std::string&
|
||||
Result.set(i, FnRet);
|
||||
++i;
|
||||
} else {
|
||||
sol::error Err = FnRet;
|
||||
beammp_lua_error(std::string("TriggerLocalEvent: ") + Err.what());
|
||||
std::string ErrStr;
|
||||
if (FnRet.get_type() == sol::type::string) {
|
||||
ErrStr = FnRet.get<sol::error>().what();
|
||||
} else {
|
||||
ErrStr = "(unknown error; error object is not inspectable)";
|
||||
}
|
||||
beammp_lua_errorf("TriggerLocalEvent: {}", ErrStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -566,37 +613,37 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerLocalEvent(const std::string&
|
||||
|
||||
sol::table TLuaEngine::StateThreadData::Lua_GetPlayerIdentifiers(int ID) {
|
||||
auto MaybeClient = GetClient(mEngine->Server(), ID);
|
||||
if (MaybeClient && !MaybeClient.value().expired()) {
|
||||
auto IDs = MaybeClient.value().lock()->GetIdentifiers();
|
||||
if (IDs.empty()) {
|
||||
return sol::lua_nil;
|
||||
if (MaybeClient) {
|
||||
if (std::shared_ptr<TClient> Locked = MaybeClient.value().lock()) {
|
||||
auto IDs = Locked->GetIdentifiers();
|
||||
if (IDs.empty()) {
|
||||
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) {
|
||||
auto MaybeClient = GetClient(mEngine->Server(), ID);
|
||||
if (MaybeClient) {
|
||||
return MaybeClient.value().lock()->GetRoles();
|
||||
} else {
|
||||
return sol::nil;
|
||||
if (auto Locked = MaybeClient.value().lock()) {
|
||||
return Locked->GetRoles();
|
||||
}
|
||||
}
|
||||
return sol::nil;
|
||||
}
|
||||
|
||||
|
||||
sol::table TLuaEngine::StateThreadData::Lua_GetPlayers() {
|
||||
sol::table Result = mStateView.create_table();
|
||||
mEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
|
||||
if (!Client.expired()) {
|
||||
auto locked = Client.lock();
|
||||
Result[locked->GetID()] = locked->GetName();
|
||||
if (auto Locked = Client.lock()) {
|
||||
Result[Locked->GetID()] = Locked->GetName();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -606,10 +653,9 @@ sol::table TLuaEngine::StateThreadData::Lua_GetPlayers() {
|
||||
int TLuaEngine::StateThreadData::Lua_GetPlayerIDByName(const std::string& Name) {
|
||||
int Id = -1;
|
||||
mEngine->mServer->ForEachClient([&Id, &Name](std::weak_ptr<TClient> Client) -> bool {
|
||||
if (!Client.expired()) {
|
||||
auto locked = Client.lock();
|
||||
if (locked->GetName() == Name) {
|
||||
Id = locked->GetID();
|
||||
if (auto Locked = Client.lock()) {
|
||||
if (Locked->GetName() == Name) {
|
||||
Id = Locked->GetID();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -646,60 +692,59 @@ sol::table TLuaEngine::StateThreadData::Lua_FS_ListDirectories(const std::string
|
||||
|
||||
std::string TLuaEngine::StateThreadData::Lua_GetPlayerName(int ID) {
|
||||
auto MaybeClient = GetClient(mEngine->Server(), ID);
|
||||
if (MaybeClient && !MaybeClient.value().expired()) {
|
||||
return MaybeClient.value().lock()->GetName();
|
||||
} else {
|
||||
return "";
|
||||
if (MaybeClient) {
|
||||
if (auto Locked = MaybeClient.value().lock()) {
|
||||
return Locked->GetName();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sol::table TLuaEngine::StateThreadData::Lua_GetPlayerVehicles(int ID) {
|
||||
auto MaybeClient = GetClient(mEngine->Server(), ID);
|
||||
if (MaybeClient && !MaybeClient.value().expired()) {
|
||||
auto Client = MaybeClient.value().lock();
|
||||
TClient::TSetOfVehicleData VehicleData;
|
||||
{ // Vehicle Data Lock Scope
|
||||
auto LockedData = Client->GetAllCars();
|
||||
VehicleData = *LockedData.VehicleData;
|
||||
} // End Vehicle Data Lock Scope
|
||||
if (VehicleData.empty()) {
|
||||
return sol::lua_nil;
|
||||
if (MaybeClient) {
|
||||
if (auto Client = MaybeClient.value().lock()) {
|
||||
TClient::TSetOfVehicleData VehicleData;
|
||||
{ // Vehicle Data Lock Scope
|
||||
auto LockedData = Client->GetAllCars();
|
||||
VehicleData = *LockedData.VehicleData;
|
||||
} // End Vehicle Data Lock Scope
|
||||
if (VehicleData.empty()) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
return sol::lua_nil;
|
||||
}
|
||||
|
||||
std::pair<sol::table, std::string> TLuaEngine::StateThreadData::Lua_GetPositionRaw(int PID, int VID) {
|
||||
std::pair<sol::table, std::string> Result;
|
||||
auto MaybeClient = GetClient(mEngine->Server(), PID);
|
||||
if (MaybeClient && !MaybeClient.value().expired()) {
|
||||
auto Client = MaybeClient.value().lock();
|
||||
std::string VehiclePos = Client->GetCarPositionRaw(VID);
|
||||
if (MaybeClient) {
|
||||
if (auto Client = MaybeClient.value().lock()) {
|
||||
std::string VehiclePos = Client->GetCarPositionRaw(VID);
|
||||
|
||||
if (VehiclePos.empty()) {
|
||||
// return std::make_tuple(sol::lua_nil, sol::make_object(StateView, "Vehicle not found"));
|
||||
Result.second = "Vehicle not found";
|
||||
if (VehiclePos.empty()) {
|
||||
Result.second = "Vehicle not found";
|
||||
return Result;
|
||||
}
|
||||
|
||||
sol::table t = Lua_JsonDecode(VehiclePos);
|
||||
if (t == sol::lua_nil) {
|
||||
Result.second = "Packet decode failed";
|
||||
}
|
||||
Result.first = t;
|
||||
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) {
|
||||
@@ -733,10 +778,38 @@ 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) {
|
||||
switch (right.type()) {
|
||||
case nlohmann::detail::value_t::null:
|
||||
AddToTable(table, left, sol::lua_nil_t {});
|
||||
AddToTable(table, left, sol::lua_nil_t { });
|
||||
return;
|
||||
case nlohmann::detail::value_t::object: {
|
||||
auto value = table.create();
|
||||
@@ -864,7 +937,15 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
|
||||
MPTable.set_function("GetPositionRaw", [&](int PID, int VID) -> std::pair<sol::table, std::string> {
|
||||
return Lua_GetPositionRaw(PID, VID);
|
||||
});
|
||||
MPTable.set_function("SendChatMessage", &LuaAPI::MP::SendChatMessage);
|
||||
MPTable.set_function("SendChatMessage", [&](sol::variadic_args Args) {
|
||||
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) {
|
||||
if (Args.size() == 2) {
|
||||
LuaAPI::MP::SendNotification(Args.get<int>(0), Args.get<std::string>(1), "", Args.get<std::string>(1));
|
||||
@@ -876,12 +957,9 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
|
||||
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) {
|
||||
LuaAPI::MP::ConfirmationDialog(ID, Title, Body, Buttons, InteractionID);
|
||||
}
|
||||
));
|
||||
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) {
|
||||
LuaAPI::MP::ConfirmationDialog(ID, Title, Body, Buttons, InteractionID);
|
||||
}));
|
||||
MPTable.set_function("GetPlayers", [&]() -> sol::table {
|
||||
return Lua_GetPlayers();
|
||||
});
|
||||
@@ -925,7 +1003,13 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
|
||||
MPTable.set_function("CancelEventTimer", [&](const std::string& EventName) {
|
||||
mEngine->CancelEventTimers(EventName, mStateId);
|
||||
});
|
||||
MPTable.set_function("Set", &LuaAPI::MP::Set);
|
||||
if (mDisableMPSet) {
|
||||
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);
|
||||
|
||||
auto UtilTable = StateView.create_named_table("Util");
|
||||
@@ -1046,15 +1130,16 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
|
||||
Start();
|
||||
}
|
||||
|
||||
std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueScript(const TLuaChunk& Script) {
|
||||
std::shared_ptr<TLuaVoidResult> TLuaEngine::StateThreadData::EnqueueScript(const TLuaChunk& Script) {
|
||||
std::unique_lock Lock(mStateExecuteQueueMutex);
|
||||
auto Result = std::make_shared<TLuaResult>();
|
||||
auto Result = std::make_shared<TLuaVoidResult>(mStateId);
|
||||
mStateExecuteQueue.push({ Script, 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) {
|
||||
// TODO: Document all this
|
||||
std::unique_lock Lock(mStateFunctionQueueMutex);
|
||||
decltype(mStateFunctionQueue)::iterator Iter = mStateFunctionQueue.end();
|
||||
if (Strategy == CallStrategy::BestEffort) {
|
||||
Iter = std::find_if(mStateFunctionQueue.begin(), mStateFunctionQueue.end(),
|
||||
@@ -1063,10 +1148,7 @@ std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCallFrom
|
||||
});
|
||||
}
|
||||
if (Iter == mStateFunctionQueue.end()) {
|
||||
auto Result = std::make_shared<TLuaResult>();
|
||||
Result->StateId = mStateId;
|
||||
Result->Function = FunctionName;
|
||||
std::unique_lock Lock(mStateFunctionQueueMutex);
|
||||
auto Result = std::make_shared<TLuaResult>(mStateId, FunctionName);
|
||||
mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName });
|
||||
mStateFunctionQueueCond.notify_all();
|
||||
return Result;
|
||||
@@ -1076,9 +1158,7 @@ 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) {
|
||||
auto Result = std::make_shared<TLuaResult>();
|
||||
Result->StateId = mStateId;
|
||||
Result->Function = FunctionName;
|
||||
auto Result = std::make_shared<TLuaResult>(mStateId, FunctionName);
|
||||
std::unique_lock Lock(mStateFunctionQueueMutex);
|
||||
mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName });
|
||||
mStateFunctionQueueCond.notify_all();
|
||||
@@ -1089,21 +1169,6 @@ void TLuaEngine::StateThreadData::RegisterEvent(const std::string& EventName, co
|
||||
mEngine->RegisterEvent(EventName, mStateId, FunctionName);
|
||||
}
|
||||
|
||||
static sol::protected_function AddTraceback(sol::state_view StateView, sol::protected_function RawFn) {
|
||||
StateView["INTERNAL_ERROR_HANDLER"] = [](lua_State *L) {
|
||||
auto Error = sol::stack::get<std::optional<std::string>>(L);
|
||||
std::string ErrorString = "<Unknown error>";
|
||||
if (Error.has_value()) {
|
||||
ErrorString = Error.value();
|
||||
}
|
||||
auto DebugTracebackFn = sol::state_view(L).globals().get<sol::table>("debug").get<sol::protected_function>("traceback");
|
||||
// 2 = start collecting the trace one above the current function (1=current function)
|
||||
std::string Traceback = DebugTracebackFn(ErrorString, 2);
|
||||
return sol::stack::push(L, Traceback);
|
||||
};
|
||||
return sol::protected_function(RawFn, StateView["INTERNAL_ERROR_HANDLER"]);
|
||||
}
|
||||
|
||||
void TLuaEngine::StateThreadData::operator()() {
|
||||
RegisterThread("Lua:" + mStateId);
|
||||
while (!Application::IsShuttingDown()) {
|
||||
@@ -1142,14 +1207,14 @@ void TLuaEngine::StateThreadData::operator()() {
|
||||
sol::state_view StateView(mState);
|
||||
auto Res = StateView.safe_script(*S.first.Content, sol::script_pass_on_error, S.first.FileName);
|
||||
if (Res.valid()) {
|
||||
S.second->Error = false;
|
||||
S.second->Result = std::move(Res);
|
||||
// Script-load completion should not serialize the script's return value.
|
||||
// A loaded chunk may legally return non-serializable Lua values such as
|
||||
// functions or function tables. For this reason, we don't pass anything
|
||||
// to the result here.
|
||||
S.second->MarkReadySuccess();
|
||||
} else {
|
||||
S.second->Error = true;
|
||||
sol::error Err = Res;
|
||||
S.second->ErrorMessage = Err.what();
|
||||
S.second->MarkReadyError(std::move(Res));
|
||||
}
|
||||
S.second->MarkAsReady();
|
||||
}
|
||||
}
|
||||
{ // StateFunctionQueue Scope
|
||||
@@ -1166,59 +1231,54 @@ void TLuaEngine::StateThreadData::operator()() {
|
||||
auto& Result = TheQueuedFunction.Result;
|
||||
auto Args = TheQueuedFunction.Args;
|
||||
// TODO: Use TheQueuedFunction.EventName for errors, warnings, etc
|
||||
Result->StateId = mStateId;
|
||||
Result->SetOwnerState(mStateId);
|
||||
sol::state_view StateView(mState);
|
||||
auto RawFn = StateView[FnName];
|
||||
if (RawFn.valid() && RawFn.get_type() == sol::type::function) {
|
||||
auto Fn = StateView[FnName];
|
||||
if (Fn.valid() && Fn.get_type() == sol::type::function) {
|
||||
std::vector<sol::object> LuaArgs;
|
||||
for (const auto& Arg : Args) {
|
||||
if (Arg.valueless_by_exception()) {
|
||||
continue;
|
||||
}
|
||||
switch (Arg.index()) {
|
||||
case TLuaType::String:
|
||||
LuaArgs.push_back(sol::make_object(StateView, std::get<std::string>(Arg)));
|
||||
break;
|
||||
case TLuaType::Int:
|
||||
LuaArgs.push_back(sol::make_object(StateView, std::get<int>(Arg)));
|
||||
break;
|
||||
case TLuaType::Json: {
|
||||
auto LocalArgs = JsonStringToArray(std::get<JsonString>(Arg));
|
||||
LuaArgs.insert(LuaArgs.end(), LocalArgs.begin(), LocalArgs.end());
|
||||
break;
|
||||
}
|
||||
case TLuaType::Bool:
|
||||
LuaArgs.push_back(sol::make_object(StateView, std::get<bool>(Arg)));
|
||||
break;
|
||||
case TLuaType::StringStringMap: {
|
||||
auto Map = std::get<std::unordered_map<std::string, std::string>>(Arg);
|
||||
auto Table = StateView.create_table();
|
||||
for (const auto& [k, v] : Map) {
|
||||
Table[k] = v;
|
||||
std::visit([&LuaArgs, &StateView, this](const auto& arg) {
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<T, std::string>) {
|
||||
LuaArgs.push_back(sol::make_object(StateView, arg));
|
||||
} else if constexpr (std::is_same_v<T, int>) {
|
||||
LuaArgs.push_back(sol::make_object(StateView, arg));
|
||||
} else if constexpr (std::is_same_v<T, bool>) {
|
||||
LuaArgs.push_back(sol::make_object(StateView, arg));
|
||||
} else if constexpr (std::is_same_v<T, JsonString>) {
|
||||
auto LocalArgs = JsonStringToArray(arg);
|
||||
LuaArgs.insert(LuaArgs.end(), LocalArgs.begin(), LocalArgs.end());
|
||||
} else if constexpr (std::is_same_v<T, std::monostate>) {
|
||||
beammp_lua_error("Unknown argument type, passed as nil");
|
||||
LuaArgs.push_back(sol::lua_nil_t());
|
||||
} else if constexpr (std::is_same_v<T, std::unordered_map<std::string, std::string>>) {
|
||||
auto Table = StateView.create_table();
|
||||
for (const auto& [k, v] : arg) {
|
||||
Table[k] = v;
|
||||
}
|
||||
LuaArgs.push_back(Table);
|
||||
} else if constexpr (std::is_same_v<T, float>) {
|
||||
LuaArgs.push_back(sol::make_object(StateView, arg));
|
||||
} else {
|
||||
static_assert(AlwaysFalseV<T>, "unhandled variant");
|
||||
}
|
||||
LuaArgs.push_back(sol::make_object(StateView, Table));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
beammp_error("Unknown argument type, passed as nil");
|
||||
break;
|
||||
}
|
||||
}, Arg);
|
||||
}
|
||||
auto Fn = AddTraceback(StateView, RawFn);
|
||||
auto Res = Fn(sol::as_args(LuaArgs));
|
||||
if (Res.valid()) {
|
||||
Result->Error = false;
|
||||
Result->Result = std::move(Res);
|
||||
try {
|
||||
Result->MarkReadySuccess(std::move(Res));
|
||||
} catch (const std::exception& e) {
|
||||
Result->MarkReadyError(fmt::format("Call was successful, but result could not be serialized"));
|
||||
}
|
||||
} else {
|
||||
Result->Error = true;
|
||||
sol::error Err = Res;
|
||||
Result->ErrorMessage = Err.what();
|
||||
Result->MarkReadyError(std::move(Res));
|
||||
}
|
||||
Result->MarkAsReady();
|
||||
} else {
|
||||
Result->Error = true;
|
||||
Result->ErrorMessage = BeamMPFnNotFoundError; // special error kind that we can ignore later
|
||||
Result->MarkAsReady();
|
||||
Result->MarkReadyError(BeamMPFnNotFoundError);
|
||||
}
|
||||
auto ProfEnd = prof::now();
|
||||
auto ProfDuration = prof::duration(ProfStart, ProfEnd);
|
||||
@@ -1228,7 +1288,7 @@ void TLuaEngine::StateThreadData::operator()() {
|
||||
}
|
||||
}
|
||||
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> TLuaEngine::StateThreadData::Debug_GetStateExecuteQueue() {
|
||||
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> TLuaEngine::StateThreadData::Debug_GetStateExecuteQueue() {
|
||||
std::unique_lock Lock(mStateExecuteQueueMutex);
|
||||
return mStateExecuteQueue;
|
||||
}
|
||||
@@ -1271,21 +1331,6 @@ void TLuaEngine::StateThreadData::AddPath(const fs::path& 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)
|
||||
: Content(Content)
|
||||
, FileName(FileName)
|
||||
@@ -1300,3 +1345,110 @@ bool TLuaEngine::TimedEvent::Expired() {
|
||||
void TLuaEngine::TimedEvent::Reset() {
|
||||
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();
|
||||
}
|
||||
|
||||
+4
-3
@@ -44,7 +44,7 @@ TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const
|
||||
std::transform(secondStr.begin(), secondStr.end(), secondStr.begin(), ::tolower);
|
||||
return firstStr < secondStr;
|
||||
});
|
||||
std::vector<std::pair<fs::path, std::shared_ptr<TLuaResult>>> ResultsToCheck;
|
||||
std::vector<std::pair<fs::path, std::shared_ptr<TLuaVoidResult>>> ResultsToCheck;
|
||||
for (const auto& Entry : Entries) {
|
||||
// read in entire file
|
||||
try {
|
||||
@@ -63,8 +63,9 @@ TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const
|
||||
}
|
||||
for (auto& Result : ResultsToCheck) {
|
||||
Result.second->WaitUntilReady();
|
||||
if (Result.second->Error) {
|
||||
beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Result.second->ErrorMessage);
|
||||
auto Snapshot = Result.second->GetSnapshot();
|
||||
if (Snapshot.Error) {
|
||||
beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Snapshot.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
// 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));
|
||||
}
|
||||
+392
-131
@@ -20,6 +20,7 @@
|
||||
#include "Client.h"
|
||||
#include "Common.h"
|
||||
#include "LuaAPI.h"
|
||||
#include "TConnectionLimiter.h"
|
||||
#include "THeartbeatThread.h"
|
||||
#include "TLuaEngine.h"
|
||||
#include "TScopedTimer.h"
|
||||
@@ -27,17 +28,27 @@
|
||||
#include <CustomAssert.h>
|
||||
#include <Http.h>
|
||||
#include <array>
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/address_v4.hpp>
|
||||
#include <boost/asio/ip/address_v6.hpp>
|
||||
#include <boost/asio/ip/v6_only.hpp>
|
||||
#include <boost/asio/socket_base.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <variant>
|
||||
#include <zlib.h>
|
||||
|
||||
typedef boost::asio::detail::socket_option::integer<SOL_SOCKET, SO_RCVTIMEO> rcv_timeout_option;
|
||||
|
||||
static constexpr uint8_t MAX_CONCURRENT_CONNECTIONS = 10;
|
||||
static constexpr uint8_t MAX_GLOBAL_CONNECTIONS = 128;
|
||||
static constexpr uint8_t READ_TIMEOUT_S = 10; // seconds
|
||||
|
||||
std::vector<uint8_t> StringToVector(const std::string& Str) {
|
||||
return std::vector<uint8_t>(Str.data(), Str.data() + Str.size());
|
||||
}
|
||||
@@ -51,22 +62,42 @@ static void CompressProperly(std::vector<uint8_t>& Data) {
|
||||
Data = CombinedData;
|
||||
}
|
||||
|
||||
// for unit-tests, otherwise unused
|
||||
static bool OpenLoopbackSocketPair(io_context& IoCtx, ip::tcp::socket& ClientSocket, ip::tcp::socket& ServerSocket, boost::system::error_code& Ec) {
|
||||
ip::tcp::acceptor Acceptor(IoCtx);
|
||||
Acceptor.open(ip::tcp::v4(), Ec);
|
||||
if (Ec) {
|
||||
return true;
|
||||
}
|
||||
Acceptor.bind(ip::tcp::endpoint(ip::address_v4::loopback(), 0), Ec);
|
||||
if (Ec) {
|
||||
return true;
|
||||
}
|
||||
Acceptor.listen(socket_base::max_listen_connections, Ec);
|
||||
if (Ec) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto Port = Acceptor.local_endpoint(Ec).port();
|
||||
if (Ec) {
|
||||
return true;
|
||||
}
|
||||
ClientSocket.connect(ip::tcp::endpoint(ip::address_v4::loopback(), Port), Ec);
|
||||
if (Ec) {
|
||||
return true;
|
||||
}
|
||||
Acceptor.accept(ServerSocket, Ec);
|
||||
return true;
|
||||
}
|
||||
|
||||
TNetwork::TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& ResourceManager)
|
||||
: mServer(Server)
|
||||
, mPPSMonitor(PPSMonitor)
|
||||
, mUDPSock(Server.IoCtx())
|
||||
, mResourceManager(ResourceManager) {
|
||||
, mResourceManager(ResourceManager)
|
||||
, mConnectionLimiter(MAX_CONCURRENT_CONNECTIONS, MAX_GLOBAL_CONNECTIONS) {
|
||||
Application::SetSubsystemStatus("TCPNetwork", Application::Status::Starting);
|
||||
Application::SetSubsystemStatus("UDPNetwork", Application::Status::Starting);
|
||||
Application::RegisterShutdownHandler([&] {
|
||||
beammp_debug("Kicking all players due to shutdown");
|
||||
Server.ForEachClient([&](std::weak_ptr<TClient> client) -> bool {
|
||||
if (!client.expired()) {
|
||||
ClientKick(*client.lock(), "Server shutdown");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
Application::RegisterShutdownHandler([&] {
|
||||
Application::SetSubsystemStatus("UDPNetwork", Application::Status::ShuttingDown);
|
||||
if (mUDPThread.joinable()) {
|
||||
@@ -89,14 +120,14 @@ void TNetwork::UDPServerMain() {
|
||||
RegisterThread("UDPServer");
|
||||
|
||||
boost::system::error_code ec;
|
||||
auto address = ip::make_address(Application::Settings.getAsString(Settings::Key::General_IP), ec);
|
||||
auto address = boost::asio::ip::make_address(Application::Settings.getAsString(Settings::Key::General_IP), ec);
|
||||
|
||||
if (ec) {
|
||||
beammp_errorf("Failed to parse IP: {}", ec.message());
|
||||
Application::GracefullyShutdown();
|
||||
}
|
||||
|
||||
ip::udp::endpoint UdpListenEndpoint(address, Application::Settings.getAsInt(Settings::Key::General_Port));
|
||||
boost::asio::ip::udp::endpoint UdpListenEndpoint(address, Application::Settings.getAsInt(Settings::Key::General_Port));
|
||||
|
||||
mUDPSock.open(UdpListenEndpoint.protocol(), ec);
|
||||
if (ec) {
|
||||
@@ -121,13 +152,13 @@ void TNetwork::UDPServerMain() {
|
||||
+ std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) + (" Clients"));
|
||||
while (!Application::IsShuttingDown()) {
|
||||
try {
|
||||
ip::udp::endpoint remote_client_ep {};
|
||||
boost::asio::ip::udp::endpoint remote_client_ep { };
|
||||
std::vector<uint8_t> Data = UDPRcvFromClient(remote_client_ep);
|
||||
if (Data.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (Data.size() == 1 && Data.at(0) == 'P') {
|
||||
mUDPSock.send_to(const_buffer("P", 1), remote_client_ep, {}, ec);
|
||||
mUDPSock.send_to(boost::asio::const_buffer("P", 1), remote_client_ep, { }, ec);
|
||||
// ignore errors
|
||||
(void)ec;
|
||||
continue;
|
||||
@@ -141,14 +172,14 @@ void TNetwork::UDPServerMain() {
|
||||
std::shared_ptr<TClient> Client;
|
||||
{
|
||||
ReadLock Lock(mServer.GetClientMutex());
|
||||
if (!ClientPtr.expired()) {
|
||||
Client = ClientPtr.lock();
|
||||
if (auto Locked = ClientPtr.lock()) {
|
||||
Client = std::move(Locked);
|
||||
} else
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Client->GetID() == ID) {
|
||||
if (Client->GetUDPAddr() == ip::udp::endpoint {} && !Client->IsUDPConnected() && !Client->GetMagic().empty()) {
|
||||
if (Client->GetUDPAddr() == boost::asio::ip::udp::endpoint { } && !Client->IsUDPConnected() && !Client->GetMagic().empty()) {
|
||||
if (Data.size() != 66) {
|
||||
beammp_debugf("Invalid size for UDP value. IP: {} ID: {}", remote_client_ep.address().to_string(), ID);
|
||||
return false;
|
||||
@@ -161,7 +192,7 @@ void TNetwork::UDPServerMain() {
|
||||
return false;
|
||||
}
|
||||
|
||||
Client->SetMagic({});
|
||||
Client->SetMagic({ });
|
||||
Client->SetUDPAddr(remote_client_ep);
|
||||
Client->SetIsUDPConnected(true);
|
||||
return false;
|
||||
@@ -188,16 +219,16 @@ void TNetwork::TCPServerMain() {
|
||||
RegisterThread("TCPServer");
|
||||
|
||||
boost::system::error_code ec;
|
||||
auto address = ip::make_address(Application::Settings.getAsString(Settings::Key::General_IP), ec);
|
||||
auto address = boost::asio::ip::make_address(Application::Settings.getAsString(Settings::Key::General_IP), ec);
|
||||
if (ec) {
|
||||
beammp_errorf("Failed to parse IP: {}", ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
ip::tcp::endpoint ListenEp(address,
|
||||
boost::asio::ip::tcp::endpoint ListenEp(address,
|
||||
uint16_t(Application::Settings.getAsInt(Settings::Key::General_Port)));
|
||||
|
||||
ip::tcp::socket Listener(mServer.IoCtx());
|
||||
boost::asio::ip::tcp::socket Listener(mServer.IoCtx());
|
||||
Listener.open(ListenEp.protocol(), ec);
|
||||
if (ec) {
|
||||
beammp_errorf("Failed to open socket: {}", ec.message());
|
||||
@@ -213,7 +244,7 @@ void TNetwork::TCPServerMain() {
|
||||
beammp_warnf("WARNING: On FreeBSD, for IPv4 to work, you must run `sysctl net.inet6.ip6.v6only=0`!");
|
||||
beammp_debugf("This is due to an annoying detail in the *BSDs: In the name of security, unsetting the IPV6_V6ONLY option does not work by default (but does not fail???), as it allows IPv4 mapped IPv6 like ::ffff:127.0.0.1, which they deem a security issue. For more information, see RFC 2553, section 3.7.");
|
||||
#endif
|
||||
socket_base::linger LingerOpt {};
|
||||
socket_base::linger LingerOpt { };
|
||||
LingerOpt.enabled(false);
|
||||
Listener.set_option(LingerOpt, ec);
|
||||
if (ec) {
|
||||
@@ -222,7 +253,7 @@ void TNetwork::TCPServerMain() {
|
||||
ec.message());
|
||||
}
|
||||
|
||||
ip::tcp::acceptor Acceptor(mServer.IoCtx(), ListenEp);
|
||||
boost::asio::ip::tcp::acceptor Acceptor(mServer.IoCtx(), ListenEp);
|
||||
Acceptor.listen(socket_base::max_listen_connections, ec);
|
||||
if (ec) {
|
||||
beammp_errorf("listen() failed, which is needed for the server to operate. "
|
||||
@@ -239,13 +270,26 @@ void TNetwork::TCPServerMain() {
|
||||
beammp_debug("shutdown during TCP wait for accept loop");
|
||||
break;
|
||||
}
|
||||
ip::tcp::endpoint ClientEp;
|
||||
ip::tcp::socket ClientSocket = Acceptor.accept(ClientEp, ec);
|
||||
boost::asio::ip::tcp::socket ClientSocket = Acceptor.accept(ec);
|
||||
if (ec) {
|
||||
beammp_errorf("Failed to accept() new client: {}", ec.message());
|
||||
continue;
|
||||
}
|
||||
boost::asio::ip::tcp::endpoint ClientEp = ClientSocket.remote_endpoint(ec);
|
||||
if (ec) {
|
||||
beammp_errorf("Accepted socket but failed to query remote endpoint for IP address: {}", ec.message());
|
||||
continue;
|
||||
}
|
||||
std::string ClientIP = ClientEp.address().to_string();
|
||||
auto MaybeGuard = mConnectionLimiter.TryAcquire(ClientIP);
|
||||
if (!MaybeGuard.has_value()) {
|
||||
beammp_debugf("Connection rejected for {} due to the global or concurrent connection limit", ClientIP);
|
||||
continue;
|
||||
}
|
||||
// move-swap to avoid copy ctor (deleted)
|
||||
auto Guard = std::move(MaybeGuard.value());
|
||||
TConnection Conn { std::move(ClientSocket), ClientEp };
|
||||
std::thread ID(&TNetwork::Identify, this, std::move(Conn));
|
||||
std::thread ID(&TNetwork::Identify, this, std::move(Conn), std::move(Guard));
|
||||
ID.detach(); // TODO: Add to a queue and attempt to join periodically
|
||||
} catch (const std::exception& e) {
|
||||
beammp_errorf("Exception in accept routine: {}", e.what());
|
||||
@@ -258,15 +302,17 @@ void TNetwork::TCPServerMain() {
|
||||
#include "Json.h"
|
||||
namespace json = rapidjson;
|
||||
|
||||
void TNetwork::Identify(TConnection&& RawConnection) {
|
||||
void TNetwork::Identify(TConnection&& RawConnection, TConnectionLimiter::TGuard&& Guard) {
|
||||
RegisterThreadAuto();
|
||||
char Code;
|
||||
|
||||
boost::system::error_code ec;
|
||||
read(RawConnection.Socket, buffer(&Code, 1), ec);
|
||||
boost::system::error_code ec = ReadWithTimeout(RawConnection, &Code, 1, std::chrono::seconds(READ_TIMEOUT_S));
|
||||
if (ec) {
|
||||
// TODO: is this right?!
|
||||
beammp_debug("Error occured reading code");
|
||||
RawConnection.Socket.shutdown(socket_base::shutdown_both, ec);
|
||||
// TODO: is this right too?
|
||||
RawConnection.Socket.close(ec);
|
||||
return;
|
||||
}
|
||||
std::shared_ptr<TClient> Client { nullptr };
|
||||
@@ -277,8 +323,7 @@ void TNetwork::Identify(TConnection&& RawConnection) {
|
||||
beammp_errorf("Old download packet detected - the client is wildly out of date, this will be ignored");
|
||||
return;
|
||||
} else if (Code == 'P') {
|
||||
boost::system::error_code ec;
|
||||
write(RawConnection.Socket, buffer("P"), ec);
|
||||
boost::asio::write(RawConnection.Socket, boost::asio::buffer("P"), ec);
|
||||
return;
|
||||
} else if (Code == 'I') {
|
||||
const std::string Data = Application::Settings.getAsBool(Settings::Key::General_InformationPacket) ? THeartbeatThread::lastCall : "";
|
||||
@@ -290,14 +335,14 @@ void TNetwork::Identify(TConnection&& RawConnection) {
|
||||
std::memcpy(ToSend.data() + sizeof(Size), Data.data(), Data.size());
|
||||
|
||||
boost::system::error_code ec;
|
||||
write(RawConnection.Socket, buffer(ToSend), ec);
|
||||
boost::asio::write(RawConnection.Socket, boost::asio::buffer(ToSend), ec);
|
||||
} else {
|
||||
beammp_errorf("Invalid code got in Identify: '{}'", Code);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
beammp_errorf("Error during handling of code {} - client left in invalid state, closing socket: {}", Code, e.what());
|
||||
boost::system::error_code ec;
|
||||
RawConnection.Socket.shutdown(socket_base::shutdown_both, ec);
|
||||
RawConnection.Socket.shutdown(boost::asio::socket_base::shutdown_both, ec);
|
||||
if (ec) {
|
||||
beammp_debugf("Failed to shutdown client socket: {}", ec.message());
|
||||
}
|
||||
@@ -308,8 +353,6 @@ void TNetwork::Identify(TConnection&& RawConnection) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
std::string HashPassword(const std::string& str) {
|
||||
std::stringstream ret;
|
||||
unsigned char* hash = SHA256(reinterpret_cast<const unsigned char*>(str.c_str()), str.length(), nullptr);
|
||||
@@ -337,7 +380,12 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
|
||||
|
||||
beammp_info("Identifying new ClientConnection...");
|
||||
|
||||
auto Data = TCPRcv(*Client);
|
||||
auto Data = TCPRcv(*Client, true);
|
||||
if (Data.empty()) {
|
||||
beammp_debug("Authentication failed: did not receive version packet");
|
||||
ClientKick(*Client, "Connection closed during version handshake");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
constexpr std::string_view VC = "VC";
|
||||
if (Data.size() > 3 && std::equal(Data.begin(), Data.begin() + VC.size(), VC.begin(), VC.end())) {
|
||||
@@ -359,7 +407,12 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
|
||||
// TODO: handle
|
||||
}
|
||||
|
||||
Data = TCPRcv(*Client);
|
||||
Data = TCPRcv(*Client, true);
|
||||
if (Data.empty()) {
|
||||
beammp_debug("Authentication failed: did not receive auth key packet");
|
||||
ClientKick(*Client, "Connection closed during authentication");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (Data.size() > 50) {
|
||||
ClientKick(*Client, "Invalid Key (too long)!");
|
||||
@@ -367,11 +420,15 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
|
||||
}
|
||||
|
||||
std::string Key(reinterpret_cast<const char*>(Data.data()), Data.size());
|
||||
if (Key.empty()) {
|
||||
ClientKick(*Client, "Invalid Key (empty)!");
|
||||
return nullptr;
|
||||
}
|
||||
std::string AuthKey = Application::Settings.getAsString(Settings::Key::General_AuthKey);
|
||||
std::string ClientIp = Client->GetIdentifiers().at("ip");
|
||||
|
||||
nlohmann::json AuthReq {};
|
||||
std::string AuthResStr {};
|
||||
nlohmann::json AuthReq { };
|
||||
std::string AuthResStr { };
|
||||
try {
|
||||
AuthReq = nlohmann::json {
|
||||
{ "key", Key },
|
||||
@@ -395,7 +452,7 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
|
||||
try {
|
||||
nlohmann::json AuthRes = nlohmann::json::parse(AuthResStr);
|
||||
|
||||
if (AuthRes["username"].is_string() && AuthRes["roles"].is_string()
|
||||
if (AuthRes["username"].is_string() && AuthRes["username"].size() > 0 && AuthRes["roles"].is_string()
|
||||
&& AuthRes["guest"].is_boolean() && AuthRes["identifiers"].is_array()) {
|
||||
|
||||
Client->SetName(AuthRes["username"]);
|
||||
@@ -423,13 +480,13 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
|
||||
std::shared_ptr<TClient> Cl;
|
||||
{
|
||||
ReadLock Lock(mServer.GetClientMutex());
|
||||
if (!ClientPtr.expired()) {
|
||||
Cl = ClientPtr.lock();
|
||||
if (auto Locked = ClientPtr.lock()) {
|
||||
Cl = std::move(Locked);
|
||||
} else
|
||||
return true;
|
||||
}
|
||||
if (Cl->GetName() == Client->GetName() && Cl->IsGuest() == Client->IsGuest()) {
|
||||
Cl->Disconnect("Stale Client (not a real player)");
|
||||
DisconnectClient(Cl, "Stale Client (not a real player)");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -442,23 +499,31 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
|
||||
bool BypassLimit = false;
|
||||
|
||||
for (const auto& Result : Futures) {
|
||||
if (!Result->Error && Result->Result.is<int>()) {
|
||||
auto Res = Result->Result.as<int>();
|
||||
auto Snapshot = Result->GetDetachedSnapshot();
|
||||
if (!Snapshot.Error) {
|
||||
const int* MaybeInt = std::get_if<int>(&Snapshot.Result.V);
|
||||
if (MaybeInt != nullptr) {
|
||||
auto Res = *MaybeInt;
|
||||
|
||||
if (Res == 1) {
|
||||
NotAllowed = true;
|
||||
break;
|
||||
} else if (Res == 2) {
|
||||
BypassLimit = true;
|
||||
if (Res == 1) {
|
||||
NotAllowed = true;
|
||||
break;
|
||||
} else if (Res == 2) {
|
||||
BypassLimit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string Reason;
|
||||
bool NotAllowedWithReason = std::any_of(Futures.begin(), Futures.end(),
|
||||
[&Reason](const std::shared_ptr<TLuaResult>& Result) -> bool {
|
||||
if (!Result->Error && Result->Result.is<std::string>()) {
|
||||
Reason = Result->Result.as<std::string>();
|
||||
return true;
|
||||
auto Snapshot = Result->GetDetachedSnapshot();
|
||||
if (!Snapshot.Error) {
|
||||
const std::string* MaybeStr = std::get_if<std::string>(&Snapshot.Result.V);
|
||||
if (MaybeStr != nullptr) {
|
||||
Reason = *MaybeStr;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -469,8 +534,8 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
|
||||
}
|
||||
|
||||
if (!NotAllowed && !NotAllowedWithReason && mServer.ClientCount() >= size_t(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) && !BypassLimit) {
|
||||
NotAllowedWithReason = true;
|
||||
Reason = "Server full!";
|
||||
NotAllowedWithReason = true;
|
||||
Reason = "Server full!";
|
||||
}
|
||||
|
||||
if (NotAllowedWithReason) {
|
||||
@@ -492,7 +557,7 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
|
||||
return Client;
|
||||
}
|
||||
|
||||
std::shared_ptr<TClient> TNetwork::CreateClient(ip::tcp::socket&& TCPSock) {
|
||||
std::shared_ptr<TClient> TNetwork::CreateClient(boost::asio::ip::tcp::socket&& TCPSock) {
|
||||
auto c = std::make_shared<TClient>(mServer, std::move(TCPSock));
|
||||
return c;
|
||||
}
|
||||
@@ -525,55 +590,71 @@ bool TNetwork::TCPSend(TClient& c, const std::vector<uint8_t>& Data, bool IsSync
|
||||
std::memcpy(ToSend.data(), &Size, sizeof(Size));
|
||||
std::memcpy(ToSend.data() + sizeof(Size), Data.data(), Data.size());
|
||||
boost::system::error_code ec;
|
||||
write(Sock, buffer(ToSend), ec);
|
||||
boost::asio::write(Sock, boost::asio::buffer(ToSend), ec);
|
||||
if (ec) {
|
||||
beammp_debugf("write(): {}", ec.message());
|
||||
c.Disconnect("write() failed");
|
||||
DisconnectClient(c, "write() failed");
|
||||
return false;
|
||||
}
|
||||
c.UpdatePingTime();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> TNetwork::TCPRcv(TClient& c) {
|
||||
std::vector<uint8_t> TNetwork::TCPRcv(TClient& c, bool WithTimeout) {
|
||||
if (c.IsDisconnected()) {
|
||||
beammp_error("Client disconnected, cancelling TCPRcv");
|
||||
return {};
|
||||
return { };
|
||||
}
|
||||
|
||||
int32_t Header {};
|
||||
int32_t Header { };
|
||||
auto& Sock = c.GetTCPSock();
|
||||
|
||||
boost::system::error_code ec;
|
||||
std::array<uint8_t, sizeof(Header)> HeaderData;
|
||||
read(Sock, buffer(HeaderData), ec);
|
||||
if (WithTimeout) {
|
||||
ec = ReadWithTimeout(Sock, HeaderData.data(), HeaderData.size(), std::chrono::seconds(READ_TIMEOUT_S));
|
||||
} else {
|
||||
boost::asio::read(Sock, boost::asio::buffer(HeaderData), ec);
|
||||
}
|
||||
if (ec) {
|
||||
// TODO: handle this case (read failed)
|
||||
beammp_debugf("TCPRcv: Reading header failed: {}", ec.message());
|
||||
return {};
|
||||
return { };
|
||||
}
|
||||
Header = *reinterpret_cast<int32_t*>(HeaderData.data());
|
||||
|
||||
if (Header < 0) {
|
||||
ClientKick(c, "Invalid packet - header negative");
|
||||
beammp_errorf("Client {} send negative TCP header, ignoring packet", c.GetID());
|
||||
return {};
|
||||
return { };
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Data;
|
||||
// TODO: This is arbitrary, this needs to be handled another way
|
||||
if (Header < int32_t(100 * MB)) {
|
||||
bool isUnauthenticated = c.GetName().empty();
|
||||
int32_t maxHeaderSize = isUnauthenticated ? 4096 : int32_t(100 * MB);
|
||||
if (Header < maxHeaderSize) {
|
||||
Data.resize(Header);
|
||||
} else {
|
||||
ClientKick(c, "Header size limit exceeded");
|
||||
beammp_warn("Client " + c.GetName() + " (" + std::to_string(c.GetID()) + ") sent header of >100MB - assuming malicious intent and disconnecting the client.");
|
||||
return {};
|
||||
beammp_warn("Client " + c.GetName() + " (" + std::to_string(c.GetID()) + ") sent header larger than expected - assuming malicious intent and disconnecting the client.");
|
||||
return { };
|
||||
}
|
||||
std::size_t N = 0;
|
||||
if (WithTimeout) {
|
||||
if (!Data.empty()) {
|
||||
ec = ReadWithTimeout(Sock, Data.data(), Data.size(), std::chrono::seconds(READ_TIMEOUT_S));
|
||||
if (!ec) {
|
||||
N = Data.size();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
N = boost::asio::read(Sock, boost::asio::buffer(Data), ec);
|
||||
}
|
||||
auto N = read(Sock, buffer(Data), ec);
|
||||
if (ec) {
|
||||
// TODO: handle this case properly
|
||||
beammp_debugf("TCPRcv: Reading data failed: {}", ec.message());
|
||||
return {};
|
||||
return { };
|
||||
}
|
||||
|
||||
if (N != Header) {
|
||||
@@ -585,7 +666,7 @@ std::vector<uint8_t> TNetwork::TCPRcv(TClient& c) {
|
||||
Data.erase(Data.begin(), Data.begin() + ABG.size());
|
||||
try {
|
||||
return DeComp(Data);
|
||||
} catch (const InvalidDataError& ) {
|
||||
} catch (const InvalidDataError&) {
|
||||
beammp_errorf("Failed to decompress packet from a client. The receive failed and the client may be disconnected as a result");
|
||||
// return empty -> error
|
||||
return std::vector<uint8_t>();
|
||||
@@ -604,13 +685,29 @@ void TNetwork::ClientKick(TClient& c, const std::string& R) {
|
||||
if (!TCPSend(c, StringToVector("K" + R))) {
|
||||
beammp_debugf("tried to kick player '{}' (id {}), but was already disconnected", c.GetName(), c.GetID());
|
||||
}
|
||||
c.Disconnect("Kicked");
|
||||
DisconnectClient(c, "Kicked");
|
||||
}
|
||||
|
||||
void TNetwork::DisconnectClient(const std::weak_ptr<TClient>& c, const std::string& R) {
|
||||
if (auto locked = c.lock()) {
|
||||
DisconnectClient(*locked, R);
|
||||
} else {
|
||||
beammp_debugf("Tried to disconnect a non existant client with reason: {}", R);
|
||||
}
|
||||
}
|
||||
|
||||
void TNetwork::DisconnectClient(TClient& c, const std::string& R) {
|
||||
// Keep this unconditional; TClient::Disconnect() is the single-winner guard.
|
||||
(void)c.Disconnect(R);
|
||||
}
|
||||
|
||||
void TNetwork::Looper(const std::weak_ptr<TClient>& c) {
|
||||
RegisterThreadAuto();
|
||||
while (!c.expired()) {
|
||||
while (true) {
|
||||
auto Client = c.lock();
|
||||
if (!Client) {
|
||||
break;
|
||||
}
|
||||
if (Client->IsDisconnected()) {
|
||||
beammp_debug("client is disconnected, breaking client loop");
|
||||
break;
|
||||
@@ -618,7 +715,7 @@ void TNetwork::Looper(const std::weak_ptr<TClient>& c) {
|
||||
if (!Client->IsSyncing() && Client->IsSynced() && Client->MissedPacketQueueSize() != 0) {
|
||||
// debug("sending " + std::to_string(Client->MissedPacketQueueSize()) + " queued packets");
|
||||
while (Client->MissedPacketQueueSize() > 0) {
|
||||
std::vector<uint8_t> QData {};
|
||||
std::vector<uint8_t> QData { };
|
||||
{ // locked context
|
||||
std::unique_lock lock(Client->MissedPacketQueueMutex());
|
||||
if (Client->MissedPacketQueueSize() <= 0) {
|
||||
@@ -629,7 +726,7 @@ void TNetwork::Looper(const std::weak_ptr<TClient>& c) {
|
||||
} // end locked context
|
||||
// beammp_debug("sending a missed packet: " + QData);
|
||||
if (!TCPSend(*Client, QData, true)) {
|
||||
Client->Disconnect("Failed to TCPSend while clearing the missed packet queue");
|
||||
DisconnectClient(Client, "Failed to TCPSend while clearing the missed packet queue");
|
||||
std::unique_lock lock(Client->MissedPacketQueueMutex());
|
||||
while (!Client->MissedPacketQueue().empty()) {
|
||||
Client->MissedPacketQueue().pop();
|
||||
@@ -644,20 +741,29 @@ void TNetwork::Looper(const std::weak_ptr<TClient>& c) {
|
||||
}
|
||||
|
||||
void TNetwork::TCPClient(const std::weak_ptr<TClient>& c) {
|
||||
// TODO: the c.expired() might cause issues here, remove if you end up here with your debugger
|
||||
if (c.expired() || !c.lock()->GetTCPSock().is_open()) {
|
||||
if (auto Client = c.lock()) {
|
||||
if (Client->IsDisconnected()) {
|
||||
mServer.RemoveClient(c);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
mServer.RemoveClient(c);
|
||||
return;
|
||||
}
|
||||
OnConnect(c);
|
||||
RegisterThread("(" + std::to_string(c.lock()->GetID()) + ") \"" + c.lock()->GetName() + "\"");
|
||||
if (auto Client = c.lock()) {
|
||||
RegisterThread("(" + std::to_string(Client->GetID()) + ") \"" + Client->GetName() + "\"");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread QueueSync(&TNetwork::Looper, this, c);
|
||||
std::jthread QueueSync(&TNetwork::Looper, this, c);
|
||||
|
||||
while (true) {
|
||||
if (c.expired())
|
||||
break;
|
||||
auto Client = c.lock();
|
||||
if (!Client) {
|
||||
break;
|
||||
}
|
||||
if (Client->IsDisconnected()) {
|
||||
beammp_debug("client status < 0, breaking client loop");
|
||||
break;
|
||||
@@ -666,17 +772,19 @@ void TNetwork::TCPClient(const std::weak_ptr<TClient>& c) {
|
||||
auto res = TCPRcv(*Client);
|
||||
if (res.empty()) {
|
||||
beammp_debug("TCPRcv empty");
|
||||
Client->Disconnect("TCPRcv failed");
|
||||
DisconnectClient(Client, "TCPRcv failed");
|
||||
break;
|
||||
}
|
||||
try {
|
||||
mServer.GlobalParser(c, std::move(res), mPPSMonitor, *this, false);
|
||||
} catch (const std::exception& e) {
|
||||
beammp_warnf("Failed to receive/parse packet via TCP from client {}: {}", Client->GetID(), e.what());
|
||||
DisconnectClient(Client, "Failed to parse packet");
|
||||
break;
|
||||
}
|
||||
mServer.GlobalParser(c, std::move(res), mPPSMonitor, *this, false);
|
||||
}
|
||||
|
||||
if (QueueSync.joinable())
|
||||
QueueSync.join();
|
||||
|
||||
if (!c.expired()) {
|
||||
auto Client = c.lock();
|
||||
if (auto Client = c.lock()) {
|
||||
OnDisconnect(c);
|
||||
} else {
|
||||
beammp_warn("client expired in TCPClient, should never happen");
|
||||
@@ -687,8 +795,7 @@ void TNetwork::UpdatePlayer(TClient& Client) {
|
||||
std::string Packet = ("Ss") + std::to_string(mServer.ClientCount()) + "/" + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) + ":";
|
||||
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
|
||||
ReadLock Lock(mServer.GetClientMutex());
|
||||
if (!ClientPtr.expired()) {
|
||||
auto c = ClientPtr.lock();
|
||||
if (auto c = ClientPtr.lock()) {
|
||||
Packet += c->GetName() + ",";
|
||||
}
|
||||
return true;
|
||||
@@ -698,15 +805,144 @@ void TNetwork::UpdatePlayer(TClient& Client) {
|
||||
//(void)Respond(Client, Packet, true);
|
||||
}
|
||||
|
||||
static boost::system::error_code ReadSocketWithTimeout(
|
||||
boost::asio::ip::tcp::socket& socket,
|
||||
void* buffer,
|
||||
std::size_t length,
|
||||
std::chrono::steady_clock::duration timeout);
|
||||
|
||||
boost::system::error_code TNetwork::ReadWithTimeout(TConnection& Connection, void* Buf, size_t Len, std::chrono::steady_clock::duration Timeout) {
|
||||
return ReadWithTimeout(Connection.Socket, Buf, Len, Timeout);
|
||||
}
|
||||
|
||||
boost::system::error_code TNetwork::ReadWithTimeout(boost::asio::ip::tcp::socket& Socket, void* Buf, size_t Len, std::chrono::steady_clock::duration Timeout) {
|
||||
return ReadSocketWithTimeout(Socket, Buf, Len, Timeout);
|
||||
}
|
||||
|
||||
static boost::system::error_code ReadSocketWithTimeout(
|
||||
boost::asio::ip::tcp::socket& Socket,
|
||||
void* Buffer,
|
||||
std::size_t Length,
|
||||
std::chrono::steady_clock::duration Timeout) {
|
||||
namespace asio = boost::asio;
|
||||
using boost::system::error_code;
|
||||
|
||||
struct TTimeoutState {
|
||||
explicit TTimeoutState(const boost::asio::any_io_executor& Executor)
|
||||
: Timer(Executor) { }
|
||||
|
||||
asio::steady_timer Timer;
|
||||
std::promise<std::pair<error_code, std::size_t>> Promise;
|
||||
std::atomic_bool Completed { false };
|
||||
};
|
||||
|
||||
auto State = std::make_shared<TTimeoutState>(Socket.get_executor());
|
||||
auto Future = State->Promise.get_future();
|
||||
|
||||
asio::async_read(
|
||||
Socket,
|
||||
asio::buffer(Buffer, Length),
|
||||
[State](error_code ec, std::size_t n) {
|
||||
if (!State->Completed.exchange(true)) {
|
||||
State->Timer.cancel();
|
||||
State->Promise.set_value({ ec, n });
|
||||
}
|
||||
});
|
||||
|
||||
State->Timer.expires_after(Timeout);
|
||||
State->Timer.async_wait(
|
||||
[State, &Socket](error_code ec) {
|
||||
if (ec == asio::error::operation_aborted)
|
||||
return;
|
||||
|
||||
if (!State->Completed.exchange(true)) {
|
||||
error_code IgnoredEc;
|
||||
Socket.cancel(IgnoredEc);
|
||||
State->Promise.set_value({ asio::error::timed_out, 0 });
|
||||
}
|
||||
});
|
||||
|
||||
auto [ec, NRead] = Future.get();
|
||||
return ec;
|
||||
}
|
||||
|
||||
TEST_CASE("ReadSocketWithTimeout returns timed_out when peer sends no data") {
|
||||
TIoPollThread TimerThread;
|
||||
boost::system::error_code Ec;
|
||||
ip::tcp::socket ClientSocket(TimerThread.IoCtx());
|
||||
ip::tcp::socket ServerSocket(TimerThread.IoCtx());
|
||||
OpenLoopbackSocketPair(TimerThread.IoCtx(), ClientSocket, ServerSocket, Ec);
|
||||
REQUIRE(!Ec);
|
||||
|
||||
uint8_t ReadByte = 0;
|
||||
const auto ReadEc = ReadSocketWithTimeout(ServerSocket, &ReadByte, 1, std::chrono::milliseconds(50));
|
||||
|
||||
CHECK(ReadEc == error::timed_out);
|
||||
}
|
||||
|
||||
TEST_CASE("ReadSocketWithTimeout reads small payload") {
|
||||
TIoPollThread TimerThread;
|
||||
boost::system::error_code Ec;
|
||||
ip::tcp::socket ClientSocket(TimerThread.IoCtx());
|
||||
ip::tcp::socket ServerSocket(TimerThread.IoCtx());
|
||||
OpenLoopbackSocketPair(TimerThread.IoCtx(), ClientSocket, ServerSocket, Ec);
|
||||
REQUIRE(!Ec);
|
||||
|
||||
const std::array<uint8_t, 2> Sent { 'O', 'K' };
|
||||
boost::asio::write(ClientSocket, boost::asio::buffer(Sent), Ec);
|
||||
REQUIRE(!Ec);
|
||||
std::array<uint8_t, 2> Received { };
|
||||
const auto ReadEc = ReadSocketWithTimeout(ServerSocket, Received.data(), Received.size(), std::chrono::milliseconds(200));
|
||||
|
||||
CHECK(!ReadEc);
|
||||
CHECK(Received == Sent);
|
||||
}
|
||||
|
||||
TEST_CASE("ReadSocketWithTimeout reads large payload") {
|
||||
TIoPollThread TimerThread;
|
||||
boost::system::error_code Ec;
|
||||
ip::tcp::socket ClientSocket(TimerThread.IoCtx());
|
||||
ip::tcp::socket ServerSocket(TimerThread.IoCtx());
|
||||
OpenLoopbackSocketPair(TimerThread.IoCtx(), ClientSocket, ServerSocket, Ec);
|
||||
REQUIRE(!Ec);
|
||||
|
||||
constexpr size_t PacketSize = 2 * 1024 * 1024;
|
||||
std::vector<uint8_t> Sent(PacketSize, uint8_t(0x7A));
|
||||
boost::asio::write(ClientSocket, boost::asio::buffer(Sent), Ec);
|
||||
REQUIRE(!Ec);
|
||||
std::vector<uint8_t> Received(PacketSize);
|
||||
const auto ReadEc = ReadSocketWithTimeout(ServerSocket, Received.data(), Received.size(), std::chrono::seconds(2));
|
||||
|
||||
CHECK(!ReadEc);
|
||||
CHECK(Received == Sent);
|
||||
}
|
||||
|
||||
TEST_CASE("ReadSocketWithTimeout can timeout then retry successfully") {
|
||||
TIoPollThread TimerThread;
|
||||
boost::system::error_code Ec;
|
||||
ip::tcp::socket ClientSocket(TimerThread.IoCtx());
|
||||
ip::tcp::socket ServerSocket(TimerThread.IoCtx());
|
||||
OpenLoopbackSocketPair(TimerThread.IoCtx(), ClientSocket, ServerSocket, Ec);
|
||||
REQUIRE(!Ec);
|
||||
|
||||
uint8_t Received = 0;
|
||||
CHECK(ReadSocketWithTimeout(ServerSocket, &Received, 1, std::chrono::milliseconds(20)) == error::timed_out);
|
||||
|
||||
const uint8_t Sent = 0x42;
|
||||
boost::asio::write(ClientSocket, boost::asio::buffer(&Sent, 1), Ec);
|
||||
REQUIRE(!Ec);
|
||||
const auto ReadEc = ReadSocketWithTimeout(ServerSocket, &Received, 1, std::chrono::milliseconds(200));
|
||||
|
||||
CHECK(!ReadEc);
|
||||
CHECK(Received == Sent);
|
||||
}
|
||||
|
||||
void TNetwork::OnDisconnect(const std::weak_ptr<TClient>& ClientPtr) {
|
||||
std::shared_ptr<TClient> LockedClientPtr { nullptr };
|
||||
try {
|
||||
LockedClientPtr = ClientPtr.lock();
|
||||
} catch (const std::exception&) {
|
||||
auto LockedClientPtr = ClientPtr.lock();
|
||||
if (!LockedClientPtr) {
|
||||
beammp_warn("Client expired in OnDisconnect, this is unexpected");
|
||||
return;
|
||||
}
|
||||
beammp_assert(LockedClientPtr != nullptr);
|
||||
TClient& c = *LockedClientPtr;
|
||||
beammp_info(c.GetName() + (" Connection Terminated"));
|
||||
std::string Packet;
|
||||
@@ -725,7 +961,7 @@ void TNetwork::OnDisconnect(const std::weak_ptr<TClient>& ClientPtr) {
|
||||
Packet.clear();
|
||||
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onPlayerDisconnect", "", c.GetID());
|
||||
LuaAPI::MP::Engine->WaitForAll(Futures);
|
||||
c.Disconnect("Already Disconnected (OnDisconnect)");
|
||||
DisconnectClient(c, "Already Disconnected (OnDisconnect)");
|
||||
mServer.RemoveClient(ClientPtr);
|
||||
}
|
||||
|
||||
@@ -737,8 +973,7 @@ int TNetwork::OpenID() {
|
||||
found = true;
|
||||
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
|
||||
ReadLock Lock(mServer.GetClientMutex());
|
||||
if (!ClientPtr.expired()) {
|
||||
auto c = ClientPtr.lock();
|
||||
if (auto c = ClientPtr.lock()) {
|
||||
if (c->GetID() == ID) {
|
||||
found = false;
|
||||
ID++;
|
||||
@@ -751,9 +986,11 @@ int TNetwork::OpenID() {
|
||||
}
|
||||
|
||||
void TNetwork::OnConnect(const std::weak_ptr<TClient>& c) {
|
||||
beammp_assert(!c.expired());
|
||||
beammp_info("Client connected");
|
||||
auto LockedClient = c.lock();
|
||||
if (!LockedClient) {
|
||||
return;
|
||||
}
|
||||
beammp_info("Client connected");
|
||||
LockedClient->SetID(OpenID());
|
||||
beammp_info("Assigned ID " + std::to_string(LockedClient->GetID()) + " to " + LockedClient->GetName());
|
||||
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onPlayerConnecting", "", LockedClient->GetID()));
|
||||
@@ -785,6 +1022,7 @@ void TNetwork::SyncResources(TClient& c) {
|
||||
while (!c.IsDisconnected()) {
|
||||
Data = TCPRcv(c);
|
||||
if (Data.empty()) {
|
||||
DisconnectClient(c, "TCPRcv failed during resource sync");
|
||||
break;
|
||||
}
|
||||
constexpr std::string_view Done = "Done";
|
||||
@@ -833,7 +1071,7 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) {
|
||||
for (auto mod : mResourceManager.GetMods()) {
|
||||
if (mod["file_name"].get<std::string>() == FileName && mod["protected"] == true) {
|
||||
beammp_warn("Client tried to access protected file " + UnsafeName);
|
||||
c.Disconnect("Mod is protected thus cannot be downloaded");
|
||||
DisconnectClient(c, "Mod is protected thus cannot be downloaded");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -860,9 +1098,9 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) {
|
||||
#if defined(BEAMMP_LINUX)
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <signal.h>
|
||||
#include <sys/sendfile.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#endif
|
||||
void TNetwork::SendFileToClient(TClient& c, size_t Size, const std::string& Name) {
|
||||
TScopedTimer timer(fmt::format("Download of '{}' for client {}", Name, c.GetID()));
|
||||
@@ -877,17 +1115,40 @@ void TNetwork::SendFileToClient(TClient& c, size_t Size, const std::string& Name
|
||||
// native handle, needed in order to make native syscalls with it
|
||||
int socket = c.GetTCPSock().native_handle();
|
||||
|
||||
ssize_t ret = 0;
|
||||
auto ToSendTotal = Size;
|
||||
auto Start = 0;
|
||||
while (ret < ssize_t(ToSendTotal)) {
|
||||
auto SysOffset = off_t(Start + size_t(ret));
|
||||
ret = sendfile(socket, fd, &SysOffset, ToSendTotal - size_t(ret));
|
||||
if (ret < 0) {
|
||||
beammp_errorf("Failed to send mod '{}' to client {}: {}", Name, c.GetID(), std::strerror(errno));
|
||||
const auto ToSendTotal = Size;
|
||||
size_t TotalSent = 0;
|
||||
while (TotalSent < ToSendTotal) {
|
||||
off_t SysOffset = off_t(TotalSent);
|
||||
const ssize_t SentNow = sendfile(socket, fd, &SysOffset, ToSendTotal - TotalSent);
|
||||
if (SentNow > 0) {
|
||||
TotalSent += size_t(SentNow);
|
||||
continue;
|
||||
}
|
||||
if (SentNow == 0) {
|
||||
beammp_errorf("Failed to send mod '{}' to client {}: sendfile returned 0 before all bytes were sent", Name, c.GetID());
|
||||
::close(fd);
|
||||
DisconnectClient(c, "sendfile returned 0 during mod download");
|
||||
return;
|
||||
}
|
||||
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (errno == EAGAIN
|
||||
#if EWOULDBLOCK != EAGAIN
|
||||
|| errno == EWOULDBLOCK
|
||||
#endif
|
||||
) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
beammp_errorf("Failed to send mod '{}' to client {}: {}", Name, c.GetID(), std::strerror(errno));
|
||||
::close(fd);
|
||||
DisconnectClient(c, "sendfile failed during mod download");
|
||||
return;
|
||||
}
|
||||
::close(fd);
|
||||
|
||||
#else
|
||||
std::ifstream f(Name.c_str(), std::ios::binary);
|
||||
@@ -897,7 +1158,7 @@ void TNetwork::SendFileToClient(TClient& c, size_t Size, const std::string& Name
|
||||
Data.resize(Split);
|
||||
else
|
||||
Data.resize(Size);
|
||||
ip::tcp::socket* TCPSock = &c.GetTCPSock();
|
||||
boost::asio::ip::tcp::socket* TCPSock = &c.GetTCPSock();
|
||||
std::streamsize Sent = 0;
|
||||
while (!c.IsDisconnected() && Sent < Size) {
|
||||
size_t Diff = Size - Sent;
|
||||
@@ -906,7 +1167,7 @@ void TNetwork::SendFileToClient(TClient& c, size_t Size, const std::string& Name
|
||||
f.read(reinterpret_cast<char*>(Data.data()), Split);
|
||||
if (!TCPSendRaw(c, *TCPSock, Data.data(), Split)) {
|
||||
if (!c.IsDisconnected())
|
||||
c.Disconnect("TCPSendRaw failed in mod download (1)");
|
||||
DisconnectClient(c, "TCPSendRaw failed in mod download (1)");
|
||||
break;
|
||||
}
|
||||
Sent += Split;
|
||||
@@ -915,7 +1176,7 @@ void TNetwork::SendFileToClient(TClient& c, size_t Size, const std::string& Name
|
||||
f.read(reinterpret_cast<char*>(Data.data()), Diff);
|
||||
if (!TCPSendRaw(c, *TCPSock, Data.data(), int32_t(Diff))) {
|
||||
if (!c.IsDisconnected())
|
||||
c.Disconnect("TCPSendRaw failed in mod download (2)");
|
||||
DisconnectClient(c, "TCPSendRaw failed in mod download (2)");
|
||||
break;
|
||||
}
|
||||
Sent += Diff;
|
||||
@@ -924,9 +1185,9 @@ void TNetwork::SendFileToClient(TClient& c, size_t Size, const std::string& Name
|
||||
#endif
|
||||
}
|
||||
|
||||
bool TNetwork::TCPSendRaw(TClient& C, ip::tcp::socket& socket, const uint8_t* Data, size_t Size) {
|
||||
bool TNetwork::TCPSendRaw(TClient& C, boost::asio::ip::tcp::socket& socket, const uint8_t* Data, size_t Size) {
|
||||
boost::system::error_code ec;
|
||||
write(socket, buffer(Data, Size), ec);
|
||||
boost::asio::write(socket, boost::asio::buffer(Data, Size), ec);
|
||||
if (ec) {
|
||||
beammp_errorf("Failed to send raw data to client: {}", ec.message());
|
||||
return false;
|
||||
@@ -956,10 +1217,10 @@ bool TNetwork::Respond(TClient& c, const std::vector<uint8_t>& MSG, bool Rel, bo
|
||||
}
|
||||
|
||||
bool TNetwork::SyncClient(const std::weak_ptr<TClient>& c) {
|
||||
if (c.expired()) {
|
||||
auto LockedClient = c.lock();
|
||||
if (!LockedClient) {
|
||||
return false;
|
||||
}
|
||||
auto LockedClient = c.lock();
|
||||
if (LockedClient->IsSynced())
|
||||
return true;
|
||||
// Syncing, later set isSynced
|
||||
@@ -978,8 +1239,8 @@ bool TNetwork::SyncClient(const std::weak_ptr<TClient>& c) {
|
||||
std::shared_ptr<TClient> client;
|
||||
{
|
||||
ReadLock Lock(mServer.GetClientMutex());
|
||||
if (!ClientPtr.expired()) {
|
||||
client = ClientPtr.lock();
|
||||
if (auto Locked = ClientPtr.lock()) {
|
||||
client = std::move(Locked);
|
||||
} else
|
||||
return true;
|
||||
}
|
||||
@@ -1016,14 +1277,14 @@ void TNetwork::SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self
|
||||
char C = Data.at(0);
|
||||
bool ret = true;
|
||||
mServer.ForEachClient([&](std::weak_ptr<TClient> ClientPtr) -> bool {
|
||||
std::shared_ptr<TClient> Client;
|
||||
try {
|
||||
std::shared_ptr<TClient> Client { nullptr };
|
||||
{
|
||||
ReadLock Lock(mServer.GetClientMutex());
|
||||
Client = ClientPtr.lock();
|
||||
} catch (const std::exception&) {
|
||||
// continue
|
||||
beammp_warn("Client expired, shouldn't happen - if a client disconnected recently, you can ignore this");
|
||||
return true;
|
||||
if (auto Locked = ClientPtr.lock()) {
|
||||
Client = std::move(Locked);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (Self || Client.get() != c) {
|
||||
if (Client->IsSynced() || Client->IsSyncing()) {
|
||||
@@ -1067,23 +1328,23 @@ bool TNetwork::UDPSend(TClient& Client, std::vector<uint8_t> Data) {
|
||||
CompressProperly(Data);
|
||||
}
|
||||
boost::system::error_code ec;
|
||||
mUDPSock.send_to(buffer(Data), Addr, 0, ec);
|
||||
mUDPSock.send_to(boost::asio::buffer(Data), Addr, 0, ec);
|
||||
if (ec) {
|
||||
beammp_debugf("UDP sendto() failed: {}", ec.message());
|
||||
if (!Client.IsDisconnected())
|
||||
Client.Disconnect("UDP send failed");
|
||||
DisconnectClient(Client, "UDP send failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> TNetwork::UDPRcvFromClient(ip::udp::endpoint& ClientEndpoint) {
|
||||
std::array<char, 1024> Ret {};
|
||||
std::vector<uint8_t> TNetwork::UDPRcvFromClient(boost::asio::ip::udp::endpoint& ClientEndpoint) {
|
||||
std::array<char, 1024> Ret { };
|
||||
boost::system::error_code ec;
|
||||
const auto Rcv = mUDPSock.receive_from(mutable_buffer(Ret.data(), Ret.size()), ClientEndpoint, 0, ec);
|
||||
const auto Rcv = mUDPSock.receive_from(boost::asio::mutable_buffer(Ret.data(), Ret.size()), ClientEndpoint, 0, ec);
|
||||
if (ec) {
|
||||
beammp_errorf("UDP recvfrom() failed: {}", ec.message());
|
||||
return {};
|
||||
return { };
|
||||
}
|
||||
beammp_assert(Rcv <= Ret.size());
|
||||
return std::vector<uint8_t>(Ret.begin(), Ret.begin() + Rcv);
|
||||
|
||||
+3
-3
@@ -55,8 +55,8 @@ void TPPSMonitor::operator()() {
|
||||
std::shared_ptr<TClient> c;
|
||||
{
|
||||
ReadLock Lock(mServer.GetClientMutex());
|
||||
if (!ClientPtr.expired()) {
|
||||
c = ClientPtr.lock();
|
||||
if (auto Locked = ClientPtr.lock()) {
|
||||
c = std::move(Locked);
|
||||
} else
|
||||
return true;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ void TPPSMonitor::operator()() {
|
||||
return true;
|
||||
});
|
||||
for (auto& ClientToKick : TimedOutClients) {
|
||||
ClientToKick->Disconnect("Timeout");
|
||||
Network().DisconnectClient(*ClientToKick, "Timeout");
|
||||
}
|
||||
TimedOutClients.clear();
|
||||
if (C == 0 || mInternalPPS == 0) {
|
||||
|
||||
@@ -69,8 +69,9 @@ void TPluginMonitor::operator()() {
|
||||
auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path());
|
||||
auto Res = mEngine->EnqueueScript(StateID, Chunk);
|
||||
Res->WaitUntilReady();
|
||||
if (Res->Error) {
|
||||
beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Res->ErrorMessage);
|
||||
if (Res->IsError()) {
|
||||
auto Snapshot = Res->GetSnapshot();
|
||||
beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Snapshot.ErrorMessage);
|
||||
} else {
|
||||
mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit"));
|
||||
mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first));
|
||||
|
||||
+65
-27
@@ -28,6 +28,7 @@
|
||||
#include <any>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
@@ -53,7 +54,6 @@ static std::optional<std::pair<int, int>> GetPidVid(const std::string& str) {
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
TEST_CASE("GetPidVid") {
|
||||
SUBCASE("Valid singledigit") {
|
||||
const auto MaybePidVid = GetPidVid("0-1");
|
||||
@@ -120,7 +120,6 @@ TEST_CASE("GetPidVid") {
|
||||
CHECK(!MaybePidVid);
|
||||
}
|
||||
}
|
||||
|
||||
TServer::TServer(const std::vector<std::string_view>& Arguments) {
|
||||
beammp_info("BeamMP Server v" + Application::ServerVersionString());
|
||||
Application::SetSubsystemStatus("Server", Application::Status::Starting);
|
||||
@@ -128,19 +127,15 @@ TServer::TServer(const std::vector<std::string_view>& Arguments) {
|
||||
}
|
||||
|
||||
void TServer::RemoveClient(const std::weak_ptr<TClient>& WeakClientPtr) {
|
||||
std::shared_ptr<TClient> LockedClientPtr { nullptr };
|
||||
try {
|
||||
LockedClientPtr = WeakClientPtr.lock();
|
||||
} catch (const std::exception&) {
|
||||
// silently fail, as there's nothing to do
|
||||
auto LockedClientPtr = WeakClientPtr.lock();
|
||||
if (!LockedClientPtr) {
|
||||
return;
|
||||
}
|
||||
beammp_assert(LockedClientPtr != nullptr);
|
||||
TClient& Client = *LockedClientPtr;
|
||||
beammp_debug("removing client " + Client.GetName() + " (" + std::to_string(ClientCount()) + ")");
|
||||
Client.ClearCars();
|
||||
WriteLock Lock(mClientsMutex);
|
||||
mClients.erase(WeakClientPtr.lock());
|
||||
mClients.erase(LockedClientPtr);
|
||||
}
|
||||
|
||||
void TServer::ForEachClient(const std::function<bool(std::weak_ptr<TClient>)>& Fn) {
|
||||
@@ -168,14 +163,16 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
|
||||
try {
|
||||
Packet = DeComp(Packet);
|
||||
} catch (const InvalidDataError& ) {
|
||||
auto LockedClient = Client.lock();
|
||||
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)");
|
||||
if (auto LockedClient = Client.lock()) {
|
||||
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)");
|
||||
}
|
||||
return;
|
||||
} catch (const std::runtime_error& e) {
|
||||
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());
|
||||
Network.ClientKick(*LockedClient, "Decompression failed (likely a server-side problem)");
|
||||
if (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());
|
||||
Network.ClientKick(*LockedClient, "Decompression failed (likely a server-side problem)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -183,10 +180,10 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
|
||||
return;
|
||||
}
|
||||
|
||||
if (Client.expired()) {
|
||||
auto LockedClient = Client.lock();
|
||||
if (!LockedClient) {
|
||||
return;
|
||||
}
|
||||
auto LockedClient = Client.lock();
|
||||
|
||||
std::any Res;
|
||||
char Code = Packet.at(0);
|
||||
@@ -198,7 +195,8 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
|
||||
int PID = -1;
|
||||
int VID = -1;
|
||||
|
||||
auto MaybePidVid = GetPidVid(StringPacket.substr(3).substr(0, StringPacket.substr(3).find(':', 1)));
|
||||
auto pidVidPart = StringPacket.substr(3);
|
||||
auto MaybePidVid = GetPidVid(pidVidPart.substr(0, pidVidPart.find(':')));
|
||||
if (MaybePidVid) {
|
||||
std::tie(PID, VID) = MaybePidVid.value();
|
||||
}
|
||||
@@ -224,7 +222,7 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
|
||||
case 'p':
|
||||
if (!Network.Respond(*LockedClient, StringToVector("p"), false)) {
|
||||
// failed to send
|
||||
LockedClient->Disconnect("Failed to send ping");
|
||||
Network.DisconnectClient(*LockedClient, "Failed to send ping");
|
||||
} else {
|
||||
Network.UpdatePlayer(*LockedClient);
|
||||
}
|
||||
@@ -256,14 +254,24 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
|
||||
beammp_debugf("Empty chat message received from '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
|
||||
return;
|
||||
}
|
||||
if (Message.size() > 500) {
|
||||
beammp_debugf("Chat message too long from '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
|
||||
return;
|
||||
}
|
||||
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onChatMessage", "", LockedClient->GetID(), LockedClient->GetName(), Message);
|
||||
TLuaEngine::WaitForAll(Futures);
|
||||
LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), PacketAsString.substr(PacketAsString.find(':', 3) + 1));
|
||||
bool Rejected = std::any_of(Futures.begin(), Futures.end(),
|
||||
[](const std::shared_ptr<TLuaResult>& Elem) {
|
||||
return !Elem->Error
|
||||
&& Elem->Result.is<int>()
|
||||
&& bool(Elem->Result.as<int>());
|
||||
auto Snapshot = Elem->GetDetachedSnapshot();
|
||||
if (Snapshot.Error) {
|
||||
return false;
|
||||
}
|
||||
const int* MaybeInt = std::get_if<int>(&Snapshot.Result.V);
|
||||
if (MaybeInt == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return bool(*MaybeInt);
|
||||
});
|
||||
if (!Rejected) {
|
||||
std::string SanitizedPacket = fmt::format("C:{}: {}", LockedClient->GetName(), Message);
|
||||
@@ -289,7 +297,8 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
|
||||
int PID = -1;
|
||||
int VID = -1;
|
||||
|
||||
auto MaybePidVid = GetPidVid(StringPacket.substr(3).substr(0, StringPacket.substr(3).find(':', 1)));
|
||||
auto pidVidPart = StringPacket.substr(3);
|
||||
auto MaybePidVid = GetPidVid(pidVidPart.substr(0, pidVidPart.find(':')));
|
||||
if (MaybePidVid) {
|
||||
std::tie(PID, VID) = MaybePidVid.value();
|
||||
}
|
||||
@@ -375,7 +384,15 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
|
||||
TLuaEngine::WaitForAll(Futures);
|
||||
bool ShouldntSpawn = std::any_of(Futures.begin(), Futures.end(),
|
||||
[](const std::shared_ptr<TLuaResult>& Result) {
|
||||
return !Result->Error && Result->Result.is<int>() && Result->Result.as<int>() != 0;
|
||||
auto Snapshot = Result->GetDetachedSnapshot();
|
||||
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;
|
||||
@@ -412,7 +429,15 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
|
||||
TLuaEngine::WaitForAll(Futures);
|
||||
bool ShouldntAllow = std::any_of(Futures.begin(), Futures.end(),
|
||||
[](const std::shared_ptr<TLuaResult>& Result) {
|
||||
return !Result->Error && Result->Result.is<int>() && Result->Result.as<int>() != 0;
|
||||
auto Snapshot = Result->GetDetachedSnapshot();
|
||||
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('{');
|
||||
@@ -466,7 +491,13 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
|
||||
}
|
||||
|
||||
if (PID != -1 && VID != -1 && PID == c.GetID()) {
|
||||
Data = Data.substr(Data.find('{'));
|
||||
auto BracketPos = 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));
|
||||
Network.SendToAll(&c, StringToVector(Packet), false, true);
|
||||
}
|
||||
@@ -495,7 +526,14 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
|
||||
}
|
||||
|
||||
if (PID != -1 && VID != -1 && PID == c.GetID()) {
|
||||
Data = Data.substr(Data.find('['));
|
||||
auto BracketPos = 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));
|
||||
Network.SendToAll(&c, StringToVector(Packet), false, true);
|
||||
|
||||
|
||||
+15
-4
@@ -17,6 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "ArgsParser.h"
|
||||
#include "Client.h"
|
||||
#include "Common.h"
|
||||
#include "Http.h"
|
||||
#include "LuaAPI.h"
|
||||
@@ -175,10 +176,6 @@ int BeamMPServerMain(MainArguments Arguments) {
|
||||
Application::SetSubsystemStatus("Main", Application::Status::ShuttingDown);
|
||||
Shutdown = true;
|
||||
});
|
||||
Application::RegisterShutdownHandler([] {
|
||||
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onShutdown", "");
|
||||
TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5));
|
||||
});
|
||||
|
||||
TServer Server(Arguments.List);
|
||||
|
||||
@@ -200,6 +197,20 @@ int BeamMPServerMain(MainArguments Arguments) {
|
||||
|
||||
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)");
|
||||
|
||||
std::set<std::string> IgnoreSubsystems {
|
||||
|
||||
+1
-1
Submodule vcpkg updated: 6978381401...5bf0c55239
+14
-2
@@ -14,6 +14,18 @@
|
||||
"openssl",
|
||||
"rapidjson",
|
||||
"sol2",
|
||||
"curl"
|
||||
]
|
||||
"curl",
|
||||
"lua"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"name": "sol2",
|
||||
"version": "3.3.1"
|
||||
},
|
||||
{
|
||||
"name": "lua",
|
||||
"version": "5.3.5#6"
|
||||
}
|
||||
],
|
||||
"builtin-baseline": "5bf0c55239da398b8c6f450818c9e28d36bf9966"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user