diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..040ecfaf8 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(target_env = "msvc")'] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5b4aa4bdb..95482ba6a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,10 +25,9 @@ jobs: cmake-path: /usr/bin/ cmake-args: -G Ninja -DTEST_MYSQL=ON cmake-init-env: CXXFLAGS=-Werror + gtest-env: GTEST_FILTER=-*SQLite* package-file: "*-linux_x86_64.tar.xz" fancy: false - env: - GTEST_FILTER: -*SQLite* - os: macOS-latest cmake-args: -G Ninja cmake-init-env: CXXFLAGS=-Werror @@ -55,6 +54,7 @@ jobs: - name: Prepare Linux (non-fancy) if: ${{ contains(matrix.os, 'ubuntu') && !matrix.fancy }} run: | + rustup default 1.48.0 sudo rm -rf /var/lib/mysql/ /var/run/mysqld sudo mkdir /var/lib/mysql/ /var/run/mysqld/ sudo chown mysql:mysql /var/lib/mysql/ /var/run/mysqld/ @@ -93,7 +93,6 @@ jobs: sudo rm -rf /Library/Developer/CommandLineTools - name: Build in debug mode - env: ${{ matrix.env }} run: | mkdir debug cd debug @@ -101,19 +100,16 @@ jobs: ${{ matrix.cmake-path }}cmake --build . --config Debug --target everything ${{ matrix.build-args }} - name: Test debug - env: ${{ matrix.env }} run: | cd debug - ${{ matrix.cmake-path }}cmake --build . --config Debug --target run_tests ${{ matrix.build-args }} + ${{ matrix.cmake-path }}cmake -E env ${{ matrix.gtest-env }} ${{ matrix.cmake-path }}cmake --build . --config Debug --target run_tests ${{ matrix.build-args }} - name: Run debug server - env: ${{ matrix.env }} run: | cd debug ./DDNet-Server shutdown - name: Build in release mode - env: ${{ matrix.env }} run: | mkdir release cd release @@ -121,20 +117,17 @@ jobs: ${{ matrix.cmake-path }}cmake --build . --config Release --target everything ${{ matrix.build-args }} - name: Test release - env: ${{ matrix.env }} run: | cd release - ${{ matrix.cmake-path }}cmake --build . --config Release --target run_tests ${{ matrix.build-args }} + ${{ matrix.cmake-path }}cmake -E env ${{ matrix.gtest-env }} ${{ matrix.cmake-path }}cmake --build . --config Release --target run_tests ${{ matrix.build-args }} - name: Run release server - env: ${{ matrix.env }} run: | cd release ./DDNet-Server shutdown - name: Build headless client if: contains(matrix.os, 'ubuntu-latest') - env: ${{ matrix.env }} run: | mkdir headless cd headless @@ -178,7 +171,6 @@ jobs: - name: Build in release mode with debug info and all features on if: matrix.fancy - env: ${{ matrix.env }} run: | mkdir fancy cd fancy @@ -187,14 +179,13 @@ jobs: - name: Test fancy if: matrix.fancy - env: ${{ matrix.env }} run: | + find /usr/lib/ -name '*libwebsockets*' cd fancy - ${{ matrix.cmake-path }}cmake --build . --config RelWithDebInfo --target run_tests ${{ matrix.build-args }} + ${{ matrix.cmake-path }}cmake -E env ${{ matrix.gtest-env }} ${{ matrix.cmake-path }}cmake --build . --config RelWithDebInfo --target run_tests ${{ matrix.build-args }} - name: Run fancy server if: matrix.fancy - env: ${{ matrix.env }} run: | cd fancy ./DDNet-Server shutdown diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 000000000..85af18488 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,42 @@ +name: Check Rust + +on: + push: + branches-ignore: + - staging.tmp + - trying.tmp + - staging-squash-merge.tmp + pull_request: + +jobs: + rustdoc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run Rustdoc + run: | + RUSTDOCFLAGS=-Dwarnings DDNET_TEST_NO_LINK=1 cargo doc + + rustfmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run Rustfmt + run: + cargo fmt -- --check + + cargo-deny: + runs-on: ubuntu-latest + strategy: + matrix: + checks: + - advisories + - bans licenses sources + + continue-on-error: ${{ matrix.checks == 'advisories' }} + + steps: + - uses: actions/checkout@v2 + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check ${{ matrix.checks }} diff --git a/.gitignore b/.gitignore index 51c352cf4..fcec9b1f6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ bundle/ .DS_Store .ninja_deps .ninja_log +CACHEDIR.TAG CMakeCache.txt CMakeFiles CMakeSettings* @@ -20,10 +21,12 @@ CTestTestfile.cmake Debug Makefile Release +SAN.* _CPack_Packages/ build.ninja checksummed_* cmake_install.cmake +debug gmock.pc gmock_main.pc googletest-build/ @@ -34,8 +37,8 @@ gtest_main.pc install_manifest*.txt ninja_package pack_*/ +release rules.ninja -SAN.* testrunner\[1\]_include.cmake vulkan_shaders_sha256.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 05d78ebb3..171282781 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -547,6 +547,7 @@ find_package(Opus) find_package(Opusfile) find_package(PNG) find_package(PythonInterp 3) +find_package(Rust) find_package(SDL2) find_package(SQLite3) if(DISCORD) @@ -590,6 +591,7 @@ if(TARGET_OS STREQUAL "mac") endif() message(STATUS ${TARGET}) message(STATUS "Compiler: ${CMAKE_CXX_COMPILER}") +message(STATUS "Rust version: ${RUST_VERSION_STRING}") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") message(STATUS "Dependencies:") @@ -654,6 +656,9 @@ endif() if(NOT(PYTHONINTERP_FOUND)) message(SEND_ERROR "You must install Python to compile ${CMAKE_PROJECT_NAME}") endif() +if(NOT(RUST_FOUND)) + message(SEND_ERROR "You must install Rust and Cargo to compile ${CMAKE_PROJECT_NAME}") +endif() if(NOT(SQLite3_FOUND)) message(SEND_ERROR "You must install SQLite3 to compile ${CMAKE_PROJECT_NAME}") endif() @@ -721,7 +726,12 @@ endif() if(TARGET_OS STREQUAL "windows") set(PLATFORM_CLIENT) set(PLATFORM_CLIENT_LIBS opengl32 winmm) - set(PLATFORM_LIBS shlwapi version ws2_32) # Windows sockets + set(PLATFORM_LIBS) + list(APPEND PLATFORM_LIBS shlwapi) # PathIsRelativeW + list(APPEND PLATFORM_LIBS version ws2_32) # Windows sockets + list(APPEND PLATFORM_LIBS bcrypt userenv) # for Rust (https://github.com/rust-lang/rust/issues/91974) + list(APPEND PLATFORM_LIBS ole32) # CoInitialize(Ex) + list(APPEND PLATFORM_LIBS shell32) elseif(TARGET_OS STREQUAL "mac") find_library(CARBON Carbon) find_library(COCOA Cocoa) @@ -753,7 +763,9 @@ else() set(PLATFORM_CLIENT_INCLUDE_DIRS ${OPENGL_INCLUDE_DIR} ${NOTIFY_INCLUDE_DIRS}) set(PLATFORM_CLIENT) if(TARGET_OS STREQUAL "linux") - set(PLATFORM_LIBS rt) # clock_gettime for glibc < 2.17 + set(PLATFORM_LIBS) + list(APPEND PLATFORM_LIBS rt) # clock_gettime for glibc < 2.17 + list(APPEND PLATFORM_LIBS dl) # for Rust else() set(PLATFORM_LIBS) endif() @@ -834,6 +846,161 @@ if(NOT CRYPTO_FOUND) set(DEP_MD5 $) endif() +######################################################################## +# RUST +######################################################################## + +set_glob(RUST_BASE GLOB_RECURSE "rs;toml" src/base + Cargo.toml + color.rs + lib.rs + rust.rs +) + +set_glob(RUST_ENGINE_INTERFACE GLOB "rs;toml" src/engine + Cargo.toml + console.rs + lib.rs +) + +set_glob(RUST_ENGINE_SHARED GLOB_RECURSE "rs;toml" src/engine/shared + Cargo.toml + build.rs + config.rs + lib.rs + rust_version.rs +) + +set_src(RUST_BRIDGE_SHARED GLOB_RECURSE src/rust-bridge + cpp/console.cpp + cpp/console.h + engine/shared/rust_version.cpp + engine/shared/rust_version.h +) + +set_glob(RUST_MASTERSRV GLOB "rs;toml" src/mastersrv/src + addr.rs + locations.rs + main.rs +) + +add_library(rust-bridge-shared EXCLUDE_FROM_ALL OBJECT ${RUST_BRIDGE_SHARED}) +list(APPEND TARGETS_OWN rust-bridge-shared) + +set(CARGO_BUILD_DIR "") +set(CARGO_BUILD ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${PROJECT_BINARY_DIR} DDNET_TEST_NO_LINK=1 ${RUST_CARGO} build) +set(CARGO_TEST ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${PROJECT_BINARY_DIR} ${RUST_CARGO} test) +if(MSVC) + list(INSERT CARGO_BUILD 0 ${CMAKE_COMMAND} -E env $<$:CFLAGS=/MTd> $<$:CXXFLAGS=/MTd>) + list(INSERT CARGO_TEST 0 ${CMAKE_COMMAND} -E env RUSTFLAGS=-Ctarget-feature=+crt-static) +endif() +if(RUST_NIGHTLY) + list(APPEND CARGO_BUILD -Z build-std=std,panic_abort) +endif() +if(NOT CMAKE_OSX_ARCHITECTURES AND (DEFINED CMAKE_RUST_COMPILER_TARGET OR RUST_NIGHTLY)) + if(DEFINED CMAKE_RUST_COMPILER_TARGET) + list(APPEND CARGO_TEST --target ${CMAKE_RUST_COMPILER_TARGET}) + set(RUST_TARGET ${CMAKE_RUST_COMPILER_TARGET}) + else() + set(RUST_TARGET ${RUST_TARGET_HOST}) + endif() + list(APPEND CARGO_BUILD --target ${RUST_TARGET}) + set(CARGO_BUILD_DIR "${RUST_TARGET}/") +endif() +set(CARGO_BUILD_DIR_DEBUG "${CARGO_BUILD_DIR}debug") +set(CARGO_BUILD_DIR_RELEASE "${CARGO_BUILD_DIR}release") +if(GENERATOR_IS_MULTI_CONFIG) + if(CMAKE_VERSION VERSION_LESS 3.20) + message(SEND_ERROR "Multi-config generators only supported from CMake 3.20 and up") + else() + set(CARGO_BUILD_DIR "${CARGO_BUILD_DIR}$<$:debug>$<$>:release>") + endif() +else() + if(CMAKE_BUILD_TYPE STREQUAL Debug) + set(CARGO_BUILD_DIR "${CARGO_BUILD_DIR_DEBUG}") + else() + set(CARGO_BUILD_DIR "${CARGO_BUILD_DIR_RELEASE}") + endif() +endif() +list(APPEND CARGO_BUILD $<$>:--release>) + +if(CMAKE_OSX_ARCHITECTURES) + set(RUST_OSX_ARCHITECTURES) + foreach(arch ${CMAKE_OSX_ARCHITECTURES}) + if(${arch} STREQUAL arm64) + list(APPEND RUST_OSX_ARCHITECTURES aarch64-apple-darwin) + elseif(${arch} STREQUAL x86_64) + list(APPEND RUST_OSX_ARCHITECTURES x86_64-apple-darwin) + else() + message(SEND_ERROR "CMAKE_OSX_ARCHITECTURES' architecture ${arch} unknown, can't build Rust code (known: arm64, x86_64)") + endif() + endforeach() +endif() + +set(RUST_SRC + ${RUST_BASE} + ${RUST_ENGINE_INTERFACE} + ${RUST_ENGINE_SHARED} + Cargo.toml + Cargo.lock +) +set(RUST_TARGETS engine_shared) +if(NOT CMAKE_OSX_ARCHITECTURES) + set(RUST_OUTPUTS) + foreach(rust_target ${RUST_TARGETS}) + set(LIBRARY_NAME "${CMAKE_STATIC_LIBRARY_PREFIX}ddnet_${rust_target}${CMAKE_STATIC_LIBRARY_SUFFIX}") + add_library(rust_${rust_target} STATIC IMPORTED GLOBAL) + add_custom_target(rust_${rust_target}_target DEPENDS "${PROJECT_BINARY_DIR}/${CARGO_BUILD_DIR}/${LIBRARY_NAME}") + add_dependencies(rust_${rust_target} rust_${rust_target}_target) + set_target_properties(rust_${rust_target} PROPERTIES + IMPORTED_LOCATION "${PROJECT_BINARY_DIR}/${CARGO_BUILD_DIR_RELEASE}/${LIBRARY_NAME}" + IMPORTED_LOCATION_DEBUG "${PROJECT_BINARY_DIR}/${CARGO_BUILD_DIR_DEBUG}/${LIBRARY_NAME}" + ) + list(APPEND RUST_OUTPUTS "${PROJECT_BINARY_DIR}/${CARGO_BUILD_DIR}/${LIBRARY_NAME}") + endforeach() + add_custom_command( + OUTPUT ${RUST_OUTPUTS} + COMMAND ${CARGO_BUILD} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + USES_TERMINAL + DEPENDS ${RUST_SRC} + ) +else() + foreach(rust_target ${RUST_TARGETS}) + set(LIBRARY_NAME "${CMAKE_STATIC_LIBRARY_PREFIX}ddnet_${rust_target}${CMAKE_STATIC_LIBRARY_SUFFIX}") + add_library(rust_${rust_target} STATIC IMPORTED GLOBAL) + set_target_properties(rust_${rust_target} PROPERTIES + IMPORTED_LOCATION "${PROJECT_BINARY_DIR}/${CARGO_BUILD_DIR_RELEASE}/${LIBRARY_NAME}" + IMPORTED_LOCATION_DEBUG "${PROJECT_BINARY_DIR}/${CARGO_BUILD_DIR_DEBUG}/${LIBRARY_NAME}" + ) + add_custom_target(rust_${rust_target}_target DEPENDS "${PROJECT_BINARY_DIR}/${CARGO_BUILD_DIR}/${LIBRARY_NAME}") + add_dependencies(rust_${rust_target} rust_${rust_target}_target) + set(ARCH_LIBRARIES) + foreach(arch ${RUST_OSX_ARCHITECTURES}) + list(APPEND ARCH_LIBRARIES "${PROJECT_BINARY_DIR}/${arch}/${CARGO_BUILD_DIR}/${LIBRARY_NAME}") + endforeach() + add_custom_command( + OUTPUT "${PROJECT_BINARY_DIR}/${CARGO_BUILD_DIR}/${LIBRARY_NAME}" + COMMAND lipo ${ARCH_LIBRARIES} -create -output "${PROJECT_BINARY_DIR}/${CARGO_BUILD_DIR}/${LIBRARY_NAME}" + DEPENDS ${ARCH_LIBRARIES} + ) + endforeach() + foreach(arch ${RUST_OSX_ARCHITECTURES}) + set(RUST_OUTPUTS) + foreach(rust_target ${RUST_TARGETS}) + set(LIBRARY_NAME "${CMAKE_STATIC_LIBRARY_PREFIX}ddnet_${rust_target}${CMAKE_STATIC_LIBRARY_SUFFIX}") + list(APPEND RUST_OUTPUTS "${PROJECT_BINARY_DIR}/${arch}/${CARGO_BUILD_DIR}/${LIBRARY_NAME}") + endforeach() + add_custom_command( + OUTPUT ${RUST_OUTPUTS} + COMMAND ${CARGO_BUILD} --target=${arch} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + USES_TERMINAL + DEPENDS ${RUST_SRC} + ) + endforeach() +endif() + ######################################################################## # DATA ######################################################################## @@ -1703,6 +1870,7 @@ set_src(BASE GLOB_RECURSE src/base log.h logger.h math.h + rust.h system.cpp system.h tl/threading.h @@ -1733,6 +1901,7 @@ set_src(ENGINE_INTERFACE GLOB src/engine keys.h map.h message.h + rust.h server.h serverbrowser.h sound.h @@ -2131,7 +2300,6 @@ if(CLIENT) # Libraries set(LIBS_CLIENT - ${LIBS} ${FREETYPE_LIBRARIES} ${GLEW_LIBRARIES} ${PNG_LIBRARIES} @@ -2147,12 +2315,10 @@ if(CLIENT) ${VULKAN_LIBRARIES} ${TARGET_STEAMAPI} + rust_engine_shared ${PLATFORM_CLIENT_LIBS} - - # Add pthreads (on non-Windows) at the end, so that other libraries can depend - # on it. - ${CMAKE_THREAD_LIBS_INIT} + ${LIBS} ) if(DISCORD) @@ -2188,6 +2354,7 @@ if(CLIENT) $ $ $ + $ ) else() add_executable(${TARGET_CLIENT} WIN32 @@ -2198,6 +2365,7 @@ if(CLIENT) $ $ $ + $ ) endif() target_link_libraries(${TARGET_CLIENT} ${LIBS_CLIENT}) @@ -2370,13 +2538,12 @@ if(SERVER) # Libraries set(LIBS_SERVER - ${LIBS} + ${MINIUPNPC_LIBRARIES} ${MYSQL_LIBRARIES} ${TARGET_ANTIBOT} - ${MINIUPNPC_LIBRARIES} - # Add pthreads (on non-Windows) at the end, so that other libraries can depend - # on it. - ${CMAKE_THREAD_LIBS_INIT} + rust_engine_shared + + ${LIBS} ) # Target @@ -2387,6 +2554,7 @@ if(SERVER) ${SERVER_ICON} $ $ + $ ) target_link_libraries(${TARGET_SERVER} ${LIBS_SERVER}) target_include_directories(${TARGET_SERVER} PRIVATE ${PNG_INCLUDE_DIRS}) @@ -2439,7 +2607,7 @@ if(TOOLS) set(TOOL_LIBS ${LIBS}) if(TOOL MATCHES "^(dilate|map_convert_07|map_create_pixelart|map_optimize|map_extract|map_replace_image)$") list(APPEND TOOL_INCLUDE_DIRS ${PNG_INCLUDE_DIRS}) - list(APPEND TOOL_DEPS $) + list(APPEND TOOL_DEPS $) list(APPEND TOOL_LIBS ${PNG_LIBRARIES}) endif() if(TOOL MATCHES "^config_") @@ -2571,7 +2739,7 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST) $ ${DEPS} ) - target_link_libraries(${TARGET_TESTRUNNER} ${LIBS} ${MYSQL_LIBRARIES} ${PNG_LIBRARIES} ${GTEST_LIBRARIES}) + target_link_libraries(${TARGET_TESTRUNNER} ${MYSQL_LIBRARIES} ${PNG_LIBRARIES} ${GTEST_LIBRARIES} ${LIBS}) target_include_directories(${TARGET_TESTRUNNER} SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS}) list(APPEND TARGETS_OWN ${TARGET_TESTRUNNER}) @@ -2583,8 +2751,37 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST) DEPENDS ${TARGET_TESTRUNNER} USES_TERMINAL ) + if(NOT MSVC OR CMAKE_BUILD_TYPE STREQUAL Release) + # On MSVC, Rust tests only work in the release mode because we link our C++ + # code with the debug C standard library (/MTd) but Rust only supports + # linking to the release C standard library (/MT). + # + # See also https://github.com/rust-lang/rust/issues/39016. + add_dependencies(run_tests run_rust_tests) + endif() endif() +add_library(rust_test STATIC EXCLUDE_FROM_ALL + $ + $ + $ + $ + ${DEPS} +) + +list(APPEND TARGETS_OWN rust_test) +list(APPEND TARGETS_LINK rust_test) + +set(RUST_TEST_LIBS ${LIBS} $) +list(REMOVE_ITEM RUST_TEST_LIBS "-pthread") +add_custom_target(run_rust_tests + COMMAND ${CMAKE_COMMAND} -E env "DDNET_TEST_LIBRARIES=${RUST_TEST_LIBS}" ${CARGO_TEST} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + USES_TERMINAL + DEPENDS rust_test + VERBATIM +) + add_custom_target(run_integration_tests COMMAND ${PROJECT_BINARY_DIR}/integration_test.sh ${INTEGRATIONTESTRUNNER_ARGS} COMMENT Running integration tests @@ -3060,6 +3257,7 @@ foreach(target ${TARGETS_OWN}) endif() target_include_directories(${target} PRIVATE ${PROJECT_BINARY_DIR}/src) target_include_directories(${target} PRIVATE src) + target_include_directories(${target} PRIVATE src/rust-bridge) target_compile_definitions(${target} PRIVATE $<$:CONF_DEBUG>) target_include_directories(${target} SYSTEM PRIVATE ${CURL_INCLUDE_DIRS} ${SQLite3_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS}) target_compile_definitions(${target} PRIVATE GLEW_STATIC) diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..507539cde --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,114 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cxx" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5469a6f42296f4fd40789b397383718f9a0bd75d2f9b7cedbb249996811fba27" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fef2b4ffdc935c973bc7817d541fc936fdc8a85194cfdd9c761aca8387edd48" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3a240a54f5526967ffae81fdcda1fc80564964220d90816960b2eae2eab7f4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ddnet-base" +version = "0.0.1" +dependencies = [ + "cxx", + "ddnet-test", +] + +[[package]] +name = "ddnet-engine" +version = "0.0.1" +dependencies = [ + "cxx", + "ddnet-base", + "ddnet-engine-shared", + "ddnet-test", +] + +[[package]] +name = "ddnet-engine-shared" +version = "0.0.1" +dependencies = [ + "cxx", + "ddnet-base", + "ddnet-engine", + "ddnet-test", +] + +[[package]] +name = "ddnet-test" +version = "0.0.1" + +[[package]] +name = "link-cplusplus" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cae2cd7ba2f3f63938b9c724475dfb7b9861b545a90324476324ed21dbc8c8" +dependencies = [ + "cc", +] + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..62299adcc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +members = [ + "src/base", + "src/engine", + "src/engine/shared", + "src/rust-bridge/test", +] + +[profile.dev] +panic = "abort" + +[profile.release] +lto = "thin" +panic = "abort" + diff --git a/README.md b/README.md index a36c1d54c..0a102121f 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ Expect a large slow down. Building on Windows with Visual Studio -------------------------------------- -Download and install some version of [Microsoft Visual Studio](https://www.visualstudio.com/) (as of writing, MSVS Community 2017) with **C++ support**, install [Python 3](https://www.python.org/downloads/) **for all users** and install [CMake](https://cmake.org/download/#latest). +Download and install some version of [Microsoft Visual Studio](https://www.visualstudio.com/) (as of writing, MSVS Community 2017) with **C++ support**, install [Python 3](https://www.python.org/downloads/) **for all users** and install [CMake](https://cmake.org/download/#latest). You also need to install [Rust](https://rustup.rs/). Start CMake and select the source code folder (where DDNet resides, the directory with `CMakeLists.txt`). Additionally select a build folder, e.g. create a build subdirectory in the source code directory. Click "Configure" and select the Visual Studio generator (it should be pre-selected, so pressing "Finish" will suffice). After configuration finishes and the "Generate" reactivates, click it. When that finishes, click "Open Project". Visual Studio should open. You can compile the DDNet client by right-clicking the DDNet project (not the solution) and select "Select as StartUp project". Now you should be able to compile DDNet by clicking the green, triangular "Run" button. diff --git a/cmake/FindRust.cmake b/cmake/FindRust.cmake new file mode 100644 index 000000000..9c2bf7ca7 --- /dev/null +++ b/cmake/FindRust.cmake @@ -0,0 +1,29 @@ +find_program(RUST_RUSTC rustc) +find_program(RUST_CARGO cargo) + +if(RUST_RUSTC) + execute_process(COMMAND ${RUST_RUSTC} --version --verbose OUTPUT_VARIABLE RUSTC_VERSION_OUTPUT) + string(REPLACE "\n" ";" RUSTC_VERSION_OUTPUT "${RUSTC_VERSION_OUTPUT}") + set(RUST_NIGHTLY OFF) + foreach(line ${RUSTC_VERSION_OUTPUT}) + if(NOT DEFINED RUST_VERSION_STRING) + set(RUST_VERSION_STRING ${line}) + endif() + if(line MATCHES "^([^:]+): (.*)$") + set(KEY ${CMAKE_MATCH_1}) + set(VALUE ${CMAKE_MATCH_2}) + if(KEY STREQUAL "release") + set(RUST_VERSION ${VALUE}) + if(VALUE MATCHES "nightly") + set(RUST_NIGHTLY ON) + endif() + elseif(KEY STREQUAL "host") + set(RUST_TARGET_HOST ${VALUE}) + endif() + endif() + endforeach() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Rust DEFAULT_MSG RUST_RUSTC RUST_CARGO) +mark_as_advanced(RUST_RUSTC RUST_CARGO) diff --git a/cmake/toolchains/Emscripten.toolchain b/cmake/toolchains/Emscripten.toolchain index 0ed57443b..09c75e0b9 100644 --- a/cmake/toolchains/Emscripten.toolchain +++ b/cmake/toolchains/Emscripten.toolchain @@ -35,3 +35,4 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread -D_REENTRANT -g -O3 ${WASM_CXX_ set(CMAKE_C_FLAGS "${CMAKE_CXX_FLAGS} -pthread -D_REENTRANT -g -O3 ${WASM_CXX_ENGINE_FLAGS}") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread ${WASM_ENGINE_FLAGS}") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${WASM_ENGINE_FLAGS}") +set(CMAKE_RUST_COMPILER_TARGET wasm32-unknown-emscripten) diff --git a/cmake/toolchains/darwin-arm64.toolchain b/cmake/toolchains/darwin-arm64.toolchain index 2801912cd..cc3ee909b 100644 --- a/cmake/toolchains/darwin-arm64.toolchain +++ b/cmake/toolchains/darwin-arm64.toolchain @@ -11,6 +11,7 @@ set(CMAKE_C_COMPILER oa64-clang) set(CMAKE_CXX_COMPILER oa64-clang++) set(CMAKE_INSTALL_NAME_TOOL aarch64-apple-$ENV{OSXCROSS_TARGET}-install_name_tool) set(CMAKE_OTOOL aarch64-apple-$ENV{OSXCROSS_TARGET}-otool) +set(CMAKE_RUST_COMPILER_TARGET aarch64-apple-darwin) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) diff --git a/cmake/toolchains/darwin-x86_64.toolchain b/cmake/toolchains/darwin-x86_64.toolchain index d6824e772..f4a7ec3b1 100644 --- a/cmake/toolchains/darwin-x86_64.toolchain +++ b/cmake/toolchains/darwin-x86_64.toolchain @@ -11,6 +11,7 @@ set(CMAKE_C_COMPILER o64-clang) set(CMAKE_CXX_COMPILER o64-clang++) set(CMAKE_INSTALL_NAME_TOOL x86_64-apple-$ENV{OSXCROSS_TARGET}-install_name_tool) set(CMAKE_OTOOL x86_64-apple-$ENV{OSXCROSS_TARGET}-otool) +set(CMAKE_RUST_COMPILER_TARGET x86_64-apple-darwin) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) diff --git a/cmake/toolchains/mingw32.toolchain b/cmake/toolchains/mingw32.toolchain index c4c065b7e..e920543d6 100644 --- a/cmake/toolchains/mingw32.toolchain +++ b/cmake/toolchains/mingw32.toolchain @@ -3,6 +3,7 @@ set(CMAKE_SYSTEM_NAME Windows) set(CMAKE_C_COMPILER i686-w64-mingw32-gcc) set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++) set(CMAKE_RC_COMPILER i686-w64-mingw32-windres) +set(CMAKE_RUST_COMPILER_TARGET i686-pc-windows-gnu) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) diff --git a/cmake/toolchains/mingw64.toolchain b/cmake/toolchains/mingw64.toolchain index bec41e944..a43605b2a 100644 --- a/cmake/toolchains/mingw64.toolchain +++ b/cmake/toolchains/mingw64.toolchain @@ -3,6 +3,7 @@ set(CMAKE_SYSTEM_NAME Windows) set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) +set(CMAKE_RUST_COMPILER_TARGET x86_64-pc-windows-gnu) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) diff --git a/deny.toml b/deny.toml new file mode 100644 index 000000000..bb2881b17 --- /dev/null +++ b/deny.toml @@ -0,0 +1,6 @@ +[licenses] +allow = [ + "Apache-2.0", + "MIT", + "Zlib" +] diff --git a/scripts/check_header_guards.py b/scripts/check_header_guards.py index 4cd926c0f..275ee6e9d 100755 --- a/scripts/check_header_guards.py +++ b/scripts/check_header_guards.py @@ -6,9 +6,9 @@ os.chdir(os.path.dirname(__file__) + "/..") PATH = "src/" EXCEPTIONS = [ - "src/base/unicode/confusables.h", + "src/base/unicode/confusables.h", "src/base/unicode/confusables_data.h", - "src/base/unicode/tolower.h", + "src/base/unicode/tolower.h", "src/base/unicode/tolower_data.h", "src/tools/config_common.h" ] @@ -40,7 +40,7 @@ def check_dir(directory): for file in file_list: path = directory + file if os.path.isdir(path): - if file not in ("external", "generated"): + if file not in ("external", "generated", "rust-bridge"): errors += check_dir(path + "/") elif file.endswith(".h") and file != "keynames.h": errors += check_file(path) diff --git a/scripts/fix_style.py b/scripts/fix_style.py index 07eee751d..245ee44ef 100755 --- a/scripts/fix_style.py +++ b/scripts/fix_style.py @@ -20,7 +20,8 @@ IGNORE_FILES = [ def filter_ignored(filenames): return [filename for filename in filenames if filename not in IGNORE_FILES - and not filename.startswith("src/game/generated/")] + and not filename.startswith("src/game/generated/") + and not filename.startswith("src/rust-bridge")] def filter_cpp(filenames): return [filename for filename in filenames diff --git a/src/base/Cargo.toml b/src/base/Cargo.toml new file mode 100644 index 000000000..709a35c8e --- /dev/null +++ b/src/base/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ddnet-base" +version = "0.0.1" +edition = "2018" +publish = false +license = "Zlib" + +[lib] +path = "lib.rs" + +[dependencies] +cxx = "1.0" + +[dev-dependencies] +ddnet-test = { path = "../rust-bridge/test", features = ["link-test-libraries"] } diff --git a/src/base/color.rs b/src/base/color.rs new file mode 100644 index 000000000..5514c7e47 --- /dev/null +++ b/src/base/color.rs @@ -0,0 +1,77 @@ +/// Color, in RGBA format. Corresponds to the C++ type `ColorRGBA`. +/// +/// The color is represented by red, green, blue and alpha values between `0.0` +/// and `1.0`. +/// +/// See also . +/// +/// # Examples +/// +/// ``` +/// use ddnet_base::ColorRGBA; +/// +/// let white = ColorRGBA { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }; +/// let black = ColorRGBA { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }; +/// let red = ColorRGBA { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }; +/// let transparent = ColorRGBA { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }; +/// +/// // #ffa500 +/// let ddnet_logo_color = ColorRGBA { r: 1.0, g: 0.6470588235294118, b: 0.0, a: 1.0 }; +/// ``` +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(C)] +pub struct ColorRGBA { + /// Red + pub r: f32, + /// Green + pub g: f32, + /// Blue + pub b: f32, + /// Alpha (i.e. opacity. `0.0` means fully transparent, `1.0` + /// nontransparent). + pub a: f32, +} + +unsafe impl cxx::ExternType for ColorRGBA { + type Id = cxx::type_id!("ColorRGBA"); + type Kind = cxx::kind::Trivial; +} + +/// Color, in HSLA format. Corresponds to the C++ type `ColorHSLA`. +/// +/// The color is represented by hue, saturation, lightness and alpha values +/// between `0.0` and `1.0`. +/// +/// See also . +/// +/// # Examples +/// +/// ``` +/// use ddnet_base::ColorHSLA; +/// +/// let white = ColorHSLA { h: 0.0, s: 0.0, l: 1.0, a: 1.0 }; +/// let black = ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }; +/// let red = ColorHSLA { h: 0.0, s: 1.0, l: 0.5, a: 1.0 }; +/// let transparent = ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 0.0 }; +/// +/// // #ffa500 +/// let ddnet_logo_color = ColorHSLA { h: 0.10784314, s: 1.0, l: 0.5, a: 1.0 }; +/// ``` +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(C)] +pub struct ColorHSLA { + /// Hue + pub h: f32, + /// Saturation + pub s: f32, + /// Lightness + pub l: f32, + /// Alpha (i.e. opacity. `0.0` means fully transparent, `1.0` + /// nontransparent). + pub a: f32, +} + +unsafe impl cxx::ExternType for ColorHSLA { + type Id = cxx::type_id!("ColorHSLA"); + type Kind = cxx::kind::Trivial; +} diff --git a/src/base/lib.rs b/src/base/lib.rs new file mode 100644 index 000000000..1816e4655 --- /dev/null +++ b/src/base/lib.rs @@ -0,0 +1,19 @@ +//! DDNet's base library, Rust part. +//! +//! DDNet's code base is separated into three major parts, `base`, `engine` and +//! `game`. +//! +//! The base library consists of operating system abstractions, and +//! game-independent data structures such as color handling and math vectors. +//! Additionally, it contains some types to support the C++-Rust-translation. + +#![warn(missing_docs)] + +#[cfg(test)] +extern crate ddnet_test; + +mod color; +mod rust; + +pub use color::*; +pub use rust::*; diff --git a/src/base/rust.h b/src/base/rust.h new file mode 100644 index 000000000..a1c41c118 --- /dev/null +++ b/src/base/rust.h @@ -0,0 +1,5 @@ +#ifndef BASE_RUST_H +#define BASE_RUST_H +typedef const char *StrRef; +typedef void *UserPtr; +#endif // BASE_RUST_H diff --git a/src/base/rust.rs b/src/base/rust.rs new file mode 100644 index 000000000..540bd27ed --- /dev/null +++ b/src/base/rust.rs @@ -0,0 +1,268 @@ +use std::cmp; +use std::ffi::CStr; +use std::fmt; +use std::marker::PhantomData; +use std::ops; +use std::os::raw::c_char; +use std::ptr; +use std::str; + +/// User pointer, as used in callbacks. Corresponds to the C++ type `void *`. +/// +/// Callbacks in C are usually represented by a function pointer and some +/// "userdata" pointer that is also passed to the function pointer. This allows +/// to hand data to the callback. This type represents such a userdata poiner. +/// +/// It is `unsafe` to convert the `UserPtr` back to its original pointer using +/// [`UserPtr::cast`] because its lifetime and type information was lost. +/// +/// When dealing with Rust code exclusively, closures are preferred. +/// +/// # Examples +/// +/// ``` +/// use ddnet_base::UserPtr; +/// +/// struct CallbackData { +/// favorite_color: &'static str, +/// } +/// +/// let data = CallbackData { +/// favorite_color: "green", +/// }; +/// +/// callback(UserPtr::from(&data)); +/// +/// fn callback(pointer: UserPtr) { +/// let data: &CallbackData = unsafe { pointer.cast() }; +/// println!("favorite color: {}", data.favorite_color); +/// } +/// ``` +#[repr(transparent)] +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct UserPtr(*mut ()); + +unsafe impl cxx::ExternType for UserPtr { + type Id = cxx::type_id!("UserPtr"); + type Kind = cxx::kind::Trivial; +} + +impl UserPtr { + /// Create a null `UserPtr`. + /// + /// # Examples + /// + /// ``` + /// use ddnet_base::UserPtr; + /// + /// // Can't do anything useful with this. + /// let _user = UserPtr::null(); + /// ``` + pub fn null() -> UserPtr { + UserPtr(ptr::null_mut()) + } + + /// Cast `UserPtr` back to a reference to its real type. + /// + /// # Safety + /// + /// The caller is responsible for checking type and lifetime correctness. + /// Also, they must make sure that there are only immutable references or at + /// most one mutable reference live at the same time. + /// + /// # Examples + /// + /// ``` + /// use ddnet_base::UserPtr; + /// + /// let the_answer = 42; + /// let user = UserPtr::from(&the_answer); + /// + /// assert_eq!(unsafe { *user.cast::() }, 42); + /// ``` + pub unsafe fn cast(&self) -> &T { + &*(self.0 as *const _) + } + + /// Cast `UserPtr` back to a mutable reference to its real type. + /// + /// See [`UserPtr`] documentation for details and an example. + /// + /// # Safety + /// + /// The caller is responsible for checking type and lifetime correctness. + /// Also, they must make sure that there are only immutable references or at + /// most one mutable reference live at the same time. + /// + /// # Examples + /// + /// ``` + /// use ddnet_base::UserPtr; + /// + /// let mut seen_it = false; + /// let mut user = UserPtr::from(&mut seen_it); + /// + /// unsafe { + /// *user.cast_mut() = true; + /// } + /// + /// assert_eq!(seen_it, true); + /// ``` + pub unsafe fn cast_mut(&mut self) -> &mut T { + &mut *(self.0 as *mut _) + } +} + +impl<'a, T> From<&'a T> for UserPtr { + fn from(t: &'a T) -> UserPtr { + UserPtr(t as *const _ as *mut _) + } +} + +impl<'a, T> From<&'a mut T> for UserPtr { + fn from(t: &'a mut T) -> UserPtr { + UserPtr(t as *mut _ as *mut _) + } +} + +/// C-style string pointer to UTF-8 data. Corresponds to the C++ type `const +/// char *`. +/// +/// The lifetime is the lifetime of the underlying string. +/// +/// This is a separate type from [`std::ffi::CStr`] because that type is not +/// FFI-safe and does not guarantee UTF-8. +/// +/// In Rust code, [`String`] is preferred. For constructing C strings, +/// [`std::ffi::CString`] or this crate's [`s!`](`crate::s!`) macro can be used. +/// +/// # Examples +/// +/// ``` +/// # fn some_c_function(_: StrRef<'_>) {} +/// use ddnet_base::StrRef; +/// use ddnet_base::s; +/// use std::ffi::CStr; +/// use std::ffi::CString; +/// use std::process; +/// +/// some_c_function(CStr::from_bytes_with_nul(b"Hello!\0").unwrap().into()); +/// +/// let string = CString::new(format!("Current PID is {}.", process::id())).unwrap(); +/// some_c_function(string.as_ref().into()); +/// +/// fn c_function_wrapper(s: &CStr) { +/// some_c_function(s.into()); +/// } +/// +/// some_c_function(s!("こんにちはC言語")); +/// ``` +#[repr(transparent)] +#[derive(Eq)] +pub struct StrRef<'a>(*const c_char, PhantomData<&'a ()>); + +unsafe impl<'a> cxx::ExternType for StrRef<'a> { + type Id = cxx::type_id!("StrRef"); + type Kind = cxx::kind::Trivial; +} + +impl<'a> StrRef<'a> { + /// Get the wrapped string reference. + /// + /// This does the same as the `Deref` implementation, differing only in the + /// returned lifetime. `Deref`'s return type is bound by `self`'s lifetime, + /// this returns the more correct and longer lifetime. + /// + /// This is an O(n) operation as it needs to calculate the length of a C + /// string by finding the first NUL byte. + /// + /// # Examples + /// + /// ``` + /// use ddnet_base::s; + /// + /// let str1: &'static str = s!("static string").to_str(); + /// ``` + /// + /// ```compile_fail + /// use ddnet_base::s; + /// + /// // Wrong lifetime. + /// let str2: &'static str = &*s!("another static string"); + /// ``` + /// + pub fn to_str(&self) -> &'a str { + unsafe { str::from_utf8_unchecked(CStr::from_ptr(self.0).to_bytes()) } + } +} + +impl<'a> From<&'a CStr> for StrRef<'a> { + fn from(s: &'a CStr) -> StrRef<'a> { + let bytes = s.to_bytes_with_nul(); + str::from_utf8(bytes).expect("valid UTF-8"); + StrRef(bytes.as_ptr() as *const _, PhantomData) + } +} + +impl<'a> fmt::Debug for StrRef<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_str().fmt(f) + } +} + +impl<'a> fmt::Display for StrRef<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_str().fmt(f) + } +} + +impl<'a> cmp::PartialEq for StrRef<'a> { + fn eq(&self, other: &StrRef<'a>) -> bool { + self.to_str().eq(other.to_str()) + } +} + +impl<'a> cmp::PartialEq<&'a str> for StrRef<'a> { + fn eq(&self, other: &&'a str) -> bool { + self.to_str().eq(*other) + } +} + +impl<'a> cmp::PartialOrd for StrRef<'a> { + fn partial_cmp(&self, other: &StrRef<'a>) -> Option { + self.to_str().partial_cmp(other.to_str()) + } +} + +impl<'a> cmp::Ord for StrRef<'a> { + fn cmp(&self, other: &StrRef<'a>) -> cmp::Ordering { + self.to_str().cmp(other.to_str()) + } +} + +impl<'a> ops::Deref for StrRef<'a> { + type Target = str; + fn deref(&self) -> &str { + self.to_str() + } +} + +/// Construct a [`StrRef`] statically. +/// +/// # Examples +/// +/// ``` +/// use ddnet_base::StrRef; +/// use ddnet_base::s; +/// +/// let greeting: StrRef<'static> = s!("Hallöchen, C!"); +/// let status: StrRef<'static> = s!(concat!("Current file: ", file!())); +/// ``` +#[macro_export] +macro_rules! s { + ($str:expr) => { + ::ddnet_base::StrRef::from( + ::std::ffi::CStr::from_bytes_with_nul(::std::concat!($str, "\0").as_bytes()).unwrap(), + ) + }; +} diff --git a/src/engine/Cargo.toml b/src/engine/Cargo.toml new file mode 100644 index 000000000..b6d484cd0 --- /dev/null +++ b/src/engine/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ddnet-engine" +version = "0.0.1" +edition = "2018" +publish = false +license = "Zlib" + +[lib] +path = "lib.rs" + +[dependencies] +ddnet-base = { path = "../base" } + +cxx = "1.0" + +[dev-dependencies] +ddnet-engine-shared = { path = "shared" } +ddnet-test = { path = "../rust-bridge/test", features = ["link-test-libraries"] } diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index e1ec2f06e..e47bdd111 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include @@ -4448,9 +4449,11 @@ void CClient::RegisterCommands() m_pConsole->Register("demo_play", "", CFGFLAG_CLIENT, Con_DemoPlay, this, "Play demo"); m_pConsole->Register("demo_speed", "i[speed]", CFGFLAG_CLIENT, Con_DemoSpeed, this, "Set demo speed"); - m_pConsole->Register("save_replay", "?i[length] ?s[filename]", CFGFLAG_CLIENT, Con_SaveReplay, this, "Save a replay of the last defined amount of seconds"); + m_pConsole->Register("save_replay", "?i[length] s[filename]", CFGFLAG_CLIENT, Con_SaveReplay, this, "Save a replay of the last defined amount of seconds"); m_pConsole->Register("benchmark_quit", "i[seconds] r[file]", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_BenchmarkQuit, this, "Benchmark frame times for number of seconds to file, then quit"); + RustVersionRegister(*m_pConsole); + m_pConsole->Chain("cl_timeout_seed", ConchainTimeoutSeed, this); m_pConsole->Chain("cl_replays", ConchainReplays, this); @@ -4607,7 +4610,7 @@ int main(int argc, const char **argv) // create the components IEngine *pEngine = CreateEngine(GAME_NAME, pFutureConsoleLogger, 2); - IConsole *pConsole = CreateConsole(CFGFLAG_CLIENT); + IConsole *pConsole = CreateConsole(CFGFLAG_CLIENT).release(); IStorage *pStorage = CreateStorage(IStorage::STORAGETYPE_CLIENT, argc, (const char **)argv); IConfigManager *pConfigManager = CreateConfigManager(); IEngineSound *pEngineSound = CreateEngineSound(); diff --git a/src/engine/console.h b/src/engine/console.h index c8f95003d..ca8669740 100644 --- a/src/engine/console.h +++ b/src/engine/console.h @@ -7,6 +7,8 @@ #include #include +#include + static const ColorRGBA gs_ConsoleDefaultColor(1, 1, 1, 1); enum LEVEL : char; @@ -48,10 +50,10 @@ public: IResult() { m_NumArgs = 0; } virtual ~IResult() {} - virtual int GetInteger(unsigned Index) = 0; - virtual float GetFloat(unsigned Index) = 0; - virtual const char *GetString(unsigned Index) = 0; - virtual ColorHSLA GetColor(unsigned Index, bool Light) = 0; + virtual int GetInteger(unsigned Index) const = 0; + virtual float GetFloat(unsigned Index) const = 0; + virtual const char *GetString(unsigned Index) const = 0; + virtual ColorHSLA GetColor(unsigned Index, bool Light) const = 0; virtual void RemoveArgument(unsigned Index) = 0; @@ -60,7 +62,7 @@ public: // DDRace - virtual int GetVictim() = 0; + virtual int GetVictim() const = 0; }; class CCommandInfo @@ -110,7 +112,7 @@ public: virtual void ExecuteFile(const char *pFilename, int ClientID = -1, bool LogFailure = false, int StorageType = IStorage::TYPE_ALL) = 0; virtual char *Format(char *pBuf, int Size, const char *pFrom, const char *pStr) = 0; - virtual void Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor = gs_ConsoleDefaultColor) = 0; + virtual void Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor = gs_ConsoleDefaultColor) const = 0; virtual void SetTeeHistorianCommandCallback(FTeeHistorianCommandCallback pfnCallback, void *pUser) = 0; virtual void SetUnknownCommandCallback(FUnknownCommandCallback pfnCallback, void *pUser) = 0; virtual void InitChecksum(CChecksumData *pData) const = 0; @@ -127,6 +129,6 @@ public: virtual void SetFlagMask(int FlagMask) = 0; }; -extern IConsole *CreateConsole(int FlagMask); +std::unique_ptr CreateConsole(int FlagMask); #endif // FILE_ENGINE_CONSOLE_H diff --git a/src/engine/console.rs b/src/engine/console.rs new file mode 100644 index 000000000..62dea88dc --- /dev/null +++ b/src/engine/console.rs @@ -0,0 +1,544 @@ +use ddnet_base::ColorRGBA; +use ddnet_base::UserPtr; + +pub use self::ffi::CreateConsole; +pub use self::ffi::IConsole; +pub use self::ffi::IConsole_IResult; + +/// Command callback for `IConsole`. +/// +/// See [`IConsole::Register`] for an example. +#[allow(non_camel_case_types)] +#[repr(transparent)] +pub struct IConsole_FCommandCallback(pub extern "C" fn(result: &IConsole_IResult, user: UserPtr)); + +unsafe impl cxx::ExternType for IConsole_FCommandCallback { + type Id = cxx::type_id!("IConsole_FCommandCallback"); + type Kind = cxx::kind::Trivial; +} + +#[cxx::bridge] +mod ffi { + unsafe extern "C++" { + include!("base/rust.h"); + include!("engine/console.h"); + include!("engine/rust.h"); + + type ColorRGBA = ddnet_base::ColorRGBA; + type ColorHSLA = ddnet_base::ColorHSLA; + type StrRef<'a> = ddnet_base::StrRef<'a>; + type UserPtr = ddnet_base::UserPtr; + type IConsole_FCommandCallback = super::IConsole_FCommandCallback; + + /// Represents the arguments to a console command for [`IConsole`]. + /// + /// You can only obtain this type in the command callback + /// [`IConsole_FCommandCallback`] specified in [`IConsole::Register`]. + type IConsole_IResult; + + /// Get the n-th parameter of the command as an integer. + /// + /// If the index is out of range, this returns 0. If the parameter + /// cannot be parsed as an integer, this also returns 0. + /// + /// # Examples + /// + /// ``` + /// # extern crate ddnet_test; + /// # use ddnet_base::UserPtr; + /// # use ddnet_base::s; + /// # use ddnet_engine::CreateConsole; + /// # use ddnet_engine::IConsole; + /// # use ddnet_engine::IConsole_FCommandCallback; + /// # use ddnet_engine::IConsole_IResult; + /// # use ddnet_engine_shared::CFGFLAG_SERVER; + /// # + /// # let mut console = CreateConsole(CFGFLAG_SERVER); + /// # let mut executed = false; + /// # console.pin_mut().Register(s!("command"), s!("sss"), CFGFLAG_SERVER, IConsole_FCommandCallback(callback), UserPtr::from(&mut executed), s!("")); + /// # console.pin_mut().ExecuteLine(s!(r#"command "1337" abc -7331def"#), -1, true); + /// # extern "C" fn callback(result_param: &IConsole_IResult, mut user: UserPtr) { + /// # unsafe { *user.cast_mut::() = true; } + /// let result: &IConsole_IResult /* = `command "1337" abc -7331def` */; + /// # result = result_param; + /// assert_eq!(result.GetInteger(0), 1337); + /// assert_eq!(result.GetInteger(1), 0); // unparsable + /// assert_eq!(result.GetInteger(2), -7331); // parsable start + /// assert_eq!(result.GetInteger(3), 0); // out of range + /// # } + /// # assert!(executed); + /// ``` + pub fn GetInteger(self: &IConsole_IResult, Index: u32) -> i32; + + /// Get the n-th parameter of the command as a floating point number. + /// + /// If the index is out of range, this returns 0.0. If the parameter + /// cannot be parsed as a floating point number, this also returns 0.0. + /// + /// # Examples + /// + /// ``` + /// # extern crate ddnet_test; + /// # use ddnet_base::UserPtr; + /// # use ddnet_base::s; + /// # use ddnet_engine::CreateConsole; + /// # use ddnet_engine::IConsole; + /// # use ddnet_engine::IConsole_FCommandCallback; + /// # use ddnet_engine::IConsole_IResult; + /// # use ddnet_engine_shared::CFGFLAG_SERVER; + /// # + /// # let mut console = CreateConsole(CFGFLAG_SERVER); + /// # let mut executed = false; + /// # console.pin_mut().Register(s!("command"), s!("sss"), CFGFLAG_SERVER, IConsole_FCommandCallback(callback), UserPtr::from(&mut executed), s!("")); + /// # console.pin_mut().ExecuteLine(s!(r#"command "13.37" abc -73.31def"#), -1, true); + /// # extern "C" fn callback(result_param: &IConsole_IResult, mut user: UserPtr) { + /// # unsafe { *user.cast_mut::() = true; } + /// let result: &IConsole_IResult /* = `command "13.37" abc -73.31def` */; + /// # result = result_param; + /// assert_eq!(result.GetFloat(0), 13.37); + /// assert_eq!(result.GetFloat(1), 0.0); // unparsable + /// assert_eq!(result.GetFloat(2), -73.31); // parsable start + /// assert_eq!(result.GetFloat(3), 0.0); // out of range + /// # } + /// # assert!(executed); + /// ``` + pub fn GetFloat(self: &IConsole_IResult, Index: u32) -> f32; + + /// Get the n-th parameter of the command as a string. + /// + /// If the index is out of range, this returns the empty string `""`. + /// + /// # Examples + /// + /// ``` + /// # extern crate ddnet_test; + /// # use ddnet_base::UserPtr; + /// # use ddnet_base::s; + /// # use ddnet_engine::CreateConsole; + /// # use ddnet_engine::IConsole; + /// # use ddnet_engine::IConsole_FCommandCallback; + /// # use ddnet_engine::IConsole_IResult; + /// # use ddnet_engine_shared::CFGFLAG_SERVER; + /// # + /// # let mut console = CreateConsole(CFGFLAG_SERVER); + /// # let mut executed = false; + /// # console.pin_mut().Register(s!("command"), s!("sss"), CFGFLAG_SERVER, IConsole_FCommandCallback(callback), UserPtr::from(&mut executed), s!("")); + /// # console.pin_mut().ExecuteLine(s!(r#"command "I'm in space" '' "\"\\Escapes\?\"\n""#), -1, true); + /// # extern "C" fn callback(result_param: &IConsole_IResult, mut user: UserPtr) { + /// # unsafe { *user.cast_mut::() = true; } + /// let result: &IConsole_IResult /* = `command "I'm in space" '' "\"\\Escapes\?\"\n"` */; + /// # result = result_param; + /// assert_eq!(result.GetString(0), "I'm in space"); + /// assert_eq!(result.GetString(1), "''"); + /// assert_eq!(result.GetString(2), r#""\Escapes\?"\n"#); // only \\ and \" escapes + /// assert_eq!(result.GetString(3), ""); // out of range + /// # } + /// # assert!(executed); + /// ``` + pub fn GetString(self: &IConsole_IResult, Index: u32) -> StrRef<'_>; + + /// Get the n-th parameter of the command as a color. + /// + /// If the index is out of range, this returns black. If the parameter + /// cannot be parsed as a color, this also returns black. + /// + /// It supports the following formats: + /// - `$XXX` (RGB, e.g. `$f00` for red) + /// - `$XXXXXX` (RGB, e.g. `$ffa500` for DDNet's logo color) + /// - base 10 integers (24/32 bit HSL in base 10, e.g. `0` for black) + /// - the following color names: `red`, `yellow`, `green`, `cyan`, + /// `blue`, `magenta`, `white`, `gray`, `black`. + /// + /// The `Light` parameter influences the interpretation of base 10 + /// integers, if it is set, the lightness channel is divided by 2 and + /// 0.5 is added, making 0.5 the darkest and 1.0 the lightest possible + /// value. + /// + /// # Examples + /// + /// ``` + /// # extern crate ddnet_test; + /// # use ddnet_base::ColorHSLA; + /// # use ddnet_base::UserPtr; + /// # use ddnet_base::s; + /// # use ddnet_engine::CreateConsole; + /// # use ddnet_engine::IConsole; + /// # use ddnet_engine::IConsole_FCommandCallback; + /// # use ddnet_engine::IConsole_IResult; + /// # use ddnet_engine_shared::CFGFLAG_SERVER; + /// # + /// # let mut console = CreateConsole(CFGFLAG_SERVER); + /// # let mut executed = false; + /// # console.pin_mut().Register(s!("command"), s!("ssssss"), CFGFLAG_SERVER, IConsole_FCommandCallback(callback), UserPtr::from(&mut executed), s!("")); + /// # console.pin_mut().ExecuteLine(s!(r#"command "$f00" $ffa500 $1234 shiny cyan -16777216"#), -1, true); + /// # extern "C" fn callback(result_param: &IConsole_IResult, mut user: UserPtr) { + /// # unsafe { *user.cast_mut::() = true; } + /// let result: &IConsole_IResult /* = `command "$f00" $ffa500 $1234 shiny cyan -16777216` */; + /// # result = result_param; + /// assert_eq!(result.GetColor(0, false), ColorHSLA { h: 0.0, s: 1.0, l: 0.5, a: 1.0 }); // red + /// assert_eq!(result.GetColor(1, false), ColorHSLA { h: 0.10784314, s: 1.0, l: 0.5, a: 1.0 }); // DDNet logo color + /// assert_eq!(result.GetColor(2, false), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // cannot be parsed => black + /// assert_eq!(result.GetColor(3, false), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // unknown color name => black + /// assert_eq!(result.GetColor(4, false), ColorHSLA { h: 0.5, s: 1.0, l: 0.5, a: 1.0 }); // cyan + /// assert_eq!(result.GetColor(5, false), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // black + /// assert_eq!(result.GetColor(6, false), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // out of range => black + /// + /// assert_eq!(result.GetColor(0, true), result.GetColor(0, false)); + /// assert_eq!(result.GetColor(1, true), result.GetColor(1, false)); + /// assert_eq!(result.GetColor(2, true), result.GetColor(2, false)); + /// assert_eq!(result.GetColor(3, true), result.GetColor(3, false)); + /// assert_eq!(result.GetColor(4, true), result.GetColor(4, false)); + /// assert_eq!(result.GetColor(5, true), ColorHSLA { h: 0.0, s: 0.0, l: 0.5, a: 1.0 }); // black, but has the `Light` parameter set + /// assert_eq!(result.GetColor(6, true), result.GetColor(6, false)); + /// # } + /// # assert!(executed); + /// ``` + pub fn GetColor(self: &IConsole_IResult, Index: u32, Light: bool) -> ColorHSLA; + + /// Get the number of parameters passed. + /// + /// This is mostly important for commands that have optional parameters + /// (`?`) and thus support variable numbers of arguments. + /// + /// See [`IConsole::Register`] for more details. + /// + /// # Examples + /// + /// ``` + /// # extern crate ddnet_test; + /// # use ddnet_base::ColorHSLA; + /// # use ddnet_base::UserPtr; + /// # use ddnet_base::s; + /// # use ddnet_engine::CreateConsole; + /// # use ddnet_engine::IConsole; + /// # use ddnet_engine::IConsole_FCommandCallback; + /// # use ddnet_engine::IConsole_IResult; + /// # use ddnet_engine_shared::CFGFLAG_SERVER; + /// # + /// # let mut console = CreateConsole(CFGFLAG_SERVER); + /// # let mut executed: u32 = 0; + /// # console.pin_mut().Register(s!("command"), s!("s?ss"), CFGFLAG_SERVER, IConsole_FCommandCallback(callback), UserPtr::from(&mut executed), s!("")); + /// # console.pin_mut().ExecuteLine(s!(r#"command one two three"#), -1, true); + /// # console.pin_mut().ExecuteLine(s!(r#"command "one two" "three four""#), -1, true); + /// # extern "C" fn callback(result_param: &IConsole_IResult, mut user: UserPtr) { + /// # let executed; + /// # unsafe { executed = *user.cast_mut::(); *user.cast_mut::() += 1; } + /// # if executed == 0 { + /// let result: &IConsole_IResult /* = `command one two three` */; + /// # result = result_param; + /// assert_eq!(result.NumArguments(), 3); + /// + /// # } else if executed == 1 { + /// let result: &IConsole_IResult /* = `command "one two" "three four"` */; + /// # result = result_param; + /// assert_eq!(result.NumArguments(), 2); + /// # } + /// # } + /// # assert!(executed == 2); + /// ``` + pub fn NumArguments(self: &IConsole_IResult) -> i32; + + /// Get the value of the sole victim (`v`) parameter. + /// + /// This is mostly important for commands that have optional parameters + /// and thus support variable numbers of arguments. + /// + /// See [`IConsole::Register`] for more details. + /// + /// # Examples + /// + /// ``` + /// # extern crate ddnet_test; + /// # use ddnet_base::UserPtr; + /// # use ddnet_base::s; + /// # use ddnet_engine::CreateConsole; + /// # use ddnet_engine::IConsole; + /// # use ddnet_engine::IConsole_FCommandCallback; + /// # use ddnet_engine::IConsole_IResult; + /// # use ddnet_engine_shared::CFGFLAG_SERVER; + /// # + /// # let mut console = CreateConsole(CFGFLAG_SERVER); + /// # let mut executed: u32 = 0; + /// # console.pin_mut().Register(s!("command"), s!("v"), CFGFLAG_SERVER, IConsole_FCommandCallback(callback), UserPtr::from(&mut executed), s!("")); + /// # console.pin_mut().ExecuteLine(s!(r#"command me"#), 33, true); + /// # console.pin_mut().ExecuteLine(s!(r#"command string"#), 33, true); + /// # console.pin_mut().ExecuteLine(s!(r#"command 42"#), 33, true); + /// # console.pin_mut().ExecuteLine(s!(r#"command all"#), 33, true); + /// # extern "C" fn callback(result_param: &IConsole_IResult, mut user: UserPtr) { + /// # let executed; + /// # unsafe { executed = *user.cast_mut::(); *user.cast_mut::() += 1; } + /// # match executed { + /// # 0 => { + /// let result: &IConsole_IResult /* = `command me` */; + /// # result = result_param; + /// // Assume the executing client ID is 33. + /// assert_eq!(result.GetVictim(), 33); + /// + /// # } + /// # 1 => { + /// let result: &IConsole_IResult /* = `command string` */; + /// # result = result_param; + /// assert_eq!(result.GetVictim(), 0); + /// + /// # } + /// # 2 => { + /// let result: &IConsole_IResult /* = `command 42` */; + /// # result = result_param; + /// assert_eq!(result.GetVictim(), 42); + /// + /// # } + /// # 3 => { + /// let result: &IConsole_IResult /* = `command all` first invocation */; + /// # result = result_param; + /// assert_eq!(result.GetVictim(), 0); + /// # } + /// # 4 => { + /// let result: &IConsole_IResult /* = `command all` second invocation */; + /// # result = result_param; + /// assert_eq!(result.GetVictim(), 1); + /// # } + /// // … + /// # 66 => { + /// let result: &IConsole_IResult /* = `command all` last invocation */; + /// # result = result_param; + /// assert_eq!(result.GetVictim(), 63); + /// # } + /// # _ => {} + /// # } + /// # } + /// # assert!(executed == 67); + /// ``` + pub fn GetVictim(self: &IConsole_IResult) -> i32; + + /// Console interface, consists of logging output and command execution. + /// + /// This is used for the local console in the client and the remote + /// console of the server. It handles commands, their inputs and + /// outputs, command completion and also command and log output. + /// + /// Call [`CreateConsole`] to obtain an instance. + type IConsole; + + /// Execute a command in the console. + /// + /// Commands can be separated by semicolons (`;`), everything after a + /// hash sign (`#`) is treated as a comment and discarded. Parameters + /// are separated by spaces (` `). By quoting arguments with double + /// quotes (`"`), the special meaning of the other characters can be + /// disabled. Double quotes can be escaped as `\"` and backslashes can + /// be escaped as `\\` inside double quotes. + /// + /// When `InterpretSemicolons` is `false`, semicolons are not + /// interpreted unless the command starts with `mc;`. + /// + /// The `ClientID` parameter defaults to -1, `InterpretSemicolons` to + /// `true` in C++. + pub fn ExecuteLine( + self: Pin<&mut IConsole>, + pStr: StrRef<'_>, + ClientID: i32, + InterpretSemicolons: bool, + ); + + /// Log a message. + /// + /// `Level` is one of + /// - [`IConsole_OUTPUT_LEVEL_STANDARD`](constant.IConsole_OUTPUT_LEVEL_STANDARD.html) + /// - [`IConsole_OUTPUT_LEVEL_ADDINFO`](constant.IConsole_OUTPUT_LEVEL_ADDINFO.html) + /// - [`IConsole_OUTPUT_LEVEL_DEBUG`](constant.IConsole_OUTPUT_LEVEL_DEBUG.html) + /// + /// `pFrom` is some sort of module name, e.g. for code in the console, + /// it is usually "console". Other examples include "client", + /// "client/network", … + /// + /// `pStr` is the actual log message. + /// + /// `PrintColor` is the intended log color. + /// + /// `PrintColor` defaults to [`gs_ConsoleDefaultColor`] in C++. + pub fn Print( + self: &IConsole, + Level: i32, + pFrom: StrRef<'_>, + pStr: StrRef<'_>, + PrintColor: ColorRGBA, + ); + + /// Register a command. + /// + /// This function needs a command name, a parameter shortcode, some + /// flags that specify metadata about the command, a callback function + /// with associated `UserPtr` and a help string. + /// + /// `Flags` must be a combination of + /// - [`CFGFLAG_SAVE`](../ddnet_engine_shared/constant.CFGFLAG_SAVE.html) + /// - [`CFGFLAG_CLIENT`](../ddnet_engine_shared/constant.CFGFLAG_CLIENT.html) + /// - [`CFGFLAG_SERVER`](../ddnet_engine_shared/constant.CFGFLAG_SERVER.html) + /// - [`CFGFLAG_STORE`](../ddnet_engine_shared/constant.CFGFLAG_STORE.html) + /// - [`CFGFLAG_MASTER`](../ddnet_engine_shared/constant.CFGFLAG_MASTER.html) + /// - [`CFGFLAG_ECON`](../ddnet_engine_shared/constant.CFGFLAG_ECON.html) + /// - [`CMDFLAG_TEST`](../ddnet_engine_shared/constant.CMDFLAG_TEST.html) + /// - [`CFGFLAG_CHAT`](../ddnet_engine_shared/constant.CFGFLAG_CHAT.html) + /// - [`CFGFLAG_GAME`](../ddnet_engine_shared/constant.CFGFLAG_GAME.html) + /// - [`CFGFLAG_NONTEEHISTORIC`](../ddnet_engine_shared/constant.CFGFLAG_NONTEEHISTORIC.html) + /// - [`CFGFLAG_COLLIGHT`](../ddnet_engine_shared/constant.CFGFLAG_COLLIGHT.html) + /// - [`CFGFLAG_INSENSITIVE`](../ddnet_engine_shared/constant.CFGFLAG_INSENSITIVE.html) + /// + /// # Parameter shortcode + /// + /// The following parameter types are supported: + /// + /// - `i`, `f`, `s`: **I**nteger, **f**loat, and **s**tring parameter, + /// respectively. Since they're not type-checked, they're all the + /// same, one word delimited by whitespace or any quoted string. + /// Examples: `12`, `example`, `"Hello, World!"`. + /// - `r`: **R**est of the command line, possibly multiple word, until + /// the next command or the end of line. Alternatively one quoted + /// parameter. Examples: `multiple words even without quotes`, + /// `"quotes are fine, too"`. + /// - `v`: **V**ictim client ID for this command. Supports the special + /// parameters `me` for the executing client ID, and `all` to target + /// all players. Examples: `0`, `63`, `words` (gets parsed as `0`), + /// `me`, `all`. + /// + /// The parameter shortcode is now a string consisting of any number of + /// these parameter type characters, a question mark `?` to mark the + /// beginning of optional parameters and `[]` help strings after + /// each parameter that can have a short explanation. + /// + /// Examples: + /// + /// - `echo` has `r[text]`: Takes the whole command line as the first + /// parameter. This means we get the following arguments: + /// ```text + /// ## "1" + /// echo 1 + /// + /// ## "multiple words" + /// echo multiple words + /// + /// ## error: missing argument + /// echo + /// + /// ## "multiple" + /// echo "multiple" "quoted" "arguments" + /// + /// ## "comments" + /// echo comments # work too + /// + /// ## "multiple"; "commands" + /// echo multiple; echo commands + /// ``` + /// - `muteid` has `v[id] i[seconds] ?r[reason]`. Assume the local + /// player has client ID 33, then we get the following + /// arguments: + /// ```text + /// ## "33" "60" + /// muteid me 60 + /// + /// ## "12" "120" "You're a wonderful person" + /// muteid 12 120 You're a wonderful person + /// + /// ## "0" "180"; "1" "180"; …, "63" "180" + /// muteid all 180 + /// ``` + /// + /// # Examples + /// + /// ``` + /// # extern crate ddnet_test; + /// use ddnet_base::UserPtr; + /// use ddnet_base::s; + /// use ddnet_engine::CreateConsole; + /// use ddnet_engine::IConsole; + /// use ddnet_engine::IConsole_FCommandCallback; + /// use ddnet_engine::IConsole_IResult; + /// use ddnet_engine::IConsole_OUTPUT_LEVEL_STANDARD; + /// use ddnet_engine::gs_ConsoleDefaultColor; + /// use ddnet_engine_shared::CFGFLAG_SERVER; + /// + /// extern "C" fn print_callback(_: &IConsole_IResult, user: UserPtr) { + /// let console: &IConsole = unsafe { user.cast() }; + /// console.Print(IConsole_OUTPUT_LEVEL_STANDARD, s!("example"), s!("print callback"), gs_ConsoleDefaultColor); + /// } + /// + /// let mut console = CreateConsole(CFGFLAG_SERVER); + /// let user = (&*console).into(); + /// console.pin_mut().Register(s!("example"), s!(""), CFGFLAG_SERVER, IConsole_FCommandCallback(print_callback), user, s!("Example command outputting a single line into the console")); + /// console.pin_mut().ExecuteLine(s!("example"), -1, true); + /// ``` + pub fn Register( + self: Pin<&mut IConsole>, + pName: StrRef<'_>, + pParams: StrRef<'_>, + Flags: i32, + pfnFunc: IConsole_FCommandCallback, + pUser: UserPtr, + pHelp: StrRef<'_>, + ); + + /// Create a console instance. + /// + /// It comes with a few registered commands by default. Only the + /// commands sharing a bit with the `FlagMask` parameter are considered + /// when executing commands. + /// + /// # Examples + /// + /// ``` + /// # extern crate ddnet_test; + /// use ddnet_base::s; + /// use ddnet_engine::CreateConsole; + /// use ddnet_engine_shared::CFGFLAG_SERVER; + /// + /// let mut console = CreateConsole(CFGFLAG_SERVER); + /// console.pin_mut().ExecuteLine(s!("cmdlist; echo hi!"), -1, true); + /// ``` + pub fn CreateConsole(FlagMask: i32) -> UniquePtr; + } +} + +/// Default console output color. White. +/// +/// Corresponds to the `gs_ConsoleDefaultColor` constant of the same name in +/// C++. +/// +/// Used as a last parameter in [`IConsole::Print`]. +/// +/// It is treated as "no color" in the console code, meaning that the default +/// color of the output medium isn't overriden. +#[allow(non_upper_case_globals)] +pub const gs_ConsoleDefaultColor: ColorRGBA = ColorRGBA { + r: 1.0, + g: 1.0, + b: 1.0, + a: 1.0, +}; + +/// Default console output level. +/// +/// Corresponds to the `IConsole::OUTPUT_LEVEL_STANDARD` enum value in C++. +/// +/// Used as a first parameter in [`IConsole::Print`]. +/// +/// This is the only level that is shown by default. +#[allow(non_upper_case_globals)] +pub const IConsole_OUTPUT_LEVEL_STANDARD: i32 = 0; +/// First more verbose console output level. +/// +/// Corresponds to the `IConsole::OUTPUT_LEVEL_ADDINFO` enum value in C++. +/// +/// Used as a first parameter in [`IConsole::Print`]. +/// +/// This output level is not shown by default. +#[allow(non_upper_case_globals)] +pub const IConsole_OUTPUT_LEVEL_ADDINFO: i32 = 1; +/// Most verbose console output level. +/// +/// Corresponds to the `IConsole::OUTPUT_LEVEL_DEBUG` enum value in C++. +/// +/// Used as a first parameter in [`IConsole::Print`]. +/// +/// This output level is not shown by default. +#[allow(non_upper_case_globals)] +pub const IConsole_OUTPUT_LEVEL_DEBUG: i32 = 2; diff --git a/src/engine/lib.rs b/src/engine/lib.rs new file mode 100644 index 000000000..eef3120d3 --- /dev/null +++ b/src/engine/lib.rs @@ -0,0 +1,19 @@ +//! DDNet's engine interfaces, Rust part. +//! +//! DDNet's code base is separated into three major parts, `base`, `engine` and +//! `game`. +//! +//! The engine consists of game-independent code such as display setup, +//! low-level network protocol, low-level map format, etc. +//! +//! This crate in particular corresponds to the `src/engine` directory that only +//! contains interfaces used by `engine` and `game` code. + +#![warn(missing_docs)] + +#[cfg(test)] +extern crate ddnet_test; + +mod console; + +pub use console::*; diff --git a/src/engine/rust.h b/src/engine/rust.h new file mode 100644 index 000000000..6e6ac2e12 --- /dev/null +++ b/src/engine/rust.h @@ -0,0 +1,7 @@ +#ifndef ENGINE_RUST_H +#define ENGINE_RUST_H +#include "console.h" + +typedef IConsole::IResult IConsole_IResult; +typedef IConsole::FCommandCallback IConsole_FCommandCallback; +#endif // ENGINE_RUST_H diff --git a/src/engine/server/main.cpp b/src/engine/server/main.cpp index 10fd8f16c..605a41abd 100644 --- a/src/engine/server/main.cpp +++ b/src/engine/server/main.cpp @@ -108,7 +108,7 @@ int main(int argc, const char **argv) IEngine *pEngine = CreateEngine(GAME_NAME, pFutureConsoleLogger, 2); IEngineMap *pEngineMap = CreateEngineMap(); IGameServer *pGameServer = CreateGameServer(); - IConsole *pConsole = CreateConsole(CFGFLAG_SERVER | CFGFLAG_ECON); + IConsole *pConsole = CreateConsole(CFGFLAG_SERVER | CFGFLAG_ECON).release(); IStorage *pStorage = CreateStorage(IStorage::STORAGETYPE_SERVER, argc, argv); IConfigManager *pConfigManager = CreateConfigManager(); IEngineAntibot *pEngineAntibot = CreateEngineAntibot(); diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index fc637a0f8..2e55840e8 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -3666,6 +3667,8 @@ void CServer::RegisterCommands() Console()->Register("name_unban", "s[name]", CFGFLAG_SERVER, ConNameUnban, this, "Unban a certain nickname"); Console()->Register("name_bans", "", CFGFLAG_SERVER, ConNameBans, this, "List all name bans"); + RustVersionRegister(*Console()); + Console()->Chain("sv_name", ConchainSpecialInfoupdate, this); Console()->Chain("loglevel", ConchainLoglevel, this); Console()->Chain("password", ConchainSpecialInfoupdate, this); diff --git a/src/engine/shared/Cargo.toml b/src/engine/shared/Cargo.toml new file mode 100644 index 000000000..b939c23d1 --- /dev/null +++ b/src/engine/shared/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ddnet-engine-shared" +version = "0.0.1" +edition = "2018" +publish = false +license = "Zlib" + +[lib] +crate-type = ["rlib", "staticlib"] +path = "lib.rs" + +[dependencies] +ddnet-base = { path = "../../base" } +ddnet-engine = { path = ".." } + +cxx = "1.0" + +[dev-dependencies] +ddnet-test = { path = "../../rust-bridge/test", features = ["link-test-libraries"] } diff --git a/src/engine/shared/build.rs b/src/engine/shared/build.rs new file mode 100644 index 000000000..a9d62f96b --- /dev/null +++ b/src/engine/shared/build.rs @@ -0,0 +1,18 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let mut out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); + out.push("rustc-version"); + let rustc = env::var_os("RUSTC").expect("RUSTC"); + let rustc_output = Command::new(rustc) + .arg("--version") + .output() + .expect("rustc --version"); + if !rustc_output.status.success() { + panic!("rustc --version: exit status {}", rustc_output.status); + } + fs::write(&out, rustc_output.stdout).expect("file write"); +} diff --git a/src/engine/shared/config.rs b/src/engine/shared/config.rs new file mode 100644 index 000000000..ca42303ee --- /dev/null +++ b/src/engine/shared/config.rs @@ -0,0 +1,57 @@ +/// Config variable that is saved when the client is closed. +/// +/// Has no effect on other commands. +pub const CFGFLAG_SAVE: i32 = 1 << 0; + +/// Command that is available in the client. +pub const CFGFLAG_CLIENT: i32 = 1 << 1; + +/// Command that is available on the server. +pub const CFGFLAG_SERVER: i32 = 1 << 2; + +/// Command that is delayed in the execution until +/// `IConsole::StoreCommands(false)` is called. +pub const CFGFLAG_STORE: i32 = 1 << 3; + +/// Command that is available in the master server. +pub const CFGFLAG_MASTER: i32 = 1 << 4; + +/// Command that has something to do with the external console (econ). +pub const CFGFLAG_ECON: i32 = 1 << 5; + +/// Command that can be used for testing or cheating maps. Only available if +/// `sv_test_cmds 1` is set. +pub const CMDFLAG_TEST: i32 = 1 << 6; + +/// Command that can be used from the chat on the server. +pub const CFGFLAG_CHAT: i32 = 1 << 7; + +/// Command that can be used from a map config. +/// +/// Only commands that are not security sensitive should have this flag. +pub const CFGFLAG_GAME: i32 = 1 << 8; + +/// Command that is not recorded into teehistorian. +/// +/// This should only be set for security sensitive commands like passwords etc. +/// that should not be recorded. +pub const CFGFLAG_NONTEEHISTORIC: i32 = 1 << 9; + +/// Color config variable that can only have lightness 0.5 to 1.0. +/// +/// This is achieved by dividing the lightness channel by and adding 0.5, i.e. +/// remapping all the colors. +/// +/// Has no effect on other commands or config variables. +pub const CFGFLAG_COLLIGHT: i32 = 1 << 10; + +/// Color config variable that includes an alpha (opacity) value. +/// +/// Has no effect on other commands or config variables. +pub const CFGFLAG_COLALPHA: i32 = 1 << 11; + +/// Config variable with insensitive data that can be included in client +/// integrity checks. +/// +/// This should only be set on config variables the server could observe anyway. +pub const CFGFLAG_INSENSITIVE: i32 = 1 << 12; diff --git a/src/engine/shared/console.cpp b/src/engine/shared/console.cpp index d0e0f39cb..2542c8383 100644 --- a/src/engine/shared/console.cpp +++ b/src/engine/shared/console.cpp @@ -19,28 +19,28 @@ // todo: rework this -const char *CConsole::CResult::GetString(unsigned Index) +const char *CConsole::CResult::GetString(unsigned Index) const { if(Index >= m_NumArgs) return ""; return m_apArgs[Index]; } -int CConsole::CResult::GetInteger(unsigned Index) +int CConsole::CResult::GetInteger(unsigned Index) const { if(Index >= m_NumArgs) return 0; return str_toint(m_apArgs[Index]); } -float CConsole::CResult::GetFloat(unsigned Index) +float CConsole::CResult::GetFloat(unsigned Index) const { if(Index >= m_NumArgs) return 0.0f; return str_tofloat(m_apArgs[Index]); } -ColorHSLA CConsole::CResult::GetColor(unsigned Index, bool Light) +ColorHSLA CConsole::CResult::GetColor(unsigned Index, bool Light) const { ColorHSLA Hsla = ColorHSLA(0, 0, 0); if(Index >= m_NumArgs) @@ -322,7 +322,7 @@ LOG_COLOR ColorToLogColor(ColorRGBA Color) (uint8_t)(Color.b * 255.0)}; } -void CConsole::Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor) +void CConsole::Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor) const { LEVEL LogLevel = IConsole::ToLogLevel(Level); // if the color is pure white, use default terminal color @@ -1268,7 +1268,7 @@ const IConsole::CCommandInfo *CConsole::GetCommandInfo(const char *pName, int Fl return 0; } -extern IConsole *CreateConsole(int FlagMask) { return new CConsole(FlagMask); } +std::unique_ptr CreateConsole(int FlagMask) { return std::make_unique(FlagMask); } void CConsole::ResetServerGameSettings() { @@ -1307,7 +1307,7 @@ void CConsole::ResetServerGameSettings() #undef MACRO_CONFIG_STR } -int CConsole::CResult::GetVictim() +int CConsole::CResult::GetVictim() const { return m_Victim; } diff --git a/src/engine/shared/console.h b/src/engine/shared/console.h index c107c9eb0..f55f77e5b 100644 --- a/src/engine/shared/console.h +++ b/src/engine/shared/console.h @@ -114,10 +114,10 @@ class CConsole : public IConsole m_apArgs[m_NumArgs++] = pArg; } - const char *GetString(unsigned Index) override; - int GetInteger(unsigned Index) override; - float GetFloat(unsigned Index) override; - ColorHSLA GetColor(unsigned Index, bool Light) override; + const char *GetString(unsigned Index) const override; + int GetInteger(unsigned Index) const override; + float GetFloat(unsigned Index) const override; + ColorHSLA GetColor(unsigned Index, bool Light) const override; void RemoveArgument(unsigned Index) override { @@ -142,7 +142,7 @@ class CConsole : public IConsole bool HasVictim(); void SetVictim(int Victim); void SetVictim(const char *pVictim); - int GetVictim() override; + int GetVictim() const override; }; int ParseStart(CResult *pResult, const char *pString, int Length); @@ -215,7 +215,7 @@ public: void ExecuteFile(const char *pFilename, int ClientID = -1, bool LogFailure = false, int StorageType = IStorage::TYPE_ALL) override; char *Format(char *pBuf, int Size, const char *pFrom, const char *pStr) override; - void Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor = gs_ConsoleDefaultColor) override; + void Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor = gs_ConsoleDefaultColor) const override; void SetTeeHistorianCommandCallback(FTeeHistorianCommandCallback pfnCallback, void *pUser) override; void SetUnknownCommandCallback(FUnknownCommandCallback pfnCallback, void *pUser) override; void InitChecksum(CChecksumData *pData) const override; diff --git a/src/engine/shared/lib.rs b/src/engine/shared/lib.rs new file mode 100644 index 000000000..95002283b --- /dev/null +++ b/src/engine/shared/lib.rs @@ -0,0 +1,21 @@ +//! DDNet's engine interfaces, Rust part. +//! +//! DDNet's code base is separated into three major parts, `base`, `engine` and +//! `game`. +//! +//! The engine consists of game-independent code such as display setup, +//! low-level network protocol, low-level map format, etc. +//! +//! This crate in particular corresponds to the `src/engine/shared` directory +//! that contains code shared between client, server and other components. + +#![warn(missing_docs)] + +#[cfg(test)] +extern crate ddnet_test; + +mod config; +mod rust_version; + +pub use config::*; +pub use rust_version::*; diff --git a/src/engine/shared/rust_version.rs b/src/engine/shared/rust_version.rs new file mode 100644 index 000000000..dc65764dc --- /dev/null +++ b/src/engine/shared/rust_version.rs @@ -0,0 +1,59 @@ +use super::CFGFLAG_CLIENT; +use super::CFGFLAG_SERVER; +use ddnet_base::s; +use ddnet_base::UserPtr; +use ddnet_engine::gs_ConsoleDefaultColor; +use ddnet_engine::IConsole; +use ddnet_engine::IConsole_FCommandCallback; +use ddnet_engine::IConsole_IResult; +use ddnet_engine::IConsole_OUTPUT_LEVEL_STANDARD; +use std::pin::Pin; + +#[cxx::bridge] +mod ffi { + extern "C++" { + include!("base/rust.h"); + include!("engine/console.h"); + + type IConsole = ddnet_engine::IConsole; + } + extern "Rust" { + fn RustVersionPrint(console: &IConsole); + fn RustVersionRegister(console: Pin<&mut IConsole>); + } +} + +/// Print the Rust version used for compiling this crate. +/// +/// Uses [`IConsole::Print`] for printing. +#[allow(non_snake_case)] +pub fn RustVersionPrint(console: &IConsole) { + console.Print( + IConsole_OUTPUT_LEVEL_STANDARD, + s!("rust_version"), + s!(include_str!(concat!(env!("OUT_DIR"), "/rustc-version"))), + gs_ConsoleDefaultColor, + ); +} + +#[allow(non_snake_case)] +extern "C" fn PrintRustVersionCallback(_: &IConsole_IResult, user: UserPtr) { + RustVersionPrint(unsafe { user.cast() }) +} + +/// Register the `rust_version` command to the given console instance. +/// +/// This command calls the [`RustVersionPrint`] function to print the Rust +/// version used for compiling this crate. +#[allow(non_snake_case)] +pub fn RustVersionRegister(console: Pin<&mut IConsole>) { + let user = console.as_ref().get_ref().into(); + console.Register( + s!("rust_version"), + s!(""), + CFGFLAG_CLIENT | CFGFLAG_SERVER, + IConsole_FCommandCallback(PrintRustVersionCallback), + user, + s!("Prints the Rust version used to compile DDNet"), + ); +} diff --git a/src/game/ddracecommands.h b/src/game/ddracecommands.h index 9d43aadff..1c6265e8e 100644 --- a/src/game/ddracecommands.h +++ b/src/game/ddracecommands.h @@ -43,8 +43,8 @@ CONSOLE_COMMAND("move_raw", "i[x] i[y]", CFGFLAG_SERVER | CMDFLAG_TEST, ConMoveR CONSOLE_COMMAND("force_pause", "v[id] i[seconds]", CFGFLAG_SERVER, ConForcePause, this, "Force i to pause for i seconds") CONSOLE_COMMAND("force_unpause", "v[id]", CFGFLAG_SERVER, ConForcePause, this, "Set force-pause timer of i to 0.") -CONSOLE_COMMAND("set_team_ddr", "v[id] ?i[team]", CFGFLAG_SERVER, ConSetDDRTeam, this, "Set ddrace team of a player") -CONSOLE_COMMAND("uninvite", "v[id] ?i[team]", CFGFLAG_SERVER, ConUninvite, this, "Uninvite player from team") +CONSOLE_COMMAND("set_team_ddr", "v[id] i[team]", CFGFLAG_SERVER, ConSetDDRTeam, this, "Set ddrace team of a player") +CONSOLE_COMMAND("uninvite", "v[id] i[team]", CFGFLAG_SERVER, ConUninvite, this, "Uninvite player from team") CONSOLE_COMMAND("vote_mute", "v[id] i[seconds]", CFGFLAG_SERVER, ConVoteMute, this, "Remove v's right to vote for i seconds") CONSOLE_COMMAND("vote_unmute", "v[id]", CFGFLAG_SERVER, ConVoteUnmute, this, "Give back v's right to vote.") diff --git a/src/mastersrv/Cargo.toml b/src/mastersrv/Cargo.toml index cba7d1988..603536304 100644 --- a/src/mastersrv/Cargo.toml +++ b/src/mastersrv/Cargo.toml @@ -3,6 +3,10 @@ name = "mastersrv" version = "0.0.1" authors = ["heinrich5991 "] edition = "2018" +publish = false +license = "Zlib" + +[workspace] [dependencies] arrayvec = { version = "0.5.2", features = ["serde"] } diff --git a/src/rust-bridge/.clang-tidy b/src/rust-bridge/.clang-tidy new file mode 100644 index 000000000..d550bc98a --- /dev/null +++ b/src/rust-bridge/.clang-tidy @@ -0,0 +1,3 @@ +# Need at least one check, otherwise clang-tidy fails, use one that can't +# happen in our code since we don't use OpenMP API. +Checks: '-*,openmp-exception-escape' diff --git a/src/rust-bridge/cpp/console.cpp b/src/rust-bridge/cpp/console.cpp new file mode 100644 index 000000000..8c0b00bb4 --- /dev/null +++ b/src/rust-bridge/cpp/console.cpp @@ -0,0 +1,164 @@ +#include "base/rust.h" +#include "engine/console.h" +#include "engine/rust.h" +#include +#include +#include +#include +#include +#include + +namespace rust { +inline namespace cxxbridge1 { +// #include "rust/cxx.h" + +#ifndef CXXBRIDGE1_IS_COMPLETE +#define CXXBRIDGE1_IS_COMPLETE +namespace detail { +namespace { +template +struct is_complete : std::false_type {}; +template +struct is_complete : std::true_type {}; +} // namespace +} // namespace detail +#endif // CXXBRIDGE1_IS_COMPLETE + +#ifndef CXXBRIDGE1_RELOCATABLE +#define CXXBRIDGE1_RELOCATABLE +namespace detail { +template +struct make_void { + using type = void; +}; + +template +using void_t = typename make_void::type; + +template class, typename...> +struct detect : std::false_type {}; +template