diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index 3ddf653..b831148 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -7,38 +7,72 @@ env: jobs: linux-build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 - with: - submodules: 'recursive' + - uses: actions/checkout@v2 + with: + submodules: "recursive" - - name: Install Dependencies - env: - beammp_sentry_url: ${{ secrets.BEAMMP_SECRET_SENTRY_URL }} - run: | + - name: Install Dependencies + env: + beammp_sentry_url: ${{ secrets.BEAMMP_SECRET_SENTRY_URL }} + run: | echo ${#beammp_sentry_url} sudo apt-get update - sudo apt-get install -y libz-dev rapidjson-dev liblua5.3 libssl-dev libwebsocketpp-dev libcurl4-openssl-dev + sudo apt-get install -y libz-dev rapidjson-dev liblua5.3 libssl-dev libwebsocketpp-dev libcurl4-openssl-dev cmake g++-10 libboost1.74-all-dev - - name: Create Build Environment - run: cmake -E make_directory ${{github.workspace}}/build-linux + - name: Create Build Environment + run: cmake -E make_directory ${{github.workspace}}/build-linux - - name: Configure CMake - shell: bash - working-directory: ${{github.workspace}}/build-linux - env: - beammp_sentry_url: ${{ secrets.BEAMMP_SECRET_SENTRY_URL }} - run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_CXX_COMPILER=g++-10 -DBEAMMP_SECRET_SENTRY_URL="$beammp_sentry_url" + - name: Configure CMake + shell: bash + working-directory: ${{github.workspace}}/build-linux + env: + beammp_sentry_url: ${{ secrets.BEAMMP_SECRET_SENTRY_URL }} + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_CXX_COMPILER=g++-10 -DBEAMMP_SECRET_SENTRY_URL="$beammp_sentry_url" - - name: Build - working-directory: ${{github.workspace}}/build-linux - shell: bash - run: cmake --build . --config $BUILD_TYPE + - name: Build Server + working-directory: ${{github.workspace}}/build-linux + shell: bash + run: cmake --build . --config $BUILD_TYPE -t BeamMP-Server --parallel - - name: Archive artifacts - uses: actions/upload-artifact@v2 - with: - name: BeamMP-Server-linux - path: ${{github.workspace}}/build-linux/BeamMP-Server + - name: Build Tests + working-directory: ${{github.workspace}}/build-linux + shell: bash + run: cmake --build . --config $BUILD_TYPE -t BeamMP-Server-tests --parallel + + - name: Archive server artifact + uses: actions/upload-artifact@v2 + with: + name: BeamMP-Server-linux + path: ${{github.workspace}}/build-linux/BeamMP-Server + + - name: Archive test artifact + uses: actions/upload-artifact@v2 + with: + name: BeamMP-Server-linux-tests + path: ${{github.workspace}}/build-linux/BeamMP-Server-tests + + run-tests: + needs: linux-build + runs-on: ubuntu-22.04 + + steps: + - uses: actions/download-artifact@master + with: + name: BeamMP-Server-linux-tests + path: ${{github.workspace}} + + - name: Install Runtime Dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y liblua5.3-0 libssl3 curl + + - name: Test + working-directory: ${{github.workspace}} + shell: bash + run: | + chmod +x ./BeamMP-Server-tests + ./BeamMP-Server-tests diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index b882766..4c2ba3b 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -20,7 +20,7 @@ jobs: with: vcpkgArguments: 'lua zlib rapidjson openssl websocketpp curl' vcpkgDirectory: '${{ runner.workspace }}/b/vcpkg' - vcpkgGitCommitId: 'a106de33bbee694e3be6243718aa2a549a692832' + vcpkgGitCommitId: "06b5f4a769d848d1a20fa0acd556019728b56273" vcpkgTriplet: 'x64-windows-static' - name: Create Build Environment diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 2f8e3f3..9b9733d 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -32,7 +32,7 @@ jobs: upload-release-files-linux: name: Upload Linux Release Files - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: create-release steps: - uses: actions/checkout@v2 @@ -42,7 +42,7 @@ jobs: - name: Install Dependencies run: | sudo apt-get update - sudo apt-get install -y libz-dev rapidjson-dev liblua5.3 libssl-dev libwebsocketpp-dev libcurl4-openssl-dev + sudo apt-get install -y libz-dev rapidjson-dev liblua5.3 libssl-dev libwebsocketpp-dev libcurl4-openssl-dev libboost-dev libboost1.74-all-dev libboost1.74-dev - name: Create Build Environment run: cmake -E make_directory ${{github.workspace}}/build-linux @@ -85,7 +85,7 @@ jobs: with: vcpkgArguments: 'lua zlib rapidjson openssl websocketpp curl' vcpkgDirectory: '${{ runner.workspace }}/b/vcpkg' - vcpkgGitCommitId: 'a106de33bbee694e3be6243718aa2a549a692832' + vcpkgGitCommitId: '06b5f4a769d848d1a20fa0acd556019728b56273' vcpkgTriplet: 'x64-windows-static' - name: Create Build Environment diff --git a/.gitmodules b/.gitmodules index b54a97b..e513ccf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,9 @@ [submodule "deps/json"] path = deps/json url = https://github.com/nlohmann/json +[submodule "deps/fmt"] + path = deps/fmt + url = https://github.com/fmtlib/fmt +[submodule "deps/doctest"] + path = deps/doctest + url = https://github.com/doctest/doctest diff --git a/CMakeLists.txt b/CMakeLists.txt index 72b1cef..31c196e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,5 @@ -cmake_minimum_required(VERSION 3.0) +# 3.4 is required for imported targets. +cmake_minimum_required(VERSION 3.4 FATAL_ERROR) message(STATUS "You can find build instructions and a list of dependencies in the README at \ https://github.com/BeamMP/BeamMP-Server") @@ -8,19 +9,26 @@ project(BeamMP-Server HOMEPAGE_URL https://beammp.com LANGUAGES CXX C) +find_package(Git REQUIRED) +# Update submodules as needed +option(GIT_SUBMODULE "Check submodules during build" ON) +if(GIT_SUBMODULE) + message(STATUS "Submodule update") + execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE GIT_SUBMOD_RESULT) + if(NOT GIT_SUBMOD_RESULT EQUAL "0") + message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") + endif() +endif() + + set(HTTPLIB_REQUIRE_OPENSSL ON) +set(SENTRY_BUILD_SHARED_LIBS OFF) -include_directories("${PROJECT_SOURCE_DIR}/deps/asio/asio/include") -include_directories("${PROJECT_SOURCE_DIR}/deps/rapidjson/include") -include_directories("${PROJECT_SOURCE_DIR}/deps/websocketpp") -include_directories("${PROJECT_SOURCE_DIR}/deps/commandline") -include_directories("${PROJECT_SOURCE_DIR}/deps/sol2/include") -include_directories("${PROJECT_SOURCE_DIR}/deps/cpp-httplib") -include_directories("${PROJECT_SOURCE_DIR}/deps/json/single_include") -include_directories("${PROJECT_SOURCE_DIR}/deps") - -add_compile_definitions(CPPHTTPLIB_OPENSSL_SUPPORT) +add_compile_definitions(CPPHTTPLIB_OPENSSL_SUPPORT=1) +# ------------------------ APPLE --------------------------------- if(APPLE) if(IS_DIRECTORY /opt/homebrew/Cellar/lua@5.3/5.3.6) set(LUA_INCLUDE_DIR /opt/homebrew/Cellar/lua@5.3/5.3.6/include/lua5.3) @@ -37,20 +45,28 @@ if(APPLE) include_directories(/usr/local/opt/openssl@1.1/include) link_directories(/usr/local/opt/openssl@1.1/lib) endif() -endif() - -if (WIN32) +# ------------------------ WINDOWS --------------------------------- +option(WIN32_STATIC_RUNTIME "Build statically-linked runtime on windows (don't touch unless you know what you're doing)" ON) +elseif (WIN32) # this has to happen before sentry, so that crashpad on windows links with these settings. - message(STATUS "MSVC -> forcing use of statically-linked runtime.") - STRING(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE}) - STRING(REPLACE "/MDd" "/MTd" CMAKE_CXX_FLAGS_DEBUG ${CMAKE_CXX_FLAGS_DEBUG}) -endif() + if (WIN32_STATIC_RUNTIME) + message(STATUS "MSVC -> forcing use of statically-linked runtime.") + STRING(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE}) + STRING(REPLACE "/MDd" "/MTd" CMAKE_CXX_FLAGS_DEBUG ${CMAKE_CXX_FLAGS_DEBUG}) + endif() +# ------------------------ LINUX --------------------------------- +elseif (UNIX) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -g") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2 -fno-builtin") + option(SANITIZE "Turns on thread and UB sanitizers" OFF) + if (SANITIZE) + message(STATUS "sanitize is ON") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize={address,thread,undefined}") + endif (SANITIZE) +endif () -include_directories("include/sentry-native/include") set(BUILD_SHARED_LIBS OFF) -if (MSVC) - set(SENTRY_BUILD_RUNTIMESTATIC ON) -endif() +# ------------------------ SENTRY --------------------------------- message(STATUS "Checking for Sentry URL") # this is set by the build system. # IMPORTANT: if you're building from source, just leave this empty @@ -60,120 +76,186 @@ if (NOT DEFINED BEAMMP_SECRET_SENTRY_URL) set(BEAMMP_SECRET_SENTRY_URL "") set(SENTRY_BACKEND none) else() - string(LENGTH ${BEAMMP_SECRET_SENTRY_URL} URL_LEN) - message(STATUS "Sentry URL is length ${URL_LEN}") set(SENTRY_BACKEND breakpad) endif() add_subdirectory("deps/sentry-native") +# ------------------------ C++ SETUP --------------------------------- +set(CMAKE_CXX_STANDARD 17) if (MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") endif () +# ------------------------ DEPENDENCIES ------------------------------ message(STATUS "Adding local source dependencies") # this has to happen before -DDEBUG since it wont compile properly with -DDEBUG add_subdirectory(deps) -message(STATUS "Setting compiler flags") -if (WIN32) +# ------------------------ VARIABLES --------------------------------- - #-DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static - set(VcpkgRoot ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}) - include_directories(${VcpkgRoot}/include) - link_directories(${VcpkgRoot}/lib) -elseif (UNIX) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -static-libstdc++") - set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -g") - set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2 -fno-builtin") - if (SANITIZE) - message(STATUS "sanitize is ON") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined,thread") - endif (SANITIZE) +include(FindLua) +include(FindOpenSSL) +include(FindThreads) +include(FindZLIB) + +find_package(Boost 1.70 REQUIRED COMPONENTS system) + +set(BeamMP_Sources + include/TConsole.h src/TConsole.cpp + include/TServer.h src/TServer.cpp + include/Compat.h src/Compat.cpp + include/Common.h src/Common.cpp + include/Client.h src/Client.cpp + include/VehicleData.h src/VehicleData.cpp + include/TConfig.h src/TConfig.cpp + include/TLuaEngine.h src/TLuaEngine.cpp + include/TLuaPlugin.h src/TLuaPlugin.cpp + include/TResourceManager.h src/TResourceManager.cpp + include/THeartbeatThread.h src/THeartbeatThread.cpp + include/Http.h src/Http.cpp + include/TSentry.h src/TSentry.cpp + include/TPPSMonitor.h src/TPPSMonitor.cpp + include/TNetwork.h src/TNetwork.cpp + include/LuaAPI.h src/LuaAPI.cpp + include/TScopedTimer.h src/TScopedTimer.cpp + include/SignalHandling.h src/SignalHandling.cpp + include/ArgsParser.h src/ArgsParser.cpp + include/TPluginMonitor.h src/TPluginMonitor.cpp + include/Environment.h + include/BoostAliases.h +) + +set(BeamMP_Includes + ${LUA_INCLUDE_DIR} + ${CURL_INCLUDE_DIRS} + "${CMAKE_CURRENT_SOURCE_DIR}/deps/cpp-httplib" + "${CMAKE_CURRENT_SOURCE_DIR}/deps/commandline" + "${CMAKE_CURRENT_SOURCE_DIR}/deps/json/single_include" + "${CMAKE_CURRENT_SOURCE_DIR}/deps/sol2/include" + "${CMAKE_CURRENT_SOURCE_DIR}/deps/rapidjson/include" + "${CMAKE_CURRENT_SOURCE_DIR}/deps/asio/asio/include" + "${CMAKE_CURRENT_SOURCE_DIR}/deps" +) + +set(BeamMP_Definitions + SECRET_SENTRY_URL="${BEAMMP_SECRET_SENTRY_URL}" +) + +if (WIN32) + list(APPEND BeamMP_Definitions _WIN32_WINNT=0x0601) + list(APPEND BeamMP_Definitions _CRT_SECURE_NO_WARNINGS) +endif() +if (UNIX) + set(BeamMP_CompileOptions + -Wall + -Wextra + -Wpedantic + + -Werror=uninitialized + -Werror=float-equal + -Werror=pointer-arith + -Werror=double-promotion + -Werror=write-strings + -Werror=cast-qual + -Werror=init-self + -Werror=cast-align + -Werror=unreachable-code + -Werror=strict-aliasing -fstrict-aliasing + -Werror=redundant-decls + -Werror=missing-declarations + -Werror=missing-field-initializers + -Werror=write-strings + -Werror=ctor-dtor-privacy + -Werror=switch-enum + -Werror=switch-default + -Werror=old-style-cast + -Werror=overloaded-virtual + -Werror=overloaded-virtual + -Werror=missing-include-dirs + -Werror=unused-result + + -fstack-protector + -Wzero-as-null-pointer-constant + ) +endif() + +set(BeamMP_Libraries + Boost::boost + Boost::system + doctest::doctest + OpenSSL::SSL + OpenSSL::Crypto + sol2::sol2 + fmt::fmt + Threads::Threads + ZLIB::ZLIB + ${LUA_LIBRARIES} + commandline + sentry +) + +if (WIN32) + set(BeamMP_PlatformLibs wsock32 ws2_32) endif () - - -set(CMAKE_CXX_STANDARD 17) - -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") +# ------------------------ BEAMMP SERVER ----------------------------- add_executable(BeamMP-Server src/main.cpp - include/TConsole.h src/TConsole.cpp - include/TServer.h src/TServer.cpp - include/Compat.h src/Compat.cpp - include/Common.h src/Common.cpp - include/Client.h src/Client.cpp - include/VehicleData.h src/VehicleData.cpp - include/TConfig.h src/TConfig.cpp - include/TLuaEngine.h src/TLuaEngine.cpp - include/TLuaPlugin.h src/TLuaPlugin.cpp - include/TResourceManager.h src/TResourceManager.cpp - include/THeartbeatThread.h src/THeartbeatThread.cpp - include/Http.h src/Http.cpp - include/TSentry.h src/TSentry.cpp - include/TPPSMonitor.h src/TPPSMonitor.cpp - include/TNetwork.h src/TNetwork.cpp - include/LuaAPI.h src/LuaAPI.cpp - include/TScopedTimer.h src/TScopedTimer.cpp - include/SignalHandling.h src/SignalHandling.cpp - include/ArgsParser.h src/ArgsParser.cpp - include/Environment.h) + ${BeamMP_Sources} +) -target_compile_definitions(BeamMP-Server PRIVATE SECRET_SENTRY_URL="${BEAMMP_SECRET_SENTRY_URL}") -include_directories(BeamMP-Server PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_compile_definitions(BeamMP-Server PRIVATE + ${BeamMP_Definitions} + DOCTEST_CONFIG_DISABLE +) -target_include_directories(BeamMP-Server PUBLIC - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${CMAKE_CURRENT_SOURCE_DIR}/commandline") +target_compile_options(BeamMP-Server PRIVATE + ${BeamMP_CompileOptions} +) -if (APPLE) - message(STATUS "NOT looking for Lua on APPLE") -else() - message(STATUS "Looking for Lua") - find_package(Lua REQUIRED VERSION 5.3) +target_include_directories(BeamMP-Server PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/include" +) + +target_include_directories(BeamMP-Server SYSTEM PRIVATE + ${BeamMP_Includes} +) + +target_link_libraries(BeamMP-Server + ${BeamMP_Libraries} + ${BeamMP_PlatformLibs} +) + +# ------------------------ BEAMMP SERVER TESTS ----------------------- + +option(BUILD_TESTS "Build BeamMP-Server tests" ON) + +if(BUILD_TESTS) + add_executable(BeamMP-Server-tests + test/test_main.cpp + ${BeamMP_Sources} + ) + + target_compile_definitions(BeamMP-Server-tests PRIVATE + ${BeamMP_Definitions} + ) + + target_compile_options(BeamMP-Server-tests PRIVATE + ${BeamMP_CompileOptions} + ) + + target_include_directories(BeamMP-Server-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/include" + ) + + target_include_directories(BeamMP-Server-tests SYSTEM PRIVATE + ${BeamMP_Includes} + ) + + target_link_libraries(BeamMP-Server-tests + ${BeamMP_Libraries} + ${BeamMP_PlatformLibs} + ) endif() -target_include_directories(BeamMP-Server PUBLIC - ${LUA_INCLUDE_DIR} - ${CURL_INCLUDE_DIRS} - "include/tomlplusplus" - "include/sentry-native/include" - "include/curl/include") - -message(STATUS "Looking for SSL") - -if (APPLE) - set(OPENSSL_LIBRARIES ssl crypto) -else() - find_package(OpenSSL REQUIRED) -endif() - -target_link_libraries(BeamMP-Server sol2::sol2 ${LUA_LIBRARIES}) -message(STATUS "CURL IS ${CURL_LIBRARIES}") - -if (UNIX) - target_link_libraries(BeamMP-Server - z - pthread - ${LUA_LIBRARIES} - crypto - ${OPENSSL_LIBRARIES} - commandline - sentry - ssl) -elseif (WIN32) - include(FindLua) - message(STATUS "Looking for libz") - find_package(ZLIB REQUIRED) - message(STATUS "Looking for RapidJSON") - find_package(RapidJSON CONFIG REQUIRED) - target_include_directories(BeamMP-Server PRIVATE ${RAPIDJSON_INCLUDE_DIRS}) - target_link_libraries(BeamMP-Server - ws2_32 - ZLIB::ZLIB - ${LUA_LIBRARIES} - ${OPENSSL_LIBRARIES} - commandline - sentry) -endif () diff --git a/Changelog.md b/Changelog.md index 0486505..9ed32e4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,29 @@ + +# v3.1.0 + +- ADDED Tab autocomplete in console, smart tab autocomplete (understands lua tables and types) in the lua console +- ADDED lua debug facilities (type `:help` when attached to lua via `lua`) +- ADDED Util.JsonEncode() and Util.JsonDecode(), which turn lua tables into json and vice-versa +- ADDED FS.ListFiles and FS.ListDirectories +- ADDED onFileChanged event, triggered when a server plugin file changes +- ADDED MP.GetPositionRaw(), which can be used to retrieve the latest position packet per player, per vehicle +- ADDED error messages to some lua functions +- ADDED HOME and END button working in console +- ADDED `MP.TriggerClientEventJson()` which takes a table as the data argument and sends it as JSON +- ADDED identifiers (beammp id, ip) to onPlayerAuth (4th argument) +- ADDED more network debug logging +- CHANGED all networking to be more stable, performant, and safe +- FIXED `ip` in MP.GetPlayerIdentifiers +- FIXED issue with client->server events which contain `:` +- FIXED a fatal exception on LuaEngine startup if Resources/Server is a symlink +- FIXED onInit not being called on hot-reload +- FIXED incorrect timing calculation of Lua EventTimer loop +- FIXED bug which caused hot-reload not to report syntax errors +- FIXED missing error messages on some event handler calls +- FIXED vehicles not deleting for all players if an edit was cancelled by Lua +- FIXED server not handling binary UDP packets properly +- REMOVED "Backend response failed to parse as valid json" message + # v3.0.2 - ADDED Periodic update message if a new server is released diff --git a/README.md b/README.md index 64006d8..724ca30 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ These values are guesstimated and are subject to change with each release. ## Contributing -TLDR; [Issues](https://github.com/BeamMP/BeamMP-Server/issues) with the "help wanted" label or with nobody assigned. +TLDR; [Issues](https://github.com/BeamMP/BeamMP-Server/issues) with the "help wanted" or "good first issue" label or with nobody assigned. To contribute, look at the active [issues](https://github.com/BeamMP/BeamMP-Server/issues). Any issues that have the "help wanted" label or don't have anyone assigned are good tasks to take on. You can either contribute by programming or by testing and adding more info and ideas. @@ -39,7 +39,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 `v1.20`. +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`. ## Supported Operating Systems @@ -51,7 +51,7 @@ You can find precompiled binaries under [Releases](https://github.com/BeamMP/Bea ## Build Instructions -**__Do not compile from `master`. Always build from a release tag, i.e. `tags/v2.3.3`!__** +**__Do not compile from `master`. Always build from a release tag, i.e. `tags/v3.1.0`!__** Currently only Linux and Windows are supported (generally). See [Releases](https://github.com/BeamMP/BeamMP-Server/releases/) for official binary releases. On systems to which we do not provide binaries (so anything but windows), you are allowed to compile the program and use it. Other restrictions, such as not being allowed to distribute those binaries, still apply (see [copyright notice](#copyright)). @@ -59,36 +59,100 @@ Currently only Linux and Windows are supported (generally). See [Releases](https #### Windows +There are **no runtime libraries** needed for Windows. + Please use the prepackaged binaries in [Releases](https://github.com/BeamMP/BeamMP-Server/releases/). Dependencies for **Windows** can be installed with `vcpkg`. These are: ``` -lua -zlib -rapidjson -openssl -websocketpp -curl +lua zlib rapidjson openssl websocketpp curl ``` +The triplet we use for releases is `x64-windows-static`. #### Linux -Runtime dependencies - you want to find packages for: -- libz -- rapidjson -- lua5.3 -- ssl / openssl -- websocketpp -- curl (with ssl support) +We recommend Ubuntu 22.04 or Arch Linux. Any Linux distribution will work, but you have to figure out the package names yourself (please feel free to PR in a change to this README with that info). -Build-time dependencies are: +##### Runtime Dependencies + +These are needed to *run* the server. + +
+ +Ubuntu 22.04 + + +`apt-get install` the following libraries: +``` +liblua5.3-0 +libssl3 +curl +``` +
+ +
+ +Arch Linux + + +`pacman -Syu` the following libraries: +``` +lua53 +openssl +curl +``` +
+ +##### Build Dependencies +These are needed for you to *build* the server, in addition to the [runtime dependencies](#runtime-dependencies). + +**Ubuntu 22.04** +``` + +``` + +
+ +Ubuntu 22.04 + + +`apt-get install` the following libraries and programs: ``` git -make +libz-dev +rapidjson-dev +liblua5.3 +libssl-dev +libwebsocketpp-dev +libcurl4-openssl-dev +cmake +g++-10 +libboost1.74-all-dev +libssl3 +curl +``` +
+ +
+ +Arch Linux + + +`pacman -Syu` the following libraries and programs: +``` +lua53 +openssl +curl +git cmake g++ +cmake +zlib +boost +websocketpp ``` +
#### macOS @@ -106,13 +170,12 @@ brew install curl zlib git make On Windows, use git-bash for these commands. On Linux, these should work in your shell. 1. Make sure you have all [prerequisites](#prerequisites) installed -2. Clone the repository in a location of your choice with `git clone --recurse-submodules https://github.com/BeamMP/BeamMP-Server`. +2. Clone the repository in a location of your choice with `git clone https://github.com/BeamMP/BeamMP-Server` . 3. Change into the BeamMP-Server directory by running `cd BeamMP-Server`. -4. Checkout the branch of the release you want to compile, for example `git checkout tags/v3.0.2` for version 3.0.2. You can find the latest version [here](https://github.com/BeamMP/BeamMP-Server/tags). -5. Ensure that all submodules are initialized by running `git submodule update --init --recursive` -6. Run `cmake . -DCMAKE_BUILD_TYPE=Release` (with `.`) -7. Run `make` -8. You will now have a `BeamMP-Server` file in your directory, which is executable with `./BeamMP-Server` (`.\BeamMP-Server.exe` for windows). Follow the (Windows or Linux, doesnt matter) instructions on the [wiki](https://wiki.beammp.com/en/home/server-installation) for further setup after installation (which we just did), such as port-forwarding and getting a key to actually run the server. +4. Checkout the branch or tag of the release you want to compile, for example `git checkout tags/v3.0.2` for version 3.0.2. You can find the latest version [here](https://github.com/BeamMP/BeamMP-Server/tags). +6. Run `cmake . -DCMAKE_BUILD_TYPE=Release` (with `.`). This may take some time, and will update all submodules and prepare the build. +7. Run `make -j` . This step will take some time and will use a lot of CPU and RAM. Remove the `-j` if you run out of memory. *If you change something in the source code, you only have to re-run this step.* +8. You now have a `BeamMP-Server` file in your directory, which is executable with `./BeamMP-Server` (`.\BeamMP-Server.exe` for windows). Follow the (Windows or Linux, doesnt matter) instructions on the [wiki](https://wiki.beammp.com/en/home/server-installation) for further setup after installation (which we just did), such as port-forwarding and getting a key to actually run the server. *tip: to run the server in the background, simply (in bash, zsh, etc) run:* `nohup ./BeamMP-Server &`*.* diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 4e556b0..f439b19 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -1,9 +1,6 @@ -include_directories("${PROJECT_SOURCE_DIR}/deps/asio/asio/include") -include_directories("${PROJECT_SOURCE_DIR}/deps/rapidjson/include") -include_directories("${PROJECT_SOURCE_DIR}/deps/websocketpp") -include_directories("${PROJECT_SOURCE_DIR}/deps/commandline") -include_directories("${PROJECT_SOURCE_DIR}/deps/sol2/include") include_directories("${PROJECT_SOURCE_DIR}/deps") add_subdirectory("${PROJECT_SOURCE_DIR}/deps/commandline") +add_subdirectory("${PROJECT_SOURCE_DIR}/deps/fmt") add_subdirectory("${PROJECT_SOURCE_DIR}/deps/sol2") +add_subdirectory("${PROJECT_SOURCE_DIR}/deps/doctest") diff --git a/deps/asio b/deps/asio index d038fb3..4915cfd 160000 --- a/deps/asio +++ b/deps/asio @@ -1 +1 @@ -Subproject commit d038fb3c2fb56fb91ff1d17b0715cff7887aa09e +Subproject commit 4915cfd8a1653c157a1480162ae5601318553eb8 diff --git a/deps/commandline b/deps/commandline index d6b1c32..470cf2d 160000 --- a/deps/commandline +++ b/deps/commandline @@ -1 +1 @@ -Subproject commit d6b1c32c8af6ad5306f9f001305b3be9928ae4bb +Subproject commit 470cf2df4a6c94847b3a22868139095ae51902e6 diff --git a/deps/cpp-httplib b/deps/cpp-httplib index b324921..d92c314 160000 --- a/deps/cpp-httplib +++ b/deps/cpp-httplib @@ -1 +1 @@ -Subproject commit b324921c1aeff2976544128e4bb2a0979a4aa595 +Subproject commit d92c31446687cfa336a6332b1015b4fe289fbdec diff --git a/deps/doctest b/deps/doctest new file mode 160000 index 0000000..b7c21ec --- /dev/null +++ b/deps/doctest @@ -0,0 +1 @@ +Subproject commit b7c21ec5ceeadb4951b00396fc1e4642dd347e5f diff --git a/deps/fmt b/deps/fmt new file mode 160000 index 0000000..c4ee726 --- /dev/null +++ b/deps/fmt @@ -0,0 +1 @@ +Subproject commit c4ee726532178e556d923372f29163bd206d7732 diff --git a/deps/json b/deps/json index eb21824..69d7448 160000 --- a/deps/json +++ b/deps/json @@ -1 +1 @@ -Subproject commit eb2182414749825be086c825edb5229e5c28503d +Subproject commit 69d744867f8847c91a126fa25e9a6a3d67b3be41 diff --git a/deps/libzip b/deps/libzip index 76df02f..5532f9b 160000 --- a/deps/libzip +++ b/deps/libzip @@ -1 +1 @@ -Subproject commit 76df02f86b9746e139fd9fc934a70e3a21bbc557 +Subproject commit 5532f9baa0c44cc5435ad135686a4ea009075b9a diff --git a/deps/sentry-native b/deps/sentry-native index 90966cc..28be51f 160000 --- a/deps/sentry-native +++ b/deps/sentry-native @@ -1 +1 @@ -Subproject commit 90966cc1022b8155681b6899539b35466baccf2c +Subproject commit 28be51f5e3acb01327b1164206d3145464577670 diff --git a/deps/sol2 b/deps/sol2 index c068aef..eba8662 160000 --- a/deps/sol2 +++ b/deps/sol2 @@ -1 +1 @@ -Subproject commit c068aefbeddb3dd1f1fd38d42843ecb49a3b4cdb +Subproject commit eba86625b707e3c8c99bbfc4624e51f42dc9e561 diff --git a/deps/toml11 b/deps/toml11 index 1400dd2..c7627ff 160000 --- a/deps/toml11 +++ b/deps/toml11 @@ -1 +1 @@ -Subproject commit 1400dd223fb4297337266fcb5d04b700338aea71 +Subproject commit c7627ff6a1eb6f34fbd98369990a9442e2836c25 diff --git a/include/BoostAliases.h b/include/BoostAliases.h new file mode 100644 index 0000000..0348350 --- /dev/null +++ b/include/BoostAliases.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +using namespace boost::asio; diff --git a/include/Client.h b/include/Client.h index d513659..b8858d1 100644 --- a/include/Client.h +++ b/include/Client.h @@ -7,6 +7,7 @@ #include #include +#include "BoostAliases.h" #include "Common.h" #include "Compat.h" #include "VehicleData.h" @@ -19,9 +20,8 @@ class TServer; #endif // WINDOWS struct TConnection final { - SOCKET Socket; - struct sockaddr SockAddr; - socklen_t SockAddrLen; + ip::tcp::socket Socket; + ip::tcp::endpoint SockAddr; }; class TClient final { @@ -33,27 +33,34 @@ public: std::unique_lock Lock; }; - explicit TClient(TServer& Server); + TClient(TServer& Server, ip::tcp::socket&& Socket); TClient(const TClient&) = delete; + ~TClient(); TClient& operator=(const TClient&) = delete; void AddNewCar(int Ident, const std::string& Data); void SetCarData(int Ident, const std::string& Data); + void SetCarPosition(int Ident, const std::string& Data); TVehicleDataLockPair GetAllCars(); void SetName(const std::string& Name) { mName = Name; } void SetRoles(const std::string& Role) { mRole = Role; } void SetIdentifier(const std::string& key, const std::string& value) { mIdentifiers[key] = value; } std::string GetCarData(int Ident); - void SetUDPAddr(sockaddr_in Addr) { mUDPAddress = Addr; } - void SetDownSock(SOCKET CSock) { mSocket[1] = CSock; } - void SetTCPSock(SOCKET CSock) { mSocket[0] = CSock; } - void SetStatus(int Status) { mStatus = Status; } + std::string GetCarPositionRaw(int Ident); + void SetUDPAddr(const ip::udp::endpoint& Addr) { mUDPAddress = Addr; } + void SetDownSock(ip::tcp::socket&& CSock) { mDownSocket = std::move(CSock); } + void SetTCPSock(ip::tcp::socket&& CSock) { mSocket = std::move(CSock); } + void Disconnect(std::string_view Reason); + bool IsDisconnected() const { return !mSocket.is_open(); } // locks void DeleteCar(int Ident); [[nodiscard]] const std::unordered_map& GetIdentifiers() const { return mIdentifiers; } - [[nodiscard]] sockaddr_in GetUDPAddr() const { return mUDPAddress; } - [[nodiscard]] SOCKET GetDownSock() const { return mSocket[1]; } - [[nodiscard]] SOCKET GetTCPSock() const { return mSocket[0]; } + [[nodiscard]] const ip::udp::endpoint& GetUDPAddr() const { return mUDPAddress; } + [[nodiscard]] ip::udp::endpoint& GetUDPAddr() { return mUDPAddress; } + [[nodiscard]] ip::tcp::socket& GetDownSock() { return mDownSocket; } + [[nodiscard]] const ip::tcp::socket& GetDownSock() const { return mDownSocket; } + [[nodiscard]] ip::tcp::socket& GetTCPSock() { return mSocket; } + [[nodiscard]] const ip::tcp::socket& GetTCPSock() const { return mSocket; } [[nodiscard]] std::string GetRoles() const { return mRole; } [[nodiscard]] std::string GetName() const { return mName; } void SetUnicycleID(int ID) { mUnicycleID = ID; } @@ -61,7 +68,6 @@ public: [[nodiscard]] int GetOpenCarID() const; [[nodiscard]] int GetCarCount() const; void ClearCars(); - [[nodiscard]] int GetStatus() const { return mStatus; } [[nodiscard]] int GetID() const { return mID; } [[nodiscard]] int GetUnicycleID() const { return mUnicycleID; } [[nodiscard]] bool IsConnected() const { return mIsConnected; } @@ -71,9 +77,9 @@ public: void SetIsGuest(bool NewIsGuest) { mIsGuest = NewIsGuest; } void SetIsSynced(bool NewIsSynced) { mIsSynced = NewIsSynced; } void SetIsSyncing(bool NewIsSyncing) { mIsSyncing = NewIsSyncing; } - void EnqueuePacket(const std::string& Packet); - [[nodiscard]] std::queue& MissedPacketQueue() { return mPacketsSync; } - [[nodiscard]] const std::queue& MissedPacketQueue() const { return mPacketsSync; } + void EnqueuePacket(const std::vector& Packet); + [[nodiscard]] std::queue>& MissedPacketQueue() { return mPacketsSync; } + [[nodiscard]] const std::queue>& MissedPacketQueue() const { return mPacketsSync; } [[nodiscard]] size_t MissedPacketQueueSize() const { return mPacketsSync.size(); } [[nodiscard]] std::mutex& MissedPacketQueueMutex() const { return mMissedPacketsMutex; } void SetIsConnected(bool NewIsConnected) { mIsConnected = NewIsConnected; } @@ -89,18 +95,20 @@ private: bool mIsSynced = false; bool mIsSyncing = false; mutable std::mutex mMissedPacketsMutex; - std::queue mPacketsSync; + std::queue> mPacketsSync; std::unordered_map mIdentifiers; bool mIsGuest = false; mutable std::mutex mVehicleDataMutex; + mutable std::mutex mVehiclePositionMutex; TSetOfVehicleData mVehicleData; + SparseArray mVehiclePosition; std::string mName = "Unknown Client"; - SOCKET mSocket[2] { SOCKET(0), SOCKET(0) }; - sockaddr_in mUDPAddress {}; // is this initialization OK? yes it is + ip::tcp::socket mSocket; + ip::tcp::socket mDownSocket; + ip::udp::endpoint mUDPAddress {}; int mUnicycleID = -1; std::string mRole; std::string mDID; - int mStatus = 0; int mID = -1; std::chrono::time_point mLastPingTime; }; diff --git a/include/Common.h b/include/Common.h index 39714d0..06b24c8 100644 --- a/include/Common.h +++ b/include/Common.h @@ -8,13 +8,18 @@ extern TSentry Sentry; #include #include #include +#include #include #include #include +#include #include +#include #include -#include "Compat.h" +#include +#include +namespace fs = std::filesystem; #include "TConsole.h" @@ -27,6 +32,9 @@ struct Version { std::string AsString(); }; +template +using SparseArray = std::unordered_map; + // static class handling application start, shutdown, etc. // yes, static classes, singletons, globals are all pretty // bad idioms. In this case we need a central way to access @@ -50,6 +58,7 @@ public: bool DebugModeEnabled { false }; int Port { 30814 }; std::string CustomIP {}; + bool LogChat { true }; bool SendErrors { true }; bool SendErrorsMessageEnabled { true }; int HTTPServerPort { 8080 }; @@ -71,7 +80,7 @@ public: static TConsole& Console() { return *mConsole; } static std::string ServerVersionString(); static const Version& ServerVersion() { return mVersion; } - static std::string ClientVersionString() { return "2.0"; } + static uint8_t ClientMajorVersion() { return 2; } static std::string PPS() { return mPPS; } static void SetPPS(const std::string& NewPPS) { mPPS = NewPPS; } @@ -90,6 +99,8 @@ public: static void CheckForUpdates(); static std::array VersionStrToInts(const std::string& str); static bool IsOutdated(const Version& Current, const Version& Newest); + static bool IsShuttingDown(); + static void SleepSafeSeconds(size_t Seconds); static void InitializeConsole() { if (!mConsole) { @@ -115,99 +126,144 @@ public: static void SetSubsystemStatus(const std::string& Subsystem, Status status); private: + static void SetShutdown(bool Val); + static inline SystemStatusMap mSystemStatusMap {}; static inline std::mutex mSystemStatusMapMutex {}; static inline std::string mPPS; static inline std::unique_ptr mConsole; + static inline std::shared_mutex mShutdownMtx {}; + static inline bool mShutdown { false }; static inline std::mutex mShutdownHandlersMutex {}; static inline std::deque mShutdownHandlers {}; - static inline Version mVersion { 3, 0, 2 }; + static inline Version mVersion { 3, 1, 0 }; }; std::string ThreadName(bool DebugModeOverride = false); void RegisterThread(const std::string& str); #define RegisterThreadAuto() RegisterThread(__func__) -#define KB 1024 -#define MB (KB * 1024) +#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__) #define _in_lambda (std::string(__func__) == "operator()") -// we would like the full function signature 'void a::foo() const' -// on windows this is __FUNCSIG__, on GCC it's __PRETTY_FUNCTION__, -// feel free to add more -#if defined(WIN32) -#define _function_name std::string(__FUNCSIG__) -#elif defined(__unix) || defined(__unix__) -#define _function_name std::string(__PRETTY_FUNCTION__) -#else -#define _function_name std::string(__func__) -#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) -#define _this_location (ThreadName() + _function_name + " ") -#else -#define _this_location (ThreadName() + _file_basename + ":" + _line + " ") -#endif -#define SU_RAW SSU_UNRAW - -#else // !defined(DEBUG) - -#define SU_RAW RAWIFY(SSU_UNRAW) -#define _this_location (ThreadName()) - -#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) \ - do { \ - Application::Console().Write(_this_location + std::string("[ERROR] ") + (x)); \ - Sentry.AddErrorBreadcrumb((x), _file_basename, _line); \ - } while (false) -#define beammp_lua_error(x) \ - do { \ - Application::Console().Write(_this_location + std::string("[LUA ERROR] ") + (x)); \ - } while (false) -#define beammp_lua_warn(x) \ - do { \ - Application::Console().Write(_this_location + std::string("[LUA WARN] ") + (x)); \ - } while (false) -#define luaprint(x) Application::Console().Write(_this_location + std::string("[LUA] ") + (x)) -#define beammp_debug(x) \ - do { \ - if (Application::Settings.DebugModeEnabled) { \ - Application::Console().Write(_this_location + std::string("[DEBUG] ") + (x)); \ - } \ - } while (false) -#define beammp_event(x) \ - do { \ - if (Application::Settings.DebugModeEnabled) { \ - Application::Console().Write(_this_location + std::string("[EVENT] ") + (x)); \ - } \ - } while (false) // for those times when you just need to ignore something :^) // explicity disables a [[nodiscard]] warning #define beammp_ignore(x) (void)x -// trace() is a debug-build debug() + +// clang-format off +#ifdef DOCTEST_CONFIG_DISABLE + + // we would like the full function signature 'void a::foo() const' + // on windows this is __FUNCSIG__, on GCC it's __PRETTY_FUNCTION__, + // feel free to add more + #if defined(WIN32) + #define _function_name std::string(__FUNCSIG__) + #elif defined(__unix) || defined(__unix__) + #define _function_name std::string(__PRETTY_FUNCTION__) + #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) + #define _this_location (ThreadName() + _function_name + " ") + #else + #define _this_location (ThreadName() + _file_basename + ":" + _line + " ") + #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) \ + do { \ + Application::Console().Write(_this_location + std::string("[ERROR] ") + (x)); \ + Sentry.AddErrorBreadcrumb((x), _file_basename, _line); \ + } while (false) + #define beammp_lua_error(x) \ + do { \ + Application::Console().Write(_this_location + std::string("[LUA ERROR] ") + (x)); \ + } while (false) + #define beammp_lua_warn(x) \ + do { \ + Application::Console().Write(_this_location + std::string("[LUA WARN] ") + (x)); \ + } while (false) + #define luaprint(x) Application::Console().Write(_this_location + std::string("[LUA] ") + (x)) + #define beammp_debug(x) \ + do { \ + if (Application::Settings.DebugModeEnabled) { \ + Application::Console().Write(_this_location + std::string("[DEBUG] ") + (x)); \ + } \ + } while (false) + #define beammp_event(x) \ + do { \ + if (Application::Settings.DebugModeEnabled) { \ + Application::Console().Write(_this_location + std::string("[EVENT] ") + (x)); \ + } \ + } while (false) + // trace() is a debug-build debug() + #if defined(DEBUG) + #define beammp_trace(x) \ + do { \ + if (Application::Settings.DebugModeEnabled) { \ + Application::Console().Write(_this_location + std::string("[TRACE] ") + (x)); \ + } \ + } while (false) + #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__)) + #define beammp_warnf(...) beammp_warn(fmt::format(__VA_ARGS__)) + #define beammp_tracef(...) beammp_trace(fmt::format(__VA_ARGS__)) + #define beammp_lua_errorf(...) beammp_lua_error(fmt::format(__VA_ARGS__)) + #define beammp_lua_warnf(...) beammp_lua_warn(fmt::format(__VA_ARGS__)) + +#else // DOCTEST_CONFIG_DISABLE + + #define beammp_error(x) /* x */ + #define beammp_lua_error(x) /* x */ + #define beammp_warn(x) /* x */ + #define beammp_lua_warn(x) /* x */ + #define beammp_info(x) /* x */ + #define beammp_event(x) /* x */ + #define beammp_debug(x) /* x */ + #define beammp_trace(x) /* x */ + #define luaprint(x) /* x */ + #define beammp_errorf(...) beammp_error(fmt::format(__VA_ARGS__)) + #define beammp_infof(...) beammp_info(fmt::format(__VA_ARGS__)) + #define beammp_warnf(...) beammp_warn(fmt::format(__VA_ARGS__)) + #define beammp_debugf(...) beammp_debug(fmt::format(__VA_ARGS__)) + #define beammp_tracef(...) beammp_trace(fmt::format(__VA_ARGS__)) + #define beammp_lua_errorf(...) beammp_lua_error(fmt::format(__VA_ARGS__)) + #define beammp_lua_warnf(...) beammp_lua_warn(fmt::format(__VA_ARGS__)) + +#endif // DOCTEST_CONFIG_DISABLE + #if defined(DEBUG) -#define beammp_trace(x) \ - do { \ - if (Application::Settings.DebugModeEnabled) { \ - Application::Console().Write(_this_location + std::string("[TRACE] ") + (x)); \ - } \ - } while (false) + #define SU_RAW SSU_UNRAW #else -#define beammp_trace(x) -#endif // defined(DEBUG) + #define SU_RAW RAWIFY(SSU_UNRAW) + #define _this_location (ThreadName()) +#endif + +// clang-format on void LogChatMessage(const std::string& name, int id, const std::string& msg); @@ -219,11 +275,11 @@ inline T Comp(const T& Data) { // obsolete C.fill(0); z_stream defstream; - defstream.zalloc = Z_NULL; - defstream.zfree = Z_NULL; - defstream.opaque = Z_NULL; - defstream.avail_in = (uInt)Data.size(); - defstream.next_in = (Bytef*)&Data[0]; + defstream.zalloc = nullptr; + defstream.zfree = nullptr; + defstream.opaque = nullptr; + defstream.avail_in = uInt(Data.size()); + defstream.next_in = const_cast(reinterpret_cast(&Data[0])); defstream.avail_out = Biggest; defstream.next_out = reinterpret_cast(C.data()); deflateInit(&defstream, Z_BEST_COMPRESSION); @@ -244,13 +300,13 @@ inline T DeComp(const T& Compressed) { // not needed C.fill(0); z_stream infstream; - infstream.zalloc = Z_NULL; - infstream.zfree = Z_NULL; - infstream.opaque = Z_NULL; + infstream.zalloc = nullptr; + infstream.zfree = nullptr; + infstream.opaque = nullptr; infstream.avail_in = Biggest; - infstream.next_in = (Bytef*)(&Compressed[0]); + infstream.next_in = const_cast(reinterpret_cast(&Compressed[0])); infstream.avail_out = Biggest; - infstream.next_out = (Bytef*)(C.data()); + infstream.next_out = const_cast(reinterpret_cast(C.data())); inflateInit(&infstream); inflate(&infstream, Z_SYNC_FLUSH); inflate(&infstream, Z_FINISH); @@ -265,5 +321,3 @@ inline T DeComp(const T& Compressed) { std::string GetPlatformAgnosticErrorString(); #define S_DSN SU_RAW - -void LogChatMessage(const std::string& name, int id, const std::string& msg); diff --git a/include/Compat.h b/include/Compat.h index e1e906d..fd4de59 100644 --- a/include/Compat.h +++ b/include/Compat.h @@ -5,49 +5,23 @@ // ======================= UNIX ======================== #ifdef BEAMMP_LINUX -#include -#include +#include #include #include -#include -using SOCKET = int; -using DWORD = unsigned long; -using PDWORD = unsigned long*; -using LPDWORD = unsigned long*; char _getch(); -inline void CloseSocketProper(int TheSocket) { - shutdown(TheSocket, SHUT_RDWR); - close(TheSocket); -} #endif // unix // ======================= APPLE ======================== #ifdef BEAMMP_APPLE -#include -#include +#include #include #include -#include -using SOCKET = int; -using DWORD = unsigned long; -using PDWORD = unsigned long*; -using LPDWORD = unsigned long*; char _getch(); -inline void CloseSocketProper(int TheSocket) { - shutdown(TheSocket, SHUT_RDWR); - close(TheSocket); -} #endif // unix // ======================= WINDOWS ======================= #ifdef BEAMMP_WINDOWS #include -#include -inline void CloseSocketProper(SOCKET TheSocket) { - shutdown(TheSocket, 2); // 2 == SD_BOTH - closesocket(TheSocket); - -} #endif // WIN32 diff --git a/include/CustomAssert.h b/include/CustomAssert.h index 73179f0..df3c4c6 100644 --- a/include/CustomAssert.h +++ b/include/CustomAssert.h @@ -58,16 +58,17 @@ inline void _assert([[maybe_unused]] const char* file, [[maybe_unused]] const ch #define beammp_assert(cond) _assert(__FILE__, __func__, __LINE__, #cond, (cond)) #define beammp_assert_not_reachable() _assert(__FILE__, __func__, __LINE__, "reached unreachable code", false) #else -// In release build, these macros turn into NOPs. The compiler will optimize these out. -#define beammp_assert(cond) \ - do { \ - bool result = (cond); \ - if (!result) { \ - Sentry.LogAssert(#cond, _file_basename, _line, __func__); \ - } \ +#define beammp_assert(cond) \ + do { \ + bool result = (cond); \ + if (!result) { \ + beammp_errorf("Assertion failed in '{}:{}': {}.", __func__, _line, #cond); \ + Sentry.LogAssert(#cond, _file_basename, _line, __func__); \ + } \ } while (false) -#define beammp_assert_not_reachable() \ - do { \ - Sentry.LogAssert("code is unreachable", _file_basename, _line, __func__); \ +#define beammp_assert_not_reachable() \ + do { \ + beammp_errorf("Assertion failed in '{}:{}': Unreachable code reached. This may result in a crash or undefined state of the program.", __func__, _line); \ + Sentry.LogAssert("code is unreachable", _file_basename, _line, __func__); \ } while (false) #endif // DEBUG diff --git a/include/Http.h b/include/Http.h index 3ff2cd5..0898f2e 100644 --- a/include/Http.h +++ b/include/Http.h @@ -3,14 +3,14 @@ #include #include #include -#include -#include #include #include #if defined(BEAMMP_LINUX) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#pragma GCC diagnostic ignored "-Wcast-qual" +#pragma GCC diagnostic ignored "-Wold-style-cast" #endif #include #if defined(BEAMMP_LINUX) @@ -19,10 +19,6 @@ namespace fs = std::filesystem; -namespace Crypto { -constexpr size_t RSA_DEFAULT_KEYLENGTH { 2048 }; -} - 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 = {}); @@ -32,13 +28,9 @@ namespace Status { const std::string ErrorString = "-1"; namespace Server { - void SetupEnvironment(); - // todo: Add non TLS Server Instance, this one is TLS only class THttpServerInstance { public: THttpServerInstance(); - static fs::path KeyFilePath; - static fs::path CertFilePath; protected: void operator()(); @@ -46,15 +38,5 @@ namespace Server { private: std::thread mThread; }; - // todo: all of these functions are likely unsafe, - // todo: replace with something that's managed by a domain specific crypto library - class Tx509KeypairGenerator { - public: - static long GenerateRandomId(); - static bool EnsureTLSConfigExists(); - static X509* GenerateCertificate(EVP_PKEY& pkey); - static EVP_PKEY* GenerateKey(); - static void GenerateAndWriteToDisk(const fs::path& KeyFilePath, const fs::path& CertFilePath); - }; } } diff --git a/include/LuaAPI.h b/include/LuaAPI.h index 0cd0e1f..6d2af31 100644 --- a/include/LuaAPI.h +++ b/include/LuaAPI.h @@ -12,17 +12,26 @@ namespace MP { std::string GetOSName(); std::tuple GetServerVersion(); - bool TriggerClientEvent(int PlayerID, const std::string& EventName, const std::string& Data); + std::pair TriggerClientEvent(int PlayerID, const std::string& EventName, const sol::object& Data); + std::pair TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data); inline size_t GetPlayerCount() { return Engine->Server().ClientCount(); } - void DropPlayer(int ID, std::optional MaybeReason); - void SendChatMessage(int ID, const std::string& Message); - void RemoveVehicle(int PlayerID, int VehicleID); + std::pair DropPlayer(int ID, std::optional MaybeReason); + std::pair SendChatMessage(int ID, const std::string& Message); + std::pair RemoveVehicle(int PlayerID, int VehicleID); void Set(int ConfigID, sol::object NewValue); bool IsPlayerGuest(int ID); bool IsPlayerConnected(int ID); void Sleep(size_t Ms); void PrintRaw(sol::variadic_args); + std::string JsonEncode(const sol::table& object); + std::string JsonDiff(const std::string& a, const std::string& b); + std::string JsonDiffApply(const std::string& data, const std::string& patch); + std::string JsonPrettify(const std::string& json); + std::string JsonMinify(const std::string& json); + std::string JsonFlatten(const std::string& json); + std::string JsonUnflatten(const std::string& json); } + namespace FS { std::pair CreateDirectory(const std::string& Path); std::pair Remove(const std::string& Path); diff --git a/include/TConfig.h b/include/TConfig.h index 3fcc597..d9d6045 100644 --- a/include/TConfig.h +++ b/include/TConfig.h @@ -3,6 +3,7 @@ #include "Common.h" #include +#include #define TOML11_PRESERVE_COMMENTS_BY_DEFAULT #include // header-only version of TOML++ @@ -18,7 +19,7 @@ public: void FlushToFile(); private: - void CreateConfigFile(std::string_view name); + void CreateConfigFile(); void ParseFromFile(std::string_view name); void PrintDebug(); void TryReadValue(toml::value& Table, const std::string& Category, const std::string_view& Key, std::string& OutValue); diff --git a/include/TConsole.h b/include/TConsole.h index 839f862..22a3b01 100644 --- a/include/TConsole.h +++ b/include/TConsole.h @@ -4,6 +4,11 @@ #include "commandline.h" #include #include +#include +#include +#include +#include +#include class TLuaEngine; @@ -22,13 +27,33 @@ private: void RunAsCommand(const std::string& cmd, bool IgnoreNotACommand = false); void ChangeToLuaConsole(const std::string& LuaStateId); void ChangeToRegularConsole(); + void HandleLuaInternalCommand(const std::string& cmd); - void Command_Lua(const std::string& cmd); - void Command_Help(const std::string& cmd); - void Command_Kick(const std::string& cmd); - void Command_Say(const std::string& cmd); - void Command_List(const std::string& cmd); - void Command_Status(const std::string& cmd); + void Command_Lua(const std::string& cmd, const std::vector& args); + void Command_Help(const std::string& cmd, const std::vector& args); + void Command_Kick(const std::string& cmd, const std::vector& args); + void Command_List(const std::string& cmd, const std::vector& args); + void Command_Status(const std::string& cmd, const std::vector& args); + void Command_Settings(const std::string& cmd, const std::vector& args); + void Command_Clear(const std::string&, const std::vector& args); + + void Command_Say(const std::string& FullCommand); + bool EnsureArgsCount(const std::vector& args, size_t n); + bool EnsureArgsCount(const std::vector& args, size_t min, size_t max); + + static std::tuple> ParseCommand(const std::string& cmd); + static std::string ConcatArgs(const std::vector& args, char space = ' '); + + std::unordered_map&)>> mCommandMap = { + { "lua", [this](const auto& a, const auto& b) { Command_Lua(a, b); } }, + { "help", [this](const auto& a, const auto& b) { Command_Help(a, b); } }, + { "kick", [this](const auto& a, const auto& b) { Command_Kick(a, b); } }, + { "list", [this](const auto& a, const auto& b) { Command_List(a, b); } }, + { "status", [this](const auto& a, const auto& b) { Command_Status(a, b); } }, + { "settings", [this](const auto& a, const auto& b) { Command_Settings(a, b); } }, + { "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 + }; Commandline mCommandline; std::vector mCachedLuaHistory; diff --git a/include/THeartbeatThread.h b/include/THeartbeatThread.h index 1f0ab14..1063be6 100644 --- a/include/THeartbeatThread.h +++ b/include/THeartbeatThread.h @@ -15,7 +15,6 @@ private: std::string GenerateCall(); std::string GetPlayers(); - bool mShutdown = false; TResourceManager& mResourceManager; TServer& mServer; }; diff --git a/include/TLuaEngine.h b/include/TLuaEngine.h index 5ca2222..de19f13 100644 --- a/include/TLuaEngine.h +++ b/include/TLuaEngine.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -24,17 +25,18 @@ namespace fs = std::filesystem; /** * std::variant means, that TLuaArgTypes may be one of the Types listed as template args */ -using TLuaArgTypes = std::variant; +using TLuaArgTypes = std::variant>; static constexpr size_t TLuaArgTypes_String = 0; static constexpr size_t TLuaArgTypes_Int = 1; static constexpr size_t TLuaArgTypes_VariadicArgs = 2; static constexpr size_t TLuaArgTypes_Bool = 3; +static constexpr size_t TLuaArgTypes_StringStringMap = 4; class TLuaPlugin; struct TLuaResult { - std::atomic_bool Ready; - std::atomic_bool Error; + bool Ready; + bool Error; std::string ErrorMessage; sol::object Result { sol::lua_nil }; TLuaStateId StateId; @@ -47,6 +49,7 @@ struct TLuaPluginConfig { static inline const std::string FileName = "PluginConfig.toml"; TLuaStateId StateId; // TODO: Add execute list + // TODO: Build a better toml serializer, or some way to do this in an easier way }; struct TLuaChunk { @@ -58,20 +61,7 @@ struct TLuaChunk { std::string PluginPath; }; -class TPluginMonitor : IThreaded { -public: - TPluginMonitor(const fs::path& Path, TLuaEngine& Engine, std::atomic_bool& Shutdown); - - void operator()(); - -private: - TLuaEngine& mEngine; - fs::path mPath; - std::atomic_bool& mShutdown; - std::unordered_map mFileTimes; -}; - -class TLuaEngine : IThreaded { +class TLuaEngine : public std::enable_shared_from_this, IThreaded { public: enum CallStrategy : int { BestEffort, @@ -102,10 +92,18 @@ public: std::unique_lock Lock(mResultsToCheckMutex); return mResultsToCheck.size(); } + size_t GetLuaStateCount() { std::unique_lock Lock(mLuaStatesMutex); return mLuaStates.size(); } + std::vector GetLuaStateNames() { + std::vector names{}; + for(auto const& [stateId, _ ] : mLuaStates) { + names.push_back(stateId); + } + return names; + } size_t GetTimedEventsCount() { std::unique_lock Lock(mTimedEventsMutex); return mTimedEvents.size(); @@ -129,7 +127,6 @@ public: [[nodiscard]] std::shared_ptr EnqueueFunctionCall(TLuaStateId StateID, const std::string& FunctionName, const std::vector& Args); void EnsureStateExists(TLuaStateId StateId, const std::string& Name, bool DontCallOnInit = false); void RegisterEvent(const std::string& EventName, TLuaStateId StateId, const std::string& FunctionName); - template /** * * @tparam ArgsT Template Arguments for the event (Metadata) todo: figure out what this means @@ -138,6 +135,7 @@ public: * @param Args * @return */ + template [[nodiscard]] std::vector> TriggerEvent(const std::string& EventName, TLuaStateId IgnoreId, ArgsT&&... Args) { std::unique_lock Lock(mLuaEventsMutex); beammp_event(EventName); @@ -157,6 +155,21 @@ public: } return Results; // } + template + [[nodiscard]] std::vector> TriggerLocalEvent(const TLuaStateId& StateId, const std::string& EventName, ArgsT&&... Args) { + std::unique_lock Lock(mLuaEventsMutex); + beammp_event(EventName + " in '" + StateId + "'"); + if (mLuaEvents.find(EventName) == mLuaEvents.end()) { // if no event handler is defined for 'EventName', return immediately + return {}; + } + std::vector> Results; + std::vector Arguments { TLuaArgTypes { std::forward(Args) }... }; + const auto Handlers = GetEventHandlersForState(EventName, StateId); + for (const auto& Handler : Handlers) { + Results.push_back(EnqueueFunctionCall(StateId, Handler, Arguments)); + } + return Results; + } std::set GetEventHandlersForState(const std::string& EventName, TLuaStateId StateId); void CreateEventTimer(const std::string& EventName, TLuaStateId StateId, size_t IntervalMS, CallStrategy Strategy); void CancelEventTimers(const std::string& EventName, TLuaStateId StateId); @@ -166,6 +179,15 @@ public: static constexpr const char* BeamMPFnNotFoundError = "BEAMMP_FN_NOT_FOUND"; + std::vector GetStateGlobalKeysForState(TLuaStateId StateId); + std::vector GetStateTableKeysForState(TLuaStateId StateId, std::vector keys); + + // Debugging functions (slow) + std::unordered_map /* handlers */> Debug_GetEventsForState(TLuaStateId StateId); + std::queue>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId); + std::vector Debug_GetStateFunctionQueueForState(TLuaStateId StateId); + std::vector Debug_GetResultsToCheckForState(TLuaStateId StateId); + private: void CollectAndInitPlugins(); void InitializePlugin(const fs::path& Folder, const TLuaPluginConfig& Config); @@ -174,7 +196,7 @@ private: class StateThreadData : IThreaded { public: - StateThreadData(const std::string& Name, std::atomic_bool& Shutdown, TLuaStateId StateId, TLuaEngine& Engine); + StateThreadData(const std::string& Name, TLuaStateId StateId, TLuaEngine& Engine); StateThreadData(const StateThreadData&) = delete; ~StateThreadData() noexcept { beammp_debug("\"" + mStateId + "\" destroyed"); } [[nodiscard]] std::shared_ptr EnqueueScript(const TLuaChunk& Script); @@ -185,6 +207,13 @@ private: void operator()() override; sol::state_view State() { return sol::state_view(mState); } + std::vector GetStateGlobalKeys(); + std::vector GetStateTableKeys(const std::vector& keys); + + // Debug functions, slow + std::queue>> Debug_GetStateExecuteQueue(); + std::vector Debug_GetStateFunctionQueue(); + private: sol::table Lua_TriggerGlobalEvent(const std::string& EventName, sol::variadic_args EventArgs); sol::table Lua_TriggerLocalEvent(const std::string& EventName, sol::variadic_args EventArgs); @@ -192,11 +221,14 @@ private: sol::table Lua_GetPlayers(); std::string Lua_GetPlayerName(int ID); sol::table Lua_GetPlayerVehicles(int ID); + std::pair Lua_GetPositionRaw(int PID, int VID); sol::table Lua_HttpCreateConnection(const std::string& host, uint16_t port); + sol::table Lua_JsonDecode(const std::string& str); int Lua_GetPlayerIDByName(const std::string& Name); + sol::table Lua_FS_ListFiles(const std::string& Path); + sol::table Lua_FS_ListDirectories(const std::string& Path); std::string mName; - std::atomic_bool& mShutdown; TLuaStateId mStateId; lua_State* mState; std::thread mThread; @@ -209,6 +241,8 @@ private: sol::state_view mStateView { mState }; std::queue mPaths; std::recursive_mutex mPathsMutex; + std::mt19937 mMersenneTwister; + std::uniform_real_distribution mUniformRealDistribution01; }; struct TimedEvent { @@ -223,9 +257,7 @@ private: TNetwork* mNetwork; TServer* mServer; - TPluginMonitor mPluginMonitor; - std::atomic_bool mShutdown { false }; - fs::path mResourceServerPath; + const fs::path mResourceServerPath; std::vector> mLuaPlugins; std::unordered_map> mLuaStates; std::recursive_mutex mLuaStatesMutex; diff --git a/include/TNetwork.h b/include/TNetwork.h index 528aef4..3b4980e 100644 --- a/include/TNetwork.h +++ b/include/TNetwork.h @@ -1,8 +1,11 @@ #pragma once +#include "BoostAliases.h" #include "Compat.h" #include "TResourceManager.h" #include "TServer.h" +#include +#include struct TConnection; @@ -10,19 +13,18 @@ class TNetwork { public: TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& ResourceManager); - [[nodiscard]] bool TCPSend(TClient& c, const std::string& Data, bool IsSync = false); - [[nodiscard]] bool SendLarge(TClient& c, std::string Data, bool isSync = false); - [[nodiscard]] bool Respond(TClient& c, const std::string& MSG, bool Rel, bool isSync = false); - std::shared_ptr CreateClient(SOCKET TCPSock); - std::string TCPRcv(TClient& c); + [[nodiscard]] bool TCPSend(TClient& c, const std::vector& Data, bool IsSync = false); + [[nodiscard]] bool SendLarge(TClient& c, std::vector Data, bool isSync = false); + [[nodiscard]] bool Respond(TClient& c, const std::vector& MSG, bool Rel, bool isSync = false); + std::shared_ptr CreateClient(ip::tcp::socket&& TCPSock); + std::vector TCPRcv(TClient& c); void ClientKick(TClient& c, const std::string& R); [[nodiscard]] bool SyncClient(const std::weak_ptr& c); - void Identify(const TConnection& client); - void Authentication(const TConnection& ClientConnection); - [[nodiscard]] bool CheckBytes(TClient& c, int32_t BytesRcv); + void Identify(TConnection&& client); + std::shared_ptr Authentication(TConnection&& ClientConnection); void SyncResources(TClient& c); - [[nodiscard]] bool UDPSend(TClient& Client, std::string Data) const; - void SendToAll(TClient* c, const std::string& Data, bool Self, bool Rel); + [[nodiscard]] bool UDPSend(TClient& Client, std::vector Data); + void SendToAll(TClient* c, const std::vector& Data, bool Self, bool Rel); void UpdatePlayer(TClient& Client); private: @@ -31,21 +33,23 @@ private: TServer& mServer; TPPSMonitor& mPPSMonitor; - SOCKET mUDPSock {}; - bool mShutdown { false }; + ip::udp::socket mUDPSock; TResourceManager& mResourceManager; std::thread mUDPThread; std::thread mTCPThread; - std::string UDPRcvFromClient(sockaddr_in& client) const; - void HandleDownload(SOCKET TCPSock); + std::vector UDPRcvFromClient(ip::udp::endpoint& ClientEndpoint); + void HandleDownload(TConnection&& TCPSock); void OnConnect(const std::weak_ptr& c); void TCPClient(const std::weak_ptr& c); void Looper(const std::weak_ptr& c); int OpenID(); - void OnDisconnect(const std::weak_ptr& ClientPtr, bool kicked); - void Parse(TClient& c, const std::string& Packet); + void OnDisconnect(const std::weak_ptr& ClientPtr); + void Parse(TClient& c, const std::vector& Packet); void SendFile(TClient& c, const std::string& Name); - static bool TCPSendRaw(TClient& C, SOCKET socket, char* Data, int32_t Size); + static bool TCPSendRaw(TClient& C, ip::tcp::socket& socket, const uint8_t* Data, size_t Size); static void SplitLoad(TClient& c, size_t Sent, size_t Size, bool D, const std::string& Name); + static const uint8_t* SendSplit(TClient& c, ip::tcp::socket& Socket, const uint8_t* DataPtr, size_t Size); }; + +std::vector StringToVector(const std::string& Str); diff --git a/include/TPPSMonitor.h b/include/TPPSMonitor.h index 508dfc5..0718f10 100644 --- a/include/TPPSMonitor.h +++ b/include/TPPSMonitor.h @@ -22,6 +22,5 @@ private: TServer& mServer; std::optional> mNetwork { std::nullopt }; - bool mShutdown { false }; int mInternalPPS { 0 }; -}; \ No newline at end of file +}; diff --git a/include/TPluginMonitor.h b/include/TPluginMonitor.h new file mode 100644 index 0000000..2ed77bc --- /dev/null +++ b/include/TPluginMonitor.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Common.h" +#include "IThreaded.h" + +#include +#include +#include + +class TLuaEngine; + +class TPluginMonitor : IThreaded, public std::enable_shared_from_this { +public: + TPluginMonitor(const fs::path& Path, std::shared_ptr Engine); + + void operator()(); + +private: + std::shared_ptr mEngine; + fs::path mPath; + std::unordered_map mFileTimes; +}; diff --git a/include/TServer.h b/include/TServer.h index 2e51e12..0967381 100644 --- a/include/TServer.h +++ b/include/TServer.h @@ -8,6 +8,8 @@ #include #include +#include "BoostAliases.h" + class TClient; class TNetwork; class TPPSMonitor; @@ -19,23 +21,35 @@ public: TServer(const std::vector& Arguments); void InsertClient(const std::shared_ptr& Ptr); - std::weak_ptr InsertNewClient(); void RemoveClient(const std::weak_ptr&); // in Fn, return true to continue, return false to break void ForEachClient(const std::function)>& Fn); size_t ClientCount() const; - static void GlobalParser(const std::weak_ptr& Client, std::string Packet, TPPSMonitor& PPSMonitor, TNetwork& Network); + static void GlobalParser(const std::weak_ptr& Client, std::vector&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network); static void HandleEvent(TClient& c, const std::string& Data); RWMutex& GetClientMutex() const { return mClientsMutex; } - const TScopedTimer UptimeTimer; + + // asio io context + io_context& IoCtx() { return mIoCtx; } + private: + io_context mIoCtx {}; TClientSet mClients; mutable RWMutex mClientsMutex; static void ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network); static bool ShouldSpawn(TClient& c, const std::string& CarJson, int ID); static bool IsUnicycle(TClient& c, const std::string& CarJson); static void Apply(TClient& c, int VID, const std::string& pckt); + static void HandlePosition(TClient& c, const std::string& Packet); +}; + +struct BufferView { + uint8_t* Data { nullptr }; + size_t Size { 0 }; + const uint8_t* data() const { return Data; } + uint8_t* data() { return Data; } + size_t size() const { return Size; } }; diff --git a/src/ArgsParser.cpp b/src/ArgsParser.cpp index 8440661..f0d6257 100644 --- a/src/ArgsParser.cpp +++ b/src/ArgsParser.cpp @@ -1,6 +1,7 @@ #include "ArgsParser.h" #include "Common.h" #include +#include void ArgsParser::Parse(const std::vector& ArgList) { for (const auto& Arg : ArgList) { @@ -12,7 +13,7 @@ void ArgsParser::Parse(const std::vector& ArgList) { ConsumeLongFlag(std::string(Arg)); } } else { - beammp_error("Error parsing commandline arguments: Supplied argument '" + std::string(Arg) + "' is not a valid argument and was ignored."); + beammp_errorf("Error parsing commandline arguments: Supplied argument '{}' is not a valid argument and was ignored.", Arg); } } } @@ -21,7 +22,7 @@ bool ArgsParser::Verify() { bool Ok = true; for (const auto& RegisteredArg : mRegisteredArguments) { if (RegisteredArg.Flags & Flags::REQUIRED && !FoundArgument(RegisteredArg.Names)) { - beammp_error("Error in commandline arguments: Argument '" + std::string(RegisteredArg.Names.at(0)) + "' is required but wasn't found."); + beammp_errorf("Error in commandline arguments: Argument '{}' is required but wasn't found.", RegisteredArg.Names.at(0)); Ok = false; continue; } else if (FoundArgument(RegisteredArg.Names)) { @@ -92,3 +93,78 @@ void ArgsParser::ConsumeLongFlag(const std::string& Arg) { beammp_warn("Argument '" + Name + "' was supplied but isn't a known argument, so it is likely being ignored."); } } + +TEST_CASE("ArgsParser") { + ArgsParser parser; + + SUBCASE("Simple args") { + parser.RegisterArgument({ "a" }, ArgsParser::Flags::NONE); + parser.RegisterArgument({ "hello" }, ArgsParser::Flags::NONE); + parser.Parse({ "--a", "--hello" }); + CHECK(parser.Verify()); + CHECK(parser.FoundArgument({ "a" })); + CHECK(parser.FoundArgument({ "hello" })); + CHECK(parser.FoundArgument({ "a", "hello" })); + CHECK(!parser.FoundArgument({ "b" })); + CHECK(!parser.FoundArgument({ "goodbye" })); + } + + SUBCASE("No args") { + parser.RegisterArgument({ "a" }, ArgsParser::Flags::NONE); + parser.RegisterArgument({ "hello" }, ArgsParser::Flags::NONE); + parser.Parse({}); + CHECK(parser.Verify()); + CHECK(!parser.FoundArgument({ "a" })); + CHECK(!parser.FoundArgument({ "hello" })); + CHECK(!parser.FoundArgument({ "a", "hello" })); + CHECK(!parser.FoundArgument({ "b" })); + CHECK(!parser.FoundArgument({ "goodbye" })); + CHECK(!parser.FoundArgument({ "" })); + } + + SUBCASE("Value args") { + parser.RegisterArgument({ "a" }, ArgsParser::Flags::HAS_VALUE); + parser.RegisterArgument({ "hello" }, ArgsParser::Flags::HAS_VALUE); + parser.Parse({ "--a=5", "--hello=world" }); + CHECK(parser.Verify()); + REQUIRE(parser.FoundArgument({ "a" })); + REQUIRE(parser.FoundArgument({ "hello" })); + CHECK(parser.GetValueOfArgument({ "a" }).has_value()); + CHECK(parser.GetValueOfArgument({ "a" }).value() == "5"); + CHECK(parser.GetValueOfArgument({ "hello" }).has_value()); + CHECK(parser.GetValueOfArgument({ "hello" }).value() == "world"); + } + + SUBCASE("Mixed value & no-value args") { + parser.RegisterArgument({ "a" }, ArgsParser::Flags::HAS_VALUE); + parser.RegisterArgument({ "hello" }, ArgsParser::Flags::NONE); + parser.Parse({ "--a=5", "--hello" }); + CHECK(parser.Verify()); + REQUIRE(parser.FoundArgument({ "a" })); + REQUIRE(parser.FoundArgument({ "hello" })); + CHECK(parser.GetValueOfArgument({ "a" }).has_value()); + CHECK(parser.GetValueOfArgument({ "a" }).value() == "5"); + CHECK(!parser.GetValueOfArgument({ "hello" }).has_value()); + } + + SUBCASE("Required args") { + SUBCASE("Two required, two present") { + parser.RegisterArgument({ "a" }, ArgsParser::Flags::REQUIRED); + parser.RegisterArgument({ "hello" }, ArgsParser::Flags::REQUIRED); + parser.Parse({ "--a", "--hello" }); + CHECK(parser.Verify()); + } + SUBCASE("Two required, one present") { + parser.RegisterArgument({ "a" }, ArgsParser::Flags::REQUIRED); + parser.RegisterArgument({ "hello" }, ArgsParser::Flags::REQUIRED); + parser.Parse({ "--a" }); + CHECK(!parser.Verify()); + } + SUBCASE("Two required, none present") { + parser.RegisterArgument({ "a" }, ArgsParser::Flags::REQUIRED); + parser.RegisterArgument({ "hello" }, ArgsParser::Flags::REQUIRED); + parser.Parse({ "--b" }); + CHECK(!parser.Verify()); + } + } +} diff --git a/src/Client.cpp b/src/Client.cpp index 4edbbc7..b03c99a 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -5,8 +5,6 @@ #include #include -// FIXME: add debug prints - void TClient::DeleteCar(int Ident) { std::unique_lock lock(mVehicleDataMutex); auto iter = std::find_if(mVehicleData.begin(), mVehicleData.end(), [&](auto& elem) { @@ -49,6 +47,34 @@ TClient::TVehicleDataLockPair TClient::GetAllCars() { return { &mVehicleData, std::unique_lock(mVehicleDataMutex) }; } +std::string TClient::GetCarPositionRaw(int Ident) { + std::unique_lock lock(mVehiclePositionMutex); + try { + return mVehiclePosition.at(Ident); + } catch (const std::out_of_range& oor) { + return ""; + } + return ""; +} + +void TClient::Disconnect(std::string_view Reason) { + beammp_debugf("Disconnecting client {} for reason: {}", GetID(), Reason); + boost::system::error_code ec; + mSocket.shutdown(socket_base::shutdown_both, ec); + if (ec) { + beammp_debugf("Failed to shutdown client socket: {}", ec.message()); + } + mSocket.close(ec); + if (ec) { + beammp_debugf("Failed to close client socket: {}", ec.message()); + } +} + +void TClient::SetCarPosition(int Ident, const std::string& Data) { + std::unique_lock lock(mVehiclePositionMutex); + mVehiclePosition[Ident] = Data; +} + std::string TClient::GetCarData(int Ident) { { // lock std::unique_lock lock(mVehicleDataMutex); @@ -83,16 +109,22 @@ TServer& TClient::Server() const { return mServer; } -void TClient::EnqueuePacket(const std::string& Packet) { +void TClient::EnqueuePacket(const std::vector& Packet) { std::unique_lock Lock(mMissedPacketsMutex); mPacketsSync.push(Packet); } -TClient::TClient(TServer& Server) +TClient::TClient(TServer& Server, ip::tcp::socket&& Socket) : mServer(Server) + , mSocket(std::move(Socket)) + , mDownSocket(ip::tcp::socket(Server.IoCtx())) , mLastPingTime(std::chrono::high_resolution_clock::now()) { } +TClient::~TClient() { + beammp_debugf("client destroyed: {} ('{}')", this->GetID(), this->GetName()); +} + void TClient::UpdatePingTime() { mLastPingTime = std::chrono::high_resolution_clock::now(); } diff --git a/src/Common.cpp b/src/Common.cpp index 34b28ea..5b153ca 100644 --- a/src/Common.cpp +++ b/src/Common.cpp @@ -9,9 +9,13 @@ #include #include +#include "Compat.h" #include "CustomAssert.h" #include "Http.h" +// global, yes, this is ugly, no, it cant be done another way +TSentry Sentry {}; + Application::TSettings Application::Settings = {}; void Application::RegisterShutdownHandler(const TShutdownHandler& Handler) { @@ -22,6 +26,7 @@ void Application::RegisterShutdownHandler(const TShutdownHandler& Handler) { } void Application::GracefullyShutdown() { + SetShutdown(true); static bool AlreadyShuttingDown = false; static uint8_t ShutdownAttempts = 0; if (AlreadyShuttingDown) { @@ -43,6 +48,7 @@ void Application::GracefullyShutdown() { beammp_info("Subsystem " + std::to_string(i + 1) + "/" + std::to_string(mShutdownHandlers.size()) + " shutting down"); mShutdownHandlers[i](); } + // std::exit(-1); } std::string Application::ServerVersionString() { @@ -60,7 +66,23 @@ std::array Application::VersionStrToInts(const std::string& str) { return Version; } -// FIXME: This should be used by operator< on Version +TEST_CASE("Application::VersionStrToInts") { + auto v = Application::VersionStrToInts("1.2.3"); + CHECK(v[0] == 1); + CHECK(v[1] == 2); + CHECK(v[2] == 3); + + v = Application::VersionStrToInts("10.20.30"); + CHECK(v[0] == 10); + CHECK(v[1] == 20); + CHECK(v[2] == 30); + + v = Application::VersionStrToInts("100.200.255"); + CHECK(v[0] == 100); + CHECK(v[1] == 200); + CHECK(v[2] == 255); +} + bool Application::IsOutdated(const Version& Current, const Version& Newest) { if (Newest.major > Current.major) { return true; @@ -73,6 +95,65 @@ bool Application::IsOutdated(const Version& Current, const Version& Newest) { } } +bool Application::IsShuttingDown() { + std::shared_lock Lock(mShutdownMtx); + return mShutdown; +} + +void Application::SleepSafeSeconds(size_t Seconds) { + // Sleeps for 500 ms, checks if a shutdown occurred, and so forth + for (size_t i = 0; i < Seconds * 2; ++i) { + if (Application::IsShuttingDown()) { + return; + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + } +} + +TEST_CASE("Application::IsOutdated (version check)") { + SUBCASE("Same version") { + CHECK(!Application::IsOutdated({ 1, 2, 3 }, { 1, 2, 3 })); + } + // we need to use over 1-2 digits to test against lexical comparisons + SUBCASE("Patch outdated") { + for (uint8_t Patch = 0; Patch < 10; ++Patch) { + for (uint8_t Minor = 0; Minor < 10; ++Minor) { + for (uint8_t Major = 0; Major < 10; ++Major) { + CHECK(Application::IsOutdated({ uint8_t(Major), uint8_t(Minor), uint8_t(Patch) }, { uint8_t(Major), uint8_t(Minor), uint8_t(Patch + 1) })); + } + } + } + } + SUBCASE("Minor outdated") { + for (uint8_t Patch = 0; Patch < 10; ++Patch) { + for (uint8_t Minor = 0; Minor < 10; ++Minor) { + for (uint8_t Major = 0; Major < 10; ++Major) { + CHECK(Application::IsOutdated({ uint8_t(Major), uint8_t(Minor), uint8_t(Patch) }, { uint8_t(Major), uint8_t(Minor + 1), uint8_t(Patch) })); + } + } + } + } + SUBCASE("Major outdated") { + for (uint8_t Patch = 0; Patch < 10; ++Patch) { + for (uint8_t Minor = 0; Minor < 10; ++Minor) { + for (uint8_t Major = 0; Major < 10; ++Major) { + CHECK(Application::IsOutdated({ uint8_t(Major), uint8_t(Minor), uint8_t(Patch) }, { uint8_t(Major + 1), uint8_t(Minor), uint8_t(Patch) })); + } + } + } + } + SUBCASE("All outdated") { + for (uint8_t Patch = 0; Patch < 10; ++Patch) { + for (uint8_t Minor = 0; Minor < 10; ++Minor) { + for (uint8_t Major = 0; Major < 10; ++Major) { + CHECK(Application::IsOutdated({ uint8_t(Major), uint8_t(Minor), uint8_t(Patch) }, { uint8_t(Major + 1), uint8_t(Minor + 1), uint8_t(Patch + 1) })); + } + } + } + } +} + void Application::SetSubsystemStatus(const std::string& Subsystem, Status status) { switch (status) { case Status::Good: @@ -90,18 +171,34 @@ void Application::SetSubsystemStatus(const std::string& Subsystem, Status status case Status::Shutdown: beammp_trace("Subsystem '" + Subsystem + "': Shutdown"); break; + default: + beammp_assert_not_reachable(); } std::unique_lock Lock(mSystemStatusMapMutex); mSystemStatusMap[Subsystem] = status; } +void Application::SetShutdown(bool Val) { + std::unique_lock Lock(mShutdownMtx); + mShutdown = Val; +} + +TEST_CASE("Application::SetSubsystemStatus") { + Application::SetSubsystemStatus("Test", Application::Status::Good); + auto Map = Application::GetSubsystemStatuses(); + CHECK(Map.at("Test") == Application::Status::Good); + Application::SetSubsystemStatus("Test", Application::Status::Bad); + Map = Application::GetSubsystemStatuses(); + CHECK(Map.at("Test") == Application::Status::Bad); +} + void Application::CheckForUpdates() { Application::SetSubsystemStatus("UpdateCheck", Application::Status::Starting); static bool FirstTime = true; // checks current version against latest version std::regex VersionRegex { R"(\d+\.\d+\.\d+\n*)" }; for (const auto& url : GetBackendUrlsInOrder()) { - auto Response = Http::GET(GetBackendUrlsInOrder().at(0), 443, "/v/s"); + auto Response = Http::GET(url, 443, "/v/s"); bool Matches = std::regex_match(Response, VersionRegex); if (Matches) { auto MyVersion = ServerVersion(); @@ -152,6 +249,25 @@ std::string ThreadName(bool DebugModeOverride) { return ""; } +TEST_CASE("ThreadName") { + RegisterThread("MyThread"); + auto OrigDebug = Application::Settings.DebugModeEnabled; + + // ThreadName adds a space at the end, legacy but we need it still + SUBCASE("Debug mode enabled") { + Application::Settings.DebugModeEnabled = true; + CHECK(ThreadName(true) == "MyThread "); + CHECK(ThreadName(false) == "MyThread "); + } + SUBCASE("Debug mode disabled") { + Application::Settings.DebugModeEnabled = false; + CHECK(ThreadName(true) == "MyThread "); + CHECK(ThreadName(false) == ""); + } + // cleanup + Application::Settings.DebugModeEnabled = OrigDebug; +} + void RegisterThread(const std::string& str) { std::string ThreadId; #ifdef BEAMMP_WINDOWS @@ -162,13 +278,18 @@ void RegisterThread(const std::string& str) { ThreadId = std::to_string(gettid()); #endif if (Application::Settings.DebugModeEnabled) { - std::ofstream ThreadFile("Threads.log", std::ios::app); + std::ofstream ThreadFile(".Threads.log", std::ios::app); ThreadFile << ("Thread \"" + str + "\" is TID " + ThreadId) << std::endl; } auto Lock = std::unique_lock(ThreadNameMapMutex); threadNameMap[std::this_thread::get_id()] = str; } +TEST_CASE("RegisterThread") { + RegisterThread("MyThread"); + CHECK(threadNameMap.at(std::this_thread::get_id()) == "MyThread"); +} + Version::Version(uint8_t major, uint8_t minor, uint8_t patch) : major(major) , minor(minor) @@ -179,22 +300,30 @@ Version::Version(const std::array& v) } std::string Version::AsString() { - std::stringstream ss {}; - ss << int(major) << "." << int(minor) << "." << int(patch); - return ss.str(); + return fmt::format("{:d}.{:d}.{:d}", major, minor, patch); +} + +TEST_CASE("Version::AsString") { + CHECK(Version { 0, 0, 0 }.AsString() == "0.0.0"); + CHECK(Version { 1, 2, 3 }.AsString() == "1.2.3"); + CHECK(Version { 255, 255, 255 }.AsString() == "255.255.255"); } void LogChatMessage(const std::string& name, int id, const std::string& msg) { - std::stringstream ss; - ss << ThreadName(); - ss << "[CHAT] "; - if (id != -1) { - ss << "(" << id << ") <" << name << "> "; - } else { - ss << name << ""; + if (Application::Settings.LogChat) { + std::stringstream ss; + ss << ThreadName(); + ss << "[CHAT] "; + if (id != -1) { + ss << "(" << id << ") <" << name << "> "; + } else { + ss << name << ""; + } + ss << msg; +#ifdef DOCTEST_CONFIG_DISABLE + Application::Console().Write(ss.str()); +#endif } - ss << msg; - Application::Console().Write(ss.str()); } std::string GetPlatformAgnosticErrorString() { @@ -221,5 +350,7 @@ std::string GetPlatformAgnosticErrorString() { } #elif defined(BEAMMP_LINUX) || defined(BEAMMP_APPLE) return std::strerror(errno); +#else + return "(no human-readable errors on this platform)"; #endif } diff --git a/src/Compat.cpp b/src/Compat.cpp index a1765b7..46efeda 100644 --- a/src/Compat.cpp +++ b/src/Compat.cpp @@ -1,10 +1,13 @@ #include "Compat.h" +#include +#include + #ifndef WIN32 static struct termios old, current; -void initTermios(int echo) { +static void initTermios(int echo) { tcgetattr(0, &old); /* grab old terminal i/o settings */ current = old; /* make new settings same as old settings */ current.c_lflag &= ~ICANON; /* disable buffered i/o */ @@ -16,14 +19,40 @@ void initTermios(int echo) { tcsetattr(0, TCSANOW, ¤t); /* use these new terminal i/o settings now */ } -void resetTermios(void) { +static void resetTermios(void) { tcsetattr(0, TCSANOW, &old); } -char getch_(int echo) { +TEST_CASE("init and reset termios") { + if (isatty(STDIN_FILENO)) { + struct termios original; + tcgetattr(0, &original); + SUBCASE("no echo") { + initTermios(false); + } + SUBCASE("yes echo") { + initTermios(true); + } + resetTermios(); + struct termios current; + tcgetattr(0, ¤t); + CHECK_EQ(std::memcmp(¤t.c_cc, &original.c_cc, sizeof(current.c_cc)), 0); + CHECK_EQ(current.c_cflag, original.c_cflag); + CHECK_EQ(current.c_iflag, original.c_iflag); + CHECK_EQ(current.c_ispeed, original.c_ispeed); + CHECK_EQ(current.c_lflag, original.c_lflag); + CHECK_EQ(current.c_line, original.c_line); + CHECK_EQ(current.c_oflag, original.c_oflag); + CHECK_EQ(current.c_ospeed, original.c_ospeed); + } +} + +static char getch_(int echo) { char ch; initTermios(echo); - read(STDIN_FILENO, &ch, 1); + if (read(STDIN_FILENO, &ch, 1) < 0) { + // ignore, not much we can do + } resetTermios(); return ch; } diff --git a/src/Http.cpp b/src/Http.cpp index aeb66d5..9f87ea4 100644 --- a/src/Http.cpp +++ b/src/Http.cpp @@ -4,20 +4,15 @@ #include "Common.h" #include "CustomAssert.h" #include "LuaAPI.h" -#include "httplib.h" #include +#include #include -#include -#include -#include -#include #include -fs::path Http::Server::THttpServerInstance::KeyFilePath; -fs::path Http::Server::THttpServerInstance::CertFilePath; + // TODO: Add sentry error handling back -namespace json = rapidjson; +using json = nlohmann::json; std::string Http::GET(const std::string& host, int port, const std::string& target, unsigned int* status) { httplib::SSLClient client(host, port); @@ -146,145 +141,10 @@ std::string Http::Status::ToString(int Code) { } } -long Http::Server::Tx509KeypairGenerator::GenerateRandomId() { - std::random_device R; - std::default_random_engine E1(R()); - std::uniform_int_distribution UniformDist(0, ULONG_MAX); - return UniformDist(E1); -} - -// Http::Server::THttpServerInstance::THttpServerInstance() { } -EVP_PKEY* Http::Server::Tx509KeypairGenerator::GenerateKey() { - /** - * Allocate memory for the pkey - */ - EVP_PKEY* PKey = EVP_PKEY_new(); - if (PKey == nullptr) { - beammp_error("Could not allocate memory for X.509 private key (PKEY) generation."); - throw std::runtime_error { std::string { "X.509 PKEY allocation error" } }; - } - BIGNUM* E = BN_new(); - beammp_assert(E); // TODO: replace all these asserts with beammp_errors - unsigned char three = 3; - BIGNUM* EErr = BN_bin2bn(&three, sizeof(three), E); - beammp_assert(EErr); - RSA* Rsa = RSA_new(); - beammp_assert(Rsa); - int Ret = RSA_generate_key_ex(Rsa, Crypto::RSA_DEFAULT_KEYLENGTH, E, nullptr); - beammp_assert(Ret == 1); - BN_free(E); - if (!EVP_PKEY_assign_RSA(PKey, Rsa)) { - EVP_PKEY_free(PKey); - beammp_error(std::string("Could not generate " + std::to_string(Crypto::RSA_DEFAULT_KEYLENGTH) + "-bit RSA key.")); - throw std::runtime_error { std::string("X.509 RSA key generation error") }; - } - // todo: figure out if returning by reference instead of passing pointers is a security breach - return PKey; -} - -X509* Http::Server::Tx509KeypairGenerator::GenerateCertificate(EVP_PKEY& PKey) { - X509* X509 = X509_new(); - if (X509 == nullptr) { - X509_free(X509); - beammp_error("Could not allocate memory for X.509 certificate generation."); - throw std::runtime_error { std::string("X.509 certificate generation error") }; - } - - /**Set the metadata of the certificate*/ - ASN1_INTEGER_set(X509_get_serialNumber(X509), GenerateRandomId()); - - /**Set the cert validity to a year*/ - X509_gmtime_adj(X509_get_notBefore(X509), 0); - X509_gmtime_adj(X509_get_notAfter(X509), 31536000L); - - /**Set the public key of the cert*/ - X509_set_pubkey(X509, &PKey); - - X509_NAME* Name = X509_get_subject_name(X509); - - /**Set cert metadata*/ - X509_NAME_add_entry_by_txt(Name, "C", MBSTRING_ASC, (unsigned char*)"GB", -1, -1, 0); - X509_NAME_add_entry_by_txt(Name, "O", MBSTRING_ASC, (unsigned char*)"BeamMP Ltd.", -1, -1, 0); - X509_NAME_add_entry_by_txt(Name, "CN", MBSTRING_ASC, (unsigned char*)"localhost", -1, -1, 0); - - X509_set_issuer_name(X509, Name); - - // TODO: Hashing with sha256 might cause problems, check later - if (!X509_sign(X509, &PKey, EVP_sha1())) { - X509_free(X509); - beammp_error("Could not sign X.509 certificate."); - throw std::runtime_error { std::string("X.509 certificate signing error") }; - } - return X509; -} - -void Http::Server::Tx509KeypairGenerator::GenerateAndWriteToDisk(const fs::path& KeyFilePath, const fs::path& CertFilePath) { - // todo: generate directories for ssl keys - FILE* KeyFile = std::fopen(reinterpret_cast(KeyFilePath.c_str()), "wb"); - if (!KeyFile) { - beammp_error("Could not create file 'key.pem', check your permissions"); - throw std::runtime_error("Could not create file 'key.pem'"); - } - - EVP_PKEY* PKey = Http::Server::Tx509KeypairGenerator::GenerateKey(); - - bool WriteOpResult = PEM_write_PrivateKey(KeyFile, PKey, nullptr, nullptr, 0, nullptr, nullptr); - fclose(KeyFile); - - if (!WriteOpResult) { - beammp_error("Could not write to file 'key.pem', check your permissions"); - throw std::runtime_error("Could not write to file 'key.pem'"); - } - - FILE* CertFile = std::fopen(reinterpret_cast(CertFilePath.c_str()), "wb"); // x509 file - if (!CertFile) { - beammp_error("Could not create file 'cert.pem', check your permissions"); - throw std::runtime_error("Could not create file 'cert.pem'"); - } - - X509* x509 = Http::Server::Tx509KeypairGenerator::GenerateCertificate(*PKey); - WriteOpResult = PEM_write_X509(CertFile, x509); - fclose(CertFile); - - if (!WriteOpResult) { - beammp_error("Could not write to file 'cert.pem', check your permissions"); - throw std::runtime_error("Could not write to file 'cert.pem'"); - } - EVP_PKEY_free(PKey); - X509_free(x509); - return; -} - -bool Http::Server::Tx509KeypairGenerator::EnsureTLSConfigExists() { - if (fs::is_regular_file(Application::Settings.SSLKeyPath) - && fs::is_regular_file(Application::Settings.SSLCertPath)) { - return true; - } else { - return false; - } -} - -void Http::Server::SetupEnvironment() { - if (!Application::Settings.HTTPServerUseSSL) { - return; - } - auto parent = fs::path(Application::Settings.SSLKeyPath).parent_path(); - if (!fs::exists(parent)) - fs::create_directories(parent); - - Application::TSettings defaultSettings {}; - if (!Tx509KeypairGenerator::EnsureTLSConfigExists()) { - beammp_warn(std::string("No default TLS Key / Cert found. " - "IF YOU HAVE NOT MODIFIED THE SSLKeyPath OR SSLCertPath VALUES " - "THIS IS NORMAL ON FIRST STARTUP! BeamMP will generate it's own certs in the default directory " - "(Check for permissions or corrupted key-/certfile)")); - Tx509KeypairGenerator::GenerateAndWriteToDisk(defaultSettings.SSLKeyPath, defaultSettings.SSLCertPath); - Http::Server::THttpServerInstance::KeyFilePath = defaultSettings.SSLKeyPath; - Http::Server::THttpServerInstance::CertFilePath = defaultSettings.SSLCertPath; - } else { - Http::Server::THttpServerInstance::KeyFilePath = Application::Settings.SSLKeyPath; - Http::Server::THttpServerInstance::CertFilePath = Application::Settings.SSLCertPath; - } +TEST_CASE("Http::Status::ToString") { + CHECK(Http::Status::ToString(200) == "OK"); + CHECK(Http::Status::ToString(696969) == "696969"); + CHECK(Http::Status::ToString(-1) == "Invalid Response Code"); } Http::Server::THttpServerInstance::THttpServerInstance() { @@ -296,13 +156,7 @@ Http::Server::THttpServerInstance::THttpServerInstance() { void Http::Server::THttpServerInstance::operator()() try { beammp_info("HTTP(S) Server started on port " + std::to_string(Application::Settings.HTTPServerPort)); std::unique_ptr HttpLibServerInstance; - if (Application::Settings.HTTPServerUseSSL) { - HttpLibServerInstance = std::make_unique( - reinterpret_cast(Http::Server::THttpServerInstance::CertFilePath.c_str()), - reinterpret_cast(Http::Server::THttpServerInstance::KeyFilePath.c_str())); - } else { - HttpLibServerInstance = std::make_unique(); - } + HttpLibServerInstance = std::make_unique(); // todo: make this IP agnostic so people can set their own IP HttpLibServerInstance->Get("/", [](const httplib::Request&, httplib::Response& res) { res.set_content("

Hello World!

BeamMP Server can now serve HTTP requests!

", "text/html"); @@ -322,50 +176,18 @@ void Http::Server::THttpServerInstance::operator()() try { case Application::Status::Bad: SystemsBad++; break; + default: + beammp_assert_not_reachable(); } } - res.set_content(SystemsBad == 0 ? "0" : "1", "text/plain"); + res.set_content( + json { + { "ok", SystemsBad == 0 }, + } + .dump(), + "application/json"); res.status = 200; }); - /* - HttpLibServerInstance->Get("/status", [](const httplib::Request&, httplib::Response& res) { - try { - json::Document response; - response.SetObject(); - rapidjson::Document::AllocatorType& Allocator = response.GetAllocator(); - // add to response - auto& Server = LuaAPI::MP::Engine->Server(); - size_t CarCount = 0; - size_t GuestCount = 0; - json::Value Array(rapidjson::kArrayType); - LuaAPI::MP::Engine->Server().ForEachClient([&](std::weak_ptr Client) -> bool { - if (!Client.expired()) { - auto Locked = Client.lock(); - CarCount += Locked->GetCarCount(); - GuestCount += Locked->IsGuest() ? 1 : 0; - json::Value Player(json::kObjectType); - Player.AddMember("name", json::StringRef(Locked->GetName().c_str()), Allocator); - Player.AddMember("id", Locked->GetID(), Allocator); - Array.PushBack(Player, Allocator); - } - return true; - }); - response.AddMember("players", Array, Allocator); - response.AddMember("player_count", Server.ClientCount(), Allocator); - response.AddMember("guest_count", GuestCount, Allocator); - response.AddMember("car_count", CarCount, Allocator); - - // compile & send response - json::StringBuffer sb; - json::Writer writer(sb); - response.Accept(writer); - res.set_content(sb.GetString(), "application/json"); - } catch (const std::exception& e) { - beammp_error("Exception in /status endpoint: " + std::string(e.what())); - res.status = 500; - } - }); - */ // magic endpoint HttpLibServerInstance->Get({ 0x2f, 0x6b, 0x69, 0x74, 0x74, 0x79 }, [](const httplib::Request&, httplib::Response& res) { res.set_content(std::string(Magic), "text/plain"); diff --git a/src/LuaAPI.cpp b/src/LuaAPI.cpp index 4a76cdc..08dec7e 100644 --- a/src/LuaAPI.cpp +++ b/src/LuaAPI.cpp @@ -1,8 +1,11 @@ #include "LuaAPI.h" #include "Client.h" #include "Common.h" +#include "CustomAssert.h" #include "TLuaEngine.h" +#include + #define SOL_ALL_SAFETIES_ON 1 #include @@ -72,8 +75,10 @@ std::string LuaAPI::LuaToString(const sol::object Value, size_t Indent, bool Quo ss << "[[function: " << Value.as().pointer() << "]]"; return ss.str(); } + case sol::type::poly: + return ""; default: - return "((unprintable type))"; + return ""; } } @@ -100,67 +105,103 @@ void LuaAPI::Print(sol::variadic_args Args) { luaprint(ToPrint); } -bool LuaAPI::MP::TriggerClientEvent(int PlayerID, const std::string& EventName, const std::string& Data) { - std::string Packet = "E:" + EventName + ":" + Data; - if (PlayerID == -1) - Engine->Network().SendToAll(nullptr, Packet, true, true); - else { - auto MaybeClient = GetClient(Engine->Server(), PlayerID); - if (!MaybeClient || MaybeClient.value().expired()) { - beammp_lua_error("TriggerClientEvent invalid Player ID"); - return false; - } - auto c = MaybeClient.value().lock(); - if (!Engine->Network().Respond(*c, Packet, true)) { - beammp_lua_error("Respond failed, dropping client " + std::to_string(PlayerID)); - Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets"); - return false; - } - } - return true; +TEST_CASE("LuaAPI::MP::GetServerVersion") { + const auto [ma, mi, pa] = LuaAPI::MP::GetServerVersion(); + const auto real = Application::ServerVersion(); + CHECK(ma == real.major); + CHECK(mi == real.minor); + CHECK(pa == real.patch); } -void LuaAPI::MP::DropPlayer(int ID, std::optional MaybeReason) { +static inline std::pair InternalTriggerClientEvent(int PlayerID, const std::string& EventName, const std::string& Data) { + std::string Packet = "E:" + EventName + ":" + Data; + if (PlayerID == -1) { + LuaAPI::MP::Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true); + return { true, "" }; + } else { + auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID); + if (!MaybeClient || MaybeClient.value().expired()) { + beammp_lua_errorf("TriggerClientEvent invalid Player ID '{}'", PlayerID); + return { false, "Invalid Player ID" }; + } + auto c = MaybeClient.value().lock(); + if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) { + beammp_lua_errorf("Respond failed, dropping client {}", PlayerID); + LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets"); + return { false, "Respond failed, dropping client" }; + } + return { true, "" }; + } +} + +std::pair LuaAPI::MP::TriggerClientEvent(int PlayerID, const std::string& EventName, const sol::object& DataObj) { + std::string Data = DataObj.as(); + return InternalTriggerClientEvent(PlayerID, EventName, Data); +} + +std::pair LuaAPI::MP::DropPlayer(int ID, std::optional MaybeReason) { auto MaybeClient = GetClient(Engine->Server(), ID); if (!MaybeClient || MaybeClient.value().expired()) { - beammp_lua_error("Tried to drop client with id " + std::to_string(ID) + ", who doesn't exist"); - return; + beammp_lua_errorf("Tried to drop client with id {}, who doesn't exist", ID); + return { false, "Player does not exist" }; } auto c = MaybeClient.value().lock(); LuaAPI::MP::Engine->Network().ClientKick(*c, MaybeReason.value_or("No reason")); + return { true, "" }; } -void LuaAPI::MP::SendChatMessage(int ID, const std::string& Message) { +std::pair LuaAPI::MP::SendChatMessage(int ID, const std::string& Message) { + std::pair Result; std::string Packet = "C:Server: " + Message; if (ID == -1) { LogChatMessage(" (to everyone) ", -1, Message); - Engine->Network().SendToAll(nullptr, Packet, true, true); + 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()) - return; + if (!c->IsSynced()) { + Result.first = false; + Result.second = "Player still syncing data"; + return Result; + } LogChatMessage(" (to \"" + c->GetName() + "\")", -1, Message); - Engine->Network().Respond(*c, Packet, true); + 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 = "Invalid Player ID"; } + return Result; } + return Result; } -void LuaAPI::MP::RemoveVehicle(int PID, int VID) { +std::pair LuaAPI::MP::RemoveVehicle(int PID, int VID) { + std::pair Result; auto MaybeClient = GetClient(Engine->Server(), PID); if (!MaybeClient || MaybeClient.value().expired()) { beammp_lua_error("RemoveVehicle invalid Player ID"); - return; + 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); - Engine->Network().SendToAll(nullptr, Destroy, true, true); + 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; } void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) { @@ -169,50 +210,57 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) { if (NewValue.is()) { Application::Settings.DebugModeEnabled = NewValue.as(); beammp_info(std::string("Set `Debug` to ") + (Application::Settings.DebugModeEnabled ? "true" : "false")); - } else + } else { beammp_lua_error("set invalid argument [2] expected boolean"); + } break; case 1: // private if (NewValue.is()) { Application::Settings.Private = NewValue.as(); beammp_info(std::string("Set `Private` to ") + (Application::Settings.Private ? "true" : "false")); - } else + } else { beammp_lua_error("set invalid argument [2] expected boolean"); + } break; case 2: // max cars if (NewValue.is()) { Application::Settings.MaxCars = NewValue.as(); beammp_info(std::string("Set `MaxCars` to ") + std::to_string(Application::Settings.MaxCars)); - } else + } else { beammp_lua_error("set invalid argument [2] expected integer"); + } break; case 3: // max players if (NewValue.is()) { Application::Settings.MaxPlayers = NewValue.as(); beammp_info(std::string("Set `MaxPlayers` to ") + std::to_string(Application::Settings.MaxPlayers)); - } else + } else { beammp_lua_error("set invalid argument [2] expected integer"); + } break; case 4: // Map if (NewValue.is()) { Application::Settings.MapName = NewValue.as(); beammp_info(std::string("Set `Map` to ") + Application::Settings.MapName); - } else + } else { beammp_lua_error("set invalid argument [2] expected string"); + } break; case 5: // Name if (NewValue.is()) { Application::Settings.ServerName = NewValue.as(); beammp_info(std::string("Set `Name` to ") + Application::Settings.ServerName); - } else + } else { beammp_lua_error("set invalid argument [2] expected string"); + } break; case 6: // Desc if (NewValue.is()) { Application::Settings.ServerDesc = NewValue.as(); beammp_info(std::string("Set `Description` to ") + Application::Settings.ServerDesc); - } else + } else { beammp_lua_error("set invalid argument [2] expected string"); + } break; default: beammp_warn("Invalid config ID \"" + std::to_string(ConfigID) + "\". Use `MP.Settings.*` enum for this."); @@ -248,7 +296,9 @@ void LuaAPI::MP::PrintRaw(sol::variadic_args Args) { ToPrint += LuaToString(static_cast(Arg)); ToPrint += "\t"; } +#ifdef DOCTEST_CONFIG_DISABLE Application::Console().WriteRaw(ToPrint); +#endif } int LuaAPI::PanicHandler(lua_State* State) { @@ -271,7 +321,7 @@ static std::pair FSWrapper(FnT Fn, ArgsT&&... Args) { std::pair LuaAPI::FS::CreateDirectory(const std::string& Path) { std::error_code errc; std::pair Result; - fs::create_directories(fs::relative(Path), errc); + fs::create_directories(Path, errc); Result.first = errc == std::error_code {}; if (!Result.first) { Result.second = errc.message(); @@ -279,6 +329,33 @@ std::pair LuaAPI::FS::CreateDirectory(const std::string& Path return Result; } +TEST_CASE("LuaAPI::FS::CreateDirectory") { + std::string TestDir = "beammp_test_dir"; + fs::remove_all(TestDir); + SUBCASE("Single level dir") { + const auto [Ok, Err] = LuaAPI::FS::CreateDirectory(TestDir); + CHECK(Ok); + CHECK(Err == ""); + CHECK(fs::exists(TestDir)); + } + SUBCASE("Multi level dir") { + const auto [Ok, Err] = LuaAPI::FS::CreateDirectory(TestDir + "/a/b/c"); + CHECK(Ok); + CHECK(Err == ""); + CHECK(fs::exists(TestDir + "/a/b/c")); + } + SUBCASE("Already exists") { + const auto [Ok, Err] = LuaAPI::FS::CreateDirectory(TestDir); + CHECK(Ok); + CHECK(Err == ""); + CHECK(fs::exists(TestDir)); + const auto [Ok2, Err2] = LuaAPI::FS::CreateDirectory(TestDir); + CHECK(Ok2); + CHECK(Err2 == ""); + } + fs::remove_all(TestDir); +} + std::pair LuaAPI::FS::Remove(const std::string& Path) { std::error_code errc; std::pair Result; @@ -290,10 +367,30 @@ std::pair LuaAPI::FS::Remove(const std::string& Path) { return Result; } +TEST_CASE("LuaAPI::FS::Remove") { + const std::string TestFileOrDir = "beammp_test_thing"; + SUBCASE("Remove existing directory") { + fs::create_directory(TestFileOrDir); + const auto [Ok, Err] = LuaAPI::FS::Remove(TestFileOrDir); + CHECK(Ok); + CHECK_EQ(Err, ""); + CHECK(!fs::exists(TestFileOrDir)); + } + SUBCASE("Remove non-existing directory") { + fs::remove_all(TestFileOrDir); + const auto [Ok, Err] = LuaAPI::FS::Remove(TestFileOrDir); + CHECK(Ok); + CHECK_EQ(Err, ""); + CHECK(!fs::exists(TestFileOrDir)); + } + // TODO: add tests for files + // TODO: add tests for files and folders without access permissions (failure) +} + std::pair LuaAPI::FS::Rename(const std::string& Path, const std::string& NewPath) { std::error_code errc; std::pair Result; - fs::rename(fs::relative(Path), fs::relative(NewPath), errc); + fs::rename(Path, NewPath, errc); Result.first = errc == std::error_code {}; if (!Result.first) { Result.second = errc.message(); @@ -301,10 +398,25 @@ std::pair LuaAPI::FS::Rename(const std::string& Path, const s return Result; } +TEST_CASE("LuaAPI::FS::Rename") { + const auto TestDir = "beammp_test_dir"; + const auto OtherTestDir = "beammp_test_dir_2"; + fs::remove_all(OtherTestDir); + fs::create_directory(TestDir); + const auto [Ok, Err] = LuaAPI::FS::Rename(TestDir, OtherTestDir); + CHECK(Ok); + CHECK_EQ(Err, ""); + CHECK(!fs::exists(TestDir)); + CHECK(fs::exists(OtherTestDir)); + + fs::remove_all(OtherTestDir); + fs::remove_all(TestDir); +} + std::pair LuaAPI::FS::Copy(const std::string& Path, const std::string& NewPath) { std::error_code errc; std::pair Result; - fs::copy(fs::relative(Path), fs::relative(NewPath), fs::copy_options::recursive, errc); + fs::copy(Path, NewPath, fs::copy_options::recursive, errc); Result.first = errc == std::error_code {}; if (!Result.first) { Result.second = errc.message(); @@ -312,30 +424,86 @@ std::pair LuaAPI::FS::Copy(const std::string& Path, const std return Result; } +TEST_CASE("LuaAPI::FS::Copy") { + const auto TestDir = "beammp_test_dir"; + const auto OtherTestDir = "beammp_test_dir_2"; + fs::remove_all(OtherTestDir); + fs::create_directory(TestDir); + const auto [Ok, Err] = LuaAPI::FS::Copy(TestDir, OtherTestDir); + CHECK(Ok); + CHECK_EQ(Err, ""); + CHECK(fs::exists(TestDir)); + CHECK(fs::exists(OtherTestDir)); + + fs::remove_all(OtherTestDir); + fs::remove_all(TestDir); +} + bool LuaAPI::FS::Exists(const std::string& Path) { - return fs::exists(fs::relative(Path)); + return fs::exists(Path); +} + +TEST_CASE("LuaAPI::FS::Exists") { + const auto TestDir = "beammp_test_dir"; + const auto OtherTestDir = "beammp_test_dir_2"; + fs::remove_all(OtherTestDir); + fs::create_directory(TestDir); + + CHECK(LuaAPI::FS::Exists(TestDir)); + CHECK(!LuaAPI::FS::Exists(OtherTestDir)); + + fs::remove_all(OtherTestDir); + fs::remove_all(TestDir); } std::string LuaAPI::FS::GetFilename(const std::string& Path) { return fs::path(Path).filename().string(); } +TEST_CASE("LuaAPI::FS::GetFilename") { + CHECK(LuaAPI::FS::GetFilename("test.txt") == "test.txt"); + CHECK(LuaAPI::FS::GetFilename("/test.txt") == "test.txt"); + CHECK(LuaAPI::FS::GetFilename("place/test.txt") == "test.txt"); + CHECK(LuaAPI::FS::GetFilename("/some/../place/test.txt") == "test.txt"); +} + std::string LuaAPI::FS::GetExtension(const std::string& Path) { return fs::path(Path).extension().string(); } +TEST_CASE("LuaAPI::FS::GetExtension") { + CHECK(LuaAPI::FS::GetExtension("test.txt") == ".txt"); + CHECK(LuaAPI::FS::GetExtension("/test.txt") == ".txt"); + CHECK(LuaAPI::FS::GetExtension("place/test.txt") == ".txt"); + CHECK(LuaAPI::FS::GetExtension("/some/../place/test.txt") == ".txt"); + CHECK(LuaAPI::FS::GetExtension("/some/../place/test") == ""); + CHECK(LuaAPI::FS::GetExtension("/some/../place/test.a.b.c") == ".c"); + CHECK(LuaAPI::FS::GetExtension("/some/../place/test.") == "."); + CHECK(LuaAPI::FS::GetExtension("/some/../place/test.a.b.") == "."); +} + std::string LuaAPI::FS::GetParentFolder(const std::string& Path) { return fs::path(Path).parent_path().string(); } +TEST_CASE("LuaAPI::FS::GetParentFolder") { + CHECK(LuaAPI::FS::GetParentFolder("test.txt") == ""); + CHECK(LuaAPI::FS::GetParentFolder("/test.txt") == "/"); + CHECK(LuaAPI::FS::GetParentFolder("place/test.txt") == "place"); + CHECK(LuaAPI::FS::GetParentFolder("/some/../place/test.txt") == "/some/../place"); +} + +// TODO: add tests bool LuaAPI::FS::IsDirectory(const std::string& Path) { return fs::is_directory(Path); } +// TODO: add tests bool LuaAPI::FS::IsFile(const std::string& Path) { return fs::is_regular_file(Path); } +// TODO: add tests std::string LuaAPI::FS::ConcatPaths(sol::variadic_args Args) { fs::path Path; for (size_t i = 0; i < Args.size(); ++i) { @@ -352,3 +520,161 @@ std::string LuaAPI::FS::ConcatPaths(sol::variadic_args Args) { auto Result = Path.lexically_normal().string(); return Result; } + +static void JsonEncodeRecursive(nlohmann::json& json, const sol::object& left, const sol::object& right, bool is_array, size_t depth = 0) { + if (depth > 100) { + beammp_lua_error("json serialize will not go deeper than 100 nested tables, internal references assumed, aborted this path"); + return; + } + std::string key {}; + switch (left.get_type()) { + case sol::type::lua_nil: + case sol::type::none: + case sol::type::poly: + case sol::type::boolean: + case sol::type::lightuserdata: + case sol::type::userdata: + case sol::type::thread: + case sol::type::function: + case sol::type::table: + beammp_lua_error("JsonEncode: left side of table field is unexpected type"); + return; + case sol::type::string: + key = left.as(); + break; + case sol::type::number: + key = std::to_string(left.as()); + break; + default: + beammp_assert_not_reachable(); + } + nlohmann::json value; + switch (right.get_type()) { + case sol::type::lua_nil: + case sol::type::none: + return; + case sol::type::poly: + beammp_lua_warn("unsure what to do with poly type in JsonEncode, ignoring"); + return; + case sol::type::boolean: + value = right.as(); + break; + case sol::type::lightuserdata: + beammp_lua_warn("unsure what to do with lightuserdata in JsonEncode, ignoring"); + return; + case sol::type::userdata: + beammp_lua_warn("unsure what to do with userdata in JsonEncode, ignoring"); + return; + case sol::type::thread: + beammp_lua_warn("unsure what to do with thread in JsonEncode, ignoring"); + return; + case sol::type::string: + value = right.as(); + break; + case sol::type::number: + value = right.as(); + break; + case sol::type::function: + beammp_lua_warn("unsure what to do with function in JsonEncode, ignoring"); + return; + case sol::type::table: { + bool local_is_array = true; + for (const auto& pair : right.as()) { + if (pair.first.get_type() != sol::type::number) { + local_is_array = false; + } + } + for (const auto& pair : right.as()) { + JsonEncodeRecursive(value, pair.first, pair.second, local_is_array, depth + 1); + } + break; + } + default: + beammp_assert_not_reachable(); + } + if (is_array) { + json.push_back(value); + } else { + json[key] = value; + } +} + +std::string LuaAPI::MP::JsonEncode(const sol::table& object) { + nlohmann::json json; + // table + bool is_array = true; + for (const auto& pair : object.as()) { + if (pair.first.get_type() != sol::type::number) { + is_array = false; + } + } + for (const auto& entry : object) { + JsonEncodeRecursive(json, entry.first, entry.second, is_array); + } + return json.dump(); +} + +std::string LuaAPI::MP::JsonDiff(const std::string& a, const std::string& b) { + if (!nlohmann::json::accept(a)) { + beammp_lua_error("JsonDiff first argument is not valid json: `" + a + "`"); + return ""; + } + if (!nlohmann::json::accept(b)) { + beammp_lua_error("JsonDiff second argument is not valid json: `" + b + "`"); + return ""; + } + auto a_json = nlohmann::json::parse(a); + auto b_json = nlohmann::json::parse(b); + return nlohmann::json::diff(a_json, b_json).dump(); +} + +std::string LuaAPI::MP::JsonDiffApply(const std::string& data, const std::string& patch) { + if (!nlohmann::json::accept(data)) { + beammp_lua_error("JsonDiffApply first argument is not valid json: `" + data + "`"); + return ""; + } + if (!nlohmann::json::accept(patch)) { + beammp_lua_error("JsonDiffApply second argument is not valid json: `" + patch + "`"); + return ""; + } + auto a_json = nlohmann::json::parse(data); + auto b_json = nlohmann::json::parse(patch); + a_json.patch(b_json); + return a_json.dump(); +} + +std::string LuaAPI::MP::JsonPrettify(const std::string& json) { + if (!nlohmann::json::accept(json)) { + beammp_lua_error("JsonPrettify argument is not valid json: `" + json + "`"); + return ""; + } + return nlohmann::json::parse(json).dump(4); +} + +std::string LuaAPI::MP::JsonMinify(const std::string& json) { + if (!nlohmann::json::accept(json)) { + beammp_lua_error("JsonMinify argument is not valid json: `" + json + "`"); + return ""; + } + return nlohmann::json::parse(json).dump(-1); +} + +std::string LuaAPI::MP::JsonFlatten(const std::string& json) { + if (!nlohmann::json::accept(json)) { + beammp_lua_error("JsonFlatten argument is not valid json: `" + json + "`"); + return ""; + } + return nlohmann::json::parse(json).flatten().dump(-1); +} + +std::string LuaAPI::MP::JsonUnflatten(const std::string& json) { + if (!nlohmann::json::accept(json)) { + beammp_lua_error("JsonUnflatten argument is not valid json: `" + json + "`"); + return ""; + } + return nlohmann::json::parse(json).unflatten().dump(-1); +} + +std::pair LuaAPI::MP::TriggerClientEventJson(int PlayerID, const std::string& EventName, const sol::table& Data) { + return InternalTriggerClientEvent(PlayerID, EventName, JsonEncode(Data)); +} diff --git a/src/TConfig.cpp b/src/TConfig.cpp index 531fd59..8c93aa4 100644 --- a/src/TConfig.cpp +++ b/src/TConfig.cpp @@ -17,6 +17,7 @@ static constexpr std::string_view StrName = "Name"; static constexpr std::string_view StrDescription = "Description"; static constexpr std::string_view StrResourceFolder = "ResourceFolder"; static constexpr std::string_view StrAuthKey = "AuthKey"; +static constexpr std::string_view StrLogChat = "LogChat"; // Misc static constexpr std::string_view StrSendErrors = "SendErrors"; @@ -31,12 +32,40 @@ static constexpr std::string_view StrSSLCertPath = "SSLCertPath"; static constexpr std::string_view StrHTTPServerPort = "HTTPServerPort"; static constexpr std::string_view StrHTTPServerIP = "HTTPServerIP"; +TEST_CASE("TConfig::TConfig") { + const std::string CfgFile = "beammp_server_testconfig.toml"; + fs::remove(CfgFile); + + TConfig Cfg(CfgFile); + + CHECK(fs::file_size(CfgFile) != 0); + + std::string buf; + { + buf.resize(fs::file_size(CfgFile)); + auto fp = std::fopen(CfgFile.c_str(), "r"); + auto res = std::fread(buf.data(), 1, buf.size(), fp); + if (res != buf.size()) { + // IGNORE? + } + std::fclose(fp); + } + INFO("file contents are:", buf); + + const auto table = toml::parse(CfgFile); + CHECK(table.at("General").is_table()); + CHECK(table.at("Misc").is_table()); + CHECK(table.at("HTTP").is_table()); + + fs::remove(CfgFile); +} + TConfig::TConfig(const std::string& ConfigFileName) : mConfigFileName(ConfigFileName) { Application::SetSubsystemStatus("Config", Application::Status::Starting); if (!fs::exists(mConfigFileName) || !fs::is_regular_file(mConfigFileName)) { beammp_info("No config file found! Generating one..."); - CreateConfigFile(mConfigFileName); + CreateConfigFile(); } if (!mFailed) { if (fs::exists("Server.cfg")) { @@ -66,6 +95,8 @@ void TConfig::FlushToFile() { auto data = toml::value {}; data["General"][StrAuthKey.data()] = Application::Settings.Key; SetComment(data["General"][StrAuthKey.data()].comments(), " AuthKey has to be filled out in order to run the server"); + data["General"][StrLogChat.data()] = Application::Settings.LogChat; + SetComment(data["General"][StrLogChat.data()].comments(), " Whether to log chat messages in the console / log"); data["General"][StrDebug.data()] = Application::Settings.DebugModeEnabled; data["General"][StrPrivate.data()] = Application::Settings.Private; data["General"][StrPort.data()] = Application::Settings.Port; @@ -110,7 +141,7 @@ void TConfig::FlushToFile() { std::fclose(File); } -void TConfig::CreateConfigFile(std::string_view name) { +void TConfig::CreateConfigFile() { // build from old config Server.cfg try { @@ -139,7 +170,7 @@ void TConfig::TryReadValue(toml::value& Table, const std::string& Category, cons void TConfig::TryReadValue(toml::value& Table, const std::string& Category, const std::string_view& Key, int& OutValue) { if (Table[Category.c_str()][Key.data()].is_integer()) { - OutValue = Table[Category.c_str()][Key.data()].as_integer(); + OutValue = int(Table[Category.c_str()][Key.data()].as_integer()); } } @@ -157,6 +188,7 @@ void TConfig::ParseFromFile(std::string_view name) { TryReadValue(data, "General", StrDescription, Application::Settings.ServerDesc); TryReadValue(data, "General", StrResourceFolder, Application::Settings.Resource); TryReadValue(data, "General", StrAuthKey, Application::Settings.Key); + TryReadValue(data, "General", StrLogChat, Application::Settings.LogChat); // Misc TryReadValue(data, "Misc", StrSendErrors, Application::Settings.SendErrors); TryReadValue(data, "Misc", StrHideUpdateMessages, Application::Settings.HideUpdateMessages); @@ -200,6 +232,7 @@ void TConfig::PrintDebug() { beammp_debug(std::string(StrMap) + ": \"" + Application::Settings.MapName + "\""); beammp_debug(std::string(StrName) + ": \"" + Application::Settings.ServerName + "\""); beammp_debug(std::string(StrDescription) + ": \"" + Application::Settings.ServerDesc + "\""); + beammp_debug(std::string(StrLogChat) + ": \"" + (Application::Settings.LogChat ? "true" : "false") + "\""); beammp_debug(std::string(StrResourceFolder) + ": \"" + Application::Settings.Resource + "\""); beammp_debug(std::string(StrSSLKeyPath) + ": \"" + Application::Settings.SSLKeyPath + "\""); beammp_debug(std::string(StrSSLCertPath) + ": \"" + Application::Settings.SSLCertPath + "\""); diff --git a/src/TConsole.cpp b/src/TConsole.cpp index f6eb1fa..5420d39 100644 --- a/src/TConsole.cpp +++ b/src/TConsole.cpp @@ -14,6 +14,17 @@ static inline bool StringStartsWith(const std::string& What, const std::string& return What.size() >= StartsWith.size() && What.substr(0, StartsWith.size()) == StartsWith; } +TEST_CASE("StringStartsWith") { + CHECK(StringStartsWith("Hello, World", "Hello")); + CHECK(StringStartsWith("Hello, World", "H")); + CHECK(StringStartsWith("Hello, World", "")); + CHECK(!StringStartsWith("Hello, World", "ello")); + CHECK(!StringStartsWith("Hello, World", "World")); + CHECK(StringStartsWith("", "")); + CHECK(!StringStartsWith("", "hello")); +} + +// Trims leading and trailing spaces, newlines, tabs, etc. static inline std::string TrimString(std::string S) { S.erase(S.begin(), std::find_if(S.begin(), S.end(), [](unsigned char ch) { return !std::isspace(ch); @@ -25,7 +36,33 @@ static inline std::string TrimString(std::string S) { return S; } -std::string GetDate() { +TEST_CASE("TrimString") { + CHECK(TrimString("hel lo") == "hel lo"); + CHECK(TrimString(" hel lo") == "hel lo"); + CHECK(TrimString(" hel lo ") == "hel lo"); + CHECK(TrimString("hel lo ") == "hel lo"); + CHECK(TrimString(" hel lo") == "hel lo"); + CHECK(TrimString("hel lo ") == "hel lo"); + CHECK(TrimString(" hel lo ") == "hel lo"); + CHECK(TrimString("\t\thel\nlo\n\n") == "hel\nlo"); + CHECK(TrimString("\n\thel\tlo\n\t") == "hel\tlo"); + CHECK(TrimString(" ") == ""); + CHECK(TrimString(" \t\n\r ") == ""); + CHECK(TrimString("") == ""); +} + +// TODO: add unit tests to SplitString +static inline void SplitString(std::string const& str, const char delim, std::vector& out) { + size_t start; + size_t end = 0; + + while ((start = str.find_first_not_of(delim, end)) != std::string::npos) { + end = str.find(delim, start); + out.push_back(str.substr(start, end - start)); + } +} + +static std::string GetDate() { std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); time_t tt = std::chrono::system_clock::to_time_t(now); auto local_tm = std::localtime(&tt); @@ -118,10 +155,10 @@ void TConsole::ChangeToLuaConsole(const std::string& LuaStateId) { mStateId = LuaStateId; mIsLuaConsole = true; if (mStateId != mDefaultStateId) { - Application::Console().WriteRaw("Entered Lua console for state '" + mStateId + "'. To exit, type `exit()`"); + Application::Console().WriteRaw("Attached to Lua state '" + mStateId + "'. For help, type `:help`. To detach, type `:exit`"); mCommandline.set_prompt("lua @" + LuaStateId + "> "); } else { - Application::Console().WriteRaw("Entered Lua console. To exit, type `exit()`"); + Application::Console().WriteRaw("Attached to Lua. For help, type `:help`. To detach, type `:exit`"); mCommandline.set_prompt("lua> "); } mCachedRegularHistory = mCommandline.history(); @@ -133,9 +170,9 @@ void TConsole::ChangeToRegularConsole() { if (mIsLuaConsole) { mIsLuaConsole = false; if (mStateId != mDefaultStateId) { - Application::Console().WriteRaw("Left Lua console for state '" + mStateId + "'."); + Application::Console().WriteRaw("Detached from Lua state '" + mStateId + "'."); } else { - Application::Console().WriteRaw("Left Lua console."); + Application::Console().WriteRaw("Detached from Lua."); } mCachedLuaHistory = mCommandline.history(); mCommandline.set_history(mCachedRegularHistory); @@ -144,21 +181,54 @@ void TConsole::ChangeToRegularConsole() { } } -void TConsole::Command_Lua(const std::string& cmd) { - if (cmd.size() > 3) { - auto NewStateId = cmd.substr(4); +bool TConsole::EnsureArgsCount(const std::vector& args, size_t n) { + if (n == 0 && args.size() != 0) { + Application::Console().WriteRaw("This command expects no arguments."); + return false; + } else if (args.size() != n) { + Application::Console().WriteRaw("Expected " + std::to_string(n) + " argument(s), instead got " + std::to_string(args.size())); + return false; + } else { + return true; + } +} + +bool TConsole::EnsureArgsCount(const std::vector& args, size_t min, size_t max) { + if (min == max) { + return EnsureArgsCount(args, min); + } else { + if (args.size() > max) { + Application::Console().WriteRaw("Too many arguments. At most " + std::to_string(max) + " arguments expected, got " + std::to_string(args.size()) + " instead."); + return false; + } else if (args.size() < min) { + Application::Console().WriteRaw("Too few arguments. At least " + std::to_string(max) + " arguments expected, got " + std::to_string(args.size()) + " instead."); + return false; + } + } + return true; +} + +void TConsole::Command_Lua(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0, 1)) { + return; + } + if (args.size() == 1) { + auto NewStateId = args.at(0); beammp_assert(!NewStateId.empty()); if (mLuaEngine->HasState(NewStateId)) { ChangeToLuaConsole(NewStateId); } else { Application::Console().WriteRaw("Lua state '" + NewStateId + "' is not a known state. Didn't switch to Lua."); } - } else { + } else if (args.size() == 0) { ChangeToLuaConsole(mDefaultStateId); } } -void TConsole::Command_Help(const std::string&) { +void TConsole::Command_Help(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0)) { + return; + } static constexpr const char* sHelpString = R"( Commands: help displays this help @@ -167,53 +237,138 @@ void TConsole::Command_Help(const std::string&) { list lists all players and info about them say sends the message to all players in chat lua [state id] switches to lua, optionally into a specific state id's lua - status how the server is doing and what it's up to)"; + 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)"; Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString)); } -void TConsole::Command_Kick(const std::string& cmd) { - if (cmd.size() > 4) { - auto Name = cmd.substr(5); - std::string Reason = "Kicked by server console"; - auto SpacePos = Name.find(' '); - if (SpacePos != Name.npos) { - Reason = Name.substr(SpacePos + 1); - Name = cmd.substr(5, cmd.size() - Reason.size() - 5 - 1); +std::string TConsole::ConcatArgs(const std::vector& args, char space) { + std::string Result; + for (const auto& arg : args) { + Result += arg + space; + } + Result = Result.substr(0, Result.size() - 1); // strip trailing space + return Result; +} + +void TConsole::Command_Clear(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0, size_t(-1))) { + return; + } + mCommandline.write("\x1b[;H\x1b[2J"); +} + +void TConsole::Command_Kick(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 1, size_t(-1))) { + return; + } + auto Name = args.at(0); + std::string Reason = "Kicked by server console"; + if (args.size() > 1) { + Reason = ConcatArgs({ args.begin() + 1, args.end() }); + } + beammp_trace("attempt to kick '" + Name + "' for '" + Reason + "'"); + bool Kicked = false; + // TODO: this sucks, tolower is locale-dependent. + auto NameCompare = [](std::string Name1, std::string Name2) -> bool { + std::for_each(Name1.begin(), Name1.end(), [](char& c) { c = char(std::tolower(char(c))); }); + std::for_each(Name2.begin(), Name2.end(), [](char& c) { c = char(std::tolower(char(c))); }); + return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1); + }; + mLuaEngine->Server().ForEachClient([&](std::weak_ptr Client) -> bool { + if (!Client.expired()) { + auto locked = Client.lock(); + if (NameCompare(locked->GetName(), Name)) { + mLuaEngine->Network().ClientKick(*locked, Reason); + Kicked = true; + return false; + } } - beammp_trace("attempt to kick '" + Name + "' for '" + Reason + "'"); - bool Kicked = false; - auto NameCompare = [](std::string Name1, std::string Name2) -> bool { - std::for_each(Name1.begin(), Name1.end(), [](char& c) { c = tolower(c); }); - std::for_each(Name2.begin(), Name2.end(), [](char& c) { c = tolower(c); }); - return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1); - }; - mLuaEngine->Server().ForEachClient([&](std::weak_ptr Client) -> bool { - if (!Client.expired()) { - auto locked = Client.lock(); - if (NameCompare(locked->GetName(), Name)) { - mLuaEngine->Network().ClientKick(*locked, Reason); - Kicked = true; - return false; + return true; + }); + if (!Kicked) { + Application::Console().WriteRaw("Error: No player with name matching '" + Name + "' was found."); + } else { + Application::Console().WriteRaw("Kicked player '" + Name + "' for reason: '" + Reason + "'."); + } +} + +std::tuple> TConsole::ParseCommand(const std::string& CommandWithArgs) { + // Algorithm designed and implemented by Lion Kortlepel (c) 2022 + // It correctly splits arguments, including respecting single and double quotes, as well as backticks + auto End_i = CommandWithArgs.find_first_of(' '); + std::string Command = CommandWithArgs.substr(0, End_i); + std::string ArgsStr {}; + if (End_i != std::string::npos) { + ArgsStr = CommandWithArgs.substr(End_i); + } + std::vector Args; + char* PrevPtr = ArgsStr.data(); + char* Ptr = ArgsStr.data(); + const char* End = ArgsStr.data() + ArgsStr.size(); + while (Ptr != End) { + std::string Arg = ""; + // advance while space + while (Ptr != End && std::isspace(*Ptr)) + ++Ptr; + PrevPtr = Ptr; + // advance while NOT space, also handle quotes + while (Ptr != End && !std::isspace(*Ptr)) { + // TODO: backslash escaping quotes + for (char Quote : { '"', '\'', '`' }) { + if (*Ptr == Quote) { + // seek if there's a closing quote + // if there is, go there and continue, otherwise ignore + char* Seeker = Ptr + 1; + while (Seeker != End && *Seeker != Quote) + ++Seeker; + if (Seeker != End) { + // found closing quote + Ptr = Seeker; + } + break; // exit for loop } } - return true; - }); - if (!Kicked) { - Application::Console().WriteRaw("Error: No player with name matching '" + Name + "' was found."); - } else { - Application::Console().WriteRaw("Kicked player '" + Name + "' for reason: '" + Reason + "'."); + ++Ptr; + } + // this is required, otherwise we get negative int to unsigned cast in the next operations + beammp_assert(PrevPtr <= Ptr); + Arg = std::string(PrevPtr, std::string::size_type(Ptr - PrevPtr)); + // remove quotes if enclosed in quotes + for (char Quote : { '"', '\'', '`' }) { + if (!Arg.empty() && Arg.at(0) == Quote && Arg.at(Arg.size() - 1) == Quote) { + Arg = Arg.substr(1, Arg.size() - 2); + break; + } + } + if (!Arg.empty()) { + Args.push_back(Arg); + } + } + return { Command, Args }; +} + +void TConsole::Command_Settings(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0)) { + return; + } +} + +void TConsole::Command_Say(const std::string& FullCmd) { + if (FullCmd.size() > 3) { + auto Message = FullCmd.substr(4); + LuaAPI::MP::SendChatMessage(-1, Message); + if (!Application::Settings.LogChat) { + Application::Console().WriteRaw("Chat message sent!"); } } } -void TConsole::Command_Say(const std::string& cmd) { - if (cmd.size() > 3) { - auto Message = cmd.substr(4); - LuaAPI::MP::SendChatMessage(-1, Message); +void TConsole::Command_List(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0)) { + return; } -} - -void TConsole::Command_List(const std::string&) { if (mLuaEngine->Server().ClientCount() == 0) { Application::Console().WriteRaw("No players online."); } else { @@ -233,7 +388,10 @@ void TConsole::Command_List(const std::string&) { } } -void TConsole::Command_Status(const std::string&) { +void TConsole::Command_Status(const std::string&, const std::vector& args) { + if (!EnsureArgsCount(args, 0)) { + return; + } std::stringstream Status; size_t CarCount = 0; @@ -292,6 +450,8 @@ void TConsole::Command_Status(const std::string&) { SystemsShutdown++; SystemsShutdownList += NameStatusPair.first + ", "; break; + default: + beammp_assert_not_reachable(); } } // remove ", " at the end @@ -310,7 +470,7 @@ void TConsole::Command_Status(const std::string&) { << "\tConnected Players: " << ConnectedCount << "\n" << "\tGuests: " << GuestCount << "\n" << "\tCars: " << CarCount << "\n" - << "\tUptime: " << ElapsedTime << "ms (~" << size_t(ElapsedTime / 1000.0 / 60.0 / 60.0) << "h) \n" + << "\tUptime: " << ElapsedTime << "ms (~" << size_t(double(ElapsedTime) / 1000.0 / 60.0 / 60.0) << "h) \n" << "\tLua:\n" << "\t\tQueued results to check: " << mLuaEngine->GetResultsToCheckSize() << "\n" << "\t\tStates: " << mLuaEngine->GetLuaStateCount() << "\n" @@ -318,12 +478,12 @@ void TConsole::Command_Status(const std::string&) { << "\t\tEvent handlers: " << mLuaEngine->GetRegisteredEventHandlerCount() << "\n" << "\tSubsystems:\n" << "\t\tGood/Starting/Bad: " << SystemsGood << "/" << SystemsStarting << "/" << SystemsBad << "\n" - << "\t\tShutting down/Shutdown: " << SystemsShuttingDown << "/" << SystemsShutdown << "\n" + << "\t\tShutting down/Shut down: " << SystemsShuttingDown << "/" << SystemsShutdown << "\n" << "\t\tGood: [ " << SystemsGoodList << " ]\n" << "\t\tStarting: [ " << SystemsStartingList << " ]\n" << "\t\tBad: [ " << SystemsBadList << " ]\n" << "\t\tShutting down: [ " << SystemsShuttingDownList << " ]\n" - << "\t\tShutdown: [ " << SystemsShutdownList << " ]\n" + << "\t\tShut down: [ " << SystemsShutdownList << " ]\n" << ""; Application::Console().WriteRaw(Status.str()); @@ -375,6 +535,58 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) { } } +void TConsole::HandleLuaInternalCommand(const std::string& cmd) { + if (cmd == "exit") { + ChangeToRegularConsole(); + } else if (cmd == "queued") { + auto QueuedFunctions = LuaAPI::MP::Engine->Debug_GetStateFunctionQueueForState(mStateId); + Application::Console().WriteRaw("Pending functions in State '" + mStateId + "'"); + std::unordered_map FunctionsCount; + std::vector FunctionsInOrder; + while (!QueuedFunctions.empty()) { + auto Tuple = QueuedFunctions.front(); + QueuedFunctions.erase(QueuedFunctions.begin()); + FunctionsInOrder.push_back(Tuple.FunctionName); + FunctionsCount[Tuple.FunctionName] += 1; + } + std::set Uniques; + for (const auto& Function : FunctionsInOrder) { + if (Uniques.count(Function) == 0) { + Uniques.insert(Function); + if (FunctionsCount.at(Function) > 1) { + Application::Console().WriteRaw(" " + Function + " (" + std::to_string(FunctionsCount.at(Function)) + "x)"); + } else { + Application::Console().WriteRaw(" " + Function); + } + } + } + Application::Console().WriteRaw("Executed functions waiting to be checked in State '" + mStateId + "'"); + for (const auto& Function : LuaAPI::MP::Engine->Debug_GetResultsToCheckForState(mStateId)) { + Application::Console().WriteRaw(" '" + Function.Function + "' (Ready? " + (Function.Ready ? "Yes" : "No") + ", Error? " + (Function.Error ? "Yes: '" + Function.ErrorMessage + "'" : "No") + ")"); + } + } else if (cmd == "events") { + auto Events = LuaAPI::MP::Engine->Debug_GetEventsForState(mStateId); + Application::Console().WriteRaw("Registered Events + Handlers for State '" + mStateId + "'"); + for (const auto& EventHandlerPair : Events) { + Application::Console().WriteRaw(" Event '" + EventHandlerPair.first + "'"); + for (const auto& Handler : EventHandlerPair.second) { + Application::Console().WriteRaw(" " + Handler); + } + } + } else if (cmd == "help") { + Application::Console().WriteRaw(R"(BeamMP Lua Debugger + All commands must be prefixed with a `:`. Non-prefixed commands are interpreted as Lua. + +Commands + :exit detaches (exits) from this Lua console + :help displays this help + :events shows a list of currently registered events + :queued shows a list of all pending and queued functions)"); + } else { + beammp_error("internal command '" + cmd + "' is not known"); + } +} + TConsole::TConsole() { mCommandline.enable_history(); mCommandline.set_history_limit(20); @@ -382,21 +594,22 @@ TConsole::TConsole() { BackupOldLog(); mCommandline.on_command = [this](Commandline& c) { try { - auto cmd = c.get_command(); - cmd = TrimString(cmd); - mCommandline.write(mCommandline.prompt() + cmd); + auto TrimmedCmd = c.get_command(); + TrimmedCmd = TrimString(TrimmedCmd); + auto [cmd, args] = ParseCommand(TrimmedCmd); + mCommandline.write(mCommandline.prompt() + TrimmedCmd); if (mIsLuaConsole) { if (!mLuaEngine) { beammp_info("Lua not started yet, please try again in a second"); - } else if (cmd == "exit()") { - ChangeToRegularConsole(); + } else if (!cmd.empty() && cmd.at(0) == ':') { + HandleLuaInternalCommand(cmd.substr(1)); } else { - auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared(cmd), "", "" }); + auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared(TrimmedCmd), "", "" }); while (!Future->Ready) { std::this_thread::yield(); // TODO: Add a timeout } if (Future->Error) { - beammp_lua_error(Future->ErrorMessage); + beammp_lua_error("error in " + mStateId + ": " + Future->ErrorMessage); } } } else { @@ -405,31 +618,88 @@ TConsole::TConsole() { } else if (cmd == "exit") { beammp_info("gracefully shutting down"); Application::GracefullyShutdown(); - } else if (StringStartsWith(cmd, "lua")) { - Command_Lua(cmd); - } else if (StringStartsWith(cmd, "help")) { - RunAsCommand(cmd, true); - Command_Help(cmd); - } else if (StringStartsWith(cmd, "kick")) { - RunAsCommand(cmd, true); - Command_Kick(cmd); - } else if (StringStartsWith(cmd, "say")) { - RunAsCommand(cmd, true); - Command_Say(cmd); - } else if (StringStartsWith(cmd, "list")) { - RunAsCommand(cmd, true); - Command_List(cmd); - } else if (StringStartsWith(cmd, "status")) { - RunAsCommand(cmd, true); - Command_Status(cmd); - } else if (!cmd.empty()) { - RunAsCommand(cmd); + } else if (cmd == "say") { + RunAsCommand(TrimmedCmd, true); + Command_Say(TrimmedCmd); + } else { + if (mCommandMap.find(cmd) != mCommandMap.end()) { + mCommandMap.at(cmd)(cmd, args); + RunAsCommand(TrimmedCmd, true); + } else { + RunAsCommand(TrimmedCmd); + } } } } catch (const std::exception& e) { beammp_error("Console died with: " + std::string(e.what()) + ". This could be a fatal error and could cause the server to terminate."); } }; + mCommandline.on_autocomplete = [this](Commandline&, std::string stub, int) { + std::vector suggestions; + try { + if (mIsLuaConsole) { // if lua + if (!mLuaEngine) { + beammp_info("Lua not started yet, please try again in a second"); + } else { + 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); + stub = stub.substr(i); + break; + } + } + + // turn string into vector of keys + std::vector tablekeys; + + SplitString(stub, '.', tablekeys); + + // remove last key if incomplete + if (stub.rfind('.') != stub.size() - 1 && !tablekeys.empty()) { + tablekeys.pop_back(); + } + + auto keys = mLuaEngine->GetStateTableKeysForState(mStateId, tablekeys); + + for (const auto& key : keys) { // go through each bottom-level key + auto last_dot = stub.rfind('.'); + std::string last_atom; + if (last_dot != std::string::npos) { + last_atom = stub.substr(last_dot + 1); + } + std::string before_last_atom = stub.substr(0, last_dot + 1); // get last confirmed key + auto last = stub.substr(stub.rfind('.') + 1); + std::string::size_type n = key.find(last); + if (n == 0) { + suggestions.push_back(prefix + before_last_atom + key); + } + } + } + } else { // if not lua + if (stub.find("lua") == 0) { // starts with "lua" means we should suggest state names + std::string after_prefix = TrimString(stub.substr(3)); + auto stateNames = mLuaEngine->GetLuaStateNames(); + + for (const auto& name : stateNames) { + if (name.find(after_prefix) == 0) { + suggestions.push_back("lua " + name); + } + } + } else { + for (const auto& [cmd_name, cmd_fn] : mCommandMap) { + if (cmd_name.find(stub) == 0) { + suggestions.push_back(cmd_name); + } + } + } + } + } catch (const std::exception& e) { + beammp_error("Console died with: " + std::string(e.what()) + ". This could be a fatal error and could cause the server to terminate."); + } + std::sort(suggestions.begin(), suggestions.end()); + return suggestions; + }; } void TConsole::Write(const std::string& str) { diff --git a/src/THeartbeatThread.cpp b/src/THeartbeatThread.cpp index d65380e..873825a 100644 --- a/src/THeartbeatThread.cpp +++ b/src/THeartbeatThread.cpp @@ -20,7 +20,7 @@ void THeartbeatThread::operator()() { static std::chrono::high_resolution_clock::time_point LastNormalUpdateTime = std::chrono::high_resolution_clock::now(); bool isAuth = false; size_t UpdateReminderCounter = 0; - while (!mShutdown) { + while (!Application::IsShuttingDown()) { ++UpdateReminderCounter; Body = GenerateCall(); // a hot-change occurs when a setting has changed, to update the backend of that change. @@ -40,10 +40,6 @@ void THeartbeatThread::operator()() { Body += "&ip=" + Application::Settings.CustomIP; } - Body += "&pps=" + Application::PPS(); - - beammp_trace("heartbeat body: '" + Body + "'"); - auto SentryReportError = [&](const std::string& transaction, int status) { auto Lock = Sentry.CreateExclusiveContext(); Sentry.SetContext("heartbeat", @@ -61,11 +57,12 @@ void THeartbeatThread::operator()() { 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" } }); - beammp_trace(T); Doc.Parse(T.data(), T.size()); if (Doc.HasParseError() || !Doc.IsObject()) { - beammp_debug("Failed to contact backend at " + Url + " (this is not an error)."); - beammp_trace("Response was: " + T); + if (!Application::Settings.Private) { + beammp_trace("Backend response failed to parse as valid json"); + beammp_trace("Response was: `" + T + "`"); + } Sentry.SetContext("JSON Response", { { "reponse", T } }); SentryReportError(Url + Target, ResponseCode); } else if (ResponseCode != 200) { @@ -113,21 +110,21 @@ void THeartbeatThread::operator()() { } } - if (Ok && !isAuth) { + if (Ok && !isAuth && !Application::Settings.Private) { if (Status == "2000") { - beammp_info(("Authenticated!")); + beammp_info(("Authenticated! " + Message)); isAuth = true; } else if (Status == "200") { - beammp_info(("Resumed authenticated session!")); + beammp_info(("Resumed authenticated session! " + Message)); isAuth = true; } else { if (Message.empty()) { - Message = "Backend didn't provide a reason"; + Message = "Backend didn't provide a reason."; } - beammp_error("Backend REFUSED the auth key. " + Message); + beammp_error("Backend REFUSED the auth key. Reason: " + Message); } } - if (isAuth) { + if (isAuth || Application::Settings.Private) { Application::SetSubsystemStatus("Heartbeat", Application::Status::Good); } if (!Application::Settings.HideUpdateMessages && UpdateReminderCounter % 5) { @@ -146,7 +143,7 @@ std::string THeartbeatThread::GenerateCall() { << "&map=" << Application::Settings.MapName << "&private=" << (Application::Settings.Private ? "true" : "false") << "&version=" << Application::ServerVersionString() - << "&clientversion=" << Application::ClientVersionString() + << "&clientversion=" << std::to_string(Application::ClientMajorVersion()) + ".0" // FIXME: Wtf. << "&name=" << Application::Settings.ServerName << "&modlist=" << mResourceManager.TrimmedList() << "&modstotalsize=" << mResourceManager.MaxModSize() @@ -162,7 +159,6 @@ THeartbeatThread::THeartbeatThread(TResourceManager& ResourceManager, TServer& S Application::RegisterShutdownHandler([&] { Application::SetSubsystemStatus("Heartbeat", Application::Status::ShuttingDown); if (mThread.joinable()) { - mShutdown = true; mThread.join(); } Application::SetSubsystemStatus("Heartbeat", Application::Status::Shutdown); diff --git a/src/TLuaEngine.cpp b/src/TLuaEngine.cpp index 1d684d8..87f13fa 100644 --- a/src/TLuaEngine.cpp +++ b/src/TLuaEngine.cpp @@ -4,10 +4,11 @@ #include "Http.h" #include "LuaAPI.h" #include "TLuaPlugin.h" +#include "sol/object.hpp" #include #include -#include +#include #include #include #include @@ -15,26 +16,29 @@ TLuaEngine* LuaAPI::MP::Engine; TLuaEngine::TLuaEngine() - : mPluginMonitor(fs::path(Application::Settings.Resource) / "Server", *this, mShutdown) { + : mResourceServerPath(fs::path(Application::Settings.Resource) / "Server") { Application::SetSubsystemStatus("LuaEngine", Application::Status::Starting); LuaAPI::MP::Engine = this; if (!fs::exists(Application::Settings.Resource)) { fs::create_directory(Application::Settings.Resource); } - fs::path Path = fs::path(Application::Settings.Resource) / "Server"; - if (!fs::exists(Path)) { - fs::create_directory(Path); + if (!fs::exists(mResourceServerPath)) { + fs::create_directory(mResourceServerPath); } - mResourceServerPath = Path; Application::RegisterShutdownHandler([&] { Application::SetSubsystemStatus("LuaEngine", Application::Status::ShuttingDown); - mShutdown = true; if (mThread.joinable()) { mThread.join(); } Application::SetSubsystemStatus("LuaEngine", Application::Status::Shutdown); }); - Start(); + IThreaded::Start(); +} + +TEST_CASE("TLuaEngine ctor & dtor") { + Application::Settings.Resource = "beammp_server_test_resources"; + TLuaEngine engine; + Application::GracefullyShutdown(); } void TLuaEngine::operator()() { @@ -53,30 +57,28 @@ void TLuaEngine::operator()() { auto ResultCheckThread = std::thread([&] { RegisterThread("ResultCheckThread"); - while (!mShutdown) { + while (!Application::IsShuttingDown()) { std::unique_lock Lock(mResultsToCheckMutex); - mResultsToCheckCond.wait_for(Lock, std::chrono::milliseconds(20)); if (!mResultsToCheck.empty()) { mResultsToCheck.remove_if([](const std::shared_ptr& Ptr) -> bool { if (Ptr->Ready) { - return true; - } else if (Ptr->Error) { - if (Ptr->ErrorMessage != BeamMPFnNotFoundError) { - beammp_lua_error(Ptr->Function + ": " + Ptr->ErrorMessage); + if (Ptr->Error) { + if (Ptr->ErrorMessage != BeamMPFnNotFoundError) { + beammp_lua_error(Ptr->Function + ": " + Ptr->ErrorMessage); + } } return true; } return false; }); + } else { + mResultsToCheckCond.wait_for(Lock, std::chrono::milliseconds(20)); } } }); // event loop auto Before = std::chrono::high_resolution_clock::now(); - while (!mShutdown) { - if (mLuaStates.size() == 0) { - std::this_thread::sleep_for(std::chrono::seconds(100)); - } + while (!Application::IsShuttingDown()) { { // Timed Events Scope std::unique_lock Lock(mTimedEventsMutex); for (auto& Timer : mTimedEvents) { @@ -102,12 +104,18 @@ void TLuaEngine::operator()() { } } } - const auto Expected = std::chrono::milliseconds(10); - if (auto Diff = std::chrono::high_resolution_clock::now() - Before; - Diff < Expected) { - std::this_thread::sleep_for(Expected - Diff); + if (mLuaStates.size() == 0) { + beammp_trace("No Lua states, event loop running extremely sparsely"); + Application::SleepSafeSeconds(10); } else { - beammp_trace("Event loop cannot keep up! Running " + std::to_string(Diff.count()) + "s behind"); + constexpr double NsFactor = 1000000.0; + constexpr double Expected = 10.0; // ms + const auto Diff = (std::chrono::high_resolution_clock::now() - Before).count() / NsFactor; + if (Diff < Expected) { + std::this_thread::sleep_for(std::chrono::nanoseconds(size_t((Expected - Diff) * NsFactor))); + } else { + beammp_tracef("Event loop cannot keep up! Running {}ms behind", Diff); + } } Before = std::chrono::high_resolution_clock::now(); } @@ -154,10 +162,130 @@ void TLuaEngine::AddResultToCheck(const std::shared_ptr& Result) { mResultsToCheckCond.notify_one(); } +std::unordered_map /* handlers */> TLuaEngine::Debug_GetEventsForState(TLuaStateId StateId) { + std::unordered_map> Result; + std::unique_lock Lock(mLuaEventsMutex); + for (const auto& EventNameToEventMap : mLuaEvents) { + for (const auto& IdSetOfHandlersPair : EventNameToEventMap.second) { + if (IdSetOfHandlersPair.first == StateId) { + for (const auto& Handler : IdSetOfHandlersPair.second) { + Result[EventNameToEventMap.first].push_back(Handler); + } + } + } + } + return Result; +} + +std::queue>> TLuaEngine::Debug_GetStateExecuteQueueForState(TLuaStateId StateId) { + std::queue>> Result; + std::unique_lock Lock(mLuaStatesMutex); + Result = mLuaStates.at(StateId)->Debug_GetStateExecuteQueue(); + return Result; +} + +std::vector TLuaEngine::Debug_GetStateFunctionQueueForState(TLuaStateId StateId) { + std::vector Result; + std::unique_lock Lock(mLuaStatesMutex); + Result = mLuaStates.at(StateId)->Debug_GetStateFunctionQueue(); + return Result; +} + +std::vector TLuaEngine::Debug_GetResultsToCheckForState(TLuaStateId StateId) { + std::unique_lock Lock(mResultsToCheckMutex); + auto ResultsToCheckCopy = mResultsToCheck; + Lock.unlock(); + std::vector Result; + while (!ResultsToCheckCopy.empty()) { + auto ResultToCheck = std::move(ResultsToCheckCopy.front()); + ResultsToCheckCopy.pop_front(); + if (ResultToCheck->StateId == StateId) { + Result.push_back(*ResultToCheck); + } + } + return Result; +} + +std::vector TLuaEngine::GetStateGlobalKeysForState(TLuaStateId StateId) { + std::unique_lock Lock(mLuaStatesMutex); + auto Result = mLuaStates.at(StateId)->GetStateGlobalKeys(); + return Result; +} + +std::vector TLuaEngine::StateThreadData::GetStateGlobalKeys() { + auto globals = mStateView.globals(); + std::vector Result; + for (const auto& [key, value] : globals) { + Result.push_back(key.as()); + } + return Result; +} + +std::vector TLuaEngine::GetStateTableKeysForState(TLuaStateId StateId, std::vector keys) { + std::unique_lock Lock(mLuaStatesMutex); + auto Result = mLuaStates.at(StateId)->GetStateTableKeys(keys); + return Result; +} + +std::vector TLuaEngine::StateThreadData::GetStateTableKeys(const std::vector& keys) { + auto globals = mStateView.globals(); + + sol::table current = globals; + std::vector Result {}; + + for (const auto& [key, value] : current) { + std::string s = key.as(); + if (value.get_type() == sol::type::function) { + s += "("; + } + Result.push_back(s); + } + + if (!keys.empty()) { + Result.clear(); + } + + for (size_t i = 0; i < keys.size(); ++i) { + auto obj = current.get(keys.at(i)); + if (obj.get_type() == sol::type::nil) { + // error + break; + } else if (i == keys.size() - 1) { + if (obj.get_type() == sol::type::table) { + for (const auto& [key, value] : obj.as()) { + std::string s = key.as(); + if (value.get_type() == sol::type::function) { + s += "("; + } + Result.push_back(s); + } + } else { + Result = { obj.as() }; + } + break; + } + if (obj.get_type() == sol::type::table) { + current = obj; + } else { + // error + break; + } + } + + return Result; +} + +/* + + _G.a.b.c.d. + +*/ + void TLuaEngine::WaitForAll(std::vector>& Results, const std::optional& Max) { for (const auto& Result : Results) { bool Cancelled = false; size_t ms = 0; + std::set WarnedResults; while (!Result->Ready && !Cancelled) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); ms += 10; @@ -165,7 +293,11 @@ void TLuaEngine::WaitForAll(std::vector>& Results, c beammp_trace("'" + Result->Function + "' in '" + Result->StateId + "' did not finish executing in time (took: " + std::to_string(ms) + "ms)."); Cancelled = true; } else if (ms > 1000 * 60) { - beammp_lua_warn("'" + Result->Function + "' in '" + Result->StateId + "' is taking very long. The event it's handling is too important to discard the result of this handler, but may block this event and possibly the whole lua state."); + auto ResultId = Result->StateId + "_" + Result->Function; + if (WarnedResults.count(ResultId) == 0) { + WarnedResults.insert(ResultId); + beammp_lua_warn("'" + Result->Function + "' in '" + Result->StateId + "' is taking very long. The event it's handling is too important to discard the result of this handler, but may block this event and possibly the whole lua state."); + } } } if (Cancelled) { @@ -204,6 +336,9 @@ std::shared_ptr TLuaEngine::EnqueueFunctionCall(TLuaStateId StateID, } void TLuaEngine::CollectAndInitPlugins() { + if (!fs::exists(mResourceServerPath)) { + fs::create_directories(mResourceServerPath); + } for (const auto& Dir : fs::directory_iterator(mResourceServerPath)) { auto Path = Dir.path(); Path = fs::relative(Path); @@ -253,7 +388,7 @@ void TLuaEngine::EnsureStateExists(TLuaStateId StateId, const std::string& Name, std::unique_lock Lock(mLuaStatesMutex); if (mLuaStates.find(StateId) == mLuaStates.end()) { beammp_debug("Creating lua state for state id \"" + StateId + "\""); - auto DataPtr = std::make_unique(Name, mShutdown, StateId, *this); + auto DataPtr = std::make_unique(Name, StateId, *this); mLuaStates[StateId] = std::move(DataPtr); RegisterEvent("onInit", StateId, "onInit"); if (!DontCallOnInit) { @@ -277,14 +412,12 @@ std::set TLuaEngine::GetEventHandlersForState(const std::string& Ev sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string& EventName, sol::variadic_args EventArgs) { auto Return = mEngine->TriggerEvent(EventName, mStateId, EventArgs); - // TODO Synchronous call to the event handlers auto MyHandlers = mEngine->GetEventHandlersForState(EventName, mStateId); for (const auto& Handler : MyHandlers) { auto Fn = mStateView[Handler]; if (Fn.valid()) { auto LuaResult = Fn(EventArgs); auto Result = std::make_shared(); - Result->Ready = true; if (LuaResult.valid()) { Result->Error = false; Result->Result = LuaResult; @@ -292,6 +425,7 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string Result->Error = true; Result->ErrorMessage = "Function result in TriggerGlobalEvent was invalid"; } + Result->Ready = true; Return.push_back(Result); } } @@ -335,7 +469,7 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerLocalEvent(const std::string& Result.add(FnRet); } else { sol::error Err = FnRet; - beammp_lua_error(Err.what()); + beammp_lua_error(std::string("TriggerLocalEvent: ") + Err.what()); } } } @@ -386,6 +520,32 @@ int TLuaEngine::StateThreadData::Lua_GetPlayerIDByName(const std::string& Name) return Id; } +sol::table TLuaEngine::StateThreadData::Lua_FS_ListFiles(const std::string& Path) { + if (!std::filesystem::exists(Path)) { + return sol::lua_nil; + } + auto table = mStateView.create_table(); + for (const auto& entry : std::filesystem::directory_iterator(Path)) { + if (entry.is_regular_file() || entry.is_symlink()) { + table[table.size() + 1] = entry.path().lexically_relative(Path).string(); + } + } + return table; +} + +sol::table TLuaEngine::StateThreadData::Lua_FS_ListDirectories(const std::string& Path) { + if (!std::filesystem::exists(Path)) { + return sol::lua_nil; + } + auto table = mStateView.create_table(); + for (const auto& entry : std::filesystem::directory_iterator(Path)) { + if (entry.is_directory()) { + table[table.size() + 1] = entry.path().lexically_relative(Path).string(); + } + } + return table; +} + std::string TLuaEngine::StateThreadData::Lua_GetPlayerName(int ID) { auto MaybeClient = GetClient(mEngine->Server(), ID); if (MaybeClient && !MaybeClient.value().expired()) { @@ -417,6 +577,33 @@ sol::table TLuaEngine::StateThreadData::Lua_GetPlayerVehicles(int ID) { return sol::lua_nil; } +std::pair TLuaEngine::StateThreadData::Lua_GetPositionRaw(int PID, int VID) { + std::pair Result; + auto MaybeClient = GetClient(mEngine->Server(), PID); + if (MaybeClient && !MaybeClient.value().expired()) { + 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"; + 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 = "Client expired"; + return Result; + } +} + sol::table TLuaEngine::StateThreadData::Lua_HttpCreateConnection(const std::string& host, uint16_t port) { auto table = mStateView.create_table(); constexpr const char* InternalClient = "__InternalClient"; @@ -439,9 +626,87 @@ sol::table TLuaEngine::StateThreadData::Lua_HttpCreateConnection(const std::stri return table; } -TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, std::atomic_bool& Shutdown, TLuaStateId StateId, TLuaEngine& Engine) +template +static void AddToTable(sol::table& table, const std::string& left, const T& value) { + if (left.empty()) { + table[table.size() + 1] = value; + } else { + table[left] = value; + } +} + +static void JsonDecodeRecursive(sol::state_view& StateView, sol::table& table, const std::string& left, const nlohmann::json& right) { + switch (right.type()) { + case nlohmann::detail::value_t::null: + return; + case nlohmann::detail::value_t::object: { + auto value = table.create(); + value.clear(); + for (const auto& entry : right.items()) { + JsonDecodeRecursive(StateView, value, entry.key(), entry.value()); + } + AddToTable(table, left, value); + break; + } + case nlohmann::detail::value_t::array: { + auto value = table.create(); + value.clear(); + for (const auto& entry : right.items()) { + JsonDecodeRecursive(StateView, value, "", entry.value()); + } + AddToTable(table, left, value); + break; + } + case nlohmann::detail::value_t::string: + AddToTable(table, left, right.get()); + break; + case nlohmann::detail::value_t::boolean: + AddToTable(table, left, right.get()); + break; + case nlohmann::detail::value_t::number_integer: + AddToTable(table, left, right.get()); + break; + case nlohmann::detail::value_t::number_unsigned: + AddToTable(table, left, right.get()); + break; + case nlohmann::detail::value_t::number_float: + AddToTable(table, left, right.get()); + break; + case nlohmann::detail::value_t::binary: + beammp_lua_error("JsonDecode can't handle binary blob in json, ignoring"); + return; + case nlohmann::detail::value_t::discarded: + return; + default: + beammp_assert_not_reachable(); + } +} + +sol::table TLuaEngine::StateThreadData::Lua_JsonDecode(const std::string& str) { + sol::state_view StateView(mState); + auto table = StateView.create_table(); + if (!nlohmann::json::accept(str)) { + beammp_lua_error("string given to JsonDecode is not valid json: `" + str + "`"); + return sol::lua_nil; + } + nlohmann::json json = nlohmann::json::parse(str); + if (json.is_object()) { + for (const auto& entry : json.items()) { + JsonDecodeRecursive(StateView, table, entry.key(), entry.value()); + } + } else if (json.is_array()) { + for (const auto& entry : json) { + JsonDecodeRecursive(StateView, table, "", entry); + } + } else { + beammp_lua_error("JsonDecode expected array or object json, instead got " + std::string(json.type_name())); + return sol::lua_nil; + } + return table; +} + +TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateId StateId, TLuaEngine& Engine) : mName(Name) - , mShutdown(Shutdown) , mStateId(StateId) , mState(luaL_newstate()) , mEngine(&Engine) { @@ -484,6 +749,7 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, std::atomi return Lua_TriggerLocalEvent(EventName, EventArgs); }); MPTable.set_function("TriggerClientEvent", &LuaAPI::MP::TriggerClientEvent); + MPTable.set_function("TriggerClientEventJson", &LuaAPI::MP::TriggerClientEventJson); MPTable.set_function("GetPlayerCount", &LuaAPI::MP::GetPlayerCount); MPTable.set_function("IsPlayerConnected", &LuaAPI::MP::IsPlayerConnected); MPTable.set_function("GetPlayerIDByName", [&](const std::string& Name) -> int { @@ -496,6 +762,9 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, std::atomi MPTable.set_function("GetPlayerVehicles", [&](int ID) -> sol::table { return Lua_GetPlayerVehicles(ID); }); + MPTable.set_function("GetPositionRaw", [&](int PID, int VID) -> std::pair { + return Lua_GetPositionRaw(PID, VID); + }); MPTable.set_function("SendChatMessage", &LuaAPI::MP::SendChatMessage); MPTable.set_function("GetPlayers", [&]() -> sol::table { return Lua_GetPlayers(); @@ -538,6 +807,27 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, std::atomi mEngine->CancelEventTimers(EventName, mStateId); }); MPTable.set_function("Set", &LuaAPI::MP::Set); + + auto UtilTable = StateView.create_named_table("Util"); + UtilTable.set_function("JsonEncode", &LuaAPI::MP::JsonEncode); + UtilTable.set_function("JsonDecode", [this](const std::string& str) { + return Lua_JsonDecode(str); + }); + UtilTable.set_function("JsonDiff", &LuaAPI::MP::JsonDiff); + UtilTable.set_function("JsonFlatten", &LuaAPI::MP::JsonFlatten); + UtilTable.set_function("JsonUnflatten", &LuaAPI::MP::JsonUnflatten); + UtilTable.set_function("JsonPrettify", &LuaAPI::MP::JsonPrettify); + UtilTable.set_function("JsonMinify", &LuaAPI::MP::JsonMinify); + UtilTable.set_function("Random", [this] { + return mUniformRealDistribution01(mMersenneTwister); + }); + UtilTable.set_function("RandomRange", [this](double min, double max) -> double { + return std::uniform_real_distribution(min, max)(mMersenneTwister); + }); + UtilTable.set_function("RandomIntRange", [this](int64_t min, int64_t max) -> int64_t { + return std::uniform_int_distribution(min, max)(mMersenneTwister); + }); + auto HttpTable = StateView.create_named_table("Http"); HttpTable.set_function("CreateConnection", [this](const std::string& host, uint16_t port) { return Lua_HttpCreateConnection(host, port); @@ -568,6 +858,12 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, std::atomi FSTable.set_function("IsDirectory", &LuaAPI::FS::IsDirectory); FSTable.set_function("IsFile", &LuaAPI::FS::IsFile); FSTable.set_function("ConcatPaths", &LuaAPI::FS::ConcatPaths); + FSTable.set_function("ListFiles", [this](const std::string& Path) { + return Lua_FS_ListFiles(Path); + }); + FSTable.set_function("ListDirectories", [this](const std::string& Path) { + return Lua_FS_ListDirectories(Path); + }); Start(); } @@ -616,7 +912,7 @@ void TLuaEngine::StateThreadData::RegisterEvent(const std::string& EventName, co void TLuaEngine::StateThreadData::operator()() { RegisterThread("Lua:" + mStateId); - while (!mShutdown) { + while (!Application::IsShuttingDown()) { { // StateExecuteQueue Scope std::unique_lock Lock(mStateExecuteQueueMutex); if (!mStateExecuteQueue.empty()) { @@ -651,7 +947,6 @@ 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); - S.second->Ready = true; if (Res.valid()) { S.second->Error = false; S.second->Result = std::move(Res); @@ -660,6 +955,7 @@ void TLuaEngine::StateThreadData::operator()() { sol::error Err = Res; S.second->ErrorMessage = Err.what(); } + S.second->Ready = true; } } { // StateFunctionQueue Scope @@ -697,6 +993,15 @@ void TLuaEngine::StateThreadData::operator()() { case TLuaArgTypes_Bool: LuaArgs.push_back(sol::make_object(StateView, std::get(Arg))); break; + case TLuaArgTypes_StringStringMap: { + auto Map = std::get>(Arg); + auto Table = StateView.create_table(); + for (const auto& [k, v] : Map) { + Table[k] = v; + } + LuaArgs.push_back(sol::make_object(StateView, Table)); + break; + } default: beammp_error("Unknown argument type, passed as nil"); break; @@ -722,6 +1027,16 @@ void TLuaEngine::StateThreadData::operator()() { } } +std::queue>> TLuaEngine::StateThreadData::Debug_GetStateExecuteQueue() { + std::unique_lock Lock(mStateExecuteQueueMutex); + return mStateExecuteQueue; +} + +std::vector TLuaEngine::StateThreadData::Debug_GetStateFunctionQueue() { + std::unique_lock Lock(mStateFunctionQueueMutex); + return mStateFunctionQueue; +} + void TLuaEngine::CreateEventTimer(const std::string& EventName, TLuaStateId StateId, size_t IntervalMS, CallStrategy Strategy) { std::unique_lock Lock(mTimedEventsMutex); TimedEvent Event { @@ -776,60 +1091,3 @@ bool TLuaEngine::TimedEvent::Expired() { void TLuaEngine::TimedEvent::Reset() { LastCompletion = std::chrono::high_resolution_clock::now(); } - -TPluginMonitor::TPluginMonitor(const fs::path& Path, TLuaEngine& Engine, std::atomic_bool& Shutdown) - : mEngine(Engine) - , mPath(Path) - , mShutdown(Shutdown) { - if (!fs::exists(mPath)) { - fs::create_directories(mPath); - } - for (const auto& Entry : fs::recursive_directory_iterator(mPath)) { - // TODO: trigger an event when a subfolder file changes - if (Entry.is_regular_file()) { - mFileTimes[Entry.path().string()] = fs::last_write_time(Entry.path()); - } - } - Start(); -} - -void TPluginMonitor::operator()() { - RegisterThread("PluginMonitor"); - beammp_info("PluginMonitor started"); - while (!mShutdown) { - std::this_thread::sleep_for(std::chrono::seconds(3)); - for (const auto& Pair : mFileTimes) { - auto CurrentTime = fs::last_write_time(Pair.first); - if (CurrentTime != Pair.second) { - mFileTimes[Pair.first] = CurrentTime; - // grandparent of the path should be Resources/Server - if (fs::equivalent(fs::path(Pair.first).parent_path().parent_path(), mPath)) { - beammp_info("File \"" + Pair.first + "\" changed, reloading"); - // is in root folder, so reload - std::ifstream FileStream(Pair.first, std::ios::in | std::ios::binary); - auto Size = std::filesystem::file_size(Pair.first); - auto Contents = std::make_shared(); - Contents->resize(Size); - FileStream.read(Contents->data(), Contents->size()); - TLuaChunk Chunk(Contents, Pair.first, fs::path(Pair.first).parent_path().string()); - auto StateID = mEngine.GetStateIDForPlugin(fs::path(Pair.first).parent_path()); - auto Res = mEngine.EnqueueScript(StateID, Chunk); - // TODO: call onInit - mEngine.AddResultToCheck(Res); - } else { - // TODO: trigger onFileChanged event - beammp_trace("Change detected in file \"" + Pair.first + "\", event trigger not implemented yet"); - /* - // is in subfolder, dont reload, just trigger an event - auto Results = mEngine.TriggerEvent("onFileChanged", "", Pair.first); - mEngine.WaitForAll(Results); - for (const auto& Result : Results) { - if (Result->Error) { - beammp_lua_error(Result->ErrorMessage); - } - }*/ - } - } - } - } -} diff --git a/src/TNetwork.cpp b/src/TNetwork.cpp index d9edc81..6fbc16c 100644 --- a/src/TNetwork.cpp +++ b/src/TNetwork.cpp @@ -1,15 +1,33 @@ #include "TNetwork.h" #include "Client.h" +#include "Common.h" #include "LuaAPI.h" #include "TLuaEngine.h" +#include "nlohmann/json.hpp" #include #include #include +#include +#include #include +std::vector StringToVector(const std::string& Str) { + return std::vector(Str.data(), Str.data() + Str.size()); +} + +static void CompressProperly(std::vector& Data) { + constexpr std::string_view ABG = "ABG:"; + auto CombinedData = std::vector(ABG.begin(), ABG.end()); + auto CompData = Comp(Data); + CombinedData.resize(ABG.size() + CompData.size()); + std::copy(CompData.begin(), CompData.end(), CombinedData.begin() + ABG.size()); + Data = CombinedData; +} + TNetwork::TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& ResourceManager) : mServer(Server) , mPPSMonitor(PPSMonitor) + , mUDPSock(Server.IoCtx()) , mResourceManager(ResourceManager) { Application::SetSubsystemStatus("TCPNetwork", Application::Status::Starting); Application::SetSubsystemStatus("UDPNetwork", Application::Status::Starting); @@ -25,7 +43,6 @@ TNetwork::TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& R Application::RegisterShutdownHandler([&] { Application::SetSubsystemStatus("UDPNetwork", Application::Status::ShuttingDown); if (mUDPThread.joinable()) { - mShutdown = true; mUDPThread.detach(); } Application::SetSubsystemStatus("UDPNetwork", Application::Status::Shutdown); @@ -33,7 +50,6 @@ TNetwork::TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& R Application::RegisterShutdownHandler([&] { Application::SetSubsystemStatus("TCPNetwork", Application::Status::ShuttingDown); if (mTCPThread.joinable()) { - mShutdown = true; mTCPThread.detach(); } Application::SetSubsystemStatus("TCPNetwork", Application::Status::Shutdown); @@ -44,40 +60,30 @@ TNetwork::TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& R void TNetwork::UDPServerMain() { RegisterThread("UDPServer"); -#if defined(BEAMMP_WINDOWS) - WSADATA data; - if (WSAStartup(514, &data)) { - beammp_error(("Can't start Winsock!")); - // return; - } -#endif // WINDOWS - mUDPSock = socket(AF_INET, SOCK_DGRAM, 0); - // Create a server hint structure for the server - sockaddr_in serverAddr {}; - serverAddr.sin_addr.s_addr = INADDR_ANY; // Any Local - serverAddr.sin_family = AF_INET; // Address format is IPv4 - serverAddr.sin_port = htons(uint16_t(Application::Settings.Port)); // Convert from little to big endian - - // Try and bind the socket to the IP and port - if (bind(mUDPSock, (sockaddr*)&serverAddr, sizeof(serverAddr)) != 0) { - beammp_error("bind() failed: " + GetPlatformAgnosticErrorString()); + ip::udp::endpoint UdpListenEndpoint(ip::address::from_string("0.0.0.0"), Application::Settings.Port); + boost::system::error_code ec; + mUDPSock.open(UdpListenEndpoint.protocol(), ec); + if (ec) { + beammp_error("open() failed: " + ec.message()); std::this_thread::sleep_for(std::chrono::seconds(5)); - exit(-1); // TODO: Wtf. - // return; + Application::GracefullyShutdown(); + } + mUDPSock.bind(UdpListenEndpoint, ec); + if (ec) { + beammp_error("bind() failed: " + ec.message()); + std::this_thread::sleep_for(std::chrono::seconds(5)); + Application::GracefullyShutdown(); } Application::SetSubsystemStatus("UDPNetwork", Application::Status::Good); beammp_info(("Vehicle data network online on port ") + std::to_string(Application::Settings.Port) + (" with a Max of ") + std::to_string(Application::Settings.MaxPlayers) + (" Clients")); - while (!mShutdown) { + while (!Application::IsShuttingDown()) { try { - sockaddr_in client {}; - std::string Data = UDPRcvFromClient(client); // Receives any data from Socket - size_t Pos = Data.find(':'); - if (Data.empty() || Pos > 2) + ip::udp::endpoint client {}; + std::vector Data = UDPRcvFromClient(client); // Receives any data from Socket + auto Pos = std::find(Data.begin(), Data.end(), ':'); + if (Data.empty() || Pos > Data.begin() + 2) continue; - /*char clientIp[256]; - ZeroMemory(clientIp, 256); ///Code to get IP we don't need that yet - inet_ntop(AF_INET, &client.sin_addr, clientIp, 256);*/ uint8_t ID = uint8_t(Data.at(0)) - 1; mServer.ForEachClient([&](std::weak_ptr ClientPtr) -> bool { std::shared_ptr Client; @@ -92,7 +98,8 @@ void TNetwork::UDPServerMain() { if (Client->GetID() == ID) { Client->SetUDPAddr(client); Client->SetIsConnected(true); - TServer::GlobalParser(ClientPtr, Data.substr(2), mPPSMonitor, *this); + Data.erase(Data.begin(), Data.begin() + 2); + TServer::GlobalParser(ClientPtr, std::move(Data), mPPSMonitor, *this); } return true; @@ -105,82 +112,52 @@ void TNetwork::UDPServerMain() { void TNetwork::TCPServerMain() { RegisterThread("TCPServer"); -#if defined(BEAMMP_WINDOWS) - WSADATA wsaData; - if (WSAStartup(514, &wsaData)) { - beammp_error("Can't start Winsock!"); + + ip::tcp::endpoint ListenEp(ip::address::from_string("0.0.0.0"), Application::Settings.Port); + ip::tcp::socket Listener(mServer.IoCtx()); + boost::system::error_code ec; + Listener.open(ListenEp.protocol(), ec); + if (ec) { + beammp_errorf("Failed to open socket: {}", ec.message()); return; } -#endif // WINDOWS - TConnection client {}; - SOCKET Listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - int optval = 1; -#if defined(BEAMMP_WINDOWS) - const char* optval_ptr = reinterpret_cast(&optval); -#elif defined(BEAMMP_LINUX) || defined(BEAMMP_APPLE) - void* optval_ptr = reinterpret_cast(&optval); -#endif - setsockopt(Listener, SOL_SOCKET, SO_REUSEADDR, optval_ptr, sizeof(optval)); - // TODO: check optval or return value idk - sockaddr_in addr {}; - addr.sin_addr.s_addr = INADDR_ANY; - addr.sin_family = AF_INET; - addr.sin_port = htons(uint16_t(Application::Settings.Port)); - if (bind(Listener, (sockaddr*)&addr, sizeof(addr)) != 0) { - beammp_error("bind() failed: " + GetPlatformAgnosticErrorString()); - std::this_thread::sleep_for(std::chrono::seconds(5)); - exit(-1); // TODO: Wtf. + socket_base::linger LingerOpt {}; + LingerOpt.enabled(false); + Listener.set_option(LingerOpt, ec); + if (ec) { + beammp_errorf("Failed to set up listening socket to not linger / reuse address. " + "This may cause the socket to refuse to bind(). Error: {}", + ec.message()); } - if (Listener == -1) { - beammp_error("Invalid listening socket"); - return; - } - if (listen(Listener, SOMAXCONN)) { - beammp_error("listen() failed: " + GetPlatformAgnosticErrorString()); - // FIXME leak Listener - return; + + ip::tcp::acceptor Acceptor(mServer.IoCtx(), ListenEp); + Acceptor.listen(socket_base::max_listen_connections, ec); + if (ec) { + beammp_errorf("listen() failed, which is needed for the server to operate. " + "Shutting down. Error: {}", + ec.message()); + Application::GracefullyShutdown(); } Application::SetSubsystemStatus("TCPNetwork", Application::Status::Good); - beammp_info(("Vehicle event network online")); + beammp_info("Vehicle event network online"); do { try { - if (mShutdown) { + if (Application::IsShuttingDown()) { beammp_debug("shutdown during TCP wait for accept loop"); break; } - client.SockAddrLen = sizeof(client.SockAddr); - client.Socket = accept(Listener, &client.SockAddr, &client.SockAddrLen); - if (client.Socket == -1) { - beammp_warn(("Got an invalid client socket on connect! Skipping...")); - continue; + ip::tcp::endpoint ClientEp; + ip::tcp::socket ClientSocket = Acceptor.accept(ClientEp, ec); + if (ec) { + beammp_errorf("failed to accept: {}", ec.message()); } - // set timeout (DWORD, aka uint32_t) - uint32_t SendTimeoutMS = 30 * 1000; -#if defined(BEAMMP_WINDOWS) - int ret = ::setsockopt(client.Socket, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&SendTimeoutMS), sizeof(SendTimeoutMS)); -#else // POSIX - struct timeval optval; - optval.tv_sec = (int)(SendTimeoutMS / 1000); - optval.tv_usec = (SendTimeoutMS % 1000) * 1000; - int ret = ::setsockopt(client.Socket, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&optval), sizeof(optval)); -#endif - if (ret < 0) { - throw std::runtime_error("setsockopt recv timeout: " + GetPlatformAgnosticErrorString()); - } - std::thread ID(&TNetwork::Identify, this, client); + TConnection Conn { std::move(ClientSocket), ClientEp }; + std::thread ID(&TNetwork::Identify, this, std::move(Conn)); ID.detach(); // TODO: Add to a queue and attempt to join periodically } catch (const std::exception& e) { - beammp_error(("fatal: ") + std::string(e.what())); + beammp_error("fatal: " + std::string(e.what())); } - } while (client.Socket); - - beammp_debug("all ok, arrived at " + std::string(__func__) + ":" + std::to_string(__LINE__)); - - CloseSocketProper(client.Socket); -#ifdef BEAMMP_WINDOWS - CloseSocketProper(client.Socket); - WSACleanup(); -#endif // WINDOWS + } while (!Application::IsShuttingDown()); } #undef GetObject // Fixes Windows @@ -188,34 +165,38 @@ void TNetwork::TCPServerMain() { #include "Json.h" namespace json = rapidjson; -void TNetwork::Identify(const TConnection& client) { +void TNetwork::Identify(TConnection&& RawConnection) { RegisterThreadAuto(); char Code; - if (recv(client.Socket, &Code, 1, 0) != 1) { - CloseSocketProper(client.Socket); + + boost::system::error_code ec; + read(RawConnection.Socket, buffer(&Code, 1), ec); + if (ec) { + // TODO: is this right?! + RawConnection.Socket.shutdown(socket_base::shutdown_both, ec); return; } + std::shared_ptr Client { nullptr }; if (Code == 'C') { - Authentication(client); + Client = Authentication(std::move(RawConnection)); } else if (Code == 'D') { - HandleDownload(client.Socket); + HandleDownload(std::move(RawConnection)); } else if (Code == 'P') { -#if defined(BEAMMP_LINUX) || defined(BEAMMP_APPLE) - send(client.Socket, "P", 1, MSG_NOSIGNAL); -#else - send(client.Socket, "P", 1, 0); -#endif - CloseSocketProper(client.Socket); + boost::system::error_code ec; + write(RawConnection.Socket, buffer("P"), ec); return; } else { - CloseSocketProper(client.Socket); + beammp_errorf("Invalid code got in Identify: '{}'", Code); } } -void TNetwork::HandleDownload(SOCKET TCPSock) { +void TNetwork::HandleDownload(TConnection&& Conn) { char D; - if (recv(TCPSock, &D, 1, 0) != 1) { - CloseSocketProper(TCPSock); + boost::system::error_code ec; + read(Conn.Socket, buffer(&D, 1), ec); + if (ec) { + Conn.Socket.shutdown(socket_base::shutdown_both, ec); + // ignore ec return; } auto ID = uint8_t(D); @@ -224,97 +205,78 @@ void TNetwork::HandleDownload(SOCKET TCPSock) { if (!ClientPtr.expired()) { auto c = ClientPtr.lock(); if (c->GetID() == ID) { - c->SetDownSock(TCPSock); + c->SetDownSock(std::move(Conn.Socket)); } } return true; }); } -void TNetwork::Authentication(const TConnection& ClientConnection) { - auto Client = CreateClient(ClientConnection.Socket); - char AddrBuf[64]; - // TODO: IPv6 would need this to be changed - auto str = inet_ntop(AF_INET, reinterpret_cast(&ClientConnection.SockAddr), AddrBuf, sizeof(ClientConnection.SockAddr)); - beammp_trace("This thread is ip " + std::string(str)); - Client->SetIdentifier("ip", str); +std::shared_ptr TNetwork::Authentication(TConnection&& RawConnection) { + auto Client = CreateClient(std::move(RawConnection.Socket)); + Client->SetIdentifier("ip", RawConnection.SockAddr.address().to_string()); + beammp_tracef("This thread is ip {}", RawConnection.SockAddr.address().to_string()); - std::string Rc; // TODO: figure out why this is not default constructed beammp_info("Identifying new ClientConnection..."); - Rc = TCPRcv(*Client); + auto Data = TCPRcv(*Client); - if (Rc.size() > 3 && Rc.substr(0, 2) == "VC") { - Rc = Rc.substr(2); - if (Rc.length() > 4 || Rc != Application::ClientVersionString()) { + constexpr std::string_view VC = "VC"; + if (Data.size() > 3 && std::equal(Data.begin(), Data.begin() + VC.size(), VC.begin(), VC.end())) { + std::string ClientVersionStr(reinterpret_cast(Data.data() + 2), Data.size() - 2); + Version ClientVersion = Application::VersionStrToInts(ClientVersionStr + ".0"); + if (ClientVersion.major != Application::ClientMajorVersion()) { + beammp_errorf("Client tried to connect with version '{}', but only versions '{}.x.x' is allowed", + ClientVersion.AsString(), Application::ClientMajorVersion()); ClientKick(*Client, "Outdated Version!"); - return; + return nullptr; } } else { - ClientKick(*Client, "Invalid version header!"); - return; + ClientKick(*Client, fmt::format("Invalid version header: '{}' ({})", std::string(reinterpret_cast(Data.data()), Data.size()), Data.size())); + return nullptr; } - if (!TCPSend(*Client, "S")) { + if (!TCPSend(*Client, StringToVector("S"))) { // TODO: handle } - Rc = TCPRcv(*Client); + Data = TCPRcv(*Client); - if (Rc.size() > 50) { - ClientKick(*Client, "Invalid Key!"); - return; + if (Data.size() > 50) { + ClientKick(*Client, "Invalid Key (too long)!"); + return nullptr; } - auto RequestString = R"({"key":")" + Rc + "\"}"; - + nlohmann::json AuthReq { + { "key", std::string(reinterpret_cast(Data.data()), Data.size()) } + }; auto Target = "/pkToUser"; unsigned int ResponseCode = 0; - if (!Rc.empty()) { - Rc = Http::POST(Application::GetBackendUrlForAuth(), 443, Target, RequestString, "application/json", &ResponseCode); - } + const auto AuthResStr = Http::POST(Application::GetBackendUrlForAuth(), 443, Target, AuthReq.dump(), "application/json", &ResponseCode); - json::Document AuthResponse; - AuthResponse.Parse(Rc.c_str()); - if (Rc == Http::ErrorString || AuthResponse.HasParseError()) { + try { + nlohmann::json AuthRes = nlohmann::json::parse(AuthResStr); + + if (AuthRes["username"].is_string() && AuthRes["roles"].is_string() + && AuthRes["guest"].is_boolean() && AuthRes["identifiers"].is_array()) { + + Client->SetName(AuthRes["username"]); + Client->SetRoles(AuthRes["roles"]); + Client->SetIsGuest(AuthRes["guest"]); + for (const auto& ID : AuthRes["identifiers"]) { + auto Raw = std::string(ID); + auto SepIndex = Raw.find(':'); + Client->SetIdentifier(Raw.substr(0, SepIndex), Raw.substr(SepIndex + 1)); + } + } else { + beammp_error("Invalid authentication data received from authentication backend"); + ClientKick(*Client, "Invalid authentication data!"); + return nullptr; + } + } catch (const std::exception& e) { + beammp_errorf("Client sent invalid key. Error was: {}", e.what()); + // TODO: we should really clarify that this was a backend response or parsing error ClientKick(*Client, "Invalid key! Please restart your game."); - return; - } - - if (!AuthResponse.IsObject()) { - if (Rc == "0") { - auto Lock = Sentry.CreateExclusiveContext(); - Sentry.SetContext("auth", - { { "response-body", Rc }, - { "key", RequestString } }); - Sentry.SetTransaction(Application::GetBackendUrlForAuth() + Target); - Sentry.Log(SentryLevel::Info, "default", "backend returned 0 instead of json (" + std::to_string(ResponseCode) + ")"); - } else { // Rc != "0" - ClientKick(*Client, "Backend returned invalid auth response format."); - beammp_error("Backend returned invalid auth response format. This should never happen."); - auto Lock = Sentry.CreateExclusiveContext(); - Sentry.SetContext("auth", - { { "response-body", Rc }, - { "key", RequestString } }); - Sentry.SetTransaction(Application::GetBackendUrlForAuth() + Target); - Sentry.Log(SentryLevel::Error, "default", "unexpected backend response (" + std::to_string(ResponseCode) + ")"); - } - return; - } - - if (AuthResponse["username"].IsString() && AuthResponse["roles"].IsString() - && AuthResponse["guest"].IsBool() && AuthResponse["identifiers"].IsArray()) { - - Client->SetName(AuthResponse["username"].GetString()); - Client->SetRoles(AuthResponse["roles"].GetString()); - Client->SetIsGuest(AuthResponse["guest"].GetBool()); - for (const auto& ID : AuthResponse["identifiers"].GetArray()) { - auto Raw = std::string(ID.GetString()); - auto SepIndex = Raw.find(':'); - Client->SetIdentifier(Raw.substr(0, SepIndex), Raw.substr(SepIndex + 1)); - } - } else { - ClientKick(*Client, "Invalid authentication data!"); - return; + return nullptr; } beammp_debug("Name -> " + Client->GetName() + ", Guest -> " + std::to_string(Client->IsGuest()) + ", Roles -> " + Client->GetRoles()); @@ -328,15 +290,14 @@ void TNetwork::Authentication(const TConnection& ClientConnection) { return true; } if (Cl->GetName() == Client->GetName() && Cl->IsGuest() == Client->IsGuest()) { - CloseSocketProper(Cl->GetTCPSock()); - Cl->SetStatus(-2); + Cl->Disconnect("Stale Client (not a real player)"); return false; } return true; }); - auto Futures = LuaAPI::MP::Engine->TriggerEvent("onPlayerAuth", "", Client->GetName(), Client->GetRoles(), Client->IsGuest()); + auto Futures = LuaAPI::MP::Engine->TriggerEvent("onPlayerAuth", "", Client->GetName(), Client->GetRoles(), Client->IsGuest(), Client->GetIdentifiers()); TLuaEngine::WaitForAll(Futures); bool NotAllowed = std::any_of(Futures.begin(), Futures.end(), [](const std::shared_ptr& Result) { @@ -354,27 +315,28 @@ void TNetwork::Authentication(const TConnection& ClientConnection) { if (NotAllowed) { ClientKick(*Client, "you are not allowed on the server!"); - return; + return {}; } else if (NotAllowedWithReason) { ClientKick(*Client, Reason); - return; + return {}; } if (mServer.ClientCount() < size_t(Application::Settings.MaxPlayers)) { beammp_info("Identification success"); mServer.InsertClient(Client); TCPClient(Client); - } else + } else { ClientKick(*Client, "Server full!"); + } + return Client; } -std::shared_ptr TNetwork::CreateClient(SOCKET TCPSock) { - auto c = std::make_shared(mServer); - c->SetTCPSock(TCPSock); +std::shared_ptr TNetwork::CreateClient(ip::tcp::socket&& TCPSock) { + auto c = std::make_shared(mServer, std::move(TCPSock)); return c; } -bool TNetwork::TCPSend(TClient& c, const std::string& Data, bool IsSync) { +bool TNetwork::TCPSend(TClient& c, const std::vector& Data, bool IsSync) { if (!IsSync) { if (c.IsSyncing()) { if (!Data.empty()) { @@ -386,120 +348,100 @@ bool TNetwork::TCPSend(TClient& c, const std::string& Data, bool IsSync) { } } - int32_t Size, Sent; - std::string Send(4, 0); - Size = int32_t(Data.size()); - memcpy(&Send[0], &Size, sizeof(Size)); - Send += Data; - Sent = 0; - Size += 4; - do { -#if defined(BEAMMP_WINDOWS) - int32_t Temp = send(c.GetTCPSock(), &Send[Sent], Size - Sent, 0); -#elif defined(BEAMMP_LINUX) || defined(BEAMMP_APPLE) - int32_t Temp = send(c.GetTCPSock(), &Send[Sent], Size - Sent, MSG_NOSIGNAL); -#endif - if (Temp == 0) { - beammp_debug("send() == 0: " + GetPlatformAgnosticErrorString()); - if (c.GetStatus() > -1) - c.SetStatus(-1); - return false; - } else if (Temp < 0) { - beammp_debug("send() < 0: " + GetPlatformAgnosticErrorString()); // TODO fix it was spamming yet everyone stayed on the server - if (c.GetStatus() > -1) - c.SetStatus(-1); - CloseSocketProper(c.GetTCPSock()); - return false; - } - Sent += Temp; - c.UpdatePingTime(); - } while (Sent < Size); + auto& Sock = c.GetTCPSock(); + + /* + * our TCP protocol sends a header of 4 bytes, followed by the data. + * + * [][][][][][]...[] + * ^------^^---...-^ + * size data + */ + + const auto Size = int32_t(Data.size()); + std::vector ToSend; + ToSend.resize(Data.size() + sizeof(Size)); + std::memcpy(ToSend.data(), &Size, sizeof(Size)); + std::memcpy(ToSend.data() + sizeof(Size), Data.data(), Data.size()); + boost::system::error_code ec; + write(Sock, buffer(ToSend), ec); + if (ec) { + beammp_debugf("write(): {}", ec.message()); + c.Disconnect("write() failed"); + return false; + } + c.UpdatePingTime(); return true; } -bool TNetwork::CheckBytes(TClient& c, int32_t BytesRcv) { - if (BytesRcv == 0) { - beammp_trace("(TCP) Connection closing..."); - if (c.GetStatus() > -1) - c.SetStatus(-1); - return false; - } else if (BytesRcv < 0) { - beammp_debug("(TCP) recv() failed: " + GetPlatformAgnosticErrorString()); - if (c.GetStatus() > -1) - c.SetStatus(-1); - CloseSocketProper(c.GetTCPSock()); - return false; +std::vector TNetwork::TCPRcv(TClient& c) { + if (c.IsDisconnected()) { + beammp_error("Client disconnected, cancelling TCPRcv"); + return {}; } - return true; -} -std::string TNetwork::TCPRcv(TClient& c) { - int32_t Header, BytesRcv = 0, Temp; - if (c.GetStatus() < 0) - return ""; + int32_t Header {}; + auto& Sock = c.GetTCPSock(); - std::vector Data(sizeof(Header)); - do { - Temp = recv(c.GetTCPSock(), &Data[BytesRcv], 4 - BytesRcv, 0); - if (!CheckBytes(c, Temp)) { - return ""; - } - BytesRcv += Temp; - } while (size_t(BytesRcv) < sizeof(Header)); - memcpy(&Header, &Data[0], sizeof(Header)); - - if (!CheckBytes(c, BytesRcv)) { - return ""; + boost::system::error_code ec; + std::array HeaderData; + read(Sock, buffer(HeaderData), ec); + if (ec) { + // TODO: handle this case (read failed) + beammp_debugf("TCPRcv: Reading header failed: {}", ec.message()); + return {}; } - if (Header < 100 * MB) { + Header = *reinterpret_cast(HeaderData.data()); + + std::vector Data; + // TODO: This is arbitrary, this needs to be handled another way + if (Header < int32_t(100 * MB)) { Data.resize(Header); } else { ClientKick(c, "Header size limit exceeded"); beammp_warn("Client " + c.GetName() + " (" + std::to_string(c.GetID()) + ") sent header of >100MB - assuming malicious intent and disconnecting the client."); - return ""; + return {}; + } + auto N = read(Sock, buffer(Data), ec); + if (ec) { + // TODO: handle this case properly + beammp_debugf("TCPRcv: Reading data failed: {}", ec.message()); + return {}; } - BytesRcv = 0; - do { - Temp = recv(c.GetTCPSock(), &Data[BytesRcv], Header - BytesRcv, 0); - if (!CheckBytes(c, Temp)) { - return ""; - } - BytesRcv += Temp; - } while (BytesRcv < Header); - std::string Ret(Data.data(), Header); - if (Ret.substr(0, 4) == "ABG:") { - Ret = DeComp(Ret.substr(4)); + if (N != Header) { + beammp_errorf("Expected to read {} bytes, instead got {}", Header, N); + } + + constexpr std::string_view ABG = "ABG:"; + if (Data.size() >= ABG.size() && std::equal(Data.begin(), Data.begin() + ABG.size(), ABG.begin(), ABG.end())) { + Data.erase(Data.begin(), Data.begin() + ABG.size()); + return DeComp(Data); + } else { + return Data; } - return Ret; } void TNetwork::ClientKick(TClient& c, const std::string& R) { beammp_info("Client kicked: " + R); - if (!TCPSend(c, "K" + R)) { - // TODO handle + if (!TCPSend(c, StringToVector("K" + R))) { + beammp_debugf("tried to kick player '{}' (id {}), but was already connected", c.GetName(), c.GetID()); } - c.SetStatus(-2); - - if (c.GetTCPSock()) - CloseSocketProper(c.GetTCPSock()); - - if (c.GetDownSock()) - CloseSocketProper(c.GetDownSock()); + c.Disconnect("Kicked"); } void TNetwork::Looper(const std::weak_ptr& c) { RegisterThreadAuto(); while (!c.expired()) { auto Client = c.lock(); - if (Client->GetStatus() < 0) { - beammp_debug("client status < 0, breaking client loop"); + if (Client->IsDisconnected()) { + beammp_debug("client is disconnected, breaking client loop"); break; } if (!Client->IsSyncing() && Client->IsSynced() && Client->MissedPacketQueueSize() != 0) { // debug("sending " + std::to_string(Client->MissedPacketQueueSize()) + " queued packets"); while (Client->MissedPacketQueueSize() > 0) { - std::string QData {}; + std::vector QData {}; { // locked context std::unique_lock lock(Client->MissedPacketQueueMutex()); if (Client->MissedPacketQueueSize() <= 0) { @@ -510,15 +452,11 @@ void TNetwork::Looper(const std::weak_ptr& c) { } // end locked context // beammp_debug("sending a missed packet: " + QData); if (!TCPSend(*Client, QData, true)) { - if (Client->GetStatus() > -1) - Client->SetStatus(-1); - { - std::unique_lock lock(Client->MissedPacketQueueMutex()); - while (!Client->MissedPacketQueue().empty()) { - Client->MissedPacketQueue().pop(); - } + Client->Disconnect("Failed to TCPSend while clearing the missed packet queue"); + std::unique_lock lock(Client->MissedPacketQueueMutex()); + while (!Client->MissedPacketQueue().empty()) { + Client->MissedPacketQueue().pop(); } - CloseSocketProper(Client->GetTCPSock()); break; } } @@ -530,7 +468,7 @@ void TNetwork::Looper(const std::weak_ptr& c) { void TNetwork::TCPClient(const std::weak_ptr& c) { // TODO: the c.expired() might cause issues here, remove if you end up here with your debugger - if (c.expired() || c.lock()->GetTCPSock() == -1) { + if (c.expired() || !c.lock()->GetTCPSock().is_open()) { mServer.RemoveClient(c); return; } @@ -543,24 +481,26 @@ void TNetwork::TCPClient(const std::weak_ptr& c) { if (c.expired()) break; auto Client = c.lock(); - if (Client->GetStatus() < 0) { + if (Client->IsDisconnected()) { beammp_debug("client status < 0, breaking client loop"); break; } auto res = TCPRcv(*Client); - if (res == "") { - beammp_debug("TCPRcv error, break client loop"); + if (res.empty()) { + beammp_debug("TCPRcv empty"); + Client->Disconnect("TCPRcv failed"); break; } - TServer::GlobalParser(c, res, mPPSMonitor, *this); + TServer::GlobalParser(c, std::move(res), mPPSMonitor, *this); } + if (QueueSync.joinable()) QueueSync.join(); if (!c.expired()) { auto Client = c.lock(); - OnDisconnect(c, Client->GetStatus() == -2); + OnDisconnect(c); } else { beammp_warn("client expired in TCPClient, should never happen"); } @@ -577,11 +517,11 @@ void TNetwork::UpdatePlayer(TClient& Client) { return true; }); Packet = Packet.substr(0, Packet.length() - 1); - Client.EnqueuePacket(Packet); + Client.EnqueuePacket(StringToVector(Packet)); //(void)Respond(Client, Packet, true); } -void TNetwork::OnDisconnect(const std::weak_ptr& ClientPtr, bool kicked) { +void TNetwork::OnDisconnect(const std::weak_ptr& ClientPtr) { beammp_assert(!ClientPtr.expired()); auto LockedClientPtr = ClientPtr.lock(); TClient& c = *LockedClientPtr; @@ -594,20 +534,14 @@ void TNetwork::OnDisconnect(const std::weak_ptr& ClientPtr, bool kicked } // End Vehicle Data Lock Scope for (auto& v : VehicleData) { Packet = "Od:" + std::to_string(c.GetID()) + "-" + std::to_string(v.ID()); - SendToAll(&c, Packet, false, true); + SendToAll(&c, StringToVector(Packet), false, true); } - if (kicked) - Packet = ("L") + c.GetName() + (" was kicked!"); - else - Packet = ("L") + c.GetName() + (" left the server!"); - SendToAll(&c, Packet, false, true); + Packet = ("L") + c.GetName() + (" left the server!"); + SendToAll(&c, StringToVector(Packet), false, true); Packet.clear(); auto Futures = LuaAPI::MP::Engine->TriggerEvent("onPlayerDisconnect", "", c.GetID()); LuaAPI::MP::Engine->ReportErrors(Futures); - if (c.GetTCPSock()) - CloseSocketProper(c.GetTCPSock()); - if (c.GetDownSock()) - CloseSocketProper(c.GetDownSock()); + c.Disconnect("Already Disconnected (OnDisconnect)"); mServer.RemoveClient(ClientPtr); } @@ -639,44 +573,39 @@ void TNetwork::OnConnect(const std::weak_ptr& c) { beammp_info("Assigned ID " + std::to_string(LockedClient->GetID()) + " to " + LockedClient->GetName()); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onPlayerConnecting", "", LockedClient->GetID())); SyncResources(*LockedClient); - if (LockedClient->GetStatus() < 0) + if (LockedClient->IsDisconnected()) return; - (void)Respond(*LockedClient, "M" + Application::Settings.MapName, true); // Send the Map on connect + (void)Respond(*LockedClient, StringToVector("M" + Application::Settings.MapName), true); // Send the Map on connect beammp_info(LockedClient->GetName() + " : Connected"); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onPlayerJoining", "", LockedClient->GetID())); } void TNetwork::SyncResources(TClient& c) { -#ifndef DEBUG - try { -#endif - if (!TCPSend(c, "P" + std::to_string(c.GetID()))) { - // TODO handle - } - std::string Data; - while (c.GetStatus() > -1) { - Data = TCPRcv(c); - if (Data == "Done") - break; - Parse(c, Data); - } -#ifndef DEBUG - } catch (std::exception& e) { - beammp_error("Exception! : " + std::string(e.what())); - c.SetStatus(-1); + if (!TCPSend(c, StringToVector("P" + std::to_string(c.GetID())))) { + // TODO handle + } + std::vector Data; + while (!c.IsDisconnected()) { + Data = TCPRcv(c); + if (Data.empty()) { + break; + } + constexpr std::string_view Done = "Done"; + if (std::equal(Data.begin(), Data.end(), Done.begin(), Done.end())) + break; + Parse(c, Data); } -#endif } -void TNetwork::Parse(TClient& c, const std::string& Packet) { +void TNetwork::Parse(TClient& c, const std::vector& Packet) { if (Packet.empty()) return; char Code = Packet.at(0), SubCode = 0; - if (Packet.length() > 1) + if (Packet.size() > 1) SubCode = Packet.at(1); switch (Code) { case 'f': - SendFile(c, Packet.substr(1)); + SendFile(c, std::string(reinterpret_cast(Packet.data() + 1), Packet.size() - 1)); return; case 'S': if (SubCode == 'R') { @@ -684,7 +613,7 @@ void TNetwork::Parse(TClient& c, const std::string& Packet) { std::string ToSend = mResourceManager.FileList() + mResourceManager.FileSizes(); if (ToSend.empty()) ToSend = "-"; - if (!TCPSend(c, ToSend)) { + if (!TCPSend(c, StringToVector(ToSend))) { // TODO: error } } @@ -698,7 +627,7 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) { beammp_info(c.GetName() + " requesting : " + UnsafeName.substr(UnsafeName.find_last_of('/'))); if (!fs::path(UnsafeName).has_filename()) { - if (!TCPSend(c, "CO")) { + if (!TCPSend(c, StringToVector("CO"))) { // TODO: handle } beammp_warn("File " + UnsafeName + " is not a file!"); @@ -708,28 +637,28 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) { FileName = Application::Settings.Resource + "/Client/" + FileName; if (!std::filesystem::exists(FileName)) { - if (!TCPSend(c, "CO")) { + if (!TCPSend(c, StringToVector("CO"))) { // TODO: handle } beammp_warn("File " + UnsafeName + " could not be accessed!"); return; } - if (!TCPSend(c, "AG")) { + if (!TCPSend(c, StringToVector("AG"))) { // TODO: handle } /// Wait for connections int T = 0; - while (c.GetDownSock() < 1 && T < 50) { + while (!c.GetDownSock().is_open() && T < 50) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); T++; } - if (c.GetDownSock() < 1) { + if (!c.GetDownSock().is_open()) { beammp_error("Client doesn't have a download socket!"); - if (c.GetStatus() > -1) - c.SetStatus(-1); + if (!c.IsDisconnected()) + c.Disconnect("Missing download socket"); return; } @@ -753,37 +682,84 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) { } } +static std::pair SplitIntoChunks(size_t FullSize, size_t ChunkSize) { + if (FullSize < ChunkSize) { + return { 0, FullSize }; + } + size_t Count = FullSize / (FullSize / ChunkSize); + size_t LastChunkSize = FullSize - (Count * ChunkSize); + return { Count, LastChunkSize }; +} + +TEST_CASE("SplitIntoChunks") { + size_t FullSize; + size_t ChunkSize; + SUBCASE("Normal case") { + FullSize = 1234567; + ChunkSize = 1234; + } + SUBCASE("Zero original size") { + FullSize = 0; + ChunkSize = 100; + } + SUBCASE("Equal full size and chunk size") { + FullSize = 125; + ChunkSize = 125; + } + SUBCASE("Even split") { + FullSize = 10000; + ChunkSize = 100; + } + SUBCASE("Odd split") { + FullSize = 13; + ChunkSize = 2; + } + SUBCASE("Large sizes") { + FullSize = 10 * GB; + ChunkSize = 125 * MB; + } + auto [Count, LastSize] = SplitIntoChunks(FullSize, ChunkSize); + CHECK((Count * ChunkSize) + LastSize == FullSize); +} + +const uint8_t* /* end ptr */ TNetwork::SendSplit(TClient& c, ip::tcp::socket& Socket, const uint8_t* DataPtr, size_t Size) { + if (TCPSendRaw(c, Socket, DataPtr, Size)) { + return DataPtr + Size; + } else { + return nullptr; + } +} + void TNetwork::SplitLoad(TClient& c, size_t Sent, size_t Size, bool D, const std::string& Name) { std::ifstream f(Name.c_str(), std::ios::binary); - uint32_t Split = 0x7735940; // 125MB - std::vector Data; + uint32_t Split = 125 * MB; + std::vector Data; if (Size > Split) Data.resize(Split); else Data.resize(Size); - SOCKET TCPSock; + ip::tcp::socket* TCPSock { nullptr }; if (D) - TCPSock = c.GetDownSock(); + TCPSock = &c.GetDownSock(); else - TCPSock = c.GetTCPSock(); - beammp_debug("Split load Socket " + std::to_string(TCPSock)); - while (c.GetStatus() > -1 && Sent < Size) { + TCPSock = &c.GetTCPSock(); + while (!c.IsDisconnected() && Sent < Size) { size_t Diff = Size - Sent; if (Diff > Split) { f.seekg(Sent, std::ios_base::beg); - f.read(Data.data(), Split); - if (!TCPSendRaw(c, TCPSock, Data.data(), Split)) { - if (c.GetStatus() > -1) - c.SetStatus(-1); + f.read(reinterpret_cast(Data.data()), Split); + if (!TCPSendRaw(c, *TCPSock, Data.data(), Split)) { + if (!c.IsDisconnected()) + c.Disconnect("TCPSendRaw failed in mod download (1)"); break; } Sent += Split; } else { f.seekg(Sent, std::ios_base::beg); - f.read(Data.data(), Diff); - if (!TCPSendRaw(c, TCPSock, Data.data(), int32_t(Diff))) { - if (c.GetStatus() > -1) - c.SetStatus(-1); + f.read(reinterpret_cast(Data.data()), Diff); + if (!TCPSendRaw(c, *TCPSock, Data.data(), int32_t(Diff))) { + if (!c.IsDisconnected()) + c.Disconnect("TCPSendRaw failed in mod download (2)"); break; } Sent += Diff; @@ -791,37 +767,28 @@ void TNetwork::SplitLoad(TClient& c, size_t Sent, size_t Size, bool D, const std } } -bool TNetwork::TCPSendRaw(TClient& C, SOCKET socket, char* Data, int32_t Size) { - intmax_t Sent = 0; - do { -#if defined(BEAMMP_LINUX) || defined(BEAMMP_APPLE) - intmax_t Temp = send(socket, &Data[Sent], int(Size - Sent), MSG_NOSIGNAL); -#else - intmax_t Temp = send(socket, &Data[Sent], int(Size - Sent), 0); -#endif - if (Temp < 1) { - beammp_info("Socket Closed! " + std::to_string(socket)); - CloseSocketProper(socket); - return false; - } - Sent += Temp; - C.UpdatePingTime(); - } while (Sent < Size); +bool TNetwork::TCPSendRaw(TClient& C, ip::tcp::socket& socket, const uint8_t* Data, size_t Size) { + boost::system::error_code ec; + write(socket, buffer(Data, Size), ec); + if (ec) { + beammp_errorf("Failed to send raw data to client: {}", ec.message()); + return false; + } + C.UpdatePingTime(); return true; } -bool TNetwork::SendLarge(TClient& c, std::string Data, bool isSync) { - if (Data.length() > 400) { - std::string CMP(Comp(Data)); - Data = "ABG:" + CMP; +bool TNetwork::SendLarge(TClient& c, std::vector Data, bool isSync) { + if (Data.size() > 400) { + CompressProperly(Data); } return TCPSend(c, Data, isSync); } -bool TNetwork::Respond(TClient& c, const std::string& MSG, bool Rel, bool isSync) { +bool TNetwork::Respond(TClient& c, const std::vector& MSG, bool Rel, bool isSync) { char C = MSG.at(0); if (Rel || C == 'W' || C == 'Y' || C == 'V' || C == 'E') { - if (C == 'O' || C == 'T' || MSG.length() > 1000) { + if (C == 'O' || C == 'T' || MSG.size() > 1000) { return SendLarge(c, MSG, isSync); } else { return TCPSend(c, MSG, isSync); @@ -840,11 +807,11 @@ bool TNetwork::SyncClient(const std::weak_ptr& c) { return true; // Syncing, later set isSynced // after syncing is done, we apply all packets they missed - if (!Respond(*LockedClient, ("Sn") + LockedClient->GetName(), true)) { + if (!Respond(*LockedClient, StringToVector("Sn" + LockedClient->GetName()), true)) { return false; } // ignore error - (void)SendToAll(LockedClient.get(), ("JWelcome ") + LockedClient->GetName() + "!", false, true); + (void)SendToAll(LockedClient.get(), StringToVector("JWelcome " + LockedClient->GetName() + "!"), false, true); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onPlayerJoin", "", LockedClient->GetID())); LockedClient->SetIsSyncing(true); @@ -866,12 +833,12 @@ bool TNetwork::SyncClient(const std::weak_ptr& c) { } // End Vehicle Data Lock Scope if (client != LockedClient) { for (auto& v : VehicleData) { - if (LockedClient->GetStatus() < 0) { + if (LockedClient->IsDisconnected()) { Return = true; res = false; return false; } - res = Respond(*LockedClient, v.Data(), true, true); + res = Respond(*LockedClient, StringToVector(v.Data()), true, true); } } @@ -886,7 +853,7 @@ bool TNetwork::SyncClient(const std::weak_ptr& c) { return true; } -void TNetwork::SendToAll(TClient* c, const std::string& Data, bool Self, bool Rel) { +void TNetwork::SendToAll(TClient* c, const std::vector& Data, bool Self, bool Rel) { if (!Self) beammp_assert(c); char C = Data.at(0); @@ -903,10 +870,11 @@ void TNetwork::SendToAll(TClient* c, const std::string& Data, bool Self, bool Re if (Self || Client.get() != c) { if (Client->IsSynced() || Client->IsSyncing()) { if (Rel || C == 'W' || C == 'Y' || C == 'V' || C == 'E') { - if (C == 'O' || C == 'T' || Data.length() > 1000) { - if (Data.length() > 400) { - std::string CMP(Comp(Data)); - Client->EnqueuePacket("ABG:" + CMP); + if (C == 'O' || C == 'T' || Data.size() > 1000) { + if (Data.size() > 400) { + auto CompressedData = Data; + CompressProperly(CompressedData); + Client->EnqueuePacket(CompressedData); } else { Client->EnqueuePacket(Data); } @@ -928,55 +896,37 @@ void TNetwork::SendToAll(TClient* c, const std::string& Data, bool Self, bool Re return; } -bool TNetwork::UDPSend(TClient& Client, std::string Data) const { - if (!Client.IsConnected() || Client.GetStatus() < 0) { +bool TNetwork::UDPSend(TClient& Client, std::vector Data) { + if (!Client.IsConnected() || Client.IsDisconnected()) { // this can happen if we try to send a packet to a client that is either // 1. not yet fully connected, or // 2. disconnected and not yet fully removed // this is fine can can be ignored :^) return true; } - sockaddr_in Addr = Client.GetUDPAddr(); - auto AddrSize = sizeof(Client.GetUDPAddr()); - if (Data.length() > 400) { - std::string CMP(Comp(Data)); - Data = "ABG:" + CMP; + const auto Addr = Client.GetUDPAddr(); + if (Data.size() > 400) { + CompressProperly(Data); } -#ifdef WIN32 - int sendOk; - int len = static_cast(Data.size()); -#else - int64_t sendOk; - size_t len = Data.size(); -#endif // WIN32 - - sendOk = sendto(mUDPSock, Data.c_str(), len, 0, (sockaddr*)&Addr, int(AddrSize)); - if (sendOk == -1) { - beammp_debug("(UDP) sendto() failed: " + GetPlatformAgnosticErrorString()); - if (Client.GetStatus() > -1) - Client.SetStatus(-1); - return false; - } else if (sendOk == 0) { - beammp_debug(("(UDP) sendto() returned 0")); - if (Client.GetStatus() > -1) - Client.SetStatus(-1); + boost::system::error_code ec; + mUDPSock.send_to(buffer(Data), Addr, 0, ec); + if (ec) { + beammp_debugf("UDP sendto() failed: {}", ec.message()); + if (!Client.IsDisconnected()) + Client.Disconnect("UDP send failed"); return false; } return true; } -std::string TNetwork::UDPRcvFromClient(sockaddr_in& client) const { - size_t clientLength = sizeof(client); +std::vector TNetwork::UDPRcvFromClient(ip::udp::endpoint& ClientEndpoint) { std::array Ret {}; -#ifdef WIN32 - auto Rcv = recvfrom(mUDPSock, Ret.data(), int(Ret.size()), 0, (sockaddr*)&client, (int*)&clientLength); -#else // unix - int64_t Rcv = recvfrom(mUDPSock, Ret.data(), Ret.size(), 0, (sockaddr*)&client, (socklen_t*)&clientLength); -#endif // WIN32 - - if (Rcv == -1) { - beammp_error("(UDP) Error receiving from client! recvfrom() failed: " + GetPlatformAgnosticErrorString()); - return ""; + boost::system::error_code ec; + const auto Rcv = mUDPSock.receive_from(mutable_buffer(Ret.data(), Ret.size()), ClientEndpoint, 0, ec); + if (ec) { + beammp_errorf("UDP recvfrom() failed: {}", ec.message()); + return {}; } - return std::string(Ret.begin(), Ret.begin() + Rcv); + beammp_assert(Rcv <= Ret.size()); + return std::vector(Ret.begin(), Ret.begin() + Rcv); } diff --git a/src/TPPSMonitor.cpp b/src/TPPSMonitor.cpp index b5b4951..3edd17d 100644 --- a/src/TPPSMonitor.cpp +++ b/src/TPPSMonitor.cpp @@ -10,7 +10,6 @@ TPPSMonitor::TPPSMonitor(TServer& Server) Application::SetSubsystemStatus("PPSMonitor", Application::Status::ShuttingDown); if (mThread.joinable()) { beammp_debug("shutting down PPSMonitor"); - mShutdown = true; mThread.join(); beammp_debug("shut down PPSMonitor"); } @@ -27,7 +26,7 @@ void TPPSMonitor::operator()() { beammp_debug("PPSMonitor starting"); Application::SetSubsystemStatus("PPSMonitor", Application::Status::Good); std::vector> TimedOutClients; - while (!mShutdown) { + while (!Application::IsShuttingDown()) { std::this_thread::sleep_for(std::chrono::seconds(1)); int C = 0, V = 0; if (mServer.ClientCount() == 0) { diff --git a/src/TPluginMonitor.cpp b/src/TPluginMonitor.cpp new file mode 100644 index 0000000..3332ae1 --- /dev/null +++ b/src/TPluginMonitor.cpp @@ -0,0 +1,75 @@ +#include "TPluginMonitor.h" + +#include "TLuaEngine.h" + +TPluginMonitor::TPluginMonitor(const fs::path& Path, std::shared_ptr Engine) + : mEngine(Engine) + , mPath(Path) { + Application::SetSubsystemStatus("PluginMonitor", Application::Status::Starting); + if (!fs::exists(mPath)) { + fs::create_directories(mPath); + } + for (const auto& Entry : fs::recursive_directory_iterator(mPath)) { + // TODO: trigger an event when a subfolder file changes + if (Entry.is_regular_file()) { + mFileTimes[Entry.path().string()] = fs::last_write_time(Entry.path()); + } + } + + Application::RegisterShutdownHandler([this] { + if (mThread.joinable()) { + mThread.join(); + } + }); + + Start(); +} + +void TPluginMonitor::operator()() { + RegisterThread("PluginMonitor"); + beammp_info("PluginMonitor started"); + Application::SetSubsystemStatus("PluginMonitor", Application::Status::Good); + while (!Application::IsShuttingDown()) { + std::vector ToRemove; + for (const auto& Pair : mFileTimes) { + try { + auto CurrentTime = fs::last_write_time(Pair.first); + if (CurrentTime > Pair.second) { + mFileTimes[Pair.first] = CurrentTime; + // grandparent of the path should be Resources/Server + if (fs::equivalent(fs::path(Pair.first).parent_path().parent_path(), mPath)) { + beammp_infof("File \"{}\" changed, reloading", Pair.first); + // is in root folder, so reload + std::ifstream FileStream(Pair.first, std::ios::in | std::ios::binary); + auto Size = std::filesystem::file_size(Pair.first); + auto Contents = std::make_shared(); + Contents->resize(Size); + FileStream.read(Contents->data(), Contents->size()); + TLuaChunk Chunk(Contents, Pair.first, fs::path(Pair.first).parent_path().string()); + auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path()); + auto Res = mEngine->EnqueueScript(StateID, Chunk); + Res->WaitUntilReady(); + if (Res->Error) { + beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Res->ErrorMessage); + } else { + mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit")); + mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first)); + } + } else { + // is in subfolder, dont reload, just trigger an event + beammp_debugf("File \"{}\" changed, not reloading because it's in a subdirectory. Triggering 'onFileChanged' event instead", Pair.first); + mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first)); + } + } + } catch (const std::exception& e) { + ToRemove.push_back(Pair.first); + } + } + Application::SleepSafeSeconds(3); + for (const auto& File : ToRemove) { + mFileTimes.erase(File); + beammp_warnf("File \"{}\" couldn't be accessed, so it was removed from plugin hot reload monitor (probably got deleted)", File); + } + } + Application::SetSubsystemStatus("PluginMonitor", Application::Status::Shutdown); +} diff --git a/src/TResourceManager.cpp b/src/TResourceManager.cpp index afb0e6b..e12ef17 100644 --- a/src/TResourceManager.cpp +++ b/src/TResourceManager.cpp @@ -28,8 +28,9 @@ TResourceManager::TResourceManager() { } } - if (mModsLoaded) + if (mModsLoaded) { beammp_info("Loaded " + std::to_string(mModsLoaded) + " Mods"); + } Application::SetSubsystemStatus("ResourceManager", Application::Status::Good); } diff --git a/src/TSentry.cpp b/src/TSentry.cpp index da05c10..ceab947 100644 --- a/src/TSentry.cpp +++ b/src/TSentry.cpp @@ -6,7 +6,7 @@ #include TSentry::TSentry() { - if (std::strlen(S_DSN) == 0) { + if (std::strlen(S_DSN) == /* DISABLES CODE */ (0)) { mValid = false; } else { mValid = true; @@ -72,7 +72,7 @@ void TSentry::Log(SentryLevel level, const std::string& logger, const std::strin SetContext("threads", { { "thread-name", ThreadName(true) } }); auto Msg = sentry_value_new_message_event(sentry_level_t(level), logger.c_str(), text.c_str()); sentry_capture_event(Msg); - sentry_remove_transaction(); + sentry_set_transaction(nullptr); } void TSentry::LogError(const std::string& text, const std::string& file, const std::string& line) { diff --git a/src/TServer.cpp b/src/TServer.cpp index cee40b3..f160e6a 100644 --- a/src/TServer.cpp +++ b/src/TServer.cpp @@ -4,6 +4,7 @@ #include "TNetwork.h" #include "TPPSMonitor.h" #include +#include #include #include @@ -15,6 +16,66 @@ #include "Json.h" +static std::optional> GetPidVid(const std::string& str) { + auto IDSep = str.find('-'); + std::string pid = str.substr(0, IDSep); + std::string vid = str.substr(IDSep + 1); + + if (pid.find_first_not_of("0123456789") == std::string::npos && vid.find_first_not_of("0123456789") == std::string::npos) { + try { + int PID = stoi(pid); + int VID = stoi(vid); + return { { PID, VID } }; + } catch (const std::exception&) { + return std::nullopt; + } + } + return std::nullopt; +} + +TEST_CASE("GetPidVid") { + SUBCASE("Valid singledigit") { + const auto MaybePidVid = GetPidVid("0-1"); + CHECK(MaybePidVid); + auto [pid, vid] = MaybePidVid.value(); + + CHECK_EQ(pid, 0); + CHECK_EQ(vid, 1); + } + SUBCASE("Valid doubledigit") { + const auto MaybePidVid = GetPidVid("10-12"); + CHECK(MaybePidVid); + auto [pid, vid] = MaybePidVid.value(); + + CHECK_EQ(pid, 10); + CHECK_EQ(vid, 12); + } + SUBCASE("Empty string") { + const auto MaybePidVid = GetPidVid(""); + CHECK(!MaybePidVid); + } + SUBCASE("Invalid separator") { + const auto MaybePidVid = GetPidVid("0x0"); + CHECK(!MaybePidVid); + } + SUBCASE("Missing pid") { + const auto MaybePidVid = GetPidVid("-0"); + CHECK(!MaybePidVid); + } + SUBCASE("Missing vid") { + const auto MaybePidVid = GetPidVid("0-"); + CHECK(!MaybePidVid); + } + SUBCASE("Invalid pid") { + const auto MaybePidVid = GetPidVid("x-0"); + CHECK(!MaybePidVid); + } + SUBCASE("Invalid vid") { + const auto MaybePidVid = GetPidVid("0-x"); + CHECK(!MaybePidVid); + } +} + TServer::TServer(const std::vector& Arguments) { beammp_info("BeamMP Server v" + Application::ServerVersionString()); Application::SetSubsystemStatus("Server", Application::Status::Starting); @@ -42,13 +103,6 @@ void TServer::RemoveClient(const std::weak_ptr& WeakClientPtr) { } } -std::weak_ptr TServer::InsertNewClient() { - beammp_debug("inserting new client (" + std::to_string(ClientCount()) + ")"); - WriteLock Lock(mClientsMutex); - auto [Iter, Replaced] = mClients.insert(std::make_shared(*this)); - return *Iter; -} - void TServer::ForEachClient(const std::function)>& Fn) { decltype(mClients) Clients; { @@ -67,12 +121,11 @@ size_t TServer::ClientCount() const { return mClients.size(); } -void TServer::GlobalParser(const std::weak_ptr& Client, std::string Packet, TPPSMonitor& PPSMonitor, TNetwork& Network) { - if (Packet.find("Zp") != std::string::npos && Packet.size() > 500) { - // abort(); - } - if (Packet.substr(0, 4) == "ABG:") { - Packet = DeComp(Packet.substr(4)); +void TServer::GlobalParser(const std::weak_ptr& Client, std::vector&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network) { + constexpr std::string_view ABG = "ABG:"; + if (Packet.size() >= ABG.size() && std::equal(Packet.begin(), Packet.begin() + ABG.size(), ABG.begin(), ABG.end())) { + Packet.erase(Packet.begin(), Packet.begin() + ABG.size()); + Packet = DeComp(Packet); } if (Packet.empty()) { return; @@ -86,46 +139,44 @@ void TServer::GlobalParser(const std::weak_ptr& Client, std::string Pac std::any Res; char Code = Packet.at(0); - // V to Z - if (Code <= 90 && Code >= 86) { + std::string StringPacket(reinterpret_cast(Packet.data()), Packet.size()); + + // V to Y + if (Code <= 89 && Code >= 86) { PPSMonitor.IncrementInternalPPS(); Network.SendToAll(LockedClient.get(), Packet, false, false); return; } switch (Code) { case 'H': // initial connection - beammp_trace(std::string("got 'H' packet: '") + Packet + "' (" + std::to_string(Packet.size()) + ")"); if (!Network.SyncClient(Client)) { // TODO handle } return; case 'p': - if (!Network.Respond(*LockedClient, ("p"), false)) { + if (!Network.Respond(*LockedClient, StringToVector("p"), false)) { // failed to send - if (LockedClient->GetStatus() > -1) { - LockedClient->SetStatus(-1); - } + LockedClient->Disconnect("Failed to send ping"); } else { Network.UpdatePlayer(*LockedClient); } return; case 'O': - if (Packet.length() > 1000) { - beammp_debug(("Received data from: ") + LockedClient->GetName() + (" Size: ") + std::to_string(Packet.length())); + if (Packet.size() > 1000) { + beammp_debug(("Received data from: ") + LockedClient->GetName() + (" Size: ") + std::to_string(Packet.size())); } - ParseVehicle(*LockedClient, Packet, Network); + ParseVehicle(*LockedClient, StringPacket, Network); return; case 'J': - beammp_trace(std::string(("got 'J' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); Network.SendToAll(LockedClient.get(), Packet, false, true); return; case 'C': { - beammp_trace(std::string(("got 'C' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); - if (Packet.length() < 4 || Packet.find(':', 3) == std::string::npos) + if (Packet.size() < 4 || std::find(Packet.begin() + 3, Packet.end(), ':') == Packet.end()) break; - auto Futures = LuaAPI::MP::Engine->TriggerEvent("onChatMessage", "", LockedClient->GetID(), LockedClient->GetName(), Packet.substr(Packet.find(':', 3) + 2)); + const auto PacketAsString = std::string(reinterpret_cast(Packet.data()), Packet.size()); + auto Futures = LuaAPI::MP::Engine->TriggerEvent("onChatMessage", "", LockedClient->GetID(), LockedClient->GetName(), PacketAsString.substr(PacketAsString.find(':', 3) + 2)); TLuaEngine::WaitForAll(Futures); - LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), Packet.substr(Packet.find(':', 3) + 1)); // FIXME: this needs to be adjusted once lua is merged + LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), PacketAsString.substr(PacketAsString.find(':', 3) + 1)); if (std::any_of(Futures.begin(), Futures.end(), [](const std::shared_ptr& Elem) { return !Elem->Error @@ -138,38 +189,34 @@ void TServer::GlobalParser(const std::weak_ptr& Client, std::string Pac return; } case 'E': - beammp_trace(std::string(("got 'E' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); - HandleEvent(*LockedClient, Packet); + HandleEvent(*LockedClient, StringPacket); return; case 'N': beammp_trace("got 'N' packet (" + std::to_string(Packet.size()) + ")"); Network.SendToAll(LockedClient.get(), Packet, false, true); return; + case 'Z': // position packet + PPSMonitor.IncrementInternalPPS(); + Network.SendToAll(LockedClient.get(), Packet, false, false); + + HandlePosition(*LockedClient, StringPacket); default: return; } } -void TServer::HandleEvent(TClient& c, const std::string& Data) { - std::stringstream ss(Data); - std::string t, Name; - int a = 0; - while (std::getline(ss, t, ':')) { - switch (a) { - case 1: - Name = t; - break; - case 2: - LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent(Name, "", c.GetID(), t)); - break; - default: - break; - } - if (a == 2) - break; - a++; +void TServer::HandleEvent(TClient& c, const std::string& RawData) { + // E:Name:Data + // Data is allowed to have ':' + auto NameDataSep = RawData.find(':', 2); + if (NameDataSep == std::string::npos) { + beammp_warn("received event in invalid format (missing ':'), got: '" + RawData + "'"); } + std::string Name = RawData.substr(2, NameDataSep - 2); + std::string Data = RawData.substr(NameDataSep + 1); + LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent(Name, "", c.GetID(), Data)); } + bool TServer::IsUnicycle(TClient& c, const std::string& CarJson) { try { auto Car = nlohmann::json::parse(CarJson); @@ -178,7 +225,7 @@ bool TServer::IsUnicycle(TClient& c, const std::string& CarJson) { return true; } } catch (const std::exception& e) { - beammp_error("Failed to parse vehicle data as json for client " + std::to_string(c.GetID()) + ": '" + CarJson + "'"); + beammp_warn("Failed to parse vehicle data as json for client " + std::to_string(c.GetID()) + ": '" + CarJson + "'."); } return false; } @@ -193,19 +240,19 @@ bool TServer::ShouldSpawn(TClient& c, const std::string& CarJson, int ID) { } void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network) { - if (Pckt.length() < 4) + if (Pckt.length() < 6) return; std::string Packet = Pckt; char Code = Packet.at(1); int PID = -1; - int VID = -1, Pos; + int VID = -1; std::string Data = Packet.substr(3), pid, vid; switch (Code) { // Spawned Destroyed Switched/Moved NotFound Reset case 's': - beammp_trace(std::string(("got 'Os' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); + beammp_tracef("got 'Os' packet: '{}' ({})", Packet, Packet.size()); if (Data.at(0) == '0') { int CarID = c.GetOpenCarID(); - beammp_debug(c.GetName() + (" created a car with ID ") + std::to_string(CarID)); + beammp_debugf("'{}' created a car with ID {}", c.GetName(), CarID); std::string CarJson = Packet.substr(5); Packet = "Os:" + c.GetRoles() + ":" + c.GetName() + ":" + std::to_string(c.GetID()) + "-" + std::to_string(CarID) + ":" + CarJson; @@ -218,26 +265,24 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ if (ShouldSpawn(c, CarJson, CarID) && !ShouldntSpawn) { c.AddNewCar(CarID, Packet); - Network.SendToAll(nullptr, Packet, true, true); + Network.SendToAll(nullptr, StringToVector(Packet), true, true); } else { - if (!Network.Respond(c, Packet, true)) { + if (!Network.Respond(c, StringToVector(Packet), true)) { // TODO: handle } std::string Destroy = "Od:" + std::to_string(c.GetID()) + "-" + std::to_string(CarID); - if (!Network.Respond(c, Destroy, true)) { + if (!Network.Respond(c, StringToVector(Destroy), true)) { // TODO: handle } - beammp_debug(c.GetName() + (" (force : car limit/lua) removed ID ") + std::to_string(CarID)); + beammp_debugf("{} (force : car limit/lua) removed ID {}", c.GetName(), CarID); } } return; - case 'c': + case 'c': { beammp_trace(std::string(("got 'Oc' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); - pid = Data.substr(0, Data.find('-')); - vid = Data.substr(Data.find('-') + 1, Data.find(':', 1) - Data.find('-') - 1); - if (pid.find_first_not_of("0123456789") == std::string::npos && vid.find_first_not_of("0123456789") == std::string::npos) { - PID = stoi(pid); - VID = stoi(vid); + auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1))); + if (MaybePidVid) { + std::tie(PID, VID) = MaybePidVid.value(); } if (PID != -1 && VID != -1 && PID == c.GetID()) { auto Futures = LuaAPI::MP::Engine->TriggerEvent("onVehicleEdited", "", c.GetID(), VID, Packet.substr(3)); @@ -251,62 +296,57 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ FoundPos = FoundPos == std::string::npos ? 0 : FoundPos; // attempt at sanitizing this if ((c.GetUnicycleID() != VID || IsUnicycle(c, Packet.substr(FoundPos))) && !ShouldntAllow) { - Network.SendToAll(&c, Packet, false, true); + Network.SendToAll(&c, StringToVector(Packet), false, true); Apply(c, VID, Packet); } else { if (c.GetUnicycleID() == VID) { c.SetUnicycleID(-1); } std::string Destroy = "Od:" + std::to_string(c.GetID()) + "-" + std::to_string(VID); - if (!Network.Respond(c, Destroy, true)) { - // TODO: handle - } + Network.SendToAll(nullptr, StringToVector(Destroy), true, true); c.DeleteCar(VID); } } return; - case 'd': + } + case 'd': { beammp_trace(std::string(("got 'Od' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); - pid = Data.substr(0, Data.find('-')); - vid = Data.substr(Data.find('-') + 1); - if (pid.find_first_not_of("0123456789") == std::string::npos && vid.find_first_not_of("0123456789") == std::string::npos) { - PID = stoi(pid); - VID = stoi(vid); + auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1))); + if (MaybePidVid) { + std::tie(PID, VID) = MaybePidVid.value(); } if (PID != -1 && VID != -1 && PID == c.GetID()) { if (c.GetUnicycleID() == VID) { c.SetUnicycleID(-1); } - Network.SendToAll(nullptr, Packet, true, true); + Network.SendToAll(nullptr, StringToVector(Packet), true, true); // TODO: should this trigger on all vehicle deletions? LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", c.GetID(), VID)); c.DeleteCar(VID); beammp_debug(c.GetName() + (" deleted car with ID ") + std::to_string(VID)); } return; - case 'r': + } + case 'r': { beammp_trace(std::string(("got 'Or' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); - Pos = int(Data.find('-')); - pid = Data.substr(0, Pos++); - vid = Data.substr(Pos, Data.find(':') - Pos); - - if (pid.find_first_not_of("0123456789") == std::string::npos && vid.find_first_not_of("0123456789") == std::string::npos) { - PID = stoi(pid); - VID = stoi(vid); + auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1))); + if (MaybePidVid) { + std::tie(PID, VID) = MaybePidVid.value(); } if (PID != -1 && VID != -1 && PID == c.GetID()) { Data = Data.substr(Data.find('{')); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleReset", "", c.GetID(), VID, Data)); - Network.SendToAll(&c, Packet, false, true); + Network.SendToAll(&c, StringToVector(Packet), false, true); } return; + } case 't': beammp_trace(std::string(("got 'Ot' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); - Network.SendToAll(&c, Packet, false, true); + Network.SendToAll(&c, StringToVector(Packet), false, true); return; case 'm': - Network.SendToAll(&c, Packet, true, true); + Network.SendToAll(&c, StringToVector(Packet), true, true); return; default: beammp_trace(std::string(("possibly not implemented: '") + Packet + ("' (") + std::to_string(Packet.size()) + (")"))); @@ -373,3 +413,36 @@ void TServer::InsertClient(const std::shared_ptr& NewClient) { WriteLock Lock(mClientsMutex); // TODO why is there 30+ threads locked here (void)mClients.insert(NewClient); } + +void TServer::HandlePosition(TClient& c, const std::string& Packet) { + if (Packet.size() < 3) { + // invalid packet + return; + } + // Zp:serverVehicleID:data + // Zp:0:data + std::string withoutCode = Packet.substr(3); + auto NameDataSep = withoutCode.find(':', 2); + if (NameDataSep == std::string::npos || NameDataSep < 2) { + // invalid packet + return; + } + // FIXME: ensure that -2 does what it should... it seems weird. + std::string ServerVehicleID = withoutCode.substr(2, NameDataSep - 2); + if (NameDataSep + 1 > withoutCode.size()) { + // invalid packet + return; + } + std::string Data = withoutCode.substr(NameDataSep + 1); + + // parse veh ID + auto MaybePidVid = GetPidVid(ServerVehicleID); + if (MaybePidVid) { + int PID = -1; + int VID = -1; + // FIXME: check that the VID and PID are valid, so that we don't waste memory + std::tie(PID, VID) = MaybePidVid.value(); + + c.SetCarPosition(VID, Data); + } +} diff --git a/src/main.cpp b/src/main.cpp index e220636..54f0f62 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,7 +2,6 @@ #include "ArgsParser.h" #include "Common.h" -#include "CustomAssert.h" #include "Http.h" #include "LuaAPI.h" #include "SignalHandling.h" @@ -11,13 +10,13 @@ #include "TLuaEngine.h" #include "TNetwork.h" #include "TPPSMonitor.h" +#include "TPluginMonitor.h" #include "TResourceManager.h" -#include "TScopedTimer.h" #include "TServer.h" #include #include -#define CPPHTTPLIB_OPENSSL_SUPPORT 1 + static const std::string sCommandlineArguments = R"( USAGE: BeamMP-Server [arguments] @@ -44,10 +43,6 @@ EXAMPLES: 'MyWestCoastServerConfig.toml'. )"; -// this is provided by the build system, leave empty for source builds -// global, yes, this is ugly, no, it cant be done another way -TSentry Sentry {}; - struct MainArguments { int argc {}; char** argv {}; @@ -59,7 +54,7 @@ int BeamMPServerMain(MainArguments Arguments); int main(int argc, char** argv) { MainArguments Args { argc, argv, {}, argv[0] }; - Args.List.reserve(argc); + Args.List.reserve(size_t(argc)); for (int i = 1; i < argc; ++i) { Args.List.push_back(argv[i]); } @@ -72,7 +67,7 @@ int main(int argc, char** argv) { Sentry.LogException(e, _file_basename, _line); MainRet = -1; } - return MainRet; + std::exit(MainRet); } int BeamMPServerMain(MainArguments Arguments) { @@ -113,7 +108,7 @@ int BeamMPServerMain(MainArguments Arguments) { try { fs::current_path(fs::path(MaybeWorkingDirectory.value())); } catch (const std::exception& e) { - beammp_error("Could not set working directory to '" + MaybeWorkingDirectory.value() + "': " + e.what()); + beammp_errorf("Could not set working directory to '{}': {}", MaybeWorkingDirectory.value(), e.what()); } } } @@ -137,9 +132,9 @@ int BeamMPServerMain(MainArguments Arguments) { TServer Server(Arguments.List); TConfig Config(ConfigPath); - TLuaEngine LuaEngine; - LuaEngine.SetServer(&Server); - Application::Console().InitializeLuaConsole(LuaEngine); + auto LuaEngine = std::make_shared(); + LuaEngine->SetServer(&Server); + Application::Console().InitializeLuaConsole(*LuaEngine); if (Config.Failed()) { beammp_info("Closing in 10 seconds"); @@ -159,12 +154,13 @@ int BeamMPServerMain(MainArguments Arguments) { TPPSMonitor PPSMonitor(Server); THeartbeatThread Heartbeat(ResourceManager, Server); TNetwork Network(Server, PPSMonitor, ResourceManager); - LuaEngine.SetNetwork(&Network); + LuaEngine->SetNetwork(&Network); PPSMonitor.SetNetwork(Network); Application::CheckForUpdates(); + TPluginMonitor PluginMonitor(fs::path(Application::Settings.Resource) / "Server", LuaEngine); + if (Application::Settings.HTTPServerEnabled) { - Http::Server::SetupEnvironment(); Http::Server::THttpServerInstance HttpServerInstance {}; } diff --git a/test/test_main.cpp b/test/test_main.cpp new file mode 100644 index 0000000..01af393 --- /dev/null +++ b/test/test_main.cpp @@ -0,0 +1,22 @@ +#define DOCTEST_CONFIG_IMPLEMENT +#include + +#include + +int main(int argc, char** argv) { + doctest::Context context; + + // Application::InitializeConsole(); + + context.applyCommandLine(argc, argv); + + int res = context.run(); // run + + if (context.shouldExit()) // important - query flags (and --exit) rely on the user doing this + return res; // propagate the result of the tests + + int client_stuff_return_code = 0; + // your program - if the testing framework is integrated in your production code + + return res + client_stuff_return_code; // the result from doctest is propagated here as well +}