Compare commits

..

13 Commits

Author SHA1 Message Date
77adc0ea8a unlikely added 2026-01-04 03:27:46 +03:00
83f17892d6 added unlikely 2026-01-03 08:57:56 +03:00
26fd8e158a i dont like icons now 2026-01-01 03:59:12 +03:00
2af77d30d4 Feature/elf pattern scan (#133)
* added some code

* improvement

* visit

* added scanner code

* update

* fixed naming

* added const

* added type casting

* added file

* patch

* added unlikely

* added in module scanner

* fixing test

* fix

* remove

* improvement

* fix

* Update source/utility/elf_pattern_scan.cpp

Co-authored-by: Saikari <lin@sz.cn.eu.org>

* rev

* fix

* patch

* decomposed method

* fix

* fix

* improvement

* fix

* fix

* commented stuff

---------

Co-authored-by: Saikari <lin@sz.cn.eu.org>
2026-01-01 03:37:55 +03:00
368272b1e8 fixed tests 2025-12-30 08:40:42 +03:00
Saikari
0ca471ed4f Use LLVM coverage and LCOV genhtml on Windows OS (#131) 2025-12-29 21:11:13 +03:00
Saikari
f0145ec68e Use LLVM coverage and LCOV genhtml on Windows OS (#129) 2025-12-28 02:44:00 +03:00
771e1e68fe improved code style 2025-12-28 02:32:35 +03:00
ffcf448a07 improved code style 2025-12-28 02:29:19 +03:00
00fcdc86bc Update license badge in README
Replaced the static badge with a dynamic GitHub license badge.
2025-12-27 20:07:45 +03:00
2c11c5ce1a returned back to zlib 2025-12-27 20:06:17 +03:00
fdb2ad099a umped version 2025-12-27 19:43:56 +03:00
88ce5e6b8c warning fix 2025-12-25 02:35:50 +03:00
28 changed files with 1013 additions and 319 deletions

View File

@@ -188,7 +188,12 @@ jobs:
- name: Configure (cmake --preset) - name: Configure (cmake --preset)
shell: bash shell: bash
run: cmake --preset ${{ matrix.preset }} -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DOMATH_ENABLE_COVERAGE=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" run: |
cmake --preset ${{ matrix.preset }} \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
- name: Build - name: Build
shell: bash shell: bash
@@ -198,35 +203,121 @@ jobs:
shell: bash shell: bash
run: ./out/Release/unit_tests.exe run: ./out/Release/unit_tests.exe
- name: Install OpenCppCoverage with Chocolatey ##########################################################################
# Coverage (x64-windows only)
##########################################################################
- name: Install LLVM
if: ${{ matrix.triplet == 'x64-windows' }} if: ${{ matrix.triplet == 'x64-windows' }}
run: choco install opencppcoverage -y run: |
choco install llvm -y
- name: Clean Build Directory for Coverage
if: ${{ matrix.triplet == 'x64-windows' }}
shell: pwsh
run: |
$buildDir = "cmake-build/build/${{ matrix.preset }}"
if (Test-Path $buildDir) {
Write-Host "Cleaning build directory to prevent compiler conflict: $buildDir"
Remove-Item -Path $buildDir -Recurse -Force
}
- name: Build Debug for Coverage - name: Build Debug for Coverage
if: ${{ matrix.triplet == 'x64-windows' }} if: ${{ matrix.triplet == 'x64-windows' }}
shell: bash shell: bash
run: | run: |
cmake --preset ${{ matrix.preset }} \ cmake --preset ${{ matrix.preset }} \
-DCMAKE_C_COMPILER="C:/Program Files/LLVM/bin/clang-cl.exe" \
-DCMAKE_CXX_COMPILER="C:/Program Files/LLVM/bin/clang-cl.exe" \
-DCMAKE_LINKER="C:/Program Files/LLVM/bin/lld-link.exe" \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=ON \ -DOMATH_ENABLE_COVERAGE=ON \
-DOMATH_THREAT_WARNING_AS_ERROR=OFF \
-DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_BUILD_TYPE=Debug \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
cmake --build cmake-build/build/${{ matrix.preset }} --config Debug --target unit_tests omath cmake --build cmake-build/build/${{ matrix.preset }} --config Debug --target unit_tests omath
- name: Run Coverage - name: Run Tests (Generates .profraw)
if: ${{ matrix.triplet == 'x64-windows' }}
shell: pwsh
env:
LLVM_PROFILE_FILE: "cmake-build/build/${{ matrix.preset }}/unit_tests.profraw"
run: |
./out/Debug/unit_tests.exe
- name: Process Coverage (llvm-profdata & llvm-cov)
if: ${{ matrix.triplet == 'x64-windows' }} if: ${{ matrix.triplet == 'x64-windows' }}
shell: pwsh shell: pwsh
run: | run: |
$env:Path = "C:\Program Files\OpenCppCoverage;$env:Path" $BUILD_DIR = "cmake-build/build/${{ matrix.preset }}"
cmake --build cmake-build/build/${{ matrix.preset }} --target coverage --config Debug $EXE_PATH = "./out/Debug/unit_tests.exe"
- name: Upload Coverage # 1. Merge raw profile data (essential step)
& "C:/Program Files/LLVM/bin/llvm-profdata.exe" merge `
-sparse "$BUILD_DIR/unit_tests.profraw" `
-o "$BUILD_DIR/unit_tests.profdata"
# 2. Export to LCOV format
# NOTE: We explicitly ignore vcpkg_installed and system headers
& "C:/Program Files/LLVM/bin/llvm-cov.exe" export "$EXE_PATH" `
-instr-profile="$BUILD_DIR/unit_tests.profdata" `
-format=lcov `
-ignore-filename-regex="vcpkg_installed|external|tests" `
> "$BUILD_DIR/lcov.info"
if (Test-Path "$BUILD_DIR/lcov.info") {
Write-Host "✅ LCOV info created at $BUILD_DIR/lcov.info"
} else {
Write-Error "Failed to create LCOV info"
exit 1
}
- name: Install LCOV (for genhtml)
if: ${{ matrix.triplet == 'x64-windows' }}
run: choco install lcov -y
- name: Generate HTML Report
if: ${{ matrix.triplet == 'x64-windows' }}
shell: bash
run: |
BUILD_DIR="cmake-build/build/${{ matrix.preset }}"
LCOV_INFO="${BUILD_DIR}/lcov.info"
HTML_DIR="${BUILD_DIR}/coverage-html"
# Fix paths for genhtml (Perl hates backslashes)
sed -i 's|\\|/|g' "${LCOV_INFO}"
# Locate genhtml provided by 'choco install lcov'
# It is typically in ProgramData/chocolatey/lib/lcov/tools/bin
GENHTML=$(find /c/ProgramData/chocolatey -name genhtml -print -quit)
if [ -z "$GENHTML" ]; then
echo "Error: genhtml executable not found"
exit 1
fi
echo "Using genhtml: $GENHTML"
mkdir -p "$HTML_DIR"
# Run genhtml
# Added --demangle-cpp if your version supports it, otherwise remove it
perl "$GENHTML" \
"${LCOV_INFO}" \
--output-directory "$HTML_DIR" \
--title "OMath Coverage Report" \
--legend \
--show-details \
--branch-coverage \
--ignore-errors source
echo "✅ LCOV HTML report generated at $HTML_DIR"
- name: Upload Coverage (HTML Report)
if: ${{ matrix.triplet == 'x64-windows' }} if: ${{ matrix.triplet == 'x64-windows' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: coverage-report-windows name: coverage-html-windows
path: cmake-build/build/${{ matrix.preset }}/coverage/ path: cmake-build/build/${{ matrix.preset }}/coverage-html/
- name: Upload logs on failure - name: Upload logs on failure
if: ${{ failure() }} if: ${{ failure() }}
@@ -662,3 +753,61 @@ jobs:
path: | path: |
cmake-build/build/${{ matrix.preset }}/**/*.log cmake-build/build/${{ matrix.preset }}/**/*.log
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log ${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
##############################################################################
# 9) Valgrind Memory Check
##############################################################################
valgrind-memory-check:
name: Valgrind Analysis (All Targets)
runs-on: ubuntu-latest
needs: [linux-build-and-test]
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Install toolchain
shell: bash
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"
sudo apt-get update
sudo apt-get install -y git build-essential cmake ninja-build \
zip unzip curl pkg-config ca-certificates \
clang-21 lld-21 libc++-21-dev libc++abi-21-dev \
llvm-21 valgrind libxmu-dev libxi-dev libgl-dev libxinerama-dev libxcursor-dev xorg-dev libglu1-mesa-dev
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 100
sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 100
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up vcpkg
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset linux-release-vcpkg \
-DCMAKE_BUILD_TYPE=Debug \
-DOMATH_BUILD_EXAMPLES=OFF \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=ON \
-DOMATH_ENABLE_VALGRIND=ON \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;benchmark"
- name: Build All Targets
shell: bash
run: cmake --build cmake-build/build/linux-release-vcpkg
- name: Run Valgrind (All Registered Targets)
shell: bash
working-directory: cmake-build/build/linux-release-vcpkg
run: |
cmake --build . --target valgrind_all

View File

@@ -5,7 +5,9 @@ project(omath VERSION ${OMATH_VERSION} LANGUAGES CXX)
include(CMakePackageConfigHelpers) include(CMakePackageConfigHelpers)
include(CheckCXXCompilerFlag) include(CheckCXXCompilerFlag)
include(cmake/Coverage.cmake) include(cmake/Coverage.cmake)
include(cmake/Valgrind.cmake)
if (MSVC) if (MSVC)
check_cxx_compiler_flag("/arch:AVX2" COMPILER_SUPPORTS_AVX2) check_cxx_compiler_flag("/arch:AVX2" COMPILER_SUPPORTS_AVX2)
@@ -61,6 +63,8 @@ if (${PROJECT_IS_TOP_LEVEL})
message(STATUS "[${PROJECT_NAME}]: ImGUI integration feature status ${OMATH_IMGUI_INTEGRATION}") message(STATUS "[${PROJECT_NAME}]: ImGUI integration feature status ${OMATH_IMGUI_INTEGRATION}")
message(STATUS "[${PROJECT_NAME}]: Legacy features support ${OMATH_ENABLE_LEGACY}") message(STATUS "[${PROJECT_NAME}]: Legacy features support ${OMATH_ENABLE_LEGACY}")
message(STATUS "[${PROJECT_NAME}]: Building using vcpkg ${OMATH_BUILD_VIA_VCPKG}") message(STATUS "[${PROJECT_NAME}]: Building using vcpkg ${OMATH_BUILD_VIA_VCPKG}")
message(STATUS "[${PROJECT_NAME}]: Coverage feature status ${OMATH_ENABLE_COVERAGE}")
message(STATUS "[${PROJECT_NAME}]: Valgrind feature status ${OMATH_ENABLE_VALGRIND}")
endif () endif ()
file(GLOB_RECURSE OMATH_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp") file(GLOB_RECURSE OMATH_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp")
@@ -143,6 +147,7 @@ endif()
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
if (OMATH_BUILD_TESTS) if (OMATH_BUILD_TESTS)
add_subdirectory(tests) add_subdirectory(tests)
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_BUILD_TESTS) target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_BUILD_TESTS)

44
LICENSE
View File

@@ -1,4 +1,4 @@
Copyright (C) 2024-2025 Orange++ <orange_github@proton.me> Copyright (C) 2023-2025 Orange++ orange_github@proton.me
This software is provided 'as-is', without any express or implied This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages warranty. In no event will the authors be held liable for any damages
@@ -15,45 +15,3 @@ freely, subject to the following restrictions:
2. Altered source versions must be plainly marked as such, and must not be 2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software. misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution. 3. This notice may not be removed or altered from any source distribution.
4. If you are an employee, contractor, volunteer, representative,
or have any other affiliation (past or present)
with any of the following entities:
* "Advertising Placement Services" LLC
* "NEW SOLUTIONS VERTICALS" LLC
* "Autoexpert" LLC
* "Creditit" LLC
* "Yandex.Taxi" LLC
* "Yandex.Eda" LLC
* "Yandex.Lavka" LLC
* "Yandex.Telecom" LLC
* "Yandex.Cloud" LLC
* "Micromobility" LLC
* "MM-Tech" LLC
* "Carsharing" LLC
* "Yandex.Drive" LLC
* "EDADIL PROMO" LLC
* "Kinopoisk" LLC
* "Yandex.Music" LLC
* "Refueling (Yandex.Zapravki)" LLC
* "Yandex.Pay" LLC
* "Financial and Payment Technologies" LLC
* "Yandex.Delivery" LLC
* "Delivery Club" LLC
* "Yandex.Check" LLC
* "SMB-Service" LLC
* "ADV-TECH" LLC
* "Yandex Fantech" LLC
* "Yandex Smena" LLC
* "Market.Operations" LLC
* "Yandex.Market" LLC
* "ID Tech" LLC
* "Yandex.Crowd" LLC
* "Yandex" LLC
* "Rutube" LLC
* "Kaspersky" LLC
Or if you represent or are associated with any legal, organizational, or
professional entity providing services to or on behalf of the aforementioned entities:
You are expressly forbidden from accessing, using, modifying, distributing, or
interacting with the Software and its source code in any form. You must immediately
delete or destroy any physical or digital copies of the Software and/or
its source code, including any derivative works, tools, or information obtained from the Software.

View File

@@ -2,7 +2,7 @@
![banner](docs/images/logos/omath_logo_macro.png) ![banner](docs/images/logos/omath_logo_macro.png)
![Static Badge](https://img.shields.io/badge/license-libomath-orange) ![GitHub License](https://img.shields.io/github/license/orange-cpp/omath)
![GitHub contributors](https://img.shields.io/github/contributors/orange-cpp/omath) ![GitHub contributors](https://img.shields.io/github/contributors/orange-cpp/omath)
![GitHub top language](https://img.shields.io/github/languages/top/orange-cpp/omath) ![GitHub top language](https://img.shields.io/github/languages/top/orange-cpp/omath)
![GitHub repo size](https://img.shields.io/github/repo-size/orange-cpp/omath) ![GitHub repo size](https://img.shields.io/github/repo-size/orange-cpp/omath)
@@ -44,7 +44,7 @@ It provides the latest features, is highly customizable, has all for cheat devel
</a> </a>
</div> </div>
## 🚀 Quick Example ## Quick Example
```cpp ```cpp
#include <omath/omath.hpp> #include <omath/omath.hpp>
@@ -69,20 +69,20 @@ if (auto screen = camera.world_to_screen(world_position)) {
} }
``` ```
**[➡️ See more examples and tutorials][TUTORIALS]** **[See more examples and tutorials][TUTORIALS]**
# Features # Features
- **🚀 Efficiency**: Optimized for performance, ensuring quick computations using AVX2. - **Efficiency**: Optimized for performance, ensuring quick computations using AVX2.
- **🎯 Versatility**: Includes a wide array of mathematical functions and algorithms. - **Versatility**: Includes a wide array of mathematical functions and algorithms.
- **Ease of Use**: Simplified interface for convenient integration into various projects. - **Ease of Use**: Simplified interface for convenient integration into various projects.
- **🎮 Projectile Prediction**: Projectile prediction engine with O(N) algo complexity, that can power you projectile aim-bot. - **Projectile Prediction**: Projectile prediction engine with O(N) algo complexity, that can power you projectile aim-bot.
- **📐 3D Projection**: No need to find view-projection matrix anymore you can make your own projection pipeline. - **3D Projection**: No need to find view-projection matrix anymore you can make your own projection pipeline.
- **💥 Collision Detection**: Production ready code to handle collision detection by using simple interfaces. - **Collision Detection**: Production ready code to handle collision detection by using simple interfaces.
- **📦 No Additional Dependencies**: No additional dependencies need to use OMath except unit test execution - **No Additional Dependencies**: No additional dependencies need to use OMath except unit test execution
- **🔧 Ready for meta-programming**: Omath use templates for common types like Vectors, Matrixes etc, to handle all types! - **Ready for meta-programming**: Omath use templates for common types like Vectors, Matrixes etc, to handle all types!
- **🎯 Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine and canonical OpenGL**. - **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine and canonical OpenGL**.
- **🌍 Cross platform**: Supports Windows, MacOS and Linux. - **Cross platform**: Supports Windows, MacOS and Linux.
- **🔍 Algorithms**: Has ability to scan for byte pattern with wildcards in PE files/modules, binary slices, works even with Wine apps. - **Algorithms**: Has ability to scan for byte pattern with wildcards in PE files/modules, binary slices, works even with Wine apps.
<div align = center> <div align = center>
# Gallery # Gallery
@@ -116,7 +116,7 @@ if (auto screen = camera.world_to_screen(world_position)) {
</div> </div>
## 📚 Documentation ## Documentation
- **[Getting Started Guide](http://libomath.org/getting_started/)** - Installation and first steps - **[Getting Started Guide](http://libomath.org/getting_started/)** - Installation and first steps
- **[API Overview](http://libomath.org/api_overview/)** - Complete API reference - **[API Overview](http://libomath.org/api_overview/)** - Complete API reference
@@ -125,14 +125,14 @@ if (auto screen = camera.world_to_screen(world_position)) {
- **[Troubleshooting](http://libomath.org/troubleshooting/)** - Solutions to common issues - **[Troubleshooting](http://libomath.org/troubleshooting/)** - Solutions to common issues
- **[Best Practices](http://libomath.org/best_practices/)** - Guidelines for effective usage - **[Best Practices](http://libomath.org/best_practices/)** - Guidelines for effective usage
## 🤝 Community & Support ## Community & Support
- **Discord**: [Join our community](https://discord.gg/eDgdaWbqwZ) - **Discord**: [Join our community](https://discord.gg/eDgdaWbqwZ)
- **Telegram**: [@orangennotes](https://t.me/orangennotes) - **Telegram**: [@orangennotes](https://t.me/orangennotes)
- **Issues**: [Report bugs or request features](https://github.com/orange-cpp/omath/issues) - **Issues**: [Report bugs or request features](https://github.com/orange-cpp/omath/issues)
- **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines - **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines
# 💘 Acknowledgments # Acknowledgments
- [All contributors](https://github.com/orange-cpp/omath/graphs/contributors) - [All contributors](https://github.com/orange-cpp/omath/graphs/contributors)
<!----------------------------------{ Images }---------------------------------> <!----------------------------------{ Images }--------------------------------->

View File

@@ -1 +1 @@
4.4.0 4.6.0

View File

@@ -17,3 +17,7 @@ else()
find_package(benchmark CONFIG REQUIRED) # Benchmark is being linked as vcpkg package find_package(benchmark CONFIG REQUIRED) # Benchmark is being linked as vcpkg package
target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark omath) target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark omath)
endif () endif ()
if(OMATH_ENABLE_VALGRIND)
omath_setup_valgrind(${PROJECT_NAME})
endif()

View File

@@ -6,8 +6,20 @@ function(omath_setup_coverage TARGET_NAME)
return() return()
endif() endif()
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang") if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND MSVC)
# Apply to ALL configs when coverage is enabled (not just Debug) target_compile_options(${TARGET_NAME} PRIVATE
-fprofile-instr-generate
-fcoverage-mapping
/Zi
)
target_link_options(${TARGET_NAME} PRIVATE
-fprofile-instr-generate
-fcoverage-mapping
/DEBUG:FULL
/INCREMENTAL:NO
/PROFILE
)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang")
target_compile_options(${TARGET_NAME} PRIVATE target_compile_options(${TARGET_NAME} PRIVATE
-fprofile-instr-generate -fprofile-instr-generate
-fcoverage-mapping -fcoverage-mapping
@@ -18,7 +30,6 @@ function(omath_setup_coverage TARGET_NAME)
-fprofile-instr-generate -fprofile-instr-generate
-fcoverage-mapping -fcoverage-mapping
) )
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(${TARGET_NAME} PRIVATE target_compile_options(${TARGET_NAME} PRIVATE
--coverage --coverage
@@ -28,68 +39,22 @@ function(omath_setup_coverage TARGET_NAME)
target_link_options(${TARGET_NAME} PRIVATE target_link_options(${TARGET_NAME} PRIVATE
--coverage --coverage
) )
elseif(MSVC)
target_compile_options(${TARGET_NAME} PRIVATE
/Zi
/Od
/Ob0
)
target_link_options(${TARGET_NAME} PRIVATE
/DEBUG:FULL
/INCREMENTAL:NO
)
endif() endif()
# Create coverage target only once
if(TARGET coverage) if(TARGET coverage)
return() return()
endif() endif()
if(MSVC OR MINGW) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND MSVC)
# Windows: OpenCppCoverage message(STATUS "MSVC detected: Use VS Code Coverage from CI workflow")
find_program(OPENCPPCOVERAGE_EXECUTABLE
NAMES OpenCppCoverage OpenCppCoverage.exe
PATHS
"$ENV{ProgramFiles}/OpenCppCoverage"
"$ENV{ProgramW6432}/OpenCppCoverage"
"C:/Program Files/OpenCppCoverage"
DOC "Path to OpenCppCoverage executable"
)
if(NOT OPENCPPCOVERAGE_EXECUTABLE)
message(WARNING "OpenCppCoverage not found. Install with: choco install opencppcoverage")
set(OPENCPPCOVERAGE_EXECUTABLE "C:/Program Files/OpenCppCoverage/OpenCppCoverage.exe")
else()
message(STATUS "Found OpenCppCoverage: ${OPENCPPCOVERAGE_EXECUTABLE}")
endif()
file(TO_NATIVE_PATH "${CMAKE_SOURCE_DIR}" COVERAGE_ROOT_PATH)
file(TO_NATIVE_PATH "${CMAKE_BINARY_DIR}/coverage" COVERAGE_OUTPUT_PATH)
file(TO_NATIVE_PATH "${CMAKE_BINARY_DIR}/coverage.xml" COVERAGE_XML_PATH)
file(TO_NATIVE_PATH "${OPENCPPCOVERAGE_EXECUTABLE}" OPENCPPCOVERAGE_NATIVE)
add_custom_target(coverage add_custom_target(coverage
DEPENDS unit_tests DEPENDS unit_tests
COMMAND "${OPENCPPCOVERAGE_NATIVE}" COMMAND $<TARGET_FILE:unit_tests>
--verbose
--sources "${COVERAGE_ROOT_PATH}"
--modules "${COVERAGE_ROOT_PATH}"
--excluded_sources "*\\tests\\*"
--excluded_sources "*\\gtest\\*"
--excluded_sources "*\\googletest\\*"
--excluded_sources "*\\_deps\\*"
--excluded_sources "*\\vcpkg_installed\\*"
--export_type "html:${COVERAGE_OUTPUT_PATH}"
--export_type "cobertura:${COVERAGE_XML_PATH}"
--cover_children
-- "$<TARGET_FILE:unit_tests>"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Running OpenCppCoverage" COMMENT "Running tests for coverage (use VS Code Coverage from CI)"
) )
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang") elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang")
# Linux/macOS: LLVM coverage via script
add_custom_target(coverage add_custom_target(coverage
DEPENDS unit_tests DEPENDS unit_tests
COMMAND bash "${CMAKE_SOURCE_DIR}/scripts/coverage-llvm.sh" COMMAND bash "${CMAKE_SOURCE_DIR}/scripts/coverage-llvm.sh"
@@ -102,7 +67,6 @@ function(omath_setup_coverage TARGET_NAME)
) )
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# GCC: lcov/gcov
add_custom_target(coverage add_custom_target(coverage
DEPENDS unit_tests DEPENDS unit_tests
COMMAND $<TARGET_FILE:unit_tests> || true COMMAND $<TARGET_FILE:unit_tests> || true

45
cmake/Valgrind.cmake Normal file
View File

@@ -0,0 +1,45 @@
# cmake/Valgrind.cmake
if(DEFINED __OMATH_VALGRIND_INCLUDED)
return()
endif()
set(__OMATH_VALGRIND_INCLUDED TRUE)
find_program(VALGRIND_EXECUTABLE valgrind)
option(OMATH_ENABLE_VALGRIND "Enable Valgrind target for memory checking" ON)
if(OMATH_ENABLE_VALGRIND AND NOT TARGET valgrind_all)
add_custom_target(valgrind_all)
endif()
function(omath_setup_valgrind TARGET_NAME)
if(NOT OMATH_ENABLE_VALGRIND)
return()
endif()
if(NOT VALGRIND_EXECUTABLE)
message(WARNING "OMATH_ENABLE_VALGRIND is ON, but 'valgrind' executable was not found.")
return()
endif()
set(VALGRIND_FLAGS
--leak-check=full
--show-leak-kinds=all
--track-origins=yes
--error-exitcode=99
)
set(VALGRIND_TARGET "valgrind_${TARGET_NAME}")
if(NOT TARGET ${VALGRIND_TARGET})
add_custom_target(${VALGRIND_TARGET}
DEPENDS ${TARGET_NAME}
COMMAND ${VALGRIND_EXECUTABLE} ${VALGRIND_FLAGS} $<TARGET_FILE:${TARGET_NAME}>
WORKING_DIRECTORY $<TARGET_FILE_DIR:${TARGET_NAME}>
COMMENT "Running Valgrind memory check on ${TARGET_NAME}..."
USES_TERMINAL
)
add_dependencies(valgrind_all ${VALGRIND_TARGET})
endif()
endfunction()

View File

@@ -23,10 +23,15 @@ add_executable(example_glfw3 example_glfw3.cpp)
set_target_properties(example_glfw3 PROPERTIES CXX_STANDARD 26 set_target_properties(example_glfw3 PROPERTIES CXX_STANDARD 26
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
) )
find_package(GLEW REQUIRED) find_package(GLEW REQUIRED)
find_package(glfw3 CONFIG REQUIRED) find_package(glfw3 CONFIG REQUIRED)
target_link_libraries(example_glfw3 PRIVATE omath::omath GLEW::GLEW glfw) target_link_libraries(example_glfw3 PRIVATE omath::omath GLEW::GLEW glfw)
if(OMATH_ENABLE_VALGRIND)
omath_setup_valgrind(example_projection_matrix_builder)
omath_setup_valgrind(example_signature_scan)
omath_setup_valgrind(example_glfw3)
endif()

View File

@@ -239,8 +239,8 @@ int main()
// flatten EBO to GL indices // flatten EBO to GL indices
std::vector<GLuint> flatIndices; std::vector<GLuint> flatIndices;
flatIndices.reserve(cube.m_vertex_array_object.size() * 3); flatIndices.reserve(cube.m_element_buffer_object.size() * 3);
for (const auto& tri : cube.m_vertex_array_object) for (const auto& tri : cube.m_element_buffer_object)
{ {
flatIndices.push_back(tri.x); flatIndices.push_back(tri.x);
flatIndices.push_back(tri.y); flatIndices.push_back(tri.y);

View File

@@ -0,0 +1,25 @@
//
// Created by Vladislav on 30.12.2025.
//
#pragma once
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string_view>
#include "section_scan_result.hpp"
namespace omath
{
class ElfPatternScanner final
{
public:
[[nodiscard]]
static std::optional<std::uintptr_t>
scan_for_pattern_in_loaded_module(const void* module_base_address, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
[[nodiscard]]
static std::optional<SectionScanResult>
scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
};
} // namespace omath

View File

@@ -7,23 +7,20 @@
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <string_view> #include <string_view>
#include "section_scan_result.hpp"
namespace omath namespace omath
{ {
struct PeSectionScanResult
{
std::uint64_t virtual_base_addr;
std::uint64_t raw_base_addr;
std::ptrdiff_t target_offset;
};
class PePatternScanner final class PePatternScanner final
{ {
public: public:
[[nodiscard]] [[nodiscard]]
static std::optional<std::uintptr_t> scan_for_pattern_in_loaded_module(const void* module_base_address, static std::optional<std::uintptr_t>
const std::string_view& pattern); scan_for_pattern_in_loaded_module(const void* module_base_address, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
[[nodiscard]] [[nodiscard]]
static std::optional<PeSectionScanResult> static std::optional<SectionScanResult>
scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern, scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern,
const std::string_view& target_section_name = ".text"); const std::string_view& target_section_name = ".text");
}; };

View File

@@ -0,0 +1,16 @@
//
// Created by Vladislav on 01.01.2026.
//
#pragma once
#include <cstddef>
#include <cstdint>
namespace omath
{
struct SectionScanResult final
{
std::uintptr_t virtual_base_addr;
std::uintptr_t raw_base_addr;
std::ptrdiff_t target_offset;
};
}

77
scripts/valgrind.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/bin/bash
# =============================================================================
# Local Reproduction of "Valgrind Analysis" GitHub Action
# =============================================================================
# Stop on error, undefined variables, or pipe failures
set -euo pipefail
# --- 1. Environment Setup ---
# Determine VCPKG_ROOT
# If VCPKG_ROOT is not set in your shell, we check if a local folder exists.
if [[ -z "${VCPKG_ROOT:-}" ]]; then
if [[ -d "./vcpkg" ]]; then
export VCPKG_ROOT="$(pwd)/vcpkg"
echo "Found local vcpkg at: $VCPKG_ROOT"
# Bootstrap vcpkg if the executable doesn't exist
if [[ ! -f "$VCPKG_ROOT/vcpkg" ]]; then
echo "Bootstrapping vcpkg..."
"$VCPKG_ROOT/bootstrap-vcpkg.sh"
fi
else
echo "Error: VCPKG_ROOT is not set and ./vcpkg directory not found."
echo "Please install vcpkg or set the VCPKG_ROOT environment variable."
exit 1
fi
else
echo "Using existing VCPKG_ROOT: $VCPKG_ROOT"
fi
# Set the build directory matching the YAML's preset expectation
# Assuming the preset writes to: cmake-build/build/linux-release-vcpkg
BUILD_DIR="cmake-build/build/linux-release-vcpkg"
# Check if Valgrind is installed
if ! command -v valgrind &> /dev/null; then
echo "Error: valgrind is not installed. Please install it (e.g., sudo apt install valgrind)."
exit 1
fi
echo "----------------------------------------------------"
echo "Starting Configuration (Debug Build with Valgrind)..."
echo "----------------------------------------------------"
# --- 2. Configure (CMake) ---
# We force CMAKE_BUILD_TYPE=Debug even though the preset says 'release'
# to ensure Valgrind has access to debug symbols (line numbers).
cmake --preset linux-release-vcpkg \
-DCMAKE_BUILD_TYPE=Debug \
-DOMATH_BUILD_EXAMPLES=OFF \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=ON \
-DOMATH_ENABLE_VALGRIND=ON \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;benchmark"
echo "----------------------------------------------------"
echo "Building Targets..."
echo "----------------------------------------------------"
# --- 3. Build ---
# Using the specific build directory defined by the preset structure
cmake --build "$BUILD_DIR"
echo "----------------------------------------------------"
echo "Running Valgrind Analysis..."
echo "----------------------------------------------------"
# --- 4. Run Valgrind ---
# Runs the specific custom target defined in your CMakeLists.txt
cmake --build "$BUILD_DIR" --target valgrind_all
echo "----------------------------------------------------"
echo "Valgrind Analysis Complete."
echo "----------------------------------------------------"

View File

@@ -0,0 +1,325 @@
//
// Created by Vladislav on 30.12.2025.
//
#include "omath/utility/pattern_scan.hpp"
#include <array>
#include <fstream>
#include <omath/utility/elf_pattern_scan.hpp>
#include <utility>
#include <variant>
#include <vector>
#pragma pack(push, 1)
namespace
{
// Common
constexpr uint8_t ei_nident = 16;
constexpr uint8_t ei_class = 4;
constexpr uint8_t elfclass32 = 1;
constexpr uint8_t elfclass64 = 2;
// ReSharper disable CppDeclaratorNeverUsed
struct Elf32Ehdr final
{
unsigned char e_ident[ei_nident];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint32_t e_entry;
uint32_t e_phoff;
uint32_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
};
struct Elf64Ehdr final
{
unsigned char e_ident[ei_nident];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint64_t e_entry;
uint64_t e_phoff;
uint64_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
};
struct Elf32Shdr final
{
uint32_t sh_name;
uint32_t sh_type;
uint32_t sh_flags;
uint32_t sh_addr;
uint32_t sh_offset;
uint32_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint32_t sh_addralign;
uint32_t sh_entsize;
};
struct Elf64Shdr final
{
uint32_t sh_name;
uint32_t sh_type;
uint64_t sh_flags;
uint64_t sh_addr;
uint64_t sh_offset;
uint64_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint64_t sh_addralign;
uint64_t sh_entsize;
};
// ReSharper restore CppDeclaratorNeverUsed
#pragma pack(pop)
} // namespace
namespace
{
enum class FileArch : std::int8_t
{
x32,
x64,
};
template<FileArch arch>
struct ElfHeaders
{
using FileHeader = std::conditional_t<arch == FileArch::x64, Elf64Ehdr, Elf32Ehdr>;
using SectionHeader = std::conditional_t<arch == FileArch::x64, Elf64Shdr, Elf32Shdr>;
FileHeader file_header;
SectionHeader section_header;
};
[[nodiscard]]
bool not_elf_file(std::fstream& file)
{
constexpr std::string_view valid_elf_signature = "\x7F"
"ELF";
std::array<char, valid_elf_signature.size() + 1> elf_signature{};
const std::streampos back_up_pose = file.tellg();
file.seekg(0, std::ios_base::beg);
file.read(elf_signature.data(), 4);
file.seekg(back_up_pose, std::ios_base::beg);
return std::string_view{elf_signature.data(), 4} != valid_elf_signature;
}
[[nodiscard]]
std::optional<FileArch> get_file_arch(std::fstream& file)
{
std::array<char, ei_nident> e_ident{};
const std::streampos back_up_pose = file.tellg();
file.seekg(0, std::ios_base::beg);
file.read(e_ident.data(), e_ident.size());
file.seekg(back_up_pose, std::ios_base::beg);
if (e_ident[ei_class] == elfclass64)
return FileArch::x64;
if (e_ident[ei_class] == elfclass32)
return FileArch::x32;
return std::nullopt;
}
struct ExtractedSection final
{
std::uintptr_t virtual_base_addr{};
std::uintptr_t raw_base_addr{};
std::vector<std::byte> data;
};
[[maybe_unused]]
std::optional<ExtractedSection> get_elf_section_by_name(const std::filesystem::path& path,
const std::string_view& section_name)
{
std::fstream file(path, std::ios::binary | std::ios::in);
if (!file.is_open()) [[unlikely]]
return std::nullopt;
if (not_elf_file(file)) [[unlikely]]
return std::nullopt;
const auto architecture = get_file_arch(file);
if (!architecture.has_value()) [[unlikely]]
return std::nullopt;
std::variant<ElfHeaders<FileArch::x64>, ElfHeaders<FileArch::x32>> elf_headers;
if (architecture.value() == FileArch::x64)
elf_headers = ElfHeaders<FileArch::x64>{};
else if (architecture.value() == FileArch::x32)
elf_headers = ElfHeaders<FileArch::x32>{};
return std::visit(
[&](auto& header) -> std::optional<ExtractedSection>
{
auto& [file_header, section_header] = header;
file.seekg(0, std::ios_base::beg);
if (!file.read(reinterpret_cast<char*>(&file_header), sizeof(file_header))) [[unlikely]]
return std::nullopt;
const std::streamoff shstr_off =
static_cast<std::streamoff>(file_header.e_shoff)
+ static_cast<std::streamoff>(file_header.e_shstrndx) * sizeof(section_header);
file.seekg(shstr_off, std::ios_base::beg);
if (!file.read(reinterpret_cast<char*>(&section_header), sizeof(section_header))) [[unlikely]]
return std::nullopt;
std::vector<char> shstrtab(static_cast<std::size_t>(section_header.sh_size));
file.seekg(section_header.sh_offset, std::ios_base::beg);
if (!file.read(shstrtab.data(), static_cast<std::streamsize>(shstrtab.size()))) [[unlikely]]
return std::nullopt;
for (std::uint16_t i = 0; i < file_header.e_shnum; ++i)
{
decltype(section_header) current_section{};
const std::streamoff off = static_cast<std::streamoff>(file_header.e_shoff)
+ static_cast<std::streamoff>(i) * sizeof(current_section);
file.seekg(off, std::ios_base::beg);
if (!file.read(reinterpret_cast<char*>(&current_section), sizeof(current_section))) [[unlikely]]
return std::nullopt;
if (current_section.sh_name >= shstrtab.size()) [[unlikely]]
continue;
// ReSharper disable once CppTooWideScopeInitStatement
const std::string_view name = &shstrtab[current_section.sh_name];
if (section_name != name)
continue;
ExtractedSection out;
out.virtual_base_addr = static_cast<std::uintptr_t>(current_section.sh_addr);
out.raw_base_addr = static_cast<std::uintptr_t>(current_section.sh_offset);
out.data.resize(static_cast<std::size_t>(current_section.sh_size));
file.seekg(static_cast<std::streamoff>(out.raw_base_addr), std::ios_base::beg);
if (!file.read(reinterpret_cast<char*>(out.data.data()),
static_cast<std::streamsize>(out.data.size()))) [[unlikely]]
return std::nullopt;
return out;
}
return std::nullopt;
},
elf_headers);
}
template<class FileHeader, class SectionHeader>
std::optional<std::uintptr_t> scan_in_module_impl(const std::byte* base, const std::string_view pattern,
const std::string_view target_section_name)
{
const auto* file_header = reinterpret_cast<const FileHeader*>(base);
const auto shoff = static_cast<std::size_t>(file_header->e_shoff);
const auto shnum = static_cast<std::size_t>(file_header->e_shnum);
const auto shstrnd = static_cast<std::size_t>(file_header->e_shstrndx);
const auto shstrtab_off = shoff + shstrnd * sizeof(SectionHeader);
const auto* shstrtab_hdr = reinterpret_cast<const SectionHeader*>(base + shstrtab_off);
const auto shstrtab = reinterpret_cast<const char*>(base + static_cast<std::size_t>(shstrtab_hdr->sh_offset));
const auto shstrtab_size = static_cast<std::size_t>(shstrtab_hdr->sh_size);
for (std::size_t i = 0; i < shnum; ++i)
{
const auto section_off = shoff + i * sizeof(SectionHeader);
const auto* section = reinterpret_cast<const SectionHeader*>(base + section_off);
if (section->sh_size == 0)
continue;
if (std::cmp_greater_equal(section->sh_name, shstrtab_size))
continue;
if (std::string_view{shstrtab + section->sh_name} != target_section_name)
continue;
const auto* section_begin = base + static_cast<std::size_t>(section->sh_addr);
const auto* section_end = section_begin + static_cast<std::size_t>(section->sh_size);
const auto scan_result = omath::PatternScanner::scan_for_pattern(section_begin, section_end, pattern);
if (scan_result == section_end)
return std::nullopt;
return reinterpret_cast<std::uintptr_t>(scan_result);
}
return std::nullopt;
}
} // namespace
namespace omath
{
std::optional<std::uintptr_t>
ElfPatternScanner::scan_for_pattern_in_loaded_module(const void* module_base_address,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
if (module_base_address == nullptr) [[unlikely]]
return std::nullopt;
const auto* base = static_cast<const std::byte*>(module_base_address);
// Validate ELF signature.
constexpr std::string_view valid_elf_signature = "\x7F"
"ELF";
if (std::string_view{reinterpret_cast<const char*>(base), valid_elf_signature.size()} != valid_elf_signature)
return std::nullopt;
// Detect architecture.
const auto ei_class_value = static_cast<uint8_t>(base[ei_class]);
const auto arch = ei_class_value == elfclass64 ? FileArch::x64
: ei_class_value == elfclass32 ? FileArch::x32
: std::optional<FileArch>{};
if (!arch.has_value()) [[unlikely]]
return std::nullopt;
if (arch == FileArch::x64)
return scan_in_module_impl<Elf64Ehdr, Elf64Shdr>(static_cast<const std::byte*>(module_base_address),
pattern, target_section_name);
if (arch == FileArch::x32)
return scan_in_module_impl<Elf32Ehdr, Elf32Shdr>(static_cast<const std::byte*>(module_base_address),
pattern, target_section_name);
std::unreachable();
}
std::optional<SectionScanResult>
ElfPatternScanner::scan_for_pattern_in_file(const std::filesystem::path& path_to_file,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
const auto pe_section = get_elf_section_by_name(path_to_file, target_section_name);
if (!pe_section.has_value()) [[unlikely]]
return std::nullopt;
const auto scan_result =
PatternScanner::scan_for_pattern(pe_section->data.cbegin(), pe_section->data.cend(), pattern);
if (scan_result == pe_section->data.cend())
return std::nullopt;
const auto offset = std::distance(pe_section->data.begin(), scan_result);
return SectionScanResult{.virtual_base_addr = pe_section->virtual_base_addr,
.raw_base_addr = pe_section->raw_base_addr,
.target_offset = offset};
}
} // namespace omath

View File

@@ -239,8 +239,8 @@ namespace
struct ExtractedSection struct ExtractedSection
{ {
std::uint64_t virtual_base_addr; std::uintptr_t virtual_base_addr;
std::uint64_t raw_base_addr; std::uintptr_t raw_base_addr;
std::vector<std::byte> data; std::vector<std::byte> data;
}; };
@@ -261,7 +261,7 @@ namespace
const auto nt_headers = get_nt_header_from_file(file, dos_header); const auto nt_headers = get_nt_header_from_file(file, dos_header);
if (!nt_headers) if (!nt_headers) [[unlikely]]
return std::nullopt; return std::nullopt;
if (invalid_nt_header_file(nt_headers.value())) [[unlikely]] if (invalid_nt_header_file(nt_headers.value())) [[unlikely]]
@@ -290,9 +290,10 @@ namespace
file.seekg(current_section.ptr_raw_data, std::ios::beg); file.seekg(current_section.ptr_raw_data, std::ios::beg);
file.read(reinterpret_cast<char*>(section_data.data()), file.read(reinterpret_cast<char*>(section_data.data()),
static_cast<std::streamsize>(section_data.size())); static_cast<std::streamsize>(section_data.size()));
return ExtractedSection{.virtual_base_addr = current_section.virtual_address return ExtractedSection{
+ concrete_headers.optional_header.image_base, .virtual_base_addr = static_cast<std::uintptr_t>(
.raw_base_addr = current_section.ptr_raw_data, current_section.virtual_address + concrete_headers.optional_header.image_base),
.raw_base_addr = static_cast<std::uintptr_t>(current_section.ptr_raw_data),
.data = std::move(section_data)}; .data = std::move(section_data)};
} }
return std::nullopt; return std::nullopt;
@@ -304,46 +305,71 @@ namespace
namespace omath namespace omath
{ {
std::optional<std::uintptr_t> PePatternScanner::scan_for_pattern_in_loaded_module(const void* module_base_address, std::optional<std::uintptr_t>
const std::string_view& pattern) PePatternScanner::scan_for_pattern_in_loaded_module(const void* module_base_address,
const std::string_view& pattern,
const std::string_view& target_section_name)
{ {
const auto base_address = reinterpret_cast<std::uintptr_t>(module_base_address); const auto base_address = reinterpret_cast<std::uintptr_t>(module_base_address);
const auto* base_bytes = static_cast<const std::byte*>(module_base_address);
if (!base_address) if (!base_address)
return std::nullopt; return std::nullopt;
auto nt_header_variant = get_nt_header_from_loaded_module(module_base_address); const auto* dos_header = static_cast<const DosHeader*>(module_base_address);
if (!nt_header_variant) if (invalid_dos_header_file(*dos_header)) [[unlikely]]
return std::nullopt;
const auto nt_header_variant = get_nt_header_from_loaded_module(module_base_address);
if (!nt_header_variant) [[unlikely]]
return std::nullopt; return std::nullopt;
return std::visit( return std::visit(
[base_address, &pattern](const auto& nt_header) -> std::optional<std::uintptr_t> [base_bytes, base_address, lfanew = dos_header->e_lfanew, &target_section_name,
&pattern](const auto& nt_header) -> std::optional<std::uintptr_t>
{ {
// Define .text segment as scan area constexpr std::size_t signature_size = sizeof(nt_header.signature);
const auto start = nt_header.optional_header.base_of_code; const auto section_table_off = static_cast<std::size_t>(lfanew) + signature_size
const auto scan_size = nt_header.optional_header.size_code; + sizeof(FileHeader) + nt_header.file_header.size_optional_header;
const auto scan_range = std::span{reinterpret_cast<std::byte*>(base_address) + start, scan_size}; const auto* section_table = reinterpret_cast<const SectionHeader*>(base_bytes + section_table_off);
// ReSharper disable once CppTooWideScopeInitStatement for (std::size_t i = 0; i < nt_header.file_header.num_sections; ++i)
const auto result = PatternScanner::scan_for_pattern(scan_range, pattern); {
const auto* section = section_table + i;
if (std::string_view{section->name} != target_section_name || section->size_raw_data == 0)
continue;
const auto section_size = section->virtual_size != 0
? static_cast<std::size_t>(section->virtual_size)
: static_cast<std::size_t>(section->size_raw_data);
const auto* section_begin =
reinterpret_cast<std::byte*>(base_address + section->virtual_address);
const auto scan_range = std::span{section_begin, section_size};
const auto result =
PatternScanner::scan_for_pattern(scan_range.begin(), scan_range.end(), pattern);
if (result != scan_range.end()) if (result != scan_range.end())
return reinterpret_cast<std::uintptr_t>(&*result); return reinterpret_cast<std::uintptr_t>(&*result);
}
return std::nullopt; return std::nullopt;
}, },
nt_header_variant.value()); nt_header_variant.value());
} }
std::optional<PeSectionScanResult> std::optional<SectionScanResult>
PePatternScanner::scan_for_pattern_in_file(const std::filesystem::path& path_to_file, PePatternScanner::scan_for_pattern_in_file(const std::filesystem::path& path_to_file,
const std::string_view& pattern, const std::string_view& pattern,
const std::string_view& target_section_name) const std::string_view& target_section_name)
{ {
const auto pe_section = extract_section_from_pe_file(path_to_file, target_section_name); const auto pe_section = extract_section_from_pe_file(path_to_file, target_section_name);
if (!pe_section.has_value()) if (!pe_section.has_value()) [[unlikely]]
return std::nullopt; return std::nullopt;
const auto scan_result = const auto scan_result =
@@ -353,7 +379,7 @@ namespace omath
return std::nullopt; return std::nullopt;
const auto offset = std::distance(pe_section->data.begin(), scan_result); const auto offset = std::distance(pe_section->data.begin(), scan_result);
return PeSectionScanResult{.virtual_base_addr = pe_section->virtual_base_addr, return SectionScanResult{.virtual_base_addr = pe_section->virtual_base_addr,
.raw_base_addr = pe_section->raw_base_addr, .raw_base_addr = pe_section->raw_base_addr,
.target_offset = offset}; .target_offset = offset};
} }

View File

@@ -26,6 +26,10 @@ if(OMATH_ENABLE_COVERAGE)
omath_setup_coverage(${PROJECT_NAME}) omath_setup_coverage(${PROJECT_NAME})
endif() endif()
if(OMATH_ENABLE_VALGRIND)
omath_setup_valgrind(${PROJECT_NAME})
endif()
# Skip test discovery for Android/iOS builds or when cross-compiling - binaries cannot run on host # Skip test discovery for Android/iOS builds or when cross-compiling - binaries cannot run on host
if (NOT (ANDROID OR IOS OR EMSCRIPTEN)) if (NOT (ANDROID OR IOS OR EMSCRIPTEN))
gtest_discover_tests(${PROJECT_NAME}) gtest_discover_tests(${PROJECT_NAME})

View File

@@ -38,14 +38,14 @@ static void expect_matrix_near(const MatT& a, const MatT& b, float eps = 1e-5f)
// Generic tests for PredEngineTrait behaviour across engines // Generic tests for PredEngineTrait behaviour across engines
TEST(TraitTests, Frostbite_Pred_And_Mesh_And_Camera) TEST(TraitTests, Frostbite_Pred_And_Mesh_And_Camera)
{ {
namespace E = omath::frostbite_engine; namespace e = omath::frostbite_engine;
projectile_prediction::Projectile p; projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f}; p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f; p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f; p.m_gravity_scale = 1.f;
const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.x, 0.f, 1e-4f); EXPECT_NEAR(pos.x, 0.f, 1e-4f);
EXPECT_NEAR(pos.z, 10.f, 1e-4f); EXPECT_NEAR(pos.z, 10.f, 1e-4f);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f); EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
@@ -54,57 +54,57 @@ TEST(TraitTests, Frostbite_Pred_And_Mesh_And_Camera)
t.m_origin = {0.f, 5.f, 0.f}; t.m_origin = {0.f, 5.f, 0.f};
t.m_velocity = {2.f, 0.f, 0.f}; t.m_velocity = {2.f, 0.f, 0.f};
t.m_is_airborne = true; t.m_is_airborne = true;
const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 4.f, 1e-6f); EXPECT_NEAR(pred.x, 4.f, 1e-6f);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
// Also test non-airborne path (no gravity applied) // Also test non-airborne path (no gravity applied)
t.m_is_airborne = false; t.m_is_airborne = false;
const auto pred_ground = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred_ground = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground.x, 4.f, 1e-6f); EXPECT_NEAR(pred_ground.x, 4.f, 1e-6f);
EXPECT_NEAR(pred_ground.y, 5.f, 1e-6f); EXPECT_NEAR(pred_ground.y, 5.f, 1e-6f);
EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
std::optional<float> pitch = 45.f; std::optional<float> pitch = 45.f;
auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
// Direct angles // Direct angles
Vector3<float> origin{0.f, 0.f, 0.f}; Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{0.f, 1.f, 1.f}; Vector3<float> view_to{0.f, 1.f, 1.f};
const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized(); const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f); EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f);
const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.x, dir.z)), 1e-3f); EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.x, dir.z)), 1e-3f);
// MeshTrait simply forwards to rotation_matrix; ensure it compiles and returns something // MeshTrait simply forwards to rotation_matrix; ensure it compiles and returns something
E::ViewAngles va; e::ViewAngles va;
const auto m1 = E::MeshTrait::rotation_matrix(va); const auto m1 = e::MeshTrait::rotation_matrix(va);
const auto m2 = E::rotation_matrix(va); const auto m2 = e::rotation_matrix(va);
expect_matrix_near(m1, m2); expect_matrix_near(m1, m2);
// CameraTrait look at should be callable // CameraTrait look at should be callable
const auto angles = E::CameraTrait::calc_look_at_angle({0, 0, 0}, {0, 1, 1}); const auto angles = e::CameraTrait::calc_look_at_angle({0, 0, 0}, {0, 1, 1});
(void)angles; (void)angles;
const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f); const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f); const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected); expect_matrix_near(proj, expected);
} }
TEST(TraitTests, IW_Pred_And_Mesh_And_Camera) TEST(TraitTests, IW_Pred_And_Mesh_And_Camera)
{ {
namespace E = omath::iw_engine; namespace e = omath::iw_engine;
projectile_prediction::Projectile p; projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f}; p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f; p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f; p.m_gravity_scale = 1.f;
const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.x, 10.f, 1e-4f); EXPECT_NEAR(pos.x, 10.f, 1e-4f);
EXPECT_NEAR(pos.z, -9.81f * 0.5f, 1e-4f); EXPECT_NEAR(pos.z, -9.81f * 0.5f, 1e-4f);
@@ -112,50 +112,50 @@ TEST(TraitTests, IW_Pred_And_Mesh_And_Camera)
t.m_origin = {0.f, 0.f, 5.f}; t.m_origin = {0.f, 0.f, 5.f};
t.m_velocity = {0.f, 0.f, 2.f}; t.m_velocity = {0.f, 0.f, 2.f};
t.m_is_airborne = true; t.m_is_airborne = true;
const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
// predicted = origin + velocity * t -> z = 5 + 2*2 = 9; then gravity applied // predicted = origin + velocity * t -> z = 5 + 2*2 = 9; then gravity applied
EXPECT_NEAR(pred.z, 9.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); EXPECT_NEAR(pred.z, 9.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 4.f, 0.f}), 5.f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 4.f, 0.f}), 5.f, 1e-6f);
EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 3.f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 3.f, 1e-6f);
std::optional<float> pitch = 45.f; std::optional<float> pitch = 45.f;
auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
Vector3<float> origin{0.f, 0.f, 0.f}; Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{1.f, 1.f, 1.f}; Vector3<float> view_to{1.f, 1.f, 1.f};
const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dist = origin.distance_to(view_to); const auto dist = origin.distance_to(view_to);
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin((view_to.z - origin.z) / dist)), 1e-3f); EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin((view_to.z - origin.z) / dist)), 1e-3f);
const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
const auto delta = view_to - origin; const auto delta = view_to - origin;
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(delta.y, delta.x)), 1e-3f); EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(delta.y, delta.x)), 1e-3f);
E::ViewAngles va; e::ViewAngles va;
expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va)); expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(45.f), {1920.f, 1080.f}, 0.1f, 1000.f); const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(45.f), {1920.f, 1080.f}, 0.1f, 1000.f);
const auto expected = E::calc_perspective_projection_matrix(45.f, 1920.f / 1080.f, 0.1f, 1000.f); const auto expected = e::calc_perspective_projection_matrix(45.f, 1920.f / 1080.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected); expect_matrix_near(proj, expected);
// non-airborne // non-airborne
t.m_is_airborne = false; t.m_is_airborne = false;
const auto pred_ground_iw = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred_ground_iw = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground_iw.z, 9.f, 1e-6f); EXPECT_NEAR(pred_ground_iw.z, 9.f, 1e-6f);
} }
TEST(TraitTests, OpenGL_Pred_And_Mesh_And_Camera) TEST(TraitTests, OpenGL_Pred_And_Mesh_And_Camera)
{ {
namespace E = omath::opengl_engine; namespace e = omath::opengl_engine;
projectile_prediction::Projectile p; projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f}; p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f; p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f; p.m_gravity_scale = 1.f;
const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.z, -10.f, 1e-4f); EXPECT_NEAR(pos.z, -10.f, 1e-4f);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f); EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
@@ -163,49 +163,49 @@ TEST(TraitTests, OpenGL_Pred_And_Mesh_And_Camera)
t.m_origin = {0.f, 5.f, 0.f}; t.m_origin = {0.f, 5.f, 0.f};
t.m_velocity = {2.f, 0.f, 0.f}; t.m_velocity = {2.f, 0.f, 0.f};
t.m_is_airborne = true; t.m_is_airborne = true;
const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 4.f, 1e-6f); EXPECT_NEAR(pred.x, 4.f, 1e-6f);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
std::optional<float> pitch = 45.f; std::optional<float> pitch = 45.f;
auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
Vector3<float> origin{0.f, 0.f, 0.f}; Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{0.f, 1.f, 1.f}; Vector3<float> view_to{0.f, 1.f, 1.f};
const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized(); const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f); EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f);
const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(-std::atan2(dir.x, -dir.z)), 1e-3f); EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(-std::atan2(dir.x, -dir.z)), 1e-3f);
E::ViewAngles va; e::ViewAngles va;
expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va)); expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f); const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f); const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected); expect_matrix_near(proj, expected);
// non-airborne // non-airborne
t.m_is_airborne = false; t.m_is_airborne = false;
const auto pred_ground_gl = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred_ground_gl = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground_gl.x, 4.f, 1e-6f); EXPECT_NEAR(pred_ground_gl.x, 4.f, 1e-6f);
} }
TEST(TraitTests, Unity_Pred_And_Mesh_And_Camera) TEST(TraitTests, Unity_Pred_And_Mesh_And_Camera)
{ {
namespace E = omath::unity_engine; namespace e = omath::unity_engine;
projectile_prediction::Projectile p; projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f}; p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f; p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f; p.m_gravity_scale = 1.f;
const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.z, 10.f, 1e-4f); EXPECT_NEAR(pos.z, 10.f, 1e-4f);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f); EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
@@ -213,49 +213,49 @@ TEST(TraitTests, Unity_Pred_And_Mesh_And_Camera)
t.m_origin = {0.f, 5.f, 0.f}; t.m_origin = {0.f, 5.f, 0.f};
t.m_velocity = {2.f, 0.f, 0.f}; t.m_velocity = {2.f, 0.f, 0.f};
t.m_is_airborne = true; t.m_is_airborne = true;
const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 4.f, 1e-6f); EXPECT_NEAR(pred.x, 4.f, 1e-6f);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
std::optional<float> pitch = 45.f; std::optional<float> pitch = 45.f;
auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
Vector3<float> origin{0.f, 0.f, 0.f}; Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{0.f, 1.f, 1.f}; Vector3<float> view_to{0.f, 1.f, 1.f};
const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized(); const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f); EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f);
const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.x, dir.z)), 1e-3f); EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.x, dir.z)), 1e-3f);
E::ViewAngles va; e::ViewAngles va;
expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va)); expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f); const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f); const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected); expect_matrix_near(proj, expected);
// non-airborne // non-airborne
t.m_is_airborne = false; t.m_is_airborne = false;
const auto pred_ground_unity = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred_ground_unity = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground_unity.x, 4.f, 1e-6f); EXPECT_NEAR(pred_ground_unity.x, 4.f, 1e-6f);
} }
TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera) TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
{ {
namespace E = omath::unreal_engine; namespace e = omath::unreal_engine;
projectile_prediction::Projectile p; projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f}; p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f; p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f; p.m_gravity_scale = 1.f;
const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.x, 10.f, 1e-4f); EXPECT_NEAR(pos.x, 10.f, 1e-4f);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f); EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
@@ -263,35 +263,35 @@ TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
t.m_origin = {0.f, 5.f, 0.f}; t.m_origin = {0.f, 5.f, 0.f};
t.m_velocity = {2.f, 0.f, 0.f}; t.m_velocity = {2.f, 0.f, 0.f};
t.m_is_airborne = true; t.m_is_airborne = true;
const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 4.f, 1e-6f); EXPECT_NEAR(pred.x, 4.f, 1e-6f);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
std::optional<float> pitch = 45.f; std::optional<float> pitch = 45.f;
auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
Vector3<float> origin{0.f, 0.f, 0.f}; Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{1.f, 1.f, 1.f}; Vector3<float> view_to{1.f, 1.f, 1.f};
const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized(); const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3f); EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3f);
const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3f); EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3f);
E::ViewAngles va; e::ViewAngles va;
expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va)); expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f); const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f); const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected); expect_matrix_near(proj, expected);
// non-airborne // non-airborne
t.m_is_airborne = false; t.m_is_airborne = false;
const auto pred_ground_unreal = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred_ground_unreal = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground_unreal.x, 4.f, 1e-6f); EXPECT_NEAR(pred_ground_unreal.x, 4.f, 1e-6f);
} }

