├── .clang-format ├── .clang-tidy ├── .cmake-format.yml ├── .github ├── actions │ └── build │ │ └── action.yml └── workflows │ └── build-and-release.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── CPPLINT.cfg ├── Doxyfile ├── LICENSE.md ├── README.md ├── data ├── 0001-Fix-to-get-CMake-work-with-clang-cl.patch └── patches.txt ├── docker-compose.yml ├── docker ├── Dockerfile └── clang-cl-msvc.Dockerfile ├── scripts ├── build-and-test.sh ├── debug.sh ├── iwyu-check.py ├── iwyu-check.sh ├── parse_logfile.sh ├── patch_gamemd.py └── tools.sh ├── src ├── CMakeLists.txt ├── addscn │ ├── CMakeLists.txt │ └── addscn.cpp ├── asio_utils.cpp ├── asio_utils.hpp ├── async_map.hpp ├── async_queue.hpp ├── auto_thread.cpp ├── auto_thread.hpp ├── client_connection.cpp ├── client_connection.hpp ├── client_utils.hpp ├── command │ ├── command_manager.cpp │ ├── command_manager.hpp │ ├── is_command.cpp │ └── is_command.hpp ├── commands_builtin.cpp ├── commands_builtin.hpp ├── commands_game.cpp ├── commands_game.hpp ├── commands_yr.cpp ├── commands_yr.hpp ├── config.cpp ├── config.hpp ├── constants.hpp ├── dll_inject.cpp ├── dll_inject.hpp ├── errors.cpp ├── errors.hpp ├── hook.cpp ├── hook.hpp ├── hooks_yr.cpp ├── hooks_yr.hpp ├── instrumentation_client.cpp ├── instrumentation_client.hpp ├── instrumentation_service.cpp ├── instrumentation_service.hpp ├── is_context.cpp ├── is_context.hpp ├── logging.cpp ├── logging.hpp ├── multi_client.cpp ├── multi_client.hpp ├── process.cpp ├── process.hpp ├── protocol │ ├── CMakeLists.txt │ ├── helpers.cpp │ ├── helpers.hpp │ ├── protocol.cpp │ └── protocol.hpp ├── ra2 │ ├── abi.cpp │ ├── abi.hpp │ ├── common.cpp │ ├── common.hpp │ ├── event_list.cpp │ ├── event_list.hpp │ ├── state_context.cpp │ ├── state_context.hpp │ ├── state_parser.cpp │ ├── state_parser.hpp │ ├── yrpp_export.cpp │ └── yrpp_export.hpp ├── ra2yrcpp.cpp ├── ra2yrcpp.hpp ├── ra2yrcppcli │ ├── CMakeLists.txt │ ├── main.cpp │ ├── ra2yrcppcli.cpp │ └── ra2yrcppcli.hpp ├── ring_buffer.hpp ├── types.h ├── util_string.cpp ├── util_string.hpp ├── utility.h ├── utility │ ├── array_iterator.hpp │ ├── function_traits.hpp │ ├── scope_guard.hpp │ ├── serialize.hpp │ ├── sync.cpp │ ├── sync.hpp │ └── time.hpp ├── version.rc ├── websocket_connection.cpp ├── websocket_connection.hpp ├── websocket_server.cpp ├── websocket_server.hpp ├── win32 │ ├── win_message.cpp │ ├── win_message.hpp │ ├── windows_debug.cpp │ ├── windows_debug.hpp │ ├── windows_utils.cpp │ └── windows_utils.hpp ├── x86.cpp ├── x86.hpp ├── yrclient_dll.cpp └── yrclient_dll.hpp ├── tests ├── CMakeLists.txt ├── common_multi.cpp ├── common_multi.hpp ├── dummy_program.cpp ├── test_dll_inject.cpp ├── test_hooks.cpp ├── test_instrumentation_service.cpp ├── test_is_stress_test.cpp ├── test_multi_client.cpp ├── test_process.cpp ├── test_protocol.cpp └── util_proto.hpp └── toolchains ├── clang-cl-msvc.cmake └── mingw-w64-i686.cmake /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | # We'll use defaults from the LLVM style, but with 4 columns indentation. 3 | BasedOnStyle: Google 4 | --- 5 | Language: Cpp 6 | # Force pointers to the type for C++. 7 | DerivePointerAlignment: false 8 | PointerAlignment: Left 9 | ReflowComments: false 10 | SeparateDefinitionBlocks: Always 11 | IncludeBlocks: Regroup 12 | IncludeCategories: 13 | - Regex: '^"(protocol/protocol.hpp|ra2yrproto/.+)' # must be included before windows includes! 14 | Priority: 1 15 | - Regex: '^".+' 16 | Priority: 2 17 | - Regex: "^<(fmt|google|xbyak|argparse|asio|websocketpp|gtest)/.+" 18 | Priority: 3 19 | - Regex: "" 20 | Priority: 4 21 | - Regex: "windows.h" 22 | Priority: 5 23 | - Regex: "<(errhandling|handle|libloader|memory|processthreads|ps|synch)api.h>" 24 | Priority: 6 25 | - Regex: "<(direct|dbghelp|malloc|minwindef|tlhelp32|winbase|winternl).h>" 26 | Priority: 6 27 | - Regex: "<.+>" 28 | Priority: 7 29 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | Checks: >- 2 | clang-diagnostic-*, 3 | clang-analyzer-*, 4 | cppcoreguidelines-*, 5 | modernize-*, 6 | -modernize-use-trailing-return-type, 7 | -cppcoreguidelines-pro-bounds-pointer-arithmetic, 8 | -cppcoreguidelines-pro-type-union-access, 9 | -cppcoreguidelines-macro-usage 10 | HeaderFilterRegex: "" 11 | AnalyzeTemporaryDtors: false 12 | WarningsAsErrors: false 13 | -------------------------------------------------------------------------------- /.cmake-format.yml: -------------------------------------------------------------------------------- 1 | additional_commands: 2 | new_make_test: 3 | kwargs: 4 | "NAME": '1' 5 | "SRC": '*' 6 | "LIB": '1' 7 | autosort: True 8 | enable_sort: True 9 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: Build ra2yrcpp 2 | env: 3 | CMAKE_TOOLCHAIN_FILE: toolchains/mingw-w64-i686.cmake 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Build and verify with docker 8 | shell: bash 9 | run: | 10 | mkdir -p cbuild wine-dir 11 | ./scripts/tools.sh docker-release 12 | 13 | - name: Upload build artifacts 14 | uses: actions/upload-artifact@v4 15 | with: 16 | name: ra2yrcpp-${{ github.sha }}.zip 17 | path: cbuild/mingw-w64-i686-Release/ra2yrcpp.zip 18 | -------------------------------------------------------------------------------- /.github/workflows/build-and-release.yml: -------------------------------------------------------------------------------- 1 | name: Main workflow file 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | - develop-* 7 | 8 | permissions: 9 | contents: write 10 | 11 | env: 12 | CMAKE_TOOLCHAIN_FILE: toolchains/mingw-w64-i686.cmake 13 | 14 | jobs: 15 | build-docker-image: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - run: mkdir -p ~/image-cache 21 | - id: image-cache 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/image-cache 25 | key: image-cache-${{ runner.os }}-${{ hashFiles('./docker/Dockerfile') }} 26 | - if: steps.image-cache.outputs.cache-hit != 'true' 27 | run: | 28 | docker compose build builder 29 | docker save -o ~/image-cache/builder.tar shmocz/ra2yrcpp 30 | build-and-verify: 31 | runs-on: ubuntu-latest 32 | needs: build-docker-image 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | with: 37 | submodules: recursive 38 | - id: image-cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ~/image-cache 42 | key: image-cache-${{ runner.os }} 43 | - if: steps.image-cache.outputs.cache-hit == 'true' 44 | run: | 45 | docker load -i ~/image-cache/builder.tar 46 | - name: Build and verify the library 47 | uses: ./.github/actions/build 48 | 49 | - name: Delete Previous Release 50 | uses: dev-drprasad/delete-tag-and-release@v1.0 51 | with: 52 | tag_name: latest 53 | github_token: ${{ secrets.GITHUB_TOKEN }} 54 | delete_release: true 55 | continue-on-error: true 56 | 57 | - name: Upload New Release. 58 | uses: softprops/action-gh-release@v1 59 | with: 60 | name: Latest 61 | tag_name: latest 62 | body: Latest ra2yrcpp build. 63 | files: | 64 | cbuild/mingw-w64-i686-Release/ra2yrcpp.zip 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude everything 2 | ** 3 | # Whitelist 4 | !.clang-format 5 | !.clang-tidy 6 | !.cmake-format 7 | !.gitignore 8 | !/.github 9 | !/.github/** 10 | !/data 11 | !/data/** 12 | !/docker 13 | !/docker/** 14 | !/pyra2yr 15 | !/pyra2yr/** 16 | !/scripts 17 | !/scripts/** 18 | !/src 19 | !/src/** 20 | !/test_data 21 | !/test_data/** 22 | !/tests 23 | !/tests/** 24 | !/toolchains 25 | !/toolchains/** 26 | !3rdparty 27 | !3rdparty/YRpp 28 | !3rdparty/argparse 29 | !3rdparty/asio 30 | !3rdparty/fmt 31 | !3rdparty/protobuf 32 | !3rdparty/websocketpp 33 | !3rdparty/xbyak 34 | !CMakeLists.txt 35 | !CPPLINT.cfg 36 | !Doxyfile 37 | !LICENSE.md 38 | !Makefile 39 | !README.md 40 | !docker-compose*.yml 41 | # Additional ignores 42 | *__pycache__* 43 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "3rdparty/xbyak"] 2 | path = 3rdparty/xbyak 3 | url = https://github.com/herumi/xbyak.git 4 | [submodule "3rdparty/argparse"] 5 | path = 3rdparty/argparse 6 | url = https://github.com/p-ranav/argparse.git 7 | [submodule "3rdparty/fmt"] 8 | path = 3rdparty/fmt 9 | url = https://github.com/fmtlib/fmt 10 | [submodule "3rdparty/websocketpp"] 11 | path = 3rdparty/websocketpp 12 | url = https://github.com/zaphoyd/websocketpp.git 13 | [submodule "3rdparty/asio"] 14 | path = 3rdparty/asio 15 | url = https://github.com/chriskohlhoff/asio.git 16 | [submodule "3rdparty/YRpp"] 17 | path = 3rdparty/YRpp 18 | url = https://github.com/shmocz/YRpp.git 19 | [submodule "3rdparty/protobuf"] 20 | path = 3rdparty/protobuf 21 | url = https://github.com/shmocz/protobuf.git 22 | [submodule "src/protocol/ra2yrproto"] 23 | path = src/protocol/ra2yrproto 24 | url = https://github.com/shmocz/ra2yrproto.git 25 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0...4.0) 2 | project(app) 3 | 4 | # Fix warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24 5 | if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") 6 | cmake_policy(SET CMP0135 NEW) 7 | endif() 8 | 9 | option(RA2YRCPP_BUILD_CLI_TOOL "Build the main CLI tool" ON) 10 | option(RA2YRCPP_BUILD_MAIN_DLL "Build the main library for Windows targets" ON) 11 | option(RA2YRCPP_BUILD_TESTS "Build tests" ON) 12 | option(RA2YRCPP_DEBUG_LOG "Enable debug logs even for non-debug builds" OFF) 13 | 14 | add_compile_definitions(RA2YRCPP_VERSION=${RA2YRCPP_VERSION}) 15 | 16 | if(MINGW) 17 | add_compile_definitions(__MINGW_FORCE_SYS_INTRINS) 18 | add_compile_options(-masm=intel) 19 | endif() 20 | 21 | if(RA2YRCPP_DEBUG_LOG) 22 | add_compile_definitions(DEBUG_LOG) 23 | endif() 24 | 25 | # FIXME: needed? 26 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 27 | 28 | if(NOT PROTOC_PATH) 29 | find_program(PROTOC_PATH protoc REQUIRED) 30 | endif() 31 | 32 | include(FetchContent) 33 | FetchContent_Declare( 34 | googletest 35 | # Specify the commit you depend on and update it regularly. 36 | URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip 37 | ) 38 | # For Windows: Prevent overriding the parent project's compiler/linker settings 39 | set(gtest_force_shared_crt 40 | ON 41 | CACHE BOOL "" FORCE) 42 | FetchContent_MakeAvailable(googletest) 43 | 44 | if(WIN32) 45 | if(MINGW) 46 | find_library(LIB_WSOCK32 wsock32 REQUIRED) 47 | find_library(LIB_WS2_32 ws2_32 REQUIRED) 48 | find_package(Threads REQUIRED) 49 | else() 50 | set(LIB_WSOCK32 wsock32) 51 | set(LIB_WS2_32 ws2_32) 52 | endif() 53 | endif() 54 | 55 | if(MINGW) 56 | # find MinGW libraries these are copied just for test executables, to avoid 57 | # statically linking each of them 58 | set(MINGW_LIBS zlib1.dll libgcc_s_dw2-1.dll libstdc++-6.dll 59 | libwinpthread-1.dll) 60 | set(MINGW_LIBS_FULL "") 61 | foreach(X IN LISTS MINGW_LIBS) 62 | find_file(LIB_OUT ${X} PATHS ${CMAKE_SYSROOT}/lib ${CMAKE_SYSROOT}/bin 63 | NO_CACHE REQUIRED) 64 | list(APPEND MINGW_LIBS_FULL ${LIB_OUT}) 65 | unset(LIB_OUT) 66 | endforeach() 67 | endif() 68 | 69 | if(NOT MINGW AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 70 | set(cdecl_s "__attribute__((cdecl))") 71 | add_compile_definitions(__cdecl=${cdecl_s}) 72 | endif() 73 | 74 | find_package(ZLIB REQUIRED) 75 | # TODO: check that works for MSVC too 76 | if(WIN32) 77 | add_compile_definitions(protobuf_MSVC_STATIC_RUNTIME=OFF) 78 | endif(WIN32) 79 | set(protobuf_WITH_ZLIB 80 | ON 81 | CACHE BOOL "" FORCE) 82 | if(MINGW) 83 | add_compile_definitions(_WEBSOCKETPP_CPP11_THREAD_) 84 | endif() 85 | add_compile_definitions(ASIO_STANDALONE) 86 | set(Protobuf_USE_STATIC_LIBS 87 | ON 88 | CACHE BOOL "" FORCE) 89 | 90 | if(ZLIB_INCLUDE_DIR) 91 | include_directories(${ZLIB_INCLUDE_DIR}) 92 | endif() 93 | 94 | include_directories(${CMAKE_CURRENT_BINARY_DIR}) 95 | include_directories(${CMAKE_CURRENT_BINARY_DIR}/src/protocol) 96 | include_directories(src) 97 | # FIXME: override protobuf include path 98 | if(NOT RA2YRCPP_SYSTEM_PROTOBUF) 99 | include_directories(SYSTEM 3rdparty/protobuf/src) 100 | endif() 101 | include_directories(SYSTEM 3rdparty/xbyak) 102 | include_directories(3rdparty/argparse/include) 103 | include_directories(SYSTEM 3rdparty/fmt/include) 104 | include_directories(SYSTEM 3rdparty/asio/asio/include) 105 | include_directories(SYSTEM 3rdparty/websocketpp) 106 | add_subdirectory(3rdparty/fmt) 107 | if(RA2YRCPP_BUILD_MAIN_DLL) 108 | include_directories(SYSTEM 3rdparty/YRpp) 109 | add_subdirectory(3rdparty/YRpp) 110 | add_subdirectory(src/addscn) 111 | target_compile_options(YRpp INTERFACE -Wno-pragmas -Wno-return-type 112 | -Wno-inconsistent-missing-override) 113 | endif() 114 | add_subdirectory(src) 115 | 116 | if(RA2YRCPP_BUILD_TESTS) 117 | add_subdirectory(tests) 118 | endif() 119 | -------------------------------------------------------------------------------- /CPPLINT.cfg: -------------------------------------------------------------------------------- 1 | set noparent 2 | 3 | # Global filters 4 | filter=-build/include_order,-build/include_subdir,-build/namespaces 5 | filter=-build/c++11,-legal/copyright,-readability/todo, 6 | filter=-runtime/int,-runtime/string,-runtime/printf 7 | filter=-whitespace/indent_namespace,-whitespace/line_length 8 | 9 | # File-specific filters 10 | filter=-build/c++17:tests/test_protocol.cpp 11 | filter=-readability/casting:src/win32/windows_debug.cpp 12 | -------------------------------------------------------------------------------- /data/0001-Fix-to-get-CMake-work-with-clang-cl.patch: -------------------------------------------------------------------------------- 1 | From fe9e18d8dcd8d6ec8eae134025a22eb666d1f9bd Mon Sep 17 00:00:00 2001 2 | From: shmocz <112764837+shmocz@users.noreply.github.com> 3 | Date: Thu, 16 Feb 2023 04:15:27 +0200 4 | Subject: [PATCH] Fix to get CMake work with clang-cl 5 | 6 | --- 7 | CMakeLists.txt | 2 +- 8 | 1 file changed, 1 insertion(+), 1 deletion(-) 9 | 10 | diff --git a/CMakeLists.txt b/CMakeLists.txt 11 | index 04cb3303a..0a906da1d 100644 12 | --- a/CMakeLists.txt 13 | +++ b/CMakeLists.txt 14 | @@ -271,7 +271,7 @@ if (MSVC) 15 | # Suppress linker warnings about files with no symbols defined. 16 | set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_STATIC_LINKER_FLAGS} /ignore:4221") 17 | 18 | - if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 19 | + if ((CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")) 20 | # Configure Resource Compiler 21 | enable_language(RC) 22 | # use English language (0x409) in resource compiler 23 | -- 24 | 2.41.0 25 | 26 | -------------------------------------------------------------------------------- /data/patches.txt: -------------------------------------------------------------------------------- 1 | d0x7cd80f:609c5589e568646c6c00686370702e6861327972686c6962728d45f050a120127e00ffd089ec506a656872766963685f69736568696e69748d45ec508b45fc50a150127e00ffd06a0068b93800006a10ffd089ec5d9d61:s10 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | builder: 5 | image: shmocz/ra2yrcpp:latest 6 | build: 7 | context: . 8 | dockerfile: docker/Dockerfile 9 | volumes: 10 | - .:/home/user/project 11 | - ./wine-dir:/home/user/.wine 12 | user: "${UID}:${UID}" 13 | command: "${COMMAND}" 14 | working_dir: /home/user/project 15 | clang-cl: 16 | image: shmocz/clang-cl:latest 17 | build: 18 | context: . 19 | dockerfile: docker/clang-cl-msvc.Dockerfile 20 | volumes: 21 | - .:/home/user/project 22 | - ./.wine-clang:/home/user/.wine 23 | command: "${COMMAND}" 24 | working_dir: /home/user/project 25 | user: "${UID}:${UID}" 26 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 as mingw 2 | ARG CPPCHECK_VERSION=2.17.1 3 | ARG CPPLINT_VERSION=2.0.2 4 | 5 | RUN dpkg --add-architecture i386 6 | RUN \ 7 | apt-get update -y && apt-get install --no-install-recommends -y \ 8 | bash \ 9 | ca-certificates \ 10 | cmake \ 11 | g++-mingw-w64-i686-posix \ 12 | git \ 13 | make \ 14 | p7zip-full 15 | 16 | RUN \ 17 | apt-get install -y \ 18 | gdb \ 19 | gdb-mingw-w64 \ 20 | gdb-mingw-w64-target \ 21 | libpcre3-dev \ 22 | libz-mingw-w64-dev \ 23 | python3 \ 24 | python3-pip \ 25 | wine32 26 | 27 | RUN pip install -U --break-system-packages \ 28 | clang-format==19.1.6 \ 29 | cmakelang==0.6.13 \ 30 | cpplint==${CPPLINT_VERSION} \ 31 | iced-x86 32 | # Fix some library paths 33 | RUN ln -s /usr/lib/gcc/i686-w64-mingw32/*-posix/libgcc_s_dw2-1.dll \ 34 | /usr/lib/gcc/i686-w64-mingw32/*-posix/libstdc++-6.dll \ 35 | /usr/i686-w64-mingw32/lib 36 | 37 | # Create user and necessary folders 38 | RUN useradd -m user && mkdir -p /home/user/project /home/user/.wine && chmod -R 0777 /home/user 39 | 40 | RUN \ 41 | apt-get install -y \ 42 | g++ 43 | 44 | # Grab protobuf sources 45 | RUN git clone --recurse-submodules --depth 1 --branch v3.21.12 "https://github.com/protocolbuffers/protobuf.git" /app/protobuf 46 | WORKDIR /app/protobuf 47 | ADD toolchains/mingw-w64-i686.cmake . 48 | RUN mkdir -p target/linux64 target/win32 build/linux64 build/win32 49 | # Build protoc (natively) 50 | RUN cmake \ 51 | -DCMAKE_INSTALL_PREFIX=/usr \ 52 | -Dprotobuf_BUILD_LIBPROTOC=OFF \ 53 | -Dprotobuf_BUILD_PROTOC_BINARIES=ON \ 54 | -Dprotobuf_WITH_ZLIB=OFF \ 55 | -Dprotobuf_BUILD_TESTS=OFF \ 56 | -S . \ 57 | -B build/linux64 58 | 59 | RUN cmake --build build/linux64 --parallel $(nproc) 60 | RUN cd build/linux64 && make install && make clean 61 | 62 | # Build protobuf library DLL and protoc (win32) 63 | # TODO: use our makefile 64 | RUN cmake --toolchain mingw-w64-i686.cmake \ 65 | -DCMAKE_INSTALL_PREFIX=/usr/i686-w64-mingw32 \ 66 | -Dprotobuf_BUILD_LIBPROTOC=ON \ 67 | -Dprotobuf_BUILD_PROTOC_BINARIES=ON \ 68 | -Dprotobuf_WITH_ZLIB=ON \ 69 | -DProtobuf_USE_STATIC_LIBS=ON \ 70 | -Dprotobuf_MSVC_STATIC_RUNTIME=OFF \ 71 | -Dprotobuf_BUILD_EXAMPLES=OFF \ 72 | -Dprotobuf_INSTALL=ON \ 73 | -Dprotobuf_BUILD_TESTS=OFF \ 74 | -DZLIB_LIB=/usr/i686-w64-mingw32/lib \ 75 | -DZLIB_INCLUDE_DIR=/usr/i686-w64-mingw32/include \ 76 | -S . \ 77 | -B build/win32 78 | RUN cmake --build build/win32 --parallel $(nproc) 79 | RUN cd build/win32 && make install && make clean 80 | 81 | # Build newer cppcheck 82 | RUN git clone --depth 1 https://github.com/danmar/cppcheck.git --branch ${CPPCHECK_VERSION} /app/cppcheck && \ 83 | cd /app/cppcheck && mkdir build && cd build && \ 84 | cmake -DCMAKE_BUILD_TYPE=Release -DUSE_MATCHCOMPILER=ON -DHAVE_RULES=ON .. && cmake --build . --parallel $(nproc) --config Release && cmake --build . --target install 85 | 86 | # Remove temporary packages and clean cache 87 | RUN apt-get remove -y g++ && \ 88 | apt-get clean -y && \ 89 | apt-get autoremove -y && \ 90 | rm -rf /var/lib/apt/lists/* -------------------------------------------------------------------------------- /docker/clang-cl-msvc.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 as msvc-wine 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y wine64-development python3 msitools python3-simplejson git \ 5 | python3-six ca-certificates && \ 6 | apt-get clean -y && \ 7 | rm -rf /var/lib/apt/lists/* 8 | 9 | RUN mkdir /opt/msvc 10 | 11 | # Clone mstorsjo's repo 12 | RUN git clone "https://github.com/mstorsjo/msvc-wine.git" /opt/msvc-wine 13 | 14 | WORKDIR /opt/msvc-wine 15 | 16 | RUN PYTHONUNBUFFERED=1 ./vsdownload.py --accept-license --dest /opt/msvc && \ 17 | ./install.sh /opt/msvc && \ 18 | rm lowercase fixinclude install.sh vsdownload.py && \ 19 | rm -rf wrappers 20 | 21 | FROM msvc-wine as clang-cl 22 | 23 | RUN dpkg --add-architecture i386 24 | 25 | RUN --mount=type=cache,target=/var/cache/apt \ 26 | apt-get update && \ 27 | apt-get install -y \ 28 | clang-15 \ 29 | clang-tools-15 \ 30 | cmake \ 31 | libz-mingw-w64-dev \ 32 | lld-15 \ 33 | make \ 34 | ninja-build \ 35 | wine32-development 36 | 37 | RUN for p in lld-link clang-cl llvm-rc llvm-lib; do \ 38 | ln -s /usr/bin/$p-15 /usr/bin/$p; \ 39 | done 40 | 41 | # TODO: probably not needed 42 | RUN ln -s /usr/bin/llvm-rc /usr/bin/rc 43 | # Initialize wineroot 44 | RUN wine64 wineboot --init && \ 45 | while pgrep wineserver > /dev/null; do sleep 1; done 46 | 47 | RUN --mount=type=cache,target=/var/cache/apt \ 48 | apt-get install -y --no-install-recommends python3 python3-pip && \ 49 | apt-get clean -y && \ 50 | rm -rf /var/lib/apt/lists/* && \ 51 | pip install --break-system-packages -U iced-x86 52 | 53 | # Remove unused stuff 54 | RUN rm -rf /opt/msvc/VC/Tools/MSVC/*/lib/{arm,arm64,arm64ec} \ 55 | /opt/msvc/kits/10/Lib/*/{ucrt,um}/{arm,arm64} \ 56 | /opt/msvc/kits/10/Redist/*/{arm,arm64} \ 57 | /opt/msvc/kits/10/Redist/*/ucrt/DLLs/{arm,arm64} \ 58 | /opt/msvc/kits/10/bin/{arm,arm64} \ 59 | /opt/msvc/bin/{arm,arm64} 60 | 61 | RUN useradd -m user && mkdir -p /home/user/project /home/user/.wine && chmod -R 0777 /home/user 62 | -------------------------------------------------------------------------------- /scripts/build-and-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # Helper script to build and test various build setups. 3 | 4 | export BUILDDIR=cbuild_docker 5 | 6 | for type in Release; do 7 | echo clang-cl-msvc "$type" "clang-cl" 8 | echo mingw-w64-i686-docker "$type" "builder" 9 | done | while read tc tt cont; do 10 | export CMAKE_TOOLCHAIN_FILE="toolchains/$tc.cmake" 11 | export CMAKE_BUILD_TYPE="$tt" 12 | export BUILDER="$cont" 13 | export NPROC=8 14 | set -e 15 | make docker_build .c.json 5 | cp .c.json build/compile_commands.json 6 | ../iwyu/include-what-you-use/iwyu_tool.py -j 16 -p build src tests -- -Xiwyu --mapping_file="$(pwd)/.tmp/srv.imp" | tee iwyu.out 7 | ./scripts/iwyu-check.py iwyu.out | 8 | grep -Pv "vcruntime_new|gtest-|websocketpp|net/proto2|xtree|xtr1common|type_traits.+for\s+move|system_error.+for\s+error_code" | 9 | tee iwyu.filt.out 10 | -------------------------------------------------------------------------------- /scripts/parse_logfile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | perl -ne ' 4 | my ($level, $tid, $ts, $msg) = ($_ =~ m/(DEBUG|ERROR):\s+\[thread\s+(\d+)\s+TS:\s+(\d+)\]:\s*(.+)/g); 5 | if ($msg) { 6 | $msg =~ s/\s+$//g; 7 | $ts = $ts / 1e9; 8 | print join("\t", $level, $tid, $ts, $msg) . "\n"; 9 | } 10 | ' 11 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${CMAKE_CURRENT_BINARY_DIR}) 2 | add_subdirectory(protocol) 3 | 4 | set(RA2YRCPP_LINKAGE "PRIVATE") 5 | set(RA2YRCPP_LIBRARY_NAME "ra2yrcpp") 6 | if(MSVC) 7 | set(RA2YRCPP_LINKAGE "PUBLIC") 8 | set(RA2YRCPP_LIBRARY_NAME "libra2yrcpp") 9 | endif() 10 | 11 | add_library( 12 | ra2yrcpp_core STATIC 13 | $ 14 | asio_utils.cpp 15 | auto_thread.cpp 16 | client_connection.cpp 17 | command/command_manager.cpp 18 | command/is_command.cpp 19 | commands_builtin.cpp 20 | config.cpp 21 | errors.cpp 22 | hook.cpp 23 | instrumentation_client.cpp 24 | instrumentation_service.cpp 25 | logging.cpp 26 | multi_client.cpp 27 | process.cpp 28 | utility/sync.cpp 29 | websocket_connection.cpp 30 | websocket_server.cpp 31 | x86.cpp) 32 | 33 | if(NOT PROTO_LIB) 34 | find_library(PROTO_LIB protobuf libprotobuf REQUIRED) 35 | endif() 36 | target_link_libraries(ra2yrcpp_core PUBLIC fmt::fmt "${PROTO_LIB}" protocol) 37 | if(WIN32) 38 | add_library( 39 | windows_utils STATIC win32/win_message.cpp win32/windows_debug.cpp 40 | win32/windows_utils.cpp) 41 | 42 | target_link_libraries(ra2yrcpp_core PUBLIC windows_utils ${LIB_WSOCK32} 43 | ${LIB_WS2_32}) 44 | endif() 45 | 46 | target_compile_options(ra2yrcpp_core PUBLIC ${RA2YRCPP_EXTRA_FLAGS}) 47 | target_link_options(ra2yrcpp_core PUBLIC ${RA2YRCPP_EXTRA_FLAGS}) 48 | 49 | if(RA2YRCPP_BUILD_MAIN_DLL) 50 | add_library( 51 | yrclient STATIC 52 | commands_game.cpp 53 | commands_yr.cpp 54 | dll_inject.cpp 55 | hooks_yr.cpp 56 | is_context.cpp 57 | ra2/abi.cpp 58 | ra2/common.cpp 59 | ra2/event_list.cpp 60 | ra2/state_context.cpp 61 | ra2/state_parser.cpp 62 | ra2/yrpp_export.cpp 63 | ra2yrcpp.cpp 64 | ra2yrcppcli/ra2yrcppcli.cpp) 65 | 66 | target_link_libraries( 67 | yrclient 68 | ${RA2YRCPP_LINKAGE} 69 | "${PROTO_LIB}" 70 | ra2yrcpp_core 71 | protocol 72 | ZLIB::ZLIB 73 | fmt::fmt 74 | YRpp) 75 | 76 | target_link_libraries(yrclient PUBLIC windows_utils) 77 | 78 | if(WIN32) 79 | target_link_libraries(yrclient PUBLIC ${LIB_WSOCK32} ${LIB_WS2_32}) 80 | endif() 81 | 82 | add_library(ra2yrcpp_dll SHARED yrclient_dll.cpp) 83 | target_sources(ra2yrcpp_dll PRIVATE version.rc) 84 | 85 | if(MINGW) 86 | target_link_libraries( 87 | ra2yrcpp_dll 88 | PUBLIC yrclient -static-libgcc -static-libstdc++ 89 | -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive) 90 | else() 91 | target_link_libraries(ra2yrcpp_dll PUBLIC yrclient) 92 | endif() 93 | 94 | set_target_properties(ra2yrcpp_dll PROPERTIES OUTPUT_NAME 95 | ${RA2YRCPP_LIBRARY_NAME}) 96 | 97 | add_custom_target(ra2yrcpp) 98 | add_dependencies(ra2yrcpp yrclient) 99 | if(RA2YRCPP_BUILD_CLI_TOOL) 100 | add_library(ra2yrcppcli OBJECT ./ra2yrcppcli/ra2yrcppcli.cpp) 101 | target_link_libraries(ra2yrcppcli PUBLIC yrclient) 102 | 103 | add_subdirectory(ra2yrcppcli) 104 | add_dependencies(ra2yrcpp ra2yrcppcli) 105 | endif() 106 | 107 | if(MINGW) 108 | list(TRANSFORM MINGW_LIBS PREPEND "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/" 109 | OUTPUT_VARIABLE MINGW_LIBS_TEST) 110 | 111 | add_custom_target(copy-mingw-libraries ALL DEPENDS ${MINGW_LIBS_TEST}) 112 | add_custom_command( 113 | OUTPUT ${MINGW_LIBS_TEST} 114 | COMMAND ${CMAKE_COMMAND} -E copy ${MINGW_LIBS_FULL} 115 | ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} 116 | DEPENDS ${MINGW_LIBS_FULL}) 117 | endif() 118 | 119 | install(TARGETS ra2yrcpp_dll RUNTIME) 120 | if(MINGW) 121 | install(FILES ${MINGW_LIBS_TEST} TYPE BIN) 122 | endif() 123 | endif() 124 | -------------------------------------------------------------------------------- /src/addscn/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(addscn) 2 | 3 | add_executable(addscn addscn.cpp) 4 | target_link_libraries(addscn -static) 5 | install(TARGETS addscn RUNTIME) 6 | -------------------------------------------------------------------------------- /src/addscn/addscn.cpp: -------------------------------------------------------------------------------- 1 | // Based on this: https://github.com/hMihaiDavid/addscn 2 | #include 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | static inline unsigned alignup(unsigned x, unsigned a) { 16 | return ((x + a - 1) / a) * a; 17 | } 18 | 19 | struct FileSize { 20 | unsigned int low; 21 | unsigned int high; 22 | }; 23 | 24 | struct ImageHeaders { 25 | PIMAGE_DOS_HEADER dos; 26 | PIMAGE_NT_HEADERS nt; 27 | PIMAGE_FILE_HEADER file; 28 | PIMAGE_SECTION_HEADER first; 29 | }; 30 | 31 | struct FileMapping { 32 | HANDLE hFileMapping{nullptr}; 33 | void* pView{nullptr}; 34 | std::unique_ptr hFile; 35 | 36 | explicit FileMapping(std::string path) 37 | : hFile(std::unique_ptr( 38 | CreateFile(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, 39 | OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL), 40 | [](auto* d) { CloseHandle(d); })) { 41 | if (hFile.get() == INVALID_HANDLE_VALUE) { 42 | throw std::runtime_error("CreateFile() failed"); 43 | } 44 | } 45 | 46 | ~FileMapping() { 47 | if (pView != nullptr && hFileMapping != nullptr) { 48 | unmap(); 49 | } 50 | } 51 | 52 | FileSize size() { 53 | DWORD high = 0U; 54 | auto low = GetFileSize(hFile.get(), &high); 55 | return {low, high}; 56 | } 57 | 58 | void map(DWORD flProtect, DWORD dwAccess, DWORD newSize = 0U) { 59 | if ((hFileMapping = CreateFileMapping(hFile.get(), NULL, flProtect, 0, 60 | newSize, NULL)) == 61 | INVALID_HANDLE_VALUE) { 62 | hFileMapping = nullptr; 63 | throw std::runtime_error("failed to map file"); 64 | } 65 | 66 | pView = MapViewOfFile(hFileMapping, dwAccess, 0, 0, 0); 67 | } 68 | 69 | ImageHeaders get_headers() { 70 | ImageHeaders I{(PIMAGE_DOS_HEADER)pView, NULL, NULL, NULL}; 71 | I.nt = 72 | (PIMAGE_NT_HEADERS)(reinterpret_cast(pView) + I.dos->e_lfanew); 73 | I.file = &(I.nt->FileHeader); 74 | I.first = (PIMAGE_SECTION_HEADER)((reinterpret_cast(I.file) + 75 | sizeof(*I.file)) + 76 | I.nt->FileHeader.SizeOfOptionalHeader); 77 | return I; 78 | } 79 | 80 | bool unmap() { 81 | UnmapViewOfFile(pView); 82 | CloseHandle(hFileMapping); 83 | pView = nullptr; 84 | hFileMapping = nullptr; 85 | return true; 86 | } 87 | 88 | void append_section(std::string name, unsigned VirtualSize, 89 | unsigned Characteristics) { 90 | auto H = get_headers(); 91 | DWORD fileAlignment = H.nt->OptionalHeader.FileAlignment; 92 | DWORD imageBase = H.nt->OptionalHeader.ImageBase; 93 | auto sz = size(); 94 | 95 | // remap 96 | unmap(); 97 | map(PAGE_READWRITE, FILE_MAP_READ | FILE_MAP_WRITE, 98 | alignup(sz.low + VirtualSize, fileAlignment)); 99 | 100 | // append section 101 | WORD numberOfSections = H.nt->FileHeader.NumberOfSections; 102 | DWORD sectionAlignment = H.nt->OptionalHeader.SectionAlignment; 103 | auto s_new = &H.first[numberOfSections]; 104 | auto s_last = &H.first[numberOfSections - 1]; 105 | 106 | memset(s_new, 0, sizeof(*s_new)); 107 | memcpy(&s_new->Name, name.c_str(), std::min(name.size(), 8U)); 108 | s_new->Misc.VirtualSize = VirtualSize; 109 | s_new->VirtualAddress = alignup( 110 | s_last->VirtualAddress + s_last->Misc.VirtualSize, sectionAlignment); 111 | s_new->SizeOfRawData = alignup(VirtualSize, fileAlignment); 112 | s_new->PointerToRawData = sz.low; 113 | s_new->Characteristics = Characteristics; 114 | 115 | H.nt->FileHeader.NumberOfSections = (numberOfSections + 1); 116 | H.nt->OptionalHeader.SizeOfImage = alignup( 117 | s_new->VirtualAddress + s_new->Misc.VirtualSize, sectionAlignment); 118 | 119 | memset((reinterpret_cast(pView) + s_new->PointerToRawData), 0, 120 | s_new->SizeOfRawData); 121 | 122 | std::cout << name.c_str() << ":0x" << std::hex << VirtualSize << ":0x" 123 | << std::hex << s_new->VirtualAddress + imageBase << ":0x" 124 | << std::hex << s_new->PointerToRawData << std::endl; 125 | } 126 | }; 127 | 128 | void add_section(std::string path, std::string section_name, 129 | unsigned int virtual_size, unsigned int characteristics) { 130 | FileMapping F(path); 131 | auto sz = F.size(); 132 | if (sz.high != 0U) { 133 | throw std::runtime_error("large files not supported"); 134 | } 135 | 136 | F.map(PAGE_READONLY, FILE_MAP_READ); 137 | 138 | auto H = F.get_headers(); 139 | #ifdef _WIN64 140 | #define MACHINE IMAGE_FILE_MACHINE_AMD64 141 | #else 142 | #define MACHINE IMAGE_FILE_MACHINE_I386 143 | #endif 144 | if (H.dos->e_magic != IMAGE_DOS_SIGNATURE || 145 | H.nt->Signature != IMAGE_NT_SIGNATURE || 146 | H.nt->FileHeader.Machine != MACHINE) { 147 | throw std::runtime_error("bad PE file"); 148 | } 149 | 150 | F.append_section(section_name, virtual_size, characteristics); 151 | } 152 | 153 | int main(int argc, const char* argv[]) { 154 | if (argc < 5) { 155 | std::cerr 156 | << "usage: " << argv[0] 157 | << "
" << std::endl 158 | << "Where VirtualSize is size of the section (hex or decimal), and " 159 | "Characteristics the COFF characteristics flag (hex number or " 160 | "decimal). Common flags: text: 0x60000020: " 161 | "data: 0xC0000040 rdata: 0x40000040" 162 | << std::endl; 163 | 164 | return EXIT_FAILURE; 165 | } 166 | 167 | try { 168 | add_section(argv[1], argv[2], std::stoul(argv[3], nullptr, 0), 169 | std::stoul(argv[4], nullptr, 0)); 170 | } catch (const std::exception& e) { 171 | std::cerr << "error: " << e.what() << std::endl; 172 | return EXIT_FAILURE; 173 | } 174 | return EXIT_SUCCESS; 175 | } 176 | -------------------------------------------------------------------------------- /src/asio_utils.cpp: -------------------------------------------------------------------------------- 1 | #include "asio_utils.hpp" 2 | 3 | #include "logging.hpp" 4 | #include "types.h" 5 | #include "utility/sync.hpp" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace ra2yrcpp::asio_utils; 18 | namespace lib = websocketpp::lib; 19 | using ios_t = lib::asio::io_context; 20 | 21 | class AsioSocket::socket_impl : public lib::asio::ip::tcp::socket { 22 | public: 23 | explicit socket_impl(ios_t& io) : lib::asio::ip::tcp::socket(io) {} // NOLINT 24 | }; 25 | 26 | /// Submit function to be executed by io_service 27 | static void do_post(void* ptr, std::function fn) { 28 | lib::asio::post(*reinterpret_cast(ptr), fn); 29 | } 30 | 31 | /// Submit a function to be executed by io_service and wait for it's completion. 32 | static void post_(void* ios, std::function fn) { 33 | util::AtomicVariable done(false); 34 | do_post(ios, [fn, &done]() { 35 | fn(); 36 | done.store(true); 37 | }); 38 | 39 | done.wait(true); 40 | } 41 | 42 | struct IOService::IOService_impl { 43 | ios_t service_; 44 | lib::asio::executor_work_guard guard_; 45 | 46 | IOService_impl() : guard_(lib::asio::make_work_guard(service_)) {} 47 | 48 | void run() { service_.run(); } 49 | 50 | void reset() { guard_.reset(); } 51 | }; 52 | 53 | IOService::IOService() 54 | : service_(std::make_unique()), main_thread_() { 55 | main_thread_ = std::make_unique( 56 | [](IOService_impl* srv) { srv->run(); }, service_.get()); 57 | } 58 | 59 | IOService::~IOService() { 60 | service_->reset(); 61 | main_thread_->join(); 62 | dprintf("exit main thread"); 63 | } 64 | 65 | void IOService::post(std::function fn, bool wait) { 66 | auto* s = &service_->service_; 67 | if (wait) { 68 | post_(s, fn); 69 | } else { 70 | do_post(s, fn); 71 | } 72 | } 73 | 74 | void* IOService::get_service() { 75 | return reinterpret_cast(&service_->service_); 76 | } 77 | 78 | AsioSocket::AsioSocket() {} 79 | 80 | AsioSocket::AsioSocket(std::shared_ptr srv) 81 | : srv(srv), 82 | socket_(std::make_unique( 83 | *reinterpret_cast(srv->get_service()))) {} 84 | 85 | AsioSocket::~AsioSocket() { 86 | lib::error_code ec; 87 | auto& s = *socket_.get(); 88 | 89 | s.shutdown(lib::asio::socket_base::shutdown_both, ec); 90 | if (ec) { 91 | eprintf("socket shutdown failed: ", ec.message()); 92 | } 93 | s.close(ec); 94 | if (ec) { 95 | eprintf("socket close failed: ", ec.message()); 96 | } 97 | } 98 | 99 | void AsioSocket::connect(std::string host, std::string port) { 100 | socket_->connect( 101 | lib::asio::ip::tcp::endpoint{lib::asio::ip::address_v4::from_string(host), 102 | static_cast(std::stoi(port))}); 103 | } 104 | 105 | std::size_t AsioSocket::write(const std::string& buffer) { 106 | lib::error_code ec; 107 | std::size_t count = 108 | lib::asio::write(*socket_.get(), lib::asio::buffer(buffer)); 109 | 110 | if (ec) { 111 | throw std::runtime_error(fmt::format("failed to write: {}", ec.message())); 112 | } 113 | return count; 114 | } 115 | 116 | std::string AsioSocket::read() { 117 | lib::error_code ec; 118 | std::string rsp; 119 | lib::asio::read(*socket_.get(), lib::asio::dynamic_buffer(rsp), 120 | lib::asio::transfer_all(), ec); 121 | if (!(!ec || ec == lib::asio::error::eof)) { 122 | throw std::runtime_error(fmt::format("failed to write: {}", ec.message())); 123 | } 124 | return rsp; 125 | } 126 | -------------------------------------------------------------------------------- /src/asio_utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace ra2yrcpp { 11 | namespace asio_utils { 12 | 13 | class IOService { 14 | public: 15 | IOService(); 16 | ~IOService(); 17 | void post(std::function fn, bool wait = true); 18 | void* get_service(); 19 | 20 | private: 21 | struct IOService_impl; 22 | 23 | std::unique_ptr service_; 24 | std::unique_ptr main_thread_; 25 | }; 26 | 27 | struct AsioSocket { 28 | AsioSocket(); 29 | explicit AsioSocket(std::shared_ptr srv); 30 | ~AsioSocket(); 31 | 32 | void connect(std::string host, std::string port); 33 | /// Synchronously write the buffer to the socket 34 | /// 35 | /// @return amount of bytes transferred. 36 | /// @exception std::runtime_error on write failure 37 | std::size_t write(const std::string& buffer); 38 | 39 | /// Synchronously read from the socket. 40 | /// @exception std::runtime_error on read failure 41 | std::string read(); 42 | 43 | std::shared_ptr srv; 44 | class socket_impl; 45 | 46 | std::unique_ptr socket_; 47 | }; 48 | 49 | } // namespace asio_utils 50 | } // namespace ra2yrcpp 51 | -------------------------------------------------------------------------------- /src/async_map.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "async_queue.hpp" 3 | #include "logging.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace async_map { 13 | 14 | namespace { 15 | using namespace std::chrono_literals; 16 | } 17 | 18 | template 19 | bool wait_until(std::unique_lock* lock, std::condition_variable* cv, 20 | PredT pred, duration_t timeout = 0.0s) { 21 | return (cv->wait_for(*lock, timeout, pred)); 22 | } 23 | 24 | // TODO(shmocz): size limit 25 | // TODO(shmocz): This is only used in multi_client. Could re-use elsewhere. 26 | template 27 | class AsyncMap : public async_queue::AsyncContainer { 28 | public: 29 | AsyncMap() = default; 30 | 31 | void put(KeyT key, T item) { 32 | std::unique_lock l(a_.get()->m); 33 | auto [it, status] = data_.try_emplace(key, item); 34 | if (!status) { 35 | throw std::runtime_error(fmt::format("key exists: {}", key)); 36 | } 37 | notify_all(); 38 | } 39 | 40 | /// Get item by key. If not found until timeout, throw exception. 41 | T get(KeyT key, duration_t timeout = 0.0s) { 42 | auto* a = a_.get(); 43 | std::unique_lockm)> l(a->m); 44 | if (timeout > 0.0s) { 45 | if (!wait_until( 46 | &l, &a->cv, [&] { return (data_.find(key) != data_.end()); }, 47 | timeout)) { 48 | throw std::runtime_error( 49 | fmt::format("Timeout after {}ms key: {}", timeout.count(), key)); 50 | } 51 | return data_.at(key); 52 | } 53 | return data_.at(key); 54 | } 55 | 56 | void erase(KeyT key) { 57 | auto* a = a_.get(); 58 | std::unique_lockm)> l(a->m); 59 | data_.erase(key); 60 | } 61 | 62 | bool empty() const { return size() == 0; } 63 | 64 | std::size_t size() const { return data_.size(); } 65 | 66 | private: 67 | std::map data_; 68 | std::mutex mut_; 69 | }; 70 | 71 | } // namespace async_map 72 | -------------------------------------------------------------------------------- /src/async_queue.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "logging.hpp" 3 | #include "utility/time.hpp" 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace async_queue { 19 | 20 | namespace { 21 | using namespace std::chrono_literals; 22 | } 23 | 24 | struct AsyncData { 25 | std::mutex m; 26 | std::condition_variable cv; 27 | }; 28 | 29 | class AsyncContainer { 30 | public: 31 | AsyncContainer() : a_(std::make_unique()) {} 32 | 33 | AsyncContainer(AsyncContainer& o) : a_(o.a_) {} 34 | 35 | void notify_all() { a_.get()->cv.notify_all(); } 36 | 37 | protected: 38 | std::shared_ptr a_; 39 | }; 40 | 41 | // NB: pop method is undefined for empty queues - ensure that this is handled 42 | // correctly elsewhere 43 | template > 44 | class AsyncQueue : public AsyncContainer { 45 | public: 46 | using queue_t = QueueT; 47 | 48 | explicit AsyncQueue(std::size_t max_size = 0U) 49 | : AsyncContainer(), max_size_(max_size) {} 50 | 51 | explicit AsyncQueue(QueueT q, std::size_t max_size = 0U) 52 | : AsyncContainer(), q_(q), max_size_(max_size) {} 53 | 54 | AsyncQueue(const AsyncQueue& o) 55 | : AsyncContainer(o), q_(o.q_), max_size_(o.max_size_) {} 56 | 57 | AsyncQueue(AsyncQueue&& o) 58 | : AsyncContainer(o), q_(o), max_size_(o.max_size_) {} 59 | 60 | AsyncQueue& operator=(const AsyncQueue& o) { 61 | a_ = o.a_; 62 | q_ = o.q_; 63 | max_size_ = o.max_size_; 64 | return *this; 65 | } 66 | 67 | void push(T t) { emplace(std::move(t)); } 68 | 69 | /// 70 | /// Put an item to the queue, notifying everyone waiting for new items. If the 71 | /// queue is bounded, block until free space is available. 72 | /// 73 | void emplace(T&& t) { 74 | std::unique_lock l(a_.get()->m); 75 | if (max_size_ > 0 && size() + 1 > max_size_) { 76 | a_.get()->cv.wait(l, [&] { return size() + 1 <= max_size_; }); 77 | } 78 | q_.emplace(std::move(t)); 79 | #ifdef LOG_TRACE 80 | dprintf("notifying, a={},sz={}", reinterpret_cast(a_.get()), size()); 81 | #endif 82 | notify_all(); 83 | } 84 | 85 | // Pop items from queue. If count < 1, pop all items. If timeout > 0, block 86 | // and wait up to that amount for results. 87 | std::vector pop( 88 | std::size_t count = 1, duration_t timeout = 0.0s, 89 | std::function predicate = [](const T&) { return true; }) { 90 | std::unique_lock l(a_.get()->m); 91 | #ifdef LOG_TRACE 92 | dprintf("locked={},asyncdata={},count={},timeout={}", l.owns_lock(), 93 | reinterpret_cast(a_.get()), count, timeout.count()); 94 | #endif 95 | std::vector res; 96 | std::vector pred_false; 97 | do { 98 | if (timeout > 0.0s) { 99 | if (!a_.get()->cv.wait_for(l, timeout, [&] { return size() > 0; })) { 100 | notify_all(); 101 | return res; 102 | } 103 | } else if (empty()) { 104 | return res; 105 | } 106 | int num_pop = count < 1 ? size() : std::min(count, size()); 107 | // TODO: use random access container to avoid popping and pushing back 108 | while (num_pop-- > 0) { 109 | auto& p = q_.front(); 110 | if (!predicate(p)) { 111 | pred_false.emplace_back(std::move(p)); 112 | } else { 113 | res.emplace_back(std::move(p)); 114 | } 115 | // TODO: ensure that this is FIFO 116 | q_.pop(); 117 | } 118 | for (auto& p : pred_false) { 119 | q_.emplace(std::move(p)); 120 | } 121 | pred_false.clear(); 122 | } while (res.size() < count); 123 | notify_all(); 124 | return res; 125 | } 126 | 127 | bool empty() const { return size() == 0; } 128 | 129 | std::size_t size() const { return q_.size(); } 130 | 131 | private: 132 | QueueT q_; 133 | std::size_t max_size_; 134 | }; 135 | }; // namespace async_queue 136 | -------------------------------------------------------------------------------- /src/auto_thread.cpp: -------------------------------------------------------------------------------- 1 | #include "auto_thread.hpp" 2 | 3 | using namespace utility; 4 | 5 | auto_thread::auto_thread(std::function fn) 6 | : _thread(std::thread(fn)) {} 7 | 8 | auto_thread::~auto_thread() { _thread.join(); } 9 | -------------------------------------------------------------------------------- /src/auto_thread.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "async_queue.hpp" 4 | #include "constants.hpp" 5 | #include "logging.hpp" 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace utility { 16 | 17 | namespace { 18 | using namespace std::chrono_literals; 19 | }; 20 | 21 | struct auto_thread { 22 | std::thread _thread; 23 | 24 | explicit auto_thread(std::function fn); 25 | 26 | auto_thread(const auto_thread& o) = delete; 27 | auto_thread& operator=(const auto_thread& o) = delete; 28 | auto_thread(auto_thread&& o) = delete; 29 | auto_thread& operator=(auto_thread&& o) = delete; 30 | 31 | ~auto_thread(); 32 | }; 33 | 34 | // TODO: why not pass fn and arg in the same struct? 35 | template 36 | struct worker_util { 37 | struct work_item { 38 | bool destroy{false}; 39 | T item; 40 | std::function consume_fn; 41 | }; 42 | 43 | async_queue::AsyncQueue work; 44 | std::function consumer_fn; 45 | utility::auto_thread t; 46 | 47 | explicit worker_util(std::function consumer_fn, 48 | std::size_t queue_size = 0U) 49 | : work(queue_size), 50 | consumer_fn(consumer_fn), 51 | t([&]() { this->worker(); }) {} 52 | 53 | worker_util(const worker_util& o) = delete; 54 | worker_util& operator=(const worker_util& o) = delete; 55 | worker_util(worker_util&& o) = delete; 56 | worker_util& operator=(worker_util&& o) = delete; 57 | 58 | ~worker_util() { work.push(work_item{true, {}, nullptr}); } 59 | 60 | void push(T item, std::function cfn = nullptr) { 61 | work.push(work_item{false, item, cfn}); 62 | } 63 | 64 | void worker() { 65 | std::vector V; 66 | try { 67 | while (V = work.pop(1, cfg::MAX_TIMEOUT), !V.empty()) { 68 | work_item w = V.back(); 69 | if (w.destroy) { 70 | break; 71 | } 72 | try { 73 | (w.consume_fn == nullptr ? consumer_fn : w.consume_fn)(w.item); 74 | } catch (const std::exception& e) { 75 | eprintf("consumer: {}", e.what()); 76 | } 77 | } 78 | } catch (const std::exception& x) { 79 | eprintf("worker died"); 80 | throw; 81 | } 82 | } 83 | }; 84 | 85 | } // namespace utility 86 | -------------------------------------------------------------------------------- /src/client_connection.cpp: -------------------------------------------------------------------------------- 1 | #include "client_connection.hpp" 2 | 3 | #include 4 | 5 | using namespace ra2yrcpp::connection; 6 | 7 | ClientConnection::ClientConnection(std::string host, std::string port) 8 | : host(host), port(port), state_(State::NONE) {} 9 | 10 | void ClientConnection::send_data(vecu8&& bytes) { send_data(bytes); } 11 | 12 | void ClientConnection::stop() {} 13 | 14 | util::AtomicVariable& ClientConnection::state() { 15 | return state_; 16 | } 17 | -------------------------------------------------------------------------------- /src/client_connection.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "types.h" 4 | #include "utility/sync.hpp" 5 | 6 | #include 7 | #include 8 | 9 | namespace ra2yrcpp::connection { 10 | 11 | enum State { NONE = 0, CONNECTING, OPEN, CLOSING, CLOSED }; 12 | 13 | class ClientConnection { 14 | public: 15 | ClientConnection(std::string host, std::string port); 16 | virtual ~ClientConnection() = default; 17 | virtual void connect() = 0; 18 | 19 | /// 20 | /// Send data with the underlying transport. 21 | /// 22 | /// @exception std::runtime_error on write failure 23 | virtual void send_data(const vecu8& bytes) = 0; 24 | void send_data(vecu8&& bytes); 25 | 26 | /// 27 | /// Read a length-prefixed data message from connection. A previous call to 28 | /// send_data must've occurred, or no data will be available to read. 29 | /// 30 | /// @exception std::runtime_error on read failure 31 | virtual vecu8 read_data() = 0; 32 | virtual void stop(); 33 | util::AtomicVariable& state(); 34 | 35 | protected: 36 | std::string host; 37 | std::string port; 38 | std::mutex state_mut_; 39 | util::AtomicVariable state_; 40 | }; 41 | 42 | } // namespace ra2yrcpp::connection 43 | -------------------------------------------------------------------------------- /src/client_utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ra2yrproto/core.pb.h" 4 | 5 | #include "instrumentation_client.hpp" 6 | #include "logging.hpp" 7 | #include "protocol/helpers.hpp" 8 | #include "types.h" 9 | #include "utility/time.hpp" 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | namespace ra2yrcpp::client_utils { 17 | 18 | namespace gpb = google::protobuf; 19 | 20 | namespace { 21 | using namespace std::chrono_literals; 22 | }; 23 | 24 | inline ra2yrproto::PollResults poll_until( 25 | instrumentation_client::InstrumentationClient* client, 26 | const duration_t timeout = 5.0s) { 27 | ra2yrproto::PollResults P; 28 | ra2yrproto::Response response; 29 | 30 | constexpr int max_tries = 4; 31 | constexpr int retry_ms = 1000; 32 | 33 | for (int i = 1; i <= max_tries; i++) { 34 | try { 35 | P = client->poll_blocking(timeout, 0u); 36 | break; 37 | } catch (const std::exception& e) { 38 | eprintf("poll failed: \"{}\", retry after {} ms, try {}/{}", e.what(), 39 | retry_ms, i, max_tries); 40 | util::sleep_ms(retry_ms); 41 | } 42 | } 43 | 44 | dprintf("size={}", P.result().results().size()); 45 | return P; 46 | } 47 | 48 | inline ra2yrproto::CommandResult run_one( 49 | const gpb::Message& M, 50 | instrumentation_client::InstrumentationClient* client, 51 | const duration_t poll_timeout = 5.0s) { 52 | auto r_ack = client->send_command(M, ra2yrproto::CLIENT_COMMAND); 53 | if (r_ack.code() == ra2yrproto::ResponseCode::ERROR) { 54 | throw std::runtime_error("ACK " + ra2yrcpp::protocol::to_json(r_ack)); 55 | } 56 | try { 57 | auto res = poll_until(client, poll_timeout); 58 | if (res.result().results_size() == 0) { 59 | return ra2yrproto::CommandResult(); 60 | } 61 | return res.result().results()[0]; 62 | } catch (const std::exception& e) { 63 | eprintf("broken connection {}", e.what()); 64 | return ra2yrproto::CommandResult(); 65 | } 66 | } 67 | 68 | template 69 | inline auto run(const T& cmd, 70 | instrumentation_client::InstrumentationClient* client) { 71 | try { 72 | auto r = run_one(cmd, client); 73 | return ra2yrcpp::protocol::from_any(r.result()); 74 | } catch (const std::exception& e) { 75 | dprintf("failed to run: {}", e.what()); 76 | throw; 77 | } 78 | } 79 | 80 | struct CommandSender { 81 | using fn_t = std::function; 82 | 83 | explicit CommandSender(fn_t fn) : fn(fn) {} 84 | 85 | template 86 | auto run(const T& cmd) { 87 | auto r = fn(cmd); 88 | return ra2yrcpp::protocol::from_any(r.result()); 89 | } 90 | 91 | fn_t fn; 92 | }; 93 | 94 | } // namespace ra2yrcpp::client_utils 95 | -------------------------------------------------------------------------------- /src/command/command_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "command/command_manager.hpp" 2 | -------------------------------------------------------------------------------- /src/command/is_command.cpp: -------------------------------------------------------------------------------- 1 | #include "command/is_command.hpp" 2 | -------------------------------------------------------------------------------- /src/command/is_command.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "command/command_manager.hpp" 3 | #include "logging.hpp" 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | namespace ra2yrcpp { 12 | class InstrumentationService; 13 | } 14 | 15 | namespace ra2yrcpp { 16 | 17 | namespace gpb = google::protobuf; 18 | 19 | namespace command { 20 | struct ISArg { 21 | void* instrumentation_service; 22 | gpb::Any M; 23 | }; 24 | 25 | using iservice_cmd = Command; 26 | 27 | /// 28 | /// Wrapper which takes a command function compatible with a supplied 29 | /// protobuf message type, and provides access to InstrumentationService. 30 | /// 31 | /// NOTE: in async commands, don't access the internal data of the object after 32 | /// exiting the command (e.g. executing a gameloop callback), because the object 33 | /// is destroyed. To access the original args, make a copy and pass by value. 34 | /// See mission_clicked() in commands_yr.cpp for example. 35 | /// 36 | /// With async commands, the command data is automatically cleared after 37 | /// command function execution, as it doesn't contain anything meaningful 38 | /// regarding the eventual results. 39 | /// 40 | /// TODO(shmocz): make a mechanism to wrap async commands so that pending flag 41 | /// is cleared automatically after execution or after exception. 42 | /// 43 | template 44 | struct ISCommand { 45 | explicit ISCommand(iservice_cmd* c) : c(c) { 46 | c->command_data()->M.UnpackTo(&command_data_); 47 | } 48 | 49 | ISCommand(const ISCommand&) = delete; 50 | ISCommand& operator=(const ISCommand&) = delete; 51 | 52 | ~ISCommand() { save_command_result(); } 53 | 54 | auto& command_data() { return command_data_; } 55 | 56 | void save_command_result() { 57 | // replace result, but only if pending is not set 58 | if (!c->pending().get()) { 59 | auto& p = c->command_data()->M; 60 | p.PackFrom(command_data_); 61 | } else { 62 | command_data_.Clear(); 63 | } 64 | } 65 | 66 | /// 67 | /// Mark this command as async. Allocate the command result and set the 68 | /// pending flag. 69 | /// 70 | void async() { 71 | save_command_result(); 72 | c->pending().store(true); 73 | } 74 | 75 | auto* I() { 76 | return reinterpret_cast( 77 | c->command_data()->instrumentation_service); 78 | } 79 | 80 | auto* M() { return &c->command_data()->M; } 81 | 82 | iservice_cmd* c; 83 | T command_data_; 84 | }; 85 | 86 | template 87 | std::pair get_cmd( 88 | std::function*)> fn, bool async = false) { 89 | return {MessageT().GetTypeName(), [=](iservice_cmd* c) { 90 | ISCommand Q(c); 91 | if (async) { 92 | Q.async(); 93 | } 94 | try { 95 | dprintf("exec {} ", MessageT().GetTypeName()); 96 | fn(&Q); 97 | } catch (...) { 98 | Q.M()->Clear(); 99 | throw; 100 | } 101 | }}; 102 | } 103 | 104 | template 105 | std::pair get_async_cmd( 106 | std::function*)> fn) { 107 | return get_cmd(fn, true); 108 | } 109 | 110 | /// 111 | /// Unpacks the message stored in cmd->result() to appropriate message type, and 112 | /// returns pointer to the result and instance of the message. 113 | /// 114 | template 115 | auto message_result(iservice_cmd* cmd) { 116 | MessageT m; 117 | cmd->command_data()->M.UnpackTo(&m); 118 | return m; 119 | } 120 | 121 | } // namespace command 122 | } // namespace ra2yrcpp 123 | -------------------------------------------------------------------------------- /src/commands_builtin.cpp: -------------------------------------------------------------------------------- 1 | #include "commands_builtin.hpp" 2 | 3 | #include "ra2yrproto/commands_builtin.pb.h" 4 | 5 | #include "asio_utils.hpp" 6 | #include "command/is_command.hpp" 7 | #include "hook.hpp" 8 | #include "instrumentation_service.hpp" 9 | #include "process.hpp" 10 | #include "types.h" 11 | #include "util_string.hpp" 12 | 13 | #include 14 | 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | using ra2yrcpp::command::get_cmd; 23 | 24 | std::map 25 | ra2yrcpp::commands_builtin::get_commands() { 26 | return { 27 | get_cmd([](auto* Q) { 28 | // NB: ensure correct radix 29 | auto& a = Q->command_data(); 30 | auto [lk, s] = Q->I()->aq_storage(); 31 | Q->I()->template store_value(a.key(), a.value().begin(), 32 | a.value().end()); 33 | }), 34 | get_cmd([](auto* Q) { 35 | auto* state = Q->command_data().mutable_state(); 36 | auto* srv = Q->I()->ws_server_.get(); 37 | srv->service_->post([state, srv]() { 38 | for (const auto& [socket_id, c] : srv->ws_conns) { 39 | auto* conn = state->add_connections(); 40 | conn->set_socket_id(socket_id); 41 | duration_t dur = c.timestamp.time_since_epoch(); 42 | conn->set_timestamp(dur.count()); 43 | } 44 | }); 45 | auto [l, rq] = Q->I()->cmd_manager().aq_results_queue(); 46 | for (const auto& [k, v] : *rq) { 47 | state->add_queues()->set_queue_id(k); 48 | } 49 | state->set_directory(process::getcwd()); 50 | }), 51 | get_cmd([](auto* Q) { 52 | // NB: ensure correct radix 53 | // FIXME: proper locking 54 | auto [lk, s] = Q->I()->aq_storage(); 55 | auto& c = Q->command_data(); 56 | c.set_value(ra2yrcpp::to_string( 57 | *reinterpret_cast(Q->I()->get_value(c.key(), false)))); 58 | }), 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/commands_builtin.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command/is_command.hpp" 4 | 5 | #include 6 | #include 7 | 8 | namespace ra2yrcpp { 9 | namespace commands_builtin { 10 | 11 | std::map 12 | get_commands(); 13 | 14 | } // namespace commands_builtin 15 | } // namespace ra2yrcpp 16 | -------------------------------------------------------------------------------- /src/commands_game.cpp: -------------------------------------------------------------------------------- 1 | #include "commands_game.hpp" 2 | 3 | #include "ra2yrproto/commands_game.pb.h" 4 | #include "ra2yrproto/ra2yr.pb.h" 5 | 6 | #include "command/is_command.hpp" 7 | #include "hooks_yr.hpp" 8 | #include "logging.hpp" 9 | #include "ra2/abi.hpp" 10 | #include "ra2/common.hpp" 11 | #include "ra2/state_context.hpp" 12 | #include "ra2/yrpp_export.hpp" 13 | #include "types.h" 14 | 15 | #include 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | using namespace ra2yrcpp::commands_game; 25 | 26 | using ra2yrcpp::command::get_async_cmd; 27 | using ra2yrcpp::command::get_cmd; 28 | using ra2yrcpp::hooks_yr::get_gameloop_command; 29 | 30 | namespace r2p = ra2yrproto::ra2yr; 31 | 32 | struct UnitOrderCtx { 33 | UnitOrderCtx(ra2::StateContext* ctx, const ra2yrproto::commands::UnitOrder* o) 34 | : ctx_(ctx), o_(o) {} 35 | 36 | const ra2yrproto::commands::UnitOrder& uo() { return *o_; } 37 | 38 | std::uintptr_t p_obj() const { 39 | return src_object_ == nullptr ? 0U : src_object_->pointer_self(); 40 | } 41 | 42 | void click_event(ra2yrproto::ra2yr::NetworkEvent e) { 43 | ctx_->abi_->ClickEvent(p_obj(), e); 44 | } 45 | 46 | bool requires_source_object() { 47 | return !(uo().action() == r2p::UnitAction::UNIT_ACTION_SELL_CELL); 48 | } 49 | 50 | bool requires_target_object() { 51 | switch (uo().action()) { 52 | case r2p::UnitAction::UNIT_ACTION_STOP: 53 | return false; 54 | default: 55 | return true; 56 | } 57 | return true; 58 | } 59 | 60 | bool requires_target_cell() { 61 | switch (uo().action()) { 62 | case r2p::UnitAction::UNIT_ACTION_STOP: 63 | return false; 64 | case r2p::UnitAction::UNIT_ACTION_CAPTURE: 65 | return false; 66 | case r2p::UnitAction::UNIT_ACTION_REPAIR: 67 | return false; 68 | default: 69 | return true; 70 | } 71 | return true; 72 | } 73 | 74 | // Return true if source object's current mission is invalid for execution of 75 | // the UnitAction. 76 | bool is_illegal_mission(ra2yrproto::ra2yr::Mission m) { 77 | return (m == r2p::Mission::Mission_None || 78 | m == r2p::Mission::Mission_Construction); 79 | } 80 | 81 | bool click_mission(ra2yrproto::ra2yr::Mission m) { 82 | CellClass* cell = nullptr; 83 | std::uintptr_t p_target = 0U; 84 | if (requires_target_object()) { 85 | p_target = uo().target_object(); 86 | } 87 | // TODO(shmocz): figure out events where the cell should be null 88 | if (requires_target_cell()) { 89 | auto c = uo().coordinates(); 90 | if (p_target != 0U && !uo().has_coordinates()) { 91 | c = ctx_->get_object_entry(p_target).o->coordinates(); 92 | } 93 | if ((cell = ra2::get_map_cell(c)) == nullptr) { 94 | throw std::runtime_error("invalid cell"); 95 | } 96 | } 97 | return ra2::abi::ClickMission::call( 98 | ctx_->abi_, p_obj(), static_cast(m), p_target, cell, nullptr); 99 | } 100 | 101 | // Apply action to single object 102 | void unit_action() { 103 | using r2p::UnitAction; 104 | if (requires_source_object() && 105 | is_illegal_mission(src_object_->current_mission())) { 106 | throw std::runtime_error( 107 | fmt::format("Object has illegal mission: {}", 108 | static_cast(src_object_->current_mission()))); 109 | } 110 | switch (uo().action()) { 111 | case UnitAction::UNIT_ACTION_DEPLOY: 112 | click_event(r2p::NETWORK_EVENT_Deploy); 113 | break; 114 | case UnitAction::UNIT_ACTION_SELL: 115 | click_event(r2p::NETWORK_EVENT_Sell); 116 | break; 117 | case UnitAction::UNIT_ACTION_SELL_CELL: { 118 | ra2yrproto::ra2yr::Event E; 119 | E.set_event_type(ra2yrproto::ra2yr::NETWORK_EVENT_SellCell); 120 | E.mutable_sell_cell()->mutable_cell()->CopyFrom(uo().coordinates()); 121 | (void)ctx_->add_event(E); 122 | } break; 123 | case UnitAction::UNIT_ACTION_SELECT: 124 | (void)ctx_->abi_->SelectObject(p_obj()); 125 | break; 126 | case UnitAction::UNIT_ACTION_MOVE: 127 | (void)click_mission(r2p::Mission_Move); 128 | break; 129 | case UnitAction::UNIT_ACTION_CAPTURE: 130 | (void)click_mission(r2p::Mission_Capture); 131 | break; 132 | case UnitAction::UNIT_ACTION_ATTACK: 133 | (void)click_mission(r2p::Mission_Attack); 134 | break; 135 | case UnitAction::UNIT_ACTION_ATTACK_MOVE: 136 | (void)click_mission(r2p::Mission_AttackMove); 137 | break; 138 | case UnitAction::UNIT_ACTION_REPAIR: 139 | (void)click_mission(r2p::Mission_Capture); 140 | break; 141 | case UnitAction::UNIT_ACTION_STOP: 142 | (void)click_mission(r2p::Mission_Stop); 143 | break; 144 | 145 | default: 146 | throw std::runtime_error("invalid unit action"); 147 | break; 148 | } 149 | } 150 | 151 | void perform() { 152 | if (uo().action() == r2p::UnitAction::UNIT_ACTION_SELL_CELL) { 153 | unit_action(); 154 | } else { 155 | for (const auto k : uo().object_addresses()) { 156 | src_object_ = ctx_->get_object_entry([&](const auto& v) { 157 | return v.o->pointer_self() == k && !v.o->in_limbo(); 158 | }) 159 | .o; 160 | unit_action(); 161 | } 162 | } 163 | } 164 | 165 | ra2::StateContext* ctx_; 166 | const ra2yrproto::commands::UnitOrder* o_; 167 | const ra2yrproto::ra2yr::Object* src_object_{nullptr}; 168 | }; 169 | 170 | // TODO(shmocz): copy args automagically in async cmds 171 | auto unit_order() { 172 | return get_cmd([](auto* Q) { 173 | auto args = Q->command_data(); 174 | 175 | get_gameloop_command(Q, [args](auto* cb) { 176 | auto* S = cb->get_state_context(); 177 | UnitOrderCtx ctx(S, &args); 178 | ctx.perform(); 179 | }); 180 | }); 181 | } 182 | 183 | auto produce_order() { 184 | return get_async_cmd([](auto* Q) { 185 | auto args = Q->command_data(); 186 | 187 | get_gameloop_command(Q, [args](auto* cb) { 188 | auto* ctx = cb->get_state_context(); 189 | const auto* tc = ctx->get_type_class(args.object_type().pointer_self()); 190 | auto can_build = ra2::abi::HouseClass_CanBuild::call( 191 | cb->abi(), 192 | reinterpret_cast(ctx->current_player()->self()), 193 | reinterpret_cast(tc->pointer_self()), false, false); 194 | if (can_build != CanBuildResult::Buildable) { 195 | throw std::runtime_error( 196 | fmt::format("unbuildable {}", args.ShortDebugString())); 197 | } 198 | dprintf("can build={}", static_cast(can_build)); 199 | 200 | ra2yrproto::ra2yr::Event E; 201 | E.set_event_type(ra2yrproto::ra2yr::NETWORK_EVENT_Produce); 202 | auto* P = E.mutable_production(); 203 | P->set_heap_id(args.object_type().array_index()); 204 | P->set_rtti_id(static_cast(args.object_type().type())); 205 | (void)ctx->add_event(E); 206 | }); 207 | }); 208 | } 209 | 210 | auto place_building() { 211 | return get_async_cmd([](auto* Q) { 212 | auto args = Q->command_data(); 213 | 214 | get_gameloop_command(Q, [args](auto* cb) { 215 | auto* ctx = cb->get_state_context(); 216 | auto& O = args.building(); 217 | ra2::ObjectEntry OE = ctx->get_object_entry(O); 218 | const auto* factory = ctx->find_factory([&](const auto& v) { 219 | return v.completed() && v.object() == OE.o->pointer_self() && 220 | v.owner() == ctx->current_player()->self(); 221 | }); 222 | if (factory == nullptr) { 223 | throw std::runtime_error( 224 | fmt::format("completed object {} not found from any factory", 225 | O.pointer_self())); 226 | } 227 | 228 | ctx->place_building(*ctx->current_player(), *OE.tc, args.coordinates()); 229 | }); 230 | }); 231 | } 232 | 233 | std::map 234 | ra2yrcpp::commands_game::get_commands() { 235 | return { 236 | unit_order(), // 237 | produce_order(), // 238 | place_building() // 239 | }; 240 | } 241 | -------------------------------------------------------------------------------- /src/commands_game.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "command/is_command.hpp" 3 | 4 | #include 5 | #include 6 | 7 | namespace ra2yrcpp { 8 | 9 | namespace commands_game { 10 | 11 | std::map 12 | get_commands(); 13 | 14 | } // namespace commands_game 15 | 16 | } // namespace ra2yrcpp 17 | -------------------------------------------------------------------------------- /src/commands_yr.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "command/is_command.hpp" 3 | 4 | #include 5 | #include 6 | 7 | namespace commands_yr { 8 | 9 | std::map 10 | get_commands(); 11 | 12 | } // namespace commands_yr 13 | -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | #include "config.hpp" 2 | 3 | #include "ra2yrproto/commands_yr.pb.h" 4 | 5 | #include "constants.hpp" 6 | #include "protocol/helpers.hpp" 7 | #include "util_string.hpp" 8 | 9 | #include 10 | #include 11 | 12 | using namespace ra2yrcpp::config; 13 | 14 | ConfigData ConfigData::parse(std::string json) { 15 | ra2yrproto::commands::Configuration C; 16 | if (!ra2yrcpp::protocol::from_json(ra2yrcpp::to_bytes(json), &C)) { 17 | throw std::runtime_error("Failed to parse configuration"); 18 | } 19 | 20 | ConfigData defaults{}; 21 | 22 | ConfigData CC{C.debug_log(), C.record_filename(), 23 | C.traffic_filename(), C.parse_map_data_interval(), 24 | C.single_step(), C.port(), 25 | C.max_connections(), C.allowed_hosts_regex(), 26 | C.log_filename()}; 27 | 28 | if (CC.max_connections == 0) { 29 | CC.max_connections = defaults.max_connections; 30 | } 31 | if (CC.port == 0) { 32 | CC.port = defaults.port; 33 | } 34 | if (CC.allowed_hosts_regex.empty()) { 35 | CC.allowed_hosts_regex = defaults.allowed_hosts_regex; 36 | } 37 | if (CC.parse_map_data_interval == 0) { 38 | CC.parse_map_data_interval = defaults.parse_map_data_interval; 39 | } 40 | 41 | return CC; 42 | } 43 | 44 | Config::Config(ConfigData c) : c_(c) {} 45 | 46 | Config::Config(std::string json) : Config(ConfigData::parse(json)) {} 47 | 48 | const ConfigData& Config::c() const { return c_; } 49 | 50 | void Config::set_debug_log(bool value) { c_.debug_log = value; } 51 | 52 | void Config::set_allowed_hosts_regex(std::string pattern) { 53 | c_.allowed_hosts_regex = pattern; 54 | } 55 | 56 | void Config::set_max_connections(unsigned value) { 57 | if (value == 0 || value > cfg::MAX_CLIENTS) { 58 | throw std::invalid_argument(fmt::format( 59 | "got {}, expecting 0 < max_connections < {}", value, cfg::MAX_CLIENTS)); 60 | } 61 | 62 | c_.max_connections = value; 63 | } 64 | 65 | void Config::set_parse_map_data_interval(unsigned value) { 66 | c_.parse_map_data_interval = value; 67 | } 68 | 69 | void Config::set_single_step(bool value) { c_.single_step = value; } 70 | 71 | std::string Config::to_json() { 72 | ra2yrproto::commands::Configuration C; 73 | #define X(k) C.set_##k(c_.k) 74 | X(debug_log); 75 | X(record_filename); 76 | X(traffic_filename); 77 | X(parse_map_data_interval); 78 | X(single_step); 79 | X(port); 80 | X(max_connections); 81 | X(allowed_hosts_regex); 82 | X(log_filename); 83 | #undef X 84 | return protocol::to_json(C); 85 | } 86 | -------------------------------------------------------------------------------- /src/config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "constants.hpp" 3 | 4 | #include 5 | 6 | namespace ra2yrcpp { 7 | namespace config { 8 | 9 | struct ConfigData { 10 | bool debug_log{false}; 11 | std::string record_filename; 12 | std::string traffic_filename; 13 | unsigned parse_map_data_interval{1U}; 14 | bool single_step{false}; 15 | unsigned port{cfg::SERVER_PORT}; 16 | unsigned max_connections{cfg::MAX_CLIENTS}; 17 | std::string allowed_hosts_regex{cfg::ALLOWED_HOSTS_REGEX}; 18 | std::string log_filename; 19 | ConfigData() = delete; 20 | 21 | /// Parse configuration from JSON string. 22 | /// 23 | /// @param json 24 | /// @exception std::runtime_error if parsing fails 25 | static ConfigData parse(std::string json); 26 | }; 27 | 28 | class Config { 29 | public: 30 | explicit Config(ConfigData c); 31 | explicit Config(std::string json); 32 | [[nodiscard]] const ConfigData& c() const; 33 | void set_debug_log(bool value); 34 | void set_allowed_hosts_regex(std::string pattern); 35 | void set_max_connections(unsigned value); 36 | void set_parse_map_data_interval(unsigned value); 37 | void set_single_step(bool value); 38 | Config() = delete; 39 | Config(const Config&) = delete; 40 | Config& operator=(const Config&) = delete; 41 | std::string to_json(); 42 | 43 | private: 44 | ConfigData c_; 45 | }; 46 | 47 | } // namespace config 48 | } // namespace ra2yrcpp 49 | -------------------------------------------------------------------------------- /src/constants.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "types.h" 4 | 5 | #include 6 | 7 | namespace cfg { 8 | namespace { 9 | using namespace std::chrono_literals; 10 | } 11 | 12 | constexpr unsigned int MAX_CLIENTS = 16; 13 | constexpr unsigned int SERVER_PORT = 14521; 14 | constexpr unsigned int MAX_MESSAGE_LENGTH = 1e7; 15 | // How long a connection thread in InstrumentationService waits for item to 16 | // appear in target queue 17 | constexpr duration_t POLL_BLOCKING_TIMEOUT = 2.5s; 18 | constexpr char SERVER_ADDRESS[] = "127.0.0.1"; 19 | constexpr char DLL_NAME[] = "libra2yrcpp.dll"; 20 | constexpr char INIT_NAME[] = "init_iservice"; 21 | constexpr unsigned int EVENT_BUFFER_SIZE = 600; 22 | constexpr unsigned int RESULT_QUEUE_SIZE = 32U; 23 | constexpr duration_t COMMAND_RESULTS_ACQUIRE_TIMEOUT = 5.0s; 24 | // General purpose "maximum" timeout value to avoid overflow in wait_for() etc. 25 | constexpr duration_t MAX_TIMEOUT = (60 * 60 * 24) * 1.0s; 26 | constexpr duration_t WEBSOCKET_READ_TIMEOUT = 5.0s; 27 | // Timeout for client when polling results from a queue. 28 | constexpr duration_t POLL_RESULTS_TIMEOUT = 1.0s; 29 | // Timeout for client to get ACK from service. 30 | constexpr duration_t COMMAND_ACK_TIMEOUT = 0.25s; 31 | constexpr char ALLOWED_HOSTS_REGEX[] = "0.0.0.0|127.0.0.1"; 32 | constexpr unsigned int PLACE_QUERY_MAX_LENGTH = 1024U; 33 | constexpr i32 PRODUCTION_STEPS = 54; 34 | constexpr char CONFIG_FILE_NAME[] = "ra2yrcpp.json"; 35 | constexpr char LOG_FILE_NAME[] = "ra2yrcpp.log"; 36 | }; // namespace cfg 37 | 38 | #if defined(_M_X64) || defined(__amd64__) 39 | #define RA2YRCPP_64 40 | #else 41 | #define RA2YRCPP_32 42 | #endif 43 | -------------------------------------------------------------------------------- /src/dll_inject.cpp: -------------------------------------------------------------------------------- 1 | #include "dll_inject.hpp" 2 | 3 | #include "process.hpp" 4 | #include "types.h" 5 | #include "utility/time.hpp" 6 | 7 | #include 8 | 9 | using namespace dll_inject; 10 | 11 | void dll_inject::inject_code(process::Process* P, int thread_id, 12 | vecu8 shellcode) { 13 | process::Thread T(thread_id); 14 | // Save EIP 15 | auto eip = *T.get_pgpr(x86Reg::eip); 16 | auto esp = *T.get_pgpr(x86Reg::esp); 17 | esp -= sizeof(esp); 18 | P->write_memory(reinterpret_cast(esp), &eip, sizeof(eip)); 19 | // Allocate memory for shellcode 20 | auto sc_addr = P->allocate_code(shellcode.size()); 21 | // Write shellcode 22 | P->write_memory(sc_addr, shellcode.data(), shellcode.size()); 23 | // Set ESP 24 | T.set_gpr(x86Reg::esp, esp); 25 | // Set EIP to shellcode 26 | T.set_gpr(x86Reg::eip, reinterpret_cast(sc_addr)); 27 | } 28 | 29 | void dll_inject::suspend_inject_resume(handle_t ex_handle, vecu8 shellcode, 30 | const DLLInjectOptions o) { 31 | process::Process P(ex_handle); 32 | P.suspend_threads(-1); 33 | int tid = -1; 34 | P.for_each_thread([&tid](process::Thread* T, void*) { 35 | if (tid == -1) { 36 | tid = T->id(); 37 | } 38 | }); 39 | if (tid == -1) { 40 | throw std::runtime_error("tid -1"); 41 | } 42 | util::sleep_ms(o.delay_post_suspend); 43 | inject_code(&P, tid, shellcode); 44 | util::sleep_ms(o.delay_post_inject); 45 | // Resume only the injected thread 46 | P.for_each_thread([&tid](auto* T, auto* ctx) { 47 | (void)ctx; 48 | if (T->id() == tid) { 49 | T->resume(); 50 | } 51 | }); 52 | util::sleep_ms(o.delay_pre_resume); 53 | // Wait a bit, then resume others 54 | P.resume_threads(tid); 55 | } 56 | -------------------------------------------------------------------------------- /src/dll_inject.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "types.h" 4 | 5 | #include 6 | #include 7 | 8 | namespace process { 9 | class Process; 10 | } 11 | 12 | namespace dll_inject { 13 | 14 | namespace { 15 | using namespace std::chrono_literals; 16 | } 17 | 18 | typedef void* handle_t; 19 | 20 | struct DLLInjectOptions { 21 | duration_t delay_pre_suspend; 22 | duration_t delay_post_suspend; 23 | duration_t delay_pre_resume; 24 | duration_t wait_process; 25 | duration_t delay_post_inject; 26 | std::string process_name; 27 | bool force; 28 | 29 | DLLInjectOptions() 30 | : delay_pre_suspend(1.0s), 31 | delay_post_suspend(1.0s), 32 | delay_pre_resume(1.0s), 33 | wait_process(0.0s), 34 | delay_post_inject(1.0s), 35 | process_name(""), 36 | force(false) {} 37 | }; 38 | 39 | namespace { 40 | 41 | using namespace std::chrono_literals; 42 | }; 43 | 44 | void inject_code(process::Process* P, int thread_id, vecu8 shellcode); 45 | /// Inject DLL from file path_dll to external process indicated by ex_handle, 46 | /// using payload shellcode. TODO: we probably don't need handle, just the 47 | /// process id. 48 | /// @exception std::runtime_error on failure 49 | void suspend_inject_resume(handle_t ex_handle, vecu8 shellcode, 50 | const DLLInjectOptions o); 51 | }; // namespace dll_inject 52 | -------------------------------------------------------------------------------- /src/errors.cpp: -------------------------------------------------------------------------------- 1 | #include "errors.hpp" 2 | 3 | #include 4 | 5 | #ifdef _WIN32 6 | #include "win32/win_message.hpp" 7 | #elif __linux__ 8 | #include 9 | 10 | #include 11 | #endif 12 | 13 | using namespace ra2yrcpp; 14 | 15 | int ra2yrcpp::get_last_error() { 16 | #ifdef _WIN32 17 | return static_cast(windows_utils::get_last_error()); 18 | #elif __linux__ 19 | return errno; 20 | #else 21 | #error Not implemented 22 | #endif 23 | } 24 | 25 | ra2yrcpp_exception_base::ra2yrcpp_exception_base(std::string prefix, 26 | std::string message) 27 | : prefix_(prefix), message_(message) {} 28 | 29 | const char* ra2yrcpp_exception_base::what() const throw() { 30 | return message_.c_str(); 31 | } 32 | 33 | std::string ra2yrcpp::get_error_message(int error_code) { 34 | if (error_code == 0) { 35 | return std::string(); 36 | } 37 | #ifdef _WIN32 38 | return windows_utils::get_error_message(error_code); 39 | #elif __linux__ 40 | return strerror(error_code); 41 | #else 42 | #error Not Implemented 43 | #endif 44 | } 45 | 46 | system_error::system_error(std::string message, int error_code) { 47 | #if defined(_WIN32) || defined(__linux__) 48 | auto msg = get_error_message(error_code); 49 | message_ = message + " " + msg; 50 | #else 51 | #error Not implemented 52 | #endif 53 | } 54 | 55 | system_error::system_error(std::string message) 56 | : system_error(message, get_last_error()) {} 57 | 58 | const char* system_error::what() const throw() { return message_.c_str(); } 59 | -------------------------------------------------------------------------------- /src/errors.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace ra2yrcpp { 6 | 7 | int get_last_error(); 8 | std::string get_error_message(int error_code); 9 | 10 | class ra2yrcpp_exception_base : public std::exception { 11 | public: 12 | explicit ra2yrcpp_exception_base(std::string prefix, std::string message); 13 | virtual const char* what() const throw(); 14 | 15 | protected: 16 | std::string prefix_; 17 | std::string message_; 18 | }; 19 | 20 | class system_error : public std::exception { 21 | public: 22 | system_error(std::string message, int error_code); 23 | explicit system_error(std::string message); 24 | virtual const char* what() const throw(); 25 | 26 | private: 27 | std::string message_; 28 | }; 29 | 30 | } // namespace ra2yrcpp 31 | -------------------------------------------------------------------------------- /src/hook.cpp: -------------------------------------------------------------------------------- 1 | #include "hook.hpp" 2 | 3 | #include "logging.hpp" 4 | #include "process.hpp" 5 | #include "x86.hpp" 6 | 7 | #include 8 | 9 | using namespace hook; 10 | 11 | static void patch_code(u8* target_address, const u8* code, 12 | std::size_t code_length) { 13 | dprintf("address={}, bytes={}", reinterpret_cast(target_address), 14 | code_length); 15 | auto P = process::get_current_process(); 16 | P.write_memory(target_address, code, code_length); 17 | } 18 | 19 | // This differs from Syringe, which uses relative call 20 | SyringeHook::SyringeHook(addr_t target, addr_t hook_function, 21 | std::size_t code_length) { 22 | nop(code_length, false); // placeholder for original instruction(s) 23 | x86::save_regs(this); 24 | push(esp); 25 | mov(eax, hook_function); 26 | call(eax); 27 | add(esp, 0x4); 28 | x86::restore_regs(this); 29 | push(target + code_length); 30 | ret(); 31 | } 32 | 33 | // TODO: fail if code is too short 34 | struct JumpTo : Xbyak::CodeGenerator { 35 | JumpTo(const u8* target, std::size_t code_length) { 36 | push(reinterpret_cast(target)); 37 | ret(); 38 | const std::size_t pad_length = code_length - getSize(); 39 | if (pad_length > 0) { 40 | nop(pad_length, false); 41 | } 42 | dprintf("Trampoline size={}", getSize()); 43 | } 44 | }; 45 | 46 | Hook::Hook(HookEntry h, hook_fn fn) 47 | : trampoline_(h.address, reinterpret_cast(fn), h.size) { 48 | // Create the main hook prologue 49 | // TODO: use correct signature 50 | auto* p = trampoline_.getCode(); 51 | 52 | // Copy original instructions to prologue 53 | patch_code(p, reinterpret_cast(h.address), h.size); 54 | 55 | // Patch target code with jump to hook prologue 56 | JumpTo D(p, h.size); 57 | patch_code(reinterpret_cast(h.address), D.getCode(), D.getSize()); 58 | } 59 | -------------------------------------------------------------------------------- /src/hook.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "types.h" 4 | 5 | #include 6 | 7 | #include 8 | #undef ERROR 9 | #undef OK 10 | 11 | namespace hook { 12 | 13 | #pragma pack(push, 16) 14 | 15 | struct alignas(16) HookEntry { 16 | u32 address; 17 | u32 size; 18 | const char* name; 19 | }; 20 | 21 | #pragma pack(pop) 22 | 23 | using hook_fn = u32 __cdecl (*)(X86Regs*); 24 | 25 | /// In Syringe hook format, the overwritten instruction is executed after hook. 26 | /// NB: We don't need to obey syringe prologue rigorously. As long export the 27 | /// hook functions in Syringe compatible format, and ensure that REGISTER 28 | /// argument is passed correctly, we should be fine. Only when ra2yrcpp is 29 | /// executed without Syringe (e.g.) with custom loader code, the internal 30 | /// prologue format should be used. 31 | struct SyringeHook : Xbyak::CodeGenerator { 32 | SyringeHook(addr_t target, addr_t hook_function, std::size_t code_length); 33 | }; 34 | 35 | class Hook { 36 | public: 37 | Hook(HookEntry h, hook_fn fn); 38 | 39 | private: 40 | SyringeHook trampoline_; 41 | }; 42 | 43 | } // namespace hook 44 | 45 | #ifdef _MSC_VER 46 | #define HOOK_SECTION_ENTRY(hook, funcname, size) \ 47 | __declspec(allocate(".syhks00")) HookEntry _hk_##hook##funcname = { \ 48 | hook, size, #funcname} 49 | #else 50 | #define HOOK_SECTION_ENTRY(hook, funcname, size) \ 51 | HookEntry __attribute__((section(".syhks00"))) _hk_##hook##funcname = { \ 52 | hook, size, #funcname} 53 | #endif 54 | 55 | // NB. Syringe headers specify the HOOK_SECTION_ENTRY inside SyringeData::Hooks 56 | // namespace. 57 | #define DEFINE_HOOK(hook, funcname, size) \ 58 | HOOK_SECTION_ENTRY(hook, funcname, size); \ 59 | extern "C" __declspec(dllexport) DWORD __cdecl funcname(void* R) 60 | -------------------------------------------------------------------------------- /src/hooks_yr.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ra2yrproto/ra2yr.pb.h" 4 | 5 | #include "async_queue.hpp" 6 | #include "command/is_command.hpp" 7 | #include "config.hpp" 8 | #include "ra2/abi.hpp" 9 | #include "ra2/state_context.hpp" 10 | #include "types.h" 11 | #include "utility/sync.hpp" 12 | 13 | #include 14 | 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace util_command { 23 | template 24 | struct ISCommand; 25 | } 26 | 27 | namespace ra2yrcpp::hooks_yr { 28 | 29 | using gpb::RepeatedPtrField; 30 | 31 | // General purpose data container to hold resources that need to be freed at 32 | // game exit. 33 | class ServiceData { 34 | public: 35 | ServiceData() = default; 36 | virtual ~ServiceData() = default; 37 | }; 38 | 39 | enum class ServiceDataId : u32 { 40 | GAME_COMMAND = 0U, 41 | RECORD_TRAFFIC = 1U, 42 | STATE_SAVE = 2U 43 | }; 44 | 45 | // Should this be a singleton? 46 | struct GameDataYR { 47 | GameDataYR(); 48 | 49 | ra2::abi::ABIGameMD abi; 50 | ra2yrproto::ra2yr::StorageValue sv; 51 | std::unique_ptr ctx{nullptr}; 52 | util::AtomicVariable game_paused{false}; 53 | }; 54 | 55 | /// Singleton 56 | class MainData { 57 | public: 58 | void initialize_service_datas(); 59 | void deinitialize_service_datas(); 60 | GameDataYR* data(); 61 | static MainData& get(); 62 | static util::acquire_t acquire(); 63 | static void lock(); 64 | static void unlock(); 65 | std::map>& service_datas(); 66 | void update_config(const ra2yrcpp::config::Config& cfg); 67 | 68 | private: 69 | static MainData* instance_; 70 | // TODO: Explain rationale behind recursive mutex 71 | static std::recursive_mutex lock_; 72 | std::unique_ptr data_; 73 | std::map> service_datas_; 74 | MainData(); 75 | ~MainData(); 76 | }; 77 | 78 | class GameDataInterface : public ServiceData { 79 | public: 80 | using tc_t = RepeatedPtrField; 81 | ra2::abi::ABIGameMD* abi(); 82 | ra2yrproto::ra2yr::GameState* game_state(); 83 | ra2yrproto::ra2yr::PrerequisiteGroups* prerequisite_groups(); 84 | tc_t* type_classes(); 85 | ra2::StateContext* get_state_context(); 86 | GameDataYR* data(); 87 | 88 | private: 89 | // NB. cyclic dependency 90 | GameDataYR* data_{nullptr}; 91 | }; 92 | 93 | class GameCommandData : public GameDataInterface { 94 | public: 95 | using work_t = std::function; 96 | static constexpr auto id = ServiceDataId::GAME_COMMAND; 97 | 98 | GameCommandData(); 99 | void put_work(work_t fn); 100 | void consume_work(); 101 | 102 | // Get global instance 103 | static GameCommandData* get(); 104 | static std::unique_ptr create(); 105 | 106 | private: 107 | async_queue::AsyncQueue work; 108 | }; 109 | 110 | template 111 | void get_gameloop_command(const ra2yrcpp::command::ISCommand* Q, 112 | std::function fn) { 113 | auto* ctx = GameCommandData::get(); 114 | auto* cmd = Q->c; 115 | cmd->set_async_handler([ctx, fn](auto*) { fn(ctx); }); 116 | ctx->put_work([cmd]() { cmd->run_async_handler(); }); 117 | } 118 | 119 | }; // namespace ra2yrcpp::hooks_yr 120 | -------------------------------------------------------------------------------- /src/instrumentation_client.cpp: -------------------------------------------------------------------------------- 1 | #include "instrumentation_client.hpp" 2 | 3 | #include "protocol/protocol.hpp" 4 | 5 | #include "client_connection.hpp" 6 | #include "errors.hpp" 7 | #include "protocol/helpers.hpp" 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | using namespace instrumentation_client; 15 | 16 | InstrumentationClient::InstrumentationClient( 17 | std::shared_ptr conn) 18 | : conn_(conn) {} 19 | 20 | ra2yrproto::PollResults InstrumentationClient::poll_blocking(duration_t timeout, 21 | u64 queue_id) { 22 | ra2yrproto::PollResults C; 23 | if (queue_id < (u64)-1) { 24 | auto* args = C.mutable_args(); 25 | args->set_queue_id(queue_id); 26 | args->set_timeout((u64)timeout.count()); 27 | } 28 | 29 | auto resp = send_command(C, ra2yrproto::POLL_BLOCKING); 30 | 31 | if (resp.code() == ra2yrcpp::RESPONSE_ERROR) { 32 | auto msg = 33 | ra2yrcpp::protocol::from_any(resp.body()); 34 | throw ra2yrcpp::system_error(msg.message()); 35 | } 36 | return ra2yrcpp::protocol::from_any(resp.body()); 37 | } 38 | 39 | void InstrumentationClient::send_data(const vecu8& data) { 40 | (void)conn_->send_data(data); 41 | } 42 | 43 | ra2yrproto::Response InstrumentationClient::send_message(const vecu8& data) { 44 | send_data(data); 45 | 46 | auto resp = conn_->read_data(); 47 | if (resp.size() == 0U) { 48 | throw std::runtime_error("empty response, likely connection closed"); 49 | } 50 | 51 | ra2yrproto::Response R; 52 | if (!R.ParseFromArray(resp.data(), resp.size())) { 53 | throw std::runtime_error( 54 | fmt::format("failed to parse response, size={}", resp.size())); 55 | } 56 | return R; 57 | } 58 | 59 | ra2yrproto::Response InstrumentationClient::send_message( 60 | const gpb::Message& M) { 61 | auto data = ra2yrcpp::to_vecu8(M); 62 | return send_message(data); 63 | } 64 | 65 | ra2yrproto::Response InstrumentationClient::send_command( 66 | const gpb::Message& cmd, ra2yrproto::CommandType type) { 67 | auto C = ra2yrcpp::create_command(cmd, type); 68 | return send_message(C); 69 | } 70 | 71 | ra2yrcpp::connection::ClientConnection* InstrumentationClient::connection() { 72 | return conn_.get(); 73 | } 74 | 75 | void InstrumentationClient::connect() { conn_->connect(); } 76 | 77 | void InstrumentationClient::disconnect() { conn_->stop(); } 78 | -------------------------------------------------------------------------------- /src/instrumentation_client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ra2yrproto/core.pb.h" 3 | 4 | #include "types.h" 5 | 6 | #include 7 | 8 | namespace ra2yrcpp::connection { 9 | class ClientConnection; 10 | } 11 | 12 | namespace google { 13 | namespace protobuf { 14 | class Message; 15 | } 16 | } // namespace google 17 | 18 | namespace instrumentation_client { 19 | 20 | namespace gpb = google::protobuf; 21 | 22 | class InstrumentationClient { 23 | public: 24 | /// Initialize client from existing connection handle. 25 | explicit InstrumentationClient( 26 | std::shared_ptr conn); 27 | 28 | /// Send bytes. 29 | /// @exception std::runtime_error on write failure 30 | void send_data(const vecu8& data); 31 | 32 | /// 33 | /// Send encoded message to server and read response back. 34 | /// @exception std::runtime_error on read/write or serialization failure. 35 | ra2yrproto::Response send_message(const vecu8& data); 36 | /// Convert message to vecu8 and send it to server. 37 | ra2yrproto::Response send_message(const gpb::Message& M); 38 | 39 | /// 40 | /// Send a command of given type to server and read response. This can block 41 | /// if there's nothing to be read. 42 | /// 43 | /// @exception std::runtime_error on read/write failure. 44 | /// 45 | ra2yrproto::Response send_command(const gpb::Message& cmd, 46 | ra2yrproto::CommandType type); 47 | /// @exception std::system_error for internal server error 48 | ra2yrproto::PollResults poll_blocking(duration_t timeout, 49 | u64 queue_id = (u64)-1); 50 | ra2yrcpp::connection::ClientConnection* connection(); 51 | /// Establishes connection 52 | /// 53 | /// @exception std::runtime_error on connection failure 54 | void connect(); 55 | 56 | /// Disconnects the internal client. 57 | void disconnect(); 58 | 59 | private: 60 | std::shared_ptr conn_; 61 | }; 62 | 63 | } // namespace instrumentation_client 64 | -------------------------------------------------------------------------------- /src/instrumentation_service.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ra2yrproto/core.pb.h" 3 | 4 | #include "command/command_manager.hpp" 5 | #include "command/is_command.hpp" 6 | #include "constants.hpp" 7 | #include "hook.hpp" 8 | #include "process.hpp" 9 | #include "types.h" 10 | #include "utility/sync.hpp" 11 | #include "websocket_server.hpp" 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | namespace ra2yrcpp { 24 | namespace asio_utils { 25 | class IOService; 26 | } 27 | } // namespace ra2yrcpp 28 | 29 | namespace ra2yrcpp { 30 | 31 | // Forward declaration 32 | class InstrumentationService; 33 | 34 | // TODO(shmocz): Deprecate storage because it's largely unused. 35 | using storage_t = 36 | std::map>>; 37 | using ra2yrcpp::websocket_server::WebsocketServer; 38 | using cmd_t = ra2yrcpp::command::iservice_cmd; 39 | using cmd_manager_t = ra2yrcpp::command::CommandManager; 40 | using command_ptr_t = cmd_manager_t::command_ptr_t; 41 | using command_hdl_t = command_ptr_t::weak_type; 42 | 43 | class InstrumentationService { 44 | public: 45 | struct Options { 46 | WebsocketServer::Options server; 47 | }; 48 | 49 | /// @param opt options 50 | /// @param on_shutdown Callback invoked upon SHUTDOWN command. Used to e.g. 51 | /// signal the Context object to delete the main service. Currently not 52 | /// utilized in practice. 53 | /// @param extra_init Function to be invoked right after starting the command 54 | /// manager. 55 | InstrumentationService( 56 | Options opt, 57 | std::function on_shutdown, 58 | std::function extra_init = nullptr); 59 | ~InstrumentationService(); 60 | 61 | cmd_manager_t& cmd_manager(); 62 | // TODO(shmocz): separate storage class 63 | util::acquire_t aq_storage(); 64 | 65 | template 66 | void store_value(std::string key, Args&&... args) { 67 | storage_[key] = std::unique_ptr( 68 | new T(std::forward(args)...), 69 | [](auto* d) { delete reinterpret_cast(d); }); 70 | } 71 | 72 | /// Retrieve value from storage 73 | /// @param key target key 74 | /// @param acquire lock storage accessing it 75 | /// @return pointer to the storage object 76 | /// @exception std::out_of_range if value doesn't exist 77 | void* get_value(std::string key, bool acquire = true); 78 | const InstrumentationService::Options& opts() const; 79 | static ra2yrcpp::InstrumentationService* create( 80 | InstrumentationService::Options O, 81 | std::map commands, 82 | std::function 83 | on_shutdown = nullptr, 84 | std::function extra_init = nullptr); 85 | ra2yrproto::Response process_request(int socket_id, vecu8* bytes, 86 | bool* is_json); 87 | std::string on_shutdown(); 88 | 89 | private: 90 | ra2yrproto::PollResults flush_results( 91 | u64 queue_id, duration_t delay = cfg::POLL_RESULTS_TIMEOUT); 92 | 93 | Options opts_; 94 | std::function on_shutdown_; 95 | cmd_manager_t cmd_manager_; 96 | storage_t storage_; 97 | std::recursive_mutex mut_storage_; 98 | std::unique_ptr io_service_; 99 | util::AtomicVariable io_service_tid_; 100 | 101 | public: 102 | std::unique_ptr ws_server_; 103 | }; 104 | 105 | const InstrumentationService::Options default_options{ 106 | {cfg::SERVER_ADDRESS, cfg::SERVER_PORT, cfg::MAX_CLIENTS, 107 | cfg::ALLOWED_HOSTS_REGEX}}; 108 | 109 | } // namespace ra2yrcpp 110 | -------------------------------------------------------------------------------- /src/is_context.cpp: -------------------------------------------------------------------------------- 1 | #include "is_context.hpp" 2 | 3 | #include "command/is_command.hpp" 4 | #include "commands_builtin.hpp" 5 | #include "commands_game.hpp" 6 | #include "commands_yr.hpp" 7 | #include "constants.hpp" 8 | #include "dll_inject.hpp" 9 | #include "hook.hpp" 10 | #include "instrumentation_service.hpp" 11 | #include "logging.hpp" 12 | #include "process.hpp" 13 | #include "types.h" 14 | #include "utility/time.hpp" 15 | #include "win32/windows_utils.hpp" 16 | #include "x86.hpp" 17 | 18 | #include 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | using namespace std::chrono_literals; 29 | 30 | using namespace is_context; 31 | using x86::bytes_to_stack; 32 | 33 | ProcAddrs is_context::get_procaddrs() { 34 | ProcAddrs A; 35 | A.p_LoadLibrary = windows_utils::get_proc_address("LoadLibraryA"); 36 | A.p_GetProcAddress = windows_utils::get_proc_address("GetProcAddress"); 37 | return A; 38 | } 39 | 40 | vecu8 is_context::vecu8cstr(std::string s) { 41 | vecu8 r(s.begin(), s.end()); 42 | r.push_back('\0'); 43 | return r; 44 | } 45 | 46 | DLLLoader::DLLLoader(DLLLoader::Options o) { 47 | vecu8 v1(o.path_dll.begin(), o.path_dll.end()); 48 | v1.push_back(0x0); 49 | vecu8 v2(o.name_init.begin(), o.name_init.end()); 50 | v2.push_back(0x0); 51 | x86::save_regs(this); 52 | // Call LoadLibrary 53 | push(ebp); 54 | mov(ebp, esp); 55 | auto sz = bytes_to_stack(this, v1); 56 | lea(eax, ptr[ebp - sz]); 57 | push(eax); 58 | if (o.indirect) { 59 | mov(eax, ptr[o.PA.p_LoadLibrary]); 60 | } else { 61 | mov(eax, o.PA.p_LoadLibrary); 62 | } 63 | call(eax); // TODO(shmocz): handle errors 64 | // restore stack 65 | mov(esp, ebp); 66 | push(eax); // save init fn address 67 | sz = bytes_to_stack(this, v2); // lpProcName 68 | lea(eax, ptr[ebp - sz - 0x4]); 69 | push(eax); // &lpProcName 70 | mov(eax, ptr[ebp - 0x4]); 71 | push(eax); // hModule 72 | if (o.indirect) { 73 | mov(eax, ptr[o.PA.p_GetProcAddress]); 74 | } else { 75 | mov(eax, o.PA.p_GetProcAddress); 76 | } 77 | call(eax); // GetProcAddress(hModule, lpProcName) 78 | // Call init routine 79 | push(static_cast(o.no_init_hooks)); 80 | push(o.port); 81 | push(o.max_clients); 82 | call(eax); 83 | // Restore registers 84 | mov(esp, ebp); 85 | pop(ebp); 86 | x86::restore_regs(this); 87 | } 88 | 89 | void is_context::get_procaddr(Xbyak::CodeGenerator* c, void* m, 90 | std::string name, 91 | const std::uintptr_t p_GetProcAddress) { 92 | using namespace Xbyak::util; 93 | vecu8 n = vecu8cstr(name); 94 | c->push(ebp); 95 | c->mov(ebp, esp); 96 | auto sz = bytes_to_stack(c, n); 97 | c->lea(eax, ptr[ebp - sz]); 98 | c->push(eax); 99 | c->push(reinterpret_cast(m)); 100 | c->mov(eax, p_GetProcAddress); 101 | c->call(eax); 102 | // restore stack 103 | c->mov(esp, ebp); 104 | c->pop(ebp); 105 | c->ret(); 106 | } 107 | 108 | ra2yrcpp::InstrumentationService* is_context::make_is( 109 | ra2yrcpp::InstrumentationService::Options O, 110 | std::function on_shutdown) { 111 | auto cmds = ra2yrcpp::commands_builtin::get_commands(); 112 | for (auto& [name, fn] : commands_yr::get_commands()) { 113 | cmds[name] = fn; 114 | } 115 | for (auto& [name, fn] : ra2yrcpp::commands_game::get_commands()) { 116 | cmds[name] = fn; 117 | } 118 | auto* I = ra2yrcpp::InstrumentationService::create( 119 | O, std::map(), on_shutdown, 120 | [cmds](auto* t) { 121 | for (auto& [name, fn] : cmds) { 122 | t->cmd_manager().add_command(name, fn); 123 | } 124 | }); 125 | 126 | return I; 127 | } 128 | 129 | void is_context::inject_dll(unsigned pid, std::string path_dll, 130 | ra2yrcpp::InstrumentationService::Options o, 131 | dll_inject::DLLInjectOptions dll) { 132 | using namespace std::chrono_literals; 133 | if (pid == 0u) { 134 | util::call_until( 135 | dll.wait_process > 0.0s ? dll.wait_process : cfg::MAX_TIMEOUT, 0.5s, 136 | [&]() { return (pid = process::get_pid(dll.process_name)) == 0u; }); 137 | if (pid == 0u) { 138 | throw std::runtime_error( 139 | fmt::format("process {} not found", dll.process_name)); 140 | } 141 | } 142 | process::Process P(pid); 143 | const auto modules = P.list_loaded_modules(); 144 | const bool is_loaded = 145 | std::find_if(modules.begin(), modules.end(), [&](auto& a) { 146 | return a.find(path_dll) != std::string::npos; 147 | }) != modules.end(); 148 | if (is_loaded && !dll.force) { 149 | iprintf("DLL {} is already loaded. Not forcing another load.", path_dll); 150 | return; 151 | } 152 | const DLLLoader::Options o_dll{is_context::get_procaddrs(), 153 | path_dll, 154 | cfg::INIT_NAME, 155 | o.server.max_connections, 156 | o.server.port, 157 | false, 158 | false}; 159 | iprintf("indirect={} pid={},p_load={:#x},p_getproc={:#x},port={}\n", 160 | o_dll.indirect, pid, o_dll.PA.p_LoadLibrary, 161 | o_dll.PA.p_GetProcAddress, o_dll.port); 162 | is_context::DLLLoader L(o_dll); 163 | auto p = L.getCode(); 164 | vecu8 sc(p, p + L.getSize()); 165 | dll_inject::suspend_inject_resume(P.handle(), sc, dll); 166 | } 167 | -------------------------------------------------------------------------------- /src/is_context.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.hpp" 3 | #include "constants.hpp" 4 | #include "instrumentation_service.hpp" 5 | #include "types.h" 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace dll_inject { 18 | struct DLLInjectOptions; 19 | } 20 | 21 | namespace is_context { 22 | 23 | struct ProcAddrs { 24 | std::uintptr_t p_LoadLibrary; 25 | std::uintptr_t p_GetProcAddress; 26 | }; 27 | 28 | ProcAddrs get_procaddrs(); 29 | vecu8 vecu8cstr(std::string s); 30 | 31 | void get_procaddr(Xbyak::CodeGenerator* c, void* m, std::string name, 32 | const std::uintptr_t p_GetProcAddress); 33 | 34 | struct DLLLoader : Xbyak::CodeGenerator { 35 | struct Options { 36 | ProcAddrs PA; 37 | std::string path_dll; 38 | std::string name_init; 39 | unsigned int max_clients; 40 | unsigned int port; 41 | bool indirect; 42 | bool no_init_hooks; 43 | }; 44 | 45 | explicit DLLLoader(DLLLoader::Options o); 46 | }; 47 | 48 | /// Create InstrumentationService instance and add both builtin commands and YR 49 | /// specific commands. 50 | ra2yrcpp::InstrumentationService* make_is( 51 | ra2yrcpp::InstrumentationService::Options O, 52 | std::function on_shutdown = 53 | nullptr); 54 | 55 | /// 56 | /// Inject ra2yrcpp DLL to target process. 57 | /// 58 | void inject_dll(unsigned pid, std::string path_dll, 59 | ra2yrcpp::InstrumentationService::Options o, 60 | dll_inject::DLLInjectOptions dll); 61 | 62 | const DLLLoader::Options default_options{ 63 | {0U, 0U}, cfg::DLL_NAME, cfg::INIT_NAME, cfg::MAX_CLIENTS, 64 | cfg::SERVER_PORT, false, false}; 65 | 66 | }; // namespace is_context 67 | -------------------------------------------------------------------------------- /src/logging.cpp: -------------------------------------------------------------------------------- 1 | #include "logging.hpp" 2 | 3 | #include 4 | 5 | #include 6 | 7 | static FILE* g_handle{nullptr}; 8 | static std::mutex g_output_handle_mutex; 9 | static bool g_handle_open{false}; 10 | 11 | bool ra2yrcpp::logging::set_output_handle(const char* path) { 12 | std::lock_guard lock(g_output_handle_mutex); 13 | if (g_handle_open) { 14 | return false; 15 | } 16 | if (g_handle == nullptr) { 17 | if (path != nullptr && (g_handle = std::fopen(path, "w")) == nullptr) { 18 | return false; 19 | } 20 | g_handle_open = true; 21 | } 22 | return true; 23 | } 24 | 25 | FILE* ra2yrcpp::logging::get_output_handle() { 26 | std::lock_guard lock(g_output_handle_mutex); 27 | if (!g_handle_open) { 28 | return stderr; 29 | } 30 | return g_handle; 31 | } 32 | 33 | void ra2yrcpp::logging::close_output_handle() { 34 | std::lock_guard lock(g_output_handle_mutex); 35 | if (!g_handle_open) { 36 | return; 37 | } 38 | if (g_handle != nullptr) { 39 | (void)std::fflush(g_handle); 40 | (void)std::fclose(g_handle); 41 | } 42 | g_handle = nullptr; 43 | g_handle_open = false; 44 | } 45 | -------------------------------------------------------------------------------- /src/logging.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #if !defined(NDEBUG) && !defined(DEBUG_LOG) 14 | #define DEBUG_LOG 15 | #endif 16 | 17 | namespace ra2yrcpp { 18 | namespace logging { 19 | 20 | enum class Level : int { ERROR = 0, DEBUG = 1, WARNING = 2, INFO = 3 }; 21 | 22 | constexpr std::array levels = {"ERROR", "DEBUG", "WARNING", 23 | "INFO"}; 24 | 25 | /// Open output log handle if no previous handle is active. 26 | /// @param path Output log path. If nullptr, set up a null handle. 27 | /// @return True if log file was opened succesfully. 28 | bool set_output_handle(const char* path); 29 | 30 | /// Get output log handle 31 | /// @return Output file handle. If no handle has been configured, or previous handle has been closed 32 | /// returns stderr. 33 | FILE* get_output_handle(); 34 | 35 | /// Flush and close a previously opened output handle. 36 | void close_output_handle(); 37 | 38 | template 39 | inline void print_message(FILE* fp, Level level, const char* fmt_s, 40 | const char* file, const char* func, int line, 41 | Args... args) { 42 | fmt::print( 43 | fp, "{}: [thread {} TS: {}]: {}:{}:{} {}\n", 44 | levels[static_cast(level)], 45 | std::hash{}(std::this_thread::get_id()), 46 | static_cast( 47 | std::chrono::high_resolution_clock::now().time_since_epoch().count()), 48 | file, func, line, fmt::format(fmt_s, args...)); 49 | } 50 | 51 | template 52 | inline void print_error(Level level, const char* fmt_s, const char* file, 53 | const char* func, int line, Args... args) { 54 | auto* fp = get_output_handle(); 55 | if (fp == nullptr) { 56 | return; 57 | } 58 | print_message(fp, level, fmt_s, file, func, line, args...); 59 | } 60 | 61 | template 62 | inline void debug(const char* s, const char* file, const char* func, 63 | const int line, Args... args) { 64 | print_error(Level::DEBUG, s, file, func, line, args...); 65 | } 66 | 67 | template 68 | inline void eerror(Args... args) { 69 | print_error(Level::ERROR, args...); 70 | } 71 | 72 | } // namespace logging 73 | } // namespace ra2yrcpp 74 | 75 | #define VA_ARGS(...) , ##__VA_ARGS__ 76 | #define LOCATION_INFO() __FILE__, __func__, __LINE__ 77 | 78 | #ifdef DEBUG_LOG 79 | #define dprintf(fmt, ...) \ 80 | do { \ 81 | ra2yrcpp::logging::debug(fmt, LOCATION_INFO() VA_ARGS(__VA_ARGS__)); \ 82 | } while (0) 83 | #else 84 | #define dprintf(...) 85 | #endif 86 | 87 | #define eprintf(fmt, ...) \ 88 | do { \ 89 | ra2yrcpp::logging::eerror(fmt, LOCATION_INFO() VA_ARGS(__VA_ARGS__)); \ 90 | } while (0) 91 | 92 | #define wrprintf(fmt, ...) \ 93 | do { \ 94 | ra2yrcpp::logging::print_error(ra2yrcpp::logging::Level::WARNING, fmt, \ 95 | LOCATION_INFO() VA_ARGS(__VA_ARGS__)); \ 96 | } while (0) 97 | 98 | #define iprintf(fmt, ...) \ 99 | do { \ 100 | ra2yrcpp::logging::print_error(ra2yrcpp::logging::Level::INFO, fmt, \ 101 | LOCATION_INFO() VA_ARGS(__VA_ARGS__)); \ 102 | } while (0) 103 | -------------------------------------------------------------------------------- /src/multi_client.cpp: -------------------------------------------------------------------------------- 1 | #include "multi_client.hpp" 2 | 3 | #include "protocol/protocol.hpp" 4 | #include "ra2yrproto/commands_builtin.pb.h" 5 | 6 | #include "client_connection.hpp" 7 | #include "errors.hpp" 8 | #include "logging.hpp" 9 | #include "protocol/helpers.hpp" 10 | #include "websocket_connection.hpp" 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace google { 21 | namespace protobuf { 22 | class Message; 23 | } 24 | } // namespace google 25 | 26 | using namespace multi_client; 27 | using connection::State; 28 | 29 | namespace connection = ra2yrcpp::connection; 30 | 31 | AutoPollClient::AutoPollClient( 32 | std::shared_ptr io_service, 33 | AutoPollClient::Options o) 34 | : opt_(o), 35 | io_service_(io_service), 36 | state_(State::NONE), 37 | poll_thread_active_(false) {} 38 | 39 | AutoPollClient::~AutoPollClient() { 40 | auto [lk, v] = state_.acquire(); 41 | if (*v == State::OPEN) { 42 | shutdown(); 43 | } 44 | } 45 | 46 | void AutoPollClient::start() { 47 | static constexpr std::array t = {ClientType::COMMAND, 48 | ClientType::POLL}; 49 | 50 | auto [lk, v] = state_.acquire(); 51 | if (*v != State::NONE) { 52 | throw std::runtime_error( 53 | fmt::format("invalid state: {}", static_cast(*v))); 54 | } 55 | for (auto i : t) { 56 | auto conn = std::make_unique( 57 | std::make_shared( 58 | opt_.host, opt_.port, io_service_.get())); 59 | conn->connect(); 60 | 61 | // send initial "handshake" message 62 | // this will also ensure we fail early in case of connection errors 63 | ra2yrproto::commands::GetSystemState cmd_gs; 64 | 65 | auto r_resp = conn->send_command(cmd_gs, ra2yrproto::CLIENT_COMMAND); 66 | auto ack = 67 | ra2yrcpp::protocol::from_any(r_resp.body()); 68 | queue_ids_[i] = ack.queue_id(); 69 | conn->poll_blocking(opt_.poll_timeout, queue_ids_[i]); 70 | is_clients_.emplace(i, std::move(conn)); 71 | } 72 | poll_thread_ = std::thread([this]() { poll_thread(); }); 73 | } 74 | 75 | void AutoPollClient::shutdown() { 76 | auto [lk, v] = state_.acquire(); 77 | *v = State::CLOSING; 78 | 79 | poll_thread_active_ = false; 80 | poll_thread_.join(); 81 | 82 | // stop both connections 83 | get_client(ClientType::POLL)->disconnect(); 84 | get_client(ClientType::COMMAND)->disconnect(); 85 | state_.store(State::CLOSED); 86 | } 87 | 88 | ra2yrproto::Response AutoPollClient::send_command(const gpb::Message& cmd) { 89 | // Send command 90 | auto resp = get_client(ClientType::COMMAND) 91 | ->send_command(cmd, ra2yrproto::CommandType::CLIENT_COMMAND); 92 | auto ack = 93 | ra2yrcpp::protocol::from_any(resp.body()); 94 | // Wait until item found from polled messages 95 | // TODO(shmocz): signal if poll_thread dies 96 | auto r = ra2yrcpp::make_response( 97 | results().get(ack.id(), opt_.command_timeout), ra2yrcpp::RESPONSE_OK); 98 | results().erase(ack.id()); 99 | return r; 100 | } 101 | 102 | void AutoPollClient::poll_thread() { 103 | // wait connection to be established 104 | get_client(ClientType::POLL)->connection()->state().wait_pred([](auto v) { 105 | return v == State::OPEN; 106 | }); 107 | state_.store(State::OPEN); 108 | poll_thread_active_ = true; 109 | 110 | while (poll_thread_active_) { 111 | try { 112 | // TODO(shmocz): return immediately if signaled to stop 113 | auto R = get_client(ClientType::POLL) 114 | ->poll_blocking(opt_.poll_timeout, 115 | get_queue_id(ClientType::COMMAND)); 116 | for (auto& r : R.result().results()) { 117 | results_.put(r.command_id(), r); 118 | } 119 | } catch (const ra2yrcpp::system_error& e) { 120 | eprintf("internal error, likely cmd connection exit: {}", e.what()); 121 | } catch (const std::exception& e) { 122 | eprintf("fatal error: {}", e.what()); 123 | } 124 | } 125 | } 126 | 127 | ResultMap& AutoPollClient::results() { return results_; } 128 | 129 | InstrumentationClient* AutoPollClient::get_client(ClientType type) { 130 | return is_clients_.at(type).get(); 131 | } 132 | 133 | u64 AutoPollClient::get_queue_id(ClientType t) const { 134 | return queue_ids_.at(t); 135 | } 136 | -------------------------------------------------------------------------------- /src/multi_client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ra2yrproto/core.pb.h" 3 | 4 | #include "async_map.hpp" 5 | #include "client_connection.hpp" 6 | #include "constants.hpp" 7 | #include "instrumentation_client.hpp" 8 | #include "types.h" 9 | #include "utility/sync.hpp" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace ra2yrcpp { 19 | namespace asio_utils { 20 | class IOService; 21 | } 22 | } // namespace ra2yrcpp 23 | 24 | namespace google { 25 | namespace protobuf { 26 | class Message; 27 | } 28 | } // namespace google 29 | 30 | namespace multi_client { 31 | namespace { 32 | using namespace instrumentation_client; 33 | using namespace async_map; 34 | 35 | } // namespace 36 | 37 | namespace connection = ra2yrcpp::connection; 38 | 39 | enum class ClientType : i32 { COMMAND = 0, POLL = 1 }; 40 | 41 | using ResultMap = AsyncMap; 42 | 43 | /// 44 | /// Client that uses two connections to fetch results in real time. One 45 | /// connection issues command executions and the other polls the results. 46 | /// 47 | /// The destructor will automatically call shutdown() if it hasn't been done 48 | /// already. 49 | /// 50 | class AutoPollClient { 51 | public: 52 | struct Options { 53 | std::string host; 54 | std::string port; 55 | duration_t poll_timeout; 56 | duration_t command_timeout; 57 | }; 58 | 59 | /// @exception std::exception on failed connection 60 | explicit AutoPollClient( 61 | std::shared_ptr io_service, 62 | AutoPollClient::Options o); 63 | ~AutoPollClient(); 64 | 65 | /// Establishes connection to InstrumentationService. 66 | /// This function may (and probably will) block until succesful 67 | /// connection. 68 | /// 69 | /// @exception std::runtime_error if attempting to start already started 70 | /// object or on connection failure 71 | void start(); 72 | /// Disconnect both clients, stop poll thread and set internal state to 73 | /// State::CLOSED. 74 | void shutdown(); 75 | 76 | /// Send command message with command client and poll results with poll client 77 | /// @exception std::runtime_error On timeout or serialization failure. 78 | ra2yrproto::Response send_command(const gpb::Message& cmd); 79 | 80 | ResultMap& results(); 81 | /// Get the providied client type initialized by start() 82 | /// 83 | /// @exception std::out_of_range if the client doesn't exist. 84 | InstrumentationClient* get_client(ClientType type); 85 | u64 get_queue_id(ClientType t) const; 86 | 87 | private: 88 | const Options opt_; 89 | std::shared_ptr io_service_; 90 | util::AtomicVariable state_; 91 | std::atomic_bool poll_thread_active_; 92 | ResultMap results_; 93 | 94 | std::map> is_clients_; 95 | std::thread poll_thread_; 96 | std::map queue_ids_; 97 | 98 | // Repeatedly executes blocking poll command on the backend, until stop signal 99 | // is given 100 | void poll_thread(); 101 | }; 102 | 103 | const AutoPollClient::Options default_options = { 104 | cfg::SERVER_ADDRESS, std::to_string(cfg::SERVER_PORT), 105 | cfg::POLL_RESULTS_TIMEOUT, cfg::COMMAND_ACK_TIMEOUT}; 106 | 107 | } // namespace multi_client 108 | -------------------------------------------------------------------------------- /src/process.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "types.h" 4 | #ifdef _WIN32 5 | #include "win32/windows_utils.hpp" 6 | #endif 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace process { 17 | 18 | using thread_id_t = u32; 19 | 20 | namespace { 21 | using namespace std::chrono_literals; 22 | } 23 | 24 | /// Return current thread id 25 | int get_current_tid(); 26 | void* get_current_process_handle(); 27 | 28 | class Thread { 29 | public: 30 | // Open handle to given thread id 31 | explicit Thread(int thread_id = -1); 32 | ~Thread(); 33 | void suspend(); 34 | void resume(); 35 | void set_gpr(x86Reg reg, int value); 36 | int* get_pgpr(x86Reg reg); 37 | void* handle(); 38 | int id(); 39 | 40 | private: 41 | int id_; 42 | void* handle_; 43 | 44 | public: 45 | #ifdef _WIN32 46 | std::unique_ptr sysdata_; 47 | #else 48 | std::unique_ptr sysdata_; 49 | #endif 50 | }; 51 | 52 | void save_context(Thread* T); 53 | std::string proc_basename(std::string name); 54 | unsigned long get_pid(void* handle); 55 | /// Get process id by name. If not found, return 0. 56 | unsigned long get_pid(std::string name); 57 | 58 | class Process { 59 | public: 60 | // Construct from existing process handle 61 | explicit Process(void* handle); 62 | // Open process handle to specified pid 63 | explicit Process(u32 pid, u32 perm = 0u); 64 | Process(const Process&) = delete; 65 | Process& operator=(const Process&) = delete; 66 | ~Process(); 67 | unsigned long get_pid() const; 68 | void* handle() const; 69 | // Write size bytes from src to dest 70 | void write_memory(void* dest, const void* src, std::size_t size, 71 | bool local = false); 72 | void read_memory(void* dest, const void* src, std::size_t size); 73 | // Allocate memory to process 74 | void* allocate_memory(size_t size, unsigned long alloc_type, 75 | unsigned long alloc_protect); 76 | // Allocate memory to process 77 | void* allocate_code(size_t size); 78 | void for_each_thread(std::function callback, 79 | void* cb_ctx = nullptr) const; 80 | // Suspend all threads. If main_tid > -1, suspend if thread's id != main_tid 81 | void suspend_threads(thread_id_t tmain_tid = -1, 82 | duration_t delay = 1.0s) const; 83 | // Suspend all threads, except threads in no_suspend 84 | void suspend_threads(std::vector no_suspend = {}, 85 | duration_t delay = 1.0s) const; 86 | void resume_threads(thread_id_t main_tid = -1) const; 87 | void resume_threads(std::vector no_resume = {}) const; 88 | std::vector list_loaded_modules() const; 89 | 90 | private: 91 | void* handle_; 92 | }; 93 | 94 | Process get_current_process(); 95 | std::string getcwd(); 96 | 97 | } // namespace process 98 | -------------------------------------------------------------------------------- /src/protocol/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(BDIR "${CMAKE_CURRENT_BINARY_DIR}") 2 | set(SDIR "${CMAKE_CURRENT_SOURCE_DIR}") 3 | file( 4 | GLOB PROTO_FILES 5 | RELATIVE "${SDIR}/ra2yrproto" 6 | "${SDIR}/ra2yrproto/ra2yrproto/*.proto") 7 | 8 | # set protobuf file paths 9 | set(PROTO_HDRS "") 10 | set(PROTO_SRCS "") 11 | set(PROTO_DEPS "") 12 | foreach(X IN LISTS PROTO_FILES) 13 | string(REPLACE ".proto" ".pb.h" FN_H ${X}) 14 | string(REPLACE ".proto" ".pb.cc" FN_S ${X}) 15 | list(APPEND PROTO_HDRS ${FN_H}) 16 | list(APPEND PROTO_SRCS ${FN_S}) 17 | list(APPEND PROTO_DEPS "ra2yrproto/${X}") 18 | unset(FN_H) 19 | unset(FN_S) 20 | endforeach() 21 | 22 | if(NOT protobuf_SOURCE_DIR) 23 | set(protobuf_SOURCE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/protobuf") 24 | endif() 25 | set(PROTOBUF_SOURCES "${protobuf_SOURCE_DIR}/src") 26 | 27 | add_custom_target(compile-protobuf ALL DEPENDS ${PROTO_SRCS} ${PROTO_HDRS}) 28 | add_custom_command( 29 | OUTPUT ${PROTO_SRCS} ${PROTO_HDRS} 30 | WORKING_DIRECTORY "${SDIR}/ra2yrproto" 31 | COMMAND 32 | ${PROTOC_PATH} -I=. -I="${SDIR}/ra2yrproto" -I="${PROTOBUF_SOURCES}" 33 | --pyi_out=${BDIR} --python_out=${BDIR} --cpp_out=${BDIR} ${PROTO_FILES} 34 | DEPENDS ${PROTO_DEPS}) 35 | 36 | add_library(protocol_o OBJECT helpers.cpp protocol.cpp ${PROTO_SRCS} 37 | ../util_string.cpp ../errors.cpp) 38 | add_library(protocol INTERFACE protocol.cpp) 39 | if(NOT protobuf_PROTOC_EXE) 40 | add_dependencies(protocol_o compile-protobuf) 41 | endif() 42 | target_link_libraries(protocol INTERFACE protocol_o) 43 | 44 | # TODO: should make generated headers as install targets 45 | -------------------------------------------------------------------------------- /src/protocol/helpers.cpp: -------------------------------------------------------------------------------- 1 | #include "protocol/helpers.hpp" 2 | 3 | #include "types.h" 4 | #include "util_string.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #undef GetMessage 22 | 23 | using namespace ra2yrcpp::protocol; 24 | 25 | bool ra2yrcpp::protocol::write_message(const gpb::Message* M, 26 | gpb::io::CodedOutputStream* is) { 27 | auto l = M->ByteSizeLong(); 28 | is->WriteVarint32(l); 29 | return M->SerializeToCodedStream(is) && !is->HadError(); 30 | } 31 | 32 | bool ra2yrcpp::protocol::read_message(gpb::Message* M, 33 | gpb::io::CodedInputStream* is) { 34 | u32 length; 35 | if (!is->ReadVarint32(&length)) { 36 | return false; 37 | } 38 | auto l = is->PushLimit(length); 39 | bool res = M->ParseFromCodedStream(is); 40 | is->PopLimit(l); 41 | return res; 42 | } 43 | 44 | MessageBuilder::MessageBuilder(std::string name) { 45 | pool = gpb::DescriptorPool::generated_pool(); 46 | desc = pool->FindMessageTypeByName(name); 47 | if (desc == nullptr) { 48 | throw std::runtime_error(std::string("no such message ") + name); 49 | } 50 | auto* msg_proto = F.GetPrototype(desc); 51 | m = msg_proto->New(); 52 | refl = m->GetReflection(); 53 | } 54 | 55 | // TODO: ensure that this works OK for nullptr stream 56 | MessageIstream::MessageIstream(std::shared_ptr is, bool gzip) 57 | : MessageStream(gzip), 58 | is(is), 59 | s_i(std::make_shared(is.get())) { 60 | if (gzip) { 61 | s_ig = std::make_shared(s_i.get()); 62 | } 63 | } 64 | 65 | MessageStream::MessageStream(bool gzip) : gzip(gzip) {} 66 | 67 | MessageOstream::MessageOstream(std::shared_ptr os, bool gzip) 68 | : MessageStream(gzip), os(os) { 69 | if (os == nullptr) { 70 | return; 71 | } 72 | s_o = std::make_shared(os.get()); 73 | if (gzip) { 74 | s_g = std::make_shared(s_o.get()); 75 | } 76 | } 77 | 78 | bool MessageOstream::write(const gpb::Message& M) { 79 | if (os == nullptr) { 80 | return false; 81 | } 82 | 83 | if (gzip) { 84 | gpb::io::CodedOutputStream co(s_g.get()); 85 | return write_message(&M, &co); 86 | } else { 87 | gpb::io::CodedOutputStream co(s_o.get()); 88 | return write_message(&M, &co); 89 | } 90 | return false; 91 | } 92 | 93 | bool MessageIstream::read(gpb::Message* M) { 94 | if (is == nullptr) { 95 | return false; 96 | } 97 | 98 | if (gzip) { 99 | gpb::io::CodedInputStream co(s_ig.get()); 100 | return read_message(M, &co); 101 | } else { 102 | gpb::io::CodedInputStream co(s_i.get()); 103 | return read_message(M, &co); 104 | } 105 | return false; 106 | } 107 | 108 | gpb::Message* ra2yrcpp::protocol::create_command_message(MessageBuilder* B, 109 | std::string args) { 110 | if (!args.empty()) { 111 | auto* cmd_args = B->m->GetReflection()->MutableMessage( 112 | B->m, B->desc->FindFieldByName("args")); 113 | gpb::util::JsonStringToMessage(args, cmd_args); 114 | } 115 | return B->m; 116 | } 117 | 118 | void ra2yrcpp::protocol::dump_messages(std::string path, const gpb::Message& M, 119 | std::function cb) { 120 | bool ok = true; 121 | auto ii = std::make_shared( 122 | path, std::ios_base::in | std::ios_base::binary); 123 | ra2yrcpp::protocol::MessageBuilder B(M.GetTypeName()); 124 | 125 | const bool use_gzip = true; 126 | ra2yrcpp::protocol::MessageIstream MS(ii, use_gzip); 127 | 128 | if (cb == nullptr) { 129 | cb = [](auto* M) { fmt::print("{}\n", ra2yrcpp::protocol::to_json(*M)); }; 130 | } 131 | 132 | while (ok) { 133 | ok = MS.read(B.m); 134 | cb(B.m); 135 | } 136 | } 137 | 138 | std::string ra2yrcpp::protocol::message_type(const gpb::Any& m) { 139 | auto toks = ra2yrcpp::split_string(m.type_url(), "/"); 140 | return toks.back(); 141 | } 142 | 143 | std::string ra2yrcpp::protocol::message_type(const gpb::Message& m) { 144 | return m.GetTypeName(); 145 | } 146 | 147 | bool ra2yrcpp::protocol::from_json(const vecu8& bytes, gpb::Message* m) { 148 | return from_json(ra2yrcpp::to_string(bytes), m); 149 | } 150 | 151 | bool ra2yrcpp::protocol::from_json(const std::string& s, gpb::Message* m) { 152 | if (gpb::util::JsonStringToMessage(s, m).ok()) { 153 | return true; 154 | } 155 | 156 | return false; 157 | } 158 | 159 | std::string ra2yrcpp::protocol::to_json(const gpb::Message& m) { 160 | std::string res; 161 | gpb::util::MessageToJsonString(m, &res); 162 | return res; 163 | } 164 | 165 | std::vector ra2yrcpp::protocol::find_set_fields( 166 | const gpb::Message& M) { 167 | auto* rfl = M.GetReflection(); 168 | std::vector out; 169 | rfl->ListFields(M, &out); 170 | return out; 171 | } 172 | 173 | void ra2yrcpp::protocol::copy_field(gpb::Message* dst, gpb::Message* src, 174 | const gpb::FieldDescriptor* f) { 175 | dst->GetReflection()->MutableMessage(dst, f)->CopyFrom( 176 | src->GetReflection()->GetMessage(*src, f)); 177 | } 178 | -------------------------------------------------------------------------------- /src/protocol/helpers.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "types.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace ra2yrcpp::protocol { 25 | 26 | namespace gpb = google::protobuf; 27 | 28 | struct MessageBuilder { 29 | gpb::DynamicMessageFactory F; 30 | const gpb::DescriptorPool* pool; 31 | const gpb::Descriptor* desc; 32 | gpb::Message* m; 33 | const gpb::Reflection* refl; 34 | explicit MessageBuilder(std::string name); 35 | }; 36 | 37 | struct MessageStream { 38 | explicit MessageStream(bool gzip); 39 | bool gzip; 40 | }; 41 | 42 | struct MessageIstream : public MessageStream { 43 | MessageIstream(std::shared_ptr is, bool gzip); 44 | bool read(gpb::Message* M); 45 | 46 | std::shared_ptr is; 47 | std::shared_ptr s_i; 48 | std::shared_ptr s_ig; 49 | }; 50 | 51 | struct MessageOstream : public MessageStream { 52 | MessageOstream(std::shared_ptr os, bool gzip); 53 | bool write(const gpb::Message& M); 54 | 55 | std::shared_ptr os; 56 | std::shared_ptr s_o; 57 | std::shared_ptr s_g; 58 | }; 59 | 60 | bool write_message(const gpb::Message* M, gpb::io::CodedOutputStream* os); 61 | bool read_message(gpb::Message* M, gpb::io::CodedInputStream* os); 62 | 63 | /// Dynamically set the field "args" of B's Message by parsing the JSON string 64 | /// in args. If args is empty, field is not set. 65 | gpb::Message* create_command_message(MessageBuilder* B, std::string args); 66 | 67 | /// Process stream of serialized protobuf messages of same type. 68 | /// 69 | /// @param path Path to the file 70 | /// @param M Message type to be read 71 | /// @param cb Callback to invoke for each processed message. If unspecified, 72 | /// dumps messages as JSON to stdout 73 | void dump_messages(std::string path, const gpb::Message& M, 74 | std::function cb = nullptr); 75 | 76 | std::string message_type(const gpb::Any& m); 77 | std::string message_type(const gpb::Message& m); 78 | 79 | [[nodiscard]] bool from_json(const vecu8& bytes, gpb::Message* m); 80 | [[nodiscard]] bool from_json(const std::string& s, gpb::Message* m); 81 | 82 | std::string to_json(const gpb::Message& m); 83 | 84 | template 85 | T from_any(const gpb::Any& a) { 86 | T res; 87 | if (!a.UnpackTo(&res)) { 88 | throw std::runtime_error( 89 | fmt::format("Could not unpack message from {} to {}", a.type_url(), 90 | message_type(res))); 91 | } 92 | return res; 93 | } 94 | 95 | std::vector find_set_fields(const gpb::Message& M); 96 | 97 | /// 98 | /// Fills with n copies of given type. 99 | /// 100 | template 101 | void fill_repeated(gpb::RepeatedPtrField* dst, std::size_t n) { 102 | for (std::size_t i = 0U; i < n; i++) { 103 | dst->Add(); 104 | } 105 | } 106 | 107 | /// 108 | /// Clears the RepeatedPtField and fills it with n copies of given type. 109 | /// 110 | template 111 | void fill_repeated_empty(gpb::RepeatedPtrField* dst, std::size_t n) { 112 | dst->Clear(); 113 | fill_repeated(dst, n); 114 | } 115 | 116 | void copy_field(gpb::Message* dst, gpb::Message* src, 117 | const gpb::FieldDescriptor* f); 118 | 119 | /// Truncate RepeatedPtrField to given size. If length of dst 120 | /// is less than n, no truncation is performed. 121 | /// 122 | /// @param dst 123 | /// @param n size to truncate to 124 | /// @return True if truncation was performed. False otherwise. 125 | template 126 | bool truncate(gpb::RepeatedPtrField* dst, int n) { 127 | if (n < dst->size()) { 128 | dst->DeleteSubrange(n, (dst->size() - n)); 129 | return true; 130 | } 131 | return false; 132 | } 133 | 134 | } // namespace ra2yrcpp::protocol 135 | -------------------------------------------------------------------------------- /src/protocol/protocol.cpp: -------------------------------------------------------------------------------- 1 | #include "protocol/protocol.hpp" 2 | 3 | #include "ra2yrproto/commands_builtin.pb.h" 4 | #include "ra2yrproto/commands_yr.pb.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | using namespace ra2yrcpp; 17 | 18 | vecu8 ra2yrcpp::to_vecu8(const gpb::Message& msg) { 19 | vecu8 res; 20 | res.resize(msg.ByteSizeLong()); 21 | if (!msg.SerializeToArray(res.data(), res.size())) { 22 | throw std::runtime_error( 23 | fmt::format("failed to serialize message {}", msg.GetTypeName())); 24 | } 25 | return res; 26 | } 27 | 28 | ra2yrproto::Response ra2yrcpp::make_response(const gpb::Message&& body, 29 | ra2yrproto::ResponseCode code) { 30 | ra2yrproto::Response r; 31 | r.set_code(code); 32 | if (!r.mutable_body()->PackFrom(body)) { 33 | throw std::runtime_error("Could not pack message body"); 34 | } 35 | return r; 36 | } 37 | 38 | ra2yrproto::Command ra2yrcpp::create_command(const gpb::Message& cmd, 39 | ra2yrproto::CommandType type) { 40 | ra2yrproto::Command C; 41 | C.set_command_type(type); 42 | if (!C.mutable_command()->PackFrom(cmd)) { 43 | throw std::runtime_error("Packing message failed"); 44 | } 45 | return C; 46 | } 47 | -------------------------------------------------------------------------------- /src/protocol/protocol.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ra2yrproto/core.pb.h" 4 | 5 | #include "types.h" 6 | 7 | namespace ra2yrcpp { 8 | 9 | namespace gpb = google::protobuf; 10 | 11 | constexpr auto RESPONSE_OK = ra2yrproto::ResponseCode::OK; 12 | constexpr auto RESPONSE_ERROR = ra2yrproto::ResponseCode::ERROR; 13 | 14 | /// Serialize message to vecu8 15 | /// @param msg 16 | /// @exception std::runtime_error on serialization failure 17 | vecu8 to_vecu8(const gpb::Message& msg); 18 | 19 | /// Create Response message 20 | /// @param body 21 | /// @param code 22 | /// @exception std::runtime_error if message packing fails 23 | ra2yrproto::Response make_response(const gpb::Message&& body, 24 | ra2yrproto::ResponseCode code = RESPONSE_OK); 25 | 26 | /// 27 | /// Create command message. 28 | /// @param cmd message to be set as command field 29 | /// @param type command type 30 | /// @exception std::runtime_error if message packing fails 31 | /// 32 | ra2yrproto::Command create_command( 33 | const gpb::Message& cmd, 34 | ra2yrproto::CommandType type = ra2yrproto::CLIENT_COMMAND); 35 | 36 | } // namespace ra2yrcpp 37 | -------------------------------------------------------------------------------- /src/ra2/abi.cpp: -------------------------------------------------------------------------------- 1 | #include "ra2/abi.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | using namespace ra2::abi; 9 | 10 | ABIGameMD::ABIGameMD() {} 11 | 12 | Xbyak::CodeGenerator* ABIGameMD::find_codegen(u32 address) { 13 | try { 14 | return code_generators().at(address).get(); 15 | } catch (const std::out_of_range& e) { 16 | } 17 | return nullptr; 18 | } 19 | 20 | codegen_store& ABIGameMD::code_generators() { return code_generators_; } 21 | 22 | bool ABIGameMD::SelectObject(u32 address) { 23 | return ra2::abi::SelectObject::call(this, address); 24 | } 25 | 26 | void ABIGameMD::SellBuilding(u32 address) { 27 | ra2::abi::SellBuilding::call(this, address, 1); 28 | } 29 | 30 | void ABIGameMD::DeployObject(u32 address) { 31 | ra2::abi::DeployObject::call(this, address); 32 | } 33 | 34 | bool ABIGameMD::ClickEvent(u32 address, u8 event) { 35 | return ra2::abi::ClickEvent::call(this, address, event); 36 | } 37 | 38 | u32 ABIGameMD::timeGetTime() { 39 | return reinterpret_cast( 40 | serialize::read_obj_le(0x7E1530))(); 41 | } 42 | 43 | bool ABIGameMD::BuildingTypeClass_CanPlaceHere(std::uintptr_t p_this, 44 | CellStruct* cell, 45 | std::uintptr_t house_owner) { 46 | return BuildingTypeClass_CanPlaceHere::call(this, p_this, cell, house_owner); 47 | } 48 | 49 | bool ABIGameMD::DisplayClass_Passes_Proximity_Check(std::uintptr_t p_this, 50 | BuildingTypeClass* p_object, 51 | u32 house_index, 52 | CellStruct* cell) { 53 | auto* fnd = BuildingTypeClass_GetFoundationData::call(this, p_object, true); 54 | 55 | return ra2::abi::DisplayClass_Passes_Proximity_Check::call( 56 | this, p_this, p_object, house_index, fnd, cell) && 57 | ra2::abi::DisplayClass_SomeCheck::call(this, p_this, p_object, 58 | house_index, fnd, cell); 59 | } 60 | 61 | void ABIGameMD::AddMessage(int id, std::string message, i32 color, i32 style, 62 | u32 duration_frames, bool single_player) { 63 | static constexpr std::uintptr_t MessageListClass = 0xA8BC60U; 64 | std::wstring m(message.begin(), message.end()); 65 | ra2::abi::AddMessage::call(this, MessageListClass, nullptr, id, m.c_str(), 66 | color, style, duration_frames, single_player); 67 | } 68 | 69 | int ra2::abi::get_tiberium_type(int overlayTypeIndex) { 70 | auto* A = OverlayTypeClass::Array.get(); 71 | if (overlayTypeIndex == -1 || !A->GetItem(overlayTypeIndex)->Tiberium) { 72 | return -1; 73 | } 74 | auto I = DVCIterator(TiberiumClass::Array.get()); 75 | for (const auto& t : I) { 76 | int ix = t->Image->ArrayIndex; 77 | if ((ix <= overlayTypeIndex && (overlayTypeIndex < t->NumImages + ix)) || 78 | (t->NumImages + ix <= overlayTypeIndex && 79 | (overlayTypeIndex < t->field_EC + t->NumImages + ix))) { 80 | return t->ArrayIndex; 81 | } 82 | } 83 | return 0; 84 | } 85 | 86 | int ra2::abi::get_tiberium_value(const CellClass& cell) { 87 | int ix = get_tiberium_type(cell.OverlayTypeIndex); 88 | if (ix == -1) { 89 | return 0; 90 | } 91 | return TiberiumClass::Array.get()->GetItem(ix)->Value * 92 | (cell.OverlayData + 1); 93 | } 94 | -------------------------------------------------------------------------------- /src/ra2/common.cpp: -------------------------------------------------------------------------------- 1 | #include "ra2/common.hpp" 2 | 3 | #include "ra2yrproto/ra2yr.pb.h" 4 | 5 | #include "ra2/yrpp_export.hpp" 6 | 7 | using namespace ra2; 8 | 9 | CellStruct ra2::coord_to_cell(const ra2yrproto::ra2yr::Coordinates& c) { 10 | CoordStruct coords{.X = c.x(), .Y = c.y(), .Z = c.z()}; 11 | return CellClass::Coord2Cell(coords); 12 | } 13 | 14 | CellClass* ra2::get_map_cell(const ra2yrproto::ra2yr::Coordinates& c) { 15 | return MapClass::Instance.get()->TryGetCellAt(coord_to_cell(c)); 16 | } 17 | -------------------------------------------------------------------------------- /src/ra2/common.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ra2/yrpp_export.hpp" 4 | 5 | namespace ra2yrproto { 6 | namespace ra2yr { 7 | class Coordinates; 8 | } 9 | } // namespace ra2yrproto 10 | 11 | namespace ra2 { 12 | CellStruct coord_to_cell(const ra2yrproto::ra2yr::Coordinates& c); 13 | CellClass* get_map_cell(const ra2yrproto::ra2yr::Coordinates& c); 14 | }; // namespace ra2 15 | -------------------------------------------------------------------------------- /src/ra2/event_list.cpp: -------------------------------------------------------------------------------- 1 | #include "ra2/event_list.hpp" 2 | 3 | #include "ra2/yrpp_export.hpp" 4 | #include "utility/serialize.hpp" 5 | 6 | #include 7 | 8 | using namespace ra2; 9 | 10 | void EventListUtil::elist_apply(EventListCtx* C, 11 | std::function fn) { 12 | for (auto i = 0; i < C->count; i++) { 13 | auto e = get_event(C, i); 14 | if (fn(e)) { 15 | break; 16 | } 17 | } 18 | } 19 | 20 | EventListCtx EventListUtil::from_eventlist(EventListType t) { 21 | switch (t) { 22 | case OUT_LIST: 23 | return EventListCtx::out_list(); 24 | case DO_LIST: 25 | return EventListCtx::do_list(); 26 | case MEGAMISSION_LIST: 27 | return EventListCtx::megamission_list(); 28 | default: 29 | throw std::runtime_error("Invalid EventList type"); 30 | } 31 | } 32 | 33 | EventEntry EventListUtil::find(EventListType t, const EventClass& E) { 34 | EventEntry res{}; 35 | auto C = EventListUtil::from_eventlist(t); 36 | auto fn = [&](const EventEntry& e) { 37 | if (e.e->HouseIndex == E.HouseIndex && e.e->Type == E.Type) { 38 | if (E.Type == EventType::SELLCELL) { 39 | if (serialize::bytes_equal(&E.Data.SellCell, &e.e->Data.SellCell)) { 40 | res = e; 41 | return true; 42 | } 43 | } else if (E.Type == EventType::PLACE) { 44 | if (E.Data.Place.RTTIType == e.e->Data.Place.RTTIType && 45 | E.Data.Place.HeapID == e.e->Data.Place.HeapID) { 46 | res = e; 47 | return true; 48 | } 49 | } else { 50 | res = e; 51 | return true; 52 | } 53 | } 54 | return false; 55 | }; 56 | elist_apply(&C, fn); 57 | return res; 58 | } 59 | -------------------------------------------------------------------------------- /src/ra2/event_list.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ra2/yrpp_export.hpp" 3 | 4 | #include 5 | 6 | namespace ra2 { 7 | struct EventEntry { 8 | const EventClass* e; 9 | int timing; 10 | int index; 11 | }; 12 | 13 | enum EventListType : int { OUT_LIST = 1, DO_LIST = 2, MEGAMISSION_LIST = 3 }; 14 | 15 | struct EventListCtx { 16 | int count; 17 | int head; 18 | int tail; 19 | EventClass* list; 20 | int* timings; 21 | int length; 22 | 23 | template 24 | static EventListCtx from_eventlist(EListT* E) { 25 | return {E->Count, E->Head, E->Tail, 26 | E->List, E->Timings, (sizeof(E->List) / sizeof(*E->List))}; 27 | } 28 | 29 | static EventListCtx out_list() { 30 | return from_eventlist(&EventClass::OutList.get()); 31 | } 32 | 33 | static EventListCtx do_list() { 34 | return from_eventlist(&EventClass::DoList.get()); 35 | } 36 | 37 | static EventListCtx megamission_list() { 38 | return from_eventlist(&EventClass::MegaMissionList.get()); 39 | } 40 | }; 41 | 42 | struct EventListUtil { 43 | static EventEntry get_event(EventListCtx* C, int i) { 44 | auto ix = static_cast((C->head + i) & (C->length - 1)); 45 | return EventEntry{&C->list[ix], C->timings[ix], i}; 46 | } 47 | 48 | template 49 | static EventEntry convert_event(EListT* list, const EventClass* E) { 50 | auto ix = (E - list->List) / sizeof(*E); 51 | // FIXME wrong ix 52 | return EventEntry{&list->List[ix], list->Timings[ix], static_cast(ix)}; 53 | } 54 | 55 | static void elist_apply(EventListCtx* C, 56 | std::function fn); 57 | 58 | template 59 | static void apply(EListT* list, std::function fn) { 60 | auto C = EventListCtx::from_eventlist(list); 61 | return elist_apply(&C, fn); 62 | } 63 | 64 | static EventListCtx from_eventlist(EventListType t); 65 | 66 | static EventEntry find(EventListType t, const EventClass& E); 67 | }; 68 | 69 | }; // namespace ra2 70 | -------------------------------------------------------------------------------- /src/ra2/state_context.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ra2yrproto/ra2yr.pb.h" 3 | 4 | #include "ra2/abi.hpp" 5 | #include "ra2/event_list.hpp" 6 | #include "types.h" 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace ra2 { 16 | 17 | using abi_t = ra2::abi::ABIGameMD; 18 | 19 | struct ObjectEntry { 20 | const ra2yrproto::ra2yr::Object* o; 21 | const ra2yrproto::ra2yr::ObjectTypeClass* tc; 22 | }; 23 | 24 | /// Helper class to inspect protobuf state and raw game state. 25 | /// Uses caching to perform fast lookups when mapping related objects 26 | /// (e.g. Object's type to ObjectTypeClass instance). 27 | class StateContext { 28 | using storage_t = ra2yrproto::ra2yr::StorageValue; 29 | using tc_cache_t = 30 | std::map; 31 | 32 | public: 33 | StateContext(abi_t* abi, storage_t* s); 34 | 35 | /// Add event to OutList. 36 | /// @param ev 37 | /// @param frame_delay how many frames to delay the execution relative to 38 | /// current frame 39 | /// @param spoof marks this event's House index to be spoofed. This requires 40 | /// additional patches to spawner. 41 | /// @param filter_duplicates throw exception if event of same type and 42 | /// house is found from any event list 43 | /// @return The resulting event added to the OutList 44 | /// @exception std::runtime_error if: 45 | /// - filter_duplicates is true and existing event was found 46 | /// - type is PRODUCE and no suitable type class was found 47 | /// - AddEvent returns false 48 | const EventEntry add_event(const ra2yrproto::ra2yr::Event& ev, 49 | u32 frame_delay = 0U, bool spoof = false, 50 | bool filter_duplicates = true); 51 | 52 | /// Retrieve ObjectTypeClass via pointer_self value. 53 | /// @param address value to be searched for 54 | /// @return pointer to the found ObjectTypeClass 55 | /// @exception std::runtime_error if not found 56 | const ra2yrproto::ra2yr::ObjectTypeClass* get_type_class( 57 | std::uintptr_t address); 58 | 59 | /// Find event with matching type and house index from all event lists. 60 | /// @param query event to be matched against 61 | /// @return the event entry and list name 62 | /// @exception 63 | const std::tuple find_event( 64 | const ra2yrproto::ra2yr::Event& query); 65 | 66 | const ra2yrproto::ra2yr::Object* get_object( 67 | std::function pred); 68 | 69 | /// Find object from current state that matches a predicate. 70 | /// @param pred predicate that returns true on match, false on failure 71 | /// @exception std::runtime_error if no object was found 72 | ObjectEntry get_object_entry( 73 | std::function pred); 74 | 75 | /// Find object from current state by address value. 76 | /// @param address 77 | /// @exception std::runtime_error if no object was found 78 | ObjectEntry get_object_entry(std::uintptr_t address); 79 | 80 | ObjectEntry get_object_entry(const ra2yrproto::ra2yr::Object& O); 81 | 82 | const ra2yrproto::ra2yr::House* get_house( 83 | std::function pred); 84 | 85 | const ra2yrproto::ra2yr::House* get_house(std::uintptr_t address); 86 | 87 | const ra2yrproto::ra2yr::GameState* current_state(); 88 | const ra2yrproto::ra2yr::House* current_player(); 89 | 90 | const ra2yrproto::ra2yr::Factory* find_factory( 91 | std::function pred); 92 | 93 | tc_cache_t& tc_cache(); 94 | 95 | void place_building(const ra2yrproto::ra2yr::House& H, 96 | const ra2yrproto::ra2yr::ObjectTypeClass& T, 97 | const ra2yrproto::ra2yr::Coordinates& C); 98 | 99 | abi_t* abi_; 100 | storage_t* s_; 101 | tc_cache_t tc_cache_; 102 | 103 | private: 104 | const ra2yrproto::ra2yr::Object* get_object(std::uintptr_t address); 105 | }; 106 | } // namespace ra2 107 | -------------------------------------------------------------------------------- /src/ra2/state_parser.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ra2yrproto/ra2yr.pb.h" 4 | 5 | #include "protocol/helpers.hpp" 6 | #include "types.h" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | class CellClass; 17 | class EventClass; 18 | class HouseClass; 19 | class MapClass; 20 | 21 | namespace ra2 { 22 | namespace abi { 23 | class ABIGameMD; 24 | } 25 | } // namespace ra2 26 | 27 | namespace ra2 { 28 | 29 | namespace gpb = google::protobuf; 30 | 31 | struct Cookie { 32 | ra2::abi::ABIGameMD* abi; 33 | void* src; 34 | }; 35 | 36 | struct ClassParser { 37 | Cookie c; 38 | ra2yrproto::ra2yr::Object* T; 39 | 40 | ClassParser(Cookie c, ra2yrproto::ra2yr::Object* T); 41 | 42 | void Object(); 43 | 44 | void Mission(); 45 | 46 | void Radio(); 47 | 48 | void Techno(); 49 | 50 | void Foot(); 51 | 52 | void Aircraft(); 53 | 54 | void Unit(); 55 | 56 | void Building(); 57 | 58 | void Infantry(); 59 | 60 | void parse(); 61 | 62 | void set_type_class(void* ttc, ra2yrproto::ra2yr::AbstractType); 63 | }; 64 | 65 | struct TypeClassParser { 66 | Cookie c; 67 | ra2yrproto::ra2yr::ObjectTypeClass* T; 68 | 69 | TypeClassParser(Cookie c, ra2yrproto::ra2yr::ObjectTypeClass* T); 70 | 71 | void AbstractType(); 72 | 73 | void AircraftType(); 74 | 75 | void TechnoType(); 76 | 77 | void UnitType(); 78 | 79 | void InfantryType(); 80 | 81 | void ObjectType(); 82 | 83 | void BuildingType(); 84 | 85 | void OverlayType(); 86 | 87 | void parse(); 88 | }; 89 | 90 | struct EventParser { 91 | const EventClass* src; 92 | ra2yrproto::ra2yr::Event* T; 93 | u32 time; 94 | EventParser(const EventClass* src, ra2yrproto::ra2yr::Event* T, u32 time); 95 | 96 | void Target(); 97 | 98 | void MegaMission(); 99 | 100 | void Deploy(); 101 | 102 | void Production(); 103 | 104 | void Place(); 105 | 106 | void Sell(); 107 | 108 | void parse(); 109 | }; 110 | 111 | void parse_HouseClass(ra2yrproto::ra2yr::House* dst, const HouseClass* src); 112 | 113 | // Intermediate structure for more efficient map data processing 114 | struct Cell { 115 | i32 land_type; 116 | // crate type and some weird data 117 | i32 overlay_data; 118 | i32 tiberium_value; 119 | // Objects in this Cell 120 | // repeated Object objects = 8; 121 | double radiation_level; 122 | u32 passability; 123 | int index; 124 | bool shrouded; 125 | char height; 126 | char level; 127 | char pad[1]; 128 | std::uintptr_t first_object; 129 | i32 wall_owner_index; 130 | i32 overlay_type_index; 131 | 132 | static void copy_to(ra2yrproto::ra2yr::Cell* dst, const Cell* src); 133 | }; 134 | 135 | void parse_MapData(ra2yrproto::ra2yr::MapData* dst, MapClass* src, 136 | ra2::abi::ABIGameMD* abi); 137 | 138 | void parse_EventLists(ra2yrproto::ra2yr::GameState* G, 139 | ra2yrproto::ra2yr::EventListsSnapshot* ES, 140 | std::size_t max_size); 141 | 142 | void parse_prerequisiteGroups(ra2yrproto::ra2yr::PrerequisiteGroups* T); 143 | 144 | void parse_map(std::vector* previous, MapClass* D, 145 | gpb::RepeatedPtrField* difference); 146 | 147 | std::vector get_valid_cells(MapClass* M); 148 | 149 | void parse_Factories(gpb::RepeatedPtrField* dst); 150 | 151 | template 152 | static auto init_arrays(U* dst) { 153 | auto* D = T::Array.get(); 154 | if (dst->size() != D->Count) { 155 | ra2yrcpp::protocol::fill_repeated_empty(dst, D->Count); 156 | } 157 | return std::make_tuple(D, dst); 158 | } 159 | 160 | gpb::RepeatedPtrField* 161 | parse_AbstractTypeClasses( 162 | gpb::RepeatedPtrField* T, 163 | ra2::abi::ABIGameMD* abi); 164 | 165 | void parse_Objects(ra2yrproto::ra2yr::GameState* G, ra2::abi::ABIGameMD* abi); 166 | void parse_HouseClasses(ra2yrproto::ra2yr::GameState* G); 167 | 168 | ra2yrproto::ra2yr::ObjectTypeClass* find_type_class( 169 | gpb::RepeatedPtrField* types, 170 | ra2yrproto::ra2yr::AbstractType rtti_id, int array_index); 171 | 172 | /// Return true if the current player is the only human player in the game. 173 | bool is_local(const gpb::RepeatedPtrField& H); 174 | 175 | } // namespace ra2 176 | -------------------------------------------------------------------------------- /src/ra2/yrpp_export.cpp: -------------------------------------------------------------------------------- 1 | #include "yrpp_export.hpp" 2 | -------------------------------------------------------------------------------- /src/ra2/yrpp_export.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // IWYU pragma: begin_exports 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | // IWYU pragma: end_exports 41 | 42 | #undef ERROR 43 | -------------------------------------------------------------------------------- /src/ra2yrcpp.cpp: -------------------------------------------------------------------------------- 1 | #include "ra2yrcpp.hpp" 2 | 3 | #include "instrumentation_service.hpp" 4 | #include "is_context.hpp" 5 | #include "win32/windows_utils.hpp" 6 | 7 | #include 8 | #include 9 | 10 | using namespace ra2yrcpp; 11 | 12 | void Main::create_hook(hook::HookEntry h, hook::hook_fn f) { 13 | iprintf("name={},target={:#x},size_bytes={}", h.name, h.address, h.size); 14 | if (hooks_.find(h.address) != hooks_.end()) { 15 | throw std::runtime_error( 16 | fmt::format("Can't overwrite existing hook (name={} address={})", 17 | h.name, reinterpret_cast(h.address))); 18 | } 19 | hooks_.try_emplace(h.address, h, f); 20 | } 21 | 22 | void Main::create_all_hooks(char* hooks_section, std::size_t section_size, 23 | void* dll_handle) { 24 | // For each hook entry 25 | const char* hooks_end = hooks_section + section_size; 26 | for (char* p = hooks_section; p < hooks_end; p += sizeof(hook::HookEntry)) { 27 | auto* H = reinterpret_cast(p); 28 | // Get corresponding function 29 | // std::string fn_name = "_" + std::string(H->hookName); 30 | std::string fn_name = std::string(H->name); 31 | auto* proc_address = reinterpret_cast( 32 | windows_utils::get_proc_address(fn_name, dll_handle)); 33 | if (proc_address == nullptr) { 34 | throw std::runtime_error( 35 | fmt::format("couldn't find hook function: {}", fn_name)); 36 | } 37 | // Patch target code 38 | create_hook(*H, proc_address); 39 | } 40 | } 41 | 42 | void Main::create_all_hooks() { 43 | auto P = process::get_current_process(); 44 | void* dll = windows_utils::find_dll(cfg::DLL_NAME); 45 | if (dll == nullptr) { 46 | throw std::runtime_error("ra2yrcpp main DLL not loaded"); 47 | } 48 | 49 | // Get syringe section 50 | auto section = windows_utils::find_section(dll, ".syhks00"); 51 | if (section.data == nullptr) { 52 | throw std::runtime_error(".syhks00 section not found from DLL"); 53 | } 54 | 55 | create_all_hooks(reinterpret_cast(section.data), section.length, dll); 56 | } 57 | 58 | Main* Main::get() { 59 | static Main* I = nullptr; 60 | if (I == nullptr) { 61 | I = new Main(); 62 | } 63 | return I; 64 | } 65 | 66 | void Main::load_configuration(const char* config_path) { 67 | std::string json = "{}"; 68 | if (config_path != nullptr) { 69 | std::ifstream ifs(config_path); 70 | json = std::string((std::istreambuf_iterator(ifs)), 71 | std::istreambuf_iterator()); 72 | } 73 | cfg_ = std::make_unique(json); 74 | 75 | if (cfg_->c().log_filename.empty()) { 76 | ra2yrcpp::logging::close_output_handle(); 77 | // TODO: Check errors 78 | (void)ra2yrcpp::logging::set_output_handle(nullptr); 79 | } 80 | } 81 | 82 | void Main::start_service() { 83 | if (service_ == nullptr) { 84 | InstrumentationService::Options o; 85 | o.server.allowed_hosts_regex = cfg_->c().allowed_hosts_regex; 86 | o.server.port = cfg_->c().port; 87 | o.server.max_connections = cfg_->c().max_connections; 88 | service_ = is_context::make_is(o); 89 | } 90 | } 91 | 92 | ra2yrcpp::config::Config& Main::config() { return *cfg_; } 93 | -------------------------------------------------------------------------------- /src/ra2yrcpp.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.hpp" 3 | #include "instrumentation_service.hpp" 4 | 5 | #include 6 | #include 7 | 8 | namespace ra2yrcpp { 9 | 10 | // Global ra2yrcpp instance 11 | struct Main { 12 | ra2yrcpp::InstrumentationService* service_; 13 | std::map hooks_; 14 | std::unique_ptr cfg_; 15 | void create_all_hooks(); 16 | void create_all_hooks(char* hooks_section, std::size_t section_size, 17 | void* dll_handle); 18 | void create_hook(hook::HookEntry h, hook::hook_fn f); 19 | void load_configuration(const char* config_path); 20 | void start_service(); 21 | ra2yrcpp::config::Config& config(); 22 | 23 | // Get global instance 24 | static Main* get(); 25 | }; 26 | 27 | } // namespace ra2yrcpp 28 | -------------------------------------------------------------------------------- /src/ra2yrcppcli/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_dependencies(ra2yrcppcli yrclient) 2 | 3 | add_executable(ra2yrcppcli-bin main.cpp) 4 | set(LINK_LIBS "") 5 | if(MINGW) 6 | set(LINK_LIBS 7 | "-static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive" 8 | ) 9 | endif() 10 | target_link_libraries(ra2yrcppcli-bin PUBLIC yrclient "${LINK_LIBS}") 11 | set_target_properties(ra2yrcppcli-bin PROPERTIES OUTPUT_NAME ra2yrcppcli) 12 | 13 | install(TARGETS ra2yrcppcli-bin RUNTIME) 14 | -------------------------------------------------------------------------------- /src/ra2yrcppcli/ra2yrcppcli.cpp: -------------------------------------------------------------------------------- 1 | #include "ra2yrcppcli/ra2yrcppcli.hpp" 2 | 3 | #include "multi_client.hpp" 4 | #include "protocol/helpers.hpp" 5 | 6 | #include 7 | 8 | ra2yrproto::Response ra2yrcppcli::send_command(multi_client::AutoPollClient* A, 9 | std::string name, 10 | std::string args) { 11 | ra2yrcpp::protocol::MessageBuilder B(name); 12 | auto* msg = ra2yrcpp::protocol::create_command_message(&B, args); 13 | return A->send_command(*msg); 14 | } 15 | -------------------------------------------------------------------------------- /src/ra2yrcppcli/ra2yrcppcli.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ra2yrproto/core.pb.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace multi_client { 11 | class AutoPollClient; 12 | } 13 | 14 | namespace ra2yrcppcli { 15 | 16 | constexpr std::array INIT_COMMANDS{ 17 | "ra2yrproto.commands.CreateHooks", "ra2yrproto.commands.CreateCallbacks"}; 18 | 19 | ra2yrproto::Response send_command(multi_client::AutoPollClient* A, 20 | std::string name, 21 | const std::string args = ""); 22 | 23 | std::map parse_kwargs( 24 | std::vector tokens); 25 | 26 | }; // namespace ra2yrcppcli 27 | -------------------------------------------------------------------------------- /src/ring_buffer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace ring_buffer { 9 | 10 | template 11 | class RingBuffer { 12 | public: 13 | explicit RingBuffer(size_t max_size = -1u) : max_size_(max_size) {} 14 | 15 | void push(T t) { emplace(std::move(t)); } 16 | 17 | void emplace(T&& t) { 18 | if (size() >= max_size_) { 19 | q_.erase(q_.begin()); 20 | } 21 | q_.emplace_back(std::move(t)); 22 | } 23 | 24 | void pop() { q_.pop_back(); } 25 | 26 | T& front() { return q_.back(); } 27 | 28 | std::size_t size() const { return q_.size(); } 29 | 30 | private: 31 | std::vector q_; 32 | std::size_t max_size_; 33 | }; 34 | } // namespace ring_buffer 35 | -------------------------------------------------------------------------------- /src/types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #define X(s) typedef uint##s##_t u##s 8 | X(8); 9 | X(16); 10 | X(32); 11 | X(64); 12 | #undef X 13 | #define X(s) typedef int##s##_t i##s 14 | X(8); 15 | X(16); 16 | X(32); 17 | X(64); 18 | #undef X 19 | 20 | typedef std::uintptr_t addr_t; 21 | 22 | union X86Regs { 23 | u32 regs[9]; 24 | 25 | struct { 26 | u32 eflags, edi, esi, ebp, esp, ebx, edx, ecx, eax; 27 | }; 28 | }; 29 | 30 | enum class x86Reg : int { 31 | eax = 0, 32 | ebx = 1, 33 | ecx = 2, 34 | edx = 3, 35 | esi = 4, 36 | edi = 5, 37 | ebp = 6, 38 | esp = 7, 39 | eip = 8 40 | }; 41 | 42 | using vecu8 = std::vector; 43 | using duration_t = std::chrono::duration; 44 | -------------------------------------------------------------------------------- /src/util_string.cpp: -------------------------------------------------------------------------------- 1 | #include "util_string.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | std::vector ra2yrcpp::split_string(const std::string& str, 8 | std::string delim) { 9 | std::vector tokens; 10 | std::regex re(delim); 11 | std::sregex_token_iterator first{str.begin(), str.end(), re, -1}, last; 12 | for (; first != last; ++first) { 13 | tokens.push_back(*first); 14 | } 15 | return tokens; 16 | } 17 | -------------------------------------------------------------------------------- /src/util_string.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "types.h" 3 | 4 | #include 5 | #include 6 | 7 | namespace ra2yrcpp { 8 | inline vecu8 to_bytes(std::string msg) { return vecu8(msg.begin(), msg.end()); } 9 | 10 | inline std::string to_string(vecu8 bytes) { 11 | return std::string(bytes.begin(), bytes.end()); 12 | } 13 | 14 | // split string by delimiter regex and return vector of strings 15 | std::vector split_string(const std::string& s, 16 | std::string delim = "[\\s]+"); 17 | 18 | } // namespace ra2yrcpp 19 | -------------------------------------------------------------------------------- /src/utility.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace ra2yrcpp { 6 | /// Returns true if @value is in @container. Otherwise false. 7 | template 8 | inline bool contains(const T& container, const V& value) { 9 | return std::find(container.begin(), container.end(), value) != 10 | container.end(); 11 | } 12 | 13 | /// Returns true if key @value is in the map @m. Otherwise false. 14 | template 15 | inline bool contains(const std::map& m, const V& value) { 16 | return m.find(value) != m.end(); 17 | } 18 | 19 | } // namespace ra2yrcpp 20 | -------------------------------------------------------------------------------- /src/utility/array_iterator.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace utility { 5 | template 6 | struct ArrayIterator { 7 | struct Iterator { 8 | using iterator_category = std::forward_iterator_tag; 9 | using difference_type = std::ptrdiff_t; 10 | using value_type = T; 11 | using pointer = value_type*; 12 | using reference = value_type&; 13 | 14 | explicit Iterator(T* ptr) : ptr_(ptr) {} 15 | 16 | reference operator*() const { return *ptr_; } 17 | 18 | pointer operator->() { return ptr_; } 19 | 20 | // Prefix increment 21 | Iterator& operator++() { 22 | ++ptr_; 23 | return *this; 24 | } 25 | 26 | // Postfix increment 27 | Iterator operator++(int) { 28 | Iterator tmp = *this; 29 | ++(*this); 30 | return tmp; 31 | } 32 | 33 | friend bool operator==(const Iterator& a, const Iterator& b) { 34 | return a.ptr_ == b.ptr_; 35 | } 36 | 37 | friend bool operator!=(const Iterator& a, const Iterator& b) { 38 | return a.ptr_ != b.ptr_; 39 | } 40 | 41 | private: 42 | T* ptr_; 43 | }; 44 | 45 | explicit ArrayIterator(T* begin, std::size_t count) 46 | : v(begin), count(count) {} 47 | 48 | Iterator begin() { return Iterator(&v[0]); } 49 | 50 | Iterator end() { return Iterator(&v[count]); } 51 | 52 | T* v; 53 | std::size_t count; 54 | }; 55 | } // namespace utility 56 | -------------------------------------------------------------------------------- /src/utility/function_traits.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // Adapted from: 5 | // https://devblogs.microsoft.com/oldnewthing/20200713-00/?p=103978 6 | 7 | namespace utility { 8 | 9 | #define CC_CDECL __cdecl 10 | #define CC_STDCALL __stdcall 11 | 12 | struct FunctionCC { 13 | using Cdecl = void(CC_CDECL*)(); // NOLINT 14 | using Stdcall = void(CC_STDCALL*)(); // NOLINT 15 | }; 16 | 17 | template 18 | struct FunctionTraits; 19 | 20 | #define MAKE_TRAIT(CC, name) \ 21 | template \ 22 | struct FunctionTraits { \ 23 | using RetType = T; \ 24 | using ArgsT = std::tuple; \ 25 | using Pointer = T(CC*)(Args...); \ 26 | using CallingConvention = FunctionCC::name; \ 27 | static constexpr auto NumArgs = std::tuple_size_v; \ 28 | } 29 | 30 | MAKE_TRAIT(CC_CDECL, Cdecl); 31 | MAKE_TRAIT(CC_STDCALL, Stdcall); 32 | 33 | #undef MAKE_TRAIT 34 | #undef CC_STDCALL 35 | #undef CC_CDECL 36 | 37 | } // namespace utility 38 | -------------------------------------------------------------------------------- /src/utility/scope_guard.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | // Adapted from 6 | // https://stackoverflow.com/questions/10270328/the-simplest-and-neatest-c11-scopeguard 7 | namespace utility { 8 | class scope_guard { 9 | private: 10 | std::function _f; 11 | 12 | public: 13 | template 14 | explicit scope_guard(FnT&& f) : _f(std::forward(f)) {} 15 | 16 | // cppcheck-suppress useInitializationList 17 | scope_guard(scope_guard&& o) : _f(std::move(o._f)) { o._f = nullptr; } 18 | 19 | ~scope_guard() { 20 | if (_f) { 21 | _f(); 22 | } 23 | } 24 | 25 | void operator=(const scope_guard&) = delete; 26 | }; 27 | } // namespace utility 28 | -------------------------------------------------------------------------------- /src/utility/serialize.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | namespace serialize { 8 | 9 | using u8 = std::uint8_t; 10 | 11 | template 12 | auto read_obj(const U* addr) { 13 | T t; 14 | std::memset(&t, 0, sizeof(t)); 15 | auto a = reinterpret_cast(addr); 16 | auto b = reinterpret_cast(&t); 17 | std::copy(a, a + sizeof(T), b); 18 | return t; 19 | } 20 | 21 | template 22 | auto read_obj(std::uintptr_t addr) { 23 | return read_obj(reinterpret_cast(addr)); 24 | } 25 | 26 | template 27 | auto read_obj_le(const U* addr) { 28 | T t{0}; 29 | auto* b = reinterpret_cast(&t); 30 | auto* a = reinterpret_cast(addr); 31 | std::copy_backward(a, a + sizeof(T), b + sizeof(T)); 32 | return t; 33 | } 34 | 35 | template 36 | auto read_obj_le(std::uintptr_t addr) { 37 | return read_obj_le(reinterpret_cast(addr)); 38 | } 39 | 40 | inline bool bytes_equal(const void* p1, const void* p2, unsigned size) { 41 | #if 0 42 | for (auto i = 0U; i < N; i++) { 43 | if (reinterpret_cast(p1)[i] != 44 | reinterpret_cast(p2)[i]) { 45 | return false; 46 | } 47 | } 48 | return true; 49 | #else 50 | return std::memcmp(p1, p2, size) == 0; 51 | #endif 52 | } 53 | 54 | template 55 | bool bytes_equal(const T* p1, const T* p2) { 56 | return bytes_equal(p1, p2, sizeof(T)); 57 | } 58 | 59 | } // namespace serialize 60 | -------------------------------------------------------------------------------- /src/utility/sync.cpp: -------------------------------------------------------------------------------- 1 | #include "utility/sync.hpp" 2 | -------------------------------------------------------------------------------- /src/utility/sync.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "types.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace util { 11 | 12 | namespace { 13 | using namespace std::chrono_literals; 14 | } 15 | 16 | template 17 | class AcquireData { 18 | public: 19 | using lock_t = std::unique_lock; 20 | using data_t = std::tuple; 21 | 22 | AcquireData(lock_t&& l, T* data) 23 | : data_(std::make_tuple(std::forward(l), data)) {} 24 | 25 | AcquireData(T* data, MutexT* m) : AcquireData(lock_t(*m), data) {} 26 | 27 | AcquireData(T* data, MutexT* m, duration_t timeout) 28 | : AcquireData(lock_t(*m, timeout), data) {} 29 | 30 | ~AcquireData() {} 31 | 32 | void unlock() { std::get<0>(data()).unlock(); } 33 | 34 | // Move constructor 35 | AcquireData(AcquireData&& other) noexcept : data_(std::move(other.data_)) {} 36 | 37 | // Move assignment operator 38 | AcquireData& operator=(AcquireData&& other) noexcept { 39 | if (this != &other) { 40 | data_ = std::move(other.data_); 41 | } 42 | return *this; 43 | } 44 | 45 | AcquireData(const AcquireData&) = delete; 46 | AcquireData& operator=(const AcquireData&) = delete; 47 | 48 | data_t& data() { return data_; } 49 | 50 | private: 51 | data_t data_; 52 | }; 53 | 54 | template 55 | using acquire_t = std::tuple, T*>; 56 | 57 | template 58 | static acquire_t acquire(T* data, MutexT* mut, Args... args) { 59 | return std::make_tuple(std::move(AcquireData(data, mut, args...)), data); 60 | } 61 | 62 | template 63 | class AtomicVariable { 64 | public: 65 | explicit AtomicVariable(T value) : v_(value) {} 66 | 67 | void wait(T value, duration_t timeout = 0.0s) { 68 | wait_pred([value](auto v) { return v == value; }, timeout); 69 | } 70 | 71 | template 72 | void wait_pred(PredT p, duration_t timeout = 0.0s) { 73 | std::unique_lock l(m_); 74 | if (timeout > 0.0s) { 75 | cv_.wait_for(l, timeout, [this, p]() { return p(v_); }); 76 | } else { 77 | cv_.wait(l, [this, p]() { return p(v_); }); 78 | } 79 | } 80 | 81 | void store(T v) { 82 | std::unique_lock l(m_); 83 | v_ = v; 84 | cv_.notify_all(); 85 | } 86 | 87 | T get() { 88 | std::unique_lock l(m_); 89 | return v_; 90 | } 91 | 92 | auto acquire() { return util::acquire(&v_, &m_); } 93 | 94 | private: 95 | T v_; 96 | MutexT m_; 97 | std::condition_variable cv_; 98 | }; 99 | 100 | } // namespace util 101 | -------------------------------------------------------------------------------- /src/utility/time.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "types.h" 3 | 4 | #include 5 | #include 6 | 7 | namespace util { 8 | inline std::chrono::system_clock::time_point current_time() { 9 | return std::chrono::system_clock::now(); 10 | } 11 | 12 | template 13 | inline void sleep_ms(T s) { 14 | std::this_thread::sleep_for(std::chrono::milliseconds(s)); 15 | } 16 | 17 | template 18 | inline void sleep_ms(std::chrono::duration s) { 19 | std::this_thread::sleep_for(std::chrono::duration_cast(s)); 20 | } 21 | 22 | // Periodically call fn until it returns true or timeout occurs 23 | template 24 | inline void call_until(duration_t timeout, duration_t rate, Pred fn) { 25 | auto deadline = util::current_time() + timeout; 26 | while (fn() && util::current_time() < deadline) { 27 | util::sleep_ms(rate); 28 | } 29 | } 30 | 31 | } // namespace util 32 | -------------------------------------------------------------------------------- /src/version.rc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define HARD_VERSION 0,0,1,0 5 | #define SOFT_VERSION 0.01 6 | #define YEAR 2023 7 | #define ABBREVIATION libra2yrcpp 8 | 9 | #define xstr(s) str(s) 10 | #define str(s) #s 11 | 12 | #define PRODUCT_NAME ra2yrcpp main DLL 13 | 14 | 1 VERSIONINFO 15 | FILEVERSION HARD_VERSION 16 | PRODUCTVERSION HARD_VERSION 17 | FILEOS 0x4 18 | FILETYPE VFT_DLL 19 | { 20 | BLOCK "StringFileInfo" 21 | { 22 | BLOCK "040904B0" 23 | { 24 | VALUE "Comment", "" 25 | VALUE "CompanyName", "https://github.com/shmocz/ra2yrcpp" 26 | VALUE "FileDescription", xstr(PRODUCT_NAME) 27 | VALUE "FileVersion", xstr(RA2YRCPP_VERSION) 28 | VALUE "InternalName", xstr(ABBREVIATION) 29 | VALUE "LegalCopyright", xstr(Copyright (c) YEAR shmocz) 30 | VALUE "LegalTrademarks", "Released under GPLv3" 31 | VALUE "OriginalFilename", "libra2yrcpp.dll" 32 | VALUE "PrivateBuild", "" 33 | VALUE "ProductName", xstr(PRODUCT_NAME) 34 | VALUE "ProductVersion", xstr(RA2YRCPP_VERSION) 35 | VALUE "SpecialBuild", "" 36 | } 37 | } 38 | 39 | BLOCK "VarFileInfo" 40 | { 41 | VALUE "Translation", 0x0409, 0x04B0 42 | } 43 | } 44 | 45 | LANGUAGE 9, SUBLANG_SYS_DEFAULT -------------------------------------------------------------------------------- /src/websocket_connection.cpp: -------------------------------------------------------------------------------- 1 | #include "websocket_connection.hpp" 2 | 3 | #include "asio_utils.hpp" 4 | #include "client_connection.hpp" 5 | #include "constants.hpp" 6 | #include "logging.hpp" 7 | #include "utility/sync.hpp" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | using namespace ra2yrcpp::connection; 20 | using namespace std::chrono_literals; 21 | 22 | using ws_error = websocketpp::lib::error_code; 23 | 24 | class ClientWebsocketConnection::client_impl 25 | : public websocketpp::client {}; 26 | 27 | void ClientWebsocketConnection::stop() { 28 | in_q_.push(std::make_shared()); 29 | } 30 | 31 | void ClientWebsocketConnection::connect() { 32 | state_.store(State::CONNECTING); 33 | ws_error ec; 34 | auto* c_ = client_.get(); 35 | 36 | #ifdef DEBUG_WEBSOCKETPP 37 | c_->set_access_channels(websocketpp::log::alevel::all); 38 | c_->set_error_channels(websocketpp::log::elevel::all); 39 | #endif 40 | 41 | c_->init_asio(reinterpret_cast(io_service_->get_service()), 42 | ec); 43 | 44 | if (ec) { 45 | throw std::runtime_error("init_asio() failed: " + ec.message()); 46 | } 47 | 48 | c_->clear_access_channels(websocketpp::log::alevel::frame_payload | 49 | websocketpp::log::alevel::frame_header); 50 | c_->set_fail_handler([this, c_](auto) { 51 | stop(); 52 | state_.store(State::CLOSED); 53 | }); 54 | 55 | c_->set_message_handler([this](auto, auto msg) { 56 | try { 57 | auto p = msg->get_payload(); 58 | in_q_.push(std::make_shared(p.begin(), p.end())); 59 | } catch (websocketpp::exception const& e) { 60 | eprintf("message_handler: {}", e.what()); 61 | } 62 | }); 63 | c_->set_open_handler([this](auto h) { 64 | connection_handle_ = h; 65 | state_.store(State::OPEN); 66 | }); 67 | 68 | c_->set_close_handler([this](auto) { state_.store(State::CLOSED); }); 69 | 70 | auto con = c_->get_connection(std::string("ws://" + host + ":" + port), ec); 71 | 72 | if (ec) { 73 | throw std::runtime_error(ec.message()); 74 | } 75 | 76 | c_->connect(con); 77 | state_.wait_pred([](auto state) { 78 | return state == State::OPEN || state == State::CLOSED; 79 | }); 80 | if (state().get() != State::OPEN) { 81 | throw std::runtime_error("failed to open connection, state=" + 82 | std::to_string(static_cast(state().get()))); 83 | } 84 | } 85 | 86 | void ClientWebsocketConnection::send_data(const vecu8& bytes) { 87 | ws_error ec; 88 | io_service_->post([this, &bytes, &ec]() { 89 | auto* c_ = client_.get(); 90 | c_->send(connection_handle_, bytes.data(), bytes.size(), 91 | websocketpp::frame::opcode::binary, ec); 92 | }); 93 | if (ec) { 94 | throw std::runtime_error( 95 | fmt::format("failed to send data: {}", ec.message())); 96 | } 97 | } 98 | 99 | vecu8 ClientWebsocketConnection::read_data() { 100 | try { 101 | auto res = in_q_.pop( 102 | 1, std::chrono::duration_cast(cfg::WEBSOCKET_READ_TIMEOUT)); 103 | return *res.at(0); 104 | } catch (const std::exception& e) { 105 | throw std::runtime_error( 106 | std::string("failed to read data (likely connection closed): ") + 107 | e.what()); 108 | } 109 | } 110 | 111 | ClientWebsocketConnection::ClientWebsocketConnection( 112 | std::string host, std::string port, 113 | ra2yrcpp::asio_utils::IOService* io_service) 114 | : ClientConnection(host, port), 115 | client_(std::make_unique()), 116 | io_service_(io_service) {} 117 | 118 | ClientWebsocketConnection::~ClientWebsocketConnection() { 119 | try { 120 | client_->close(connection_handle_, websocketpp::close::status::going_away, 121 | ""); 122 | iprintf("websocket connection closed"); 123 | } catch (const std::exception& e) { 124 | wrprintf("failed to close gracefully: {}", e.what()); 125 | } 126 | state_.wait(State::CLOSED); 127 | } 128 | -------------------------------------------------------------------------------- /src/websocket_connection.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "async_queue.hpp" 3 | #include "client_connection.hpp" 4 | #include "types.h" 5 | 6 | #include 7 | #include 8 | 9 | namespace ra2yrcpp { 10 | namespace asio_utils { 11 | class IOService; 12 | } 13 | } // namespace ra2yrcpp 14 | 15 | namespace ra2yrcpp::connection { 16 | class ClientWebsocketConnection : public ClientConnection { 17 | public: 18 | ClientWebsocketConnection(std::string host, std::string port, 19 | ra2yrcpp::asio_utils::IOService* io_service); 20 | using item_t = std::shared_ptr; 21 | ~ClientWebsocketConnection() override; 22 | void connect() override; 23 | void send_data(const vecu8& bytes) override; 24 | vecu8 read_data() override; 25 | /// Puts empty vector to input message queue to signal the reader that 26 | /// connection is closed. 27 | void stop() override; 28 | class client_impl; 29 | 30 | private: 31 | async_queue::AsyncQueue in_q_; 32 | std::unique_ptr client_; 33 | ra2yrcpp::asio_utils::IOService* io_service_; 34 | std::weak_ptr connection_handle_; 35 | }; 36 | } // namespace ra2yrcpp::connection 37 | -------------------------------------------------------------------------------- /src/websocket_server.cpp: -------------------------------------------------------------------------------- 1 | #include "websocket_server.hpp" 2 | 3 | #include "asio_utils.hpp" 4 | #include "logging.hpp" 5 | #include "utility/time.hpp" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | using namespace ra2yrcpp::websocket_server; 23 | using namespace ra2yrcpp::asio_utils; 24 | namespace lib = websocketpp::lib; 25 | using server = websocketpp::server; 26 | 27 | class WebsocketServer::server_impl : public server { 28 | public: 29 | /// This is so common operation, hence a dedicated method. 30 | auto get_socket_id(connection_hdl h) { 31 | return get_con_from_hdl(h)->get_socket().native_handle(); 32 | } 33 | }; 34 | 35 | WebsocketServer::~WebsocketServer() {} 36 | 37 | WSReply::WSReply() {} 38 | 39 | struct WSReplyImpl : public WSReply { 40 | explicit WSReplyImpl(server::message_ptr msg) : msg(msg) {} 41 | 42 | const std::string& get_payload() const override { return msg->get_payload(); } 43 | 44 | int get_opcode() const override { return msg->get_opcode(); } 45 | 46 | server::message_ptr msg; 47 | }; 48 | 49 | WebsocketServer::WebsocketServer(WebsocketServer::Options o, 50 | ra2yrcpp::asio_utils::IOService* service, 51 | Callbacks cb) 52 | : opts(o), 53 | service_(service), 54 | cb_(cb), 55 | server_(std::make_unique()) { 56 | auto& s = *server_.get(); 57 | s.set_message_handler([&](connection_hdl h, auto msg) { 58 | if (msg->get_opcode() == websocketpp::frame::opcode::text) { 59 | eprintf("got text message, expecting binary"); 60 | return; 61 | } 62 | try { 63 | WSReplyImpl R(msg); 64 | send_response(h, &R); 65 | } catch (const std::exception& e) { 66 | eprintf("error while handling message: {}", e.what()); 67 | } 68 | }); 69 | 70 | // Set TCP_NODELAY flag 71 | s.set_socket_init_handler([](auto, auto& socket) { 72 | socket.set_option(asio::ip::tcp::no_delay(true)); 73 | }); 74 | 75 | s.set_tcp_pre_init_handler([&](connection_hdl h) { 76 | auto con = s.get_con_from_hdl(h); 77 | std::smatch match; 78 | auto remote = con->get_raw_socket().remote_endpoint().address().to_string(); 79 | if (!std::regex_search(remote, match, 80 | std::regex(opts.allowed_hosts_regex))) { 81 | iprintf("address {} doesn't match pattern \"{}\", rejecting.", remote, 82 | opts.allowed_hosts_regex); 83 | return; 84 | } 85 | 86 | iprintf("connection from {}", remote); 87 | }); 88 | 89 | s.set_validate_handler([&](connection_hdl) { 90 | try { 91 | if (ws_conns.size() >= opts.max_connections) { 92 | eprintf("max connections {} exceeded", ws_conns.size()); 93 | return false; 94 | } 95 | } catch (const std::exception& e) { 96 | eprintf("validate_handler: {}", e.what()); 97 | return false; 98 | } 99 | 100 | return true; 101 | }); 102 | 103 | s.set_close_handler([&](connection_hdl h) { 104 | try { 105 | const unsigned int socket_id = s.get_socket_id(h); 106 | cb_.close(socket_id); 107 | (void)ws_conns.erase(socket_id); 108 | iprintf("closed conn {}", socket_id); 109 | } catch (const std::exception& e) { 110 | eprintf("close_handler: {}", e.what()); 111 | } 112 | }); 113 | 114 | // TODO(shmocz): thread safety 115 | s.set_open_handler([&](connection_hdl h) { 116 | try { 117 | add_connection(h); 118 | cb_.accept(s.get_socket_id(h)); 119 | } catch (const std::exception& e) { 120 | eprintf("open_handler: {}", e.what()); 121 | } 122 | }); 123 | 124 | // TODO(shmocz): thread safety 125 | s.set_interrupt_handler([&](connection_hdl h) { 126 | std::size_t count = 0; 127 | if ((count = ws_conns.erase(s.get_socket_id(h))) < 1) { 128 | wrprintf("got interrupt, but no connections were removed"); 129 | } 130 | }); 131 | 132 | // HTTP handler for use with CURL etc. 133 | s.set_http_handler([&](connection_hdl h) { 134 | auto con = s.get_con_from_hdl(h); 135 | const unsigned int id = s.get_socket_id(h); 136 | if (ws_conns.find(id) != ws_conns.end()) { 137 | eprintf("duplicate connection {}", id); 138 | return; 139 | } 140 | add_connection(h); 141 | cb_.accept(id); 142 | 143 | ws_conns[id].buffer = con->get_request_body(); 144 | con->defer_http_response(); 145 | ws_conns[id].executor->push(0, [this, con, id](int) { 146 | auto resp = cb_.receive(id, &ws_conns[id].buffer); 147 | service_->post( 148 | [this, con, resp, id]() { 149 | try { 150 | con->set_body(resp); 151 | con->set_status(websocketpp::http::status_code::ok); 152 | con->send_http_response(); 153 | cb_.close(id); 154 | } catch (const std::exception& e) { 155 | eprintf("couldn't send http response: {}", e.what()); 156 | } 157 | con->interrupt(); 158 | }, 159 | true); 160 | }); 161 | }); 162 | 163 | s.clear_access_channels(websocketpp::log::alevel::frame_payload | 164 | websocketpp::log::alevel::frame_header); 165 | 166 | #ifdef DEBUG_WEBSOCKETPP 167 | s.set_access_channels(websocketpp::log::alevel::all); 168 | s.set_error_channels(websocketpp::log::elevel::all); 169 | #endif 170 | } 171 | 172 | // TODO(shmocz): thread safety 173 | void WebsocketServer::shutdown() { 174 | if (server_->is_listening()) { 175 | server_->stop_listening(); 176 | } 177 | 178 | // Close WebSocket connections 179 | service_->post([&]() { 180 | for (auto& [k, v] : ws_conns) { 181 | auto cptr = server_->get_con_from_hdl(v.hdl); 182 | if (cptr->get_state() == websocketpp::session::state::open) { 183 | server_->close(v.hdl, websocketpp::close::status::normal, ""); 184 | } 185 | } 186 | ws_conns.clear(); 187 | }); 188 | } 189 | 190 | void WebsocketServer::send_response(connection_hdl h, WSReply* msg) { 191 | auto op = static_cast(msg->get_opcode()); 192 | auto id = server_->get_socket_id(h); 193 | 194 | ws_conns[id].buffer = msg->get_payload(); 195 | 196 | ws_conns[id].executor->push(0, [this, op, id](int) { 197 | auto& c = ws_conns[id]; 198 | auto resp = cb_.receive(id, &c.buffer); 199 | service_->post( 200 | [this, &c, resp, op]() { 201 | try { 202 | server_->send(c.hdl, resp, op); 203 | } catch (...) { 204 | eprintf("failed to send"); 205 | } 206 | }, 207 | true); 208 | }); 209 | } 210 | 211 | void WebsocketServer::start() { 212 | dprintf("init asio, port={}", opts.port); 213 | server_->init_asio( 214 | static_cast(service_->get_service())); 215 | server_->set_reuse_addr(true); 216 | server_->listen(lib::asio::ip::tcp::v4(), opts.port); 217 | server_->start_accept(); 218 | } 219 | 220 | void WebsocketServer::add_connection(connection_hdl h) { 221 | ws_conns[server_->get_socket_id(h)] = { 222 | h, util::current_time(), "", 223 | std::make_unique>(nullptr, 5)}; 224 | } 225 | 226 | std::unique_ptr ra2yrcpp::websocket_server::create_server( 227 | WebsocketServer::Options o, ra2yrcpp::asio_utils::IOService* service, 228 | WebsocketServer::Callbacks cb) { 229 | return std::make_unique(o, service, cb); 230 | } 231 | -------------------------------------------------------------------------------- /src/websocket_server.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "auto_thread.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace ra2yrcpp { 11 | namespace asio_utils { 12 | class IOService; 13 | } 14 | } // namespace ra2yrcpp 15 | 16 | namespace ra2yrcpp { 17 | 18 | namespace websocket_server { 19 | 20 | using connection_hdl = std::weak_ptr; 21 | 22 | /// This abstracts away websocketpp's message_ptr 23 | struct WSReply { 24 | WSReply(); 25 | virtual ~WSReply() = default; 26 | virtual const std::string& get_payload() const = 0; 27 | virtual int get_opcode() const = 0; 28 | }; 29 | 30 | struct SocketEntry { 31 | connection_hdl hdl; 32 | std::chrono::system_clock::time_point timestamp; 33 | std::string buffer; 34 | std::unique_ptr> executor; 35 | }; 36 | 37 | class WebsocketServer { 38 | public: 39 | using socket_t = int; 40 | 41 | struct Options { 42 | std::string host; 43 | unsigned port; 44 | unsigned max_connections; 45 | std::string allowed_hosts_regex; 46 | }; 47 | 48 | struct Callbacks { 49 | std::function receive; 50 | std::function accept; 51 | std::function close; 52 | }; 53 | 54 | WebsocketServer() = delete; 55 | WebsocketServer(WebsocketServer::Options o, 56 | ra2yrcpp::asio_utils::IOService* service, Callbacks cb); 57 | ~WebsocketServer(); 58 | 59 | void start(); 60 | /// Shutdown all active connections and stop accepting new connections. 61 | void shutdown(); 62 | /// Send a reply for a previously received message. Must be used within 63 | /// io_service's thread. 64 | void send_response(connection_hdl h, WSReply* msg); 65 | 66 | /// Add a recently accepted connection to internal connection list. 67 | void add_connection(connection_hdl h); 68 | 69 | WebsocketServer::Options opts; 70 | std::map ws_conns; 71 | ra2yrcpp::asio_utils::IOService* service_; 72 | Callbacks cb_; 73 | class server_impl; 74 | std::unique_ptr server_; 75 | }; 76 | 77 | std::unique_ptr create_server( 78 | WebsocketServer::Options o, ra2yrcpp::asio_utils::IOService* service, 79 | WebsocketServer::Callbacks cb); 80 | } // namespace websocket_server 81 | 82 | } // namespace ra2yrcpp 83 | -------------------------------------------------------------------------------- /src/win32/win_message.cpp: -------------------------------------------------------------------------------- 1 | #include "win_message.hpp" 2 | 3 | typedef void* HWND; 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | std::string windows_utils::get_error_message(int error_code) { 14 | char* buf = nullptr; 15 | std::size_t size = FormatMessageA( 16 | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | 17 | FORMAT_MESSAGE_IGNORE_INSERTS, 18 | NULL, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&buf, 19 | 0, NULL); 20 | 21 | std::string message(buf, size); 22 | LocalFree(buf); 23 | // Remove \r\n 24 | return message.substr(0, message.find("\r\n")); 25 | } 26 | 27 | unsigned long windows_utils::get_last_error() { return GetLastError(); } 28 | -------------------------------------------------------------------------------- /src/win32/win_message.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace windows_utils { 5 | 6 | std::string get_error_message(int error_code); 7 | unsigned long get_last_error(); 8 | } // namespace windows_utils 9 | -------------------------------------------------------------------------------- /src/win32/windows_debug.cpp: -------------------------------------------------------------------------------- 1 | #include "win32/windows_debug.hpp" 2 | 3 | #include "win32/windows_utils.hpp" 4 | 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | struct NtCall { 14 | HMODULE m; 15 | 16 | explicit NtCall(HMODULE m) : m(m) {} 17 | 18 | template 19 | constexpr auto call(const char* name, Args... args) const { 20 | auto const fn = reinterpret_cast( 21 | windows_utils::get_proc_address(name, m)); 22 | return fn(args...); 23 | } 24 | }; 25 | 26 | // Based on `TryDetachFromDebugger` found in CnCNet `yrpp-spawner` 27 | // cppcheck-suppress-begin cstyleCast 28 | bool windows_utils::debugger_detach() { 29 | auto GetDebuggerProcessId = [](DWORD dwSelfProcessId) -> DWORD { 30 | DWORD dwParentProcessId = -1; 31 | HANDLE hSnapshot = CreateToolhelp32Snapshot(2, 0); 32 | PROCESSENTRY32 pe32; 33 | pe32.dwSize = sizeof(PROCESSENTRY32); 34 | Process32First(hSnapshot, &pe32); 35 | do { 36 | if (pe32.th32ProcessID == dwSelfProcessId) { 37 | dwParentProcessId = pe32.th32ParentProcessID; 38 | break; 39 | } 40 | } while (Process32Next(hSnapshot, &pe32)); 41 | CloseHandle(hSnapshot); 42 | return dwParentProcessId; 43 | }; 44 | 45 | HMODULE hModule = LoadLibrary("ntdll.dll"); 46 | auto const C = NtCall(hModule); 47 | if (hModule != nullptr) { 48 | HANDLE hDebug; 49 | HANDLE hCurrentProcess = GetCurrentProcess(); 50 | NTSTATUS status = C.call("NtQueryInformationProcess", hCurrentProcess, 51 | (PROCESSINFOCLASS)30, &hDebug, sizeof(HANDLE), 0); 52 | if (0 <= status) { 53 | ULONG killProcessOnExit = FALSE; 54 | status = C.call("NtSetInformationDebugObject", hDebug, 1, 55 | &killProcessOnExit, sizeof(ULONG), NULL); 56 | if (0 <= status) { 57 | const auto pid = GetDebuggerProcessId(GetProcessId(hCurrentProcess)); 58 | status = C.call("NtRemoveProcessDebug", hCurrentProcess, hDebug); 59 | if (0 <= status) { 60 | HANDLE hDbgProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); 61 | if (INVALID_HANDLE_VALUE != hDbgProcess) { 62 | BOOL ret = TerminateProcess(hDbgProcess, EXIT_SUCCESS); 63 | CloseHandle(hDbgProcess); 64 | return ret; 65 | } 66 | } 67 | } 68 | C.call("NtClose", hDebug); 69 | } 70 | FreeLibrary(hModule); 71 | } 72 | 73 | return false; 74 | } 75 | 76 | // cppcheck-suppress-end cstyleCast 77 | -------------------------------------------------------------------------------- /src/win32/windows_debug.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace windows_utils { 4 | 5 | bool debugger_detach(); 6 | } 7 | -------------------------------------------------------------------------------- /src/win32/windows_utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "types.h" 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace windows_utils { 14 | 15 | struct ThreadEntry { 16 | unsigned long owner_process_id; 17 | unsigned long thread_id; 18 | }; 19 | 20 | struct ThreadContext { 21 | void* handle; 22 | bool acquired; 23 | std::size_t size; 24 | void* data; 25 | ThreadContext(); 26 | explicit ThreadContext(void* handle); 27 | ~ThreadContext(); 28 | int save(); 29 | int* get_pgpr(const x86Reg); 30 | }; 31 | 32 | class ExProcess { 33 | public: 34 | struct Opts { 35 | std::string cmdline_; 36 | std::string directory_; 37 | }; 38 | 39 | explicit ExProcess(std::string cmdline, std::string directory = ""); 40 | void* handle(); 41 | void join(); 42 | const Opts& opts() const; 43 | ~ExProcess(); 44 | 45 | private: 46 | Opts opt_; 47 | struct ProcessContext; 48 | std::unique_ptr ctx; 49 | }; 50 | 51 | struct ImageSection { 52 | void* data; 53 | size_t length; 54 | }; 55 | 56 | void* load_library(std::string name); 57 | std::uintptr_t get_proc_address(std::string addr, void* module = nullptr); 58 | std::string get_process_name(int pid); 59 | void* open_thread(unsigned long access, bool inherit_handle, 60 | unsigned long thread_id); 61 | void* allocate_memory(void* handle, std::size_t size, unsigned long alloc_type, 62 | unsigned long alloc_protect); 63 | void* allocate_code(void* handle, std::size_t size); 64 | std::vector enum_processes(); 65 | std::string getcwd(); 66 | std::vector list_loaded_modules(void* const handle); 67 | unsigned long suspend_thread(void* handle); 68 | void* get_current_process_handle(); 69 | int get_current_tid(); 70 | unsigned long resume_thread(void* handle); 71 | void* open_process(unsigned long access, bool inherit, unsigned long pid); 72 | int read_memory(void* handle, void* dest, const void* src, std::size_t size); 73 | int close_handle(void* handle); 74 | int write_memory(void* handle, void* dest, const void* src, std::size_t size); 75 | void write_memory_local(void* dest, const void* src, std::size_t size); 76 | unsigned long get_pid(void* handle); 77 | void for_each_thread(std::function callback); 78 | // Find DLL by name (e.g. KERNEL32.DLL) that's loaded by current process and 79 | // return a handle to it. If the DLL is not found, return NULL. 80 | void* find_dll(std::string name); 81 | ImageSection find_section(void* handle, std::string name); 82 | } // namespace windows_utils 83 | -------------------------------------------------------------------------------- /src/x86.cpp: -------------------------------------------------------------------------------- 1 | #include "x86.hpp" 2 | 3 | #include "types.h" 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | using namespace x86; 12 | 13 | std::size_t x86::bytes_to_stack(Xbyak::CodeGenerator* c, vecu8 bytes) { 14 | using namespace Xbyak::util; 15 | int off = sizeof(u32); 16 | std::size_t s = 0u; 17 | auto it = bytes.begin(); 18 | 19 | std::vector chunks; 20 | for (std::size_t i = 0; i < bytes.size(); i += off) { 21 | u32 dw{0}; 22 | std::copy(it + i, it + std::min(bytes.size(), i + off), 23 | reinterpret_cast(&dw)); 24 | chunks.push_back(dw); 25 | } 26 | // push dw:s in reverse order 27 | for (auto i2 = chunks.rbegin(); i2 != chunks.rend(); i2++) { 28 | c->push(*i2); 29 | s += off; 30 | } 31 | return s; 32 | } 33 | 34 | void x86::restore_regs(Xbyak::CodeGenerator* c) { 35 | #ifdef XBYAK32 36 | c->popfd(); 37 | c->popad(); 38 | #else 39 | c->popfq(); 40 | #endif 41 | } 42 | 43 | void x86::save_regs(Xbyak::CodeGenerator* c) { 44 | #ifdef XBYAK32 45 | c->pushad(); 46 | c->pushfd(); 47 | #else 48 | c->pushfq(); 49 | #endif 50 | } 51 | -------------------------------------------------------------------------------- /src/x86.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "types.h" 3 | 4 | #include 5 | 6 | namespace Xbyak { 7 | class CodeGenerator; 8 | } 9 | 10 | namespace x86 { 11 | 12 | std::size_t bytes_to_stack(Xbyak::CodeGenerator* c, vecu8 bytes); 13 | void restore_regs(Xbyak::CodeGenerator* c); 14 | void save_regs(Xbyak::CodeGenerator* c); 15 | 16 | }; // namespace x86 17 | -------------------------------------------------------------------------------- /src/yrclient_dll.cpp: -------------------------------------------------------------------------------- 1 | #include "yrclient_dll.hpp" 2 | 3 | #include "constants.hpp" 4 | #include "instrumentation_service.hpp" 5 | #include "logging.hpp" 6 | #include "ra2yrcpp.hpp" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | void ra2yrcpp::initialize(unsigned int max_clients, unsigned int port, 15 | bool no_init_hooks) { 16 | (void)max_clients; 17 | (void)port; 18 | static std::mutex g_lock; 19 | g_lock.lock(); 20 | auto* I = ra2yrcpp::Main::get(); 21 | if (no_init_hooks) { 22 | I->load_configuration(nullptr); 23 | I->start_service(); 24 | } else { 25 | I->create_all_hooks(); 26 | } 27 | 28 | g_lock.unlock(); 29 | } 30 | 31 | // cppcheck-suppress unusedFunction 32 | void init_iservice(unsigned int max_clients, unsigned int port, 33 | unsigned int no_init_hooks) { 34 | ra2yrcpp::initialize(max_clients, port, no_init_hooks > 0U); 35 | } 36 | 37 | // cppcheck-suppress unusedFunction 38 | int __stdcall DllMain(HANDLE hInstance, DWORD dwReason, LPVOID v) { 39 | (void)hInstance; 40 | (void)v; 41 | if (dwReason == DLL_PROCESS_ATTACH) { 42 | (void)ra2yrcpp::logging::set_output_handle(cfg::LOG_FILE_NAME); 43 | } else if (dwReason == DLL_PROCESS_DETACH) { 44 | ra2yrcpp::logging::close_output_handle(); 45 | } 46 | return 1; 47 | } 48 | -------------------------------------------------------------------------------- /src/yrclient_dll.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ra2yrcpp { 4 | 5 | /// This function should only be called if the library is to be initialized 6 | /// DLLLoader patch instead of Syringe, such in the case of legacy CnCNet 7 | /// spawner. 8 | void initialize(unsigned int max_clients, unsigned int port, 9 | bool no_init_hooks); 10 | } // namespace ra2yrcpp 11 | 12 | extern "C" { 13 | __declspec(dllexport) void __cdecl init_iservice(unsigned int max_clients, 14 | unsigned int port, 15 | unsigned int no_init_hooks); 16 | } 17 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # With MINGW, Tests that don't depend on main DLL link dynamically to all 2 | # standard libraries. 3 | 4 | include_directories(../src) 5 | 6 | set(YRCLIENT_LIB yrclient) 7 | # MinGW exports everything automatically, so we can link to DLL for better 8 | # performance 9 | if(MINGW) 10 | set(YRCLIENT_LIB ra2yrcpp_dll) 11 | endif() 12 | 13 | set(NATIVE_TARGETS "") 14 | 15 | function(new_make_test) 16 | cmake_parse_arguments(MY_FN "" "NAME" "SRC;LIB" ${ARGN}) 17 | add_executable("${MY_FN_NAME}" ${MY_FN_SRC}) 18 | target_link_libraries("${MY_FN_NAME}" PRIVATE gtest_main ${MY_FN_LIB}) 19 | add_test(NAME ${MY_FN_NAME} COMMAND ${MY_FN_NAME}) 20 | install(TARGETS "${MY_FN_NAME}" RUNTIME) 21 | list(APPEND NATIVE_TARGETS "${MY_FN_NAME}") 22 | endfunction() 23 | 24 | if(RA2YRCPP_BUILD_MAIN_DLL) 25 | new_make_test( 26 | NAME test_process 27 | SRC test_process.cpp 28 | LIB ${YRCLIENT_LIB}) 29 | 30 | new_make_test( 31 | NAME test_hooks 32 | SRC test_hooks.cpp 33 | LIB ${YRCLIENT_LIB} fmt::fmt) 34 | 35 | add_executable(dummy_program dummy_program.cpp) 36 | install(TARGETS dummy_program RUNTIME) 37 | 38 | new_make_test( 39 | NAME test_dll_inject 40 | SRC test_dll_inject.cpp 41 | LIB ${YRCLIENT_LIB} fmt::fmt) 42 | endif() 43 | 44 | # Tests that don't depend on the main DLL 45 | add_library(common_multi STATIC common_multi.cpp) 46 | target_link_libraries(common_multi PUBLIC ra2yrcpp_core ZLIB::ZLIB 47 | ${PROTOBUF_EXTRA_LIBS}) 48 | 49 | new_make_test( 50 | NAME test_instrumentation_service 51 | SRC test_instrumentation_service.cpp 52 | LIB ra2yrcpp_core "${PROTO_LIB}" ZLIB::ZLIB ${PROTOBUF_EXTRA_LIBS}) 53 | 54 | new_make_test( 55 | NAME test_multi_client 56 | SRC test_multi_client.cpp 57 | LIB common_multi) 58 | 59 | new_make_test( 60 | NAME test_is_stress_test 61 | SRC test_is_stress_test.cpp 62 | LIB common_multi) 63 | 64 | new_make_test( 65 | NAME test_protocol 66 | SRC test_protocol.cpp 67 | LIB ra2yrcpp_core "${PROTO_LIB}" ZLIB::ZLIB ${PROTOBUF_EXTRA_LIBS}) 68 | 69 | add_library(tests_native INTERFACE ${NATIVE_TARGETS}) 70 | 71 | target_compile_options(tests_native INTERFACE ${RA2YRCPP_EXTRA_FLAGS}) 72 | target_link_options(tests_native INTERFACE ${RA2YRCPP_EXTRA_FLAGS}) 73 | -------------------------------------------------------------------------------- /tests/common_multi.cpp: -------------------------------------------------------------------------------- 1 | #include "common_multi.hpp" 2 | 3 | #include "asio_utils.hpp" 4 | 5 | using namespace ra2yrcpp::tests; 6 | 7 | MultiClientTestContext::MultiClientTestContext() 8 | : srv(std::make_shared()) {} 9 | 10 | MultiClientTestContext::~MultiClientTestContext() { clients.clear(); } 11 | 12 | void MultiClientTestContext::create_client(AutoPollClient::Options o) { 13 | clients.push_back(std::make_unique(srv, o)); 14 | clients.back()->start(); 15 | } 16 | -------------------------------------------------------------------------------- /tests/common_multi.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "multi_client.hpp" 3 | 4 | #include 5 | #include 6 | 7 | namespace ra2yrcpp { 8 | namespace asio_utils { 9 | class IOService; 10 | } 11 | } // namespace ra2yrcpp 12 | 13 | namespace ra2yrcpp { 14 | namespace tests { 15 | 16 | namespace { 17 | using multi_client::AutoPollClient; 18 | } // namespace 19 | 20 | struct MultiClientTestContext { 21 | std::shared_ptr srv; 22 | std::vector> clients; 23 | 24 | MultiClientTestContext(); 25 | 26 | ~MultiClientTestContext(); 27 | 28 | void create_client(AutoPollClient::Options o); 29 | }; 30 | } // namespace tests 31 | } // namespace ra2yrcpp 32 | -------------------------------------------------------------------------------- /tests/dummy_program.cpp: -------------------------------------------------------------------------------- 1 | #include "utility/time.hpp" 2 | 3 | #include 4 | #include 5 | 6 | int main(int argc, const char* argv[]) { 7 | if (argc < 3) { 8 | std::cerr << "Usage: " << argv[0] << " count delay" << std::endl; 9 | return 1; 10 | } 11 | 12 | auto count = std::stoul(argv[1]); 13 | auto delay = std::stoul(argv[2]); 14 | for (unsigned long i = 0; i < count; i++) { 15 | std::cout << i << "/" << count << std::endl; 16 | util::sleep_ms(delay); 17 | } 18 | return 0; 19 | } 20 | -------------------------------------------------------------------------------- /tests/test_dll_inject.cpp: -------------------------------------------------------------------------------- 1 | #include "ra2yrproto/commands_builtin.pb.h" 2 | 3 | #include "asio_utils.hpp" 4 | #include "client_connection.hpp" 5 | #include "client_utils.hpp" 6 | #include "constants.hpp" 7 | #include "dll_inject.hpp" 8 | #include "instrumentation_client.hpp" 9 | #include "is_context.hpp" 10 | #include "logging.hpp" 11 | #include "types.h" 12 | #include "util_proto.hpp" 13 | #include "utility/time.hpp" 14 | #include "websocket_connection.hpp" 15 | #include "win32/windows_utils.hpp" 16 | #include "x86.hpp" 17 | 18 | #include 19 | #include 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | using instrumentation_client::InstrumentationClient; 30 | using namespace std::chrono_literals; 31 | using namespace ra2yrcpp::test_util; 32 | using namespace ra2yrcpp; 33 | 34 | class DLLInjectTest : public ::testing::Test { 35 | protected: 36 | void SetUp() override { 37 | o.PA.p_LoadLibrary = windows_utils::get_proc_address("LoadLibraryA"); 38 | o.PA.p_GetProcAddress = windows_utils::get_proc_address("GetProcAddress"); 39 | } 40 | 41 | is_context::DLLLoader::Options o{is_context::default_options}; 42 | }; 43 | 44 | struct B2STest : Xbyak::CodeGenerator { 45 | static void __cdecl copy_fn(char* dst, const char* src) { 46 | std::strcpy(dst, src); 47 | } 48 | 49 | B2STest(std::string msg, void* dest) { 50 | vecu8 v1(msg.begin(), msg.end()); 51 | v1.push_back(0x0); 52 | push(ebp); 53 | mov(ebp, esp); 54 | auto sz = x86::bytes_to_stack(this, v1); 55 | lea(eax, ptr[ebp - sz]); 56 | push(eax); 57 | push(reinterpret_cast(dest)); 58 | mov(eax, reinterpret_cast(©_fn)); 59 | call(eax); 60 | add(esp, sz + 0x8); 61 | pop(ebp); 62 | ret(); 63 | } 64 | }; 65 | 66 | TEST_F(DLLInjectTest, BytesToStackTest) { 67 | const std::string sm = "this is a test asd lol"; 68 | for (size_t i = 1; i < sm.size(); i++) { 69 | vecu8 dest(320, 0x0); 70 | std::string s = sm.substr(0, i); 71 | B2STest T(s, dest.data()); 72 | auto p = T.getCode(); 73 | p(); 74 | std::string a(dest.begin(), dest.begin() + s.size()); 75 | ASSERT_EQ(a, s); 76 | } 77 | } 78 | 79 | TEST_F(DLLInjectTest, BasicLoading) { 80 | auto addrs = is_context::get_procaddrs(); 81 | auto g = windows_utils::load_library(o.path_dll.c_str()); 82 | 83 | Xbyak::CodeGenerator C; 84 | is_context::get_procaddr(&C, g, o.name_init, addrs.p_GetProcAddress); 85 | auto f = C.getCode(); 86 | auto addr2 = f(); 87 | auto addr1 = windows_utils::get_proc_address(o.name_init, g); 88 | 89 | ASSERT_NE(addr1, 0x0); 90 | ASSERT_EQ(addr1, addr2); 91 | } 92 | 93 | TEST_F(DLLInjectTest, IServiceDLLInjectTest) { 94 | auto opts = o; 95 | opts.no_init_hooks = true; 96 | is_context::DLLLoader L(opts); 97 | L.ret(); 98 | auto p = L.getCode(); 99 | vecu8 sc(p, p + L.getSize()); 100 | windows_utils::ExProcess P("dummy_program.exe 10 500"); 101 | 102 | dll_inject::suspend_inject_resume(P.handle(), sc, 103 | dll_inject::DLLInjectOptions()); 104 | 105 | std::unique_ptr client; 106 | asio_utils::IOService srv; 107 | 108 | util::call_until(5.0s, 1.0s, [&]() { 109 | try { 110 | auto conn = std::make_shared( 111 | cfg::SERVER_ADDRESS, std::to_string(o.port), &srv); 112 | conn->connect(); 113 | client = std::make_unique(conn); 114 | return false; 115 | } catch (const std::exception& e) { 116 | eprintf("fail: {}", e.what()); 117 | return true; 118 | } 119 | }); 120 | 121 | ASSERT_NE(client.get(), nullptr); 122 | 123 | // run some commands 124 | { 125 | std::string f1 = "flag1"; 126 | std::string key = "key1"; 127 | 128 | // TODO(shmocz): don't ignore return value 129 | (void)client_utils::run(StoreValue::create({key, f1}), client.get()); 130 | auto r2 = client_utils::run(GetValue::create({key, ""}), client.get()); 131 | ASSERT_EQ(r2.value(), f1); 132 | } 133 | 134 | client = nullptr; 135 | 136 | // NB. gotta wait explicitly, cuz WaitFoSingleObject could fail and we cant 137 | // throw from dtors 138 | P.join(); 139 | } 140 | -------------------------------------------------------------------------------- /tests/test_hooks.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | #include "hook.hpp" 3 | #include "process.hpp" 4 | #include "types.h" 5 | #include "utility/time.hpp" 6 | 7 | #include 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | using namespace hook; 16 | using namespace std; 17 | 18 | // FIXME: rewrite tests 19 | #if 0 20 | /// Multiplies two unsigned integers, and returns result in EAX 21 | struct ExampleProgram : Xbyak::CodeGenerator { 22 | ExampleProgram() { 23 | mov(eax, ptr[esp + 0x4]); 24 | mul(ptr[esp + 0x8]); 25 | ret(); 26 | } 27 | 28 | static int expected(unsigned a, unsigned b) { return a * b; } 29 | }; 30 | 31 | /// 32 | /// Program that stays in infinite loop, until ECX equals specific value. 33 | size_t start_region(Xbyak::CodeGenerator* c, const unsigned int key) { 34 | using namespace Xbyak::util; 35 | c->push(key); 36 | c->mov(ecx, ptr[esp]); 37 | c->add(esp, 0x4); 38 | return c->getSize(); 39 | } 40 | 41 | struct InfiniteLoop : Xbyak::CodeGenerator { 42 | size_t start_region_size; 43 | 44 | explicit InfiniteLoop(const unsigned int key) { 45 | L("L1"); 46 | start_region_size = start_region(this, key); 47 | // Dummy instructions to decrease our chances of looping forever 48 | for (int i = 0; i < 20; i++) { 49 | mov(eax, ecx); 50 | cmp(eax, key); 51 | } 52 | je("L1"); 53 | ret(); 54 | } 55 | 56 | auto get_code() { return getCode(); } 57 | }; 58 | 59 | int __cdecl add_ints(int a, int b) { return a + b; } 60 | 61 | /// Sample function to test hooking on. Returns the number of bytes that need to 62 | /// be copied to detour 63 | size_t gen_add(Xbyak::CodeGenerator* c) { 64 | using namespace Xbyak::util; 65 | size_t sz = 8u; 66 | c->mov(eax, ptr[esp + 0x4]); 67 | c->add(eax, ptr[esp + 0x8]); 68 | c->ret(); 69 | return sz; 70 | } 71 | 72 | TEST(HookTest, XbyakCodegenTest) { 73 | Xbyak::CodeGenerator C; 74 | (void)gen_add(&C); 75 | auto f = C.getCode(); 76 | int c = 10; 77 | 78 | for (int i = 0; i < c; i++) { 79 | for (int j = 0; j < c; j++) { 80 | ASSERT_EQ(add_ints(i, j), f(i, j)); 81 | ASSERT_EQ(f(j, i), f(i, j)); 82 | } 83 | } 84 | } 85 | 86 | TEST(HookTest, BasicHookingWorks) { 87 | int a = 3; 88 | int b = 5; 89 | int res = add_ints(a, b); 90 | int cookie = 0u; 91 | auto my_cb = [](Hook* h, void* data, X86Regs* state) { 92 | (void)h; 93 | (void)state; 94 | int* p = reinterpret_cast(data); 95 | *p = 0xdeadbeef; 96 | }; 97 | HookCallback cb{my_cb, &cookie}; 98 | Xbyak::CodeGenerator C; 99 | size_t patch_size = gen_add(&C); 100 | auto f = C.getCode(); 101 | hook::Hook H(reinterpret_cast(f), patch_size); 102 | H.add_callback(cb); 103 | ASSERT_EQ(res, f(a, b)); 104 | ASSERT_EQ(cookie, 0xdeadbeef); 105 | } 106 | 107 | TEST(HookTest, TestCodeGeneration) { 108 | ExampleProgram C; 109 | int a = 10; 110 | int b = 13; 111 | auto f = C.getCode(); 112 | ASSERT_EQ(f(a, b), C.expected(a, b)); 113 | } 114 | 115 | // FIXME: inherently broken 116 | TEST(HookTest, TestJumpLocationExampleCode) { 117 | GTEST_SKIP(); 118 | auto P = process::get_current_process(); 119 | const int key = 0xdeadbeef; 120 | InfiniteLoop C(key); 121 | auto f = C.getCode(); 122 | auto t = std::thread(f); 123 | const int main_tid = process::get_current_tid(); 124 | P.suspend_threads(main_tid); 125 | // Set key to different value 126 | // NB! this is broken -- no guarantee that thread is within our code 127 | P.for_each_thread([&main_tid](auto* T, void* ctx) { 128 | (void)ctx; 129 | if (T->id() != main_tid) { 130 | T->set_gpr(x86Reg::ecx, 0); 131 | } 132 | }); 133 | util::sleep_ms(5000); 134 | // Resume threads 135 | P.resume_threads(main_tid); 136 | t.join(); 137 | } 138 | 139 | TEST(HookTest, BasicCallbackMultipleThreads) { 140 | const int key = 0xdeadbeef; 141 | const size_t num_threads = 3; 142 | // Create test function 143 | InfiniteLoop C(key); 144 | // TODO: use this pattern everywhere 145 | auto f = C.get_code(); 146 | 147 | // Callback which allows the thread to exit the infinite loop 148 | auto cb_f = [&key](Hook* h, void* data, X86Regs* state) { 149 | (void)h; 150 | (void)data; 151 | state->ecx = 0; 152 | }; 153 | HookCallback cb{cb_f, nullptr}; 154 | 155 | // spawn threads 156 | vector threads; 157 | for (auto i = 0u; i < num_threads; i++) { 158 | threads.emplace_back(thread(f)); 159 | } 160 | // create the hook with detour trampoline 161 | Hook H(reinterpret_cast(f), C.start_region_size); 162 | // add callbacks 163 | H.add_callback(cb); 164 | 165 | // join threads & do sanity check 166 | for (auto& t : threads) { 167 | t.join(); 168 | } 169 | } 170 | #endif 171 | 172 | TEST(HookTest, CorrectBehaviorWhenThreadsInHook) {} 173 | 174 | TEST(HookTest, CorrectBehaviorInAllScenarios) {} 175 | -------------------------------------------------------------------------------- /tests/test_is_stress_test.cpp: -------------------------------------------------------------------------------- 1 | #include "ra2yrproto/commands_builtin.pb.h" 2 | 3 | #include "commands_builtin.hpp" 4 | #include "common_multi.hpp" 5 | #include "instrumentation_service.hpp" 6 | #include "logging.hpp" 7 | #include "multi_client.hpp" 8 | #include "util_proto.hpp" 9 | 10 | #include 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | using namespace ra2yrcpp; 20 | using namespace ra2yrcpp::test_util; 21 | 22 | class ISStressTest : public ::testing::Test { 23 | protected: 24 | void SetUp() override { 25 | I = std::unique_ptr(InstrumentationService::create( 26 | default_options, commands_builtin::get_commands(), nullptr)); 27 | ctx = std::make_unique(); 28 | } 29 | 30 | void TearDown() override { 31 | ctx = nullptr; 32 | I = nullptr; 33 | } 34 | 35 | std::unique_ptr I; 36 | std::unique_ptr ctx; 37 | }; 38 | 39 | TEST_F(ISStressTest, DISABLED_ManyConnections) { 40 | for (int i = 0; i < 1; i++) { 41 | try { 42 | ctx->create_client(multi_client::default_options); 43 | } catch (const std::exception& e) { 44 | eprintf("connection failed: {}", e.what()); 45 | } 46 | } 47 | 48 | size_t total_bytes = 1e9; 49 | size_t chunk_size = 1e4 * 5; 50 | 51 | // spam storevalue 52 | std::string k1(chunk_size, 'A'); 53 | auto sv = StoreValue::create({k1, "tdata"}); 54 | for (size_t chunks = total_bytes / chunk_size; chunks > 0; chunks--) { 55 | auto r = ctx->clients[0]->send_command(sv); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/test_multi_client.cpp: -------------------------------------------------------------------------------- 1 | #include "ra2yrproto/commands_builtin.pb.h" 2 | #include "ra2yrproto/core.pb.h" 3 | 4 | #include "client_utils.hpp" 5 | #include "commands_builtin.hpp" 6 | #include "common_multi.hpp" 7 | #include "gtest/gtest.h" 8 | #include "instrumentation_service.hpp" 9 | #include "multi_client.hpp" 10 | #include "protocol/helpers.hpp" 11 | #include "util_proto.hpp" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | using namespace std::chrono_literals; 19 | using namespace ra2yrcpp::test_util; 20 | using namespace ra2yrcpp; 21 | 22 | using ra2yrcpp::tests::MultiClientTestContext; 23 | namespace client_utils = ra2yrcpp::client_utils; 24 | 25 | class MultiClientTest : public ::testing::Test { 26 | protected: 27 | void SetUp() override { 28 | InstrumentationService::Options opts = default_options; 29 | 30 | I = std::unique_ptr(InstrumentationService::create( 31 | opts, commands_builtin::get_commands(), nullptr)); 32 | ctx = std::make_unique(); 33 | ctx->create_client(multi_client::default_options); 34 | cs = std::make_unique([&](auto& msg) { 35 | auto r = ctx->clients[0]->send_command(msg); 36 | return protocol::from_any(r.body()); 37 | }); 38 | } 39 | 40 | void TearDown() override { 41 | cs = nullptr; 42 | ctx = nullptr; 43 | I = nullptr; 44 | } 45 | 46 | std::unique_ptr I; 47 | std::unique_ptr ctx; 48 | std::unique_ptr cs; 49 | }; 50 | 51 | TEST_F(MultiClientTest, RunRegularCommand) { 52 | const unsigned count = 5u; 53 | 54 | ra2yrproto::commands::GetSystemState cmd; 55 | for (auto i = 0u; i < count; i++) { 56 | auto r = cs->run(cmd); 57 | ASSERT_EQ(r.state().connections().size(), 2); 58 | } 59 | } 60 | 61 | TEST_F(MultiClientTest, RunCommandsAndVerify) { 62 | const int count = 10; 63 | const int val_size = 128; 64 | const std::string key = "tdata"; 65 | for (int i = 0; i < count; i++) { 66 | std::string k1(val_size, static_cast(i)); 67 | auto sv = StoreValue::create({key, k1}); 68 | auto r1 = cs->run(sv); 69 | 70 | auto gv = GetValue::create({key, ""}); 71 | auto r2 = cs->run(gv); 72 | ASSERT_EQ(r2.value(), k1); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/test_process.cpp: -------------------------------------------------------------------------------- 1 | #include "process.hpp" 2 | #include "utility.h" 3 | #include "utility/time.hpp" 4 | 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | using namespace process; 15 | 16 | class ThreadTest : public ::testing::Test { 17 | protected: 18 | void SetUp() override { 19 | close = false; 20 | constexpr size_t num_threads = 3u; 21 | auto f = [this]() { 22 | while (!close) { 23 | util::sleep_ms(100); 24 | } 25 | }; 26 | for (auto i = 0u; i < num_threads; i++) { 27 | threads.emplace_back(std::thread(f)); 28 | } 29 | } 30 | 31 | void TearDown() override { 32 | close = true; 33 | for (auto& t : threads) { 34 | t.join(); 35 | } 36 | } 37 | 38 | using pred_t = std::function; 39 | 40 | std::vector index2tid(process::Process* P, pred_t pred = nullptr) { 41 | std::vector I; 42 | auto cb = [&pred](Thread* T, void* ctx) { 43 | if (pred == nullptr || pred(T, ctx)) { 44 | reinterpret_cast(ctx)->push_back(T->id()); 45 | } 46 | }; 47 | P->for_each_thread(cb, &I); 48 | return I; 49 | } 50 | 51 | std::vector threads; 52 | std::atomic_bool close; 53 | }; 54 | 55 | TEST_F(ThreadTest, TestProcessThreadIterationWorks) { 56 | auto P = process::get_current_process(); 57 | auto tid = process::get_current_tid(); 58 | std::vector ix2tid = index2tid(&P); 59 | // Mess around with suspend/resume 60 | #if 1 61 | P.suspend_threads(tid); 62 | P.resume_threads(tid); 63 | #endif 64 | ASSERT_EQ(ix2tid.size(), threads.size() + 1); 65 | } 66 | 67 | TEST_F(ThreadTest, TestProcessThreadIterationFilterWorks) { 68 | auto P = process::get_current_process(); 69 | const int main_tid = get_current_tid(); 70 | std::vector ix2tid = index2tid(&P, [&main_tid](Thread* T, void* ctx) { 71 | (void)ctx; 72 | return T->id() != main_tid; 73 | }); 74 | 75 | ASSERT_EQ(ix2tid.size(), threads.size()); 76 | ASSERT_FALSE(ra2yrcpp::contains(ix2tid, main_tid)); 77 | } 78 | -------------------------------------------------------------------------------- /tests/test_protocol.cpp: -------------------------------------------------------------------------------- 1 | #include "ra2yrproto/commands_yr.pb.h" 2 | #include "ra2yrproto/ra2yr.pb.h" 3 | 4 | #include "config.hpp" 5 | #include "gtest/gtest.h" 6 | #include "logging.hpp" 7 | #include "protocol/helpers.hpp" 8 | #include "util_string.hpp" 9 | 10 | #include 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace fs = std::filesystem; 23 | namespace gpb = google::protobuf; 24 | using namespace ra2yrcpp; 25 | 26 | class TemporaryDirectoryTest : public ::testing::Test { 27 | protected: 28 | void SetUp() override; 29 | void TearDown() override; 30 | 31 | fs::path temp_dir_path_; 32 | }; 33 | 34 | void TemporaryDirectoryTest::SetUp() { 35 | // create a unique path for a temporary directory 36 | temp_dir_path_ = fs::temp_directory_path(); 37 | temp_dir_path_ /= std::tmpnam(nullptr); 38 | 39 | // create the temporary directory 40 | fs::create_directory(temp_dir_path_); 41 | iprintf("created temporary directory {}", temp_dir_path_.string()); 42 | } 43 | 44 | void TemporaryDirectoryTest::TearDown() { fs::remove_all(temp_dir_path_); } 45 | 46 | TEST_F(TemporaryDirectoryTest, ProtocolTest) { 47 | ASSERT_TRUE(fs::is_directory(temp_dir_path_)); 48 | fs::path record_path = temp_dir_path_; 49 | record_path /= "record.pb.gz"; 50 | std::vector messages(32); 51 | std::vector messages_out; 52 | for (std::size_t i = 0; i < messages.size(); i++) { 53 | auto& G = messages.at(i); 54 | G.set_current_frame(i + 1); 55 | } 56 | 57 | { 58 | auto record_out = std::make_shared( 59 | record_path.string(), std::ios_base::out | std::ios_base::binary); 60 | 61 | const bool use_gzip = true; 62 | protocol::MessageOstream MS(record_out, use_gzip); 63 | 64 | if (std::any_of(messages.begin(), messages.end(), 65 | [&MS](const auto& G) { return !MS.write(G); })) { 66 | throw std::runtime_error("failed to write message to output stream"); 67 | } 68 | } 69 | 70 | ASSERT_TRUE(fs::is_regular_file(record_path)); 71 | protocol::dump_messages(record_path.string(), ra2yrproto::ra2yr::GameState(), 72 | [&messages_out](auto* M) { 73 | ra2yrproto::ra2yr::GameState G; 74 | G.CopyFrom(*M); 75 | messages_out.push_back(G); 76 | }); 77 | 78 | gpb::util::MessageDifferencer D; 79 | for (std::size_t i = 0; i < messages.size(); i++) { 80 | ASSERT_TRUE(D.Equals(messages.at(i), messages_out.at(i))); 81 | } 82 | constexpr std::size_t n_empty_messages = 10U; 83 | ra2yrproto::ra2yr::GameState G0; 84 | ASSERT_TRUE(G0.houses().empty()); 85 | ra2yrcpp::protocol::fill_repeated_empty(G0.mutable_houses(), 86 | n_empty_messages); 87 | ASSERT_EQ(G0.houses().size(), n_empty_messages); 88 | G0.clear_houses(); 89 | } 90 | -------------------------------------------------------------------------------- /tests/util_proto.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "protocol/protocol.hpp" 3 | #include "ra2yrproto/commands_builtin.pb.h" 4 | #include "ra2yrproto/core.pb.h" 5 | 6 | #include 7 | 8 | namespace ra2yrcpp::test_util { 9 | 10 | struct StoreValue { 11 | std::string key; 12 | std::string value; 13 | 14 | static ra2yrproto::commands::StoreValue create(StoreValue c) { 15 | ra2yrproto::commands::StoreValue s; 16 | s.set_key(c.key); 17 | s.set_value(c.value); 18 | return s; 19 | } 20 | }; 21 | 22 | struct GetValue { 23 | std::string key; 24 | std::string value; 25 | 26 | static ra2yrproto::commands::GetValue create(GetValue c) { 27 | ra2yrproto::commands::GetValue s; 28 | s.set_key(c.key); 29 | s.set_value(c.value); 30 | return s; 31 | } 32 | }; 33 | 34 | }; // namespace ra2yrcpp::test_util 35 | -------------------------------------------------------------------------------- /toolchains/mingw-w64-i686.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Windows) 2 | set(HOST_ARCH i686) 3 | set(TOOLCHAIN_PREFIX i686-w64-mingw32) 4 | set(CMAKE_SYSROOT /usr/${TOOLCHAIN_PREFIX}) 5 | 6 | if(DEFINED ENV{TC_VERSION}) 7 | set(TC_VERSION "$ENV{TC_VERSION}") 8 | else() 9 | file(GLOB TC_PATHL "/usr/lib/gcc/${TOOLCHAIN_PREFIX}/*") 10 | list(GET TC_PATHL 0 TC_PATH) 11 | cmake_path(GET TC_PATH FILENAME TC_VERSION) 12 | endif() 13 | 14 | set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc CACHE FILEPATH "") 15 | set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++ CACHE FILEPATH "") 16 | set(CMAKE_CXX_STANDARD 17 CACHE STRING "") 17 | set(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES /usr/lib/gcc/${TOOLCHAIN_PREFIX}/${TC_VERSION}/include) 18 | 19 | # Need these to find headers etc. 20 | # TODO: copy the native protoc executable to pkg 21 | if(DEFINED ENV{FIND_ROOT_PATH}) 22 | set(CMAKE_FIND_ROOT_PATH "$ENV{FIND_ROOT_PATH}") 23 | endif() 24 | 25 | if(DEFINED ENV{PROTOC_PATH}) 26 | set(PROTOC_PATH "$ENV{PROTOC_PATH}") 27 | endif() 28 | 29 | if(DEFINED ENV{PROTO_LIB}) 30 | set(PROTO_LIB "$ENV{PROTO_LIB}") 31 | endif() 32 | 33 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ALWAYS) 34 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH) 35 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) 36 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 37 | 38 | # Enable these to strip symbols 39 | # set(CMAKE_SHARED_LINKER_FLAGS -s) 40 | # set(CMAKE_EXE_LINKER_FLAGS -s) 41 | 42 | # set(RA2YRCPP_BUILD_MAIN_DLL ON CACHE BOOL "") 43 | # set(RA2YRCPP_BUILD_TESTS ON CACHE BOOL "") --------------------------------------------------------------------------------