mirror of
https://github.com/BeamMP/BeamMP-Server.git
synced 2026-07-13 02:03:44 +00:00
Compare commits
193 Commits
v3.6.0
...
add-region-tld
| Author | SHA1 | Date | |
|---|---|---|---|
| 8239a31e4b | |||
| 088c99beb7 | |||
| a65b8b8e4c | |||
| 58aedfb9bf | |||
| 0432309e4a | |||
| e393239fdc | |||
| 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 | |||
| c4c894c1f7 | |||
| 039a44bba5 | |||
| add0b86b37 | |||
| 403c1d5f78 | |||
| 6318ca79e7 | |||
| 2bd4ee9321 | |||
| 22c0a966bb | |||
| 731599f16e | |||
| 38c6766b2b | |||
| bcb035bafc | |||
| 068f553fa9 | |||
| ca11f353b0 | |||
| b7cf304d49 | |||
| 03d3b873c4 | |||
| ea9c808233 | |||
| 40bd050ca6 | |||
| a0d75c01f0 | |||
| 8098431fad | |||
| 40c8c0c5c2 | |||
| cd39f387c2 | |||
| a5ca50866f | |||
| 10ea0cf59e | |||
| 7db40e068e | |||
| 6053aa6192 | |||
| 0bb18de9f6 | |||
| 7a439bb5b9 | |||
| 6c3174ac08 | |||
| 73e9595d14 | |||
| 3f7cf7a258 | |||
| 093310c124 | |||
| 6286457fa4 | |||
| 71b8a61c97 | |||
| f0141e4fd3 | |||
| 00560f7646 | |||
| 27d50fc2b5 | |||
| 52a1d9a99e | |||
| 2f577a2358 | |||
| 6014536f52 | |||
| fbce8a946e | |||
| bd9b6212e2 | |||
| b112ee20d8 | |||
| 8593aeb21d | |||
| 840f9b9f9d | |||
| ec21cbbe86 | |||
| e90f1af109 | |||
| c78775bfd8 | |||
| 9c3042280d | |||
| 4f2ef3c3a7 | |||
| 6a2ee052ba | |||
| 2658d0f785 | |||
| a7eeda0569 | |||
| cd29f25435 | |||
| 7c864d94b3 | |||
| 26f1be0a51 | |||
| d7e75ae0c7 | |||
| 1d90f53527 | |||
| 7dd6b41642 | |||
| d7f3bc8b9f | |||
| 687a988701 | |||
| 9046b5a4d3 | |||
| b4d4967529 | |||
| 51c24b82fe | |||
| 5e13f9dd2d | |||
| f8d66c4336 | |||
| 9875defe86 | |||
| fc208770dd | |||
| 99a51808a0 | |||
| 1c07cf83b2 | |||
| 99136f133a | |||
| 6c9d58582b | |||
| 4edae00998 | |||
| 17e9c05f46 | |||
| 71e3cb83ae | |||
| fb2e26bd28 | |||
| 976ab68ca3 | |||
| 58a76e1df2 | |||
| c696276fc3 | |||
| 6ce0608bb3 | |||
| 52ad237419 | |||
| 71038dc617 | |||
| dc3bb517a3 | |||
| 7b2c48c8d4 | |||
| ef557ebfc4 | |||
| b2e953b92a | |||
| 576d765557 | |||
| e3416804e4 | |||
| 9ad4f61209 | |||
| aed6311146 | |||
| 5e41cefa87 | |||
| b1710ee826 | |||
| 03b0bd4d2c | |||
| 5179ac1fdc | |||
| 54e31ce2ec | |||
| 4abe9b8636 | |||
| 956d6f50e1 | |||
| 6aeb2eb736 | |||
| f40d4c1ddd | |||
| 4f052a3e0a | |||
| 2bf9e7d916 | |||
| 89c906232d | |||
| c39beb5b72 | |||
| 7dd2d89ad9 | |||
| 3403c8acba | |||
| 0a6eecee69 |
@@ -0,0 +1,6 @@
|
||||
Please replace this text <-> with your PR description and leave the below declarations intact.
|
||||
|
||||
---
|
||||
|
||||
By creating this pull request, I understand that code that is AI generated or otherwise automatically generated may be rejected without further discussion.
|
||||
I declare that I fully understand all code I pushed into this PR, and wrote all this code myself and own the rights to this code.
|
||||
+14
-13
@@ -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
|
||||
@@ -84,16 +85,16 @@ jobs:
|
||||
run: ./bin/BeamMP-Server-tests
|
||||
|
||||
arm64-matrix:
|
||||
runs-on: [Linux, ARM64]
|
||||
runs-on: ubuntu-22.04-arm
|
||||
env:
|
||||
VCPKG_DEFAULT_TRIPLET: "arm64-linux"
|
||||
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 }}
|
||||
@@ -104,15 +105,15 @@ jobs:
|
||||
asset_content_type: application/x-elf
|
||||
|
||||
arm64-matrix:
|
||||
runs-on: [Linux, ARM64]
|
||||
runs-on: ubuntu-22.04-arm
|
||||
needs: create-release
|
||||
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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.idea/
|
||||
.sentry-native/
|
||||
*.orig
|
||||
*.toml
|
||||
boost_*
|
||||
|
||||
+13
-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,9 @@ set(PRJ_HEADERS
|
||||
include/Settings.h
|
||||
include/Profiling.h
|
||||
include/ChronoWrapper.h
|
||||
include/TConnectionLimiter.h
|
||||
include/TLuaResult.h
|
||||
include/RegionHandler.h
|
||||
)
|
||||
# add all source files (.cpp) to this, except the one with main()
|
||||
set(PRJ_SOURCES
|
||||
@@ -65,6 +69,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 +83,9 @@ set(PRJ_SOURCES
|
||||
src/Settings.cpp
|
||||
src/Profiling.cpp
|
||||
src/ChronoWrapper.cpp
|
||||
src/TConnectionLimiter.cpp
|
||||
src/TLuaResult.cpp
|
||||
src/RegionHandler.cpp
|
||||
)
|
||||
|
||||
find_package(Lua REQUIRED)
|
||||
@@ -104,6 +112,7 @@ set(PRJ_LIBRARIES
|
||||
httplib::httplib
|
||||
libzip::zip
|
||||
OpenSSL::SSL OpenSSL::Crypto
|
||||
CURL::libcurl
|
||||
${LUA_LIBRARIES}
|
||||
)
|
||||
|
||||
@@ -116,6 +125,7 @@ find_package(httplib CONFIG REQUIRED)
|
||||
find_package(libzip CONFIG REQUIRED)
|
||||
find_package(RapidJSON CONFIG REQUIRED)
|
||||
find_package(sol2 CONFIG REQUIRED)
|
||||
find_package(CURL CONFIG REQUIRED)
|
||||
add_subdirectory("deps/toml11")
|
||||
|
||||
include_directories(include)
|
||||
@@ -146,7 +156,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)
|
||||
|
||||
|
||||
+2
-2
@@ -108,8 +108,8 @@ A BeamMP-Developer must review your code in detail, and leave a review. If this
|
||||
5. Run `git submodule update --init --recursive`.
|
||||
6. Make a new branch for your feature or fix from the branch you are on. You can do this via `git checkout -b <branch name>`. See [this section on branches](#branches) for more info on branch naming.
|
||||
7. Install all dependencies. Those are usually listed in the `README.md` in the branch you're in, or, more reliably, in one of the files in `.github/workflows` (if you can read `yaml`).
|
||||
8. Run CMake to configure the project. You can find tutorials on this online. You will want to tell CMake to build with `CMAKE_BUILD_TYPE=Debug`, for example by passing it to CMake via the commandline switch `-DCMAKE_BUILD_TYPE=Debug`. You may also want to turn off sentry by setting `SENTRY_BACKEND=none` (for example via commandline switch `-DSENTRY_BACKEND=none`). An example invocation on Linux with GNU make would be
|
||||
`cmake . -DCMAKE_BUILD_TYPE=Debug -DSENTRY_BACKEND=none` .
|
||||
8. Run CMake to configure the project. You can find tutorials on this online. You will want to tell CMake to build with `CMAKE_BUILD_TYPE=Debug`, for example by passing it to CMake via the commandline switch `-DCMAKE_BUILD_TYPE=Debug`. An example invocation on Linux with GNU make would be
|
||||
`cmake . -DCMAKE_BUILD_TYPE=Debug` .
|
||||
9. Build the `BeamMP-Server` target to build the BeamMP-Server, or the `BeamMP-Server-tests` target to build the unit-tests (does not include a working server). In the example from 8. (on Linux), you could build with `make BeamMP-Server`, `make -j BeamMP-Server` or `cmake --build . --parallel --target BeamMP-Server` . Or, on Windows, (in Visual Studio), you would just press some big green "run" or "debug" button.
|
||||
10. When making changes, refer to [this section on how to commit properly](#how-to-commit). Not following those guidelines will result in your changes being rejected, so please take a look.
|
||||
11. Make sure to add Unit-tests with `doctest` if you build new stuff. You can find examples all over the latest version of the codebase (search for `TEST_CASE`).
|
||||
|
||||
@@ -38,7 +38,7 @@ If you need support with understanding the codebase, please write us in the Disc
|
||||
|
||||
## About Building from Source
|
||||
|
||||
We only allow building unmodified (original) source code for public use. `master` is considered **unstable** and we will not provide technical support if such a build doesn't work, so always build from a tag. You can checkout a tag with `git checkout tags/TAGNAME`, where `TAGNAME` is the tag, for example `v3.1.0`.
|
||||
We only allow building unmodified (original) source code for public use. `master` is considered **unstable** and we will not provide technical support if such a build doesn't work, so always build from a tag. You can checkout a tag with `git checkout tags/TAGNAME`, where `TAGNAME` is the tag, for example `v3.4.1`. See [the tags](https://github.com/BeamMP/BeamMP-Server/tags) for possible versions/tags, as well as [the releases](https://github.com/BeamMP/BeamMP-Server/releases) to check which version is marked as a release/prerelease. We recommend using the [latest release](https://github.com/BeamMP/BeamMP-Server/releases/latest).
|
||||
|
||||
## Supported Operating Systems
|
||||
|
||||
@@ -66,9 +66,10 @@ You can build on **Windows, Linux** or other platforms by following these steps:
|
||||
|
||||
1. Check out the repository with git: `git clone --recursive https://github.com/BeamMP/BeamMP-Server`.
|
||||
2. Go into the directory `cd BeamMP-Server`.
|
||||
3. Run CMake `cmake -S . -B bin -DCMAKE_BUILD_TYPE=Release` - this can take a few minutes and may take a lot of disk space and bandwidth.
|
||||
4. Build via `cmake --build bin --parallel --config Release -t BeamMP-Server`.
|
||||
5. Your executable can be found in `bin/`.
|
||||
3. Specify the server version to build by checking out a tag: `git checkout tags/v3.4.1` - [Possible versions/tags](https://github.com/BeamMP/BeamMP-Server/tags)
|
||||
4. Run CMake `cmake -S . -B bin -DCMAKE_BUILD_TYPE=Release` - this can take a few minutes and may take a lot of disk space and bandwidth.
|
||||
5. Build via `cmake --build bin --parallel --config Release -t BeamMP-Server`.
|
||||
6. Your executable can be found in `bin/`.
|
||||
|
||||
When you make changes to the code, you only have to run step 4 again.
|
||||
### Building for FreeBSD
|
||||
|
||||
@@ -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)
|
||||
|
||||
+21
-5
@@ -18,12 +18,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
#include "BoostAliases.h"
|
||||
#include "Common.h"
|
||||
@@ -56,19 +58,22 @@ public:
|
||||
~TClient();
|
||||
TClient& operator=(const TClient&) = delete;
|
||||
|
||||
void AddNewCar(int Ident, const std::string& Data);
|
||||
void SetCarData(int Ident, const std::string& Data);
|
||||
void AddNewCar(int Ident, const nlohmann::json& Data);
|
||||
void SetCarData(int Ident, const nlohmann::json& Data);
|
||||
void SetCarPosition(int Ident, const std::string& Data);
|
||||
TVehicleDataLockPair GetAllCars();
|
||||
void SetName(const std::string& Name) { mName = Name; }
|
||||
void SetRoles(const std::string& Role) { mRole = Role; }
|
||||
void SetIdentifier(const std::string& key, const std::string& value) { mIdentifiers[key] = value; }
|
||||
std::string GetCarData(int Ident);
|
||||
nlohmann::json GetCarData(int Ident);
|
||||
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; }
|
||||
@@ -101,8 +106,16 @@ public:
|
||||
[[nodiscard]] TServer& Server() const;
|
||||
void UpdatePingTime();
|
||||
int SecondsSinceLastPing();
|
||||
void SetMagic(std::vector<uint8_t> magic) { mMagic = std::move(magic); }
|
||||
[[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;
|
||||
@@ -118,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;
|
||||
@@ -125,6 +140,7 @@ private:
|
||||
std::string mDID;
|
||||
int mID = -1;
|
||||
std::chrono::time_point<std::chrono::high_resolution_clock> mLastPingTime = std::chrono::high_resolution_clock::now();
|
||||
std::vector<uint8_t> mMagic;
|
||||
};
|
||||
|
||||
std::optional<std::weak_ptr<TClient>> GetClient(class TServer& Server, int ID);
|
||||
|
||||
+19
-15
@@ -74,20 +74,18 @@ public:
|
||||
static TConsole& Console() { return mConsole; }
|
||||
static std::string ServerVersionString();
|
||||
static const Version& ServerVersion() { return mVersion; }
|
||||
static uint8_t ClientMajorVersion() { return 2; }
|
||||
static Version ClientMinimumVersion() { return Version { 2, 7, 0 }; }
|
||||
static std::string PPS() { return mPPS; }
|
||||
static void SetPPS(const std::string& NewPPS) { mPPS = NewPPS; }
|
||||
|
||||
static inline struct Settings Settings { };
|
||||
|
||||
static std::vector<std::string> GetBackendUrlsInOrder() {
|
||||
return {
|
||||
"backend.beammp.com",
|
||||
};
|
||||
}
|
||||
static std::vector<std::string> GetBackendUrlsInOrder();
|
||||
|
||||
static std::string GetBackendUrlForAuth() { return "auth.beammp.com"; }
|
||||
static std::string GetBackendUrlForSocketIO() { return "https://backend.beammp.com"; }
|
||||
static std::string GetServerCheckUrl();
|
||||
|
||||
static std::string GetBackendUrlForAuth();
|
||||
static std::string GetBackendUrlForSocketIO();
|
||||
static void CheckForUpdates();
|
||||
static std::array<uint8_t, 3> VersionStrToInts(const std::string& str);
|
||||
static bool IsOutdated(const Version& Current, const Version& Newest);
|
||||
@@ -126,11 +124,18 @@ private:
|
||||
static inline bool mShutdown { false };
|
||||
static inline std::mutex mShutdownHandlersMutex {};
|
||||
static inline std::deque<TShutdownHandler> mShutdownHandlers {};
|
||||
static inline int mTLDIndex { 0 };
|
||||
static inline std::vector<std::string> mValidTLDs {"beammp.com", "beammp.ru"};
|
||||
|
||||
static inline Version mVersion { 3, 6, 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);
|
||||
|
||||
std::string ThreadName(bool DebugModeOverride = false);
|
||||
void RegisterThread(const std::string& str);
|
||||
@@ -139,7 +144,6 @@ void RegisterThread(const std::string& str);
|
||||
#define KB 1024llu
|
||||
#define MB (KB * 1024llu)
|
||||
#define GB (MB * 1024llu)
|
||||
#define SSU_UNRAW SECRET_SENTRY_URL
|
||||
|
||||
#define _file_basename std::filesystem::path(__FILE__).filename().string()
|
||||
#define _line std::to_string(__LINE__)
|
||||
@@ -162,13 +166,13 @@ void RegisterThread(const std::string& str);
|
||||
#else
|
||||
#define _function_name std::string(__func__)
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define DEBUG
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(DEBUG)
|
||||
|
||||
|
||||
// if this is defined, we will show the full function signature infront of
|
||||
// each info/debug/warn... call instead of the 'filename:line' format.
|
||||
#if defined(BMP_FULL_FUNCTION_NAMES)
|
||||
@@ -178,7 +182,7 @@ void RegisterThread(const std::string& str);
|
||||
#endif
|
||||
|
||||
#endif // defined(DEBUG)
|
||||
|
||||
|
||||
#define beammp_warn(x) Application::Console().Write(_this_location + std::string("[WARN] ") + (x))
|
||||
#define beammp_info(x) Application::Console().Write(_this_location + std::string("[INFO] ") + (x))
|
||||
#define beammp_error(x) \
|
||||
@@ -221,7 +225,7 @@ void RegisterThread(const std::string& str);
|
||||
#else
|
||||
#define beammp_trace(x)
|
||||
#endif // defined(DEBUG)
|
||||
|
||||
|
||||
#define beammp_errorf(...) beammp_error(fmt::format(__VA_ARGS__))
|
||||
#define beammp_infof(...) beammp_info(fmt::format(__VA_ARGS__))
|
||||
#define beammp_debugf(...) beammp_debug(fmt::format(__VA_ARGS__))
|
||||
|
||||
@@ -26,7 +26,9 @@ enum class Key {
|
||||
// provider settings
|
||||
PROVIDER_UPDATE_MESSAGE,
|
||||
PROVIDER_DISABLE_CONFIG,
|
||||
PROVIDER_DISABLE_MP_SET,
|
||||
PROVIDER_PORT_ENV,
|
||||
PROVIDER_IP_ENV
|
||||
};
|
||||
|
||||
std::optional<std::string> Get(Key key);
|
||||
|
||||
+3
-2
@@ -23,6 +23,7 @@
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#if defined(BEAMMP_LINUX)
|
||||
#pragma GCC diagnostic push
|
||||
@@ -38,8 +39,8 @@
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace Http {
|
||||
std::string GET(const std::string& host, int port, const std::string& target, unsigned int* status = nullptr);
|
||||
std::string POST(const std::string& host, int port, const std::string& target, const std::string& body, const std::string& ContentType, unsigned int* status = nullptr, const httplib::Headers& headers = {});
|
||||
std::string GET(const std::string& url, unsigned int* status = nullptr);
|
||||
std::string POST(const std::string& url, const std::string& body, const std::string& ContentType, unsigned int* status = nullptr, const std::map<std::string, std::string>& headers = {});
|
||||
namespace Status {
|
||||
std::string ToString(int code);
|
||||
}
|
||||
|
||||
+2
-1
@@ -34,8 +34,9 @@ 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);
|
||||
void Set(int ConfigID, sol::object NewValue);
|
||||
TLuaValue Get(int ConfigID);
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
|
||||
#undef max
|
||||
#undef min
|
||||
|
||||
namespace prof {
|
||||
|
||||
using Duration = std::chrono::duration<double, std::milli>;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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 <vector>
|
||||
#include <string>
|
||||
#include <array>
|
||||
|
||||
class RegionHandler final {
|
||||
public:
|
||||
RegionHandler() = delete;
|
||||
static void TopLevelDomainFailed();
|
||||
static std::string RegionToTopLevelDomain();
|
||||
private:
|
||||
static inline unsigned int mRegionIndex { 0 };
|
||||
const static inline std::array<std::string, 2> mValidTLDs {"beammp.com", "beammp.ru"};
|
||||
};
|
||||
+4
-3
@@ -68,8 +68,6 @@ struct Settings {
|
||||
// Keys have their TOML section name as prefix
|
||||
|
||||
// [Misc]
|
||||
Misc_SendErrorsShowMessage,
|
||||
Misc_SendErrors,
|
||||
Misc_ImScaredOfUpdates,
|
||||
Misc_UpdateReminderTime,
|
||||
|
||||
@@ -81,12 +79,15 @@ struct Settings {
|
||||
General_Map,
|
||||
General_AuthKey,
|
||||
General_Private,
|
||||
General_IP,
|
||||
General_Port,
|
||||
General_MaxCars,
|
||||
General_LogChat,
|
||||
General_ResourceFolder,
|
||||
General_Debug,
|
||||
General_AllowGuests
|
||||
General_AllowGuests,
|
||||
General_InformationPacket,
|
||||
General_Region,
|
||||
};
|
||||
|
||||
Sync<std::unordered_map<Key, SettingsTypeVariant>> SettingsMap;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -59,6 +59,9 @@ private:
|
||||
void Command_Settings(const std::string& cmd, const std::vector<std::string>& args);
|
||||
void Command_Clear(const std::string&, const std::vector<std::string>& args);
|
||||
void Command_Version(const std::string& cmd, const std::vector<std::string>& args);
|
||||
void Command_ProtectMod(const std::string& cmd, const std::vector<std::string>& args);
|
||||
void Command_ReloadMods(const std::string& cmd, const std::vector<std::string>& args);
|
||||
void Command_NetTest(const std::string& cmd, const std::vector<std::string>& args);
|
||||
|
||||
void Command_Say(const std::string& FullCommand);
|
||||
bool EnsureArgsCount(const std::vector<std::string>& args, size_t n);
|
||||
@@ -77,6 +80,9 @@ private:
|
||||
{ "clear", [this](const auto& a, const auto& b) { Command_Clear(a, b); } },
|
||||
{ "say", [this](const auto&, const auto&) { Command_Say(""); } }, // shouldn't actually be called
|
||||
{ "version", [this](const auto& a, const auto& b) { Command_Version(a, b); } },
|
||||
{ "protectmod", [this](const auto& a, const auto& b) { Command_ProtectMod(a, b); } },
|
||||
{ "reloadmods", [this](const auto& a, const auto& b) { Command_ReloadMods(a, b); } },
|
||||
{ "nettest", [this](const auto& a, const auto& b) { Command_NetTest(a, b); } },
|
||||
};
|
||||
|
||||
std::unique_ptr<Commandline> mCommandline { nullptr };
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
//~THeartbeatThread();
|
||||
void operator()() override;
|
||||
|
||||
static inline std::string lastCall = "";
|
||||
private:
|
||||
std::string GenerateCall();
|
||||
std::string GetPlayers();
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
+17
-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,23 @@ 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; }
|
||||
|
||||
private:
|
||||
void UDPServerMain();
|
||||
@@ -51,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);
|
||||
@@ -65,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);
|
||||
|
||||
@@ -30,10 +30,10 @@ public:
|
||||
[[nodiscard]] std::string TrimmedList() const { return mTrimmedList; }
|
||||
[[nodiscard]] std::string FileSizes() const { return mFileSizes; }
|
||||
[[nodiscard]] int ModsLoaded() const { return mModsLoaded; }
|
||||
|
||||
[[nodiscard]] std::string NewFileList() const;
|
||||
[[nodiscard]] nlohmann::json GetMods() const { return mMods; }
|
||||
|
||||
void RefreshFiles();
|
||||
void SetProtected(const std::string& ModName, bool Protected);
|
||||
|
||||
private:
|
||||
size_t mMaxModSize = 0;
|
||||
@@ -43,5 +43,5 @@ private:
|
||||
int mModsLoaded = 0;
|
||||
|
||||
std::mutex mModsMutex;
|
||||
nlohmann::json mMods;
|
||||
nlohmann::json mMods = nlohmann::json::array();
|
||||
};
|
||||
|
||||
+4
-4
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "IThreaded.h"
|
||||
#include "RWMutex.h"
|
||||
#include "TIoPollThread.h"
|
||||
#include "TScopedTimer.h"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -44,17 +45,16 @@ public:
|
||||
void ForEachClient(const std::function<bool(std::weak_ptr<TClient>)>& Fn);
|
||||
size_t ClientCount() const;
|
||||
|
||||
void GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uint8_t>&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network);
|
||||
void GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uint8_t>&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network, bool udp);
|
||||
static void HandleEvent(TClient& c, const std::string& Data);
|
||||
RWMutex& GetClientMutex() const { return mClientsMutex; }
|
||||
|
||||
const TScopedTimer UptimeTimer;
|
||||
|
||||
// asio io context
|
||||
io_context& IoCtx() { return mIoCtx; }
|
||||
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);
|
||||
|
||||
@@ -18,11 +18,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
class TVehicleData final {
|
||||
public:
|
||||
TVehicleData(int ID, std::string Data);
|
||||
TVehicleData(int ID, nlohmann::json Data);
|
||||
~TVehicleData();
|
||||
// We cannot delete this, since vector needs to be able to copy when it resizes.
|
||||
// Deleting this causes some wacky template errors which are hard to decipher,
|
||||
@@ -32,14 +33,16 @@ public:
|
||||
[[nodiscard]] bool IsInvalid() const { return mID == -1; }
|
||||
[[nodiscard]] int ID() const { return mID; }
|
||||
|
||||
[[nodiscard]] std::string Data() const { return mData; }
|
||||
void SetData(const std::string& Data) { mData = Data; }
|
||||
[[nodiscard]] nlohmann::json Data() const { return mData; }
|
||||
[[nodiscard]] std::string DataAsPacket(const std::string& Role, const std::string& Name, int ID) const;
|
||||
|
||||
void SetData(const nlohmann::json& Data) { mData = Data; }
|
||||
|
||||
bool operator==(const TVehicleData& v) const { return mID == v.mID; }
|
||||
|
||||
private:
|
||||
int mID { -1 };
|
||||
std::string mData;
|
||||
nlohmann::json mData;
|
||||
};
|
||||
|
||||
// TODO: unused now, remove?
|
||||
|
||||
@@ -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
|
||||
|
||||
+26
-14
@@ -57,7 +57,7 @@ int TClient::GetOpenCarID() const {
|
||||
return OpenID;
|
||||
}
|
||||
|
||||
void TClient::AddNewCar(int Ident, const std::string& Data) {
|
||||
void TClient::AddNewCar(int Ident, const nlohmann::json& Data) {
|
||||
std::unique_lock lock(mVehicleDataMutex);
|
||||
mVehicleData.emplace_back(Ident, Data);
|
||||
}
|
||||
@@ -76,17 +76,30 @@ 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;
|
||||
mSocket.shutdown(socket_base::shutdown_both, ec);
|
||||
if (ec) {
|
||||
beammp_debugf("Failed to shutdown client socket: {}", ec.message());
|
||||
}
|
||||
mSocket.close(ec);
|
||||
if (ec) {
|
||||
beammp_debugf("Failed to close client socket: {}", ec.message());
|
||||
if (mSocket.is_open()) {
|
||||
mSocket.shutdown(socket_base::shutdown_both, ec);
|
||||
if (ec) {
|
||||
beammp_debugf("Failed to shutdown client socket: {}", ec.message());
|
||||
}
|
||||
mSocket.close(ec);
|
||||
if (ec) {
|
||||
beammp_debugf("Failed to close client socket: {}", ec.message());
|
||||
}
|
||||
} 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) {
|
||||
@@ -94,7 +107,7 @@ void TClient::SetCarPosition(int Ident, const std::string& Data) {
|
||||
mVehiclePosition[size_t(Ident)] = Data;
|
||||
}
|
||||
|
||||
std::string TClient::GetCarData(int Ident) {
|
||||
nlohmann::json TClient::GetCarData(int Ident) {
|
||||
{ // lock
|
||||
std::unique_lock lock(mVehicleDataMutex);
|
||||
for (auto& v : mVehicleData) {
|
||||
@@ -104,10 +117,10 @@ std::string TClient::GetCarData(int Ident) {
|
||||
}
|
||||
} // unlock
|
||||
DeleteCar(Ident);
|
||||
return "";
|
||||
return nlohmann::detail::value_t::null;
|
||||
}
|
||||
|
||||
void TClient::SetCarData(int Ident, const std::string& Data) {
|
||||
void TClient::SetCarData(int Ident, const nlohmann::json& Data) {
|
||||
{ // lock
|
||||
std::unique_lock lock(mVehicleDataMutex);
|
||||
for (auto& v : mVehicleData) {
|
||||
@@ -165,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
-1
@@ -31,9 +31,11 @@
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
#include "TLuaResult.h"
|
||||
#include "Compat.h"
|
||||
#include "CustomAssert.h"
|
||||
#include "Http.h"
|
||||
#include "RegionHandler.h"
|
||||
|
||||
void Application::RegisterShutdownHandler(const TShutdownHandler& Handler) {
|
||||
std::unique_lock Lock(mShutdownHandlersMutex);
|
||||
@@ -72,6 +74,27 @@ std::string Application::ServerVersionString() {
|
||||
return mVersion.AsString();
|
||||
}
|
||||
|
||||
std::vector<std::string> Application::GetBackendUrlsInOrder() {
|
||||
return {
|
||||
"https://backend." + RegionHandler::RegionToTopLevelDomain(),
|
||||
};
|
||||
}
|
||||
|
||||
std::string Application::GetServerCheckUrl()
|
||||
{
|
||||
return "https://check." + RegionHandler::RegionToTopLevelDomain();
|
||||
}
|
||||
|
||||
std::string Application::GetBackendUrlForAuth()
|
||||
{
|
||||
return "https://auth." + RegionHandler::RegionToTopLevelDomain();
|
||||
}
|
||||
|
||||
std::string Application::GetBackendUrlForSocketIO()
|
||||
{
|
||||
return "https://backend." + RegionHandler::RegionToTopLevelDomain();
|
||||
}
|
||||
|
||||
std::array<uint8_t, 3> Application::VersionStrToInts(const std::string& str) {
|
||||
std::array<uint8_t, 3> Version;
|
||||
std::stringstream ss(str);
|
||||
@@ -215,7 +238,7 @@ void Application::CheckForUpdates() {
|
||||
// checks current version against latest version
|
||||
std::regex VersionRegex { R"(\d+\.\d+\.\d+\n*)" };
|
||||
for (const auto& url : GetBackendUrlsInOrder()) {
|
||||
auto Response = Http::GET(url, 443, "/v/s");
|
||||
auto Response = Http::GET(url + "/v/s");
|
||||
bool Matches = std::regex_match(Response, VersionRegex);
|
||||
if (Matches) {
|
||||
auto MyVersion = ServerVersion();
|
||||
@@ -384,6 +407,13 @@ void SplitString(const std::string& str, const char delim, std::vector<std::stri
|
||||
out.push_back(str.substr(start, end - start));
|
||||
}
|
||||
}
|
||||
|
||||
std::string LowerString(std::string str) {
|
||||
std::ranges::transform(str, str.begin(), ::tolower);
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
static constexpr size_t STARTING_MAX_DECOMPRESSION_BUFFER_SIZE = 15 * 1024 * 1024;
|
||||
static constexpr size_t MAX_DECOMPRESSION_BUFFER_SIZE = 30 * 1024 * 1024;
|
||||
|
||||
|
||||
@@ -36,9 +36,15 @@ 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;
|
||||
case Key::PROVIDER_IP_ENV:
|
||||
return "BEAMMP_PROVIDER_IP_ENV";
|
||||
break;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
+169
-52
@@ -18,78 +18,195 @@
|
||||
|
||||
#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;
|
||||
struct Connection {
|
||||
std::string host {};
|
||||
int port {};
|
||||
Connection() = default;
|
||||
Connection(std::string host, int port)
|
||||
: host(host)
|
||||
, port(port) {};
|
||||
|
||||
static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
std::string* Result = reinterpret_cast<std::string*>(userp);
|
||||
std::string NewContents(reinterpret_cast<char*>(contents), size * nmemb);
|
||||
*Result += NewContents;
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
struct CurlDeleter {
|
||||
void operator()(CURL* c) const noexcept {
|
||||
if (c)
|
||||
curl_easy_cleanup(c);
|
||||
}
|
||||
};
|
||||
constexpr uint8_t CONNECTION_AMOUNT = 10;
|
||||
static thread_local uint8_t write_index = 0;
|
||||
static thread_local std::array<Connection, CONNECTION_AMOUNT> connections;
|
||||
static thread_local std::array<std::shared_ptr<httplib::SSLClient>, CONNECTION_AMOUNT> clients;
|
||||
|
||||
[[nodiscard]] static std::shared_ptr<httplib::SSLClient> getClient(Connection connectionInfo) {
|
||||
for (uint8_t i = 0; i < CONNECTION_AMOUNT; i++) {
|
||||
if (connectionInfo.host == connections[i].host
|
||||
&& connectionInfo.port == connections[i].port) {
|
||||
beammp_tracef("Old client reconnected, with ip {} and port {}", connectionInfo.host, connectionInfo.port);
|
||||
return clients[i];
|
||||
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;
|
||||
}
|
||||
}
|
||||
uint8_t i = write_index;
|
||||
write_index++;
|
||||
write_index %= CONNECTION_AMOUNT;
|
||||
clients[i] = std::make_shared<httplib::SSLClient>(connectionInfo.host, connectionInfo.port);
|
||||
connections[i] = { connectionInfo.host, connectionInfo.port };
|
||||
beammp_tracef("New client connected, with ip {} and port {}", connectionInfo.host, connectionInfo.port);
|
||||
return clients[i];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
std::string Http::GET(const std::string& host, int port, const std::string& target, unsigned int* status) {
|
||||
std::shared_ptr<httplib::SSLClient> client = getClient({ host, port });
|
||||
client->enable_server_certificate_verification(false);
|
||||
client->set_address_family(AF_INET);
|
||||
auto res = client->Get(target.c_str());
|
||||
if (res) {
|
||||
if (status) {
|
||||
*status = res->status;
|
||||
}
|
||||
return res->body;
|
||||
} else {
|
||||
return Http::ErrorString;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
std::string Http::POST(const std::string& host, int port, const std::string& target, const std::string& body, const std::string& ContentType, unsigned int* status, const httplib::Headers& headers) {
|
||||
std::shared_ptr<httplib::SSLClient> client = getClient({ host, port });
|
||||
client->set_read_timeout(std::chrono::seconds(10));
|
||||
beammp_assert(client->is_valid());
|
||||
client->enable_server_certificate_verification(false);
|
||||
client->set_address_family(AF_INET);
|
||||
auto res = client->Post(target.c_str(), headers, body.c_str(), body.size(), ContentType.c_str());
|
||||
if (res) {
|
||||
if (status) {
|
||||
*status = res->status;
|
||||
/// 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 res->body;
|
||||
return *this;
|
||||
}
|
||||
|
||||
CURL* GetHandle() const noexcept {
|
||||
return mHandle;
|
||||
}
|
||||
};
|
||||
|
||||
std::string Http::GET(const std::string& url, unsigned int* status) {
|
||||
std::string Ret;
|
||||
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, 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;
|
||||
res = curl_easy_perform(curl);
|
||||
if (res != CURLE_OK) {
|
||||
beammp_error("GET to " + url + " failed: " + std::string(curl_easy_strerror(res)));
|
||||
beammp_error("Curl error: " + std::string(errbuf));
|
||||
return Http::ErrorString;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status);
|
||||
}
|
||||
|
||||
} else {
|
||||
beammp_debug("POST failed: " + httplib::to_string(res.error()));
|
||||
beammp_error("Curl easy init failed");
|
||||
return Http::ErrorString;
|
||||
}
|
||||
return Ret;
|
||||
}
|
||||
|
||||
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;
|
||||
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, 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;
|
||||
list = curl_slist_append(list, ("Content-Type: " + ContentType).c_str());
|
||||
|
||||
for (auto [header, value] : headers) {
|
||||
list = curl_slist_append(list, (header + ": " + value).c_str());
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
|
||||
|
||||
// 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;
|
||||
res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(list);
|
||||
if (res != CURLE_OK) {
|
||||
beammp_error("POST to " + url + " failed: " + std::string(curl_easy_strerror(res)));
|
||||
beammp_error("Curl error: " + std::string(errbuf));
|
||||
return Http::ErrorString;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status);
|
||||
}
|
||||
|
||||
} else {
|
||||
beammp_error("Curl easy init failed");
|
||||
return Http::ErrorString;
|
||||
}
|
||||
return Ret;
|
||||
}
|
||||
|
||||
// RFC 2616, RFC 7231
|
||||
|
||||
+157
-62
@@ -143,17 +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" };
|
||||
if (MaybeClient) {
|
||||
if (auto c = MaybeClient.value().lock()) {
|
||||
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, "" };
|
||||
}
|
||||
}
|
||||
auto c = MaybeClient.value().lock();
|
||||
if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
|
||||
beammp_lua_errorf("Respond failed, dropping client {}", PlayerID);
|
||||
LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets");
|
||||
return { false, "Respond failed, dropping client" };
|
||||
}
|
||||
return { true, "" };
|
||||
|
||||
beammp_lua_errorf("TriggerClientEvent invalid Player ID '{}'", PlayerID);
|
||||
return { false, "Invalid Player ID" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,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;
|
||||
@@ -207,25 +224,30 @@ std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::stri
|
||||
|
||||
std::pair<bool, std::string> LuaAPI::MP::SendNotification(int ID, const std::string& Message, const std::string& Icon, const std::string& Category) {
|
||||
std::pair<bool, std::string> Result;
|
||||
std::string Packet = "N" + Category + ":" + Icon + ":" + Message;
|
||||
std::string Packet = "n" + Category + ":" + Icon + ":" + Message;
|
||||
if (ID == -1) {
|
||||
Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
|
||||
Result.first = true;
|
||||
} 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;
|
||||
@@ -236,25 +258,77 @@ std::pair<bool, std::string> LuaAPI::MP::SendNotification(int ID, const std::str
|
||||
return Result;
|
||||
}
|
||||
|
||||
std::pair<bool, std::string> LuaAPI::MP::ConfirmationDialog(int ID, const std::string& Title, const std::string& Body, const sol::table& buttons, const std::string& InteractionID, const bool& warning, const bool& reportToServer, const bool& reportToExtensions) {
|
||||
std::pair<bool, std::string> Result;
|
||||
|
||||
const nlohmann::json PacketBody = {
|
||||
{ "title", Title },
|
||||
{ "body", Body },
|
||||
{ "buttons", nlohmann::json::parse(JsonEncode(buttons), nullptr, false) },
|
||||
{ "interactionID", InteractionID },
|
||||
{ "class", warning ? "experimental" : "" },
|
||||
{ "reportToServer", reportToServer },
|
||||
{ "reportToExtensions", reportToExtensions }
|
||||
};
|
||||
|
||||
std::string Packet = "D" + PacketBody.dump();
|
||||
if (ID == -1) {
|
||||
Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
|
||||
Result.first = true;
|
||||
} else {
|
||||
auto MaybeClient = GetClient(Engine->Server(), ID);
|
||||
if (MaybeClient) {
|
||||
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 = "Invalid Player ID";
|
||||
}
|
||||
} else {
|
||||
beammp_lua_error("ConfirmationDialog invalid argument [1] invalid ID");
|
||||
Result.first = false;
|
||||
Result.second = "Invalid Player ID";
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) {
|
||||
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).empty()) {
|
||||
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;
|
||||
}
|
||||
@@ -317,6 +391,14 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
|
||||
beammp_lua_error("set invalid argument [2] expected string");
|
||||
}
|
||||
break;
|
||||
case 7: // Information packet
|
||||
if (NewValue.is<bool>()) {
|
||||
Application::Settings.set(Settings::Key::General_InformationPacket, NewValue.as<bool>());
|
||||
beammp_info(std::string("Set `InformationPacket` to ") + (Application::Settings.getAsBool(Settings::Key::General_InformationPacket) ? "true" : "false"));
|
||||
} else {
|
||||
beammp_lua_error("set invalid argument [2] expected boolean");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
beammp_warn("Invalid config ID \"" + std::to_string(ConfigID) + "\". Use `MP.Settings.*` enum for this.");
|
||||
break;
|
||||
@@ -339,6 +421,8 @@ TLuaValue LuaAPI::MP::Get(int ConfigID) {
|
||||
return Application::Settings.getAsString(Settings::Key::General_Name);
|
||||
case 6: // Desc
|
||||
return Application::Settings.getAsString(Settings::Key::General_Description);
|
||||
case 7: // Information packet
|
||||
return Application::Settings.getAsBool(Settings::Key::General_InformationPacket);
|
||||
default:
|
||||
beammp_warn("Invalid config ID \"" + std::to_string(ConfigID) + "\". Use `MP.Settings.*` enum for this.");
|
||||
return 0;
|
||||
@@ -351,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) {
|
||||
@@ -379,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,35 @@
|
||||
// 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 "RegionHandler.h"
|
||||
#include "Common.h"
|
||||
|
||||
void RegionHandler::TopLevelDomainFailed()
|
||||
{
|
||||
beammp_info("Top level domain of " + mValidTLDs[mRegionIndex % mValidTLDs.size()] + " didn't respond correctly , changing domain to " + mValidTLDs[(mRegionIndex + 1) % mValidTLDs.size()]);
|
||||
mRegionIndex++;
|
||||
}
|
||||
|
||||
std::string RegionHandler::RegionToTopLevelDomain()
|
||||
{
|
||||
static bool isDeveloperRegion = Application::Settings.getAsString(Settings::Key::General_Region) == "Developer";
|
||||
if (isDeveloperRegion) {
|
||||
return "beammp.dev";
|
||||
}
|
||||
return mValidTLDs[mRegionIndex % mValidTLDs.size()]; // Global
|
||||
}
|
||||
+6
-4
@@ -28,14 +28,15 @@ Settings::Settings() {
|
||||
{ General_Map, std::string("/levels/gridmap_v2/info.json") },
|
||||
{ General_AuthKey, std::string("") },
|
||||
{ General_Private, true },
|
||||
{ General_IP, "::"},
|
||||
{ General_Port, 30814 },
|
||||
{ General_MaxCars, 1 },
|
||||
{ General_LogChat, true },
|
||||
{ General_ResourceFolder, std::string("Resources") },
|
||||
{ General_Debug, false },
|
||||
{ General_AllowGuests, true },
|
||||
{ Misc_SendErrorsShowMessage, true },
|
||||
{ Misc_SendErrors, true },
|
||||
{ General_InformationPacket, true },
|
||||
{ General_Region, std::string("")},
|
||||
{ Misc_ImScaredOfUpdates, true },
|
||||
{ Misc_UpdateReminderTime, "30s" }
|
||||
};
|
||||
@@ -48,14 +49,15 @@ Settings::Settings() {
|
||||
{ { "General", "Map" }, { General_Map, READ_WRITE } },
|
||||
{ { "General", "AuthKey" }, { General_AuthKey, NO_ACCESS } },
|
||||
{ { "General", "Private" }, { General_Private, READ_ONLY } },
|
||||
{ { "General", "IP" }, { General_IP, READ_ONLY } },
|
||||
{ { "General", "Port" }, { General_Port, READ_ONLY } },
|
||||
{ { "General", "MaxCars" }, { General_MaxCars, READ_WRITE } },
|
||||
{ { "General", "LogChat" }, { General_LogChat, READ_ONLY } },
|
||||
{ { "General", "ResourceFolder" }, { General_ResourceFolder, READ_ONLY } },
|
||||
{ { "General", "Debug" }, { General_Debug, READ_WRITE } },
|
||||
{ { "General", "AllowGuests" }, { General_AllowGuests, READ_WRITE } },
|
||||
{ { "Misc", "SendErrorsShowMessage" }, { Misc_SendErrorsShowMessage, READ_WRITE } },
|
||||
{ { "Misc", "SendErrors" }, { Misc_SendErrors, READ_WRITE } },
|
||||
{ { "General", "InformationPacket" }, { General_InformationPacket, READ_WRITE } },
|
||||
{ { "General", "Region" }, { General_Region, READ_WRITE } },
|
||||
{ { "Misc", "ImScaredOfUpdates" }, { Misc_ImScaredOfUpdates, READ_WRITE } },
|
||||
{ { "Misc", "UpdateReminderTime" }, { Misc_UpdateReminderTime, READ_WRITE } }
|
||||
};
|
||||
|
||||
+38
-11
@@ -34,6 +34,8 @@ static constexpr std::string_view StrDebug = "Debug";
|
||||
static constexpr std::string_view EnvStrDebug = "BEAMMP_DEBUG";
|
||||
static constexpr std::string_view StrPrivate = "Private";
|
||||
static constexpr std::string_view EnvStrPrivate = "BEAMMP_PRIVATE";
|
||||
static constexpr std::string_view StrIP = "IP";
|
||||
static constexpr std::string_view EnvStrIP = "BEAMMP_IP";
|
||||
static constexpr std::string_view StrPort = "Port";
|
||||
static constexpr std::string_view EnvStrPort = "BEAMMP_PORT";
|
||||
static constexpr std::string_view StrMaxCars = "MaxCars";
|
||||
@@ -50,19 +52,23 @@ static constexpr std::string_view StrTags = "Tags";
|
||||
static constexpr std::string_view EnvStrTags = "BEAMMP_TAGS";
|
||||
static constexpr std::string_view StrResourceFolder = "ResourceFolder";
|
||||
static constexpr std::string_view EnvStrResourceFolder = "BEAMMP_RESOURCE_FOLDER";
|
||||
static constexpr std::string_view StrRegion = "BackendRegion";
|
||||
static constexpr std::string_view EnvStrRegion = "BEAMMP_BACKEND_REGION";
|
||||
static constexpr std::string_view StrAuthKey = "AuthKey";
|
||||
static constexpr std::string_view EnvStrAuthKey = "BEAMMP_AUTH_KEY";
|
||||
static constexpr std::string_view StrLogChat = "LogChat";
|
||||
static constexpr std::string_view EnvStrLogChat = "BEAMMP_LOG_CHAT";
|
||||
static constexpr std::string_view StrAllowGuests = "AllowGuests";
|
||||
static constexpr std::string_view EnvStrAllowGuests = "BEAMMP_ALLOW_GUESTS";
|
||||
static constexpr std::string_view StrInformationPacket = "InformationPacket";
|
||||
static constexpr std::string_view EnvStrInformationPacket = "BEAMMP_INFORMATION_PACKET";
|
||||
static constexpr std::string_view StrPassword = "Password";
|
||||
|
||||
// Misc
|
||||
static constexpr std::string_view StrSendErrors = "SendErrors";
|
||||
static constexpr std::string_view StrSendErrorsMessageEnabled = "SendErrorsShowMessage";
|
||||
static constexpr std::string_view StrHideUpdateMessages = "ImScaredOfUpdates";
|
||||
static constexpr std::string_view EnvStrHideUpdateMessages = "BEAMMP_IM_SCARED_OF_UPDATES";
|
||||
static constexpr std::string_view StrUpdateReminderTime = "UpdateReminderTime";
|
||||
static constexpr std::string_view EnvStrUpdateReminderTime = "BEAMMP_UPDATE_REMINDER_TIME";
|
||||
|
||||
TEST_CASE("TConfig::TConfig") {
|
||||
const std::string CfgFile = "beammp_server_testconfig.toml";
|
||||
@@ -132,8 +138,12 @@ void TConfig::FlushToFile() {
|
||||
SetComment(data["General"][StrLogChat.data()].comments(), " Whether to log chat messages in the console / log");
|
||||
data["General"][StrDebug.data()] = Application::Settings.getAsBool(Settings::Key::General_Debug);
|
||||
data["General"][StrPrivate.data()] = Application::Settings.getAsBool(Settings::Key::General_Private);
|
||||
SetComment(data["General"][StrInformationPacket.data()].comments(), " Whether to allow unconnected clients to get the public server information without joining");
|
||||
data["General"][StrInformationPacket.data()] = Application::Settings.getAsBool(Settings::Key::General_InformationPacket);
|
||||
data["General"][StrAllowGuests.data()] = Application::Settings.getAsBool(Settings::Key::General_AllowGuests);
|
||||
SetComment(data["General"][StrAllowGuests.data()].comments(), " Whether to allow guests");
|
||||
data["General"][StrIP.data()] = Application::Settings.getAsString(Settings::Key::General_IP);
|
||||
SetComment(data["General"][StrIP.data()].comments(), " The IP address to bind the server to, this is NOT related to your public IP. Can be used if your machine has multiple network interfaces");
|
||||
data["General"][StrPort.data()] = Application::Settings.getAsInt(Settings::Key::General_Port);
|
||||
data["General"][StrName.data()] = Application::Settings.getAsString(Settings::Key::General_Name);
|
||||
SetComment(data["General"][StrTags.data()].comments(), " Add custom identifying tags to your server to make it easier to find. Format should be TagA,TagB,TagC. Note the comma seperation.");
|
||||
@@ -143,17 +153,16 @@ void TConfig::FlushToFile() {
|
||||
data["General"][StrMap.data()] = Application::Settings.getAsString(Settings::Key::General_Map);
|
||||
data["General"][StrDescription.data()] = Application::Settings.getAsString(Settings::Key::General_Description);
|
||||
data["General"][StrResourceFolder.data()] = Application::Settings.getAsString(Settings::Key::General_ResourceFolder);
|
||||
if (Application::Settings.getAsString(Settings::Key::General_Region) != "") {
|
||||
data["General"][StrRegion.data()] = Application::Settings.getAsString(Settings::Key::General_Region);
|
||||
}
|
||||
// data["General"][StrPassword.data()] = Application::Settings.Password;
|
||||
// SetComment(data["General"][StrPassword.data()].comments(), " Sets a password on this server, which restricts people from joining. To join, a player must enter this exact password. Leave empty ("") to disable the password.");
|
||||
// Misc
|
||||
data["Misc"][StrHideUpdateMessages.data()] = Application::Settings.getAsBool(Settings::Key::Misc_ImScaredOfUpdates);
|
||||
SetComment(data["Misc"][StrHideUpdateMessages.data()].comments(), " Hides the periodic update message which notifies you of a new server version. You should really keep this on and always update as soon as possible. For more information visit https://wiki.beammp.com/en/home/server-maintenance#updating-the-server. An update message will always appear at startup regardless.");
|
||||
data["Misc"][StrSendErrors.data()] = Application::Settings.getAsBool(Settings::Key::Misc_SendErrors);
|
||||
data["Misc"][StrUpdateReminderTime.data()] = Application::Settings.getAsString(Settings::Key::Misc_UpdateReminderTime);
|
||||
SetComment(data["Misc"][StrUpdateReminderTime.data()].comments(), " Specifies the time between update reminders. You can use any of \"s, min, h, d\" at the end to specify the units seconds, minutes, hours or days. So 30d or 0.5min will print the update message every 30 days or half a minute.");
|
||||
SetComment(data["Misc"][StrSendErrors.data()].comments(), " If SendErrors is `true`, the server will send helpful info about crashes and other issues back to the BeamMP developers. This info may include your config, who is on your server at the time of the error, and similar general information. This kind of data is vital in helping us diagnose and fix issues faster. This has no impact on server performance. You can opt-out of this system by setting this to `false`");
|
||||
data["Misc"][StrSendErrorsMessageEnabled.data()] = Application::Settings.getAsBool(Settings::Key::Misc_SendErrorsShowMessage);
|
||||
SetComment(data["Misc"][StrSendErrorsMessageEnabled.data()].comments(), " You can turn on/off the SendErrors message you get on startup here");
|
||||
std::stringstream Ss;
|
||||
Ss << "# This is the BeamMP-Server config file.\n"
|
||||
"# Help & Documentation: `https://docs.beammp.com/server/server-maintenance/`\n"
|
||||
@@ -217,16 +226,22 @@ void TConfig::TryReadValue(toml::value& Table, const std::string& Category, cons
|
||||
if constexpr (std::is_same_v<T, std::string>) {
|
||||
if (Table[Category.c_str()][Key.data()].is_string())
|
||||
Application::Settings.set(key, Table[Category.c_str()][Key.data()].as_string());
|
||||
else if (Table[Category.c_str()][Key.data()].is_empty())
|
||||
beammp_debugf("Value '{}.{}' is empty", Category, Key);
|
||||
else
|
||||
beammp_warnf("Value '{}.{}' has unexpected type, expected type 'string'", Category, Key);
|
||||
} else if constexpr (std::is_same_v<T, int>) {
|
||||
if (Table[Category.c_str()][Key.data()].is_integer())
|
||||
Application::Settings.set(key, int(Table[Category.c_str()][Key.data()].as_integer()));
|
||||
else if (Table[Category.c_str()][Key.data()].is_empty())
|
||||
beammp_debugf("Value '{}.{}' is empty", Category, Key);
|
||||
else
|
||||
beammp_warnf("Value '{}.{}' has unexpected type, expected type 'integer'", Category, Key);
|
||||
} else if constexpr (std::is_same_v<T, bool>) {
|
||||
if (Table[Category.c_str()][Key.data()].is_boolean())
|
||||
Application::Settings.set(key, Table[Category.c_str()][Key.data()].as_boolean());
|
||||
else if (Table[Category.c_str()][Key.data()].is_empty())
|
||||
beammp_debugf("Value '{}.{}' is empty", Category, Key);
|
||||
else
|
||||
beammp_warnf("Value '{}.{}' has unexpected type, expected type 'boolean'", Category, Key);
|
||||
} else {
|
||||
@@ -248,7 +263,17 @@ void TConfig::ParseFromFile(std::string_view name) {
|
||||
// Read into new Settings Singleton
|
||||
TryReadValue(data, "General", StrDebug, EnvStrDebug, Settings::Key::General_Debug);
|
||||
TryReadValue(data, "General", StrPrivate, EnvStrPrivate, Settings::Key::General_Private);
|
||||
TryReadValue(data, "General", StrPort, EnvStrPort, Settings::Key::General_Port);
|
||||
TryReadValue(data, "General", StrInformationPacket, EnvStrInformationPacket, Settings::Key::General_InformationPacket);
|
||||
if (Env::Get(Env::Key::PROVIDER_PORT_ENV).has_value()) {
|
||||
TryReadValue(data, "General", StrPort, Env::Get(Env::Key::PROVIDER_PORT_ENV).value(), Settings::Key::General_Port);
|
||||
} else {
|
||||
TryReadValue(data, "General", StrPort, EnvStrPort, Settings::Key::General_Port);
|
||||
}
|
||||
if (Env::Get(Env::Key::PROVIDER_IP_ENV).has_value()) {
|
||||
TryReadValue(data, "General", StrIP, Env::Get(Env::Key::PROVIDER_IP_ENV).value(), Settings::Key::General_IP);
|
||||
} else {
|
||||
TryReadValue(data, "General", StrIP, EnvStrIP, Settings::Key::General_IP);
|
||||
}
|
||||
TryReadValue(data, "General", StrMaxCars, EnvStrMaxCars, Settings::Key::General_MaxCars);
|
||||
TryReadValue(data, "General", StrMaxPlayers, EnvStrMaxPlayers, Settings::Key::General_MaxPlayers);
|
||||
TryReadValue(data, "General", StrMap, EnvStrMap, Settings::Key::General_Map);
|
||||
@@ -256,14 +281,13 @@ void TConfig::ParseFromFile(std::string_view name) {
|
||||
TryReadValue(data, "General", StrDescription, EnvStrDescription, Settings::Key::General_Description);
|
||||
TryReadValue(data, "General", StrTags, EnvStrTags, Settings::Key::General_Tags);
|
||||
TryReadValue(data, "General", StrResourceFolder, EnvStrResourceFolder, Settings::Key::General_ResourceFolder);
|
||||
TryReadValue(data, "General", StrRegion, EnvStrRegion, Settings::Key::General_Region);
|
||||
TryReadValue(data, "General", StrAuthKey, EnvStrAuthKey, Settings::Key::General_AuthKey);
|
||||
TryReadValue(data, "General", StrLogChat, EnvStrLogChat, Settings::Key::General_LogChat);
|
||||
TryReadValue(data, "General", StrAllowGuests, EnvStrAllowGuests, Settings::Key::General_AllowGuests);
|
||||
// Misc
|
||||
TryReadValue(data, "Misc", StrSendErrors, "", Settings::Key::Misc_SendErrors);
|
||||
TryReadValue(data, "Misc", StrHideUpdateMessages, "", Settings::Key::Misc_ImScaredOfUpdates);
|
||||
TryReadValue(data, "Misc", StrSendErrorsMessageEnabled, "", Settings::Key::Misc_SendErrorsShowMessage);
|
||||
TryReadValue(data, "Misc", StrUpdateReminderTime, "", Settings::Key::Misc_UpdateReminderTime);
|
||||
TryReadValue(data, "Misc", StrHideUpdateMessages, EnvStrHideUpdateMessages, Settings::Key::Misc_ImScaredOfUpdates);
|
||||
TryReadValue(data, "Misc", StrUpdateReminderTime, EnvStrUpdateReminderTime, Settings::Key::Misc_UpdateReminderTime);
|
||||
|
||||
} catch (const std::exception& err) {
|
||||
beammp_error("Error parsing config file value: " + std::string(err.what()));
|
||||
@@ -299,7 +323,9 @@ void TConfig::PrintDebug() {
|
||||
}
|
||||
beammp_debug(std::string(StrDebug) + ": " + std::string(Application::Settings.getAsBool(Settings::Key::General_Debug) ? "true" : "false"));
|
||||
beammp_debug(std::string(StrPrivate) + ": " + std::string(Application::Settings.getAsBool(Settings::Key::General_Private) ? "true" : "false"));
|
||||
beammp_debug(std::string(StrInformationPacket) + ": " + std::string(Application::Settings.getAsBool(Settings::Key::General_InformationPacket) ? "true" : "false"));
|
||||
beammp_debug(std::string(StrPort) + ": " + std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)));
|
||||
beammp_debug(std::string(StrIP) + ": \"" + Application::Settings.getAsString(Settings::Key::General_IP) + "\"");
|
||||
beammp_debug(std::string(StrMaxCars) + ": " + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxCars)));
|
||||
beammp_debug(std::string(StrMaxPlayers) + ": " + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)));
|
||||
beammp_debug(std::string(StrMap) + ": \"" + Application::Settings.getAsString(Settings::Key::General_Map) + "\"");
|
||||
@@ -308,6 +334,7 @@ void TConfig::PrintDebug() {
|
||||
beammp_debug(std::string(StrTags) + ": " + TagsAsPrettyArray());
|
||||
beammp_debug(std::string(StrLogChat) + ": \"" + (Application::Settings.getAsBool(Settings::Key::General_LogChat) ? "true" : "false") + "\"");
|
||||
beammp_debug(std::string(StrResourceFolder) + ": \"" + Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "\"");
|
||||
beammp_debug(std::string(StrRegion) + ": \"" + Application::Settings.getAsString(Settings::Key::General_Region) + "\"");
|
||||
beammp_debug(std::string(StrAllowGuests) + ": \"" + (Application::Settings.getAsBool(Settings::Key::General_AllowGuests) ? "true" : "false") + "\"");
|
||||
// special!
|
||||
beammp_debug("Key Length: " + std::to_string(Application::Settings.getAsString(Settings::Key::General_AuthKey).length()) + "");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+108
-41
@@ -22,8 +22,10 @@
|
||||
|
||||
#include "Client.h"
|
||||
#include "CustomAssert.h"
|
||||
#include "Http.h"
|
||||
#include "LuaAPI.h"
|
||||
#include "TLuaEngine.h"
|
||||
#include "RegionHandler.h"
|
||||
|
||||
#include <ctime>
|
||||
#include <lua.hpp>
|
||||
@@ -159,7 +161,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) {
|
||||
@@ -197,7 +199,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);
|
||||
}
|
||||
}
|
||||
@@ -208,16 +210,18 @@ void TConsole::Command_Help(const std::string&, const std::vector<std::string>&
|
||||
}
|
||||
static constexpr const char* sHelpString = R"(
|
||||
Commands:
|
||||
help displays this help
|
||||
exit shuts down the server
|
||||
kick <name> [reason] kicks specified player with an optional reason
|
||||
list lists all players and info about them
|
||||
say <message> sends the message to all players in chat
|
||||
lua [state id] switches to lua, optionally into a specific state id's lua
|
||||
settings [command] sets or gets settings for the server, run `settings help` for more info
|
||||
status how the server is doing and what it's up to
|
||||
clear clears the console window
|
||||
version displays the server version)";
|
||||
help displays this help
|
||||
exit shuts down the server
|
||||
kick <name> [reason] kicks specified player with an optional reason
|
||||
list lists all players and info about them
|
||||
say <message> sends the message to all players in chat
|
||||
lua [state id] switches to lua, optionally into a specific state id's lua
|
||||
settings [command] sets or gets settings for the server, run `settings help` for more info
|
||||
status how the server is doing and what it's up to
|
||||
clear clears the console window
|
||||
version displays the server version
|
||||
protectmod <name> <value> sets whether a mod is protected, value can be true or false
|
||||
reloadmods reloads all mods from the Resources Client folder)";
|
||||
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
|
||||
}
|
||||
|
||||
@@ -262,6 +266,59 @@ void TConsole::Command_Version(const std::string& cmd, const std::vector<std::st
|
||||
std::string openssl_version = fmt::format("OpenSSL: v{}.{}.{}", OPENSSL_VERSION_MAJOR, OPENSSL_VERSION_MINOR, OPENSSL_VERSION_PATCH);
|
||||
Application::Console().WriteRaw(openssl_version);
|
||||
}
|
||||
void TConsole::Command_ProtectMod(const std::string& cmd, const std::vector<std::string>& args) {
|
||||
if (!EnsureArgsCount(args, 2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& ModName = args.at(0);
|
||||
const auto& Protect = args.at(1);
|
||||
|
||||
for (auto mod : mLuaEngine->Network().ResourceManager().GetMods()) {
|
||||
if (mod["file_name"].get<std::string>() == ModName) {
|
||||
mLuaEngine->Network().ResourceManager().SetProtected(ModName, Protect == "true");
|
||||
Application::Console().WriteRaw("Mod " + ModName + " is now " + (Protect == "true" ? "protected" : "unprotected"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Application::Console().WriteRaw("Mod " + ModName + " not found.");
|
||||
}
|
||||
void TConsole::Command_ReloadMods(const std::string& cmd, const std::vector<std::string>& args) {
|
||||
if (!EnsureArgsCount(args, 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mLuaEngine->Network().ResourceManager().RefreshFiles();
|
||||
Application::Console().WriteRaw("Mods reloaded.");
|
||||
}
|
||||
|
||||
void TConsole::Command_NetTest(const std::string& cmd, const std::vector<std::string>& args) {
|
||||
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);
|
||||
if (T == Http::ErrorString || status != 200) {
|
||||
RegionHandler::TopLevelDomainFailed();
|
||||
T = Http::GET(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);
|
||||
|
||||
auto Doc = nlohmann::json::parse(T, nullptr, false);
|
||||
|
||||
if (Doc.is_discarded() || !Doc.is_object()) {
|
||||
beammp_warn("Failed to parse Server Check API response, however the server will most likely still work correctly.");
|
||||
} else {
|
||||
std::string status = Doc["status"];
|
||||
std::string details = "Response from Server Check API: " + std::string(Doc["details"]);
|
||||
if (status == "ok") {
|
||||
beammp_info(details);
|
||||
} else {
|
||||
beammp_warn(details);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TConsole::Command_Kick(const std::string&, const std::vector<std::string>& args) {
|
||||
if (!EnsureArgsCount(args, 1, size_t(-1))) {
|
||||
@@ -281,10 +338,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;
|
||||
}
|
||||
@@ -303,7 +359,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);
|
||||
}
|
||||
@@ -370,7 +426,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;
|
||||
@@ -513,11 +569,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;
|
||||
});
|
||||
@@ -540,8 +595,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;
|
||||
@@ -560,11 +614,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) {
|
||||
@@ -600,6 +654,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"
|
||||
@@ -614,6 +669,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"
|
||||
@@ -630,9 +690,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;
|
||||
};
|
||||
@@ -642,7 +706,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;
|
||||
}
|
||||
}
|
||||
@@ -652,7 +716,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.");
|
||||
}
|
||||
@@ -660,14 +724,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());
|
||||
}
|
||||
@@ -748,8 +814,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 {
|
||||
@@ -781,7 +848,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);
|
||||
|
||||
+47
-36
@@ -20,13 +20,11 @@
|
||||
|
||||
#include "ChronoWrapper.h"
|
||||
#include "Client.h"
|
||||
#include "Common.h"
|
||||
#include "Http.h"
|
||||
#include "RegionHandler.h"
|
||||
// #include "SocketIO.h"
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/rapidjson.h>
|
||||
#include <sstream>
|
||||
|
||||
namespace json = rapidjson;
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
void THeartbeatThread::operator()() {
|
||||
RegisterThread("Heartbeat");
|
||||
@@ -61,15 +59,23 @@ void THeartbeatThread::operator()() {
|
||||
auto Target = "/heartbeat";
|
||||
unsigned int ResponseCode = 0;
|
||||
|
||||
json::Document Doc;
|
||||
nlohmann::json Doc;
|
||||
bool Ok = false;
|
||||
for (const auto& Url : Application::GetBackendUrlsInOrder()) {
|
||||
T = Http::POST(Url, 443, Target, Body, "application/x-www-form-urlencoded", &ResponseCode, { { "api-v", "2" } });
|
||||
Doc.Parse(T.data(), T.size());
|
||||
if (Doc.HasParseError() || !Doc.IsObject()) {
|
||||
T = Http::POST(Url + Target, Body, "application/json", &ResponseCode, { { "api-v", "2" } });
|
||||
if (T == Http::ErrorString || ResponseCode != 200) {
|
||||
RegionHandler::TopLevelDomainFailed();
|
||||
T = Http::POST(Url + Target, Body, "application/json", &ResponseCode, { { "api-v", "2" } });
|
||||
}
|
||||
|
||||
if (!Application::Settings.getAsBool(Settings::Key::General_Private)) {
|
||||
beammp_debug("Backend response was: `" + T + "`");
|
||||
}
|
||||
|
||||
Doc = nlohmann::json::parse(T, nullptr, false);
|
||||
if (Doc.is_discarded() || !Doc.is_object()) {
|
||||
if (!Application::Settings.getAsBool(Settings::Key::General_Private)) {
|
||||
beammp_trace("Backend response failed to parse as valid json");
|
||||
beammp_trace("Response was: `" + T + "`");
|
||||
}
|
||||
} else if (ResponseCode != 200) {
|
||||
beammp_errorf("Response code from the heartbeat: {}", ResponseCode);
|
||||
@@ -88,18 +94,18 @@ void THeartbeatThread::operator()() {
|
||||
const auto MessageKey = "msg";
|
||||
|
||||
if (Ok) {
|
||||
if (Doc.HasMember(StatusKey) && Doc[StatusKey].IsString()) {
|
||||
Status = Doc[StatusKey].GetString();
|
||||
if (Doc.contains(StatusKey) && Doc[StatusKey].is_string()) {
|
||||
Status = Doc[StatusKey];
|
||||
} else {
|
||||
Ok = false;
|
||||
}
|
||||
if (Doc.HasMember(CodeKey) && Doc[CodeKey].IsString()) {
|
||||
Code = Doc[CodeKey].GetString();
|
||||
if (Doc.contains(CodeKey) && Doc[CodeKey].is_string()) {
|
||||
Code = Doc[CodeKey];
|
||||
} else {
|
||||
Ok = false;
|
||||
}
|
||||
if (Doc.HasMember(MessageKey) && Doc[MessageKey].IsString()) {
|
||||
Message = Doc[MessageKey].GetString();
|
||||
if (Doc.contains(MessageKey) && Doc[MessageKey].is_string()) {
|
||||
Message = Doc[MessageKey];
|
||||
} else {
|
||||
Ok = false;
|
||||
}
|
||||
@@ -137,25 +143,30 @@ void THeartbeatThread::operator()() {
|
||||
}
|
||||
|
||||
std::string THeartbeatThread::GenerateCall() {
|
||||
std::stringstream Ret;
|
||||
nlohmann::json Ret = {
|
||||
{ "players", std::to_string(mServer.ClientCount()) },
|
||||
{ "maxplayers", std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) },
|
||||
{ "port", std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)) },
|
||||
{ "map", Application::Settings.getAsString(Settings::Key::General_Map) },
|
||||
{ "private", Application::Settings.getAsBool(Settings::Key::General_Private) ? "true" : "false" },
|
||||
{ "version", Application::ServerVersionString() },
|
||||
{ "clientversion", Application::ClientMinimumVersion().AsString() },
|
||||
{ "name", Application::Settings.getAsString(Settings::Key::General_Name) },
|
||||
{ "tags", Application::Settings.getAsString(Settings::Key::General_Tags) },
|
||||
{ "guests", Application::Settings.getAsBool(Settings::Key::General_AllowGuests) ? "true" : "false" },
|
||||
{ "modlist", mResourceManager.TrimmedList() },
|
||||
{ "modstotalsize", std::to_string(mResourceManager.MaxModSize()) },
|
||||
{ "modstotal", std::to_string(mResourceManager.ModsLoaded()) },
|
||||
{ "playerslist", GetPlayers() },
|
||||
{ "desc", Application::Settings.getAsString(Settings::Key::General_Description) }
|
||||
};
|
||||
|
||||
Ret << "uuid=" << Application::Settings.getAsString(Settings::Key::General_AuthKey)
|
||||
<< "&players=" << mServer.ClientCount()
|
||||
<< "&maxplayers=" << Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)
|
||||
<< "&port=" << Application::Settings.getAsInt(Settings::Key::General_Port)
|
||||
<< "&map=" << Application::Settings.getAsString(Settings::Key::General_Map)
|
||||
<< "&private=" << (Application::Settings.getAsBool(Settings::Key::General_Private) ? "true" : "false")
|
||||
<< "&version=" << Application::ServerVersionString()
|
||||
<< "&clientversion=" << std::to_string(Application::ClientMajorVersion()) + ".0" // FIXME: Wtf.
|
||||
<< "&name=" << Application::Settings.getAsString(Settings::Key::General_Name)
|
||||
<< "&tags=" << Application::Settings.getAsString(Settings::Key::General_Tags)
|
||||
<< "&guests=" << (Application::Settings.getAsBool(Settings::Key::General_AllowGuests) ? "true" : "false")
|
||||
<< "&modlist=" << mResourceManager.TrimmedList()
|
||||
<< "&modstotalsize=" << mResourceManager.MaxModSize()
|
||||
<< "&modstotal=" << mResourceManager.ModsLoaded()
|
||||
<< "&playerslist=" << GetPlayers()
|
||||
<< "&desc=" << Application::Settings.getAsString(Settings::Key::General_Description);
|
||||
return Ret.str();
|
||||
lastCall = Ret.dump();
|
||||
|
||||
// Add sensitive information here because value of lastCall is used for the information packet.
|
||||
Ret["uuid"] = Application::Settings.getAsString(Settings::Key::General_AuthKey);
|
||||
|
||||
return Ret.dump();
|
||||
}
|
||||
THeartbeatThread::THeartbeatThread(TResourceManager& ResourceManager, TServer& Server)
|
||||
: mResourceManager(ResourceManager)
|
||||
@@ -174,8 +185,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();
|
||||
}
|
||||
+375
-230
@@ -20,38 +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);
|
||||
|
||||
static std::optional<sol::function> GetLuaHandler(sol::state_view StateView, const std::string Handler, const std::string EventName);
|
||||
|
||||
static std::optional<sol::function> GetLuaHandler(sol::state_view StateView, const std::string Handler, const std::string EventName) {
|
||||
auto Res = StateView.safe_script("return " + Handler, sol::script_pass_on_error);
|
||||
if (!Res.valid()) {
|
||||
beammp_errorf("invalid handler for event \"{}\". handler: \"{}\"", EventName, Handler);
|
||||
} else if (Res.get_type() == sol::type::function) {
|
||||
return Res.get<sol::function>();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
TLuaEngine::TLuaEngine()
|
||||
: mResourceServerPath(fs::path(Application::Settings.getAsString(Settings::Key::General_ResourceFolder)) / "Server") {
|
||||
Application::SetSubsystemStatus("LuaEngine", Application::Status::Starting);
|
||||
@@ -72,24 +63,20 @@ 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");
|
||||
Application::SetSubsystemStatus("LuaEngine", Application::Status::Good);
|
||||
// lua engine main thread
|
||||
beammp_infof("Lua v{}.{}.{}", LUA_VERSION_MAJOR, LUA_VERSION_MINOR, LUA_VERSION_RELEASE);
|
||||
CollectAndInitPlugins();
|
||||
|
||||
Application::SetSubsystemStatus("LuaEngine", Application::Status::Good);
|
||||
// now call all onInit's
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,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;
|
||||
@@ -127,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();
|
||||
@@ -142,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 {
|
||||
@@ -215,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;
|
||||
@@ -229,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;
|
||||
@@ -269,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>();
|
||||
@@ -325,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,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);
|
||||
}
|
||||
@@ -445,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,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];
|
||||
}
|
||||
|
||||
@@ -467,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;
|
||||
@@ -506,23 +505,19 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string
|
||||
|
||||
sol::variadic_results LocalArgs = JsonStringToArray(Str);
|
||||
for (const auto& Handler : MyHandlers) {
|
||||
auto Res = GetLuaHandler(mStateView, Handler, EventName);
|
||||
if (Res.has_value()) {
|
||||
sol::function Fn = Res.value();
|
||||
Fn = AddTraceback(mStateView, Fn);
|
||||
|
||||
auto Fn = mStateView[Handler];
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -530,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;
|
||||
@@ -562,16 +591,20 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerLocalEvent(const std::string&
|
||||
sol::table Result = mStateView.create_table();
|
||||
int i = 1;
|
||||
for (const auto& Handler : mEngine->GetEventHandlersForState(EventName, mStateId)) {
|
||||
auto Res = GetLuaHandler(mStateView, Handler, EventName);
|
||||
if (Res.has_value()) {
|
||||
sol::function Fn = Res.value();
|
||||
auto Fn = mStateView[Handler];
|
||||
if (Fn.valid() && Fn.get_type() == sol::type::function) {
|
||||
auto FnRet = Fn(EventArgs);
|
||||
if (FnRet.valid()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -580,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;
|
||||
});
|
||||
@@ -620,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;
|
||||
}
|
||||
}
|
||||
@@ -660,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.Data().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) {
|
||||
@@ -747,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();
|
||||
@@ -878,16 +937,29 @@ 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() == 3) {
|
||||
if (Args.size() == 2) {
|
||||
LuaAPI::MP::SendNotification(Args.get<int>(0), Args.get<std::string>(1), "", Args.get<std::string>(1));
|
||||
} else if (Args.size() == 3) {
|
||||
LuaAPI::MP::SendNotification(Args.get<int>(0), Args.get<std::string>(1), Args.get<std::string>(2), Args.get<std::string>(1));
|
||||
} else if (Args.size() == 4) {
|
||||
LuaAPI::MP::SendNotification(Args.get<int>(0), Args.get<std::string>(1), Args.get<std::string>(2), Args.get<std::string>(3));
|
||||
} else {
|
||||
beammp_lua_error("SendNotification expects 2 or 3 arguments.");
|
||||
beammp_lua_error("SendNotification expects 2, 3 or 4 arguments.");
|
||||
}
|
||||
});
|
||||
MPTable.set_function("ConfirmationDialog", sol::overload(&LuaAPI::MP::ConfirmationDialog, [&](const int& ID, const std::string& Title, const std::string& Body, const sol::table& Buttons, const std::string& InteractionID) {
|
||||
LuaAPI::MP::ConfirmationDialog(ID, Title, Body, Buttons, InteractionID);
|
||||
}));
|
||||
MPTable.set_function("GetPlayers", [&]() -> sol::table {
|
||||
return Lua_GetPlayers();
|
||||
});
|
||||
@@ -931,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");
|
||||
@@ -1024,7 +1102,8 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
|
||||
"MaxPlayers", 3,
|
||||
"Map", 4,
|
||||
"Name", 5,
|
||||
"Description", 6);
|
||||
"Description", 6,
|
||||
"InformationPacket", 7);
|
||||
|
||||
MPTable.create_named("CallStrategy",
|
||||
"BestEffort", CallStrategy::BestEffort,
|
||||
@@ -1051,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(),
|
||||
@@ -1068,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;
|
||||
@@ -1081,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();
|
||||
@@ -1094,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()) {
|
||||
@@ -1147,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
|
||||
@@ -1171,61 +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 Res = GetLuaHandler(StateView, FnName, TheQueuedFunction.EventName);
|
||||
if (Res.has_value()) {
|
||||
sol::function Fn = Res.value();
|
||||
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);
|
||||
}
|
||||
Fn = AddTraceback(StateView, Fn);
|
||||
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);
|
||||
@@ -1235,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;
|
||||
}
|
||||
@@ -1278,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)
|
||||
@@ -1307,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));
|
||||
}
|
||||
+497
-165
File diff suppressed because it is too large
Load Diff
+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) {
|
||||
|
||||
+20
-14
@@ -57,21 +57,27 @@ void TPluginMonitor::operator()() {
|
||||
mFileTimes[Pair.first] = CurrentTime;
|
||||
// grandparent of the path should be Resources/Server
|
||||
if (fs::equivalent(fs::path(Pair.first).parent_path().parent_path(), mPath)) {
|
||||
beammp_infof("File \"{}\" changed, reloading", Pair.first);
|
||||
// is in root folder, so reload
|
||||
std::ifstream FileStream(Pair.first, std::ios::in | std::ios::binary);
|
||||
auto Size = std::filesystem::file_size(Pair.first);
|
||||
auto Contents = std::make_shared<std::string>();
|
||||
Contents->resize(Size);
|
||||
FileStream.read(Contents->data(), Contents->size());
|
||||
TLuaChunk Chunk(Contents, Pair.first, fs::path(Pair.first).parent_path().string());
|
||||
auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path());
|
||||
auto Res = mEngine->EnqueueScript(StateID, Chunk);
|
||||
Res->WaitUntilReady();
|
||||
if (Res->Error) {
|
||||
beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Res->ErrorMessage);
|
||||
if (LowerString(fs::path(Pair.first).extension().string()) == ".lua") {
|
||||
beammp_infof("File \"{}\" changed, reloading", Pair.first);
|
||||
// is in root folder, so reload
|
||||
std::ifstream FileStream(Pair.first, std::ios::in | std::ios::binary);
|
||||
auto Size = std::filesystem::file_size(Pair.first);
|
||||
auto Contents = std::make_shared<std::string>();
|
||||
Contents->resize(Size);
|
||||
FileStream.read(Contents->data(), Contents->size());
|
||||
TLuaChunk Chunk(Contents, Pair.first, fs::path(Pair.first).parent_path().string());
|
||||
auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path());
|
||||
auto Res = mEngine->EnqueueScript(StateID, Chunk);
|
||||
Res->WaitUntilReady();
|
||||
if (Res->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));
|
||||
}
|
||||
} else {
|
||||
mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit"));
|
||||
beammp_debugf("File \"{}\" changed, not reloading because it's not a lua file. Triggering 'onFileChanged' event instead", Pair.first);
|
||||
mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first));
|
||||
}
|
||||
} else {
|
||||
|
||||
+106
-4
@@ -58,22 +58,58 @@ TResourceManager::TResourceManager() {
|
||||
Application::SetSubsystemStatus("ResourceManager", Application::Status::Good);
|
||||
}
|
||||
|
||||
std::string TResourceManager::NewFileList() const {
|
||||
return mMods.dump();
|
||||
}
|
||||
void TResourceManager::RefreshFiles() {
|
||||
mMods.clear();
|
||||
std::unique_lock Lock(mModsMutex);
|
||||
|
||||
std::string Path = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client";
|
||||
|
||||
nlohmann::json modsDB;
|
||||
|
||||
if (std::filesystem::exists(Path + "/mods.json")) {
|
||||
try {
|
||||
std::ifstream stream(Path + "/mods.json");
|
||||
|
||||
stream >> modsDB;
|
||||
|
||||
stream.close();
|
||||
} catch (const std::exception& e) {
|
||||
beammp_errorf("Failed to load mods.json: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& entry : fs::directory_iterator(Path)) {
|
||||
std::string File(entry.path().string());
|
||||
|
||||
if (entry.path().filename().string() == "mods.json") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.path().extension() != ".zip" || std::filesystem::is_directory(entry.path())) {
|
||||
beammp_warnf("'{}' is not a ZIP file and will be ignored", File);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (modsDB.contains(entry.path().filename().string())) {
|
||||
auto& dbEntry = modsDB[entry.path().filename().string()];
|
||||
if (entry.last_write_time().time_since_epoch().count() > dbEntry["lastwrite"] || std::filesystem::file_size(File) != dbEntry["filesize"].get<size_t>()) {
|
||||
beammp_infof("File '{}' has been modified, rehashing", File);
|
||||
} else {
|
||||
dbEntry["exists"] = true;
|
||||
|
||||
mMods.push_back(nlohmann::json {
|
||||
{ "file_name", std::filesystem::path(File).filename() },
|
||||
{ "file_size", std::filesystem::file_size(File) },
|
||||
{ "hash_algorithm", "sha256" },
|
||||
{ "hash", dbEntry["hash"] },
|
||||
{ "protected", dbEntry["protected"] } });
|
||||
|
||||
beammp_debugf("Mod '{}' loaded from cache", File);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
EVP_MD_CTX* mdctx;
|
||||
const EVP_MD* md;
|
||||
@@ -121,6 +157,8 @@ void TResourceManager::RefreshFiles() {
|
||||
}
|
||||
EVP_MD_CTX_free(mdctx);
|
||||
|
||||
stream.close();
|
||||
|
||||
std::string result;
|
||||
for (size_t i = 0; i < sha256_len; i++) {
|
||||
result += fmt::format("{:02x}", sha256_value[i]);
|
||||
@@ -131,9 +169,73 @@ void TResourceManager::RefreshFiles() {
|
||||
{ "file_size", std::filesystem::file_size(File) },
|
||||
{ "hash_algorithm", "sha256" },
|
||||
{ "hash", result },
|
||||
});
|
||||
{ "protected", false } });
|
||||
|
||||
modsDB[std::filesystem::path(File).filename().string()] = {
|
||||
{ "lastwrite", entry.last_write_time().time_since_epoch().count() },
|
||||
{ "hash", result },
|
||||
{ "filesize", std::filesystem::file_size(File) },
|
||||
{ "protected", false },
|
||||
{ "exists", true }
|
||||
};
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
beammp_errorf("Sha256 hashing of '{}' failed: {}", File, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = modsDB.begin(); it != modsDB.end();) {
|
||||
if (!it.value().contains("exists")) {
|
||||
it = modsDB.erase(it);
|
||||
} else {
|
||||
it.value().erase("exists");
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
std::ofstream stream(Path + "/mods.json");
|
||||
|
||||
stream << modsDB.dump(4);
|
||||
|
||||
stream.close();
|
||||
} catch (std::exception& e) {
|
||||
beammp_error("Failed to update mod DB: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void TResourceManager::SetProtected(const std::string& ModName, bool Protected) {
|
||||
std::unique_lock Lock(mModsMutex);
|
||||
|
||||
for (auto& mod : mMods) {
|
||||
if (mod["file_name"].get<std::string>() == ModName) {
|
||||
mod["protected"] = Protected;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto modsDBPath = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client/mods.json";
|
||||
|
||||
if (std::filesystem::exists(modsDBPath)) {
|
||||
try {
|
||||
nlohmann::json modsDB;
|
||||
|
||||
std::fstream stream(modsDBPath);
|
||||
|
||||
stream >> modsDB;
|
||||
|
||||
if (modsDB.contains(ModName)) {
|
||||
modsDB[ModName]["protected"] = Protected;
|
||||
}
|
||||
|
||||
stream.clear();
|
||||
stream.seekp(0, std::ios::beg);
|
||||
|
||||
stream << modsDB.dump(4);
|
||||
|
||||
stream.close();
|
||||
} catch (const std::exception& e) {
|
||||
beammp_errorf("Failed to update mods.json: {}", e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+150
-56
@@ -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) {
|
||||
@@ -161,21 +156,23 @@ size_t TServer::ClientCount() const {
|
||||
return mClients.size();
|
||||
}
|
||||
|
||||
void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uint8_t>&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network) {
|
||||
void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uint8_t>&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network, bool udp) {
|
||||
constexpr std::string_view ABG = "ABG:";
|
||||
if (Packet.size() >= ABG.size() && std::equal(Packet.begin(), Packet.begin() + ABG.size(), ABG.begin(), ABG.end())) {
|
||||
Packet.erase(Packet.begin(), Packet.begin() + ABG.size());
|
||||
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);
|
||||
@@ -195,12 +192,29 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
|
||||
|
||||
// V to Y
|
||||
if (Code <= 89 && Code >= 86) {
|
||||
int PID = -1;
|
||||
int VID = -1;
|
||||
|
||||
auto pidVidPart = StringPacket.substr(3);
|
||||
auto MaybePidVid = GetPidVid(pidVidPart.substr(0, pidVidPart.find(':')));
|
||||
if (MaybePidVid) {
|
||||
std::tie(PID, VID) = MaybePidVid.value();
|
||||
}
|
||||
|
||||
if (PID == -1 || VID == -1 || PID != LockedClient->GetID()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PPSMonitor.IncrementInternalPPS();
|
||||
Network.SendToAll(LockedClient.get(), Packet, false, false);
|
||||
return;
|
||||
}
|
||||
switch (Code) {
|
||||
case 'H': // initial connection
|
||||
if (udp) {
|
||||
beammp_debugf("Received 'H' packet over UDP from client '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
|
||||
return;
|
||||
}
|
||||
if (!Network.SyncClient(Client)) {
|
||||
// TODO handle
|
||||
}
|
||||
@@ -208,18 +222,26 @@ 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);
|
||||
}
|
||||
return;
|
||||
case 'O':
|
||||
if (udp) {
|
||||
beammp_debugf("Received 'O' packet over UDP from client '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
|
||||
return;
|
||||
}
|
||||
if (Packet.size() > 1000) {
|
||||
beammp_debug(("Received data from: ") + LockedClient->GetName() + (" Size: ") + std::to_string(Packet.size()));
|
||||
}
|
||||
ParseVehicle(*LockedClient, StringPacket, Network);
|
||||
return;
|
||||
case 'C': {
|
||||
if (udp) {
|
||||
beammp_debugf("Received 'C' packet over UDP from client '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
|
||||
return;
|
||||
}
|
||||
if (Packet.size() < 4 || std::find(Packet.begin() + 3, Packet.end(), ':') == Packet.end())
|
||||
break;
|
||||
const auto PacketAsString = std::string(reinterpret_cast<const char*>(Packet.data()), Packet.size());
|
||||
@@ -232,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);
|
||||
@@ -250,17 +282,35 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
|
||||
return;
|
||||
}
|
||||
case 'E':
|
||||
if (udp) {
|
||||
beammp_debugf("Received 'E' packet over UDP from client '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID());
|
||||
return;
|
||||
}
|
||||
HandleEvent(*LockedClient, StringPacket);
|
||||
return;
|
||||
case 'N':
|
||||
beammp_trace("got 'N' packet (" + std::to_string(Packet.size()) + ")");
|
||||
Network.SendToAll(LockedClient.get(), Packet, false, true);
|
||||
return;
|
||||
case 'Z': // position packet
|
||||
case 'Z': { // position packet
|
||||
PPSMonitor.IncrementInternalPPS();
|
||||
|
||||
int PID = -1;
|
||||
int VID = -1;
|
||||
|
||||
auto pidVidPart = StringPacket.substr(3);
|
||||
auto MaybePidVid = GetPidVid(pidVidPart.substr(0, pidVidPart.find(':')));
|
||||
if (MaybePidVid) {
|
||||
std::tie(PID, VID) = MaybePidVid.value();
|
||||
}
|
||||
|
||||
if (PID == -1 || VID == -1 || PID != LockedClient->GetID()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Network.SendToAll(LockedClient.get(), Packet, false, false);
|
||||
HandlePosition(*LockedClient, StringPacket);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
@@ -279,6 +329,15 @@ void TServer::HandleEvent(TClient& c, const std::string& RawData) {
|
||||
}
|
||||
std::string Name = RawData.substr(2, NameDataSep - 2);
|
||||
std::string Data = RawData.substr(NameDataSep + 1);
|
||||
|
||||
std::vector<std::string> exclude = {"onInit", "onFileChanged","onVehicleDeleted","onConsoleInput","onPlayerAuth","postPlayerAuth", "onPlayerDisconnect",
|
||||
"onPlayerConnecting","onPlayerJoining","onPlayerJoin","onChatMessage","postChatMessage","onVehicleSpawn","postVehicleSpawn","onVehicleEdited", "postVehicleEdited",
|
||||
"onVehicleReset","onVehiclePaintChanged","onShutdown"};
|
||||
|
||||
if (std::ranges::find(exclude, Name) != exclude.end()) {
|
||||
beammp_debugf("Excluded event triggered by client '{}' ({}): '{}', ignoring.", c.GetName(), c.GetID(), Name);
|
||||
return;
|
||||
}
|
||||
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent(Name, "", c.GetID(), Data));
|
||||
}
|
||||
|
||||
@@ -325,12 +384,21 @@ 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;
|
||||
if (ShouldSpawn(c, CarJson, CarID) && !ShouldntSpawn) {
|
||||
c.AddNewCar(CarID, Packet);
|
||||
auto CarJsonDoc = nlohmann::json::parse(CarJson, nullptr, false);
|
||||
if (ShouldSpawn(c, CarJson, CarID) && !ShouldntSpawn && !CarJsonDoc.is_discarded()) {
|
||||
c.AddNewCar(CarID, CarJsonDoc);
|
||||
Network.SendToAll(nullptr, StringToVector(Packet), true, true);
|
||||
SpawnConfirmed = true;
|
||||
} else {
|
||||
@@ -361,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('{');
|
||||
@@ -415,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);
|
||||
}
|
||||
@@ -436,6 +518,38 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
|
||||
Network.SendToAll(&c, StringToVector(Packet), false, true);
|
||||
return;
|
||||
}
|
||||
case 'p': {
|
||||
beammp_trace(std::string(("got 'Op' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")"));
|
||||
auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1)));
|
||||
if (MaybePidVid) {
|
||||
std::tie(PID, VID) = MaybePidVid.value();
|
||||
}
|
||||
|
||||
if (PID != -1 && VID != -1 && PID == c.GetID()) {
|
||||
auto 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);
|
||||
|
||||
auto CarData = c.GetCarData(VID);
|
||||
if (CarData == nlohmann::detail::value_t::null)
|
||||
return;
|
||||
|
||||
if (CarData.contains("vcf") && CarData.at("vcf").is_object())
|
||||
if (CarData.at("vcf").contains("paints") && CarData.at("vcf").at("paints").is_array()) {
|
||||
CarData.at("vcf")["paints"] = nlohmann::json::parse(Data);
|
||||
c.SetCarData(VID, CarData);
|
||||
}
|
||||
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
beammp_trace(std::string(("possibly not implemented: '") + Packet + ("' (") + std::to_string(Packet.size()) + (")")));
|
||||
return;
|
||||
@@ -448,42 +562,22 @@ void TServer::Apply(TClient& c, int VID, const std::string& pckt) {
|
||||
beammp_error("Malformed packet received, no '{' found");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string Packet = pckt.substr(FoundPos);
|
||||
std::string VD = c.GetCarData(VID);
|
||||
if (VD.empty()) {
|
||||
nlohmann::json VD = c.GetCarData(VID);
|
||||
if (VD == nlohmann::detail::value_t::null) {
|
||||
beammp_error("Tried to apply change to vehicle that does not exist");
|
||||
return;
|
||||
}
|
||||
std::string Header = VD.substr(0, VD.find('{'));
|
||||
|
||||
FoundPos = VD.find('{');
|
||||
if (FoundPos == std::string::npos) {
|
||||
return;
|
||||
}
|
||||
VD = VD.substr(FoundPos);
|
||||
rapidjson::Document Veh, Pack;
|
||||
Veh.Parse(VD.c_str());
|
||||
if (Veh.HasParseError()) {
|
||||
beammp_error("Could not get vehicle config!");
|
||||
return;
|
||||
}
|
||||
Pack.Parse(Packet.c_str());
|
||||
if (Pack.HasParseError() || Pack.IsNull()) {
|
||||
nlohmann::json Pack = nlohmann::json::parse(Packet, nullptr, false);
|
||||
|
||||
if (Pack.is_discarded()) {
|
||||
beammp_error("Could not get active vehicle config!");
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& M : Pack.GetObject()) {
|
||||
if (Veh[M.name].IsNull()) {
|
||||
Veh.AddMember(M.name, M.value, Veh.GetAllocator());
|
||||
} else {
|
||||
Veh[M.name] = Pack[M.name];
|
||||
}
|
||||
}
|
||||
rapidjson::StringBuffer Buffer;
|
||||
rapidjson::Writer<rapidjson::StringBuffer> writer(Buffer);
|
||||
Veh.Accept(writer);
|
||||
c.SetCarData(VID, Header + Buffer.GetString());
|
||||
c.SetCarData(VID, Pack);
|
||||
}
|
||||
|
||||
void TServer::InsertClient(const std::shared_ptr<TClient>& NewClient) {
|
||||
|
||||
+5
-1
@@ -21,7 +21,7 @@
|
||||
#include "Common.h"
|
||||
#include <utility>
|
||||
|
||||
TVehicleData::TVehicleData(int ID, std::string Data)
|
||||
TVehicleData::TVehicleData(int ID, nlohmann::json Data)
|
||||
: mID(ID)
|
||||
, mData(std::move(Data)) {
|
||||
beammp_trace("vehicle " + std::to_string(mID) + " constructed");
|
||||
@@ -30,3 +30,7 @@ TVehicleData::TVehicleData(int ID, std::string Data)
|
||||
TVehicleData::~TVehicleData() {
|
||||
beammp_trace("vehicle " + std::to_string(mID) + " destroyed");
|
||||
}
|
||||
|
||||
std::string TVehicleData::DataAsPacket(const std::string& Role, const std::string& Name, const int ID) const {
|
||||
return "Os:" + Role + ":" + Name + ":" + std::to_string(ID) + "-" + std::to_string(this->mID) + ":" + this->mData.dump();
|
||||
}
|
||||
+25
-9
@@ -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,17 +176,9 @@ 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);
|
||||
|
||||
auto LuaEngine = std::make_shared<TLuaEngine>();
|
||||
LuaEngine->SetServer(&Server);
|
||||
Application::Console().InitializeLuaConsole(*LuaEngine);
|
||||
|
||||
RegisterThread("Main");
|
||||
|
||||
beammp_trace("Running in debug mode on a debug build");
|
||||
@@ -194,13 +187,30 @@ int BeamMPServerMain(MainArguments Arguments) {
|
||||
TPPSMonitor PPSMonitor(Server);
|
||||
THeartbeatThread Heartbeat(ResourceManager, Server);
|
||||
TNetwork Network(Server, PPSMonitor, ResourceManager);
|
||||
|
||||
auto LuaEngine = std::make_shared<TLuaEngine>();
|
||||
LuaEngine->SetServer(&Server);
|
||||
Application::Console().InitializeLuaConsole(*LuaEngine);
|
||||
LuaEngine->SetNetwork(&Network);
|
||||
PPSMonitor.SetNetwork(Network);
|
||||
Application::CheckForUpdates();
|
||||
|
||||
TPluginMonitor PluginMonitor(fs::path(Application::Settings.getAsString(Settings::Key::General_ResourceFolder)) / "Server", LuaEngine);
|
||||
|
||||
Application::SetSubsystemStatus("Main", Application::Status::Good);
|
||||
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 {
|
||||
@@ -215,6 +225,10 @@ int BeamMPServerMain(MainArguments Arguments) {
|
||||
std::string SystemsBadList {};
|
||||
auto Statuses = Application::GetSubsystemStatuses();
|
||||
for (const auto& NameStatusPair : Statuses) {
|
||||
if (NameStatusPair.first == "Main") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IgnoreSubsystems.count(NameStatusPair.first) > 0) {
|
||||
continue; // ignore
|
||||
}
|
||||
@@ -228,6 +242,8 @@ int BeamMPServerMain(MainArguments Arguments) {
|
||||
// remove ", "
|
||||
SystemsBadList = SystemsBadList.substr(0, SystemsBadList.size() - 2);
|
||||
if (FullyStarted) {
|
||||
Application::SetSubsystemStatus("Main", Application::Status::Good);
|
||||
|
||||
if (!WithErrors) {
|
||||
beammp_info("ALL SYSTEMS STARTED SUCCESSFULLY, EVERYTHING IS OKAY");
|
||||
} else {
|
||||
|
||||
+1
-1
Submodule vcpkg updated: 6978381401...5bf0c55239
+15
-2
@@ -13,6 +13,19 @@
|
||||
"nlohmann-json",
|
||||
"openssl",
|
||||
"rapidjson",
|
||||
"sol2"
|
||||
]
|
||||
"sol2",
|
||||
"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