From 7aea7078a5cd8df7abf3d433b0ad3446a65e4a9e Mon Sep 17 00:00:00 2001 From: zottce Date: Tue, 30 Jun 2026 01:17:03 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20open.mp=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/rust.yml | 60 +- Cargo.lock | 160 +- Cargo.toml | 2 + docs/build.md | 11 + openmp-component/Cargo.toml | 17 + openmp-component/README.md | 52 + openmp-component/build.rs | 7 + openmp-component/component/CMakeLists.txt | 207 +++ openmp-component/component/CMakePresets.json | 28 + .../component/copy_cxxbridge_header.cmake | 18 + openmp-component/component/src/component.cpp | 1649 +++++++++++++++++ openmp-component/src/lib.rs | 304 +++ readme.md | 12 + scripts/build-openmp-component.sh | 282 +++ server-core/Cargo.toml | 13 + {server => server-core}/src/client.rs | 2 +- server-core/src/lib.rs | 246 +++ {server => server-core}/src/server.rs | 54 +- server/Cargo.toml | 6 +- server/src/lib.rs | 183 +- 20 files changed, 3119 insertions(+), 194 deletions(-) create mode 100644 openmp-component/Cargo.toml create mode 100644 openmp-component/README.md create mode 100644 openmp-component/build.rs create mode 100644 openmp-component/component/CMakeLists.txt create mode 100644 openmp-component/component/CMakePresets.json create mode 100644 openmp-component/component/copy_cxxbridge_header.cmake create mode 100644 openmp-component/component/src/component.cpp create mode 100644 openmp-component/src/lib.rs create mode 100755 scripts/build-openmp-component.sh create mode 100644 server-core/Cargo.toml rename {server => server-core}/src/client.rs (96%) create mode 100644 server-core/src/lib.rs rename {server => server-core}/src/server.rs (89%) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fa2a55a..9dafcc5 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -26,11 +26,20 @@ jobs: toolchain: stable targets: i686-unknown-linux-gnu - - name: Install i386 deps + - name: Install build deps run: | sudo dpkg --add-architecture i386 sudo apt-get update - sudo apt-get install -y build-essential g++-multilib + sudo apt-get install -y build-essential cmake git g++-multilib + + - name: Checkout open.mp + uses: actions/checkout@v4 + with: + repository: openmultiplayer/open.mp + path: open.mp + + - name: Checkout open.mp SDK submodules + run: git -C open.mp submodule update --init --recursive SDK lib/pawn - name: Cache cargo registry uses: actions/cache@v4 @@ -56,12 +65,25 @@ jobs: - name: Build server plugin run: | cargo build --package server --release --target i686-unknown-linux-gnu - strip -s target/i686-unknown-linux-gnu/release/libserver.so -o stripped.so + strip -s target/i686-unknown-linux-gnu/release/libserver.so -o cef-samp.so - uses: actions/upload-artifact@v4 with: name: cef-${{ matrix.os }}.so - path: stripped.so + path: cef-samp.so + + - name: Build open.mp component + run: | + scripts/build-openmp-component.sh \ + --openmp-root "$GITHUB_WORKSPACE/open.mp" \ + --build-dir "$GITHUB_WORKSPACE/target/openmp-component-build-${{ matrix.os }}" \ + --jobs 2 + strip -s "target/openmp-component-build-${{ matrix.os }}/cef.so" -o cef-openmp.so + + - uses: actions/upload-artifact@v4 + with: + name: cef-openmp-${{ matrix.os }}.so + path: cef-openmp.so build_server_debian: runs-on: ubuntu-latest @@ -71,11 +93,11 @@ jobs: os: [12, 13] steps: - - name: Install i386 deps + - name: Install build deps run: | dpkg --add-architecture i386 apt-get update - apt-get install -y build-essential curl make g++-multilib + apt-get install -y build-essential ca-certificates cmake curl git make g++-multilib - name: Install stable + i686 target uses: dtolnay/rust-toolchain@master @@ -85,6 +107,15 @@ jobs: - uses: actions/checkout@v4 + - name: Checkout open.mp + uses: actions/checkout@v4 + with: + repository: openmultiplayer/open.mp + path: open.mp + + - name: Checkout open.mp SDK submodules + run: git -C open.mp submodule update --init --recursive SDK lib/pawn + - name: Cache cargo registry uses: actions/cache@v4 with: @@ -109,12 +140,25 @@ jobs: - name: Build server plugin run: | cargo build --package server --release --target i686-unknown-linux-gnu - strip -s target/i686-unknown-linux-gnu/release/libserver.so -o stripped.so + strip -s target/i686-unknown-linux-gnu/release/libserver.so -o cef-samp.so - uses: actions/upload-artifact@v4 with: name: cef-debian-${{ matrix.os }}.so - path: stripped.so + path: cef-samp.so + + - name: Build open.mp component + run: | + scripts/build-openmp-component.sh \ + --openmp-root "$GITHUB_WORKSPACE/open.mp" \ + --build-dir "$GITHUB_WORKSPACE/target/openmp-component-build-debian-${{ matrix.os }}" \ + --jobs 2 + strip -s "target/openmp-component-build-debian-${{ matrix.os }}/cef.so" -o cef-openmp.so + + - uses: actions/upload-artifact@v4 + with: + name: cef-openmp-debian-${{ matrix.os }}.so + path: cef-openmp.so build_client: runs-on: windows-latest diff --git a/Cargo.lock b/Cargo.lock index 84a1031..187f108 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.100" @@ -316,6 +322,32 @@ dependencies = [ "libloading 0.8.9", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "claxon" version = "0.4.3" @@ -369,6 +401,17 @@ dependencies = [ "cc", ] +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "colored" version = "1.9.4" @@ -465,6 +508,68 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "cxx" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2 1.0.106", + "quote 1.0.44", + "scratch", + "syn 2.0.114", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2 1.0.106", + "quote 1.0.44", + "syn 2.0.114", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf" +dependencies = [ + "indexmap", + "proc-macro2 1.0.106", + "quote 1.0.44", + "syn 2.0.114", +] + [[package]] name = "darling" version = "0.13.4" @@ -485,7 +590,7 @@ dependencies = [ "ident_case", "proc-macro2 1.0.106", "quote 1.0.44", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] @@ -611,6 +716,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1133,6 +1244,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + [[package]] name = "litemap" version = "0.8.1" @@ -2202,6 +2322,15 @@ dependencies = [ "samp-sdk", ] +[[package]] +name = "samp-cef-openmp" +version = "0.1.0" +dependencies = [ + "cxx", + "cxx-build", + "server-core", +] + [[package]] name = "samp-codegen" version = "0.1.1" @@ -2237,6 +2366,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + [[package]] name = "security-framework" version = "3.5.1" @@ -2306,14 +2441,21 @@ dependencies = [ [[package]] name = "server" version = "0.1.0" +dependencies = [ + "log", + "samp", + "server-core", +] + +[[package]] +name = "server-core" +version = "0.1.0" dependencies = [ "crossbeam-channel", "log", "messages", "network", "quick-protobuf", - "samp", - "simplelog", ] [[package]] @@ -2437,6 +2579,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2677,6 +2825,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7e75529..8e925a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,9 @@ members = [ "client", "renderer", "messages", + "server-core", "server", + "openmp-component", "cef-api", "cef-interface", "network", diff --git a/docs/build.md b/docs/build.md index 6c6a6c9..e6e460c 100644 --- a/docs/build.md +++ b/docs/build.md @@ -1,5 +1,16 @@ # Build Notes +## open.mp component +- Default local build: `scripts/build-openmp-component.sh --openmp-root /path/to/open.mp`. +- Build and install into a server: `scripts/build-openmp-component.sh --openmp-root /path/to/open.mp --server-root /path/to/omp-server`. +- Clean rebuild: add `--clean`. + +The open.mp checkout path is required through `OPEN_MP_ROOT` or `--openmp-root`. + +Installed component artifacts go into `/components`. Load order is `CEF`. + +GitHub Actions builds the open.mp component next to the SA:MP server plugin. + ## Cross-compiling the Windows client (macOS/Linux) - Install `cargo-xwin`: `cargo install cargo-xwin --locked`. - Ensure `XWIN_ACCEPT_LICENSE=1` is set (the script defaults it). diff --git a/openmp-component/Cargo.toml b/openmp-component/Cargo.toml new file mode 100644 index 0000000..8d5f2c2 --- /dev/null +++ b/openmp-component/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "samp-cef-openmp" +version = "0.1.0" +authors = ["ZOTTCE "] +edition = "2024" +rust-version = "1.93" + +[lib] +name = "samp_cef_openmp" +crate-type = ["staticlib"] + +[dependencies] +cxx = "1.0" +server-core = { path = "../server-core" } + +[build-dependencies] +cxx-build = "1.0" diff --git a/openmp-component/README.md b/openmp-component/README.md new file mode 100644 index 0000000..aadbd32 --- /dev/null +++ b/openmp-component/README.md @@ -0,0 +1,52 @@ +# open.mp component + +This crate builds the Rust side of the native open.mp CEF component. The C++ component wrapper is in `component/` and links the Rust static library through `cxx`. + +## Build + +Recommended build from the repository root: + +```sh +scripts/build-openmp-component.sh --openmp-root /path/to/open.mp +``` + +The script expects: + +- `OPEN_MP_ROOT` or `--openmp-root` +- `target/openmp-component-build` for the CEF component build directory + +To build and install into an open.mp server: + +```sh +scripts/build-openmp-component.sh --openmp-root /path/to/open.mp --server-root /path/to/omp-server +``` + +This installs `cef.*` into `/path/to/omp-server/components`. + +The output library is `target/openmp-component-build/cef.*`. Put it into the open.mp `components` directory and add `CEF`/`cef` to the server `components` config list. + +The component binds its CEF transport to `network.bind:(network.port + cef.port_offset)`. The default `cef.port_offset` is `2`, matching the SA:MP plugin behavior. + +## CMake + +The script is the easiest path, but the component can still be built directly with CMake: + +```sh +cmake -S openmp-component/component -B target/openmp-component-build \ + -DOPEN_MP_ROOT=/path/to/open.mp +cmake --build target/openmp-component-build --target cef +``` + +CMake presets are available from the component source directory: + +```sh +export OPEN_MP_ROOT=/path/to/open.mp +cmake -S openmp-component/component --preset default +(cd openmp-component/component && cmake --build --preset default) +``` + +The install rules are isolated under the `samp-cef-openmp` CMake install component, so installing CEF does not also install open.mp SDK dependencies: + +```sh +cmake --install target/openmp-component-build --component samp-cef-openmp +``` diff --git a/openmp-component/build.rs b/openmp-component/build.rs new file mode 100644 index 0000000..e21240b --- /dev/null +++ b/openmp-component/build.rs @@ -0,0 +1,7 @@ +fn main() { + cxx_build::bridge("src/lib.rs") + .std("c++17") + .compile("samp-cef-openmp-cxxbridge"); + + println!("cargo:rerun-if-changed=src/lib.rs"); +} diff --git a/openmp-component/component/CMakeLists.txt b/openmp-component/component/CMakeLists.txt new file mode 100644 index 0000000..816fdca --- /dev/null +++ b/openmp-component/component/CMakeLists.txt @@ -0,0 +1,207 @@ +cmake_minimum_required(VERSION 3.19) +project(samp-cef-openmp-component LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(SAMP_CEF_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." CACHE PATH "Path to the samp-cef repository") +set(OPEN_MP_ROOT "$ENV{OPEN_MP_ROOT}" CACHE PATH "Path to the open.mp repository") + +set(OPENMP_EXT_ROOT "$ENV{OPENMP_EXT_ROOT}" CACHE PATH "Path to the openmp-ext gRPC component repository") +set(OPENMP_EXT_BUILD_DIR "$ENV{OPENMP_EXT_BUILD_DIR}" CACHE PATH "Path to the openmp-ext build directory") +if(NOT OPENMP_EXT_BUILD_DIR AND OPENMP_EXT_ROOT) + set(OPENMP_EXT_BUILD_DIR "${OPENMP_EXT_ROOT}/build") +endif() +set(SAMP_CEF_ENABLE_GRPC_EXT "OFF" CACHE STRING "Enable openmp-ext gRPC integration: AUTO, ON, or OFF") +set_property(CACHE SAMP_CEF_ENABLE_GRPC_EXT PROPERTY STRINGS AUTO ON OFF) +set(OPEN_MP_SERVER_ROOT "$ENV{OPEN_MP_SERVER_ROOT}" CACHE PATH "Optional open.mp server root used by the install target") +set(SAMP_CEF_INSTALL_COMPONENT_DIR "" CACHE PATH "Directory where open.mp component binaries are installed") +set(SAMP_CEF_INSTALL_COMPONENT_NAME "samp-cef-openmp" CACHE STRING "CMake install component used for CEF open.mp artifacts") +set(SAMP_CEF_INSTALL_GRPC_EXT ON CACHE BOOL "Install omp-grpc next to cef when gRPC integration is enabled") + +if(NOT EXISTS "${OPEN_MP_ROOT}/SDK/CMakeLists.txt") + message(FATAL_ERROR "OPEN_MP_ROOT must point to an open.mp checkout") +endif() + +find_program(CARGO_EXECUTABLE cargo REQUIRED) + +set(CMAKE_POLICY_VERSION_MINIMUM 3.5) +add_subdirectory("${OPEN_MP_ROOT}/SDK" "${CMAKE_CURRENT_BINARY_DIR}/openmp-sdk") + +set(SAMP_CEF_RUST_TARGET "" CACHE STRING "Optional Cargo target triple for the Rust static library") +set(SAMP_CEF_CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargo-target" CACHE PATH "Cargo target directory") +set(RUST_MANIFEST "${SAMP_CEF_ROOT}/openmp-component/Cargo.toml") +set(GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/include") +set(GENERATED_CXXBRIDGE_HEADER "${GENERATED_INCLUDE_DIR}/samp-cef-openmp/src/lib.rs.h") + +set(CARGO_BUILD_ARGS build --release --manifest-path "${RUST_MANIFEST}") +if(SAMP_CEF_RUST_TARGET) + list(APPEND CARGO_BUILD_ARGS --target "${SAMP_CEF_RUST_TARGET}") + set(RUST_LIB_DIR "${SAMP_CEF_CARGO_TARGET_DIR}/${SAMP_CEF_RUST_TARGET}/release") +else() + set(RUST_LIB_DIR "${SAMP_CEF_CARGO_TARGET_DIR}/release") +endif() + +if(WIN32) + set(RUST_STATIC_LIB "${RUST_LIB_DIR}/samp_cef_openmp.lib") +else() + set(RUST_STATIC_LIB "${RUST_LIB_DIR}/libsamp_cef_openmp.a") +endif() + +add_custom_command( + OUTPUT + "${RUST_STATIC_LIB}" + "${GENERATED_CXXBRIDGE_HEADER}" + COMMAND + "${CMAKE_COMMAND}" -E env + "CARGO_TARGET_DIR=${SAMP_CEF_CARGO_TARGET_DIR}" + "${CARGO_EXECUTABLE}" ${CARGO_BUILD_ARGS} + COMMAND + "${CMAKE_COMMAND}" + "-DRUST_LIB_DIR=${RUST_LIB_DIR}" + "-DGENERATED_INCLUDE_DIR=${GENERATED_INCLUDE_DIR}" + -P "${CMAKE_CURRENT_LIST_DIR}/copy_cxxbridge_header.cmake" + WORKING_DIRECTORY "${SAMP_CEF_ROOT}" + DEPENDS + "${SAMP_CEF_ROOT}/Cargo.toml" + "${SAMP_CEF_ROOT}/Cargo.lock" + "${RUST_MANIFEST}" + "${SAMP_CEF_ROOT}/openmp-component/build.rs" + "${SAMP_CEF_ROOT}/openmp-component/src/lib.rs" + "${SAMP_CEF_ROOT}/server-core/src/lib.rs" + "${SAMP_CEF_ROOT}/server-core/src/server.rs" + "${SAMP_CEF_ROOT}/server-core/src/client.rs" + VERBATIM +) + +add_custom_target(samp_cef_openmp_rust DEPENDS "${RUST_STATIC_LIB}" "${GENERATED_CXXBRIDGE_HEADER}") + +add_library(cef SHARED + src/component.cpp +) + +set(SAMP_CEF_GRPC_EXT_ENABLED FALSE) + +add_dependencies(cef samp_cef_openmp_rust) + +target_include_directories(cef PRIVATE + "${GENERATED_INCLUDE_DIR}" + "${OPEN_MP_ROOT}/lib/pawn/source" + "${OPEN_MP_ROOT}/lib/pawn/source/linux" +) + +target_link_libraries(cef PRIVATE + OMP-SDK + "${RUST_STATIC_LIB}" +) + +target_compile_definitions(cef PRIVATE + OMP_VERSION_MAJOR=1 + OMP_VERSION_MINOR=5 + OMP_VERSION_PATCH=8 + FLOATPOINT + PAWN_CELL_SIZE=32 +) + +string(TOUPPER "${SAMP_CEF_ENABLE_GRPC_EXT}" SAMP_CEF_ENABLE_GRPC_EXT_UPPER) +set(OPENMP_EXT_PROTO_DIR "${OPENMP_EXT_BUILD_DIR}/generated/proto") +if(APPLE) + set(OPENMP_EXT_GRPC_DEFAULT_LIBRARY "${OPENMP_EXT_BUILD_DIR}/omp-grpc.dylib") +elseif(WIN32) + set(OPENMP_EXT_GRPC_DEFAULT_LIBRARY "${OPENMP_EXT_BUILD_DIR}/omp-grpc.dll") +else() + set(OPENMP_EXT_GRPC_DEFAULT_LIBRARY "${OPENMP_EXT_BUILD_DIR}/omp-grpc.so") +endif() +set(OPENMP_EXT_GRPC_LIBRARY "${OPENMP_EXT_GRPC_DEFAULT_LIBRARY}" CACHE FILEPATH "Path to the built omp-grpc shared library") + +set(OPENMP_EXT_AVAILABLE FALSE) +if(OPENMP_EXT_ROOT + AND OPENMP_EXT_BUILD_DIR + AND EXISTS "${OPENMP_EXT_ROOT}/include/omp_ext/component.hpp" + AND EXISTS "${OPENMP_EXT_PROTO_DIR}/omp_ext.pb.h" + AND EXISTS "${OPENMP_EXT_PROTO_DIR}/omp_ext.pb.cc" + AND EXISTS "${OPENMP_EXT_PROTO_DIR}/omp_ext.grpc.pb.h" + AND EXISTS "${OPENMP_EXT_GRPC_LIBRARY}") + set(OPENMP_EXT_AVAILABLE TRUE) +endif() + +if(SAMP_CEF_ENABLE_GRPC_EXT_UPPER STREQUAL "ON" OR (SAMP_CEF_ENABLE_GRPC_EXT_UPPER STREQUAL "AUTO" AND OPENMP_EXT_AVAILABLE)) + if(NOT OPENMP_EXT_AVAILABLE) + message(FATAL_ERROR "SAMP_CEF_ENABLE_GRPC_EXT is ON, but openmp-ext headers/proto/library were not found. Set OPENMP_EXT_ROOT/OPENMP_EXT_BUILD_DIR or use -DSAMP_CEF_ENABLE_GRPC_EXT=OFF.") + endif() + + find_package(Protobuf CONFIG QUIET) + find_package(gRPC CONFIG QUIET) + + if(NOT TARGET protobuf::libprotobuf OR NOT TARGET gRPC::grpc++) + if(SAMP_CEF_ENABLE_GRPC_EXT_UPPER STREQUAL "ON") + message(FATAL_ERROR "SAMP_CEF_ENABLE_GRPC_EXT is ON, but Protobuf/gRPC CMake packages were not found. Set CMAKE_PREFIX_PATH or use -DSAMP_CEF_ENABLE_GRPC_EXT=OFF.") + endif() + message(STATUS "openmp-ext gRPC integration disabled: Protobuf/gRPC CMake packages were not found") + else() + add_library(omp_grpc_ext SHARED IMPORTED) + set_target_properties(omp_grpc_ext PROPERTIES IMPORTED_LOCATION "${OPENMP_EXT_GRPC_LIBRARY}") + + set(SAMP_CEF_GRPC_EXT_ENABLED TRUE) + message(STATUS "openmp-ext gRPC integration enabled: ${OPENMP_EXT_GRPC_LIBRARY}") + target_sources(cef PRIVATE "${OPENMP_EXT_PROTO_DIR}/omp_ext.pb.cc") + target_compile_definitions(cef PRIVATE SAMP_CEF_HAS_GRPC_EXT=1) + target_include_directories(cef PRIVATE + "${OPENMP_EXT_ROOT}/include" + "${OPENMP_EXT_PROTO_DIR}" + "${OPEN_MP_ROOT}/CAPI/include" + ) + target_link_libraries(cef PRIVATE + omp_grpc_ext + protobuf::libprotobuf + gRPC::grpc++ + ) + + if(APPLE) + set_property(TARGET cef APPEND PROPERTY BUILD_RPATH "@loader_path") + elseif(UNIX) + set_property(TARGET cef APPEND PROPERTY BUILD_RPATH "$ORIGIN") + endif() + endif() +elseif(SAMP_CEF_ENABLE_GRPC_EXT_UPPER STREQUAL "AUTO") + message(STATUS "openmp-ext gRPC integration disabled: headers/proto/library were not found") +elseif(NOT SAMP_CEF_ENABLE_GRPC_EXT_UPPER STREQUAL "OFF") + message(FATAL_ERROR "SAMP_CEF_ENABLE_GRPC_EXT must be AUTO, ON, or OFF") +endif() + +if(UNIX) + target_compile_definitions(cef PRIVATE LINUX) + if(NOT APPLE) + target_compile_options(cef PRIVATE -fno-eliminate-unused-debug-types) + endif() +endif() + +if(APPLE) + find_library(SECURITY_FRAMEWORK Security REQUIRED) + find_library(COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED) + target_link_libraries(cef PRIVATE + "${SECURITY_FRAMEWORK}" + "${COREFOUNDATION_FRAMEWORK}" + ) +endif() + +set_property(TARGET cef PROPERTY OUTPUT_NAME cef) +set_property(TARGET cef PROPERTY PREFIX "") + +if(NOT SAMP_CEF_INSTALL_COMPONENT_DIR) + if(OPEN_MP_SERVER_ROOT) + set(SAMP_CEF_INSTALL_COMPONENT_DIR "${OPEN_MP_SERVER_ROOT}/components") + else() + set(SAMP_CEF_INSTALL_COMPONENT_DIR "components") + endif() +endif() + +install(TARGETS cef + RUNTIME DESTINATION "${SAMP_CEF_INSTALL_COMPONENT_DIR}" COMPONENT "${SAMP_CEF_INSTALL_COMPONENT_NAME}" + LIBRARY DESTINATION "${SAMP_CEF_INSTALL_COMPONENT_DIR}" COMPONENT "${SAMP_CEF_INSTALL_COMPONENT_NAME}" + ARCHIVE DESTINATION "${SAMP_CEF_INSTALL_COMPONENT_DIR}" COMPONENT "${SAMP_CEF_INSTALL_COMPONENT_NAME}" +) + +if(SAMP_CEF_GRPC_EXT_ENABLED AND SAMP_CEF_INSTALL_GRPC_EXT) + install(FILES "${OPENMP_EXT_GRPC_LIBRARY}" DESTINATION "${SAMP_CEF_INSTALL_COMPONENT_DIR}" COMPONENT "${SAMP_CEF_INSTALL_COMPONENT_NAME}") +endif() diff --git a/openmp-component/component/CMakePresets.json b/openmp-component/component/CMakePresets.json new file mode 100644 index 0000000..7aa24a5 --- /dev/null +++ b/openmp-component/component/CMakePresets.json @@ -0,0 +1,28 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 21, + "patch": 0 + }, + "configurePresets": [ + { + "name": "default", + "displayName": "open.mp component", + "description": "Build CEF for open.mp.", + "binaryDir": "${sourceParentDir}/../target/openmp-component-build", + "cacheVariables": { + "SAMP_CEF_ENABLE_GRPC_EXT": "OFF" + } + } + ], + "buildPresets": [ + { + "name": "default", + "configurePreset": "default", + "targets": [ + "cef" + ] + } + ] +} diff --git a/openmp-component/component/copy_cxxbridge_header.cmake b/openmp-component/component/copy_cxxbridge_header.cmake new file mode 100644 index 0000000..a7ea348 --- /dev/null +++ b/openmp-component/component/copy_cxxbridge_header.cmake @@ -0,0 +1,18 @@ +file(GLOB CXXBRIDGE_HEADERS + "${RUST_LIB_DIR}/build/samp-cef-openmp-*/out/cxxbridge/include/samp-cef-openmp/src/lib.rs.h" +) + +list(LENGTH CXXBRIDGE_HEADERS CXXBRIDGE_HEADER_COUNT) +if(CXXBRIDGE_HEADER_COUNT EQUAL 0) + message(FATAL_ERROR "Could not find generated cxxbridge header under ${RUST_LIB_DIR}/build") +endif() + +list(SORT CXXBRIDGE_HEADERS) +list(GET CXXBRIDGE_HEADERS -1 CXXBRIDGE_HEADER) + +file(MAKE_DIRECTORY "${GENERATED_INCLUDE_DIR}/samp-cef-openmp/src") +file(COPY_FILE + "${CXXBRIDGE_HEADER}" + "${GENERATED_INCLUDE_DIR}/samp-cef-openmp/src/lib.rs.h" + ONLY_IF_DIFFERENT +) diff --git a/openmp-component/component/src/component.cpp b/openmp-component/component/src/component.cpp new file mode 100644 index 0000000..c230520 --- /dev/null +++ b/openmp-component/component/src/component.cpp @@ -0,0 +1,1649 @@ +#include "samp-cef-openmp/src/lib.rs.h" + +#ifndef SAMP_CEF_HAS_GRPC_EXT +#define SAMP_CEF_HAS_GRPC_EXT 0 +#endif + +#if SAMP_CEF_HAS_GRPC_EXT +#include +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +using samp_cef::openmp::EventArguments; +using samp_cef::openmp::EventKind; +using samp_cef::openmp::ServerCore; +using samp_cef::openmp::ServerEvent; + +constexpr int CEF_DEFAULT_PORT_OFFSET = 2; +constexpr float CEF_DEFAULT_MAX_DIST = 50.0f; +constexpr float CEF_DEFAULT_REF_DIST = 15.0f; +constexpr const char* CEF_GRPC_BROWSER_EVENT_PREFIX = "cef.event."; +constexpr UID CEF_COMPONENT_UID = UID(0xCEF5A17CEFC0FFEEull); + +rust::Str asRustStr(const std::string& value) +{ + return rust::Str(value.data(), value.size()); +} + +std::string toStdString(const rust::String& value) +{ + return std::string(value.data(), value.size()); +} + +#if SAMP_CEF_HAS_GRPC_EXT +using GrpcRequest = omp_ext::IOmpGrpcComponent::InvokeRequest; +using GrpcReply = omp_ext::IOmpGrpcComponent::InvokeReply; +using GrpcNamedValue = omp_ext::IOmpGrpcComponent::NamedValue; +using GrpcFunctionDescriptor = omp_ext::IOmpGrpcComponent::FunctionDescriptor; +using GrpcEventDescriptor = omp_ext::IOmpGrpcComponent::EventDescriptor; + +const omp::ext::v1::Value* grpcArg(const GrpcRequest& request, const char* name, int index) +{ + for (const auto& arg : request.args()) + { + if (arg.name() == name) + { + return &arg.value(); + } + } + + if (index >= 0 && index < request.args_size()) + { + return &request.args(index).value(); + } + return nullptr; +} + +void grpcFail(GrpcReply& reply, const char* code, const std::string& message) +{ + auto* error = reply.mutable_error(); + error->set_code(code); + error->set_message(message); +} + +bool grpcReadIntValue(const omp::ext::v1::Value& value, int& output) +{ + if (value.has_int32_value()) + { + output = value.int32_value(); + return true; + } + if (value.has_uint32_value()) + { + output = static_cast(value.uint32_value()); + return true; + } + if (value.has_int64_value()) + { + output = static_cast(value.int64_value()); + return true; + } + if (value.has_uint64_value()) + { + output = static_cast(value.uint64_value()); + return true; + } + return false; +} + +bool grpcReadPlayerValue(const omp::ext::v1::Value& value, int& output) +{ + if (value.has_entity_value()) + { + const auto& entity = value.entity_value(); + if (entity.type() == omp::ext::v1::ENTITY_TYPE_PLAYER || entity.type() == omp::ext::v1::ENTITY_TYPE_UNSPECIFIED) + { + output = entity.id(); + return true; + } + return false; + } + return grpcReadIntValue(value, output); +} + +bool grpcReadUInt32Value(const omp::ext::v1::Value& value, std::uint32_t& output) +{ + int integer = 0; + if (!grpcReadIntValue(value, integer)) + { + return false; + } + output = static_cast(integer); + return true; +} + +bool grpcReadBoolValue(const omp::ext::v1::Value& value, bool& output) +{ + if (value.has_bool_value()) + { + output = value.bool_value(); + return true; + } + int integer = 0; + if (grpcReadIntValue(value, integer)) + { + output = integer != 0; + return true; + } + return false; +} + +bool grpcReadFloatValue(const omp::ext::v1::Value& value, float& output) +{ + if (value.has_float_value()) + { + output = value.float_value(); + return true; + } + if (value.has_double_value()) + { + output = static_cast(value.double_value()); + return true; + } + int integer = 0; + if (grpcReadIntValue(value, integer)) + { + output = static_cast(integer); + return true; + } + return false; +} + +bool grpcReadStringValue(const omp::ext::v1::Value& value, std::string& output) +{ + if (!value.has_string_value()) + { + return false; + } + output = value.string_value(); + return true; +} + +template +bool grpcReadRequired(const GrpcRequest& request, GrpcReply& reply, const char* name, int index, Reader reader, T& output, const char* expected) +{ + const auto* value = grpcArg(request, name, index); + if (!value || !reader(*value, output)) + { + grpcFail(reply, "BAD_ARGUMENT", std::string("Expected ") + expected + " argument `" + name + "`"); + return false; + } + return true; +} + +template +bool grpcReadOptional(const GrpcRequest& request, const char* name, int index, Reader reader, T& output) +{ + const auto* value = grpcArg(request, name, index); + return value ? reader(*value, output) : true; +} + +void grpcReturnBool(GrpcReply& reply, bool value) +{ + reply.mutable_return_value()->set_bool_value(value); +} + +GrpcNamedValue grpcNamedPlayer(const char* name, int playerID) +{ + GrpcNamedValue arg; + arg.set_name(name); + auto* entity = arg.mutable_value()->mutable_entity_value(); + entity->set_type(omp::ext::v1::ENTITY_TYPE_PLAYER); + entity->set_id(playerID); + return arg; +} + +GrpcNamedValue grpcNamedInt(const char* name, int value) +{ + GrpcNamedValue arg; + arg.set_name(name); + arg.mutable_value()->set_int32_value(value); + return arg; +} + +GrpcNamedValue grpcNamedBool(const char* name, bool value) +{ + GrpcNamedValue arg; + arg.set_name(name); + arg.mutable_value()->set_bool_value(value); + return arg; +} + +GrpcNamedValue grpcNamedString(const char* name, const std::string& value) +{ + GrpcNamedValue arg; + arg.set_name(name); + arg.mutable_value()->set_string_value(value); + return arg; +} + +void addGrpcParam(GrpcFunctionDescriptor& descriptor, const char* name, const char* type, + omp::ext::v1::EntityType entityType = omp::ext::v1::ENTITY_TYPE_UNSPECIFIED) +{ + omp::ext::v1::NamedType param; + param.set_name(name); + param.set_type(type); + param.set_entity_type(entityType); + descriptor.params.push_back(param); +} + +void addGrpcEventArg(GrpcEventDescriptor& descriptor, const char* name, const char* type, + omp::ext::v1::EntityType entityType = omp::ext::v1::ENTITY_TYPE_UNSPECIFIED) +{ + descriptor.args.push_back(omp_ext::EventArgDescriptor { name, type, entityType }); +} + +std::string grpcBrowserEventName(const std::string& name) +{ + return std::string(CEF_GRPC_BROWSER_EVENT_PREFIX) + name; +} +#endif + +int pawnParamCount(const cell* params) +{ + return params ? static_cast(params[0] / static_cast(sizeof(cell))) : 0; +} + +class CefComponent final : public IComponent, public CoreEventHandler, public PlayerConnectEventHandler, public PawnEventHandler +{ +public: + PROVIDE_UID(CEF_COMPONENT_UID); + + StringView componentName() const override + { + return "CEF"; + } + + SemanticVersion componentVersion() const override + { + return SemanticVersion(OMP_VERSION_MAJOR, OMP_VERSION_MINOR, OMP_VERSION_PATCH, BUILD_NUMBER); + } + + void provideConfiguration(ILogger&, IEarlyConfig& config, bool defaults) override + { + if (defaults) + { + config.setInt("cef.port_offset", CEF_DEFAULT_PORT_OFFSET); + } + } + + void onLoad(ICore* c) override + { + core_ = c; + instance_ = this; + + core_->getEventDispatcher().addEventHandler(this); + core_->getPlayers().getPlayerConnectDispatcher().addEventHandler(this); + + IConfig& config = core_->getConfig(); + StringView bindView = config.getString("network.bind"); + std::string bind(bindView.data(), bindView.length()); + if (bind.empty()) + { + bind = "0.0.0.0"; + } + + int port = 7777; + if (int* configuredPort = config.getInt("network.port")) + { + port = *configuredPort; + } + + int portOffset = CEF_DEFAULT_PORT_OFFSET; + if (int* configuredOffset = config.getInt("cef.port_offset")) + { + portOffset = *configuredOffset; + } + + const int cefPort = port + portOffset; + if (cefPort < 0 || cefPort > 65535) + { + core_->logLn(LogLevel::Error, "[CEF] Invalid CEF server port: %d", cefPort); + return; + } + + server_.emplace(samp_cef::openmp::create_server_core(asRustStr(bind), static_cast(cefPort))); + if ((*server_)->is_running()) + { + core_->printLn("[CEF] Bound CEF server on %s:%d", bind.c_str(), cefPort); + } + else + { + const std::string error = toStdString((*server_)->last_error()); + core_->logLn(LogLevel::Error, "[CEF] %s", error.c_str()); + } + } + + void onInit(IComponentList* components) override + { + pawn_ = components->queryComponent(); + if (pawn_) + { + pawn_->getEventDispatcher().addEventHandler(this); + } + +#if SAMP_CEF_HAS_GRPC_EXT + grpc_ = components->queryComponent(); + if (grpc_) + { + registerGrpcApi(); + } +#endif + } + + void onFree(IComponent* component) override + { + if (component == pawn_ && pawn_) + { + pawn_->getEventDispatcher().removeEventHandler(this); + pawn_ = nullptr; + } + +#if SAMP_CEF_HAS_GRPC_EXT + if (component == grpc_ && grpc_) + { + grpc_->unregisterOwner(this); + grpc_ = nullptr; + grpcRegisteredEvents_.clear(); + grpcBrowserEvents_.clear(); + } +#endif + } + + void free() override + { + shutdown(); + delete this; + } + + void reset() override + { + scripts_.clear(); + subscriptions_.clear(); + } + + void onTick(Microseconds, TimePoint) override + { + if (!server_ || !(*server_)->is_running()) + { + return; + } + + for (;;) + { + ServerEvent event = (*server_)->poll_event(); + if (event.kind == EventKind::None) + { + break; + } + + dispatchEvent(event); + } + } + + void onIncomingConnection(IPlayer& player, StringView ipAddress, unsigned short) override + { + if (!server_ || !(*server_)->is_running()) + { + return; + } + + const std::string ip(ipAddress.data(), ipAddress.length()); + if (!(*server_)->allow_connection(player.getID(), asRustStr(ip))) + { + core_->logLn(LogLevel::Warning, "[CEF] Invalid incoming player IP: %s", ip.c_str()); + } + } + + void onPlayerDisconnect(IPlayer& player, PeerDisconnectReason) override + { + if (server_) + { + (*server_)->remove_connection(player.getID()); + } + } + + void onAmxLoad(IPawnScript& script) override + { + static AMX_NATIVE_INFO natives[] = { + { "cef_on_player_connect", &CefComponent::nativeOnPlayerConnect }, + { "cef_on_player_disconnect", &CefComponent::nativeOnPlayerDisconnect }, + { "cef_create_browser", &CefComponent::nativeCreateBrowser }, + { "cef_destroy_browser", &CefComponent::nativeDestroyBrowser }, + { "cef_emit_event", &CefComponent::nativeEmitEvent }, + { "cef_subscribe", &CefComponent::nativeSubscribe }, + { "cef_always_listen_keys", &CefComponent::nativeAlwaysListenKeys }, + { "cef_hide_browser", &CefComponent::nativeHideBrowser }, + { "cef_focus_browser", &CefComponent::nativeFocusBrowser }, + { "cef_player_has_plugin", &CefComponent::nativePlayerHasPlugin }, + { "cef_create_ext_browser", &CefComponent::nativeCreateExternalBrowser }, + { "cef_append_to_object", &CefComponent::nativeAppendToObject }, + { "cef_remove_from_object", &CefComponent::nativeRemoveFromObject }, + { "cef_toggle_dev_tools", &CefComponent::nativeToggleDevTools }, + { "cef_set_audio_settings", &CefComponent::nativeSetAudioSettings }, + { "cef_load_url", &CefComponent::nativeLoadUrl }, + }; + + script.Register(natives, static_cast(sizeof(natives) / sizeof(natives[0]))); + + if (std::find(scripts_.begin(), scripts_.end(), &script) == scripts_.end()) + { + scripts_.push_back(&script); + } + } + + void onAmxUnload(IPawnScript& script) override + { + scripts_.erase(std::remove(scripts_.begin(), scripts_.end(), &script), scripts_.end()); + + for (auto it = subscriptions_.begin(); it != subscriptions_.end();) + { + if (it->second.script == &script) + { + it = subscriptions_.erase(it); + } + else + { + ++it; + } + } + } + +private: + struct Subscription + { + IPawnScript* script = nullptr; + std::string callback; + }; + + ICore* core_ = nullptr; + IPawnComponent* pawn_ = nullptr; +#if SAMP_CEF_HAS_GRPC_EXT + omp_ext::IOmpGrpcComponent* grpc_ = nullptr; + std::set grpcRegisteredEvents_; + std::set grpcBrowserEvents_; +#endif + std::optional> server_; + std::vector scripts_; + std::unordered_map subscriptions_; + + static CefComponent* instance_; + + void shutdown() + { +#if SAMP_CEF_HAS_GRPC_EXT + if (grpc_) + { + grpc_->unregisterOwner(this); + grpc_ = nullptr; + } + grpcRegisteredEvents_.clear(); + grpcBrowserEvents_.clear(); +#endif + + if (pawn_) + { + pawn_->getEventDispatcher().removeEventHandler(this); + pawn_ = nullptr; + } + + if (core_) + { + core_->getPlayers().getPlayerConnectDispatcher().removeEventHandler(this); + core_->getEventDispatcher().removeEventHandler(this); + core_ = nullptr; + } + + scripts_.clear(); + subscriptions_.clear(); + server_.reset(); + + if (instance_ == this) + { + instance_ = nullptr; + } + } + + void dispatchEvent(const ServerEvent& event) + { + switch (event.kind) + { + case EventKind::EmitEvent: + dispatchBrowserEvent(event); + break; + + case EventKind::PlayerInitialized: + notifyConnect(event.player_id, event.success); + break; + + case EventKind::BrowserCreated: + notifyBrowserCreated(event.player_id, event.browser_id, event.code); + break; + + case EventKind::None: + break; + } + } + + void dispatchBrowserEvent(const ServerEvent& event) + { + const std::string name = toStdString(event.event); + auto subscription = subscriptions_.find(name); + const std::string arguments = toStdString(event.arguments); + if (subscription != subscriptions_.end() && subscription->second.script) + { + subscription->second.script->Call( + subscription->second.callback.c_str(), + DefaultReturnValue_False, + event.player_id, + StringView(arguments.data(), arguments.length())); + } + +#if SAMP_CEF_HAS_GRPC_EXT + emitGrpcBrowserEvent(name, event.player_id, arguments); +#endif + } + + void notifyConnect(int playerID, bool success) + { + for (IPawnScript* script : scripts_) + { + script->Call("OnCefInitialize", DefaultReturnValue_True, playerID, success ? 1 : 0); + } + +#if SAMP_CEF_HAS_GRPC_EXT + std::vector args; + args.push_back(grpcNamedPlayer("player_id", playerID)); + args.push_back(grpcNamedBool("success", success)); + emitGrpcEvent("OnCefInitialize", std::move(args)); +#endif + } + + void notifyBrowserCreated(int playerID, std::uint32_t browserID, int code) + { + for (IPawnScript* script : scripts_) + { + script->Call("OnCefBrowserCreated", DefaultReturnValue_True, playerID, static_cast(browserID), code); + } + +#if SAMP_CEF_HAS_GRPC_EXT + std::vector args; + args.push_back(grpcNamedPlayer("player_id", playerID)); + args.push_back(grpcNamedInt("browser_id", static_cast(browserID))); + args.push_back(grpcNamedInt("status_code", code)); + emitGrpcEvent("OnCefBrowserCreated", std::move(args)); +#endif + } + + IPawnScript* scriptFromAmx(AMX* amx) const + { + return pawn_ ? pawn_->getScript(amx) : nullptr; + } + + bool getAmxString(IPawnScript& script, cell address, std::string& output) const + { + cell* physicalAddress = nullptr; + if (script.GetAddr(address, &physicalAddress) != AMX_ERR_NONE || physicalAddress == nullptr) + { + return false; + } + + int length = 0; + if (script.StrLen(physicalAddress, &length) != AMX_ERR_NONE) + { + return false; + } + + std::vector buffer(static_cast(length) + 1, '\0'); + if (script.GetString(buffer.data(), physicalAddress, false, buffer.size()) != AMX_ERR_NONE) + { + return false; + } + + output.assign(buffer.data()); + return true; + } + + bool requireParams(const cell* params, int count, const char* native) const + { + if (pawnParamCount(params) >= count) + { + return true; + } + + if (core_) + { + core_->logLn(LogLevel::Error, "[CEF] Incorrect parameters for `%s`", native); + } + return false; + } + +#if SAMP_CEF_HAS_GRPC_EXT + template + void registerGrpcFunction(const char* name, const char* returnType, Init init, omp_ext::IOmpGrpcComponent::FunctionHandler handler) + { + GrpcFunctionDescriptor descriptor; + descriptor.name = name; + descriptor.category = "CEF"; + descriptor.returnType = returnType; + descriptor.requiresCapi = false; + init(descriptor); + + if (!grpc_->registerFunction(this, std::move(descriptor), handler, this) && core_) + { + core_->logLn(LogLevel::Warning, "[CEF] Failed to register gRPC function `%s`", name); + } + } + + template + bool registerGrpcEvent(const std::string& name, Init init) + { + if (!grpc_) + { + return false; + } + + if (grpcRegisteredEvents_.find(name) != grpcRegisteredEvents_.end()) + { + return true; + } + + GrpcEventDescriptor descriptor; + descriptor.category = "CEF"; + descriptor.name = name; + init(descriptor); + + if (!grpc_->registerEvent(this, std::move(descriptor))) + { + return false; + } + + grpcRegisteredEvents_.insert(name); + return true; + } + + bool registerGrpcBrowserEvent(const std::string& name) + { + if (name.empty()) + { + return false; + } + + if (grpcBrowserEvents_.find(name) != grpcBrowserEvents_.end()) + { + return true; + } + + const bool registered = registerGrpcEvent(grpcBrowserEventName(name), + [](GrpcEventDescriptor& descriptor) + { + addGrpcEventArg(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcEventArg(descriptor, "event", "char*"); + addGrpcEventArg(descriptor, "arguments", "char*"); + }); + if (registered) + { + grpcBrowserEvents_.insert(name); + } + return registered; + } + + void registerGrpcApi() + { + registerGrpcEvent("OnCefInitialize", + [](GrpcEventDescriptor& descriptor) + { + addGrpcEventArg(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcEventArg(descriptor, "success", "bool"); + }); + + registerGrpcEvent("OnCefBrowserCreated", + [](GrpcEventDescriptor& descriptor) + { + addGrpcEventArg(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcEventArg(descriptor, "browser_id", "int"); + addGrpcEventArg(descriptor, "status_code", "int"); + }); + + registerGrpcFunction("cef_on_player_connect", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "ip", "char*"); + }, + &CefComponent::grpcOnPlayerConnect); + + registerGrpcFunction("cef_on_player_disconnect", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + }, + &CefComponent::grpcOnPlayerDisconnect); + + registerGrpcFunction("cef_create_browser", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + addGrpcParam(descriptor, "url", "char*"); + addGrpcParam(descriptor, "hidden", "bool"); + addGrpcParam(descriptor, "focused", "bool"); + }, + &CefComponent::grpcCreateBrowser); + + registerGrpcFunction("cef_destroy_browser", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + }, + &CefComponent::grpcDestroyBrowser); + + registerGrpcFunction("cef_emit_event", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "event", "char*"); + addGrpcParam(descriptor, "args", "list"); + }, + &CefComponent::grpcEmitEvent); + + registerGrpcFunction("cef_subscribe", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "event", "char*"); + }, + &CefComponent::grpcSubscribe); + + registerGrpcFunction("cef_always_listen_keys", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + addGrpcParam(descriptor, "listen", "bool"); + }, + &CefComponent::grpcAlwaysListenKeys); + + registerGrpcFunction("cef_hide_browser", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + addGrpcParam(descriptor, "hide", "bool"); + }, + &CefComponent::grpcHideBrowser); + + registerGrpcFunction("cef_focus_browser", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + addGrpcParam(descriptor, "focused", "bool"); + }, + &CefComponent::grpcFocusBrowser); + + registerGrpcFunction("cef_player_has_plugin", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + }, + &CefComponent::grpcPlayerHasPlugin); + + registerGrpcFunction("cef_create_ext_browser", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + addGrpcParam(descriptor, "texture", "char*"); + addGrpcParam(descriptor, "url", "char*"); + addGrpcParam(descriptor, "scale", "int"); + }, + &CefComponent::grpcCreateExternalBrowser); + + registerGrpcFunction("cef_append_to_object", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + addGrpcParam(descriptor, "object_id", "int"); + }, + &CefComponent::grpcAppendToObject); + + registerGrpcFunction("cef_remove_from_object", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + addGrpcParam(descriptor, "object_id", "int"); + }, + &CefComponent::grpcRemoveFromObject); + + registerGrpcFunction("cef_toggle_dev_tools", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + addGrpcParam(descriptor, "enabled", "bool"); + }, + &CefComponent::grpcToggleDevTools); + + registerGrpcFunction("cef_set_audio_settings", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + addGrpcParam(descriptor, "max_distance", "float"); + addGrpcParam(descriptor, "reference_distance", "float"); + }, + &CefComponent::grpcSetAudioSettings); + + registerGrpcFunction("cef_load_url", "bool", + [](GrpcFunctionDescriptor& descriptor) + { + addGrpcParam(descriptor, "player_id", "int", omp::ext::v1::ENTITY_TYPE_PLAYER); + addGrpcParam(descriptor, "browser_id", "int"); + addGrpcParam(descriptor, "url", "char*"); + }, + &CefComponent::grpcLoadUrl); + } + + void emitGrpcEvent(const std::string& name, std::vector args) + { + if (!grpc_) + { + return; + } + + Span view(args.data(), args.size()); + grpc_->emitEvent(StringView(name.data(), name.length()), view); + } + + void emitGrpcBrowserEvent(const std::string& name, int playerID, const std::string& arguments) + { + if (!grpc_ || grpcBrowserEvents_.find(name) == grpcBrowserEvents_.end()) + { + return; + } + + std::vector args; + args.push_back(grpcNamedPlayer("player_id", playerID)); + args.push_back(grpcNamedString("event", name)); + args.push_back(grpcNamedString("arguments", arguments)); + emitGrpcEvent(grpcBrowserEventName(name), std::move(args)); + } + + static CefComponent* grpcSelf(void* userData, GrpcReply& reply, bool requireServer = true) + { + auto* self = static_cast(userData); + if (!self || (requireServer && !self->server_)) + { + grpcFail(reply, "CEF_NOT_READY", "CEF server is not ready"); + return nullptr; + } + return self; + } + + static bool grpcPushEventArgument(EventArguments& arguments, const omp::ext::v1::Value& value, GrpcReply& reply) + { + if (value.has_string_value()) + { + arguments.push_string(asRustStr(value.string_value())); + return true; + } + if (value.has_float_value()) + { + arguments.push_float(value.float_value()); + return true; + } + if (value.has_double_value()) + { + arguments.push_float(static_cast(value.double_value())); + return true; + } + if (value.has_bool_value()) + { + arguments.push_integer(value.bool_value() ? 1 : 0); + return true; + } + + int integer = 0; + if (grpcReadIntValue(value, integer)) + { + arguments.push_integer(integer); + return true; + } + + grpcFail(reply, "BAD_ARGUMENT", "CEF event arguments must be string, integer, float, or bool"); + return false; + } + + static void grpcOnPlayerConnect(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + std::string ip; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "ip", 1, grpcReadStringValue, ip, "string")) + { + return; + } + + grpcReturnBool(reply, (*self->server_)->allow_connection(playerID, asRustStr(ip))); + } + + static void grpcOnPlayerDisconnect(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player")) + { + return; + } + + (*self->server_)->remove_connection(playerID); + grpcReturnBool(reply, true); + } + + static void grpcCreateBrowser(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + int browserID = 0; + std::string url; + bool hidden = false; + bool focused = true; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadIntValue, browserID, "int") + || !grpcReadRequired(request, reply, "url", 2, grpcReadStringValue, url, "string") + || !grpcReadRequired(request, reply, "hidden", 3, grpcReadBoolValue, hidden, "bool") + || !grpcReadRequired(request, reply, "focused", 4, grpcReadBoolValue, focused, "bool")) + { + return; + } + + (*self->server_)->create_browser(playerID, browserID, asRustStr(url), hidden, focused); + grpcReturnBool(reply, true); + } + + static void grpcDestroyBrowser(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + int browserID = 0; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadIntValue, browserID, "int")) + { + return; + } + + (*self->server_)->destroy_browser(playerID, browserID); + grpcReturnBool(reply, true); + } + + static void grpcEmitEvent(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + std::string eventName; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "event", 1, grpcReadStringValue, eventName, "string")) + { + return; + } + + auto arguments = samp_cef::openmp::new_event_arguments(); + if (const auto* args = grpcArg(request, "args", -1)) + { + if (!args->has_list_value()) + { + grpcFail(reply, "BAD_ARGUMENT", "Expected list argument `args`"); + return; + } + + for (const auto& value : args->list_value().values()) + { + if (!grpcPushEventArgument(*arguments, value, reply)) + { + return; + } + } + } + else + { + for (int index = 0; index < request.args_size(); ++index) + { + const auto& arg = request.args(index); + if (index < 2 || arg.name() == "player_id" || arg.name() == "event") + { + continue; + } + if (!grpcPushEventArgument(*arguments, arg.value(), reply)) + { + return; + } + } + } + + (*self->server_)->emit_event(playerID, asRustStr(eventName), *arguments); + grpcReturnBool(reply, true); + } + + static void grpcSubscribe(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply, false); + if (!self) + { + return; + } + + std::string eventName; + if (!grpcReadRequired(request, reply, "event", 0, grpcReadStringValue, eventName, "string")) + { + return; + } + + const bool registered = self->registerGrpcBrowserEvent(eventName); + if (!registered && self->core_) + { + self->core_->logLn(LogLevel::Warning, "[CEF] Failed to register gRPC event `%s`", eventName.c_str()); + } + grpcReturnBool(reply, registered); + } + + static void grpcAlwaysListenKeys(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + int browserID = 0; + bool listen = false; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadIntValue, browserID, "int") + || !grpcReadRequired(request, reply, "listen", 2, grpcReadBoolValue, listen, "bool")) + { + return; + } + + (*self->server_)->always_listen_keys(playerID, browserID, listen); + grpcReturnBool(reply, true); + } + + static void grpcHideBrowser(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + int browserID = 0; + bool hide = false; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadIntValue, browserID, "int") + || !grpcReadRequired(request, reply, "hide", 2, grpcReadBoolValue, hide, "bool")) + { + return; + } + + (*self->server_)->hide_browser(playerID, browserID, hide); + grpcReturnBool(reply, true); + } + + static void grpcFocusBrowser(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + int browserID = 0; + bool focused = false; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadIntValue, browserID, "int") + || !grpcReadRequired(request, reply, "focused", 2, grpcReadBoolValue, focused, "bool")) + { + return; + } + + (*self->server_)->focus_browser(playerID, browserID, focused); + grpcReturnBool(reply, true); + } + + static void grpcPlayerHasPlugin(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player")) + { + return; + } + + grpcReturnBool(reply, (*self->server_)->has_plugin(playerID)); + } + + static void grpcCreateExternalBrowser(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + int browserID = 0; + int scale = 0; + std::string texture; + std::string url; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadIntValue, browserID, "int") + || !grpcReadRequired(request, reply, "texture", 2, grpcReadStringValue, texture, "string") + || !grpcReadRequired(request, reply, "url", 3, grpcReadStringValue, url, "string") + || !grpcReadRequired(request, reply, "scale", 4, grpcReadIntValue, scale, "int")) + { + return; + } + + (*self->server_)->create_external_browser(playerID, browserID, asRustStr(texture), asRustStr(url), scale); + grpcReturnBool(reply, true); + } + + static void grpcAppendToObject(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + int browserID = 0; + int objectID = 0; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadIntValue, browserID, "int") + || !grpcReadRequired(request, reply, "object_id", 2, grpcReadIntValue, objectID, "int")) + { + return; + } + + (*self->server_)->append_to_object(playerID, browserID, objectID); + grpcReturnBool(reply, true); + } + + static void grpcRemoveFromObject(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + int browserID = 0; + int objectID = 0; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadIntValue, browserID, "int") + || !grpcReadRequired(request, reply, "object_id", 2, grpcReadIntValue, objectID, "int")) + { + return; + } + + (*self->server_)->remove_from_object(playerID, browserID, objectID); + grpcReturnBool(reply, true); + } + + static void grpcToggleDevTools(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + int browserID = 0; + bool enabled = false; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadIntValue, browserID, "int") + || !grpcReadRequired(request, reply, "enabled", 2, grpcReadBoolValue, enabled, "bool")) + { + return; + } + + (*self->server_)->toggle_dev_tools(playerID, browserID, enabled); + grpcReturnBool(reply, true); + } + + static void grpcSetAudioSettings(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + std::uint32_t browserID = 0; + float maxDistance = CEF_DEFAULT_MAX_DIST; + float referenceDistance = CEF_DEFAULT_REF_DIST; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadUInt32Value, browserID, "int") + || !grpcReadOptional(request, "max_distance", 2, grpcReadFloatValue, maxDistance) + || !grpcReadOptional(request, "reference_distance", 3, grpcReadFloatValue, referenceDistance)) + { + grpcFail(reply, "BAD_ARGUMENT", "Expected numeric audio settings"); + return; + } + + (*self->server_)->set_audio_settings(playerID, browserID, maxDistance, referenceDistance); + grpcReturnBool(reply, true); + } + + static void grpcLoadUrl(const GrpcRequest& request, GrpcReply& reply, void* userData) + { + CefComponent* self = grpcSelf(userData, reply); + if (!self) + { + return; + } + + int playerID = 0; + std::uint32_t browserID = 0; + std::string url; + if (!grpcReadRequired(request, reply, "player_id", 0, grpcReadPlayerValue, playerID, "player") + || !grpcReadRequired(request, reply, "browser_id", 1, grpcReadUInt32Value, browserID, "int") + || !grpcReadRequired(request, reply, "url", 2, grpcReadStringValue, url, "string")) + { + return; + } + + (*self->server_)->load_url(playerID, browserID, asRustStr(url)); + grpcReturnBool(reply, true); + } +#endif + + static CefComponent* component() + { + return instance_; + } + + static cell AMX_NATIVE_CALL nativeOnPlayerConnect(AMX* amx, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 2, "cef_on_player_connect")) + { + return 0; + } + + IPawnScript* script = self->scriptFromAmx(amx); + if (!script) + { + return 0; + } + + std::string ip; + if (!self->getAmxString(*script, params[2], ip)) + { + return 0; + } + + return (*self->server_)->allow_connection(static_cast(params[1]), asRustStr(ip)) ? 1 : 0; + } + + static cell AMX_NATIVE_CALL nativeOnPlayerDisconnect(AMX*, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 1, "cef_on_player_disconnect")) + { + return 0; + } + + (*self->server_)->remove_connection(static_cast(params[1])); + return 1; + } + + static cell AMX_NATIVE_CALL nativeCreateBrowser(AMX* amx, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 5, "cef_create_browser")) + { + return 0; + } + + IPawnScript* script = self->scriptFromAmx(amx); + if (!script) + { + return 0; + } + + std::string url; + if (!self->getAmxString(*script, params[3], url)) + { + return 0; + } + + (*self->server_) + ->create_browser( + static_cast(params[1]), + static_cast(params[2]), + asRustStr(url), + params[4] != 0, + params[5] != 0); + return 1; + } + + static cell AMX_NATIVE_CALL nativeDestroyBrowser(AMX*, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 2, "cef_destroy_browser")) + { + return 0; + } + + (*self->server_)->destroy_browser(static_cast(params[1]), static_cast(params[2])); + return 1; + } + + static cell AMX_NATIVE_CALL nativeEmitEvent(AMX* amx, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 2, "cef_emit_event")) + { + return 0; + } + + const int count = pawnParamCount(params); + if ((count - 2) % 2 != 0) + { + return 0; + } + + IPawnScript* script = self->scriptFromAmx(amx); + if (!script) + { + return 0; + } + + std::string eventName; + if (!self->getAmxString(*script, params[2], eventName)) + { + return 0; + } + + auto arguments = samp_cef::openmp::new_event_arguments(); + for (int idx = 3; idx <= count; idx += 2) + { + const int type = static_cast(params[idx]); + const cell value = params[idx + 1]; + + switch (type) + { + case 0: + { + std::string stringValue; + if (!self->getAmxString(*script, value, stringValue)) + { + return 0; + } + arguments->push_string(asRustStr(stringValue)); + break; + } + + case 1: + arguments->push_integer(static_cast(value)); + break; + + case 2: + arguments->push_float(amx_ctof(value)); + break; + + default: + return 0; + } + } + + (*self->server_)->emit_event(static_cast(params[1]), asRustStr(eventName), *arguments); + return 1; + } + + static cell AMX_NATIVE_CALL nativeSubscribe(AMX* amx, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->requireParams(params, 2, "cef_subscribe")) + { + return 0; + } + + IPawnScript* script = self->scriptFromAmx(amx); + if (!script) + { + return 0; + } + + std::string eventName; + std::string callback; + if (!self->getAmxString(*script, params[1], eventName) || !self->getAmxString(*script, params[2], callback)) + { + return 0; + } + + self->subscriptions_[eventName] = Subscription { script, callback }; + return 1; + } + + static cell AMX_NATIVE_CALL nativeAlwaysListenKeys(AMX*, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 3, "cef_always_listen_keys")) + { + return 0; + } + + (*self->server_) + ->always_listen_keys(static_cast(params[1]), static_cast(params[2]), params[3] != 0); + return 1; + } + + static cell AMX_NATIVE_CALL nativeHideBrowser(AMX*, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 3, "cef_hide_browser")) + { + return 0; + } + + (*self->server_)->hide_browser(static_cast(params[1]), static_cast(params[2]), params[3] != 0); + return 1; + } + + static cell AMX_NATIVE_CALL nativeFocusBrowser(AMX*, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 3, "cef_focus_browser")) + { + return 0; + } + + (*self->server_)->focus_browser(static_cast(params[1]), static_cast(params[2]), params[3] != 0); + return 1; + } + + static cell AMX_NATIVE_CALL nativePlayerHasPlugin(AMX*, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 1, "cef_player_has_plugin")) + { + return 0; + } + + return (*self->server_)->has_plugin(static_cast(params[1])) ? 1 : 0; + } + + static cell AMX_NATIVE_CALL nativeCreateExternalBrowser(AMX* amx, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 5, "cef_create_ext_browser")) + { + return 0; + } + + IPawnScript* script = self->scriptFromAmx(amx); + if (!script) + { + return 0; + } + + std::string texture; + std::string url; + if (!self->getAmxString(*script, params[3], texture) || !self->getAmxString(*script, params[4], url)) + { + return 0; + } + + (*self->server_) + ->create_external_browser( + static_cast(params[1]), + static_cast(params[2]), + asRustStr(texture), + asRustStr(url), + static_cast(params[5])); + return 1; + } + + static cell AMX_NATIVE_CALL nativeAppendToObject(AMX*, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 3, "cef_append_to_object")) + { + return 0; + } + + (*self->server_) + ->append_to_object(static_cast(params[1]), static_cast(params[2]), static_cast(params[3])); + return 1; + } + + static cell AMX_NATIVE_CALL nativeRemoveFromObject(AMX*, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 3, "cef_remove_from_object")) + { + return 0; + } + + (*self->server_) + ->remove_from_object(static_cast(params[1]), static_cast(params[2]), static_cast(params[3])); + return 1; + } + + static cell AMX_NATIVE_CALL nativeToggleDevTools(AMX*, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 3, "cef_toggle_dev_tools")) + { + return 0; + } + + (*self->server_)->toggle_dev_tools(static_cast(params[1]), static_cast(params[2]), params[3] != 0); + return 1; + } + + static cell AMX_NATIVE_CALL nativeSetAudioSettings(AMX*, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 4, "cef_set_audio_settings")) + { + return 0; + } + + (*self->server_) + ->set_audio_settings( + static_cast(params[1]), + static_cast(params[2]), + amx_ctof(params[3]), + amx_ctof(params[4])); + return 1; + } + + static cell AMX_NATIVE_CALL nativeLoadUrl(AMX* amx, const cell* params) + { + CefComponent* self = component(); + if (!self || !self->server_ || !self->requireParams(params, 3, "cef_load_url")) + { + return 0; + } + + IPawnScript* script = self->scriptFromAmx(amx); + if (!script) + { + return 0; + } + + std::string url; + if (!self->getAmxString(*script, params[3], url)) + { + return 0; + } + + (*self->server_)->load_url(static_cast(params[1]), static_cast(params[2]), asRustStr(url)); + return 1; + } +}; + +CefComponent* CefComponent::instance_ = nullptr; +} + +COMPONENT_ENTRY_POINT() +{ + return new CefComponent(); +} diff --git a/openmp-component/src/lib.rs b/openmp-component/src/lib.rs new file mode 100644 index 0000000..29f03a7 --- /dev/null +++ b/openmp-component/src/lib.rs @@ -0,0 +1,304 @@ +use std::borrow::Cow; +use std::net::{IpAddr, SocketAddr}; + +use server_core::{CoreEvent, EventValue, ServerCore as InnerServerCore}; + +#[cxx::bridge(namespace = "samp_cef::openmp")] +mod ffi { + enum EventKind { + None = 0, + EmitEvent = 1, + PlayerInitialized = 2, + BrowserCreated = 3, + } + + struct ServerEvent { + kind: EventKind, + player_id: i32, + browser_id: u32, + code: i32, + success: bool, + event: String, + arguments: String, + } + + extern "Rust" { + type ServerCore; + type EventArguments; + + fn create_server_core(bind: &str, port: u16) -> Box; + + fn is_running(self: &ServerCore) -> bool; + fn last_error(self: &ServerCore) -> String; + + fn allow_connection(self: &mut ServerCore, player_id: i32, ip: &str) -> bool; + fn remove_connection(self: &mut ServerCore, player_id: i32); + + fn create_browser( + self: &mut ServerCore, player_id: i32, browser_id: i32, url: &str, hidden: bool, + focused: bool, + ); + fn destroy_browser(self: &mut ServerCore, player_id: i32, browser_id: i32); + fn hide_browser(self: &ServerCore, player_id: i32, browser_id: i32, hide: bool); + fn focus_browser(self: &ServerCore, player_id: i32, browser_id: i32, focused: bool); + fn emit_event(self: &ServerCore, player_id: i32, event: &str, arguments: &EventArguments); + fn always_listen_keys(self: &ServerCore, player_id: i32, browser_id: i32, listen: bool); + fn has_plugin(self: &ServerCore, player_id: i32) -> bool; + fn create_external_browser( + self: &ServerCore, player_id: i32, browser_id: i32, texture: &str, url: &str, + scale: i32, + ); + fn append_to_object(self: &ServerCore, player_id: i32, browser_id: i32, object_id: i32); + fn remove_from_object(self: &ServerCore, player_id: i32, browser_id: i32, object_id: i32); + fn toggle_dev_tools(self: &ServerCore, player_id: i32, browser_id: i32, enabled: bool); + fn set_audio_settings( + self: &ServerCore, player_id: i32, browser_id: u32, max_distance: f32, + reference_distance: f32, + ); + fn load_url(self: &ServerCore, player_id: i32, browser_id: u32, url: &str); + fn poll_event(self: &mut ServerCore) -> ServerEvent; + + fn new_event_arguments() -> Box; + fn push_string(self: &mut EventArguments, value: &str); + fn push_integer(self: &mut EventArguments, value: i32); + fn push_float(self: &mut EventArguments, value: f32); + } +} + +pub struct ServerCore { + inner: Option, + last_error: String, +} + +pub struct EventArguments { + values: Vec>, +} + +fn create_server_core(bind: &str, port: u16) -> Box { + let bind = if bind.is_empty() { "0.0.0.0" } else { bind }; + let ip = match bind.parse::() { + Ok(ip) => ip, + Err(error) => { + return Box::new(ServerCore { + inner: None, + last_error: format!("invalid CEF bind address `{bind}`: {error}"), + }); + } + }; + + let addr = SocketAddr::from((ip, port)); + + match InnerServerCore::new(addr) { + Ok(inner) => Box::new(ServerCore { + inner: Some(inner), + last_error: String::new(), + }), + Err(error) => Box::new(ServerCore { + inner: None, + last_error: error, + }), + } +} + +impl ServerCore { + fn is_running(&self) -> bool { + self.inner.is_some() + } + + fn last_error(&self) -> String { + self.last_error.clone() + } + + fn allow_connection(&mut self, player_id: i32, ip: &str) -> bool { + let Some(inner) = self.inner.as_mut() else { + return false; + }; + + let Ok(ip) = ip.parse::() else { + return false; + }; + + inner.allow_connection(player_id, ip); + true + } + + fn remove_connection(&mut self, player_id: i32) { + if let Some(inner) = self.inner.as_mut() { + inner.remove_connection(player_id); + } + } + + fn create_browser( + &mut self, player_id: i32, browser_id: i32, url: &str, hidden: bool, focused: bool, + ) { + if let Some(inner) = self.inner.as_mut() { + inner.create_browser(player_id, browser_id, url.to_owned(), hidden, focused); + } + } + + fn destroy_browser(&mut self, player_id: i32, browser_id: i32) { + if let Some(inner) = self.inner.as_mut() { + inner.destroy_browser(player_id, browser_id); + } + } + + fn hide_browser(&self, player_id: i32, browser_id: i32, hide: bool) { + if let Some(inner) = self.inner.as_ref() { + inner.hide_browser(player_id, browser_id, hide); + } + } + + fn focus_browser(&self, player_id: i32, browser_id: i32, focused: bool) { + if let Some(inner) = self.inner.as_ref() { + inner.focus_browser(player_id, browser_id, focused); + } + } + + fn emit_event(&self, player_id: i32, event: &str, arguments: &EventArguments) { + if let Some(inner) = self.inner.as_ref() { + inner.emit_event(player_id, event, arguments.values.clone()); + } + } + + fn always_listen_keys(&self, player_id: i32, browser_id: i32, listen: bool) { + if let Some(inner) = self.inner.as_ref() { + inner.always_listen_keys(player_id, browser_id, listen); + } + } + + fn has_plugin(&self, player_id: i32) -> bool { + self.inner + .as_ref() + .map(|inner| inner.has_plugin(player_id)) + .unwrap_or(false) + } + + fn create_external_browser( + &self, player_id: i32, browser_id: i32, texture: &str, url: &str, scale: i32, + ) { + if let Some(inner) = self.inner.as_ref() { + inner.create_external_browser( + player_id, + browser_id, + texture.to_owned(), + url.to_owned(), + scale, + ); + } + } + + fn append_to_object(&self, player_id: i32, browser_id: i32, object_id: i32) { + if let Some(inner) = self.inner.as_ref() { + inner.append_to_object(player_id, browser_id, object_id); + } + } + + fn remove_from_object(&self, player_id: i32, browser_id: i32, object_id: i32) { + if let Some(inner) = self.inner.as_ref() { + inner.remove_from_object(player_id, browser_id, object_id); + } + } + + fn toggle_dev_tools(&self, player_id: i32, browser_id: i32, enabled: bool) { + if let Some(inner) = self.inner.as_ref() { + inner.toggle_dev_tools(player_id, browser_id, enabled); + } + } + + fn set_audio_settings( + &self, player_id: i32, browser_id: u32, max_distance: f32, reference_distance: f32, + ) { + if let Some(inner) = self.inner.as_ref() { + inner.set_audio_settings(player_id, browser_id, max_distance, reference_distance); + } + } + + fn load_url(&self, player_id: i32, browser_id: u32, url: &str) { + if let Some(inner) = self.inner.as_ref() { + inner.load_url(player_id, browser_id, url.to_owned()); + } + } + + fn poll_event(&mut self) -> ffi::ServerEvent { + let Some(inner) = self.inner.as_mut() else { + return empty_event(); + }; + + match inner.poll_event() { + Some(CoreEvent::EmitEvent { + player_id, + event, + arguments, + }) => ffi::ServerEvent { + kind: ffi::EventKind::EmitEvent, + player_id, + event, + arguments, + ..empty_event() + }, + + Some(CoreEvent::PlayerInitialized { player_id, success }) => ffi::ServerEvent { + kind: ffi::EventKind::PlayerInitialized, + player_id, + success, + ..empty_event() + }, + + Some(CoreEvent::BrowserCreated { + player_id, + browser_id, + code, + }) => ffi::ServerEvent { + kind: ffi::EventKind::BrowserCreated, + player_id, + browser_id, + code, + ..empty_event() + }, + + None => empty_event(), + } + } +} + +fn new_event_arguments() -> Box { + Box::new(EventArguments { values: Vec::new() }) +} + +impl EventArguments { + fn push_string(&mut self, value: &str) { + self.values.push(EventValue { + string_value: Some(Cow::Owned(value.to_owned())), + float_value: None, + integer_value: None, + }); + } + + fn push_integer(&mut self, value: i32) { + self.values.push(EventValue { + string_value: None, + float_value: None, + integer_value: Some(value), + }); + } + + fn push_float(&mut self, value: f32) { + self.values.push(EventValue { + string_value: None, + float_value: Some(value), + integer_value: None, + }); + } +} + +fn empty_event() -> ffi::ServerEvent { + ffi::ServerEvent { + kind: ffi::EventKind::None, + player_id: 0, + browser_id: 0, + code: 0, + success: false, + event: String::new(), + arguments: String::new(), + } +} diff --git a/readme.md b/readme.md index 742efe5..f81c610 100644 --- a/readme.md +++ b/readme.md @@ -21,6 +21,8 @@ It is **a FRAMEWORK** (or SDK), not something that you download and use. To be a - `proto` - raw proto files. - `renderer` - glue between CEF renderer process and main logic. - `server` - server side plugin. +- `server-core` - shared server logic used by the SA:MP plugin and open.mp component. +- `openmp-component` - native open.mp component built through CMake and `cxx`. ## Download Latest builds from the master branch are placed in [the Actions](https://github.com/ZOTTCE/samp-cef/actions). @@ -55,6 +57,16 @@ rustup toolchain install nightly-2022-11-06-i686-pc-windows-msvc cargo build --release ``` +### Building the open.mp component +Use the helper script from the repository root: + +```sh +scripts/build-openmp-component.sh --openmp-root /path/to/open.mp +scripts/build-openmp-component.sh --openmp-root /path/to/open.mp --server-root /path/to/omp-server +``` + +It builds `openmp-component` and installs it into `/components` when `--server-root` is provided. More details are in [docs/build.md](docs/build.md) and [openmp-component/README.md](openmp-component/README.md). + ### Cross-compiling the Windows client (macOS/Linux) Use `scripts/build-client-win32.sh` to build `client`, `renderer`, and `loader` for `i686-pc-windows-msvc` on non-Windows hosts. It relies on `cargo-xwin` (`cargo install cargo-xwin --locked`), `7z` for extracting the DirectX SDK, and downloads `libcef.lib` if `CEF_PATH` is not set. The script can also download/extract the DirectX SDK if `DX_SDK` is not set. diff --git a/scripts/build-openmp-component.sh b/scripts/build-openmp-component.sh new file mode 100755 index 0000000..17b0212 --- /dev/null +++ b/scripts/build-openmp-component.sh @@ -0,0 +1,282 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +openmp_root="${OPEN_MP_ROOT:-}" +openmp_ext_root="${OPENMP_EXT_ROOT:-}" +openmp_ext_build_dir="${OPENMP_EXT_BUILD_DIR:-}" +build_dir="${SAMP_CEF_OPENMP_BUILD_DIR:-$repo_root/target/openmp-component-build}" +grpc="${SAMP_CEF_ENABLE_GRPC_EXT:-OFF}" +build_grpc="${SAMP_CEF_BUILD_GRPC_EXT:-OFF}" +server_root="${OPEN_MP_SERVER_ROOT:-}" +install=0 +clean=0 +jobs="" +cmake_prefix_path="${CMAKE_PREFIX_PATH:-}" +install_prefix="" +install_component="samp-cef-openmp" +declare -a cef_cmake_args=() +declare -a grpc_cmake_args=() +declare -a build_parallel_args=() + +usage() { + cat <<'EOF' +Usage: scripts/build-openmp-component.sh [options] + +Builds the samp-cef open.mp component. + +Options: + --openmp-root PATH Path to the open.mp checkout. + --openmp-ext-root PATH Path to the openmp-ext checkout. + --openmp-ext-build-dir PATH Path to the openmp-ext build directory. + --build-dir PATH CEF component build directory. + --grpc auto|on|off Enable optional gRPC registration. Default: off. + --build-grpc auto|on|off Build omp-grpc before CEF. Default: off. + --server-root PATH Install into PATH/components after building. + --install Run cmake --install after building. + --prefix PATH CMAKE_INSTALL_PREFIX used with --install. + --install-component NAME CMake install component. Default: samp-cef-openmp. + --cmake-prefix-path PATH CMAKE_PREFIX_PATH for grpc/protobuf packages. + --jobs N Parallel build level passed to CMake. + --clean Remove the CEF component build directory first. + --cef-cmake-arg ARG Extra CMake configure argument for CEF. + --grpc-cmake-arg ARG Extra CMake configure argument for openmp-ext. + -h, --help Show this help. + +Examples: + OPEN_MP_ROOT=/path/to/open.mp scripts/build-openmp-component.sh + scripts/build-openmp-component.sh --openmp-root /path/to/open.mp --server-root /path/to/omp-server + scripts/build-openmp-component.sh --openmp-root /path/to/open.mp --grpc on --openmp-ext-root /path/to/openmp-ext +EOF +} + +upper() { + printf '%s' "$1" | tr '[:lower:]' '[:upper:]' +} + +component_ext() { + case "$(uname -s)" in + Darwin) printf 'dylib' ;; + MINGW*|MSYS*|CYGWIN*) printf 'dll' ;; + *) printf 'so' ;; + esac +} + +default_cmake_prefix_path() { + if [[ -n "$cmake_prefix_path" || "$(uname -s)" != "Darwin" ]]; then + return + fi + if ! command -v brew >/dev/null 2>&1; then + return + fi + + local prefixes=() + local prefix + for package in grpc protobuf; do + prefix="$(brew --prefix "$package" 2>/dev/null || true)" + if [[ -n "$prefix" && -d "$prefix" ]]; then + prefixes+=("$prefix") + fi + done + + if (( ${#prefixes[@]} > 0 )); then + local joined + joined="$(IFS=';'; printf '%s' "${prefixes[*]}")" + cmake_prefix_path="$joined" + fi +} + +cmake_build_target() { + local dir="$1" + local target="$2" + + if (( ${#build_parallel_args[@]} > 0 )); then + cmake --build "$dir" --target "$target" "${build_parallel_args[@]}" + else + cmake --build "$dir" --target "$target" + fi +} + +while (( $# > 0 )); do + case "$1" in + --openmp-root) + openmp_root="$2" + shift 2 + ;; + --openmp-ext-root) + openmp_ext_root="$2" + shift 2 + ;; + --openmp-ext-build-dir) + openmp_ext_build_dir="$2" + shift 2 + ;; + --build-dir) + build_dir="$2" + shift 2 + ;; + --grpc) + grpc="$2" + shift 2 + ;; + --build-grpc) + build_grpc="$2" + shift 2 + ;; + --server-root) + server_root="$2" + install=1 + shift 2 + ;; + --install) + install=1 + shift + ;; + --prefix) + install_prefix="$2" + shift 2 + ;; + --install-component) + install_component="$2" + shift 2 + ;; + --cmake-prefix-path) + cmake_prefix_path="$2" + shift 2 + ;; + --jobs|-j) + jobs="$2" + shift 2 + ;; + --clean) + clean=1 + shift + ;; + --cef-cmake-arg) + cef_cmake_args+=("$2") + shift 2 + ;; + --grpc-cmake-arg) + grpc_cmake_args+=("$2") + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'Unknown option: %s\n\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +grpc="$(upper "$grpc")" +build_grpc="$(upper "$build_grpc")" +case "$grpc" in AUTO|ON|OFF) ;; *) printf '%s\n' '--grpc must be auto, on, or off' >&2; exit 2 ;; esac +case "$build_grpc" in AUTO|ON|OFF) ;; *) printf '%s\n' '--build-grpc must be auto, on, or off' >&2; exit 2 ;; esac + +if [[ -z "$openmp_ext_build_dir" && -n "$openmp_ext_root" ]]; then + openmp_ext_build_dir="$openmp_ext_root/build" +fi + +if [[ -z "$openmp_root" ]]; then + printf '%s\n' 'OPEN_MP_ROOT is required. Set OPEN_MP_ROOT or pass --openmp-root PATH.' >&2 + exit 2 +fi + +if [[ ! -f "$openmp_root/SDK/CMakeLists.txt" ]]; then + printf 'OPEN_MP_ROOT does not look like an open.mp checkout: %s\n' "$openmp_root" >&2 + exit 1 +fi + +if [[ "$install" == 1 && -z "$server_root" && -z "$install_prefix" ]]; then + printf '%s\n' '--install needs --server-root PATH or --prefix PATH' >&2 + exit 2 +fi + +if [[ "$grpc" != "OFF" ]]; then + default_cmake_prefix_path + if [[ -n "$cmake_prefix_path" ]]; then + export CMAKE_PREFIX_PATH="$cmake_prefix_path" + fi +fi + +if (( clean )); then + rm -rf "$build_dir" +fi + +if [[ -n "$jobs" ]]; then + build_parallel_args+=(--parallel "$jobs") +fi + +ext="$(component_ext)" +if [[ "$grpc" != "OFF" ]]; then + if [[ -n "$openmp_ext_root" && -f "$openmp_ext_root/CMakeLists.txt" ]]; then + grpc_lib="$openmp_ext_build_dir/omp-grpc.$ext" + proto_cc="$openmp_ext_build_dir/generated/proto/omp_ext.pb.cc" + + if [[ "$build_grpc" == "ON" || ( "$build_grpc" == "AUTO" && ( ! -f "$grpc_lib" || ! -f "$proto_cc" ) ) ]]; then + printf '[samp-cef] Building omp-grpc in %s\n' "$openmp_ext_build_dir" + grpc_configure_args=( + -S "$openmp_ext_root" + -B "$openmp_ext_build_dir" + -DOPENMP_ROOT="$openmp_root" + ) + if (( ${#grpc_cmake_args[@]} > 0 )); then + grpc_configure_args+=("${grpc_cmake_args[@]}") + fi + cmake "${grpc_configure_args[@]}" + cmake_build_target "$openmp_ext_build_dir" omp-grpc + fi + elif [[ "$grpc" == "ON" ]]; then + printf 'openmp-ext was required but not found. Set OPENMP_EXT_ROOT or pass --openmp-ext-root PATH.\n' >&2 + exit 1 + fi +fi + +printf '[samp-cef] Building CEF open.mp component in %s\n' "$build_dir" +cef_configure_args=( + -S "$repo_root/openmp-component/component" + -B "$build_dir" + -DOPEN_MP_ROOT="$openmp_root" + -DOPENMP_EXT_ROOT="$openmp_ext_root" + -DOPENMP_EXT_BUILD_DIR="$openmp_ext_build_dir" + -DSAMP_CEF_ENABLE_GRPC_EXT="$grpc" + -DSAMP_CEF_INSTALL_COMPONENT_NAME="$install_component" + -DOPEN_MP_SERVER_ROOT="$server_root" +) + +if [[ -n "$install_prefix" ]]; then + cef_configure_args+=(-DCMAKE_INSTALL_PREFIX="$install_prefix") +fi +if (( ${#cef_cmake_args[@]} > 0 )); then + cef_configure_args+=("${cef_cmake_args[@]}") +fi + +cmake "${cef_configure_args[@]}" +cmake_build_target "$build_dir" cef + +if (( install )); then + printf '[samp-cef] Installing CEF open.mp component\n' + cmake --install "$build_dir" --component "$install_component" +fi + +printf '[samp-cef] Built: %s/cef.%s\n' "$build_dir" "$ext" +installed_component_dir="" +if [[ -n "$server_root" ]]; then + installed_component_dir="$server_root/components" +elif [[ -n "$install_prefix" ]]; then + installed_component_dir="$install_prefix/components" +fi + +if (( install )) && [[ -n "$installed_component_dir" ]]; then + printf '[samp-cef] Installed components into: %s\n' "$installed_component_dir" + if [[ "$grpc" == "OFF" ]]; then + printf '[samp-cef] Load order: CEF\n' + else + printf '[samp-cef] Load order: $CAPI, omp-grpc, CEF\n' + fi +fi diff --git a/server-core/Cargo.toml b/server-core/Cargo.toml new file mode 100644 index 0000000..1485064 --- /dev/null +++ b/server-core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "server-core" +version = "0.1.0" +authors = ["ZOTTCE "] +edition = "2024" +rust-version = "1.93" + +[dependencies] +crossbeam-channel = "0.5.15" +log = "0.4.29" +messages = { path = "../messages" } +network = { path = "../network" } +quick-protobuf = "0.8.1" diff --git a/server/src/client.rs b/server-core/src/client.rs similarity index 96% rename from server/src/client.rs rename to server-core/src/client.rs index d32cda5..df5921a 100644 --- a/server/src/client.rs +++ b/server-core/src/client.rs @@ -10,7 +10,7 @@ pub enum State { #[derive(Debug)] pub struct Client { - id: i32, // SA:MP player id + id: i32, state: State, addr: SocketAddr, peer: PeerId, diff --git a/server-core/src/lib.rs b/server-core/src/lib.rs new file mode 100644 index 0000000..7e35af6 --- /dev/null +++ b/server-core/src/lib.rs @@ -0,0 +1,246 @@ +use std::collections::VecDeque; +use std::net::{IpAddr, SocketAddr}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use crossbeam_channel::Receiver; +use log::trace; + +mod client; +mod server; + +use crate::server::Server; + +pub use messages::packets::EventValue; + +const INIT_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Debug, Clone)] +pub enum CoreEvent { + EmitEvent { + player_id: i32, + event: String, + arguments: String, + }, + PlayerInitialized { + player_id: i32, + success: bool, + }, + BrowserCreated { + player_id: i32, + browser_id: u32, + code: i32, + }, +} + +#[derive(Debug)] +pub(crate) enum ServerEvent { + EmitEvent { + player_id: i32, + event: String, + arguments: String, + }, + PlayerConnected(i32), + BrowserCreated { + player_id: i32, + browser_id: u32, + code: i32, + }, +} + +pub struct ServerCore { + server: Arc>, + event_rx: Receiver, + await_connect: std::collections::HashMap, + ips: std::collections::HashMap, + pending_events: VecDeque, +} + +impl ServerCore { + pub fn new(addr: SocketAddr) -> Result { + let server = Server::new(addr)?; + + let event_rx = { + let s = server.lock().unwrap(); + s.receiver() + }; + + Ok(Self { + server, + event_rx, + await_connect: std::collections::HashMap::new(), + ips: std::collections::HashMap::new(), + pending_events: VecDeque::new(), + }) + } + + pub fn allow_connection(&mut self, player_id: i32, addr: IpAddr) { + trace!("allow_connection {} {:?}", player_id, addr); + + self.ips.insert(player_id, addr); + + let already_connected = { + let server = self.server.lock().unwrap(); + server.has_plugin(player_id) + }; + + { + let mut server = self.server.lock().unwrap(); + server.allow_connection(player_id, addr); + } + + if !already_connected { + self.await_connect.insert(player_id, Instant::now()); + } + } + + pub fn remove_connection(&mut self, player_id: i32) { + trace!("remove_connection {}", player_id); + + let ip = self.ips.remove(&player_id); + + { + let mut server = self.server.lock().unwrap(); + server.remove_connection(player_id, ip); + } + + self.await_connect.remove(&player_id); + } + + pub fn create_browser( + &mut self, player_id: i32, browser_id: i32, url: String, hidden: bool, focused: bool, + ) { + let mut server = self.server.lock().unwrap(); + server.create_browser(player_id, browser_id, url, hidden, focused); + } + + pub fn destroy_browser(&mut self, player_id: i32, browser_id: i32) { + let mut server = self.server.lock().unwrap(); + server.destroy_browser(player_id, browser_id); + } + + pub fn hide_browser(&self, player_id: i32, browser_id: i32, hide: bool) { + let server = self.server.lock().unwrap(); + server.hide_browser(player_id, browser_id, hide); + } + + pub fn focus_browser(&self, player_id: i32, browser_id: i32, focused: bool) { + let server = self.server.lock().unwrap(); + server.focus_browser(player_id, browser_id, focused); + } + + pub fn emit_event(&self, player_id: i32, event: &str, arguments: Vec>) { + let server = self.server.lock().unwrap(); + server.emit_event(player_id, event, arguments); + } + + pub fn always_listen_keys(&self, player_id: i32, browser_id: i32, listen: bool) { + let server = self.server.lock().unwrap(); + server.always_listen_keys(player_id, browser_id, listen); + } + + pub fn has_plugin(&self, player_id: i32) -> bool { + let server = self.server.lock().unwrap(); + server.has_plugin(player_id) + } + + pub fn create_external_browser( + &self, player_id: i32, browser_id: i32, texture: String, url: String, scale: i32, + ) { + let server = self.server.lock().unwrap(); + server.create_external_browser(player_id, browser_id, texture, url, scale); + } + + pub fn append_to_object(&self, player_id: i32, browser_id: i32, object_id: i32) { + let server = self.server.lock().unwrap(); + server.append_to_object(player_id, browser_id, object_id); + } + + pub fn remove_from_object(&self, player_id: i32, browser_id: i32, object_id: i32) { + let server = self.server.lock().unwrap(); + server.remove_from_object(player_id, browser_id, object_id); + } + + pub fn toggle_dev_tools(&self, player_id: i32, browser_id: i32, enabled: bool) { + let server = self.server.lock().unwrap(); + server.toggle_dev_tools(player_id, browser_id, enabled); + } + + pub fn set_audio_settings( + &self, player_id: i32, browser_id: u32, max_distance: f32, reference_distance: f32, + ) { + let server = self.server.lock().unwrap(); + server.set_audio_settings(player_id, browser_id, max_distance, reference_distance); + } + + pub fn load_url(&self, player_id: i32, browser_id: u32, url: String) { + let server = self.server.lock().unwrap(); + server.load_url(player_id, browser_id, url); + } + + pub fn poll_event(&mut self) -> Option { + if let Some(event) = self.pending_events.pop_front() { + return Some(event); + } + + while let Ok(event) = self.event_rx.try_recv() { + match event { + ServerEvent::EmitEvent { + player_id, + event, + arguments, + } => { + self.pending_events.push_back(CoreEvent::EmitEvent { + player_id, + event, + arguments, + }); + } + + ServerEvent::PlayerConnected(player_id) => { + trace!("poll_event::PlayerConnected({})", player_id); + + if self.await_connect.remove(&player_id).is_some() { + self.pending_events.push_back(CoreEvent::PlayerInitialized { + player_id, + success: true, + }); + } + } + + ServerEvent::BrowserCreated { + player_id, + browser_id, + code, + } => { + self.pending_events.push_back(CoreEvent::BrowserCreated { + player_id, + browser_id, + code, + }); + } + } + } + + self.notify_timeouts(); + self.pending_events.pop_front() + } + + fn notify_timeouts(&mut self) { + let mut keys = Vec::new(); + + for (&player_id, timing) in self.await_connect.iter() { + if timing.elapsed() >= INIT_TIMEOUT { + keys.push(player_id); + } + } + + for player_id in keys { + self.await_connect.remove(&player_id); + self.pending_events.push_back(CoreEvent::PlayerInitialized { + player_id, + success: false, + }); + } + } +} diff --git a/server/src/server.rs b/server-core/src/server.rs similarity index 89% rename from server/src/server.rs rename to server-core/src/server.rs index f145759..289c2d9 100644 --- a/server/src/server.rs +++ b/server-core/src/server.rs @@ -10,8 +10,9 @@ use std::net::{IpAddr, SocketAddr}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use crate::Event; +use crate::ServerEvent; use crate::client::Client; +use crate::client::State; enum Packet { Normal { peer: PeerId, bytes: Vec }, @@ -29,16 +30,17 @@ impl Packet { } pub struct Server { - event_tx: Sender, - event_rx: Receiver, + event_tx: Sender, + event_rx: Receiver, sender: Sender, allowed: HashMap, clients: HashMap, } impl Server { - pub fn new(addr: SocketAddr) -> Arc> { - let mut socket = Socket::new_server(addr, CertStrategy::SelfSigned).unwrap(); + pub fn new(addr: SocketAddr) -> Result>, String> { + let mut socket = Socket::new_server(addr, CertStrategy::SelfSigned) + .map_err(|error| format!("failed to bind CEF server on {addr}: {error}"))?; let (sender, receiver) = crossbeam_channel::unbounded(); let (event_tx, event_rx) = crossbeam_channel::unbounded(); @@ -58,7 +60,6 @@ impl Server { loop { while let Some(event) = socket.recv() { match event { - // если послали новый пакет SocketEvent::Message(peer, bytes) => { if let Ok(proto) = deserialize_from_slice::(&bytes) { let mut server = server.lock().unwrap(); @@ -66,13 +67,11 @@ impl Server { } } - // обработка пакетов соединения SocketEvent::Connected(peer, addr) => { let mut server = server.lock().unwrap(); server.handle_new_connection(peer, addr); } - // таймауты SocketEvent::Disconnect(peer, _) => { let mut server = server.lock().unwrap(); server.handle_timeout(peer); @@ -96,14 +95,12 @@ impl Server { } }); - server_clone + Ok(server_clone) } - /// обработка пакетов от клиентов fn handle_client_packet(&mut self, peer: PeerId, packet: packets::Packet) { use messages::packets::PacketId; - // клиента нет пшел нахрен if !self.clients.contains_key(&peer) { return; } @@ -128,18 +125,19 @@ impl Server { } } - /// обработка пакета авторизации fn handle_auth(&mut self, peer: PeerId, _packet: packets::RequestJoin) { - let client = self.clients.get_mut(&peer).unwrap(); // safe + let client = self.clients.get_mut(&peer).unwrap(); let response = packets::JoinResponse { success: true, current_version: None, }; - client.set_state(crate::client::State::Connected); + client.set_state(State::Connected); - let _ = self.event_tx.send(Event::PlayerConnected(client.id())); + let _ = self + .event_tx + .send(ServerEvent::PlayerConnected(client.id())); let _ = try_into_packet(response).map(|bytes| { let packet = Packet::new(peer, bytes); @@ -148,13 +146,13 @@ impl Server { } fn handle_emit_event(&mut self, peer: PeerId, packet: packets::EmitEvent) { - let client = self.clients.get_mut(&peer).unwrap(); // safe + let client = self.clients.get_mut(&peer).unwrap(); let player_id = client.id(); if let Some(args) = &packet.args { let event = packet.event_name.to_string(); let arguments = args.to_string(); - let event = Event::EmitEvent { + let event = ServerEvent::EmitEvent { player_id, arguments, event, @@ -165,10 +163,10 @@ impl Server { } fn handle_browser_created(&mut self, peer: PeerId, packet: packets::BrowserCreated) { - let client = self.clients.get_mut(&peer).unwrap(); // safe + let client = self.clients.get_mut(&peer).unwrap(); let player_id = client.id(); - let event = Event::BrowserCreated { + let event = ServerEvent::BrowserCreated { player_id, browser_id: packet.browser_id, code: packet.status_code, @@ -177,7 +175,6 @@ impl Server { let _ = self.event_tx.send(event); } - /// выпинываем игрока из списка клиентов fn handle_timeout(&mut self, addr: PeerId) { trace!("handle_timeout {:?}", addr); self.clients.remove(&addr); @@ -186,7 +183,6 @@ impl Server { trace!("{:#?}", self.clients); } - /// обрабатывает новое входящее соединение fn handle_new_connection(&mut self, peer: PeerId, addr: SocketAddr) { trace!("handle_new_connection {:?} {:?}", peer, addr); @@ -217,11 +213,17 @@ impl Server { let _ = self.sender.send(packet); } - // samp server side - pub fn allow_connection(&mut self, player_id: i32, addr: IpAddr) { if let Some(peer) = self.peer_by_id(player_id) { - self.clients.remove(&peer); + let same_addr = self + .clients + .get(&peer) + .map(|client| client.addr().ip() == addr) + .unwrap_or(false); + + if !same_addr { + self.clients.remove(&peer); + } } self.allowed.insert(addr, player_id); @@ -379,12 +381,10 @@ impl Server { ); } - pub fn receiver(&self) -> Receiver { + pub fn receiver(&self) -> Receiver { self.event_rx.clone() } - // utils - fn send_packet<'a, T: TryInto, Error = quick_protobuf::Error>>( &self, player_id: i32, packet: T, ) { diff --git a/server/Cargo.toml b/server/Cargo.toml index ffcc0d2..c944016 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -11,8 +11,4 @@ crate-type = ["cdylib"] [dependencies] samp = { git = "https://github.com/zottce/samp-rs.git", rev = "2e9192713bf9a77ee04e0ebbcda37d078c9c47dd", features = ["encoding"] } log = "0.4.29" -crossbeam-channel = "0.5.15" -messages = { path = "../messages" } -quick-protobuf = "0.8.1" -network = { path = "../network" } -simplelog = "0.12.2" +server-core = { path = "../server-core" } diff --git a/server/src/lib.rs b/server/src/lib.rs index fb414ee..1131c39 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,48 +1,22 @@ use std::collections::HashMap; use std::net::{IpAddr, SocketAddr}; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; -use crossbeam_channel::Receiver; use log::{info, trace}; -use messages::packets::EventValue; -// use simplelog::{CombinedLogger, LevelFilter, TermLogger, TerminalMode}; +use server_core::{CoreEvent, EventValue, ServerCore}; use samp::amx::AmxIdent; use samp::args::Args; use samp::prelude::*; use samp::{exec_public, initialize_plugin, native}; -mod client; -mod server; mod utils; -use crate::server::Server; - -const INIT_TIMEOUT: Duration = Duration::from_secs(5); const PORT_OFFSET: u16 = 2; -pub enum Event { - EmitEvent { - player_id: i32, - event: String, - arguments: String, - }, - PlayerConnected(i32), - BrowserCreated { - player_id: i32, - browser_id: u32, - code: i32, - }, -} - struct CefPlugin { - server: Arc>, + core: ServerCore, events: HashMap, - event_rx: Receiver, amx_list: Vec, - await_connect: HashMap, - ips: HashMap, } impl CefPlugin { @@ -52,22 +26,14 @@ impl CefPlugin { let port = crate::utils::parse_config_field("port").unwrap_or(7777); let addr = SocketAddr::from((ip, port + PORT_OFFSET)); - let server = Server::new(addr); + let core = ServerCore::new(addr).unwrap_or_else(|error| panic!("{error}")); info!("Bind CEF server on {:?}", addr); - let event_rx = { - let s = server.lock().unwrap(); - s.receiver() - }; - CefPlugin { - server, - event_rx, + core, events: HashMap::new(), amx_list: Vec::new(), - await_connect: HashMap::new(), - ips: HashMap::new(), } } @@ -78,16 +44,7 @@ impl CefPlugin { let player_ip = player_ip.to_string(); if let Ok(addr) = player_ip.parse::() { - trace!("allow_connection {} {:?}", player_id, addr); - - self.ips.insert(player_id, addr); - - { - let mut server = self.server.lock().unwrap(); - server.allow_connection(player_id, addr); - } - - self.add_to_await_list(player_id); + self.core.allow_connection(player_id, addr); } Ok(true) @@ -95,17 +52,7 @@ impl CefPlugin { #[native(name = "cef_on_player_disconnect")] fn on_player_disconnect(&mut self, _: &Amx, player_id: i32) -> AmxResult { - trace!("remove_connection {} ", player_id); - - let ip = self.ips.remove(&player_id); - - { - let mut server = self.server.lock().unwrap(); - server.remove_connection(player_id, ip); - } - - self.remove_from_await_list(player_id); - + self.core.remove_connection(player_id); Ok(true) } @@ -114,17 +61,15 @@ impl CefPlugin { &mut self, _: &Amx, player_id: i32, browser_id: i32, url: AmxString, hidden: bool, focused: bool, ) -> AmxResult { - let mut server = self.server.lock().unwrap(); - server.create_browser(player_id, browser_id, url.to_string(), hidden, focused); + self.core + .create_browser(player_id, browser_id, url.to_string(), hidden, focused); Ok(true) } #[native(name = "cef_destroy_browser")] fn destroy_browser(&mut self, _: &Amx, player_id: i32, browser_id: i32) -> AmxResult { - let mut server = self.server.lock().unwrap(); - server.destroy_browser(player_id, browser_id); - + self.core.destroy_browser(player_id, browser_id); Ok(true) } @@ -132,9 +77,7 @@ impl CefPlugin { fn hide_browser( &mut self, _: &Amx, player_id: i32, browser_id: i32, hide: bool, ) -> AmxResult { - let server = self.server.lock().unwrap(); - server.hide_browser(player_id, browser_id, hide); - + self.core.hide_browser(player_id, browser_id, hide); Ok(true) } @@ -142,9 +85,7 @@ impl CefPlugin { fn browser_listen_events( &mut self, _: &Amx, player_id: i32, browser_id: i32, focused: bool, ) -> AmxResult { - let server = self.server.lock().unwrap(); - server.focus_browser(player_id, browser_id, focused); - + self.core.focus_browser(player_id, browser_id, focused); Ok(true) } @@ -200,8 +141,7 @@ impl CefPlugin { } } - let server = self.server.lock().unwrap(); - server.emit_event(player_id, &event_name, arguments); + self.core.emit_event(player_id, &event_name, arguments); Ok(true) } @@ -210,9 +150,7 @@ impl CefPlugin { fn block_input( &mut self, _: &Amx, player_id: i32, browser_id: i32, listen: bool, ) -> AmxResult { - let server = self.server.lock().unwrap(); - server.always_listen_keys(player_id, browser_id, listen); - + self.core.always_listen_keys(player_id, browser_id, listen); Ok(true) } @@ -231,8 +169,7 @@ impl CefPlugin { #[native(name = "cef_player_has_plugin")] fn is_player_has_plugin(&mut self, _: &Amx, player_id: i32) -> AmxResult { - let server = self.server.lock().unwrap(); - let has_plugin = server.has_plugin(player_id); + let has_plugin = self.core.has_plugin(player_id); Ok(has_plugin) } @@ -244,8 +181,8 @@ impl CefPlugin { let texture = texture.to_string(); let url = url.to_string(); - let server = self.server.lock().unwrap(); - server.create_external_browser(player_id, browser_id, texture, url, scale); + self.core + .create_external_browser(player_id, browser_id, texture, url, scale); Ok(true) } @@ -254,8 +191,7 @@ impl CefPlugin { fn append_to_object( &mut self, _: &Amx, player_id: i32, browser_id: i32, object_id: i32, ) -> AmxResult { - let server = self.server.lock().unwrap(); - server.append_to_object(player_id, browser_id, object_id); + self.core.append_to_object(player_id, browser_id, object_id); Ok(true) } @@ -263,8 +199,8 @@ impl CefPlugin { fn remove_from_object( &mut self, _: &Amx, player_id: i32, browser_id: i32, object_id: i32, ) -> AmxResult { - let server = self.server.lock().unwrap(); - server.remove_from_object(player_id, browser_id, object_id); + self.core + .remove_from_object(player_id, browser_id, object_id); Ok(true) } @@ -272,8 +208,7 @@ impl CefPlugin { fn toggle_dev_tools( &mut self, _: &Amx, player_id: i32, browser_id: i32, enabled: bool, ) -> AmxResult { - let server = self.server.lock().unwrap(); - server.toggle_dev_tools(player_id, browser_id, enabled); + self.core.toggle_dev_tools(player_id, browser_id, enabled); Ok(true) } @@ -282,8 +217,8 @@ impl CefPlugin { &mut self, _: &Amx, player_id: i32, browser_id: u32, max_distance: f32, reference_distance: f32, ) -> AmxResult { - let server = self.server.lock().unwrap(); - server.set_audio_settings(player_id, browser_id, max_distance, reference_distance); + self.core + .set_audio_settings(player_id, browser_id, max_distance, reference_distance); Ok(true) } @@ -291,35 +226,10 @@ impl CefPlugin { fn load_url( &mut self, _: &Amx, player_id: i32, browser_id: u32, url: AmxString, ) -> AmxResult { - let url = url.to_string(); - let server = self.server.lock().unwrap(); - - server.load_url(player_id, browser_id, url); - + self.core.load_url(player_id, browser_id, url.to_string()); Ok(true) } - // utils - fn notify_timeout(&mut self) { - let mut keys = Vec::new(); - - for (&player_id, timing) in self.await_connect.iter() { - if timing.elapsed() >= INIT_TIMEOUT { - keys.push(player_id); - self.notify_connect(player_id, false); - } - } - - keys.into_iter().for_each(|player_id| { - let result = self.remove_from_await_list(player_id); - - trace!( - "notify_timeout::remove_from_await_list({}) {}", - player_id, result - ); - }); - } - fn notify_connect(&self, player_id: i32, success: bool) { trace!("notify_connect({}, {})", player_id, success); @@ -335,14 +245,6 @@ impl CefPlugin { .map(|amx| exec_public!(amx, "OnCefBrowserCreated", player_id, browser_id, code)); }); } - - fn add_to_await_list(&mut self, player_id: i32) { - self.await_connect.insert(player_id, Instant::now()); - } - - fn remove_from_await_list(&mut self, player_id: i32) -> bool { - self.await_connect.remove(&player_id).is_some() - } } impl SampPlugin for CefPlugin { @@ -363,9 +265,9 @@ impl SampPlugin for CefPlugin { } fn process_tick(&mut self) { - while let Ok(event) = self.event_rx.try_recv() { + while let Some(event) = self.core.poll_event() { match event { - Event::EmitEvent { + CoreEvent::EmitEvent { player_id, event, arguments, @@ -378,27 +280,24 @@ impl SampPlugin for CefPlugin { } } - Event::PlayerConnected(player) => { - trace!("process_tick::PlayerConnected({})", player); - - if self.remove_from_await_list(player) { - self.notify_connect(player, true); - } + CoreEvent::PlayerInitialized { player_id, success } => { + trace!( + "process_tick::PlayerInitialized({}, {})", + player_id, success + ); + self.notify_connect(player_id, success); } - Event::BrowserCreated { + CoreEvent::BrowserCreated { player_id, browser_id, code, } => { trace!("process_tick::BrowserCreated({})", player_id); - self.notify_browser_created(player_id, browser_id, code); } } } - - self.notify_timeout(); } } @@ -424,23 +323,7 @@ initialize_plugin!( { samp::plugin::enable_process_tick(); samp::encoding::set_default_encoding(samp::encoding::WINDOWS_1251); - let _ = samp::plugin::logger(); // fuck logger - - // let mut config = simplelog::ConfigBuilder::new(); - - // let config = config - // .add_filter_allow_str("server") - // .set_max_level(LevelFilter::Trace) - // .build(); - - // CombinedLogger::init(vec![TermLogger::new( - // LevelFilter::Trace, - // config, - // TerminalMode::Mixed, - // simplelog::ColorChoice::Always, - // )]) - // .unwrap(); - + let _ = samp::plugin::logger(); CefPlugin::new() }