View File

@@ -1,8 +1,8 @@
// Extra collision tests: Simplex, MeshCollider, EPA // Extra collision tests: Simplex, MeshCollider, EPA
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <omath/collision/simplex.hpp>
#include <omath/collision/mesh_collider.hpp>
#include <omath/collision/epa_algorithm.hpp> #include <omath/collision/epa_algorithm.hpp>
#include <omath/collision/mesh_collider.hpp>
#include <omath/collision/simplex.hpp>
#include <omath/engines/source_engine/collider.hpp> #include <omath/engines/source_engine/collider.hpp>
using namespace omath; using namespace omath;
@@ -26,7 +26,7 @@ TEST(SimplexTest, HandleLineCollinearWithXAxis)
simplex.push_front(Vec3{-1, 0, 0}); simplex.push_front(Vec3{-1, 0, 0});
Vec3 direction{}; Vec3 direction{};
simplex.handle(direction); std::ignore = simplex.handle(direction);
EXPECT_NEAR(direction.x, 0.f, 1e-6f); EXPECT_NEAR(direction.x, 0.f, 1e-6f);
} }
@@ -54,7 +54,8 @@ TEST(CollisionExtra, SimplexTetrahedronInside)
{ {
Simplex<Vector3<float>> s; Simplex<Vector3<float>> s;
// tetra that surrounds origin roughly // tetra that surrounds origin roughly
s = { Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f}, Vector3<float>{0.f,0.f,1.f}, Vector3<float>{-1.f,-1.f,-1.f} }; s = {Vector3<float>{1.f, 0.f, 0.f}, Vector3<float>{0.f, 1.f, 0.f}, Vector3<float>{0.f, 0.f, 1.f},
Vector3<float>{-1.f, -1.f, -1.f}};
Vector3<float> dir{0, 0, 0}; Vector3<float> dir{0, 0, 0};
// if origin inside, handle returns true // if origin inside, handle returns true
const bool inside = s.handle(dir); const bool inside = s.handle(dir);
@@ -63,15 +64,10 @@ TEST(CollisionExtra, SimplexTetrahedronInside)
TEST(CollisionExtra, MeshColliderOriginAndFurthest) TEST(CollisionExtra, MeshColliderOriginAndFurthest)
{ {
omath::source_engine::Mesh mesh = { source_engine::Mesh mesh = {
std::vector<omath::primitives::Vertex<>>{ std::vector<primitives::Vertex<>>{{{1.f, 1.f, 1.f}, {}, {}}, {{-1.f, -1.f, -1.f}, {}, {}}}, {}};
{ { 1.f, 1.f, 1.f }, {}, {} },
{ {-1.f, -1.f, -1.f }, {}, {} }
},
{}
};
mesh.set_origin({0, 2, 0}); mesh.set_origin({0, 2, 0});
omath::source_engine::MeshCollider collider(mesh); source_engine::MeshCollider collider(mesh);
EXPECT_EQ(collider.get_origin(), omath::Vector3<float>(0, 2, 0)); EXPECT_EQ(collider.get_origin(), omath::Vector3<float>(0, 2, 0));
collider.set_origin({1, 2, 3}); collider.set_origin({1, 2, 3});
@@ -85,22 +81,21 @@ TEST(CollisionExtra, MeshColliderOriginAndFurthest)
TEST(CollisionExtra, EPAConvergesOnSimpleCase) TEST(CollisionExtra, EPAConvergesOnSimpleCase)
{ {
// Build two simple colliders using simple meshes that overlap // Build two simple colliders using simple meshes that overlap
omath::source_engine::Mesh meshA = { source_engine::Mesh meshA = {
std::vector<omath::primitives::Vertex<>>{{ {0.f,0.f,0.f}, {}, {} }, { {1.f,0.f,0.f}, {}, {} } }, std::vector<primitives::Vertex<>>{{{0.f, 0.f, 0.f}, {}, {}}, {{1.f, 0.f, 0.f}, {}, {}}}, {}};
{} source_engine::Mesh mesh_b = meshA;
};
omath::source_engine::Mesh mesh_b = meshA;
mesh_b.set_origin({0.5f, 0.f, 0.f}); // translate to overlap mesh_b.set_origin({0.5f, 0.f, 0.f}); // translate to overlap
omath::source_engine::MeshCollider a(meshA); source_engine::MeshCollider a(meshA);
omath::source_engine::MeshCollider b(mesh_b); source_engine::MeshCollider b(mesh_b);
// Create a simplex that approximately contains the origin in Minkowski space // Create a simplex that approximately contains the origin in Minkowski space
Simplex<omath::Vector3<float>> simplex; Simplex<Vector3<float>> simplex;
simplex = { omath::Vector3<float>{0.5f,0.f,0.f}, omath::Vector3<float>{-0.5f,0.f,0.f}, omath::Vector3<float>{0.f,0.5f,0.f}, omath::Vector3<float>{0.f,-0.5f,0.f} }; simplex = {omath::Vector3<float>{0.5f, 0.f, 0.f}, omath::Vector3<float>{-0.5f, 0.f, 0.f},
omath::Vector3<float>{0.f, 0.5f, 0.f}, omath::Vector3<float>{0.f, -0.5f, 0.f}};
auto pool = std::pmr::monotonic_buffer_resource(1024); auto pool = std::pmr::monotonic_buffer_resource(1024);
auto res = Epa<omath::source_engine::MeshCollider>::solve(a, b, simplex, {}, pool); auto res = Epa<source_engine::MeshCollider>::solve(a, b, simplex, {}, pool);
// EPA may or may not converge depending on numerics; ensure it returns optionally // EPA may or may not converge depending on numerics; ensure it returns optionally
// but if it does, fields should be finite // but if it does, fields should be finite
if (res.has_value()) if (res.has_value())

View File

@@ -0,0 +1,17 @@
//
// Created by Vladislav on 30.12.2025.
//
// /Users/vladislav/Downloads/valencia
#include <gtest/gtest.h>
#include <omath/utility/elf_pattern_scan.hpp>
#include <print>
TEST(unit_test_elf_pattern_scan_file, ScanMissingPattern)
{
//FIXME: Implement normal tests :)
//constexpr std::string_view path = "/Users/vladislav/Downloads/crackme";
//const auto res = omath::ElfPatternScanner::scan_for_pattern_in_file(path, "F3 0F 1E FA 55 48 89 E5 B8 00 00 00 00", ".text");
//EXPECT_TRUE(res.has_value());
//std::println("In virtual mem: 0x{:x}", res->virtual_base_addr+res->target_offset);
}

View File

@@ -9,7 +9,7 @@
using Mesh = omath::source_engine::Mesh; using Mesh = omath::source_engine::Mesh;
using Collider = omath::source_engine::MeshCollider; using Collider = omath::source_engine::MeshCollider;
using GJK = omath::collision::GjkAlgorithm<Collider>; using Gjk = omath::collision::GjkAlgorithm<Collider>;
using EPA = omath::collision::Epa<Collider>; using EPA = omath::collision::Epa<Collider>;
TEST(UnitTestEpa, TestCollisionTrue) TEST(UnitTestEpa, TestCollisionTrue)
@@ -37,15 +37,15 @@ TEST(UnitTestEpa, TestCollisionTrue)
Collider A(a), B(b); Collider A(a), B(b);
// GJK // GJK
auto gjk = GJK::is_collide_with_simplex_info(A, B); auto [hit, simplex] = Gjk::is_collide_with_simplex_info(A, B);
ASSERT_TRUE(gjk.hit) << "GJK should report collision"; ASSERT_TRUE(hit) << "GJK should report collision";
// EPA // EPA
EPA::Params params; EPA::Params params;
auto pool = std::make_shared<std::pmr::monotonic_buffer_resource>(1024); auto pool = std::make_shared<std::pmr::monotonic_buffer_resource>(1024);
params.max_iterations = 64; params.max_iterations = 64;
params.tolerance = 1e-4f; params.tolerance = 1e-4f;
auto epa = EPA::solve(A, B, gjk.simplex, params, *pool); auto epa = EPA::solve(A, B, simplex, params, *pool);
ASSERT_TRUE(epa.has_value()) << "EPA should converge"; ASSERT_TRUE(epa.has_value()) << "EPA should converge";
// Normal is unit // Normal is unit
@@ -70,8 +70,8 @@ TEST(UnitTestEpa, TestCollisionTrue)
Collider B_plus(b_plus), B_minus(b_minus); Collider B_plus(b_plus), B_minus(b_minus);
const bool sep_plus = !GJK::is_collide_with_simplex_info(A, B_plus).hit; const bool sep_plus = !Gjk::is_collide_with_simplex_info(A, B_plus).hit;
const bool sep_minus = !GJK::is_collide_with_simplex_info(A, B_minus).hit; const bool sep_minus = !Gjk::is_collide_with_simplex_info(A, B_minus).hit;
// Exactly one direction should separate // Exactly one direction should separate
EXPECT_NE(sep_plus, sep_minus) << "Exactly one of ±penetration must separate"; EXPECT_NE(sep_plus, sep_minus) << "Exactly one of ±penetration must separate";
@@ -81,12 +81,12 @@ TEST(UnitTestEpa, TestCollisionTrue)
Mesh b_resolved = b; Mesh b_resolved = b;
b_resolved.set_origin(b_resolved.get_origin() + resolve); b_resolved.set_origin(b_resolved.get_origin() + resolve);
EXPECT_FALSE(GJK::is_collide(A, Collider(b_resolved))) << "Resolved position should be non-colliding"; EXPECT_FALSE(Gjk::is_collide(A, Collider(b_resolved))) << "Resolved position should be non-colliding";
// Moving the other way should still collide // Moving the other way should still collide
Mesh b_wrong = b; Mesh b_wrong = b;
b_wrong.set_origin(b_wrong.get_origin() - resolve); b_wrong.set_origin(b_wrong.get_origin() - resolve);
EXPECT_TRUE(GJK::is_collide(A, Collider(b_wrong))); EXPECT_TRUE(Gjk::is_collide(A, Collider(b_wrong)));
} }
TEST(UnitTestEpa, TestCollisionTrue2) TEST(UnitTestEpa, TestCollisionTrue2)
{ {
@@ -112,7 +112,7 @@ TEST(UnitTestEpa, TestCollisionTrue2)
Collider A(a), B(b); Collider A(a), B(b);
// --- GJK must detect collision and provide simplex --- // --- GJK must detect collision and provide simplex ---
auto gjk = GJK::is_collide_with_simplex_info(A, B); auto gjk = Gjk::is_collide_with_simplex_info(A, B);
ASSERT_TRUE(gjk.hit) << "GJK should report collision for overlapping cubes"; ASSERT_TRUE(gjk.hit) << "GJK should report collision for overlapping cubes";
// --- EPA penetration --- // --- EPA penetration ---
EPA::Params params; EPA::Params params;
@@ -139,11 +139,11 @@ TEST(UnitTestEpa, TestCollisionTrue2)
// Apply once: B + pen must separate; the opposite must still collide // Apply once: B + pen must separate; the opposite must still collide
Mesh b_resolved = b; Mesh b_resolved = b;
b_resolved.set_origin(b_resolved.get_origin() + pen * margin); b_resolved.set_origin(b_resolved.get_origin() + pen * margin);
EXPECT_FALSE(GJK::is_collide(A, Collider(b_resolved))) << "Applying penetration should separate"; EXPECT_FALSE(Gjk::is_collide(A, Collider(b_resolved))) << "Applying penetration should separate";
Mesh b_wrong = b; Mesh b_wrong = b;
b_wrong.set_origin(b_wrong.get_origin() - pen * margin); b_wrong.set_origin(b_wrong.get_origin() - pen * margin);
EXPECT_TRUE(GJK::is_collide(A, Collider(b_wrong))) << "Opposite direction should still intersect"; EXPECT_TRUE(Gjk::is_collide(A, Collider(b_wrong))) << "Opposite direction should still intersect";
// Some book-keeping sanity // Some book-keeping sanity
EXPECT_GT(epa->iterations, 0); EXPECT_GT(epa->iterations, 0);

View File

@@ -32,10 +32,8 @@ TEST(EpaInternal, SolveHandlesSmallPolytope)
params.max_iterations = 16; params.max_iterations = 16;
params.tolerance = 1e-6f; params.tolerance = 1e-6f;
const auto result = EpaDummy::solve(a, b, s, params);
// Should either return a valid result or gracefully return nullopt // Should either return a valid result or gracefully return nullopt
if (result) if (const auto result = EpaDummy::solve(a, b, s, params))
{ {
EXPECT_TRUE(std::isfinite(result->depth)); EXPECT_TRUE(std::isfinite(result->depth));
EXPECT_TRUE(std::isfinite(result->normal.x)); EXPECT_TRUE(std::isfinite(result->normal.x));

View File

@@ -10,7 +10,8 @@ struct DegenerateCollider
{ {
using VectorType = Vector3f; using VectorType = Vector3f;
// returns furthest point along dir // returns furthest point along dir
VectorType find_abs_furthest_vertex_position(const VectorType& dir) const noexcept [[nodiscard]]
static VectorType find_abs_furthest_vertex_position(const VectorType& dir) noexcept
{ {
// Always return points on a small circle in XY plane so some faces become degenerate // Always return points on a small circle in XY plane so some faces become degenerate
if (dir.x > 0.5f) return {0.01f, 0.f, 0.f}; if (dir.x > 0.5f) return {0.01f, 0.f, 0.f};

View File

@@ -49,6 +49,13 @@ namespace
{ {
Ray ray; Ray ray;
bool expected_clear; // true => segment does NOT hit the triangle bool expected_clear; // true => segment does NOT hit the triangle
friend std::ostream& operator<<(std::ostream& os, const TraceCase& tc)
{
os << "{ RayStart: (" << tc.ray.start.x << ", " << tc.ray.start.y << ", " << tc.ray.start.z << "), "
<< "RayEnd: (" << tc.ray.end.x << ", " << tc.ray.end.y << ", " << tc.ray.end.z << "), "
<< "Expected: " << (tc.expected_clear ? "True" : "False") << " }";
return os;
}
}; };
class CanTraceLineParam : public LineTracerFixture, class CanTraceLineParam : public LineTracerFixture,

View File

@@ -29,9 +29,8 @@ TEST(LineTracerExtra, HitOnEdge)
{ {
constexpr Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0}); constexpr Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
constexpr Ray ray{ {0.0f,0.0f,1.f}, {0.0f,0.0f,0.f}, false }; constexpr Ray ray{ {0.0f,0.0f,1.f}, {0.0f,0.0f,0.f}, false };
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
// hitting exact vertex/edge may be considered miss; ensure function handles without crash // hitting exact vertex/edge may be considered miss; ensure function handles without crash
if (hit != ray.end) if (const auto hit = LineTracer::get_ray_hit_point(ray, tri); hit != ray.end)
{ {
EXPECT_NEAR(hit.x, 0.0f, 1e-6f); EXPECT_NEAR(hit.x, 0.0f, 1e-6f);
EXPECT_NEAR(hit.y, 0.0f, 1e-6f); EXPECT_NEAR(hit.y, 0.0f, 1e-6f);

View File

@@ -11,32 +11,60 @@ static std::vector<std::uint8_t> make_fake_module(std::uint32_t base_of_code,
std::uint32_t size_code, std::uint32_t size_code,
const std::vector<std::uint8_t>& code_bytes) const std::vector<std::uint8_t>& code_bytes)
{ {
// Constants
constexpr std::uint32_t e_lfanew = 0x80; constexpr std::uint32_t e_lfanew = 0x80;
const std::uint32_t total_size = e_lfanew + 0x200 + size_code + 0x100; constexpr std::uint32_t nt_sig = 0x4550; // "PE\0\0"
constexpr std::uint16_t opt_magic = 0x020B; // PE32+
constexpr std::uint16_t num_sections = 1;
constexpr std::uint16_t opt_hdr_size = 0xF0; // Standard PE32+ optional header size
constexpr std::uint32_t section_table_off = e_lfanew + 4 + 20 + opt_hdr_size; // sig(4) + FileHdr(20)
constexpr std::uint32_t section_header_size = 40;
constexpr std::uint32_t text_characteristics = 0x60000020; // code | execute | read
const std::uint32_t headers_end = section_table_off + section_header_size;
const std::uint32_t code_end = base_of_code + size_code;
const std::uint32_t total_size = std::max(headers_end, code_end) + 0x100; // leave some padding
std::vector<std::uint8_t> buf(total_size, 0); std::vector<std::uint8_t> buf(total_size, 0);
// DOS header: e_magic at 0, e_lfanew at offset 0x3C auto w16 = [&](std::size_t off, std::uint16_t v) { std::memcpy(buf.data() + off, &v, sizeof(v)); };
buf[0] = 0x4D; buf[1] = 0x5A; // 'M' 'Z' (little-endian 0x5A4D) auto w32 = [&](std::size_t off, std::uint32_t v) { std::memcpy(buf.data() + off, &v, sizeof(v)); };
constexpr std::uint32_t le = e_lfanew; auto w64 = [&](std::size_t off, std::uint64_t v) { std::memcpy(buf.data() + off, &v, sizeof(v)); };
std::memcpy(buf.data() + 0x3C, &le, sizeof(le));
// NT signature at e_lfanew // DOS header
constexpr std::uint32_t nt_sig = 0x4550; // 'PE\0\0' w16(0x00, 0x5A4D); // e_magic "MZ"
std::memcpy(buf.data() + e_lfanew, &nt_sig, sizeof(nt_sig)); w32(0x3C, e_lfanew); // e_lfanew
// FileHeader is 20 bytes: we only need to ensure its size is present; leave zeros // NT signature
w32(e_lfanew, nt_sig);
// OptionalHeader magic (optional header begins at e_lfanew + 4 + sizeof(FileHeader) == e_lfanew + 24) // FileHeader (starts at e_lfanew + 4)
constexpr std::uint16_t opt_magic = 0x020B; // x64 const std::size_t fh_off = e_lfanew + 4;
std::memcpy(buf.data() + e_lfanew + 24, &opt_magic, sizeof(opt_magic)); w16(fh_off + 2, num_sections); // NumberOfSections
w16(fh_off + 16, opt_hdr_size); // SizeOfOptionalHeader
// size_code is at offset 4 inside OptionalHeader -> absolute e_lfanew + 28 // OptionalHeader PE32+ (starts at e_lfanew + 4 + 20)
std::memcpy(buf.data() + e_lfanew + 28, &size_code, sizeof(size_code)); const std::size_t opt_off = fh_off + 20;
w16(opt_off + 0, opt_magic); // Magic
w32(opt_off + 4, size_code); // SizeOfCode
w32(opt_off + 16, 0); // AddressOfEntryPoint (unused in test)
w32(opt_off + 20, base_of_code); // BaseOfCode
w64(opt_off + 24, 0); // ImageBase
w32(opt_off + 32, 0x1000); // SectionAlignment
w32(opt_off + 36, 0x200); // FileAlignment
w32(opt_off + 56, code_end); // SizeOfImage (simple upper bound)
w32(opt_off + 60, headers_end); // SizeOfHeaders
w32(opt_off + 108, 0); // NumberOfRvaAndSizes (0 directories)
// base_of_code is at offset 20 inside OptionalHeader -> absolute e_lfanew + 44 // Section header (.text) at section_table_off
std::memcpy(buf.data() + e_lfanew + 44, &base_of_code, sizeof(base_of_code)); const std::size_t sh_off = section_table_off;
std::memcpy(buf.data() + sh_off + 0, ".text", 5); // Name[8]
w32(sh_off + 8, size_code); // VirtualSize
w32(sh_off + 12, base_of_code); // VirtualAddress
w32(sh_off + 16, size_code); // SizeOfRawData
w32(sh_off + 20, base_of_code); // PointerToRawData
w32(sh_off + 36, text_characteristics); // Characteristics
// place code bytes at offset base_of_code // Place code bytes at BaseOfCode
if (base_of_code + code_bytes.size() <= buf.size()) if (base_of_code + code_bytes.size() <= buf.size())
std::memcpy(buf.data() + base_of_code, code_bytes.data(), code_bytes.size()); std::memcpy(buf.data() + base_of_code, code_bytes.data(), code_bytes.size());
@@ -59,11 +87,12 @@ TEST(PePatternScanLoaded, FindsPatternAtBase)
TEST(PePatternScanLoaded, WildcardMatches) TEST(PePatternScanLoaded, WildcardMatches)
{ {
const std::vector<std::uint8_t> code = {0xDE, 0xAD, 0xBE, 0xEF}; const std::vector<std::uint8_t> code = {0xDE, 0xAD, 0xBE, 0xEF};
auto buf = make_fake_module(0x300, static_cast<std::uint32_t>(code.size()), code); constexpr std::uint32_t base_of_code = 0x300;
auto buf = make_fake_module(base_of_code, static_cast<std::uint32_t>(code.size()), code);
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE ?? BE"); const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE ?? BE", ".text");
ASSERT_TRUE(res.has_value()); ASSERT_TRUE(res.has_value());
const uintptr_t addr = res.value(); const uintptr_t addr = res.value();
const uintptr_t base = reinterpret_cast<uintptr_t>(buf.data()); const uintptr_t base = reinterpret_cast<uintptr_t>(buf.data());
EXPECT_EQ(addr - base, 0x300u); EXPECT_EQ(addr - base, base_of_code);
} }

View File

@@ -163,6 +163,23 @@ TEST(unit_test_pe_pattern_scan_more, LoadedModuleScanFinds)
std::uint32_t size_headers; /* keep space */ std::uint32_t size_headers; /* keep space */
std::uint8_t pad[200]; std::uint8_t pad[200];
}; };
struct SectionHeader
{
char name[8];
union
{
std::uint32_t physical_address;
std::uint32_t virtual_size;
};
std::uint32_t virtual_address;
std::uint32_t size_raw_data;
std::uint32_t ptr_raw_data;
std::uint32_t ptr_relocs;
std::uint32_t ptr_line_numbers;
std::uint32_t num_relocs;
std::uint32_t num_line_numbers;
std::uint32_t characteristics;
};
struct ImageNtHeadersX64 struct ImageNtHeadersX64
{ {
std::uint32_t signature; std::uint32_t signature;
@@ -176,22 +193,41 @@ TEST(unit_test_pe_pattern_scan_more, LoadedModuleScanFinds)
const std::uint32_t bufsize = 0x400 + size_code; const std::uint32_t bufsize = 0x400 + size_code;
std::vector<std::uint8_t> buf(bufsize, 0); std::vector<std::uint8_t> buf(bufsize, 0);
// DOS header // DOS header
const auto dos = reinterpret_cast<DosHeader*>(buf.data()); const auto dos = reinterpret_cast<DosHeader*>(buf.data());
dos->e_magic = 0x5A4D; dos->e_magic = 0x5A4D;
dos->e_lfanew = 0x80; dos->e_lfanew = 0x80;
// NT headers // NT headers
const auto nt = reinterpret_cast<ImageNtHeadersX64*>(buf.data() + dos->e_lfanew); const auto nt = reinterpret_cast<ImageNtHeadersX64*>(buf.data() + dos->e_lfanew);
nt->signature = 0x4550; // 'PE\0\0' nt->signature = 0x4550; // 'PE\0\0'
nt->file_header.machine = 0x8664; nt->file_header.machine = 0x8664;
nt->file_header.num_sections = 1; nt->file_header.num_sections = 1;
nt->file_header.size_optional_header = static_cast<std::uint16_t>(sizeof(OptionalHeaderX64));
nt->optional_header.magic = 0x020B; // x64 nt->optional_header.magic = 0x020B; // x64
nt->optional_header.base_of_code = base_of_code; nt->optional_header.base_of_code = base_of_code;
nt->optional_header.size_code = size_code; nt->optional_header.size_code = size_code;
// Compute section table offset: e_lfanew + 4 (sig) + FileHeader + OptionalHeader
const std::size_t section_table_off =
static_cast<std::size_t>(dos->e_lfanew) + 4 + sizeof(FileHeader) + sizeof(OptionalHeaderX64);
nt->optional_header.size_headers = static_cast<std::uint32_t>(section_table_off + sizeof(SectionHeader));
// Section header (.text)
const auto sect = reinterpret_cast<SectionHeader*>(buf.data() + section_table_off);
std::memset(sect, 0, sizeof(SectionHeader));
std::memcpy(sect->name, ".text", 5);
sect->virtual_size = size_code;
sect->virtual_address = base_of_code;
sect->size_raw_data = size_code;
sect->ptr_raw_data = base_of_code;
sect->characteristics = 0x60000020; // code | execute | read
// place code at base_of_code // place code at base_of_code
std::memcpy(buf.data() + base_of_code, pattern_bytes.data(), pattern_bytes.size()); std::memcpy(buf.data() + base_of_code, pattern_bytes.data(), pattern_bytes.size());
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE AD BE EF"); const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE AD BE EF", ".text");
EXPECT_TRUE(res.has_value()); EXPECT_TRUE(res.has_value());
} }

View File

@@ -8,39 +8,51 @@
class Player final class Player final
{ {
public: public:
virtual int foo() {return 1;} [[nodiscard]] virtual int foo() const
virtual int bar() {return 2;} {
return 1;
}
[[nodiscard]] virtual int bar() const
{
return 2;
}
omath::Vector3<float> m_origin{1.f, 2.f, 3.f}; omath::Vector3<float> m_origin{1.f, 2.f, 3.f};
int m_health{123}; int m_health{123};
}; };
class RevPlayer : omath::rev_eng::InternalReverseEngineeredObject class RevPlayer final : omath::rev_eng::InternalReverseEngineeredObject
{ {
public: public:
omath::Vector3<float> get_origin() [[nodiscard]]
omath::Vector3<float> get_origin() const
{ {
return get_by_offset<omath::Vector3<float>>(sizeof(std::uintptr_t)); return get_by_offset<omath::Vector3<float>>(sizeof(std::uintptr_t));
} }
int get_health()
[[nodiscard]]
int get_health() const
{ {
return get_by_offset<int>(sizeof(std::uintptr_t) + sizeof(omath::Vector3<float>)); return get_by_offset<int>(sizeof(std::uintptr_t) + sizeof(omath::Vector3<float>));
} }
int rev_foo() [[nodiscard]]
int rev_foo() const
{ {
return call_virtual_method<0, int>(); return call_virtual_method<0, int>();
} }
int rev_bar()
[[nodiscard]]
int rev_bar() const
{ {
return call_virtual_method<1, int>(); return call_virtual_method<1, int>();
} }
[[nodiscard]] int rev_bar_const() const [[nodiscard]] int rev_bar_const() const
{ {
return call_virtual_method<1, int>(); return call_virtual_method<1, int>();
} }
}; };
TEST(unit_test_reverse_enineering, read_test) TEST(unit_test_reverse_enineering, read_test)
{ {
Player player_original; Player player_original;