Compare commits

...

76 Commits

Author SHA1 Message Date
7567501f00 Merge pull request #175 from orange-cpp/feature/w2s_no_clip
added clip option
2026-03-21 14:12:07 +03:00
46d999f846 added clip option 2026-03-21 13:58:06 +03:00
b54601132b added doc build to release 2026-03-21 06:32:05 +03:00
5c8ce2d163 Merge pull request #174 from orange-cpp/feature/docs-pipelines
added docs pipeline
2026-03-21 06:26:21 +03:00
04a86739b4 added docs pipeline 2026-03-21 06:11:20 +03:00
575b411863 updated install md 2026-03-21 06:05:29 +03:00
5a91151bc0 fix 2026-03-19 20:27:25 +03:00
66d4df0524 fix 2026-03-19 20:17:10 +03:00
54e14760ca fix 2026-03-19 20:09:07 +03:00
ee61c47d7d Merge pull request #173 from orange-cpp/feature/targeting_algorithms
Feature/targeting algorithms
2026-03-19 19:52:22 +03:00
d737aee1c5 added by distance targeting 2026-03-19 19:29:01 +03:00
ef422f0a86 added overload 2026-03-19 19:23:39 +03:00
e99ca0bc2b update 2026-03-19 19:19:42 +03:00
5f94e36965 fix for windows specific suff related to far near macroses 2026-03-19 15:32:05 +03:00
29510cf9e7 Removed from credit by own request 2026-03-19 15:24:35 +03:00
927508a76b Merge pull request #172 from orange-cpp/feaute/methods_calling_improvement
Feaute/methods calling improvement
2026-03-19 01:33:42 +03:00
f390b386d7 fix 2026-03-19 01:06:16 +03:00
012d837e8b fix windows x32 bit 2026-03-19 00:57:54 +03:00
6236c8fd68 added nodiscard 2026-03-18 21:24:35 +03:00
06dc36089f added overload 2026-03-18 21:19:09 +03:00
91136a61c4 improvement 2026-03-18 21:12:18 +03:00
9cdffcbdb1 added tests 2026-03-18 20:12:46 +03:00
a3e93ac259 added nttp 2026-03-18 20:05:32 +03:00
59f6d7a361 added call_method 2026-03-18 19:58:52 +03:00
dcf1ef1ea9 Merge pull request #171 from orange-cpp/feaute/projectile_pred_improvement
Feaute/projectile pred improvement
2026-03-17 21:58:59 +03:00
89bd879187 added tolerance depending on arch 2026-03-17 21:15:39 +03:00
aa08c7cb65 improved projectile prediction 2026-03-17 20:43:26 +03:00
a5c0ca0cbd added stuff 2026-03-17 20:31:46 +03:00
624683aed6 added unreachanble 2026-03-17 19:53:15 +03:00
f46672b2c6 Merge pull request #170 from orange-cpp/feature/projectile_aim_widget
add projectile
2026-03-17 19:51:12 +03:00
b8e61f49fa add projectile 2026-03-17 19:36:35 +03:00
37ea091282 Merge pull request #169 from orange-cpp/feaute/hud_features
Feaute/hud features
2026-03-16 14:39:58 +03:00
29a2743728 renamed args 2026-03-16 13:17:16 +03:00
1117eb37f1 added icon 2026-03-16 13:13:41 +03:00
b6b0d4db13 added aim dot 2026-03-16 03:24:53 +03:00
2e8a74aaaf imroved spacer 2026-03-16 03:06:14 +03:00
d8632dc74c added progress ring 2026-03-16 03:03:23 +03:00
fd531c930c added spacer 2026-03-16 02:21:24 +03:00
a91673216d added const 2026-03-16 02:10:05 +03:00
6487554844 corrected code style 2026-03-16 01:54:45 +03:00
1744172694 updated credits 2026-03-15 20:42:13 +03:00
114b2a6e58 Update README to enhance library description and features 2026-03-15 20:21:08 +03:00
d90a85d8b6 Merge pull request #168 from orange-cpp/feature/hud_declarative
Feature/hud declarative
2026-03-15 20:02:32 +03:00
e0a7179812 fix 2026-03-15 19:43:55 +03:00
a99dd24d6b improvement 2026-03-15 19:39:02 +03:00
d62dec9a8f changed api 2026-03-15 19:10:15 +03:00
1a176d8f09 fix 2026-03-15 18:48:22 +03:00
8e6ed19abf added dashed bar 2026-03-15 18:39:40 +03:00
311ab45722 Merge pull request #167 from orange-cpp/feaute/sig_scan_file_in_mem
added stuff
2026-03-15 17:37:42 +03:00
130277c1ae refactored test 2026-03-15 17:20:28 +03:00
4f1c42d6f6 tests fix 2026-03-15 17:04:21 +03:00
ccea4a0f0d added stuff 2026-03-15 16:54:47 +03:00
3fb98397e4 Merge pull request #166 from orange-cpp/feature/hud_improvement
Feature/hud improvement
2026-03-15 14:01:33 +03:00
56256c40fb cleaned code 2026-03-15 13:47:41 +03:00
46c94ae541 decomposed Run 2026-03-15 13:44:25 +03:00
a45f095b9c added skeleton 2026-03-15 04:59:47 +03:00
e849d23c47 improved dashed box 2026-03-15 04:56:10 +03:00
adad66599a adde dash box 2026-03-15 04:49:01 +03:00
69bdfc3307 improved example 2026-03-15 04:43:19 +03:00
55304c5df1 fixed bug 2026-03-15 04:28:56 +03:00
19d796cd4e improvement 2026-03-15 04:23:07 +03:00
d31ea6ed4d added more stuff 2026-03-15 04:17:30 +03:00
977d772687 fix 2026-03-13 22:20:57 +03:00
746f1b84a8 hot fix 2026-03-13 22:16:42 +03:00
af399a14ed Merge pull request #165 from orange-cpp/feature/hud
Feature/hud
2026-03-13 22:11:26 +03:00
6fb420642b updated props 2026-03-13 21:58:14 +03:00
6a2b4b90b4 fix 2026-03-13 21:49:56 +03:00
371d8154ee fix 2026-03-13 21:40:30 +03:00
d6a2165f83 fix 2026-03-13 21:37:03 +03:00
bb1b5ad14a removed shit 2026-03-13 21:32:44 +03:00
f188257e0f added stuff 2026-03-13 21:28:16 +03:00
87966c82b9 added realization 2026-03-13 21:09:12 +03:00
9da19582b5 added files 2026-03-13 20:51:59 +03:00
29f3e2565d Merge pull request #164 from orange-cpp/feaute/disk_optimization
avoid saving files on disk
2026-03-13 03:55:56 +03:00
e083b15e0b update 2026-03-13 03:42:12 +03:00
ed9da79d08 avoid saving files on disk 2026-03-13 03:33:57 +03:00
59 changed files with 4555 additions and 584 deletions

View File

@@ -12,6 +12,7 @@ AlignConsecutiveMacros: AcrossEmptyLinesAndComments
AlignTrailingComments: false AlignTrailingComments: false
AllowShortBlocksOnASingleLine: Never AllowShortBlocksOnASingleLine: Never
AllowShortFunctionsOnASingleLine: None AllowShortFunctionsOnASingleLine: None
AllowShortLambdasOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false AllowShortLoopsOnASingleLine: false
BreakTemplateDeclarations: Leave BreakTemplateDeclarations: Leave

View File

@@ -370,6 +370,8 @@ jobs:
shell: bash shell: bash
run: | run: |
cmake --preset ${{ matrix.preset }} \ cmake --preset ${{ matrix.preset }} \
-DCMAKE_C_COMPILER=$(xcrun --find clang) \
-DCMAKE_CXX_COMPILER=$(xcrun --find clang++) \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \ -DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \
@@ -380,6 +382,7 @@ jobs:
run: cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath run: cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
- name: Run unit_tests - name: Run unit_tests
if: ${{ matrix.coverage != true }}
shell: bash shell: bash
run: ./out/Release/unit_tests run: ./out/Release/unit_tests

62
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Documentation
on:
push:
branches: [ main ]
paths:
- 'docs/**'
- 'mkdocs.yml'
- '.github/workflows/docs.yml'
pull_request:
branches: [ main ]
paths:
- 'docs/**'
- 'mkdocs.yml'
- '.github/workflows/docs.yml'
concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
name: Build Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install mkdocs and dependencies
run: pip install mkdocs mkdocs-bootswatch
- name: Build documentation
run: mkdocs build --strict
- name: Upload artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
with:
path: site/
deploy:
name: Deploy to GitHub Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -12,6 +12,35 @@ permissions:
contents: write contents: write
jobs: jobs:
##############################################################################
# 0) Documentation MkDocs
##############################################################################
docs-release:
name: Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install mkdocs and dependencies
run: pip install mkdocs mkdocs-bootswatch
- name: Build documentation
run: mkdocs build --strict
- name: Package
run: tar -czf omath-docs.tar.gz -C site .
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${{ github.event.release.tag_name }}" omath-docs.tar.gz --clobber
############################################################################## ##############################################################################
# 1) Linux Clang / Ninja # 1) Linux Clang / Ninja
############################################################################## ##############################################################################

View File

@@ -3,8 +3,8 @@
Thanks to everyone who made this possible, including: Thanks to everyone who made this possible, including:
- Saikari aka luadebug for VCPKG port and awesome new initial logo design. - Saikari aka luadebug for VCPKG port and awesome new initial logo design.
- AmbushedRaccoon for telegram post about omath to boost repository activity.
- Billy O'Neal aka BillyONeal for fixing compilation issues due to C math library compatibility. - Billy O'Neal aka BillyONeal for fixing compilation issues due to C math library compatibility.
- Alex2772 for reference of AUI declarative interface design for omath::hud
And a big hand to everyone else who has contributed over the past! And a big hand to everyone else who has contributed over the past!

View File

@@ -28,6 +28,29 @@ target("...")
add_packages("omath") add_packages("omath")
``` ```
## <img width="28px" src="https://conan.io/favicon.png" /> Using Conan
**Note**: Support Conan for package management
1. Install [Conan](https://conan.io/downloads)
2. Run the following command to install the omath package:
```
conan install --requires="omath/[*]" --build=missing
```
conanfile.txt
```ini
[requires]
omath/[*]
[generators]
CMakeDeps
CMakeToolchain
```
CMakeLists.txt
```cmake
find_package(omath CONFIG REQUIRED)
target_link_libraries(main PRIVATE omath::omath)
```
For more details, see the [Conan documentation](https://docs.conan.io/2/).
## <img width="28px" src="https://github.githubassets.com/favicons/favicon.svg" /> Using prebuilt binaries (GitHub Releases) ## <img width="28px" src="https://github.githubassets.com/favicons/favicon.svg" /> Using prebuilt binaries (GitHub Releases)
**Note**: This is the fastest option if you dont want to build from source. **Note**: This is the fastest option if you dont want to build from source.

View File

@@ -14,7 +14,7 @@
[![discord badge](https://dcbadge.limes.pink/api/server/https://discord.gg/eDgdaWbqwZ?style=flat)](https://discord.gg/eDgdaWbqwZ) [![discord badge](https://dcbadge.limes.pink/api/server/https://discord.gg/eDgdaWbqwZ?style=flat)](https://discord.gg/eDgdaWbqwZ)
[![telegram badge](https://img.shields.io/badge/Telegram-2CA5E0?style=flat-squeare&logo=telegram&logoColor=white)](https://t.me/orangennotes) [![telegram badge](https://img.shields.io/badge/Telegram-2CA5E0?style=flat-squeare&logo=telegram&logoColor=white)](https://t.me/orangennotes)
OMath is a 100% independent, constexpr template blazingly fast math library that doesn't have legacy C++ code. OMath is a 100% independent, constexpr template blazingly fast math/physics/games/mods/cheats development framework that doesn't have legacy C++ code.
It provides the latest features, is highly customizable, has all for cheat development, DirectX/OpenGL/Vulkan support, premade support for different game engines, much more constexpr stuff than in other libraries and more... It provides the latest features, is highly customizable, has all for cheat development, DirectX/OpenGL/Vulkan support, premade support for different game engines, much more constexpr stuff than in other libraries and more...
<br> <br>
@@ -84,7 +84,8 @@ if (auto screen = camera.world_to_screen(world_position)) {
- **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine, CryEngine and canonical OpenGL**. - **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine, CryEngine 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 ELF/Mach-O/PE files/modules, binary slices, works even with Wine apps. - **Algorithms**: Has ability to scan for byte pattern with wildcards in ELF/Mach-O/PE files/modules, binary slices, works even with Wine apps.
- **Scripting**: Supports to make scripts in Lua out of box - **Scripting**: Supports to make scripts in Lua out of box.
- **Handy**: Allow to design wall hacks in modern jetpack compose like way.
- **Battle tested**: It's already used by some big players on the market like wraith.su and bluedream.ltd - **Battle tested**: It's already used by some big players on the market like wraith.su and bluedream.ltd
<div align = center> <div align = center>

View File

@@ -1,6 +1,6 @@
# Installation # Installation Guide
## <img width="28px" src="https://vcpkg.io/assets/mark/mark.svg" /> Using vcpkg ## <img width="28px" src="https://vcpkg.io/assets/mark/mark.svg" /> Using vcpkg (recomended)
**Note**: Support vcpkg for package management **Note**: Support vcpkg for package management
1. Install [vcpkg](https://github.com/microsoft/vcpkg) 1. Install [vcpkg](https://github.com/microsoft/vcpkg)
2. Run the following command to install the orange-math package: 2. Run the following command to install the orange-math package:
@@ -28,6 +28,69 @@ target("...")
add_packages("omath") add_packages("omath")
``` ```
## <img width="28px" src="https://conan.io/favicon.png" /> Using Conan
**Note**: Support Conan for package management
1. Install [Conan](https://conan.io/downloads)
2. Run the following command to install the omath package:
```
conan install --requires="omath/[*]" --build=missing
```
conanfile.txt
```ini
[requires]
omath/[*]
[generators]
CMakeDeps
CMakeToolchain
```
CMakeLists.txt
```cmake
find_package(omath CONFIG REQUIRED)
target_link_libraries(main PRIVATE omath::omath)
```
For more details, see the [Conan documentation](https://docs.conan.io/2/).
## <img width="28px" src="https://github.githubassets.com/favicons/favicon.svg" /> Using prebuilt binaries (GitHub Releases)
**Note**: This is the fastest option if you dont want to build from source.
1. **Go to the Releases page**
- Open the projects GitHub **Releases** page and choose the latest version.
2. **Download the correct asset for your platform**
- Pick the archive that matches your OS and architecture (for example: Windows x64 / Linux x64 / macOS arm64).
3. **Extract the archive**
- You should end up with something like:
- `include/` (headers)
- `lib/` or `bin/` (library files / DLLs)
- sometimes `cmake/` (CMake package config)
4. **Use it in your project**
### Option A: CMake package (recommended if the release includes CMake config files)
If the extracted folder contains something like `lib/cmake/omath` or `cmake/omath`, you can point CMake to it:
```cmake
# Example: set this to the extracted prebuilt folder
list(APPEND CMAKE_PREFIX_PATH "path/to/omath-prebuilt")
find_package(omath CONFIG REQUIRED)
target_link_libraries(main PRIVATE omath::omath)
```
### Option B: Manual include + link (works with any layout)
If theres no CMake package config, link it manually:
```cmake
target_include_directories(main PRIVATE "path/to/omath-prebuilt/include")
# Choose ONE depending on what you downloaded:
# - Static library: .lib / .a
# - Shared library: .dll + .lib import (Windows), .so (Linux), .dylib (macOS)
target_link_directories(main PRIVATE "path/to/omath-prebuilt/lib")
target_link_libraries(main PRIVATE omath) # or the actual library filename
```
## <img width="28px" src="https://upload.wikimedia.org/wikipedia/commons/e/ef/CMake_logo.svg?" /> Build from source using CMake ## <img width="28px" src="https://upload.wikimedia.org/wikipedia/commons/e/ef/CMake_logo.svg?" /> Build from source using CMake
1. **Preparation** 1. **Preparation**
@@ -62,7 +125,7 @@ target("...")
Use **\<platform\>-\<build configuration\>** preset to build suitable version for yourself. Like **windows-release** or **linux-release**. Use **\<platform\>-\<build configuration\>** preset to build suitable version for yourself. Like **windows-release** or **linux-release**.
| Platform Name | Build Config | | Platform Name | Build Config |
|---------------|---------------| |---------------|---------------|
| windows | release/debug | | windows | release/debug |
| linux | release/debug | | linux | release/debug |
| darwin | release/debug | | darwin | release/debug |

View File

@@ -2,6 +2,7 @@ add_subdirectory(example_barycentric)
add_subdirectory(example_glfw3) add_subdirectory(example_glfw3)
add_subdirectory(example_proj_mat_builder) add_subdirectory(example_proj_mat_builder)
add_subdirectory(example_signature_scan) add_subdirectory(example_signature_scan)
add_subdirectory(example_hud)
if(OMATH_ENABLE_VALGRIND) if(OMATH_ENABLE_VALGRIND)
omath_setup_valgrind(example_projection_matrix_builder) omath_setup_valgrind(example_projection_matrix_builder)

View File

@@ -0,0 +1,16 @@
project(example_hud)
add_executable(${PROJECT_NAME} main.cpp gui/main_window.cpp gui/main_window.hpp)
set_target_properties(
${PROJECT_NAME}
PROPERTIES CXX_STANDARD 23
ARCHIVE_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}")
find_package(OpenGL)
find_package(GLEW REQUIRED)
find_package(glfw3 CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE glfw imgui::imgui omath::omath OpenGL::GL)

View File

@@ -0,0 +1,263 @@
//
// Created by Orange on 11/11/2024.
//
#include "main_window.hpp"
#include "omath/hud/renderer_realizations/imgui_renderer.hpp"
#include <GLFW/glfw3.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <omath/hud/entity_overlay.hpp>
namespace imgui_desktop::gui
{
bool MainWindow::m_canMoveWindow = false;
MainWindow::MainWindow(const std::string_view& caption, int width, int height)
{
if (!glfwInit())
std::exit(EXIT_FAILURE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, true);
m_window = glfwCreateWindow(width, height, caption.data(), nullptr, nullptr);
glfwMakeContextCurrent(m_window);
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGui::GetStyle().Colors[ImGuiCol_WindowBg] = {0.05f, 0.05f, 0.05f, 0.92f};
ImGui::GetStyle().AntiAliasedLines = false;
ImGui::GetStyle().AntiAliasedFill = false;
ImGui_ImplGlfw_InitForOpenGL(m_window, true);
ImGui_ImplOpenGL3_Init("#version 150");
}
void MainWindow::Run()
{
while (!glfwWindowShouldClose(m_window) && m_opened)
{
glfwPollEvents();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
const auto* vp = ImGui::GetMainViewport();
ImGui::GetBackgroundDrawList()->AddRectFilled({}, vp->Size, ImColor(30, 30, 30, 220));
draw_controls();
draw_overlay();
ImGui::Render();
present();
}
glfwDestroyWindow(m_window);
}
void MainWindow::draw_controls()
{
const auto* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos({0.f, 0.f});
ImGui::SetNextWindowSize({280.f, vp->Size.y});
ImGui::Begin("Controls", &m_opened,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
ImGui::PushItemWidth(160.f);
if (ImGui::CollapsingHeader("Entity", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::SliderFloat("X##ent", &m_entity_x, 100.f, vp->Size.x - 100.f);
ImGui::SliderFloat("Top Y", &m_entity_top_y, 20.f, m_entity_bottom_y - 20.f);
ImGui::SliderFloat("Bottom Y", &m_entity_bottom_y, m_entity_top_y + 20.f, vp->Size.y - 20.f);
}
if (ImGui::CollapsingHeader("Box", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Checkbox("Box##chk", &m_show_box);
ImGui::SameLine();
ImGui::Checkbox("Cornered", &m_show_cornered_box);
ImGui::SameLine();
ImGui::Checkbox("Dashed", &m_show_dashed_box);
ImGui::ColorEdit4("Color##box", reinterpret_cast<float*>(&m_box_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Fill##box", reinterpret_cast<float*>(&m_box_fill), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thickness", &m_box_thickness, 0.5f, 5.f);
ImGui::SliderFloat("Corner ratio", &m_corner_ratio, 0.05f, 0.5f);
ImGui::Separator();
ImGui::ColorEdit4("Dash color", reinterpret_cast<float*>(&m_dash_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Dash length", &m_dash_len, 2.f, 30.f);
ImGui::SliderFloat("Dash gap", &m_dash_gap, 1.f, 20.f);
ImGui::SliderFloat("Dash thick", &m_dash_thickness, 0.5f, 5.f);
}
if (ImGui::CollapsingHeader("Bars", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::ColorEdit4("Color##bar", reinterpret_cast<float*>(&m_bar_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("BG##bar", reinterpret_cast<float*>(&m_bar_bg_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Outline##bar", reinterpret_cast<float*>(&m_bar_outline_color),
ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Width##bar", &m_bar_width, 1.f, 20.f);
ImGui::SliderFloat("Value##bar", &m_bar_value, 0.f, 1.f);
ImGui::SliderFloat("Offset##bar", &m_bar_offset, 1.f, 20.f);
ImGui::Checkbox("Right##bar", &m_show_right_bar);
ImGui::SameLine();
ImGui::Checkbox("Left##bar", &m_show_left_bar);
ImGui::Checkbox("Top##bar", &m_show_top_bar);
ImGui::SameLine();
ImGui::Checkbox("Bottom##bar", &m_show_bottom_bar);
ImGui::Checkbox("Right dashed##bar", &m_show_right_dashed_bar);
ImGui::SameLine();
ImGui::Checkbox("Left dashed##bar", &m_show_left_dashed_bar);
ImGui::Checkbox("Top dashed##bar", &m_show_top_dashed_bar);
ImGui::SameLine();
ImGui::Checkbox("Bot dashed##bar", &m_show_bottom_dashed_bar);
ImGui::SliderFloat("Dash len##bar", &m_bar_dash_len, 2.f, 20.f);
ImGui::SliderFloat("Dash gap##bar", &m_bar_dash_gap, 1.f, 15.f);
}
if (ImGui::CollapsingHeader("Labels", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Checkbox("Outlined", &m_outlined);
ImGui::SliderFloat("Offset##lbl", &m_label_offset, 0.f, 15.f);
ImGui::Checkbox("Right##lbl", &m_show_right_labels);
ImGui::SameLine();
ImGui::Checkbox("Left##lbl", &m_show_left_labels);
ImGui::Checkbox("Top##lbl", &m_show_top_labels);
ImGui::SameLine();
ImGui::Checkbox("Bottom##lbl", &m_show_bottom_labels);
ImGui::Checkbox("Ctr top##lbl", &m_show_centered_top);
ImGui::SameLine();
ImGui::Checkbox("Ctr bot##lbl", &m_show_centered_bottom);
}
if (ImGui::CollapsingHeader("Skeleton"))
{
ImGui::Checkbox("Show##skel", &m_show_skeleton);
ImGui::ColorEdit4("Color##skel", reinterpret_cast<float*>(&m_skel_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thick##skel", &m_skel_thickness, 0.5f, 5.f);
}
if (ImGui::CollapsingHeader("Progress Ring"))
{
ImGui::Checkbox("Show##ring", &m_show_ring);
ImGui::ColorEdit4("Color##ring", reinterpret_cast<float*>(&m_ring_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("BG##ring", reinterpret_cast<float*>(&m_ring_bg), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Radius##ring", &m_ring_radius, 4.f, 30.f);
ImGui::SliderFloat("Value##ring", &m_ring_ratio, 0.f, 1.f);
ImGui::SliderFloat("Thick##ring", &m_ring_thickness, 0.5f, 6.f);
ImGui::SliderFloat("Offset##ring", &m_ring_offset, 0.f, 15.f);
}
if (ImGui::CollapsingHeader("Scan Marker"))
{
ImGui::Checkbox("Show##scan", &m_show_scan);
ImGui::ColorEdit4("Fill##scan", reinterpret_cast<float*>(&m_scan_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Outline##scan", reinterpret_cast<float*>(&m_scan_outline), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thick##scan", &m_scan_outline_thickness, 0.5f, 5.f);
}
if (ImGui::CollapsingHeader("Aim Dot"))
{
ImGui::Checkbox("Show##aim", &m_show_aim);
ImGui::ColorEdit4("Color##aim", reinterpret_cast<float*>(&m_aim_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Radius##aim", &m_aim_radius, 1.f, 10.f);
}
if (ImGui::CollapsingHeader("Projectile Aim"))
{
ImGui::Checkbox("Show##proj", &m_show_proj);
ImGui::ColorEdit4("Color##proj", reinterpret_cast<float*>(&m_proj_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Size##proj", &m_proj_size, 1.f, 30.f);
ImGui::SliderFloat("Line width##proj", &m_proj_line_width, 0.5f, 5.f);
ImGui::SliderFloat("Pos X##proj", &m_proj_pos_x, 0.f, vp->Size.x);
ImGui::SliderFloat("Pos Y##proj", &m_proj_pos_y, 0.f, vp->Size.y);
ImGui::Combo("Figure##proj", &m_proj_figure, "Circle\0Square\0");
}
if (ImGui::CollapsingHeader("Snap Line"))
{
ImGui::Checkbox("Show##snap", &m_show_snap);
ImGui::ColorEdit4("Color##snap", reinterpret_cast<float*>(&m_snap_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Width##snap", &m_snap_width, 0.5f, 5.f);
}
ImGui::PopItemWidth();
ImGui::End();
}
void MainWindow::draw_overlay()
{
using namespace omath::hud::widget;
using omath::hud::when;
const auto* vp = ImGui::GetMainViewport();
const Bar bar{m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_offset};
const DashedBar dbar{m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width,
m_bar_value, m_bar_dash_len, m_bar_dash_gap, m_bar_offset};
omath::hud::EntityOverlay({m_entity_x, m_entity_top_y}, {m_entity_x, m_entity_bottom_y},
std::make_shared<omath::hud::ImguiHudRenderer>())
.contents(
// ── Boxes ────────────────────────────────────────────────────
when(m_show_box, Box{m_box_color, m_box_fill, m_box_thickness}),
when(m_show_cornered_box, CorneredBox{omath::Color::from_rgba(255, 0, 255, 255), m_box_fill,
m_corner_ratio, m_box_thickness}),
when(m_show_dashed_box, DashedBox{m_dash_color, m_dash_len, m_dash_gap, m_dash_thickness}),
RightSide{
when(m_show_right_bar, bar),
when(m_show_right_dashed_bar, dbar),
when(m_show_right_labels,
Label{{0.f, 1.f, 0.f, 1.f}, m_label_offset, m_outlined, "Health: 100/100"}),
when(m_show_right_labels,
Label{{1.f, 0.f, 0.f, 1.f}, m_label_offset, m_outlined, "Shield: 125/125"}),
when(m_show_right_labels,
Label{{1.f, 0.f, 1.f, 1.f}, m_label_offset, m_outlined, "*LOCKED*"}),
SpaceVertical{10},
when(m_show_ring, ProgressRing{m_ring_color, m_ring_bg, m_ring_radius, m_ring_ratio,
m_ring_thickness, m_ring_offset}),
},
LeftSide{
when(m_show_left_bar, bar),
when(m_show_left_dashed_bar, dbar),
when(m_show_left_labels, Label{omath::Color::from_rgba(255, 128, 0, 255),
m_label_offset, m_outlined, "Armor: 75"}),
when(m_show_left_labels, Label{omath::Color::from_rgba(0, 200, 255, 255),
m_label_offset, m_outlined, "Level: 42"}),
},
TopSide{
when(m_show_top_bar, bar),
when(m_show_top_dashed_bar, dbar),
when(m_show_centered_top, Centered{Label{omath::Color::from_rgba(0, 255, 255, 255),
m_label_offset, m_outlined, "*VISIBLE*"}}),
when(m_show_top_labels, Label{omath::Color::from_rgba(255, 255, 0, 255), m_label_offset,
m_outlined, "*SCOPED*"}),
when(m_show_top_labels, Label{omath::Color::from_rgba(255, 0, 0, 255), m_label_offset,
m_outlined, "*BLEEDING*"}),
},
BottomSide{
when(m_show_bottom_bar, bar),
when(m_show_bottom_dashed_bar, dbar),
when(m_show_centered_bottom, Centered{Label{omath::Color::from_rgba(255, 255, 255, 255),
m_label_offset, m_outlined, "PlayerName"}}),
when(m_show_bottom_labels, Label{omath::Color::from_rgba(200, 200, 0, 255),
m_label_offset, m_outlined, "42m"}),
},
when(m_show_aim, AimDot{{m_entity_x, m_entity_top_y+40.f}, m_aim_color, m_aim_radius}),
when(m_show_scan, ScanMarker{m_scan_color, m_scan_outline, m_scan_outline_thickness}),
when(m_show_skeleton, Skeleton{m_skel_color, m_skel_thickness}),
when(m_show_proj, ProjectileAim{{m_proj_pos_x, m_proj_pos_y}, m_proj_color, m_proj_size, m_proj_line_width, static_cast<ProjectileAim::Figure>(m_proj_figure)}),
when(m_show_snap, SnapLine{{vp->Size.x / 2.f, vp->Size.y}, m_snap_color, m_snap_width}));
}
void MainWindow::present()
{
int w, h;
glfwGetFramebufferSize(m_window, &w, &h);
glViewport(0, 0, w, h);
glClearColor(0.f, 0.f, 0.f, 0.f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(m_window);
}
} // namespace imgui_desktop::gui

View File

@@ -0,0 +1,94 @@
//
// Created by Orange on 11/11/2024.
//
#pragma once
#include <omath/hud/entity_overlay.hpp>
#include <omath/utility/color.hpp>
#include <string_view>
struct GLFWwindow;
namespace imgui_desktop::gui
{
class MainWindow
{
public:
MainWindow(const std::string_view& caption, int width, int height);
void Run();
private:
void draw_controls();
void draw_overlay();
void present();
GLFWwindow* m_window = nullptr;
static bool m_canMoveWindow;
bool m_opened = true;
// Entity
float m_entity_x = 550.f, m_entity_top_y = 150.f, m_entity_bottom_y = 450.f;
// Box
omath::Color m_box_color{1.f, 1.f, 1.f, 1.f};
omath::Color m_box_fill{0.f, 0.f, 0.f, 0.f};
float m_box_thickness = 1.f, m_corner_ratio = 0.2f;
bool m_show_box = true, m_show_cornered_box = true, m_show_dashed_box = false;
// Dashed box
omath::Color m_dash_color = omath::Color::from_rgba(255, 200, 0, 255);
float m_dash_len = 8.f, m_dash_gap = 5.f, m_dash_thickness = 1.f;
// Bars
omath::Color m_bar_color{0.f, 1.f, 0.f, 1.f};
omath::Color m_bar_bg_color{0.f, 0.f, 0.f, 0.5f};
omath::Color m_bar_outline_color{0.f, 0.f, 0.f, 1.f};
float m_bar_width = 4.f, m_bar_value = 0.75f, m_bar_offset = 5.f;
bool m_show_right_bar = true, m_show_left_bar = true;
bool m_show_top_bar = true, m_show_bottom_bar = true;
bool m_show_right_dashed_bar = false, m_show_left_dashed_bar = false;
bool m_show_top_dashed_bar = false, m_show_bottom_dashed_bar = false;
float m_bar_dash_len = 6.f, m_bar_dash_gap = 4.f;
// Labels
float m_label_offset = 3.f;
bool m_outlined = true;
bool m_show_right_labels = true, m_show_left_labels = true;
bool m_show_top_labels = true, m_show_bottom_labels = true;
bool m_show_centered_top = true, m_show_centered_bottom = true;
// Skeleton
omath::Color m_skel_color = omath::Color::from_rgba(255, 255, 255, 200);
float m_skel_thickness = 1.f;
bool m_show_skeleton = false;
// Progress ring
omath::Color m_ring_color = omath::Color::from_rgba(0, 200, 255, 255);
omath::Color m_ring_bg{0.3f, 0.3f, 0.3f, 0.5f};
float m_ring_radius = 10.f, m_ring_ratio = 0.65f, m_ring_thickness = 2.5f, m_ring_offset = 5.f;
bool m_show_ring = false;
// Scan marker
omath::Color m_scan_color = omath::Color::from_rgba(255, 200, 0, 150);
omath::Color m_scan_outline = omath::Color::from_rgba(255, 200, 0, 255);
float m_scan_outline_thickness = 2.f;
bool m_show_scan = false;
// Aim dot
omath::Color m_aim_color = omath::Color::from_rgba(255, 0, 0, 255);
float m_aim_radius = 3.f;
bool m_show_aim = false;
// Snap line
omath::Color m_snap_color = omath::Color::from_rgba(255, 50, 50, 255);
float m_snap_width = 1.5f;
bool m_show_snap = true;
// Projectile aim
omath::Color m_proj_color = omath::Color::from_rgba(255, 50, 50, 255);
float m_proj_size = 10.f;
float m_proj_line_width = 1.5f;
float m_proj_pos_x = 300.f, m_proj_pos_y = 30.f;
int m_proj_figure = 1; // 0=circle, 1=square
bool m_show_proj = true;
};
} // namespace imgui_desktop::gui

View File

@@ -0,0 +1,8 @@
//
// Created by orange on 13.03.2026.
//
#include "gui/main_window.hpp"
int main()
{
imgui_desktop::gui::MainWindow("omath::hud", 800, 600).Run();
}

View File

@@ -0,0 +1,103 @@
//
// Created by Vladislav on 19.03.2026.
//
#pragma once
#include "omath/linear_algebra/vector3.hpp"
#include <functional>
#include <iterator>
#include <optional>
#include <ranges>
namespace omath::algorithm
{
template<class CameraType, std::input_or_output_iterator IteratorType, class FilterT>
requires std::is_invocable_r_v<bool, std::function<FilterT>, std::iter_reference_t<IteratorType>>
[[nodiscard]]
IteratorType get_closest_target_by_fov(const IteratorType& begin, const IteratorType& end, const CameraType& camera,
auto get_position,
const std::optional<std::function<FilterT>>& filter_func = std::nullopt)
{
auto best_target = end;
const auto& camera_angles = camera.get_view_angles();
const Vector2<float> camera_angles_vec = {camera_angles.pitch.as_degrees(), camera_angles.yaw.as_degrees()};
for (auto current = begin; current != end; current = std::next(current))
{
if (filter_func && !filter_func.value()(*current))
continue;
if (best_target == end)
{
best_target = current;
continue;
}
const auto current_target_angles = camera.calc_look_at_angles(get_position(*current));
const auto best_target_angles = camera.calc_look_at_angles(get_position(*best_target));
const Vector2<float> current_angles_vec = {current_target_angles.pitch.as_degrees(),
current_target_angles.yaw.as_degrees()};
const Vector2<float> best_angles_vec = {best_target_angles.pitch.as_degrees(),
best_target_angles.yaw.as_degrees()};
const auto current_target_distance = camera_angles_vec.distance_to(current_angles_vec);
const auto best_target_distance = camera_angles_vec.distance_to(best_angles_vec);
if (current_target_distance < best_target_distance)
best_target = current;
}
return best_target;
}
template<class CameraType, std::ranges::range RangeType, class FilterT>
requires std::is_invocable_r_v<bool, std::function<FilterT>,
std::ranges::range_reference_t<const RangeType>>
[[nodiscard]]
auto get_closest_target_by_fov(const RangeType& range, const CameraType& camera,
auto get_position,
const std::optional<std::function<FilterT>>& filter_func = std::nullopt)
{
return get_closest_target_by_fov<CameraType, decltype(std::ranges::begin(range)), FilterT>(
std::ranges::begin(range), std::ranges::end(range), camera, get_position, filter_func);
}
// ── By world-space distance ───────────────────────────────────────────────
template<std::input_or_output_iterator IteratorType, class FilterT>
requires std::is_invocable_r_v<bool, std::function<FilterT>, std::iter_reference_t<IteratorType>>
[[nodiscard]]
IteratorType get_closest_target_by_distance(const IteratorType& begin, const IteratorType& end,
const Vector3<float>& origin, auto get_position,
const std::optional<std::function<FilterT>>& filter_func = std::nullopt)
{
auto best_target = end;
for (auto current = begin; current != end; current = std::next(current))
{
if (filter_func && !filter_func.value()(*current))
continue;
if (best_target == end)
{
best_target = current;
continue;
}
if (origin.distance_to(get_position(*current)) < origin.distance_to(get_position(*best_target)))
best_target = current;
}
return best_target;
}
template<std::ranges::range RangeType, class FilterT>
requires std::is_invocable_r_v<bool, std::function<FilterT>,
std::ranges::range_reference_t<const RangeType>>
[[nodiscard]]
auto get_closest_target_by_distance(const RangeType& range, const Vector3<float>& origin,
auto get_position,
const std::optional<std::function<FilterT>>& filter_func = std::nullopt)
{
return get_closest_target_by_distance<decltype(std::ranges::begin(range)), FilterT>(
std::ranges::begin(range), std::ranges::end(range), origin, get_position, filter_func);
}
} // namespace omath::algorithm

View File

@@ -16,7 +16,8 @@ namespace omath::cry_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;

View File

@@ -16,7 +16,8 @@ namespace omath::frostbite_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;

View File

@@ -17,7 +17,8 @@ namespace omath::iw_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;

View File

@@ -16,7 +16,8 @@ namespace omath::opengl_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;

View File

@@ -17,7 +17,8 @@ namespace omath::source_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;

View File

@@ -16,7 +16,8 @@ namespace omath::unity_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;

View File

@@ -16,7 +16,8 @@ namespace omath::unreal_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;

View File

@@ -0,0 +1,23 @@
//
// Created by orange on 13.03.2026.
//
#pragma once
#include "omath/linear_algebra/vector2.hpp"
#include <array>
namespace omath::hud
{
class CanvasBox final
{
public:
CanvasBox(Vector2<float> top, Vector2<float> bottom, float ratio = 4.f);
[[nodiscard]]
std::array<Vector2<float>, 4> as_array() const;
Vector2<float> top_left_corner;
Vector2<float> top_right_corner;
Vector2<float> bottom_left_corner;
Vector2<float> bottom_right_corner;
};
} // namespace omath::hud

View File

@@ -0,0 +1,202 @@
//
// Created by orange on 13.03.2026.
//
#pragma once
#include "canvas_box.hpp"
#include "entity_overlay_widgets.hpp"
#include "hud_renderer_interface.hpp"
#include "omath/linear_algebra/vector2.hpp"
#include "omath/utility/color.hpp"
#include <memory>
#include <string_view>
namespace omath::hud
{
class EntityOverlay final
{
public:
EntityOverlay(const Vector2<float>& top, const Vector2<float>& bottom,
const std::shared_ptr<HudRendererInterface>& renderer);
// ── Boxes ────────────────────────────────────────────────────────
EntityOverlay& add_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f},
float thickness = 1.f);
EntityOverlay& add_cornered_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f},
float corner_ratio_len = 0.2f, float thickness = 1.f);
EntityOverlay& add_dashed_box(const Color& color, float dash_len = 8.f, float gap_len = 5.f,
float thickness = 1.f);
// ── Bars ─────────────────────────────────────────────────────────
EntityOverlay& add_right_bar(const Color& color, const Color& outline_color, const Color& bg_color, float width,
float ratio, float offset = 5.f);
EntityOverlay& add_left_bar(const Color& color, const Color& outline_color, const Color& bg_color, float width,
float ratio, float offset = 5.f);
EntityOverlay& add_top_bar(const Color& color, const Color& outline_color, const Color& bg_color, float height,
float ratio, float offset = 5.f);
EntityOverlay& add_bottom_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float offset = 5.f);
EntityOverlay& add_right_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float width, float ratio, float dash_len, float gap_len,
float offset = 5.f);
EntityOverlay& add_left_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float width, float ratio, float dash_len, float gap_len, float offset = 5.f);
EntityOverlay& add_top_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float dash_len, float gap_len, float offset = 5.f);
EntityOverlay& add_bottom_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float dash_len, float gap_len,
float offset = 5.f);
// ── Labels ───────────────────────────────────────────────────────
EntityOverlay& add_right_label(const Color& color, float offset, bool outlined, const std::string_view& text);
EntityOverlay& add_left_label(const Color& color, float offset, bool outlined, const std::string_view& text);
EntityOverlay& add_top_label(const Color& color, float offset, bool outlined, std::string_view text);
EntityOverlay& add_bottom_label(const Color& color, float offset, bool outlined, std::string_view text);
EntityOverlay& add_centered_top_label(const Color& color, float offset, bool outlined,
const std::string_view& text);
EntityOverlay& add_centered_bottom_label(const Color& color, float offset, bool outlined,
const std::string_view& text);
template<typename... Args>
EntityOverlay& add_right_label(const Color& color, const float offset, const bool outlined, std::format_string<Args...> fmt,
Args&&... args)
{
return add_right_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
template<typename... Args>
EntityOverlay& add_left_label(const Color& color, const float offset, const bool outlined, std::format_string<Args...> fmt,
Args&&... args)
{
return add_left_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
template<typename... Args>
EntityOverlay& add_top_label(const Color& color, const float offset, const bool outlined, std::format_string<Args...> fmt,
Args&&... args)
{
return add_top_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
template<typename... Args>
EntityOverlay& add_bottom_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
{
return add_bottom_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
template<typename... Args>
EntityOverlay& add_centered_top_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
{
return add_centered_top_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
template<typename... Args>
EntityOverlay& add_centered_bottom_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
{
return add_centered_bottom_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
// ── Spacers ─────────────────────────────────────────────────────
EntityOverlay& add_right_space_vertical(float size);
EntityOverlay& add_right_space_horizontal(float size);
EntityOverlay& add_left_space_vertical(float size);
EntityOverlay& add_left_space_horizontal(float size);
EntityOverlay& add_top_space_vertical(float size);
EntityOverlay& add_top_space_horizontal(float size);
EntityOverlay& add_bottom_space_vertical(float size);
EntityOverlay& add_bottom_space_horizontal(float size);
// ── Progress rings ──────────────────────────────────────────────
EntityOverlay& add_right_progress_ring(const Color& color, const Color& bg, float radius, float ratio,
float thickness = 2.f, float offset = 5.f, int segments = 0);
EntityOverlay& add_left_progress_ring(const Color& color, const Color& bg, float radius, float ratio,
float thickness = 2.f, float offset = 5.f, int segments = 0);
EntityOverlay& add_top_progress_ring(const Color& color, const Color& bg, float radius, float ratio,
float thickness = 2.f, float offset = 5.f, int segments = 0);
EntityOverlay& add_bottom_progress_ring(const Color& color, const Color& bg, float radius, float ratio,
float thickness = 2.f, float offset = 5.f, int segments = 0);
// ── Icons ────────────────────────────────────────────────────────
EntityOverlay& add_right_icon(const std::any& texture_id, float width, float height,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}, float offset = 5.f);
EntityOverlay& add_left_icon(const std::any& texture_id, float width, float height,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}, float offset = 5.f);
EntityOverlay& add_top_icon(const std::any& texture_id, float width, float height,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}, float offset = 5.f);
EntityOverlay& add_bottom_icon(const std::any& texture_id, float width, float height,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}, float offset = 5.f);
// ── Misc ─────────────────────────────────────────────────────────
EntityOverlay& add_snap_line(const Vector2<float>& start_pos, const Color& color, float width);
EntityOverlay& add_skeleton(const Color& color, float thickness = 1.f);
// ── Declarative interface ─────────────────────────────────────────
/// Pass any combination of widget:: descriptor structs (and std::optional<W>
/// from when()) to render them all in declaration order.
template<typename... Widgets>
EntityOverlay& contents(Widgets&&... widgets)
{
(dispatch(std::forward<Widgets>(widgets)), ...);
return *this;
}
private:
// optional<W> dispatch — enables when() conditional widgets
template<typename W>
void dispatch(const std::optional<W>& w)
{
if (w)
dispatch(*w);
}
void dispatch(const widget::Box& box);
void dispatch(const widget::CorneredBox& cornered_box);
void dispatch(const widget::DashedBox& dashed_box);
void dispatch(const widget::RightSide& right_side);
void dispatch(const widget::LeftSide& left_side);
void dispatch(const widget::TopSide& top_side);
void dispatch(const widget::BottomSide& bottom_side);
void dispatch(const widget::Skeleton& skeleton);
void dispatch(const widget::SnapLine& snap_line);
void dispatch(const widget::ScanMarker& scan_marker);
void dispatch(const widget::AimDot& aim_dot);
void dispatch(const widget::ProjectileAim& proj_widget);
void draw_progress_ring(const Vector2<float>& center, const widget::ProgressRing& ring);
void draw_outlined_text(const Vector2<float>& position, const Color& color, const std::string_view& text);
void draw_dashed_line(const Vector2<float>& from, const Vector2<float>& to, const Color& color, float dash_len,
float gap_len, float thickness) const;
void draw_dashed_fill(const Vector2<float>& origin, const Vector2<float>& step_dir,
const Vector2<float>& perp_dir, float full_len, float filled_len, const Color& fill_color,
const Color& split_color, float dash_len, float gap_len) const;
CanvasBox m_canvas;
Vector2<float> m_text_cursor_right;
Vector2<float> m_text_cursor_top;
Vector2<float> m_text_cursor_bottom;
Vector2<float> m_text_cursor_left;
std::shared_ptr<HudRendererInterface> m_renderer;
};
} // namespace omath::hud

View File

@@ -0,0 +1,233 @@
//
// Created by orange on 15.03.2026.
//
#pragma once
#include "omath/linear_algebra/vector2.hpp"
#include "omath/utility/color.hpp"
#include <any>
#include <initializer_list>
#include <optional>
#include <string_view>
#include <variant>
namespace omath::hud::widget
{
// ── Overloaded helper for std::visit ──────────────────────────────────────
template<typename... Ts>
struct Overloaded : Ts...
{
using Ts::operator()...;
};
template<typename... Ts>
Overloaded(Ts...) -> Overloaded<Ts...>;
// ── Standalone widgets ────────────────────────────────────────────────────
struct Box
{
Color color;
Color fill{0.f, 0.f, 0.f, 0.f};
float thickness = 1.f;
};
struct CorneredBox
{
Color color;
Color fill{0.f, 0.f, 0.f, 0.f};
float corner_ratio = 0.2f;
float thickness = 1.f;
};
struct DashedBox
{
Color color;
float dash_len = 8.f;
float gap_len = 5.f;
float thickness = 1.f;
};
struct Skeleton
{
Color color;
float thickness = 1.f;
};
struct SnapLine
{
Vector2<float> start;
Color color;
float width;
};
struct ScanMarker
{
Color color;
Color outline{0.f, 0.f, 0.f, 0.f};
float outline_thickness = 1.f;
};
/// Dot at an absolute screen position.
struct AimDot
{
Vector2<float> position;
Color color;
float radius = 3.f;
};
struct ProjectileAim
{
enum class Figure
{
CIRCLE,
SQUARE,
};
Vector2<float> position;
Color color;
float size = 3.f;
float line_size = 1.f;
Figure figure = Figure::SQUARE;
};
// ── Side-agnostic widgets (used inside XxxSide containers) ────────────────
/// A filled bar. `size` is width for left/right sides, height for top/bottom.
struct Bar
{
Color color;
Color outline;
Color bg;
float size;
float ratio;
float offset = 5.f;
};
/// A dashed bar. Same field semantics as Bar plus dash parameters.
struct DashedBar
{
Color color;
Color outline;
Color bg;
float size;
float ratio;
float dash_len;
float gap_len;
float offset = 5.f;
};
struct Label
{
Color color;
float offset;
bool outlined;
std::string_view text;
};
/// Wraps a Label to request horizontal centering (only applied in TopSide / BottomSide).
template<typename W>
struct Centered
{
W child;
};
template<typename W>
Centered(W) -> Centered<W>;
/// Empty vertical gap that advances the Y cursor without drawing.
struct SpaceVertical
{
float size;
};
/// Empty horizontal gap that advances the X cursor without drawing.
struct SpaceHorizontal
{
float size;
};
struct ProgressRing
{
Color color;
Color bg{0.3f, 0.3f, 0.3f, 0.5f};
float radius = 12.f;
float ratio;
float thickness = 2.f;
float offset = 5.f;
int segments = 32;
};
struct Icon
{
std::any texture_id;
float width;
float height;
Color tint{1.f, 1.f, 1.f, 1.f};
float offset = 5.f;
};
// ── Side widget variant ───────────────────────────────────────────────────
struct None
{
}; ///< No-op placeholder — used by widget::when for disabled elements.
using SideWidget =
std::variant<None, Bar, DashedBar, Label, Centered<Label>, SpaceVertical, SpaceHorizontal, ProgressRing, Icon>;
// ── Side containers ───────────────────────────────────────────────────────
// Storing std::initializer_list<SideWidget> is safe here: the backing array
// is a const SideWidget[] on the caller's stack whose lifetime matches the
// temporary side-container object, which is consumed within the same
// full-expression by EntityOverlay::dispatch. No heap allocation occurs.
struct RightSide
{
std::initializer_list<SideWidget> children;
RightSide(const std::initializer_list<SideWidget> c): children(c)
{
}
};
struct LeftSide
{
std::initializer_list<SideWidget> children;
LeftSide(const std::initializer_list<SideWidget> c): children(c)
{
}
};
struct TopSide
{
std::initializer_list<SideWidget> children;
TopSide(const std::initializer_list<SideWidget> c): children(c)
{
}
};
struct BottomSide
{
std::initializer_list<SideWidget> children;
BottomSide(const std::initializer_list<SideWidget> c): children(c)
{
}
};
} // namespace omath::hud::widget
namespace omath::hud::widget
{
/// Inside XxxSide containers: returns the widget as a SideWidget when condition is true,
/// or None{} otherwise. Preferred over hud::when for types inside the SideWidget variant.
template<typename W>
requires std::constructible_from<SideWidget, W>
SideWidget when(const bool condition, W widget)
{
if (condition)
return SideWidget{std::move(widget)};
return None{};
}
} // namespace omath::hud::widget
namespace omath::hud
{
/// Top-level: returns an engaged optional<W> when condition is true, std::nullopt otherwise.
/// Designed for use with EntityOverlay::contents() for top-level widget types.
template<typename W>
std::optional<W> when(const bool condition, W widget)
{
if (condition)
return std::move(widget);
return std::nullopt;
}
} // namespace omath::hud

View File

@@ -0,0 +1,47 @@
//
// Created by orange on 13.03.2026.
//
#pragma once
#include "omath/linear_algebra/vector2.hpp"
#include "omath/utility/color.hpp"
#include <any>
#include <span>
namespace omath::hud
{
class HudRendererInterface
{
public:
virtual ~HudRendererInterface() = default;
virtual void add_line(const Vector2<float>& line_start, const Vector2<float>& line_end, const Color& color,
float thickness) = 0;
virtual void add_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color,
float thickness) = 0;
virtual void add_filled_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color) = 0;
virtual void add_rectangle(const Vector2<float>& min, const Vector2<float>& max, const Color& color) = 0;
virtual void add_filled_rectangle(const Vector2<float>& min, const Vector2<float>& max, const Color& color) = 0;
virtual void add_circle(const Vector2<float>& center, float radius, const Color& color, float thickness,
int segments = 0) = 0;
virtual void add_filled_circle(const Vector2<float>& center, float radius, const Color& color,
int segments = 0) = 0;
/// Draw an arc (partial circle outline). Angles in radians, 0 = right (+X), counter-clockwise.
virtual void add_arc(const Vector2<float>& center, float radius, float a_min, float a_max, const Color& color,
float thickness, int segments = 0) = 0;
/// Draw a textured quad. texture_id is renderer-specific (e.g. ImTextureID for ImGui).
virtual void add_image(const std::any& texture_id, const Vector2<float>& min, const Vector2<float>& max,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}) = 0;
virtual void add_text(const Vector2<float>& position, const Color& color, const std::string_view& text) = 0;
[[nodiscard]]
virtual Vector2<float> calc_text_size(const std::string_view& text) = 0;
};
} // namespace omath::hud

View File

@@ -0,0 +1,33 @@
//
// Created by orange on 13.03.2026.
//
#pragma once
#include <omath/hud/hud_renderer_interface.hpp>
#ifdef OMATH_IMGUI_INTEGRATION
namespace omath::hud
{
class ImguiHudRenderer final : public HudRendererInterface
{
public:
~ImguiHudRenderer() override;
void add_line(const Vector2<float>& line_start, const Vector2<float>& line_end, const Color& color,
float thickness) override;
void add_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color, float thickness) override;
void add_filled_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color) override;
void add_rectangle(const Vector2<float>& min, const Vector2<float>& max, const Color& color) override;
void add_filled_rectangle(const Vector2<float>& min, const Vector2<float>& max, const Color& color) override;
void add_circle(const Vector2<float>& center, float radius, const Color& color, float thickness,
int segments = 0) override;
void add_filled_circle(const Vector2<float>& center, float radius, const Color& color,
int segments = 0) override;
void add_arc(const Vector2<float>& center, float radius, float a_min, float a_max, const Color& color,
float thickness, int segments = 0) override;
void add_image(const std::any& texture_id, const Vector2<float>& min, const Vector2<float>& max,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}) override;
void add_text(const Vector2<float>& position, const Color& color, const std::string_view& text) override;
[[nodiscard]]
virtual Vector2<float> calc_text_size(const std::string_view& text) override;
};
} // namespace omath::hud
#endif // OMATH_IMGUI_INTEGRATION

View File

@@ -8,12 +8,23 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
struct AimAngles
{
float pitch{};
float yaw{};
};
class ProjPredEngineInterface class ProjPredEngineInterface
{ {
public: public:
[[nodiscard]] [[nodiscard]]
virtual std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile, virtual std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile,
const Target& target) const = 0; const Target& target) const = 0;
[[nodiscard]]
virtual std::optional<AimAngles> maybe_calculate_aim_angles(const Projectile& projectile,
const Target& target) const = 0;
virtual ~ProjPredEngineInterface() = default; virtual ~ProjPredEngineInterface() = default;
}; };
} // namespace omath::projectile_prediction } // namespace omath::projectile_prediction

View File

@@ -12,6 +12,9 @@ namespace omath::projectile_prediction
[[nodiscard]] std::optional<Vector3<float>> [[nodiscard]] std::optional<Vector3<float>>
maybe_calculate_aim_point(const Projectile& projectile, const Target& target) const override; maybe_calculate_aim_point(const Projectile& projectile, const Target& target) const override;
[[nodiscard]] std::optional<AimAngles>
maybe_calculate_aim_angles(const Projectile& projectile, const Target& target) const override;
ProjPredEngineAvx2(float gravity_constant, float simulation_time_step, float maximum_simulation_time); ProjPredEngineAvx2(float gravity_constant, float simulation_time_step, float maximum_simulation_time);
~ProjPredEngineAvx2() override = default; ~ProjPredEngineAvx2() override = default;

View File

@@ -54,6 +54,36 @@ namespace omath::projectile_prediction
[[nodiscard]] [[nodiscard]]
std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile, std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile,
const Target& target) const override const Target& target) const override
{
const auto solution = find_solution(projectile, target);
if (!solution)
return std::nullopt;
return EngineTrait::calc_viewpoint_from_angles(projectile, solution->predicted_target_position,
solution->pitch);
}
[[nodiscard]]
std::optional<AimAngles> maybe_calculate_aim_angles(const Projectile& projectile,
const Target& target) const override
{
const auto solution = find_solution(projectile, target);
if (!solution)
return std::nullopt;
const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, solution->predicted_target_position);
return AimAngles{solution->pitch, yaw};
}
private:
struct Solution
{
Vector3<float> predicted_target_position;
float pitch;
};
[[nodiscard]]
std::optional<Solution> find_solution(const Projectile& projectile, const Target& target) const
{ {
for (float time = 0.f; time < m_maximum_simulation_time; time += m_simulation_time_step) for (float time = 0.f; time < m_maximum_simulation_time; time += m_simulation_time_step)
{ {
@@ -70,12 +100,11 @@ namespace omath::projectile_prediction
time)) time))
continue; continue;
return EngineTrait::calc_viewpoint_from_angles(projectile, predicted_target_position, projectile_pitch); return Solution{predicted_target_position, projectile_pitch.value()};
} }
return std::nullopt; return std::nullopt;
} }
private:
const float m_gravity_constant; const float m_gravity_constant;
const float m_simulation_time_step; const float m_simulation_time_step;
const float m_maximum_simulation_time; const float m_maximum_simulation_time;
@@ -100,10 +129,12 @@ namespace omath::projectile_prediction
{ {
const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
if (bullet_gravity == 0.f) const auto launch_origin = projectile.m_origin + projectile.m_launch_offset;
return EngineTrait::calc_direct_pitch_angle(projectile.m_origin, target_position);
const auto delta = target_position - projectile.m_origin; if (bullet_gravity == 0.f)
return EngineTrait::calc_direct_pitch_angle(launch_origin, target_position);
const auto delta = target_position - launch_origin;
const auto distance2d = EngineTrait::calc_vector_2d_distance(delta); const auto distance2d = EngineTrait::calc_vector_2d_distance(delta);
const auto distance2d_sqr = distance2d * distance2d; const auto distance2d_sqr = distance2d * distance2d;
@@ -126,7 +157,7 @@ namespace omath::projectile_prediction
bool is_projectile_reached_target(const Vector3<float>& target_position, const Projectile& projectile, bool is_projectile_reached_target(const Vector3<float>& target_position, const Projectile& projectile,
const float pitch, const float time) const noexcept const float pitch, const float time) const noexcept
{ {
const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, target_position); const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, target_position);
const auto projectile_position = const auto projectile_position =
EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant); EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant);

View File

@@ -11,6 +11,7 @@ namespace omath::projectile_prediction
{ {
public: public:
Vector3<float> m_origin; Vector3<float> m_origin;
Vector3<float> m_launch_offset{0.f, 0.f, 0.f};
float m_launch_speed{}; float m_launch_speed{};
float m_gravity_scale{}; float m_gravity_scale{};
}; };

View File

@@ -82,6 +82,11 @@ namespace omath::projection
m_view_projection_matrix = std::nullopt; m_view_projection_matrix = std::nullopt;
m_view_matrix = std::nullopt; m_view_matrix = std::nullopt;
} }
[[nodiscard]]
ViewAnglesType calc_look_at_angles(const Vector3<float>& look_to) const
{
return TraitClass::calc_look_at_angle(m_origin, look_to);
}
[[nodiscard]] [[nodiscard]]
Vector3<float> get_forward() const noexcept Vector3<float> get_forward() const noexcept
@@ -138,16 +143,16 @@ namespace omath::projection
m_projection_matrix = std::nullopt; m_projection_matrix = std::nullopt;
} }
void set_near_plane(const float near) noexcept void set_near_plane(const float near_plane) noexcept
{ {
m_near_plane_distance = near; m_near_plane_distance = near_plane;
m_view_projection_matrix = std::nullopt; m_view_projection_matrix = std::nullopt;
m_projection_matrix = std::nullopt; m_projection_matrix = std::nullopt;
} }
void set_far_plane(const float far) noexcept void set_far_plane(const float far_plane) noexcept
{ {
m_far_plane_distance = far; m_far_plane_distance = far_plane;
m_view_projection_matrix = std::nullopt; m_view_projection_matrix = std::nullopt;
m_projection_matrix = std::nullopt; m_projection_matrix = std::nullopt;
} }
@@ -199,9 +204,9 @@ namespace omath::projection
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER> template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]] std::expected<Vector3<float>, Error> [[nodiscard]] std::expected<Vector3<float>, Error>
world_to_screen(const Vector3<float>& world_position) const noexcept world_to_screen(const Vector3<float>& world_position, const bool auto_clip = true) const noexcept
{ {
const auto normalized_cords = world_to_view_port(world_position); const auto normalized_cords = world_to_view_port(world_position, auto_clip);
if (!normalized_cords.has_value()) if (!normalized_cords.has_value())
return std::unexpected{normalized_cords.error()}; return std::unexpected{normalized_cords.error()};
@@ -262,7 +267,7 @@ namespace omath::projection
} }
[[nodiscard]] std::expected<Vector3<float>, Error> [[nodiscard]] std::expected<Vector3<float>, Error>
world_to_view_port(const Vector3<float>& world_position) const noexcept world_to_view_port(const Vector3<float>& world_position, const bool auto_clip = true) const noexcept
{ {
auto projected = get_view_projection_matrix() auto projected = get_view_projection_matrix()
* mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(world_position); * mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(world_position);
@@ -273,13 +278,13 @@ namespace omath::projection
projected /= w; projected /= w;
if (is_ndc_out_of_bounds(projected)) if (auto_clip && is_ndc_out_of_bounds(projected))
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS); return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
return Vector3<float>{projected.at(0, 0), projected.at(1, 0), projected.at(2, 0)}; return Vector3<float>{projected.at(0, 0), projected.at(1, 0), projected.at(2, 0)};
} }
[[nodiscard]] [[nodiscard]]
std::expected<Vector3<float>, Error> view_port_to_screen(const Vector3<float>& ndc) const noexcept std::expected<Vector3<float>, Error> view_port_to_world(const Vector3<float>& ndc) const noexcept
{ {
const auto inv_view_proj = get_view_projection_matrix().inverted(); const auto inv_view_proj = get_view_projection_matrix().inverted();
@@ -304,7 +309,7 @@ namespace omath::projection
[[nodiscard]] [[nodiscard]]
std::expected<Vector3<float>, Error> screen_to_world(const Vector3<float>& screen_pos) const noexcept std::expected<Vector3<float>, Error> screen_to_world(const Vector3<float>& screen_pos) const noexcept
{ {
return view_port_to_screen(screen_to_ndc<screen_start>(screen_pos)); return view_port_to_world(screen_to_ndc<screen_start>(screen_pos));
} }
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER> template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>

View File

@@ -3,11 +3,43 @@
// //
#pragma once #pragma once
#include <cassert>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <string_view>
#ifdef _WIN32
#include "omath/utility/pe_pattern_scan.hpp"
#include <windows.h>
#elif defined(__APPLE__)
#include "omath/utility/macho_pattern_scan.hpp"
#include <mach-o/dyld.h>
#else
#include "omath/utility/elf_pattern_scan.hpp"
#include <link.h>
#endif
namespace omath::rev_eng namespace omath::rev_eng
{ {
template<std::size_t N>
struct FixedString final
{
char data[N]{};
// ReSharper disable once CppNonExplicitConvertingConstructor
constexpr FixedString(const char (&str)[N]) noexcept // NOLINT(*-explicit-constructor)
{
for (std::size_t i = 0; i < N; ++i)
data[i] = str[i];
}
// ReSharper disable once CppNonExplicitConversionOperator
constexpr operator std::string_view() const noexcept // NOLINT(*-explicit-constructor)
{
return {data, N - 1};
}
};
template<std::size_t N>
FixedString(const char (&)[N]) -> FixedString<N>;
class InternalReverseEngineeredObject class InternalReverseEngineeredObject
{ {
protected: protected:
@@ -23,26 +55,123 @@ namespace omath::rev_eng
return *reinterpret_cast<Type*>(reinterpret_cast<std::uintptr_t>(this) + offset); return *reinterpret_cast<Type*>(reinterpret_cast<std::uintptr_t>(this) + offset);
} }
template<std::size_t id, class ReturnType> template<class ReturnType>
ReturnType call_method(const void* ptr, auto... arg_list)
{
#ifdef _MSC_VER
using MethodType = ReturnType(__thiscall*)(void*, decltype(arg_list)...);
#else
using MethodType = ReturnType (*)(void*, decltype(arg_list)...);
#endif
return reinterpret_cast<MethodType>(const_cast<void*>(ptr))(this, arg_list...);
}
template<class ReturnType>
ReturnType call_method(const void* ptr, auto... arg_list) const
{
#ifdef _MSC_VER
using MethodType = ReturnType(__thiscall*)(const void*, decltype(arg_list)...);
#else
using MethodType = ReturnType (*)(const void*, decltype(arg_list)...);
#endif
return reinterpret_cast<MethodType>(const_cast<void*>(ptr))(this, arg_list...);
}
template<FixedString ModuleName, FixedString Pattern, class ReturnType>
ReturnType call_method(auto... arg_list)
{
static const auto* address = resolve_pattern(ModuleName, Pattern);
return call_method<ReturnType>(address, arg_list...);
}
template<FixedString ModuleName, FixedString Pattern, class ReturnType>
ReturnType call_method(auto... arg_list) const
{
static const auto* address = resolve_pattern(ModuleName, Pattern);
return call_method<ReturnType>(address, arg_list...);
}
template<class ReturnType>
ReturnType call_method(const std::string_view& module_name,const std::string_view& pattern, auto... arg_list)
{
static const auto* address = resolve_pattern(module_name, pattern);
return call_method<ReturnType>(address, arg_list...);
}
template<class ReturnType>
ReturnType call_method(const std::string_view& module_name,const std::string_view& pattern, auto... arg_list) const
{
static const auto* address = resolve_pattern(module_name, pattern);
return call_method<ReturnType>(address, arg_list...);
}
template<std::size_t Id, class ReturnType>
ReturnType call_virtual_method(auto... arg_list) ReturnType call_virtual_method(auto... arg_list)
{ {
#ifdef _MSC_VER const auto vtable = *reinterpret_cast<void***>(this);
using VirtualMethodType = ReturnType(__thiscall*)(void*, decltype(arg_list)...); return call_method<ReturnType>(vtable[Id], arg_list...);
#else
using VirtualMethodType = ReturnType (*)(void*, decltype(arg_list)...);
#endif
return (*reinterpret_cast<VirtualMethodType**>(this))[id](this, arg_list...);
} }
template<std::size_t id, class ReturnType> template<std::size_t Id, class ReturnType>
ReturnType call_virtual_method(auto... arg_list) const ReturnType call_virtual_method(auto... arg_list) const
{ {
#ifdef _MSC_VER const auto vtable = *reinterpret_cast<void* const* const*>(this);
using VirtualMethodType = ReturnType(__thiscall*)(void*, decltype(arg_list)...); return call_method<ReturnType>(vtable[Id], arg_list...);
}
private:
[[nodiscard]]
static const void* resolve_pattern(const std::string_view module_name, const std::string_view pattern)
{
const auto* base = get_module_base(module_name);
assert(base && "Failed to find module");
#ifdef _WIN32
const auto result = PePatternScanner::scan_for_pattern_in_loaded_module(base, pattern);
#elif defined(__APPLE__)
const auto result = MachOPatternScanner::scan_for_pattern_in_loaded_module(base, pattern);
#else #else
using VirtualMethodType = ReturnType (*)(void*, decltype(arg_list)...); const auto result = ElfPatternScanner::scan_for_pattern_in_loaded_module(base, pattern);
#endif
assert(result.has_value() && "Pattern scan failed");
return reinterpret_cast<const void*>(*result);
}
[[nodiscard]]
static const void* get_module_base(const std::string_view module_name)
{
#ifdef _WIN32
return GetModuleHandleA(module_name.data());
#elif defined(__APPLE__)
// On macOS, iterate loaded images to find the module by name
const auto count = _dyld_image_count();
for (std::uint32_t i = 0; i < count; ++i)
{
const auto* name = _dyld_get_image_name(i);
if (name && std::string_view{name}.find(module_name) != std::string_view::npos)
return static_cast<const void*>(_dyld_get_image_header(i));
}
return nullptr;
#else
// On Linux, use dl_iterate_phdr to find loaded module by name
struct CallbackData
{
std::string_view name;
const void* base;
} cb_data{module_name, nullptr};
dl_iterate_phdr(
[](dl_phdr_info* info, std::size_t, void* data) -> int
{
auto* cb = static_cast<CallbackData*>(data);
if (info->dlpi_name
&& std::string_view{info->dlpi_name}.find(cb->name) != std::string_view::npos)
{
cb->base = reinterpret_cast<const void*>(info->dlpi_addr);
return 1;
}
return 0;
},
&cb_data);
return cb_data.base;
#endif #endif
return (*static_cast<VirtualMethodType**>((void*)(this)))[id](
const_cast<void*>(static_cast<const void*>(this)), arg_list...);
} }
}; };
} // namespace omath::rev_eng } // namespace omath::rev_eng

View File

@@ -5,6 +5,7 @@
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <span>
#include <string_view> #include <string_view>
#include "section_scan_result.hpp" #include "section_scan_result.hpp"
namespace omath namespace omath
@@ -21,5 +22,10 @@ namespace omath
static std::optional<SectionScanResult> 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");
[[nodiscard]]
static std::optional<SectionScanResult>
scan_for_pattern_in_memory_file(std::span<const std::byte> file_data, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
}; };
} // namespace omath } // namespace omath

View File

@@ -5,6 +5,7 @@
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <span>
#include <string_view> #include <string_view>
#include "section_scan_result.hpp" #include "section_scan_result.hpp"
namespace omath namespace omath
@@ -21,5 +22,10 @@ namespace omath
static std::optional<SectionScanResult> 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");
[[nodiscard]]
static std::optional<SectionScanResult>
scan_for_pattern_in_memory_file(std::span<const std::byte> file_data, const std::string_view& pattern,
const std::string_view& target_section_name = "__text");
}; };
} // namespace omath } // namespace omath

View File

@@ -6,6 +6,7 @@
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <span>
#include <string_view> #include <string_view>
#include "section_scan_result.hpp" #include "section_scan_result.hpp"
namespace omath namespace omath
@@ -23,5 +24,10 @@ namespace omath
static std::optional<SectionScanResult> 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");
[[nodiscard]]
static std::optional<SectionScanResult>
scan_for_pattern_in_memory_file(std::span<const std::byte> file_data, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
}; };
} // namespace omath } // namespace omath

View File

@@ -16,15 +16,42 @@ echo "[*] Output dir: ${OUTPUT_DIR}"
# Find llvm tools - handle versioned names (Linux) and xcrun (macOS) # Find llvm tools - handle versioned names (Linux) and xcrun (macOS)
find_llvm_tool() { find_llvm_tool() {
local tool_name="$1" local tool_name="$1"
# macOS: use xcrun # First priority: derive from the actual compiler used by cmake (CMakeCache.txt).
# This guarantees the profraw format version matches the instrumented binary.
local cache_file="${BINARY_DIR}/CMakeCache.txt"
if [[ -f "$cache_file" ]]; then
local cmake_cxx
cmake_cxx=$(grep '^CMAKE_CXX_COMPILER:' "$cache_file" | cut -d= -f2)
if [[ -n "$cmake_cxx" && -x "$cmake_cxx" ]]; then
local tool_path
tool_path="$(dirname "$cmake_cxx")/${tool_name}"
if [[ -x "$tool_path" ]]; then
echo "$tool_path"
return 0
fi
fi
fi
# macOS: derive from xcrun clang as fallback
if [[ "$(uname)" == "Darwin" ]]; then if [[ "$(uname)" == "Darwin" ]]; then
local clang_path
clang_path=$(xcrun --find clang 2>/dev/null)
if [[ -n "$clang_path" ]]; then
local tool_path
tool_path="$(dirname "$clang_path")/${tool_name}"
if [[ -x "$tool_path" ]]; then
echo "$tool_path"
return 0
fi
fi
# Fallback: xcrun
if xcrun --find "${tool_name}" &>/dev/null; then if xcrun --find "${tool_name}" &>/dev/null; then
echo "xcrun ${tool_name}" echo "xcrun ${tool_name}"
return 0 return 0
fi fi
fi fi
# Try versioned names (Linux with LLVM 21, 20, 19, etc.) # Try versioned names (Linux with LLVM 21, 20, 19, etc.)
for version in 21 20 19 18 17 ""; do for version in 21 20 19 18 17 ""; do
local versioned_name="${tool_name}${version:+-$version}" local versioned_name="${tool_name}${version:+-$version}"
@@ -33,7 +60,7 @@ find_llvm_tool() {
return 0 return 0
fi fi
done done
echo "" echo ""
return 1 return 1
} }
@@ -51,6 +78,18 @@ fi
echo "[*] Using: ${LLVM_PROFDATA}" echo "[*] Using: ${LLVM_PROFDATA}"
echo "[*] Using: ${LLVM_COV}" echo "[*] Using: ${LLVM_COV}"
# Print version info for debugging version mismatches
if [[ "$(uname)" == "Darwin" ]]; then
echo "[*] Default clang: $(xcrun clang --version 2>&1 | head -1)"
# Show actual compiler used by the build (from CMakeCache.txt if available)
CACHE_FILE="${BINARY_DIR}/CMakeCache.txt"
if [[ -f "$CACHE_FILE" ]]; then
ACTUAL_CXX=$(grep '^CMAKE_CXX_COMPILER:' "$CACHE_FILE" | cut -d= -f2)
echo "[*] Build compiler: ${ACTUAL_CXX} ($(${ACTUAL_CXX} --version 2>&1 | head -1))"
fi
echo "[*] profdata: $(${LLVM_PROFDATA} show --version 2>&1 | head -1 || true)"
fi
# Find test binary # Find test binary
if [[ -z "${TEST_BINARY}" ]]; then if [[ -z "${TEST_BINARY}" ]]; then
for path in \ for path in \

27
source/hud/canvas_box.cpp Normal file
View File

@@ -0,0 +1,27 @@
//
// Created by orange on 13.03.2026.
//
//
// Created by Vlad on 6/17/2025.
//
#include "omath/hud/canvas_box.hpp"
namespace omath::hud
{
CanvasBox::CanvasBox(const Vector2<float> top, Vector2<float> bottom, const float ratio)
{
bottom.x = top.x;
const auto height = std::abs(top.y - bottom.y);
top_left_corner = top - Vector2<float>{height / ratio, 0};
top_right_corner = top + Vector2<float>{height / ratio, 0};
bottom_left_corner = bottom - Vector2<float>{height / ratio, 0};
bottom_right_corner = bottom + Vector2<float>{height / ratio, 0};
}
std::array<Vector2<float>, 4> CanvasBox::as_array() const
{
return {top_left_corner, top_right_corner, bottom_right_corner, bottom_left_corner};
}
} // namespace ohud

View File

@@ -0,0 +1,870 @@
//
// Created by orange on 13.03.2026.
//
#include "omath/hud/entity_overlay.hpp"
namespace omath::hud
{
EntityOverlay& EntityOverlay::add_2d_box(const Color& box_color, const Color& fill_color, const float thickness)
{
const auto points = m_canvas.as_array();
m_renderer->add_polyline({points.data(), points.size()}, box_color, thickness);
if (fill_color.value().w > 0.f)
m_renderer->add_filled_polyline({points.data(), points.size()}, fill_color);
return *this;
}
EntityOverlay& EntityOverlay::add_cornered_2d_box(const Color& box_color, const Color& fill_color,
const float corner_ratio_len, const float thickness)
{
const auto corner_line_length =
std::abs((m_canvas.top_left_corner - m_canvas.top_right_corner).x * corner_ratio_len);
if (fill_color.value().w > 0.f)
add_2d_box(fill_color, fill_color);
// Left Side
m_renderer->add_line(m_canvas.top_left_corner,
m_canvas.top_left_corner + Vector2<float>{corner_line_length, 0.f}, box_color, thickness);
m_renderer->add_line(m_canvas.top_left_corner,
m_canvas.top_left_corner + Vector2<float>{0.f, corner_line_length}, box_color, thickness);
m_renderer->add_line(m_canvas.bottom_left_corner,
m_canvas.bottom_left_corner - Vector2<float>{0.f, corner_line_length}, box_color,
thickness);
m_renderer->add_line(m_canvas.bottom_left_corner,
m_canvas.bottom_left_corner + Vector2<float>{corner_line_length, 0.f}, box_color,
thickness);
// Right Side
m_renderer->add_line(m_canvas.top_right_corner,
m_canvas.top_right_corner - Vector2<float>{corner_line_length, 0.f}, box_color, thickness);
m_renderer->add_line(m_canvas.top_right_corner,
m_canvas.top_right_corner + Vector2<float>{0.f, corner_line_length}, box_color, thickness);
m_renderer->add_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{0.f, corner_line_length}, box_color,
thickness);
m_renderer->add_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{corner_line_length, 0.f}, box_color,
thickness);
return *this;
}
EntityOverlay& EntityOverlay::add_right_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float width, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_height = std::abs(m_canvas.top_right_corner.y - m_canvas.bottom_right_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_right.x + offset, m_canvas.bottom_right_corner.y};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(width, -max_bar_height), bg_color);
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(width, -max_bar_height * ratio), color);
m_renderer->add_rectangle(bar_start - Vector2<float>(1.f, 0.f),
bar_start + Vector2<float>(width, -max_bar_height), outline_color);
m_text_cursor_right.x += offset + width;
return *this;
}
EntityOverlay& EntityOverlay::add_left_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float width, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_height = std::abs(m_canvas.top_left_corner.y - m_canvas.bottom_right_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_left.x - (offset + width), m_canvas.bottom_left_corner.y};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(width, -max_bar_height), bg_color);
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(width, -max_bar_height * ratio), color);
m_renderer->add_rectangle(bar_start - Vector2<float>(1.f, 0.f),
bar_start + Vector2<float>(width, -max_bar_height), outline_color);
m_text_cursor_left.x -= offset + width;
return *this;
}
EntityOverlay& EntityOverlay::add_right_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
if (outlined)
draw_outlined_text(m_text_cursor_right + Vector2<float>{offset, 0.f}, color, text);
else
m_renderer->add_text(m_text_cursor_right + Vector2<float>{offset, 0.f}, color, text.data());
m_text_cursor_right.y += m_renderer->calc_text_size(text.data()).y;
return *this;
}
EntityOverlay& EntityOverlay::add_top_label(const Color& color, const float offset, const bool outlined,
const std::string_view text)
{
m_text_cursor_top.y -= m_renderer->calc_text_size(text.data()).y;
if (outlined)
draw_outlined_text(m_text_cursor_top + Vector2<float>{0.f, -offset}, color, text);
else
m_renderer->add_text(m_text_cursor_top + Vector2<float>{0.f, -offset}, color, text.data());
return *this;
}
EntityOverlay& EntityOverlay::add_top_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float height, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_width = std::abs(m_canvas.top_left_corner.x - m_canvas.bottom_right_corner.x);
const auto bar_start = Vector2<float>{m_canvas.top_left_corner.x, m_text_cursor_top.y - offset};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width, -height), bg_color);
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width * ratio, -height), color);
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width, -height), outline_color);
m_text_cursor_top.y -= offset + height;
return *this;
}
EntityOverlay& EntityOverlay::add_snap_line(const Vector2<float>& start_pos, const Color& color, const float width)
{
const Vector2<float> line_end =
m_canvas.bottom_left_corner
+ Vector2<float>{m_canvas.bottom_right_corner.x - m_canvas.bottom_left_corner.x, 0.f} / 2;
m_renderer->add_line(start_pos, line_end, color, width);
return *this;
}
void EntityOverlay::draw_dashed_fill(const Vector2<float>& origin, const Vector2<float>& step_dir,
const Vector2<float>& perp_dir, const float full_len, const float filled_len,
const Color& fill_color, const Color& split_color, const float dash_len,
const float gap_len) const
{
if (full_len <= 0.f)
return;
const float step = dash_len + gap_len;
const float n = std::floor((full_len + gap_len) / step);
if (n < 1.f)
return;
const float used = n * dash_len + (n - 1.f) * gap_len;
const float offset = (full_len - used) / 2.f;
const auto fill_rect = [&](const Vector2<float>& a, const Vector2<float>& b, const Color& c)
{
m_renderer->add_filled_rectangle({std::min(a.x, b.x), std::min(a.y, b.y)},
{std::max(a.x, b.x), std::max(a.y, b.y)}, c);
};
// Draw split lines (gaps) across the full bar first
// Leading gap
if (offset > 0.f)
fill_rect(origin, origin + step_dir * offset + perp_dir, split_color);
for (float i = 0.f; i < n; ++i)
{
const float dash_start = offset + i * step;
const float dash_end = dash_start + dash_len;
const float gap_start = dash_end;
const float gap_end = dash_start + step;
// Fill dash only up to filled_len
if (dash_start < filled_len)
{
const auto a = origin + step_dir * dash_start;
const auto b = a + step_dir * std::min(dash_len, filled_len - dash_start) + perp_dir;
fill_rect(a, b, fill_color);
}
// Split line (gap) — always drawn across full bar
if (i < n - 1.f && gap_start < full_len)
{
const auto a = origin + step_dir * gap_start;
const auto b = origin + step_dir * std::min(gap_end, full_len) + perp_dir;
fill_rect(a, b, split_color);
}
}
// Trailing gap
const float trail_start = offset + n * dash_len + (n - 1.f) * gap_len;
if (trail_start < full_len)
fill_rect(origin + step_dir * trail_start, origin + step_dir * full_len + perp_dir, split_color);
}
EntityOverlay& EntityOverlay::add_right_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float width, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float height = std::abs(m_canvas.top_right_corner.y - m_canvas.bottom_right_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_right.x + offset, m_canvas.bottom_right_corner.y};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{width, -height}, bg_color);
draw_dashed_fill(bar_start, {0.f, -1.f}, {width, 0.f}, height, height * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start - Vector2<float>{1.f, 0.f}, bar_start + Vector2<float>{width, -height},
outline_color);
m_text_cursor_right.x += offset + width;
return *this;
}
EntityOverlay& EntityOverlay::add_left_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float width, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float height = std::abs(m_canvas.top_left_corner.y - m_canvas.bottom_left_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_left.x - (offset + width), m_canvas.bottom_left_corner.y};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{width, -height}, bg_color);
draw_dashed_fill(bar_start, {0.f, -1.f}, {width, 0.f}, height, height * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start - Vector2<float>{1.f, 0.f}, bar_start + Vector2<float>{width, -height},
outline_color);
m_text_cursor_left.x -= offset + width;
return *this;
}
EntityOverlay& EntityOverlay::add_top_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float height, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float bar_w = std::abs(m_canvas.top_left_corner.x - m_canvas.top_right_corner.x);
const auto bar_start = Vector2<float>{m_canvas.top_left_corner.x, m_text_cursor_top.y - offset};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{bar_w, -height}, bg_color);
draw_dashed_fill(bar_start, {1.f, 0.f}, {0.f, -height}, bar_w, bar_w * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>{bar_w, -height}, outline_color);
m_text_cursor_top.y -= offset + height;
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float height, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float bar_w = std::abs(m_canvas.bottom_left_corner.x - m_canvas.bottom_right_corner.x);
const auto bar_start = Vector2<float>{m_canvas.bottom_left_corner.x, m_text_cursor_bottom.y + offset};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{bar_w, height}, bg_color);
draw_dashed_fill(bar_start, {1.f, 0.f}, {0.f, height}, bar_w, bar_w * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>{bar_w, height}, outline_color);
m_text_cursor_bottom.y += offset + height;
return *this;
}
EntityOverlay& EntityOverlay::add_skeleton(const Color& color, const float thickness)
{
// Maps normalized (rx in [0,1], ry in [0,1]) to canvas screen position
const auto joint = [&](const float rx, const float ry) -> Vector2<float>
{
const auto top = m_canvas.top_left_corner + (m_canvas.top_right_corner - m_canvas.top_left_corner) * rx;
const auto bot =
m_canvas.bottom_left_corner + (m_canvas.bottom_right_corner - m_canvas.bottom_left_corner) * rx;
return top + (bot - top) * ry;
};
using B = std::pair<std::pair<float, float>, std::pair<float, float>>;
static constexpr std::array<B, 15> k_bones{{
// Spine
{{0.50f, 0.13f}, {0.50f, 0.22f}}, // head → neck
{{0.50f, 0.22f}, {0.50f, 0.38f}}, // neck → chest
{{0.50f, 0.38f}, {0.50f, 0.55f}}, // chest → pelvis
// Left arm
{{0.50f, 0.22f}, {0.25f, 0.25f}}, // neck → L shoulder
{{0.25f, 0.25f}, {0.13f, 0.42f}}, // L shoulder → L elbow
{{0.13f, 0.42f}, {0.08f, 0.56f}}, // L elbow → L hand
// Right arm
{{0.50f, 0.22f}, {0.75f, 0.25f}}, // neck → R shoulder
{{0.75f, 0.25f}, {0.87f, 0.42f}}, // R shoulder → R elbow
{{0.87f, 0.42f}, {0.92f, 0.56f}}, // R elbow → R hand
// Left leg
{{0.50f, 0.55f}, {0.36f, 0.58f}}, // pelvis → L hip
{{0.36f, 0.58f}, {0.32f, 0.77f}}, // L hip → L knee
{{0.32f, 0.77f}, {0.27f, 0.97f}}, // L knee → L foot
// Right leg
{{0.50f, 0.55f}, {0.64f, 0.58f}}, // pelvis → R hip
{{0.64f, 0.58f}, {0.68f, 0.77f}}, // R hip → R knee
{{0.68f, 0.77f}, {0.73f, 0.97f}}, // R knee → R foot
}};
for (const auto& [a, b] : k_bones)
m_renderer->add_line(joint(a.first, a.second), joint(b.first, b.second), color, thickness);
return *this;
}
void EntityOverlay::draw_dashed_line(const Vector2<float>& from, const Vector2<float>& to, const Color& color,
const float dash_len, const float gap_len, const float thickness) const
{
const auto total = (to - from).length();
if (total <= 0.f)
return;
const auto dir = (to - from).normalized();
const float step = dash_len + gap_len;
const float n_dashes = std::floor((total + gap_len) / step);
if (n_dashes < 1.f)
return;
const float used = n_dashes * dash_len + (n_dashes - 1.f) * gap_len;
const float offset = (total - used) / 2.f;
for (float i = 0.f; i < n_dashes; ++i)
{
const float pos = offset + i * step;
const auto dash_start = from + dir * pos;
const auto dash_end = from + dir * std::min(pos + dash_len, total);
m_renderer->add_line(dash_start, dash_end, color, thickness);
}
}
EntityOverlay& EntityOverlay::add_dashed_box(const Color& color, const float dash_len, const float gap_len,
const float thickness)
{
const float min_edge = std::min((m_canvas.top_right_corner - m_canvas.top_left_corner).length(),
(m_canvas.bottom_right_corner - m_canvas.top_right_corner).length());
const float corner_len = std::min(dash_len, min_edge / 2.f);
const auto draw_edge = [&](const Vector2<float>& from, const Vector2<float>& to)
{
const auto dir = (to - from).normalized();
m_renderer->add_line(from, from + dir * corner_len, color, thickness);
draw_dashed_line(from + dir * corner_len, to - dir * corner_len, color, dash_len, gap_len, thickness);
m_renderer->add_line(to - dir * corner_len, to, color, thickness);
};
draw_edge(m_canvas.top_left_corner, m_canvas.top_right_corner);
draw_edge(m_canvas.top_right_corner, m_canvas.bottom_right_corner);
draw_edge(m_canvas.bottom_right_corner, m_canvas.bottom_left_corner);
draw_edge(m_canvas.bottom_left_corner, m_canvas.top_left_corner);
return *this;
}
void EntityOverlay::draw_outlined_text(const Vector2<float>& position, const Color& color,
const std::string_view& text)
{
static constexpr std::array outline_offsets = {
Vector2<float>{-1, -1}, Vector2<float>{-1, 0}, Vector2<float>{-1, 1}, Vector2<float>{0, -1},
Vector2<float>{0, 1}, Vector2<float>{1, -1}, Vector2<float>{1, 0}, Vector2<float>{1, 1}};
for (const auto& outline_offset : outline_offsets)
m_renderer->add_text(position + outline_offset, Color{0.f, 0.f, 0.f, 1.f}, text.data());
m_renderer->add_text(position, color, text.data());
}
EntityOverlay& EntityOverlay::add_bottom_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float height, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_width = std::abs(m_canvas.bottom_right_corner.x - m_canvas.bottom_left_corner.x);
const auto bar_start = Vector2<float>{m_canvas.bottom_left_corner.x, m_text_cursor_bottom.y + offset};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width, height), bg_color);
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width * ratio, height), color);
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width, height), outline_color);
m_text_cursor_bottom.y += offset + height;
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_label(const Color& color, const float offset, const bool outlined,
const std::string_view text)
{
const auto text_size = m_renderer->calc_text_size(text);
if (outlined)
draw_outlined_text(m_text_cursor_bottom + Vector2<float>{0.f, offset}, color, text);
else
m_renderer->add_text(m_text_cursor_bottom + Vector2<float>{0.f, offset}, color, text);
m_text_cursor_bottom.y += text_size.y;
return *this;
}
EntityOverlay& EntityOverlay::add_left_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
const auto text_size = m_renderer->calc_text_size(text);
const auto pos = m_text_cursor_left + Vector2<float>{-(offset + text_size.x), 0.f};
if (outlined)
draw_outlined_text(pos, color, text);
else
m_renderer->add_text(pos, color, text);
m_text_cursor_left.y += text_size.y;
return *this;
}
EntityOverlay& EntityOverlay::add_centered_bottom_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
const auto text_size = m_renderer->calc_text_size(text);
const auto box_center_x =
m_canvas.bottom_left_corner.x + (m_canvas.bottom_right_corner.x - m_canvas.bottom_left_corner.x) / 2.f;
const auto pos = Vector2<float>{box_center_x - text_size.x / 2.f, m_text_cursor_bottom.y + offset};
if (outlined)
draw_outlined_text(pos, color, text);
else
m_renderer->add_text(pos, color, text);
m_text_cursor_bottom.y += text_size.y;
return *this;
}
EntityOverlay& EntityOverlay::add_centered_top_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
const auto text_size = m_renderer->calc_text_size(text);
const auto box_center_x =
m_canvas.top_left_corner.x + (m_canvas.top_right_corner.x - m_canvas.top_left_corner.x) / 2.f;
m_text_cursor_top.y -= text_size.y;
const auto pos = Vector2<float>{box_center_x - text_size.x / 2.f, m_text_cursor_top.y - offset};
if (outlined)
draw_outlined_text(pos, color, text);
else
m_renderer->add_text(pos, color, text);
return *this;
}
EntityOverlay::EntityOverlay(const Vector2<float>& top, const Vector2<float>& bottom,
const std::shared_ptr<HudRendererInterface>& renderer)
: m_canvas(top, bottom), m_text_cursor_right(m_canvas.top_right_corner),
m_text_cursor_top(m_canvas.top_left_corner), m_text_cursor_bottom(m_canvas.bottom_left_corner),
m_text_cursor_left(m_canvas.top_left_corner), m_renderer(renderer)
{
}
// ── Spacers ─────────────────────────────────────────────────────────────────
EntityOverlay& EntityOverlay::add_right_space_vertical(const float size)
{
m_text_cursor_right.y += size;
return *this;
}
EntityOverlay& EntityOverlay::add_right_space_horizontal(const float size)
{
m_text_cursor_right.x += size;
return *this;
}
EntityOverlay& EntityOverlay::add_left_space_vertical(const float size)
{
m_text_cursor_left.y += size;
return *this;
}
EntityOverlay& EntityOverlay::add_left_space_horizontal(const float size)
{
m_text_cursor_left.x -= size;
return *this;
}
EntityOverlay& EntityOverlay::add_top_space_vertical(const float size)
{
m_text_cursor_top.y -= size;
return *this;
}
EntityOverlay& EntityOverlay::add_top_space_horizontal(const float size)
{
m_text_cursor_top.x += size;
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_space_vertical(const float size)
{
m_text_cursor_bottom.y += size;
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_space_horizontal(const float size)
{
m_text_cursor_bottom.x += size;
return *this;
}
// ── Progress rings ──────────────────────────────────────────────────────────
EntityOverlay& EntityOverlay::add_right_progress_ring(const Color& color, const Color& bg, const float radius,
const float ratio, const float thickness, const float offset,
const int segments)
{
const auto cx = m_text_cursor_right.x + offset + radius;
const auto cy = m_text_cursor_right.y + radius;
draw_progress_ring({cx, cy}, widget::ProgressRing{color, bg, radius, ratio, thickness, offset, segments});
m_text_cursor_right.y += radius * 2.f;
return *this;
}
EntityOverlay& EntityOverlay::add_left_progress_ring(const Color& color, const Color& bg, const float radius,
const float ratio, const float thickness, const float offset,
const int segments)
{
const auto cx = m_text_cursor_left.x - offset - radius;
const auto cy = m_text_cursor_left.y + radius;
draw_progress_ring({cx, cy}, widget::ProgressRing{color, bg, radius, ratio, thickness, offset, segments});
m_text_cursor_left.y += radius * 2.f;
return *this;
}
EntityOverlay& EntityOverlay::add_top_progress_ring(const Color& color, const Color& bg, const float radius,
const float ratio, const float thickness, const float offset,
const int segments)
{
m_text_cursor_top.y -= radius * 2.f;
const auto cx = m_text_cursor_top.x + radius;
const auto cy = m_text_cursor_top.y - offset + radius;
draw_progress_ring({cx, cy}, widget::ProgressRing{color, bg, radius, ratio, thickness, offset, segments});
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_progress_ring(const Color& color, const Color& bg, const float radius,
const float ratio, const float thickness, const float offset,
const int segments)
{
const auto cx = m_text_cursor_bottom.x + radius;
const auto cy = m_text_cursor_bottom.y + offset + radius;
draw_progress_ring({cx, cy}, widget::ProgressRing{color, bg, radius, ratio, thickness, offset, segments});
m_text_cursor_bottom.y += radius * 2.f;
return *this;
}
// ── Icons ────────────────────────────────────────────────────────────────────
EntityOverlay& EntityOverlay::add_right_icon(const std::any& texture_id, const float width, const float height,
const Color& tint, const float offset)
{
const auto pos = m_text_cursor_right + Vector2<float>{offset, 0.f};
m_renderer->add_image(texture_id, pos, pos + Vector2<float>{width, height}, tint);
m_text_cursor_right.y += height;
return *this;
}
EntityOverlay& EntityOverlay::add_left_icon(const std::any& texture_id, const float width, const float height,
const Color& tint, const float offset)
{
const auto pos = m_text_cursor_left + Vector2<float>{-(offset + width), 0.f};
m_renderer->add_image(texture_id, pos, pos + Vector2<float>{width, height}, tint);
m_text_cursor_left.y += height;
return *this;
}
EntityOverlay& EntityOverlay::add_top_icon(const std::any& texture_id, const float width, const float height,
const Color& tint, const float offset)
{
m_text_cursor_top.y -= height;
const auto pos = m_text_cursor_top + Vector2<float>{0.f, -offset};
m_renderer->add_image(texture_id, pos, pos + Vector2<float>{width, height}, tint);
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_icon(const std::any& texture_id, const float width, const float height,
const Color& tint, const float offset)
{
const auto pos = m_text_cursor_bottom + Vector2<float>{0.f, offset};
m_renderer->add_image(texture_id, pos, pos + Vector2<float>{width, height}, tint);
m_text_cursor_bottom.y += height;
return *this;
}
// ── widget dispatch ───────────────────────────────────────────────────────
void EntityOverlay::dispatch(const widget::Box& box)
{
add_2d_box(box.color, box.fill, box.thickness);
}
void EntityOverlay::dispatch(const widget::CorneredBox& cornered_box)
{
add_cornered_2d_box(cornered_box.color, cornered_box.fill, cornered_box.corner_ratio, cornered_box.thickness);
}
void EntityOverlay::dispatch(const widget::DashedBox& dashed_box)
{
add_dashed_box(dashed_box.color, dashed_box.dash_len, dashed_box.gap_len, dashed_box.thickness);
}
void EntityOverlay::dispatch(const widget::Skeleton& skeleton)
{
add_skeleton(skeleton.color, skeleton.thickness);
}
void EntityOverlay::dispatch(const widget::SnapLine& snap_line)
{
add_snap_line(snap_line.start, snap_line.color, snap_line.width);
}
void EntityOverlay::dispatch(const widget::ScanMarker& scan_marker)
{
const auto box_width = std::abs(m_canvas.top_right_corner.x - m_canvas.top_left_corner.x);
const auto box_height = std::abs(m_canvas.bottom_left_corner.y - m_canvas.top_left_corner.y);
const auto center_x = (m_canvas.top_left_corner.x + m_canvas.top_right_corner.x) / 2.f;
const auto center_y = m_canvas.top_left_corner.y + box_height * 0.44f;
const auto side = std::min(box_width, box_height) * 0.5f;
const auto h = side * std::sqrt(3.f) / 2.f;
const std::array<Vector2<float>, 3> tri = {
Vector2<float>{center_x, center_y - h * 2.f / 3.f},
Vector2<float>{center_x - side / 2.f, center_y + h / 3.f},
Vector2<float>{center_x + side / 2.f, center_y + h / 3.f},
};
m_renderer->add_filled_polyline({tri.data(), tri.size()}, scan_marker.color);
if (scan_marker.outline.value().w > 0.f)
m_renderer->add_polyline({tri.data(), tri.size()}, scan_marker.outline, scan_marker.outline_thickness);
}
void EntityOverlay::dispatch(const widget::AimDot& aim_dot)
{
m_renderer->add_filled_circle(aim_dot.position, aim_dot.radius, aim_dot.color);
}
void EntityOverlay::dispatch(const widget::ProjectileAim& proj_widget)
{
const auto box_width = std::abs(m_canvas.top_right_corner.x - m_canvas.top_left_corner.x);
const auto box_height = std::abs(m_canvas.bottom_left_corner.y - m_canvas.top_left_corner.y);
const auto box_center = m_canvas.top_left_corner + Vector2{box_width, box_height} / 2.f;
m_renderer->add_line(box_center, proj_widget.position, proj_widget.color, proj_widget.line_size);
if (proj_widget.figure == widget::ProjectileAim::Figure::CIRCLE)
{
m_renderer->add_filled_circle(proj_widget.position, proj_widget.size, proj_widget.color);
return;
}
if (proj_widget.figure == widget::ProjectileAim::Figure::SQUARE)
{
const auto box_min = proj_widget.position - Vector2{proj_widget.size, proj_widget.size} / 2.f;
const auto box_max = proj_widget.position + Vector2{proj_widget.size, proj_widget.size} / 2.f;
m_renderer->add_filled_rectangle(box_min, box_max, proj_widget.color);
return;
}
std::unreachable();
}
void EntityOverlay::draw_progress_ring(const Vector2<float>& center, const widget::ProgressRing& ring)
{
constexpr auto pi = std::numbers::pi_v<float>;
const float ratio = std::clamp(ring.ratio, 0.f, 1.f);
m_renderer->add_circle(center, ring.radius, ring.bg, ring.thickness, ring.segments);
if (ratio > 0.f)
{
const float a_min = -pi / 2.f;
const float a_max = a_min + ratio * 2.f * pi;
m_renderer->add_arc(center, ring.radius, a_min, a_max, ring.color, ring.thickness, ring.segments);
}
}
// ── Side container dispatch ───────────────────────────────────────────────
void EntityOverlay::dispatch(const widget::RightSide& right_side)
{
for (const auto& child : right_side.children)
std::visit(
widget::Overloaded{
[](const widget::None&)
{
},
[this](const widget::Bar& w)
{
add_right_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset);
},
[this](const widget::DashedBar& w)
{
add_right_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len,
w.offset);
},
[this](const widget::Label& w)
{
add_right_label(w.color, w.offset, w.outlined, w.text);
},
[this](const widget::Centered<widget::Label>& w)
{
add_right_label(w.child.color, w.child.offset, w.child.outlined, w.child.text);
},
[this](const widget::SpaceVertical& w)
{
add_right_space_vertical(w.size);
},
[this](const widget::SpaceHorizontal& w)
{
add_right_space_horizontal(w.size);
},
[this](const widget::ProgressRing& w)
{
add_right_progress_ring(w.color, w.bg, w.radius, w.ratio, w.thickness, w.offset,
w.segments);
},
[this](const widget::Icon& w)
{
add_right_icon(w.texture_id, w.width, w.height, w.tint, w.offset);
},
},
child);
}
void EntityOverlay::dispatch(const widget::LeftSide& left_side)
{
for (const auto& child : left_side.children)
std::visit(
widget::Overloaded{
[](const widget::None&)
{
},
[this](const widget::Bar& w)
{
add_left_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset);
},
[this](const widget::DashedBar& w)
{
add_left_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len,
w.offset);
},
[this](const widget::Label& w)
{
add_left_label(w.color, w.offset, w.outlined, w.text);
},
[this](const widget::Centered<widget::Label>& w)
{
add_left_label(w.child.color, w.child.offset, w.child.outlined, w.child.text);
},
[this](const widget::SpaceVertical& w)
{
add_left_space_vertical(w.size);
},
[this](const widget::SpaceHorizontal& w)
{
add_left_space_horizontal(w.size);
},
[this](const widget::ProgressRing& w)
{
add_left_progress_ring(w.color, w.bg, w.radius, w.ratio, w.thickness, w.offset,
w.segments);
},
[this](const widget::Icon& w)
{
add_left_icon(w.texture_id, w.width, w.height, w.tint, w.offset);
},
},
child);
}
void EntityOverlay::dispatch(const widget::TopSide& top_side)
{
for (const auto& child : top_side.children)
std::visit(
widget::Overloaded{
[](const widget::None&)
{
},
[this](const widget::Bar& w)
{
add_top_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset);
},
[this](const widget::DashedBar& w)
{
add_top_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len,
w.offset);
},
[this](const widget::Label& w)
{
add_top_label(w.color, w.offset, w.outlined, w.text);
},
[this](const widget::Centered<widget::Label>& w)
{
add_centered_top_label(w.child.color, w.child.offset, w.child.outlined, w.child.text);
},
[this](const widget::SpaceVertical& w)
{
add_top_space_vertical(w.size);
},
[this](const widget::SpaceHorizontal& w)
{
add_top_space_horizontal(w.size);
},
[this](const widget::ProgressRing& w)
{
add_top_progress_ring(w.color, w.bg, w.radius, w.ratio, w.thickness, w.offset,
w.segments);
},
[this](const widget::Icon& w)
{
add_top_icon(w.texture_id, w.width, w.height, w.tint, w.offset);
},
},
child);
}
void EntityOverlay::dispatch(const widget::BottomSide& bottom_side)
{
for (const auto& child : bottom_side.children)
std::visit(
widget::Overloaded{
[](const widget::None&)
{
},
[this](const widget::Bar& w)
{
add_bottom_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset);
},
[this](const widget::DashedBar& w)
{
add_bottom_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len,
w.offset);
},
[this](const widget::Label& w)
{
add_bottom_label(w.color, w.offset, w.outlined, w.text);
},
[this](const widget::Centered<widget::Label>& w)
{
add_centered_bottom_label(w.child.color, w.child.offset, w.child.outlined,
w.child.text);
},
[this](const widget::SpaceVertical& w)
{
add_bottom_space_vertical(w.size);
},
[this](const widget::SpaceHorizontal& w)
{
add_bottom_space_horizontal(w.size);
},
[this](const widget::ProgressRing& w)
{
add_bottom_progress_ring(w.color, w.bg, w.radius, w.ratio, w.thickness, w.offset,
w.segments);
},
[this](const widget::Icon& w)
{
add_bottom_icon(w.texture_id, w.width, w.height, w.tint, w.offset);
},
},
child);
}
} // namespace omath::hud

View File

@@ -0,0 +1,82 @@
//
// Created by orange on 13.03.2026.
//
#include "omath/hud/renderer_realizations/imgui_renderer.hpp"
#ifdef OMATH_IMGUI_INTEGRATION
#include <imgui.h>
namespace omath::hud
{
ImguiHudRenderer::~ImguiHudRenderer() = default;
void ImguiHudRenderer::add_line(const Vector2<float>& line_start, const Vector2<float>& line_end,
const Color& color, const float thickness)
{
ImGui::GetBackgroundDrawList()->AddLine(line_start.to_im_vec2(), line_end.to_im_vec2(), color.to_im_color(),
thickness);
}
void ImguiHudRenderer::add_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color,
const float thickness)
{
ImGui::GetBackgroundDrawList()->AddPolyline(reinterpret_cast<const ImVec2*>(vertexes.data()),
static_cast<int>(vertexes.size()), color.to_im_color(),
ImDrawFlags_Closed, thickness);
}
void ImguiHudRenderer::add_filled_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color)
{
ImGui::GetBackgroundDrawList()->AddConvexPolyFilled(reinterpret_cast<const ImVec2*>(vertexes.data()),
static_cast<int>(vertexes.size()), color.to_im_color());
}
void ImguiHudRenderer::add_rectangle(const Vector2<float>& min, const Vector2<float>& max, const Color& color)
{
ImGui::GetBackgroundDrawList()->AddRect(min.to_im_vec2(), max.to_im_vec2(), color.to_im_color());
}
void ImguiHudRenderer::add_filled_rectangle(const Vector2<float>& min, const Vector2<float>& max,
const Color& color)
{
ImGui::GetBackgroundDrawList()->AddRectFilled(min.to_im_vec2(), max.to_im_vec2(), color.to_im_color());
}
void ImguiHudRenderer::add_circle(const Vector2<float>& center, const float radius, const Color& color,
const float thickness, const int segments)
{
ImGui::GetBackgroundDrawList()->AddCircle(center.to_im_vec2(), radius, color.to_im_color(), segments, thickness);
}
void ImguiHudRenderer::add_filled_circle(const Vector2<float>& center, const float radius, const Color& color,
const int segments)
{
ImGui::GetBackgroundDrawList()->AddCircleFilled(center.to_im_vec2(), radius, color.to_im_color(), segments);
}
void ImguiHudRenderer::add_arc(const Vector2<float>& center, const float radius, const float a_min, const float a_max,
const Color& color, const float thickness, const int segments)
{
ImGui::GetBackgroundDrawList()->PathArcTo(center.to_im_vec2(), radius, a_min, a_max, segments);
ImGui::GetBackgroundDrawList()->PathStroke(color.to_im_color(), ImDrawFlags_None, thickness);
}
void ImguiHudRenderer::add_image(const std::any& texture_id, const Vector2<float>& min, const Vector2<float>& max,
const Color& tint)
{
ImGui::GetBackgroundDrawList()->AddImage(std::any_cast<ImTextureID>(texture_id), min.to_im_vec2(),
max.to_im_vec2(), {0, 0}, {1, 1}, tint.to_im_color());
}
void ImguiHudRenderer::add_text(const Vector2<float>& position, const Color& color, const std::string_view& text)
{
ImGui::GetBackgroundDrawList()->AddText(position.to_im_vec2(), color.to_im_color(), text.data(),
text.data() + text.size());
}
[[nodiscard]]
Vector2<float> ImguiHudRenderer::calc_text_size(const std::string_view& text)
{
return Vector2<float>::from_im_vec2(ImGui::CalcTextSize(text.data()));
}
} // namespace omath::hud
#endif // OMATH_IMGUI_INTEGRATION

View File

@@ -21,7 +21,7 @@ namespace omath::projectile_prediction
const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
const float v0 = projectile.m_launch_speed; const float v0 = projectile.m_launch_speed;
const float v0_sqr = v0 * v0; const float v0_sqr = v0 * v0;
const Vector3 proj_origin = projectile.m_origin; const Vector3 proj_origin = projectile.m_origin + projectile.m_launch_offset;
constexpr int SIMD_FACTOR = 8; constexpr int SIMD_FACTOR = 8;
float current_time = m_simulation_time_step; float current_time = m_simulation_time_step;
@@ -124,6 +124,110 @@ namespace omath::projectile_prediction
std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name())); std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name()));
#endif #endif
} }
std::optional<AimAngles>
ProjPredEngineAvx2::maybe_calculate_aim_angles([[maybe_unused]] const Projectile& projectile,
[[maybe_unused]] const Target& target) const
{
#if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__)
const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
const float v0 = projectile.m_launch_speed;
const Vector3 proj_origin = projectile.m_origin + projectile.m_launch_offset;
constexpr int SIMD_FACTOR = 8;
float current_time = m_simulation_time_step;
for (; current_time <= m_maximum_simulation_time; current_time += m_simulation_time_step * SIMD_FACTOR)
{
const __m256 times
= _mm256_setr_ps(current_time, current_time + m_simulation_time_step,
current_time + m_simulation_time_step * 2, current_time + m_simulation_time_step * 3,
current_time + m_simulation_time_step * 4, current_time + m_simulation_time_step * 5,
current_time + m_simulation_time_step * 6, current_time + m_simulation_time_step * 7);
const __m256 target_x
= _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.x), times, _mm256_set1_ps(target.m_origin.x));
const __m256 target_y
= _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.y), times, _mm256_set1_ps(target.m_origin.y));
const __m256 times_sq = _mm256_mul_ps(times, times);
const __m256 target_z = _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.z), times,
_mm256_fnmadd_ps(_mm256_set1_ps(0.5f * m_gravity_constant), times_sq,
_mm256_set1_ps(target.m_origin.z)));
const __m256 delta_x = _mm256_sub_ps(target_x, _mm256_set1_ps(proj_origin.x));
const __m256 delta_y = _mm256_sub_ps(target_y, _mm256_set1_ps(proj_origin.y));
const __m256 d_sqr = _mm256_add_ps(_mm256_mul_ps(delta_x, delta_x), _mm256_mul_ps(delta_y, delta_y));
const __m256 delta_z = _mm256_sub_ps(target_z, _mm256_set1_ps(proj_origin.z));
const __m256 bg_times_sq = _mm256_mul_ps(_mm256_set1_ps(bullet_gravity), times_sq);
const __m256 term = _mm256_add_ps(delta_z, _mm256_mul_ps(_mm256_set1_ps(0.5f), bg_times_sq));
const __m256 term_sq = _mm256_mul_ps(term, term);
const __m256 numerator = _mm256_add_ps(d_sqr, term_sq);
const __m256 denominator = _mm256_add_ps(times_sq, _mm256_set1_ps(1e-8f));
const __m256 required_v0_sqr = _mm256_div_ps(numerator, denominator);
const __m256 v0_sqr_vec = _mm256_set1_ps(v0 * v0 + 1e-3f);
const __m256 mask = _mm256_cmp_ps(required_v0_sqr, v0_sqr_vec, _CMP_LE_OQ);
const unsigned valid_mask = _mm256_movemask_ps(mask);
if (!valid_mask)
continue;
alignas(32) float valid_times[SIMD_FACTOR];
_mm256_store_ps(valid_times, times);
for (int i = 0; i < SIMD_FACTOR; ++i)
{
if (!(valid_mask & (1 << i)))
continue;
const float candidate_time = valid_times[i];
if (candidate_time > m_maximum_simulation_time)
continue;
for (float fine_time = candidate_time - m_simulation_time_step * 2;
fine_time <= candidate_time + m_simulation_time_step * 2; fine_time += m_simulation_time_step)
{
if (fine_time < 0)
continue;
Vector3 target_pos = target.m_origin + target.m_velocity * fine_time;
if (target.m_is_airborne)
target_pos.z -= 0.5f * m_gravity_constant * fine_time * fine_time;
const auto pitch = calculate_pitch(proj_origin, target_pos, bullet_gravity, v0, fine_time);
if (!pitch)
continue;
const Vector3 delta = target_pos - projectile.m_origin;
const float yaw = angles::radians_to_degrees(std::atan2(delta.y, delta.x));
return AimAngles{*pitch, yaw};
}
}
}
for (; current_time <= m_maximum_simulation_time; current_time += m_simulation_time_step)
{
Vector3 target_pos = target.m_origin + target.m_velocity * current_time;
if (target.m_is_airborne)
target_pos.z -= 0.5f * m_gravity_constant * current_time * current_time;
const auto pitch = calculate_pitch(proj_origin, target_pos, bullet_gravity, v0, current_time);
if (!pitch)
continue;
const Vector3 delta = target_pos - projectile.m_origin;
const float yaw = angles::radians_to_degrees(std::atan2(delta.y, delta.x));
return AimAngles{*pitch, yaw};
}
return std::nullopt;
#else
throw std::runtime_error(
std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name()));
#endif
}
ProjPredEngineAvx2::ProjPredEngineAvx2(const float gravity_constant, const float simulation_time_step, ProjPredEngineAvx2::ProjPredEngineAvx2(const float gravity_constant, const float simulation_time_step,
const float maximum_simulation_time) const float maximum_simulation_time)
: m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step), : m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step),

View File

@@ -5,6 +5,7 @@
#include <array> #include <array>
#include <fstream> #include <fstream>
#include <omath/utility/elf_pattern_scan.hpp> #include <omath/utility/elf_pattern_scan.hpp>
#include <span>
#include <utility> #include <utility>
#include <variant> #include <variant>
#include <vector> #include <vector>
@@ -140,6 +141,87 @@ namespace
std::uintptr_t raw_base_addr{}; std::uintptr_t raw_base_addr{};
std::vector<std::byte> data; std::vector<std::byte> data;
}; };
template<FileArch arch>
std::optional<ExtractedSection> get_elf_section_from_memory_impl(const std::span<const std::byte> data,
const std::string_view& section_name)
{
using FH = typename ElfHeaders<arch>::FileHeader;
using SH = typename ElfHeaders<arch>::SectionHeader;
if (data.size() < sizeof(FH))
return std::nullopt;
const auto* file_header = reinterpret_cast<const FH*>(data.data());
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 shstrndx = static_cast<std::size_t>(file_header->e_shstrndx);
const auto shstrtab_hdr_off = shoff + shstrndx * sizeof(SH);
if (shstrtab_hdr_off + sizeof(SH) > data.size())
return std::nullopt;
const auto* shstrtab_hdr = reinterpret_cast<const SH*>(data.data() + shstrtab_hdr_off);
const auto shstrtab_off = static_cast<std::size_t>(shstrtab_hdr->sh_offset);
const auto shstrtab_size = static_cast<std::size_t>(shstrtab_hdr->sh_size);
if (shstrtab_off + shstrtab_size > data.size())
return std::nullopt;
const auto* shstrtab = reinterpret_cast<const char*>(data.data() + shstrtab_off);
for (std::size_t i = 0; i < shnum; ++i)
{
const auto sect_hdr_off = shoff + i * sizeof(SH);
if (sect_hdr_off + sizeof(SH) > data.size())
continue;
const auto* section = reinterpret_cast<const SH*>(data.data() + sect_hdr_off);
if (std::cmp_greater_equal(section->sh_name, shstrtab_size))
continue;
if (std::string_view{shstrtab + section->sh_name} != section_name)
continue;
const auto raw_off = static_cast<std::size_t>(section->sh_offset);
const auto sec_size = static_cast<std::size_t>(section->sh_size);
if (raw_off + sec_size > data.size())
return std::nullopt;
ExtractedSection out;
out.virtual_base_addr = static_cast<std::uintptr_t>(section->sh_addr);
out.raw_base_addr = raw_off;
out.data.assign(data.data() + raw_off, data.data() + raw_off + sec_size);
return out;
}
return std::nullopt;
}
std::optional<ExtractedSection> get_elf_section_by_name_from_memory(const std::span<const std::byte> data,
const std::string_view& section_name)
{
constexpr std::string_view valid_elf_signature = "\x7F"
"ELF";
if (data.size() < ei_nident)
return std::nullopt;
if (std::string_view{reinterpret_cast<const char*>(data.data()), valid_elf_signature.size()}
!= valid_elf_signature)
return std::nullopt;
const auto class_byte = static_cast<uint8_t>(data[ei_class]);
if (class_byte == elfclass64)
return get_elf_section_from_memory_impl<FileArch::x64>(data, section_name);
if (class_byte == elfclass32)
return get_elf_section_from_memory_impl<FileArch::x32>(data, section_name);
return std::nullopt;
}
[[maybe_unused]] [[maybe_unused]]
std::optional<ExtractedSection> get_elf_section_by_name(const std::filesystem::path& path, std::optional<ExtractedSection> get_elf_section_by_name(const std::filesystem::path& path,
const std::string_view& section_name) const std::string_view& section_name)
@@ -322,4 +404,27 @@ namespace omath
.raw_base_addr = pe_section->raw_base_addr, .raw_base_addr = pe_section->raw_base_addr,
.target_offset = offset}; .target_offset = offset};
} }
std::optional<SectionScanResult>
ElfPatternScanner::scan_for_pattern_in_memory_file(const std::span<const std::byte> file_data,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
const auto section = get_elf_section_by_name_from_memory(file_data, target_section_name);
if (!section.has_value()) [[unlikely]]
return std::nullopt;
const auto scan_result =
PatternScanner::scan_for_pattern(section->data.cbegin(), section->data.cend(), pattern);
if (scan_result == section->data.cend())
return std::nullopt;
const auto offset = std::distance(section->data.begin(), scan_result);
return SectionScanResult{.virtual_base_addr = section->virtual_base_addr,
.raw_base_addr = section->raw_base_addr,
.target_offset = offset};
}
} // namespace omath } // namespace omath

View File

@@ -5,6 +5,7 @@
#include "omath/utility/pattern_scan.hpp" #include "omath/utility/pattern_scan.hpp"
#include <cstring> #include <cstring>
#include <fstream> #include <fstream>
#include <span>
#include <variant> #include <variant>
#include <vector> #include <vector>
@@ -231,6 +232,96 @@ namespace
return std::nullopt; return std::nullopt;
} }
template<typename HeaderType, typename SegmentType, typename SectionType, std::uint32_t segment_cmd>
std::optional<ExtractedSection> extract_section_from_memory_impl(const std::span<const std::byte> data,
const std::string_view& section_name)
{
if (data.size() < sizeof(HeaderType))
return std::nullopt;
const auto* header = reinterpret_cast<const HeaderType*>(data.data());
std::size_t cmd_offset = sizeof(HeaderType);
for (std::uint32_t i = 0; i < header->ncmds; ++i)
{
if (cmd_offset + sizeof(LoadCommand) > data.size())
return std::nullopt;
const auto* lc = reinterpret_cast<const LoadCommand*>(data.data() + cmd_offset);
if (lc->cmd != segment_cmd)
{
cmd_offset += lc->cmdsize;
continue;
}
if (cmd_offset + sizeof(SegmentType) > data.size())
return std::nullopt;
const auto* segment = reinterpret_cast<const SegmentType*>(data.data() + cmd_offset);
if (!segment->nsects)
{
cmd_offset += lc->cmdsize;
continue;
}
std::size_t sect_offset = cmd_offset + sizeof(SegmentType);
for (std::uint32_t j = 0; j < segment->nsects; ++j)
{
if (sect_offset + sizeof(SectionType) > data.size())
return std::nullopt;
const auto* section = reinterpret_cast<const SectionType*>(data.data() + sect_offset);
if (get_section_name(section->sectname) != section_name)
{
sect_offset += sizeof(SectionType);
continue;
}
const auto raw_off = static_cast<std::size_t>(section->offset);
const auto sec_size = static_cast<std::size_t>(section->size);
if (raw_off + sec_size > data.size())
return std::nullopt;
ExtractedSection out;
out.virtual_base_addr = static_cast<std::uintptr_t>(section->addr);
out.raw_base_addr = raw_off;
out.data.assign(data.data() + raw_off, data.data() + raw_off + sec_size);
return out;
}
cmd_offset += lc->cmdsize;
}
return std::nullopt;
}
[[nodiscard]]
std::optional<ExtractedSection> get_macho_section_by_name_from_memory(const std::span<const std::byte> data,
const std::string_view& section_name)
{
if (data.size() < sizeof(std::uint32_t))
return std::nullopt;
std::uint32_t magic{};
std::memcpy(&magic, data.data(), sizeof(magic));
if (magic == mh_magic_64 || magic == mh_cigam_64)
return extract_section_from_memory_impl<MachHeader64, SegmentCommand64, Section64, lc_segment_64>(
data, section_name);
if (magic == mh_magic_32 || magic == mh_cigam_32)
return extract_section_from_memory_impl<MachHeader32, SegmentCommand32, Section32, lc_segment>(data,
section_name);
return std::nullopt;
}
[[nodiscard]] [[nodiscard]]
std::optional<ExtractedSection> get_macho_section_by_name(const std::filesystem::path& path, std::optional<ExtractedSection> get_macho_section_by_name(const std::filesystem::path& path,
const std::string_view& section_name) const std::string_view& section_name)
@@ -346,4 +437,27 @@ namespace omath
.raw_base_addr = macho_section->raw_base_addr, .raw_base_addr = macho_section->raw_base_addr,
.target_offset = offset}; .target_offset = offset};
} }
std::optional<SectionScanResult>
MachOPatternScanner::scan_for_pattern_in_memory_file(const std::span<const std::byte> file_data,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
const auto section = get_macho_section_by_name_from_memory(file_data, target_section_name);
if (!section.has_value()) [[unlikely]]
return std::nullopt;
const auto scan_result =
PatternScanner::scan_for_pattern(section->data.cbegin(), section->data.cend(), pattern);
if (scan_result == section->data.cend())
return std::nullopt;
const auto offset = std::distance(section->data.begin(), scan_result);
return SectionScanResult{.virtual_base_addr = section->virtual_base_addr,
.raw_base_addr = section->raw_base_addr,
.target_offset = offset};
}
} // namespace omath } // namespace omath

View File

@@ -7,6 +7,7 @@
#include <span> #include <span>
#include <stdexcept> #include <stdexcept>
#include <variant> #include <variant>
#include <vector>
// Internal PE shit defines // Internal PE shit defines
// Big thx for linuxpe sources as ref // Big thx for linuxpe sources as ref
@@ -244,6 +245,78 @@ namespace
std::vector<std::byte> data; std::vector<std::byte> data;
}; };
[[nodiscard]]
std::optional<ExtractedSection> extract_section_from_pe_memory(const std::span<const std::byte> data,
const std::string_view& section_name)
{
if (data.size() < sizeof(DosHeader))
return std::nullopt;
const auto* dos_header = reinterpret_cast<const DosHeader*>(data.data());
if (invalid_dos_header_file(*dos_header))
return std::nullopt;
const auto nt_off = static_cast<std::size_t>(dos_header->e_lfanew);
if (nt_off + sizeof(ImageNtHeaders<NtArchitecture::x32_bit>) > data.size())
return std::nullopt;
const auto* x86_hdrs =
reinterpret_cast<const ImageNtHeaders<NtArchitecture::x32_bit>*>(data.data() + nt_off);
NtHeaderVariant nt_headers;
if (x86_hdrs->optional_header.magic == opt_hdr32_magic)
nt_headers = *x86_hdrs;
else if (x86_hdrs->optional_header.magic == opt_hdr64_magic)
{
if (nt_off + sizeof(ImageNtHeaders<NtArchitecture::x64_bit>) > data.size())
return std::nullopt;
nt_headers = *reinterpret_cast<const ImageNtHeaders<NtArchitecture::x64_bit>*>(data.data() + nt_off);
}
else
return std::nullopt;
if (invalid_nt_header_file(nt_headers))
return std::nullopt;
return std::visit(
[&data, &section_name, nt_off](const auto& concrete_headers) -> std::optional<ExtractedSection>
{
constexpr std::size_t sig_size = sizeof(concrete_headers.signature);
const auto section_table_off = nt_off + sig_size + sizeof(FileHeader)
+ concrete_headers.file_header.size_optional_header;
for (std::size_t i = 0; i < concrete_headers.file_header.num_sections; ++i)
{
const auto sh_off = section_table_off + i * sizeof(SectionHeader);
if (sh_off + sizeof(SectionHeader) > data.size())
return std::nullopt;
const auto* section = reinterpret_cast<const SectionHeader*>(data.data() + sh_off);
if (std::string_view(section->name) != section_name)
continue;
const auto raw_off = static_cast<std::size_t>(section->ptr_raw_data);
const auto raw_size = static_cast<std::size_t>(section->size_raw_data);
if (raw_off + raw_size > data.size())
return std::nullopt;
std::vector<std::byte> section_data(data.data() + raw_off, data.data() + raw_off + raw_size);
return ExtractedSection{
.virtual_base_addr = static_cast<std::uintptr_t>(
section->virtual_address + concrete_headers.optional_header.image_base),
.raw_base_addr = raw_off,
.data = std::move(section_data)};
}
return std::nullopt;
},
nt_headers);
}
[[nodiscard]] [[nodiscard]]
std::optional<ExtractedSection> extract_section_from_pe_file(const std::filesystem::path& path_to_file, std::optional<ExtractedSection> extract_section_from_pe_file(const std::filesystem::path& path_to_file,
const std::string_view& section_name) const std::string_view& section_name)
@@ -383,4 +456,27 @@ namespace omath
.raw_base_addr = pe_section->raw_base_addr, .raw_base_addr = pe_section->raw_base_addr,
.target_offset = offset}; .target_offset = offset};
} }
std::optional<SectionScanResult>
PePatternScanner::scan_for_pattern_in_memory_file(const std::span<const std::byte> file_data,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
const auto pe_section = extract_section_from_pe_memory(file_data, 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 } // namespace omath

View File

@@ -4,7 +4,7 @@ project(unit_tests)
include(GoogleTest) include(GoogleTest)
file(GLOB_RECURSE UNIT_TESTS_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/general/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/engines/*.cpp") file(GLOB_RECURSE UNIT_TESTS_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/general/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/engines/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/*.hpp")
add_executable(${PROJECT_NAME} ${UNIT_TESTS_SOURCES} main.cpp) add_executable(${PROJECT_NAME} ${UNIT_TESTS_SOURCES} main.cpp)
set_target_properties( set_target_properties(

View File

@@ -20,6 +20,8 @@
#include <omath/engines/unreal_engine/traits/mesh_trait.hpp> #include <omath/engines/unreal_engine/traits/mesh_trait.hpp>
#include <omath/engines/unreal_engine/traits/camera_trait.hpp> #include <omath/engines/unreal_engine/traits/camera_trait.hpp>
#include <omath/engines/source_engine/traits/pred_engine_trait.hpp>
#include <omath/projectile_prediction/projectile.hpp> #include <omath/projectile_prediction/projectile.hpp>
#include <omath/projectile_prediction/target.hpp> #include <omath/projectile_prediction/target.hpp>
#include <optional> #include <optional>
@@ -35,6 +37,132 @@ static void expect_matrix_near(const MatT& a, const MatT& b, float eps = 1e-5f)
EXPECT_NEAR(a.at(r, c), b.at(r, c), eps); EXPECT_NEAR(a.at(r, c), b.at(r, c), eps);
} }
// ── Launch offset tests for all engines ──────────────────────────────────────
#include <omath/engines/cry_engine/traits/pred_engine_trait.hpp>
// Helper: verify that zero offset matches default-initialized offset behavior
template<typename Trait>
static void verify_launch_offset_at_time_zero(const Vector3<float>& origin, const Vector3<float>& offset)
{
projectile_prediction::Projectile p;
p.m_origin = origin;
p.m_launch_offset = offset;
p.m_launch_speed = 100.f;
p.m_gravity_scale = 1.f;
const auto pos = Trait::predict_projectile_position(p, 0.f, 0.f, 0.f, 9.81f);
const auto expected = origin + offset;
EXPECT_NEAR(pos.x, expected.x, 1e-4f);
EXPECT_NEAR(pos.y, expected.y, 1e-4f);
EXPECT_NEAR(pos.z, expected.z, 1e-4f);
}
template<typename Trait>
static void verify_zero_offset_matches_default()
{
projectile_prediction::Projectile p;
p.m_origin = {10.f, 20.f, 30.f};
p.m_launch_offset = {0.f, 0.f, 0.f};
p.m_launch_speed = 50.f;
p.m_gravity_scale = 1.f;
projectile_prediction::Projectile p2;
p2.m_origin = {10.f, 20.f, 30.f};
p2.m_launch_speed = 50.f;
p2.m_gravity_scale = 1.f;
const auto pos1 = Trait::predict_projectile_position(p, 15.f, 30.f, 1.f, 9.81f);
const auto pos2 = Trait::predict_projectile_position(p2, 15.f, 30.f, 1.f, 9.81f);
#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64)
constexpr float tol = 1e-6f;
#else
constexpr float tol = 1e-4f;
#endif
EXPECT_NEAR(pos1.x, pos2.x, tol);
EXPECT_NEAR(pos1.y, pos2.y, tol);
EXPECT_NEAR(pos1.z, pos2.z, tol);
}
TEST(LaunchOffsetTests, Source_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<source_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, Source_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<source_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, Frostbite_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<frostbite_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, Frostbite_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<frostbite_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, IW_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<iw_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, IW_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<iw_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, OpenGL_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<opengl_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, OpenGL_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<opengl_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, Unity_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<unity_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, Unity_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<unity_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, Unreal_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<unreal_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, Unreal_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<unreal_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, CryEngine_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<cry_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, CryEngine_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<cry_engine::PredEngineTrait>();
}
// Test that offset shifts the projectile position at t>0 as well
TEST(LaunchOffsetTests, OffsetShiftsTrajectory)
{
projectile_prediction::Projectile p_no_offset;
p_no_offset.m_origin = {0.f, 0.f, 0.f};
p_no_offset.m_launch_speed = 100.f;
p_no_offset.m_gravity_scale = 1.f;
projectile_prediction::Projectile p_with_offset;
p_with_offset.m_origin = {0.f, 0.f, 0.f};
p_with_offset.m_launch_offset = {10.f, 5.f, -3.f};
p_with_offset.m_launch_speed = 100.f;
p_with_offset.m_gravity_scale = 1.f;
const auto pos1 = source_engine::PredEngineTrait::predict_projectile_position(p_no_offset, 20.f, 45.f, 2.f, 9.81f);
const auto pos2 = source_engine::PredEngineTrait::predict_projectile_position(p_with_offset, 20.f, 45.f, 2.f, 9.81f);
// The difference should be exactly the launch offset
EXPECT_NEAR(pos2.x - pos1.x, 10.f, 1e-4f);
EXPECT_NEAR(pos2.y - pos1.y, 5.f, 1e-4f);
EXPECT_NEAR(pos2.z - pos1.z, -3.f, 1e-4f);
}
// 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)
{ {

View File

@@ -0,0 +1,192 @@
#pragma once
// Cross-platform helper for creating binary test "files" without writing to disk where possible.
//
// Strategy:
// - Linux (non-Android, or Android API >= 30): memfd_create → /proc/self/fd/<N> (no disk I/O)
// - All other platforms: anonymous temp file via std::tmpfile(), accessed via /proc/self/fd/<N>
// on Linux, or a named temp file (cleaned up on destruction) elsewhere.
//
// Usage:
// auto f = MemFdFile::create(myVector);
// ASSERT_TRUE(f.valid());
// scanner.scan_for_pattern_in_file(f.path(), ...);
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <random>
#include <string>
#include <vector>
#if defined(__linux__)
# include <unistd.h>
# include <fcntl.h>
# if defined(__ANDROID__)
# if __ANDROID_API__ >= 30
# include <sys/mman.h>
# define OMATH_TEST_USE_MEMFD 1
# endif
// Android < 30: fall through to tmpfile() path below
# else
// Desktop Linux: memfd_create available since glibc 2.27 / kernel 3.17
# include <sys/mman.h>
# define OMATH_TEST_USE_MEMFD 1
# endif
#endif
class MemFdFile
{
public:
MemFdFile() = default;
~MemFdFile()
{
#if defined(OMATH_TEST_USE_MEMFD)
if (m_fd >= 0)
::close(m_fd);
#else
if (!m_temp_path.empty())
std::filesystem::remove(m_temp_path);
#endif
}
MemFdFile(const MemFdFile&) = delete;
MemFdFile& operator=(const MemFdFile&) = delete;
MemFdFile(MemFdFile&& o) noexcept
: m_path(std::move(o.m_path))
#if defined(OMATH_TEST_USE_MEMFD)
, m_fd(o.m_fd)
#else
, m_temp_path(std::move(o.m_temp_path))
#endif
{
#if defined(OMATH_TEST_USE_MEMFD)
o.m_fd = -1;
#else
o.m_temp_path.clear();
#endif
}
[[nodiscard]] bool valid() const { return !m_path.empty(); }
[[nodiscard]] const std::filesystem::path& path() const { return m_path; }
static MemFdFile create(const std::vector<std::uint8_t>& data)
{
return create(data.data(), data.size());
}
static MemFdFile create(const std::uint8_t* data, std::size_t size)
{
MemFdFile f;
#if defined(OMATH_TEST_USE_MEMFD)
f.m_fd = static_cast<int>(::memfd_create("test_bin", 0));
if (f.m_fd < 0)
return f;
if (!write_all(f.m_fd, data, size))
{
::close(f.m_fd);
f.m_fd = -1;
return f;
}
f.m_path = "/proc/self/fd/" + std::to_string(f.m_fd);
#else
// Portable fallback: write to a uniquely-named temp file and delete on destruction
const auto tmp_dir = std::filesystem::temp_directory_path();
std::mt19937_64 rng(std::random_device{}());
const auto unique_name = "omath_test_" + std::to_string(rng()) + ".bin";
f.m_temp_path = (tmp_dir / unique_name).string();
f.m_path = f.m_temp_path;
std::ofstream out(f.m_temp_path, std::ios::binary | std::ios::trunc);
if (!out.is_open())
{
f.m_temp_path.clear();
f.m_path.clear();
return f;
}
out.write(reinterpret_cast<const char*>(data), static_cast<std::streamsize>(size));
if (!out)
{
out.close();
std::filesystem::remove(f.m_temp_path);
f.m_temp_path.clear();
f.m_path.clear();
}
#endif
return f;
}
private:
std::filesystem::path m_path;
#if defined(OMATH_TEST_USE_MEMFD)
int m_fd = -1;
static bool write_all(int fd, const std::uint8_t* data, std::size_t size)
{
std::size_t written = 0;
while (written < size)
{
const auto n = ::write(fd, data + written, size - written);
if (n <= 0)
return false;
written += static_cast<std::size_t>(n);
}
return true;
}
#else
std::string m_temp_path;
#endif
};
// ---------------------------------------------------------------------------
// Build a minimal PE binary in-memory with a single .text section.
// Layout (all offsets compile-time):
// 0x00: DOS header (64 B) 0x40: pad 0x80: NT sig 0x84: FileHeader (20 B)
// 0x98: OptionalHeader (0xF0 B) 0x188: SectionHeader (44 B) 0x1B4: section data
// ---------------------------------------------------------------------------
inline std::vector<std::uint8_t> build_minimal_pe(const std::vector<std::uint8_t>& section_bytes)
{
constexpr std::uint32_t e_lfanew = 0x80u;
constexpr std::uint16_t size_opt = 0xF0u;
constexpr std::size_t nt_off = e_lfanew;
constexpr std::size_t fh_off = nt_off + 4;
constexpr std::size_t oh_off = fh_off + 20;
constexpr std::size_t sh_off = oh_off + size_opt;
constexpr std::size_t data_off = sh_off + 44;
std::vector<std::uint8_t> buf(data_off + section_bytes.size(), 0u);
buf[0] = 'M'; buf[1] = 'Z';
std::memcpy(buf.data() + 0x3Cu, &e_lfanew, 4);
buf[nt_off] = 'P'; buf[nt_off + 1] = 'E';
const std::uint16_t machine = 0x8664u, num_sections = 1u;
std::memcpy(buf.data() + fh_off, &machine, 2);
std::memcpy(buf.data() + fh_off + 2, &num_sections, 2);
std::memcpy(buf.data() + fh_off + 16, &size_opt, 2);
const std::uint16_t magic = 0x20Bu;
std::memcpy(buf.data() + oh_off, &magic, 2);
const char name[8] = {'.','t','e','x','t',0,0,0};
std::memcpy(buf.data() + sh_off, name, 8);
const auto vsize = static_cast<std::uint32_t>(section_bytes.size());
const std::uint32_t vaddr = 0x1000u;
const auto ptr_raw = static_cast<std::uint32_t>(data_off);
std::memcpy(buf.data() + sh_off + 8, &vsize, 4);
std::memcpy(buf.data() + sh_off + 12, &vaddr, 4);
std::memcpy(buf.data() + sh_off + 16, &vsize, 4);
std::memcpy(buf.data() + sh_off + 20, &ptr_raw, 4);
std::memcpy(buf.data() + data_off, section_bytes.data(), section_bytes.size());
return buf;
}

View File

@@ -1,17 +1,214 @@
// //
// Created by Vladislav on 30.12.2025. // Created by Vladislav on 30.12.2025.
// //
// /Users/vladislav/Downloads/valencia #include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <omath/utility/elf_pattern_scan.hpp> #include <omath/utility/elf_pattern_scan.hpp>
#include <print> #include <span>
TEST(unit_test_elf_pattern_scan_file, ScanMissingPattern) #include <vector>
using namespace omath;
// ---- helpers ---------------------------------------------------------------
// Minimal ELF64 file with a single .text section containing known bytes.
// Layout:
// 0x000 : ELF64 file header (64 bytes)
// 0x040 : section data (padded to 0x20 bytes)
// 0x060 : section name table ".text\0" + "\0" (empty name for SHN_UNDEF)
// 0x080 : section header table (3 entries × 64 bytes = 0xC0)
static std::vector<std::byte> make_elf64_with_text_section(const std::vector<std::uint8_t>& code_bytes)
{ {
//FIXME: Implement normal tests :) // Fixed layout constants
//constexpr std::string_view path = "/Users/vladislav/Downloads/crackme"; constexpr std::size_t text_off = 0x40;
constexpr std::size_t text_size = 0x20; // always 32 bytes (code padded with zeros)
constexpr std::size_t shstrtab_off = text_off + text_size;
// ".text\0" = 6 chars, prepend \0 for SHN_UNDEF → "\0.text\0"
constexpr std::size_t shstrtab_size = 8; // "\0.text\0\0"
constexpr std::size_t shdr_table_off = shstrtab_off + shstrtab_size;
constexpr std::size_t shdr_size = 64; // sizeof(Elf64_Shdr)
constexpr std::size_t num_sections = 3; // null + .text + .shstrtab
constexpr std::size_t total_size = shdr_table_off + num_sections * shdr_size;
//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"); std::vector<std::byte> buf(total_size, std::byte{0});
//EXPECT_TRUE(res.has_value());
//std::println("In virtual mem: 0x{:x}", res->virtual_base_addr+res->target_offset); auto w8 = [&](std::size_t off, std::uint8_t v) { buf[off] = std::byte{v}; };
auto w16 = [&](std::size_t off, std::uint16_t v)
{ std::memcpy(buf.data() + off, &v, 2); };
auto w32 = [&](std::size_t off, std::uint32_t v)
{ std::memcpy(buf.data() + off, &v, 4); };
auto w64 = [&](std::size_t off, std::uint64_t v)
{ std::memcpy(buf.data() + off, &v, 8); };
// --- ELF64 file header ---
// e_ident
buf[0] = std::byte{0x7F};
buf[1] = std::byte{'E'};
buf[2] = std::byte{'L'};
buf[3] = std::byte{'F'};
w8(4, 2); // ELFCLASS64
w8(5, 1); // ELFDATA2LSB
w8(6, 1); // EV_CURRENT
// rest of e_ident is 0
w16(16, 2); // e_type = ET_EXEC
w16(18, 62); // e_machine = EM_X86_64
w32(20, 1); // e_version
w64(24, 0); // e_entry
w64(32, 0); // e_phoff
w64(40, static_cast<std::uint64_t>(shdr_table_off)); // e_shoff
w32(48, 0); // e_flags
w16(52, 64); // e_ehsize
w16(54, 56); // e_phentsize
w16(56, 0); // e_phnum
w16(58, static_cast<std::uint16_t>(shdr_size)); // e_shentsize
w16(60, static_cast<std::uint16_t>(num_sections)); // e_shnum
w16(62, 2); // e_shstrndx = 2 (.shstrtab is section index 2)
// --- section data (.text) ---
const std::size_t copy_len = std::min(code_bytes.size(), text_size);
for (std::size_t i = 0; i < copy_len; ++i)
buf[text_off + i] = std::byte{code_bytes[i]};
// --- .shstrtab data: "\0.text\0\0" ---
// index 0 → "" (SHN_UNDEF name)
// index 1 → ".text"
// index 7 → ".shstrtab" (we cheat and use index 1 for .shstrtab too, fine for test)
buf[shstrtab_off + 0] = std::byte{0};
buf[shstrtab_off + 1] = std::byte{'.'};
buf[shstrtab_off + 2] = std::byte{'t'};
buf[shstrtab_off + 3] = std::byte{'e'};
buf[shstrtab_off + 4] = std::byte{'x'};
buf[shstrtab_off + 5] = std::byte{'t'};
buf[shstrtab_off + 6] = std::byte{0};
buf[shstrtab_off + 7] = std::byte{0};
// --- section headers ---
// Elf64_Shdr fields (all offsets relative to start of a section header):
// 0 sh_name (4)
// 4 sh_type (4)
// 8 sh_flags (8)
// 16 sh_addr (8)
// 24 sh_offset (8)
// 32 sh_size (8)
// 40 sh_link (4)
// 44 sh_info (4)
// 48 sh_addralign(8)
// 56 sh_entsize (8)
// Section 0: null
// (all zeros already zeroed)
// Section 1: .text
{
const std::size_t base = shdr_table_off + 1 * shdr_size;
w32(base + 0, 1); // sh_name → index 1 in shstrtab → ".text"
w32(base + 4, 1); // sh_type = SHT_PROGBITS
w64(base + 8, 6); // sh_flags = SHF_ALLOC|SHF_EXECINSTR
w64(base + 16, static_cast<std::uint64_t>(text_off)); // sh_addr (same as offset in test)
w64(base + 24, static_cast<std::uint64_t>(text_off)); // sh_offset
w64(base + 32, static_cast<std::uint64_t>(text_size)); // sh_size
w64(base + 48, 16); // sh_addralign
}
// Section 2: .shstrtab
{
const std::size_t base = shdr_table_off + 2 * shdr_size;
w32(base + 0, 0); // sh_name → index 0 → "" (good enough for scanner)
w32(base + 4, 3); // sh_type = SHT_STRTAB
w64(base + 24, static_cast<std::uint64_t>(shstrtab_off)); // sh_offset
w64(base + 32, static_cast<std::uint64_t>(shstrtab_size)); // sh_size
}
return buf;
}
// ---- tests -----------------------------------------------------------------
TEST(unit_test_elf_pattern_scan_memory, finds_pattern)
{
const std::vector<std::uint8_t> code = {0x55, 0x48, 0x89, 0xE5, 0xC3};
const auto buf = make_elf64_with_text_section(code);
const auto span = std::span<const std::byte>{buf};
const auto result = ElfPatternScanner::scan_for_pattern_in_memory_file(span, "55 48 89 E5", ".text");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->target_offset, 0);
}
TEST(unit_test_elf_pattern_scan_memory, finds_pattern_with_wildcard)
{
const std::vector<std::uint8_t> code = {0xDE, 0xAD, 0xBE, 0xEF, 0x00};
const auto buf = make_elf64_with_text_section(code);
const auto result =
ElfPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "DE ?? BE EF", ".text");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->target_offset, 0);
}
TEST(unit_test_elf_pattern_scan_memory, pattern_not_found_returns_nullopt)
{
const std::vector<std::uint8_t> code = {0x01, 0x02, 0x03, 0x04};
const auto buf = make_elf64_with_text_section(code);
const auto result =
ElfPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "AA BB CC", ".text");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_elf_pattern_scan_memory, invalid_data_returns_nullopt)
{
const std::vector<std::byte> garbage(64, std::byte{0xFF});
const auto result =
ElfPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{garbage}, "FF FF", ".text");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_elf_pattern_scan_memory, empty_data_returns_nullopt)
{
const auto result = ElfPatternScanner::scan_for_pattern_in_memory_file({}, "FF", ".text");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_elf_pattern_scan_memory, missing_section_returns_nullopt)
{
const std::vector<std::uint8_t> code = {0x90, 0x90};
const auto buf = make_elf64_with_text_section(code);
const auto result = ElfPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf},
"90 90", ".nonexistent");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_elf_pattern_scan_memory, matches_file_scan)
{
// Write our synthetic ELF to a temp file and verify memory scan == file scan
const std::vector<std::uint8_t> code = {0x48, 0x89, 0xE5, 0xDE, 0xAD, 0xBE, 0xEF, 0x00};
const auto buf = make_elf64_with_text_section(code);
const auto tmp_path = std::filesystem::temp_directory_path() / "omath_elf_test.elf";
{
std::ofstream out(tmp_path, std::ios::binary);
out.write(reinterpret_cast<const char*>(buf.data()), static_cast<std::streamsize>(buf.size()));
}
const auto file_result = ElfPatternScanner::scan_for_pattern_in_file(tmp_path, "48 89 E5 DE AD", ".text");
const auto mem_result =
ElfPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "48 89 E5 DE AD", ".text");
std::filesystem::remove(tmp_path);
ASSERT_TRUE(file_result.has_value());
ASSERT_TRUE(mem_result.has_value());
EXPECT_EQ(file_result->virtual_base_addr, mem_result->virtual_base_addr);
EXPECT_EQ(file_result->raw_base_addr, mem_result->raw_base_addr);
EXPECT_EQ(file_result->target_offset, mem_result->target_offset);
} }

View File

@@ -0,0 +1,145 @@
// Tests for MachOPatternScanner::scan_for_pattern_in_memory_file
#include <cstring>
#include <gtest/gtest.h>
#include <omath/utility/macho_pattern_scan.hpp>
#include <span>
#include <vector>
using namespace omath;
// Build a minimal Mach-O 64-bit file in memory with a single __text section.
// Layout:
// 0x000 : MachHeader64 (32 bytes)
// 0x020 : SegmentCommand64 (72 bytes)
// 0x068 : Section64 (80 bytes) ← follows segment command inline
// 0x0B8 : section raw data (padded to 0x20 bytes)
static std::vector<std::byte> make_macho64_with_text_section(const std::vector<std::uint8_t>& code_bytes)
{
constexpr std::uint32_t mh_magic_64 = 0xFEEDFACF;
constexpr std::uint32_t lc_segment_64 = 0x19;
// MachHeader64 layout (32 bytes):
// 0 magic, 4 cputype, 8 cpusubtype, 12 filetype, 16 ncmds, 20 sizeofcmds, 24 flags, 28 reserved
constexpr std::size_t hdr_size = 32;
// SegmentCommand64 layout (72 bytes):
// 0 cmd, 4 cmdsize, 8 segname[16], 24 vmaddr, 32 vmsize, 40 fileoff, 48 filesize,
// 56 maxprot, 60 initprot, 64 nsects, 68 flags
constexpr std::size_t seg_size = 72;
// Section64 layout (80 bytes):
// 0 sectname[16], 16 segname[16], 32 addr, 40 size, 48 offset, 52 align,
// 56 reloff, 60 nreloc, 64 flags, 68 reserved1, 72 reserved2, 76 reserved3
constexpr std::size_t sect_hdr_size = 80;
constexpr std::size_t text_raw_off = hdr_size + seg_size + sect_hdr_size; // 0xB8
constexpr std::size_t text_raw_size = 0x20;
constexpr std::size_t total_size = text_raw_off + text_raw_size;
constexpr std::uint64_t text_vmaddr = 0x1000ULL;
constexpr std::uint32_t cmd_size =
static_cast<std::uint32_t>(seg_size + sect_hdr_size); // segment + 1 section
std::vector<std::byte> buf(total_size, std::byte{0});
auto w32 = [&](std::size_t off, std::uint32_t v) { std::memcpy(buf.data() + off, &v, 4); };
auto w64 = [&](std::size_t off, std::uint64_t v) { std::memcpy(buf.data() + off, &v, 8); };
// MachHeader64
w32(0, mh_magic_64);
w32(4, 0x0100000C); // cputype = CPU_TYPE_ARM64 (doesn't matter for scan)
w32(12, 2); // filetype = MH_EXECUTE
w32(16, 1); // ncmds = 1
w32(20, cmd_size); // sizeofcmds
// SegmentCommand64 at 0x20
constexpr std::size_t seg_off = hdr_size;
w32(seg_off + 0, lc_segment_64);
w32(seg_off + 4, cmd_size);
std::memcpy(buf.data() + seg_off + 8, "__TEXT", 6); // segname
w64(seg_off + 24, text_vmaddr); // vmaddr
w64(seg_off + 32, text_raw_size); // vmsize
w64(seg_off + 40, text_raw_off); // fileoff
w64(seg_off + 48, text_raw_size); // filesize
w32(seg_off + 64, 1); // nsects
// Section64 at 0x68
constexpr std::size_t sect_off = seg_off + seg_size;
std::memcpy(buf.data() + sect_off + 0, "__text", 6); // sectname
std::memcpy(buf.data() + sect_off + 16, "__TEXT", 6); // segname
w64(sect_off + 32, text_vmaddr); // addr
w64(sect_off + 40, text_raw_size); // size
w32(sect_off + 48, static_cast<std::uint32_t>(text_raw_off)); // offset (file offset)
// Section data
const std::size_t copy_len = std::min(code_bytes.size(), text_raw_size);
for (std::size_t i = 0; i < copy_len; ++i)
buf[text_raw_off + i] = std::byte{code_bytes[i]};
return buf;
}
// ---- tests -----------------------------------------------------------------
TEST(unit_test_macho_memory_file_scan, finds_pattern)
{
const std::vector<std::uint8_t> code = {0x55, 0x48, 0x89, 0xE5, 0xC3};
const auto buf = make_macho64_with_text_section(code);
const auto result =
MachOPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "55 48 89 E5");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->target_offset, 0);
}
TEST(unit_test_macho_memory_file_scan, finds_pattern_with_wildcard)
{
const std::vector<std::uint8_t> code = {0xDE, 0xAD, 0xBE, 0xEF};
const auto buf = make_macho64_with_text_section(code);
const auto result =
MachOPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "DE ?? BE EF");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->target_offset, 0);
}
TEST(unit_test_macho_memory_file_scan, pattern_not_found_returns_nullopt)
{
const std::vector<std::uint8_t> code = {0x01, 0x02, 0x03};
const auto buf = make_macho64_with_text_section(code);
const auto result =
MachOPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "AA BB CC");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_macho_memory_file_scan, invalid_data_returns_nullopt)
{
const std::vector<std::byte> garbage(64, std::byte{0xFF});
const auto result =
MachOPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{garbage}, "FF FF");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_macho_memory_file_scan, empty_data_returns_nullopt)
{
const auto result = MachOPatternScanner::scan_for_pattern_in_memory_file({}, "FF");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_macho_memory_file_scan, raw_addr_and_virtual_addr_correct)
{
const std::vector<std::uint8_t> code = {0xCA, 0xFE, 0xBA, 0xBE};
const auto buf = make_macho64_with_text_section(code);
constexpr std::size_t expected_raw_off = 32 + 72 + 80; // hdr + seg + sect_hdr
const auto result =
MachOPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "CA FE BA BE");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->raw_base_addr, expected_raw_off);
EXPECT_EQ(result->virtual_base_addr, 0x1000u);
}

View File

@@ -6,8 +6,8 @@
#include <omath/utility/macho_pattern_scan.hpp> #include <omath/utility/macho_pattern_scan.hpp>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <fstream>
#include <vector> #include <vector>
#include "mem_fd_helper.hpp"
using namespace omath; using namespace omath;
@@ -16,11 +16,12 @@ namespace
// Mach-O magic numbers // Mach-O magic numbers
constexpr std::uint32_t mh_magic_64 = 0xFEEDFACF; constexpr std::uint32_t mh_magic_64 = 0xFEEDFACF;
constexpr std::uint32_t mh_magic_32 = 0xFEEDFACE; constexpr std::uint32_t mh_magic_32 = 0xFEEDFACE;
constexpr std::uint32_t lc_segment = 0x1; constexpr std::uint32_t lc_segment = 0x1;
constexpr std::uint32_t lc_segment_64 = 0x19; constexpr std::uint32_t lc_segment_64 = 0x19;
constexpr std::string_view segment_name = "__TEXT"; constexpr std::string_view segment_name = "__TEXT";
constexpr std::string_view section_name = "__text"; constexpr std::string_view section_name = "__text";
#pragma pack(push, 1) #pragma pack(push, 1)
struct MachHeader64 struct MachHeader64
{ {
@@ -107,249 +108,174 @@ namespace
}; };
#pragma pack(pop) #pragma pack(pop)
// Helper function to create a minimal 64-bit Mach-O file with a __text section // Build a minimal 64-bit Mach-O binary in-memory with a __text section
bool write_minimal_macho64_file(const std::string& path, const std::vector<std::uint8_t>& section_bytes) std::vector<std::uint8_t> build_minimal_macho64(const std::vector<std::uint8_t>& section_bytes)
{ {
std::ofstream f(path, std::ios::binary); constexpr std::size_t load_cmd_size = sizeof(SegmentCommand64) + sizeof(Section64);
if (!f.is_open()) const std::size_t section_offset = sizeof(MachHeader64) + load_cmd_size;
return false;
// Calculate sizes std::vector<std::uint8_t> buf(section_offset + section_bytes.size(), 0u);
constexpr std::size_t header_size = sizeof(MachHeader64);
constexpr std::size_t segment_size = sizeof(SegmentCommand64);
constexpr std::size_t section_size = sizeof(Section64);
constexpr std::size_t load_cmd_size = segment_size + section_size;
// Section data will start after headers
const std::size_t section_offset = header_size + load_cmd_size;
// Create Mach-O header auto* header = reinterpret_cast<MachHeader64*>(buf.data());
MachHeader64 header{}; header->magic = mh_magic_64;
header.magic = mh_magic_64; header->cputype = 0x01000007; // CPU_TYPE_X86_64
header.cputype = 0x01000007; // CPU_TYPE_X86_64 header->cpusubtype = 0x3;
header.cpusubtype = 0x3; // CPU_SUBTYPE_X86_64_ALL header->filetype = 0x2; // MH_EXECUTE
header.filetype = 0x2; // MH_EXECUTE header->ncmds = 1;
header.ncmds = 1; header->sizeofcmds = static_cast<std::uint32_t>(load_cmd_size);
header.sizeofcmds = static_cast<std::uint32_t>(load_cmd_size);
header.flags = 0;
header.reserved = 0;
f.write(reinterpret_cast<const char*>(&header), sizeof(header)); auto* segment = reinterpret_cast<SegmentCommand64*>(buf.data() + sizeof(MachHeader64));
segment->cmd = lc_segment_64;
segment->cmdsize = static_cast<std::uint32_t>(load_cmd_size);
std::ranges::copy(segment_name, segment->segname);
segment->vmaddr = 0x100000000;
segment->vmsize = section_bytes.size();
segment->fileoff = section_offset;
segment->filesize = section_bytes.size();
segment->maxprot = 7;
segment->initprot = 5;
segment->nsects = 1;
// Create segment command auto* section = reinterpret_cast<Section64*>(buf.data() + sizeof(MachHeader64) + sizeof(SegmentCommand64));
SegmentCommand64 segment{}; std::ranges::copy(section_name, section->sectname);
segment.cmd = lc_segment_64; std::ranges::copy(segment_name, section->segname);
segment.cmdsize = static_cast<std::uint32_t>(load_cmd_size); section->addr = 0x100000000;
std::ranges::copy(segment_name, segment.segname); section->size = section_bytes.size();
segment.vmaddr = 0x100000000; section->offset = static_cast<std::uint32_t>(section_offset);
segment.vmsize = section_bytes.size();
segment.fileoff = section_offset;
segment.filesize = section_bytes.size();
segment.maxprot = 7; // VM_PROT_ALL
segment.initprot = 5; // VM_PROT_READ | VM_PROT_EXECUTE
segment.nsects = 1;
segment.flags = 0;
f.write(reinterpret_cast<const char*>(&segment), sizeof(segment)); std::memcpy(buf.data() + section_offset, section_bytes.data(), section_bytes.size());
return buf;
// Create section
Section64 section{};
std::ranges::copy(section_name, section.sectname);
std::ranges::copy(segment_name, segment.segname);
section.addr = 0x100000000;
section.size = section_bytes.size();
section.offset = static_cast<std::uint32_t>(section_offset);
section.align = 0;
section.reloff = 0;
section.nreloc = 0;
section.flags = 0;
section.reserved1 = 0;
section.reserved2 = 0;
section.reserved3 = 0;
f.write(reinterpret_cast<const char*>(&section), sizeof(section));
// Write section data
f.write(reinterpret_cast<const char*>(section_bytes.data()), static_cast<std::streamsize>(section_bytes.size()));
f.close();
return true;
} }
// Helper function to create a minimal 32-bit Mach-O file with a __text section // Build a minimal 32-bit Mach-O binary in-memory with a __text section
bool write_minimal_macho32_file(const std::string& path, const std::vector<std::uint8_t>& section_bytes) std::vector<std::uint8_t> build_minimal_macho32(const std::vector<std::uint8_t>& section_bytes)
{ {
std::ofstream f(path, std::ios::binary); constexpr std::size_t load_cmd_size = sizeof(SegmentCommand32) + sizeof(Section32);
if (!f.is_open()) constexpr std::size_t section_offset = sizeof(MachHeader32) + load_cmd_size;
return false;
// Calculate sizes std::vector<std::uint8_t> buf(section_offset + section_bytes.size(), 0u);
constexpr std::size_t header_size = sizeof(MachHeader32);
constexpr std::size_t segment_size = sizeof(SegmentCommand32);
constexpr std::size_t section_size = sizeof(Section32);
constexpr std::size_t load_cmd_size = segment_size + section_size;
// Section data will start after headers auto* header = reinterpret_cast<MachHeader32*>(buf.data());
constexpr std::size_t section_offset = header_size + load_cmd_size; header->magic = mh_magic_32;
header->cputype = 0x7;
header->cpusubtype = 0x3;
header->filetype = 0x2;
header->ncmds = 1;
header->sizeofcmds = static_cast<std::uint32_t>(load_cmd_size);
// Create Mach-O header auto* segment = reinterpret_cast<SegmentCommand32*>(buf.data() + sizeof(MachHeader32));
MachHeader32 header{}; segment->cmd = lc_segment;
header.magic = mh_magic_32; segment->cmdsize = static_cast<std::uint32_t>(load_cmd_size);
header.cputype = 0x7; // CPU_TYPE_X86 std::ranges::copy(segment_name, segment->segname);
header.cpusubtype = 0x3; // CPU_SUBTYPE_X86_ALL segment->vmaddr = 0x1000;
header.filetype = 0x2; // MH_EXECUTE segment->vmsize = static_cast<std::uint32_t>(section_bytes.size());
header.ncmds = 1; segment->fileoff = static_cast<std::uint32_t>(section_offset);
header.sizeofcmds = static_cast<std::uint32_t>(load_cmd_size); segment->filesize = static_cast<std::uint32_t>(section_bytes.size());
header.flags = 0; segment->maxprot = 7;
segment->initprot = 5;
segment->nsects = 1;
f.write(reinterpret_cast<const char*>(&header), sizeof(header)); auto* section = reinterpret_cast<Section32*>(buf.data() + sizeof(MachHeader32) + sizeof(SegmentCommand32));
std::ranges::copy(section_name, section->sectname);
std::ranges::copy(segment_name, section->segname);
section->addr = 0x1000;
section->size = static_cast<std::uint32_t>(section_bytes.size());
section->offset = static_cast<std::uint32_t>(section_offset);
// Create segment command std::memcpy(buf.data() + section_offset, section_bytes.data(), section_bytes.size());
SegmentCommand32 segment{}; return buf;
segment.cmd = lc_segment;
segment.cmdsize = static_cast<std::uint32_t>(load_cmd_size);
std::ranges::copy(segment_name, segment.segname);
segment.vmaddr = 0x1000;
segment.vmsize = static_cast<std::uint32_t>(section_bytes.size());
segment.fileoff = static_cast<std::uint32_t>(section_offset);
segment.filesize = static_cast<std::uint32_t>(section_bytes.size());
segment.maxprot = 7; // VM_PROT_ALL
segment.initprot = 5; // VM_PROT_READ | VM_PROT_EXECUTE
segment.nsects = 1;
segment.flags = 0;
f.write(reinterpret_cast<const char*>(&segment), sizeof(segment));
// Create section
Section32 section{};
std::ranges::copy(section_name, section.sectname);
std::ranges::copy(segment_name, segment.segname);
section.addr = 0x1000;
section.size = static_cast<std::uint32_t>(section_bytes.size());
section.offset = static_cast<std::uint32_t>(section_offset);
section.align = 0;
section.reloff = 0;
section.nreloc = 0;
section.flags = 0;
section.reserved1 = 0;
section.reserved2 = 0;
f.write(reinterpret_cast<const char*>(&section), sizeof(section));
// Write section data
f.write(reinterpret_cast<const char*>(section_bytes.data()), static_cast<std::streamsize>(section_bytes.size()));
f.close();
return true;
} }
} // namespace } // namespace
// Test scanning for a pattern that exists in a 64-bit Mach-O file
TEST(unit_test_macho_pattern_scan_file, ScanFindsPattern64) TEST(unit_test_macho_pattern_scan_file, ScanFindsPattern64)
{ {
constexpr std::string_view path = "./test_minimal_macho64.bin"; const std::vector<std::uint8_t> bytes = {0x55, 0x48, 0x89, 0xE5, 0x90, 0x90};
const std::vector<std::uint8_t> bytes = {0x55, 0x48, 0x89, 0xE5, 0x90, 0x90}; // push rbp; mov rbp, rsp; nop; nop const auto f = MemFdFile::create(build_minimal_macho64(bytes));
ASSERT_TRUE(write_minimal_macho64_file(path.data(), bytes)); ASSERT_TRUE(f.valid());
const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 48 89 E5", "__text"); const auto res = MachOPatternScanner::scan_for_pattern_in_file(f.path(), "55 48 89 E5", "__text");
EXPECT_TRUE(res.has_value()); EXPECT_TRUE(res.has_value());
if (res.has_value()) if (res.has_value())
{
EXPECT_EQ(res->target_offset, 0); EXPECT_EQ(res->target_offset, 0);
}
} }
// Test scanning for a pattern that exists in a 32-bit Mach-O file
TEST(unit_test_macho_pattern_scan_file, ScanFindsPattern32) TEST(unit_test_macho_pattern_scan_file, ScanFindsPattern32)
{ {
constexpr std::string_view path = "./test_minimal_macho32.bin"; const std::vector<std::uint8_t> bytes = {0x55, 0x89, 0xE5, 0x90, 0x90};
const std::vector<std::uint8_t> bytes = {0x55, 0x89, 0xE5, 0x90, 0x90}; // push ebp; mov ebp, esp; nop; nop const auto f = MemFdFile::create(build_minimal_macho32(bytes));
ASSERT_TRUE(write_minimal_macho32_file(path.data(), bytes)); ASSERT_TRUE(f.valid());
const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 89 E5", "__text"); const auto res = MachOPatternScanner::scan_for_pattern_in_file(f.path(), "55 89 E5", "__text");
EXPECT_TRUE(res.has_value()); EXPECT_TRUE(res.has_value());
if (res.has_value()) if (res.has_value())
{
EXPECT_EQ(res->target_offset, 0); EXPECT_EQ(res->target_offset, 0);
}
} }
// Test scanning for a pattern that does not exist
TEST(unit_test_macho_pattern_scan_file, ScanMissingPattern) TEST(unit_test_macho_pattern_scan_file, ScanMissingPattern)
{ {
constexpr std::string_view path = "./test_minimal_macho_missing.bin";
const std::vector<std::uint8_t> bytes = {0x00, 0x01, 0x02, 0x03}; const std::vector<std::uint8_t> bytes = {0x00, 0x01, 0x02, 0x03};
ASSERT_TRUE(write_minimal_macho64_file(path.data(), bytes)); const auto f = MemFdFile::create(build_minimal_macho64(bytes));
ASSERT_TRUE(f.valid());
const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "FF EE DD", "__text"); const auto res = MachOPatternScanner::scan_for_pattern_in_file(f.path(), "FF EE DD", "__text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }
// Test scanning for a pattern at a non-zero offset
TEST(unit_test_macho_pattern_scan_file, ScanPatternAtOffset) TEST(unit_test_macho_pattern_scan_file, ScanPatternAtOffset)
{ {
constexpr std::string_view path = "./test_minimal_macho_offset.bin"; const std::vector<std::uint8_t> bytes = {0x90, 0x90, 0x90, 0x55, 0x48, 0x89, 0xE5};
const std::vector<std::uint8_t> bytes = {0x90, 0x90, 0x90, 0x55, 0x48, 0x89, 0xE5}; // nops then pattern const auto f = MemFdFile::create(build_minimal_macho64(bytes));
ASSERT_TRUE(write_minimal_macho64_file(path.data(), bytes)); ASSERT_TRUE(f.valid());
const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 48 89 E5", "__text"); const auto res = MachOPatternScanner::scan_for_pattern_in_file(f.path(), "55 48 89 E5", "__text");
EXPECT_TRUE(res.has_value()); EXPECT_TRUE(res.has_value());
if (res.has_value()) if (res.has_value())
{
EXPECT_EQ(res->target_offset, 3); EXPECT_EQ(res->target_offset, 3);
}
} }
// Test scanning with wildcards
TEST(unit_test_macho_pattern_scan_file, ScanWithWildcard) TEST(unit_test_macho_pattern_scan_file, ScanWithWildcard)
{ {
constexpr std::string_view path = "./test_minimal_macho_wildcard.bin";
const std::vector<std::uint8_t> bytes = {0x55, 0x48, 0x89, 0xE5, 0x90}; const std::vector<std::uint8_t> bytes = {0x55, 0x48, 0x89, 0xE5, 0x90};
ASSERT_TRUE(write_minimal_macho64_file(path.data(), bytes)); const auto f = MemFdFile::create(build_minimal_macho64(bytes));
ASSERT_TRUE(f.valid());
const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 ? 89 E5", "__text"); const auto res = MachOPatternScanner::scan_for_pattern_in_file(f.path(), "55 ? 89 E5", "__text");
EXPECT_TRUE(res.has_value()); EXPECT_TRUE(res.has_value());
} }
// Test scanning a non-existent file
TEST(unit_test_macho_pattern_scan_file, ScanNonExistentFile) TEST(unit_test_macho_pattern_scan_file, ScanNonExistentFile)
{ {
const auto res = MachOPatternScanner::scan_for_pattern_in_file("/non/existent/file.bin", "55 48", "__text"); const auto res = MachOPatternScanner::scan_for_pattern_in_file("/non/existent/file.bin", "55 48", "__text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }
// Test scanning an invalid (non-Mach-O) file
TEST(unit_test_macho_pattern_scan_file, ScanInvalidFile) TEST(unit_test_macho_pattern_scan_file, ScanInvalidFile)
{ {
constexpr std::string_view path = "./test_invalid_macho.bin";
std::ofstream f(path.data(), std::ios::binary);
const std::vector<std::uint8_t> garbage = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; const std::vector<std::uint8_t> garbage = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05};
f.write(reinterpret_cast<const char*>(garbage.data()), static_cast<std::streamsize>(garbage.size())); const auto f = MemFdFile::create(garbage);
f.close(); ASSERT_TRUE(f.valid());
const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 48", "__text"); const auto res = MachOPatternScanner::scan_for_pattern_in_file(f.path(), "55 48", "__text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }
// Test scanning for a non-existent section
TEST(unit_test_macho_pattern_scan_file, ScanNonExistentSection) TEST(unit_test_macho_pattern_scan_file, ScanNonExistentSection)
{ {
constexpr std::string_view path = "./test_minimal_macho_nosect.bin";
const std::vector<std::uint8_t> bytes = {0x55, 0x48, 0x89, 0xE5}; const std::vector<std::uint8_t> bytes = {0x55, 0x48, 0x89, 0xE5};
ASSERT_TRUE(write_minimal_macho64_file(path.data(), bytes)); const auto f = MemFdFile::create(build_minimal_macho64(bytes));
ASSERT_TRUE(f.valid());
const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 48", "__nonexistent"); const auto res = MachOPatternScanner::scan_for_pattern_in_file(f.path(), "55 48", "__nonexistent");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }
// Test scanning with null module base address
TEST(unit_test_macho_pattern_scan_loaded, ScanNullModule) TEST(unit_test_macho_pattern_scan_loaded, ScanNullModule)
{ {
const auto res = MachOPatternScanner::scan_for_pattern_in_loaded_module(nullptr, "55 48", "__text"); const auto res = MachOPatternScanner::scan_for_pattern_in_loaded_module(nullptr, "55 48", "__text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }
// Test scanning in loaded module with invalid magic
TEST(unit_test_macho_pattern_scan_loaded, ScanInvalidMagic) TEST(unit_test_macho_pattern_scan_loaded, ScanInvalidMagic)
{ {
std::vector<std::uint8_t> invalid_data(256, 0x00); std::vector<std::uint8_t> invalid_data(256, 0x00);

View File

@@ -0,0 +1,128 @@
// Tests for PePatternScanner::scan_for_pattern_in_memory_file
#include <cstring>
#include <gtest/gtest.h>
#include <omath/utility/pe_pattern_scan.hpp>
#include <span>
#include <vector>
using namespace omath;
// Reuse the fake-module builder from unit_test_pe_pattern_scan_loaded.cpp but
// lay out the buffer as a raw PE *file* (ptr_raw_data != virtual_address).
static std::vector<std::byte> make_fake_pe_file(std::uint32_t virtual_address, std::uint32_t ptr_raw_data,
std::uint32_t section_size,
const std::vector<std::uint8_t>& code_bytes)
{
constexpr std::uint32_t e_lfanew = 0x80;
constexpr std::uint32_t nt_sig = 0x4550;
constexpr std::uint16_t opt_magic = 0x020B; // PE32+
constexpr std::uint16_t num_sections = 1;
constexpr std::uint16_t opt_hdr_size = 0xF0;
constexpr std::uint32_t section_table_off = e_lfanew + 4 + 20 + opt_hdr_size;
constexpr std::uint32_t section_header_size = 40;
const std::uint32_t total_size = ptr_raw_data + section_size + 0x100;
std::vector<std::byte> buf(total_size, std::byte{0});
auto w16 = [&](std::size_t off, std::uint16_t v) { std::memcpy(buf.data() + off, &v, 2); };
auto w32 = [&](std::size_t off, std::uint32_t v) { std::memcpy(buf.data() + off, &v, 4); };
auto w64 = [&](std::size_t off, std::uint64_t v) { std::memcpy(buf.data() + off, &v, 8); };
// DOS header
w16(0x00, 0x5A4D);
w32(0x3C, e_lfanew);
// NT signature
w32(e_lfanew, nt_sig);
// FileHeader
const std::size_t fh_off = e_lfanew + 4;
w16(fh_off + 2, num_sections);
w16(fh_off + 16, opt_hdr_size);
// OptionalHeader PE32+
const std::size_t opt_off = fh_off + 20;
w16(opt_off + 0, opt_magic);
w64(opt_off + 24, 0ULL); // ImageBase = 0 to keep virtual_base_addr in 32-bit range
// Section header (.text)
const std::size_t sh_off = section_table_off;
std::memcpy(buf.data() + sh_off, ".text", 5);
w32(sh_off + 8, section_size); // VirtualSize
w32(sh_off + 12, virtual_address); // VirtualAddress
w32(sh_off + 16, section_size); // SizeOfRawData
w32(sh_off + 20, ptr_raw_data); // PointerToRawData
// Place code at raw file offset
const std::size_t copy_len = std::min(code_bytes.size(), static_cast<std::size_t>(section_size));
for (std::size_t i = 0; i < copy_len; ++i)
buf[ptr_raw_data + i] = std::byte{code_bytes[i]};
return buf;
}
// ---- tests -----------------------------------------------------------------
TEST(unit_test_pe_memory_file_scan, finds_pattern)
{
const std::vector<std::uint8_t> code = {0x90, 0x01, 0x02, 0x03, 0x04};
const auto buf = make_fake_pe_file(0x1000, 0x400, static_cast<std::uint32_t>(code.size()), code);
const auto result = PePatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "90 01 02");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->target_offset, 0);
EXPECT_EQ(result->raw_base_addr, 0x400u);
}
TEST(unit_test_pe_memory_file_scan, finds_pattern_with_wildcard)
{
const std::vector<std::uint8_t> code = {0xDE, 0xAD, 0xBE, 0xEF};
const auto buf = make_fake_pe_file(0x2000, 0x600, static_cast<std::uint32_t>(code.size()), code);
const auto result =
PePatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "DE ?? BE EF");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->target_offset, 0);
}
TEST(unit_test_pe_memory_file_scan, pattern_not_found_returns_nullopt)
{
const std::vector<std::uint8_t> code = {0x01, 0x02, 0x03};
const auto buf = make_fake_pe_file(0x1000, 0x400, static_cast<std::uint32_t>(code.size()), code);
const auto result =
PePatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "AA BB CC");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_pe_memory_file_scan, invalid_data_returns_nullopt)
{
const std::vector<std::byte> garbage(128, std::byte{0xFF});
const auto result = PePatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{garbage}, "FF FF");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_pe_memory_file_scan, empty_data_returns_nullopt)
{
const auto result = PePatternScanner::scan_for_pattern_in_memory_file({}, "FF");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_pe_memory_file_scan, raw_addr_differs_from_virtual_address)
{
// ptr_raw_data = 0x600, virtual_address = 0x3000 — different intentionally
const std::vector<std::uint8_t> code = {0xCA, 0xFE, 0xBA, 0xBE};
const auto buf = make_fake_pe_file(0x3000, 0x600, static_cast<std::uint32_t>(code.size()), code);
const auto result =
PePatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "CA FE BA BE");
ASSERT_TRUE(result.has_value());
// raw_base_addr should be ptr_raw_data, not virtual_address
EXPECT_EQ(result->raw_base_addr, 0x600u);
// virtual_base_addr = virtual_address + image_base (image_base = 0)
EXPECT_EQ(result->virtual_base_addr, 0x3000u);
}

View File

@@ -1,114 +1,28 @@
// Unit test for PePatternScanner::scan_for_pattern_in_file using a synthetic PE-like file // Unit test for PePatternScanner::scan_for_pattern_in_file using a synthetic PE-like file
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <omath/utility/pe_pattern_scan.hpp> #include <omath/utility/pe_pattern_scan.hpp>
#include <fstream>
#include <vector>
#include <cstdint> #include <cstdint>
#include <cstring> #include <vector>
#include "mem_fd_helper.hpp"
using namespace omath; using namespace omath;
// Helper: write a trivial PE-like file with DOS header and a single section named .text
static bool write_minimal_pe_file(const std::string& path, const std::vector<std::uint8_t>& section_bytes)
{
std::ofstream f(path, std::ios::binary);
if (!f.is_open()) return false;
// Write DOS header (e_magic = 0x5A4D, e_lfanew at offset 0x3C)
std::vector<std::uint8_t> dos(64, 0);
dos[0] = 'M'; dos[1] = 'Z';
// e_lfanew -> place NT headers right after DOS (offset 0x80)
std::uint32_t e_lfanew = 0x80;
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
f.write(reinterpret_cast<const char*>(dos.data()), dos.size());
// Pad up to e_lfanew
if (f.tellp() < static_cast<std::streampos>(e_lfanew))
{
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
f.write(pad.data(), pad.size());
}
// NT headers signature 'PE\0\0'
f.put('P'); f.put('E'); f.put('\0'); f.put('\0');
// FileHeader: machine, num_sections
std::uint16_t machine = 0x8664; // x64
std::uint16_t num_sections = 1;
std::uint32_t dummy32 = 0;
std::uint32_t dummy32b = 0;
std::uint16_t size_optional = 0xF0; // reasonable
std::uint16_t characteristics = 0;
f.write(reinterpret_cast<const char*>(&machine), sizeof(machine));
f.write(reinterpret_cast<const char*>(&num_sections), sizeof(num_sections));
f.write(reinterpret_cast<const char*>(&dummy32), sizeof(dummy32));
f.write(reinterpret_cast<const char*>(&dummy32b), sizeof(dummy32b));
std::uint32_t num_symbols = 0;
f.write(reinterpret_cast<const char*>(&num_symbols), sizeof(num_symbols));
f.write(reinterpret_cast<const char*>(&size_optional), sizeof(size_optional));
f.write(reinterpret_cast<const char*>(&characteristics), sizeof(characteristics));
// OptionalHeader (x64) minimal: magic 0x20b, image_base, size_of_code, size_of_headers
std::uint16_t magic = 0x20b;
f.write(reinterpret_cast<const char*>(&magic), sizeof(magic));
// filler for rest of optional header up to size_optional
std::vector<std::uint8_t> opt(size_optional - sizeof(magic), 0);
// set size_code near end
// we'll set image_base and size_code fields in reasonable positions for extractor
// For simplicity, leave zeros; extractor primarily uses optional_header.image_base and size_code later,
// but we will craft a SectionHeader that points to raw data we append below.
f.write(reinterpret_cast<const char*>(opt.data()), opt.size());
// Section header (name 8 bytes, then remaining 36 bytes)
char name[8] = {'.','t','e','x','t',0,0,0};
f.write(name, 8);
// Write placeholder bytes for the rest of the section header and remember its start
constexpr std::uint32_t section_header_rest = 36u;
const std::streampos header_rest_pos = f.tellp();
std::vector<char> placeholder(section_header_rest, 0);
f.write(placeholder.data(), placeholder.size());
// Now write section raw data and remember its file offset
const std::streampos data_pos = f.tellp();
f.write(reinterpret_cast<const char*>(section_bytes.data()), static_cast<std::streamsize>(section_bytes.size()));
// Patch section header fields: virtual_size, virtual_address, size_raw_data, ptr_raw_data
const std::uint32_t virtual_size = static_cast<std::uint32_t>(section_bytes.size());
constexpr std::uint32_t virtual_address = 0x1000u;
const std::uint32_t size_raw_data = static_cast<std::uint32_t>(section_bytes.size());
const std::uint32_t ptr_raw_data = static_cast<std::uint32_t>(data_pos);
// Seek back to the header_rest_pos and write fields in order
f.seekp(header_rest_pos, std::ios::beg);
f.write(reinterpret_cast<const char*>(&virtual_size), sizeof(virtual_size));
f.write(reinterpret_cast<const char*>(&virtual_address), sizeof(virtual_address));
f.write(reinterpret_cast<const char*>(&size_raw_data), sizeof(size_raw_data));
f.write(reinterpret_cast<const char*>(&ptr_raw_data), sizeof(ptr_raw_data));
// Seek back to end for consistency
f.seekp(0, std::ios::end);
f.close();
return true;
}
TEST(unit_test_pe_pattern_scan_file, ScanFindsPattern) TEST(unit_test_pe_pattern_scan_file, ScanFindsPattern)
{ {
constexpr std::string_view path = "./test_minimal_pe.bin"; const std::vector<std::uint8_t> bytes = {0x55, 0x8B, 0xEC, 0x90, 0x90};
const std::vector<std::uint8_t> bytes = {0x55, 0x8B, 0xEC, 0x90, 0x90}; // pattern at offset 0 const auto f = MemFdFile::create(build_minimal_pe(bytes));
ASSERT_TRUE(write_minimal_pe_file(path.data(), bytes)); ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text"); const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "55 8B EC", ".text");
EXPECT_TRUE(res.has_value()); EXPECT_TRUE(res.has_value());
} }
TEST(unit_test_pe_pattern_scan_file, ScanMissingPattern) TEST(unit_test_pe_pattern_scan_file, ScanMissingPattern)
{ {
constexpr std::string_view path = "./test_minimal_pe_2.bin";
const std::vector<std::uint8_t> bytes = {0x00, 0x01, 0x02, 0x03}; const std::vector<std::uint8_t> bytes = {0x00, 0x01, 0x02, 0x03};
ASSERT_TRUE(write_minimal_pe_file(path.data(), bytes)); const auto f = MemFdFile::create(build_minimal_pe(bytes));
ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "FF EE DD", ".text"); const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "FF EE DD", ".text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }

View File

@@ -1,120 +1,89 @@
// Additional tests for PePatternScanner to exercise edge cases and loaded-module scanning // Additional tests for PePatternScanner to exercise edge cases and loaded-module scanning
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <fstream>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <omath/utility/pe_pattern_scan.hpp> #include <omath/utility/pe_pattern_scan.hpp>
#include <vector> #include <vector>
#include "mem_fd_helper.hpp"
using namespace omath; using namespace omath;
static bool write_bytes(const std::string& path, const std::vector<std::uint8_t>& data)
{
std::ofstream f(path, std::ios::binary);
if (!f.is_open())
return false;
f.write(reinterpret_cast<const char*>(data.data()), data.size());
return true;
}
TEST(unit_test_pe_pattern_scan_more, InvalidDosHeader) TEST(unit_test_pe_pattern_scan_more, InvalidDosHeader)
{ {
constexpr std::string_view path = "./test_bad_dos.bin";
std::vector<std::uint8_t> data(128, 0); std::vector<std::uint8_t> data(128, 0);
// write wrong magic
data[0] = 'N'; data[0] = 'N';
data[1] = 'Z'; data[1] = 'Z';
ASSERT_TRUE(write_bytes(path.data(), data)); const auto f = MemFdFile::create(data);
ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text"); const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "55 8B EC", ".text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }
TEST(unit_test_pe_pattern_scan_more, InvalidNtSignature) TEST(unit_test_pe_pattern_scan_more, InvalidNtSignature)
{ {
constexpr std::string_view path = "./test_bad_nt.bin";
std::vector<std::uint8_t> data(256, 0); std::vector<std::uint8_t> data(256, 0);
// valid DOS header
data[0] = 'M'; data[0] = 'M';
data[1] = 'Z'; data[1] = 'Z';
// point e_lfanew to 0x80
constexpr std::uint32_t e_lfanew = 0x80; constexpr std::uint32_t e_lfanew = 0x80;
std::memcpy(data.data() + 0x3C, &e_lfanew, sizeof(e_lfanew)); std::memcpy(data.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
// write garbage at e_lfanew (not 'PE\0\0')
data[e_lfanew + 0] = 'X'; data[e_lfanew + 0] = 'X';
data[e_lfanew + 1] = 'Y'; data[e_lfanew + 1] = 'Y';
data[e_lfanew + 2] = 'Z'; data[e_lfanew + 2] = 'Z';
data[e_lfanew + 3] = 'W'; data[e_lfanew + 3] = 'W';
ASSERT_TRUE(write_bytes(path.data(), data)); const auto f = MemFdFile::create(data);
ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text"); const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "55 8B EC", ".text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }
TEST(unit_test_pe_pattern_scan_more, SectionNotFound) TEST(unit_test_pe_pattern_scan_more, SectionNotFound)
{ {
// reuse minimal writer but with section named .data and search .text // Minimal PE with a .data section; scanning for .text should fail
constexpr std::string_view path = "./test_section_not_found.bin"; constexpr std::uint32_t e_lfanew = 0x80u;
std::ofstream f(path.data(), std::ios::binary); constexpr std::uint16_t size_opt = 0xF0u;
ASSERT_TRUE(f.is_open()); constexpr std::size_t nt_off = e_lfanew;
// DOS constexpr std::size_t fh_off = nt_off + 4;
std::vector<std::uint8_t> dos(64, 0); constexpr std::size_t oh_off = fh_off + 20;
dos[0] = 'M'; constexpr std::size_t sh_off = oh_off + size_opt;
dos[1] = 'Z'; constexpr std::size_t data_off = sh_off + 44;
std::uint32_t e_lfanew = 0x80;
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
f.write(reinterpret_cast<char*>(dos.data()), dos.size());
// pad
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
f.write(pad.data(), pad.size());
// NT sig
f.put('P');
f.put('E');
f.put('\0');
f.put('\0');
// FileHeader minimal
std::uint16_t machine = 0x8664;
std::uint16_t num_sections = 1;
std::uint32_t z = 0;
std::uint32_t z2 = 0;
std::uint32_t numsym = 0;
std::uint16_t size_opt = 0xF0;
std::uint16_t ch = 0;
f.write(reinterpret_cast<char*>(&machine), sizeof(machine));
f.write(reinterpret_cast<char*>(&num_sections), sizeof(num_sections));
f.write(reinterpret_cast<char*>(&z), sizeof(z));
f.write(reinterpret_cast<char*>(&z2), sizeof(z2));
f.write(reinterpret_cast<char*>(&numsym), sizeof(numsym));
f.write(reinterpret_cast<char*>(&size_opt), sizeof(size_opt));
f.write(reinterpret_cast<char*>(&ch), sizeof(ch));
// Optional header magic
std::uint16_t magic = 0x20b;
f.write(reinterpret_cast<char*>(&magic), sizeof(magic));
std::vector<std::uint8_t> opt(size_opt - sizeof(magic), 0);
f.write(reinterpret_cast<char*>(opt.data()), opt.size());
// Section header named .data
char name[8] = {'.', 'd', 'a', 't', 'a', 0, 0, 0};
f.write(name, 8);
std::uint32_t vs = 4, va = 0x1000, srd = 4, prd = 0x200;
f.write(reinterpret_cast<char*>(&vs), 4);
f.write(reinterpret_cast<char*>(&va), 4);
f.write(reinterpret_cast<char*>(&srd), 4);
f.write(reinterpret_cast<char*>(&prd), 4);
std::vector<char> rest(16, 0);
f.write(rest.data(), rest.size());
// section bytes
std::vector<std::uint8_t> sec = {0x00, 0x01, 0x02, 0x03};
f.write(reinterpret_cast<char*>(sec.data()), sec.size());
f.close();
auto res = PePatternScanner::scan_for_pattern_in_file(path, "00 01", ".text"); const std::vector<std::uint8_t> sec_data = {0x00, 0x01, 0x02, 0x03};
std::vector<std::uint8_t> buf(data_off + sec_data.size(), 0u);
buf[0] = 'M'; buf[1] = 'Z';
std::memcpy(buf.data() + 0x3C, &e_lfanew, 4);
buf[nt_off] = 'P'; buf[nt_off + 1] = 'E';
const std::uint16_t machine = 0x8664u, num_sections = 1u;
std::memcpy(buf.data() + fh_off, &machine, 2);
std::memcpy(buf.data() + fh_off + 2, &num_sections, 2);
std::memcpy(buf.data() + fh_off + 16, &size_opt, 2);
const std::uint16_t magic = 0x20Bu;
std::memcpy(buf.data() + oh_off, &magic, 2);
const char name[8] = {'.','d','a','t','a',0,0,0};
std::memcpy(buf.data() + sh_off, name, 8);
const std::uint32_t vs = 4u, va = 0x1000u, srd = 4u, prd = static_cast<std::uint32_t>(data_off);
std::memcpy(buf.data() + sh_off + 8, &vs, 4);
std::memcpy(buf.data() + sh_off + 12, &va, 4);
std::memcpy(buf.data() + sh_off + 16, &srd, 4);
std::memcpy(buf.data() + sh_off + 20, &prd, 4);
std::memcpy(buf.data() + data_off, sec_data.data(), sec_data.size());
const auto f = MemFdFile::create(buf);
ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "00 01", ".text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }
TEST(unit_test_pe_pattern_scan_more, LoadedModuleScanFinds) TEST(unit_test_pe_pattern_scan_more, LoadedModuleScanFinds)
{ {
// Create an in-memory buffer that mimics loaded module layout // Create an in-memory buffer that mimics loaded module layout
// Define local header structs matching those in source
struct DosHeader struct DosHeader
{ {
std::uint16_t e_magic; std::uint16_t e_magic;
@@ -158,9 +127,9 @@ TEST(unit_test_pe_pattern_scan_more, LoadedModuleScanFinds)
std::uint32_t base_of_code; std::uint32_t base_of_code;
std::uint64_t image_base; std::uint64_t image_base;
std::uint32_t section_alignment; std::uint32_t section_alignment;
std::uint32_t file_alignment; /* rest omitted */ std::uint32_t file_alignment;
std::uint32_t size_image; std::uint32_t size_image;
std::uint32_t size_headers; /* keep space */ std::uint32_t size_headers;
std::uint8_t pad[200]; std::uint8_t pad[200];
}; };
struct SectionHeader struct SectionHeader
@@ -188,44 +157,38 @@ TEST(unit_test_pe_pattern_scan_more, LoadedModuleScanFinds)
}; };
const std::vector<std::uint8_t> pattern_bytes = {0xDE, 0xAD, 0xBE, 0xEF, 0x90}; const std::vector<std::uint8_t> pattern_bytes = {0xDE, 0xAD, 0xBE, 0xEF, 0x90};
constexpr std::uint32_t base_of_code = 0x200; // will place bytes at offset 0x200 constexpr std::uint32_t base_of_code = 0x200;
const std::uint32_t size_code = static_cast<std::uint32_t>(pattern_bytes.size()); const std::uint32_t size_code = static_cast<std::uint32_t>(pattern_bytes.size());
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
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
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;
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->file_header.size_optional_header = static_cast<std::uint16_t>(sizeof(OptionalHeaderX64));
nt->optional_header.magic = 0x020B;
nt->optional_header.base_of_code = base_of_code;
nt->optional_header.size_code = size_code;
nt->optional_header.magic = 0x020B; // x64
nt->optional_header.base_of_code = base_of_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 = const std::size_t section_table_off =
static_cast<std::size_t>(dos->e_lfanew) + 4 + sizeof(FileHeader) + sizeof(OptionalHeaderX64); 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)); 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); const auto sect = reinterpret_cast<SectionHeader*>(buf.data() + section_table_off);
std::memset(sect, 0, sizeof(SectionHeader)); std::memset(sect, 0, sizeof(SectionHeader));
std::memcpy(sect->name, ".text", 5); std::memcpy(sect->name, ".text", 5);
sect->virtual_size = size_code; sect->virtual_size = size_code;
sect->virtual_address = base_of_code; sect->virtual_address = base_of_code;
sect->size_raw_data = size_code; sect->size_raw_data = size_code;
sect->ptr_raw_data = base_of_code; sect->ptr_raw_data = base_of_code;
sect->characteristics = 0x60000020; // code | execute | read sect->characteristics = 0x60000020;
// 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", ".text"); const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE AD BE EF", ".text");

View File

@@ -4,6 +4,7 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <omath/utility/pe_pattern_scan.hpp> #include <omath/utility/pe_pattern_scan.hpp>
#include <vector> #include <vector>
#include "mem_fd_helper.hpp"
using namespace omath; using namespace omath;
@@ -19,95 +20,6 @@ struct TestFileHeader
std::uint16_t characteristics; std::uint16_t characteristics;
}; };
static bool write_bytes(const std::string& path, const std::vector<std::uint8_t>& data)
{
std::ofstream f(path, std::ios::binary);
if (!f.is_open())
return false;
f.write(reinterpret_cast<const char*>(data.data()), data.size());
return true;
}
// Helper: write a trivial PE-like file with DOS header and a single section named .text
static bool write_minimal_pe_file(const std::string& path, const std::vector<std::uint8_t>& section_bytes)
{
std::ofstream f(path, std::ios::binary);
if (!f.is_open())
return false;
// Write DOS header (e_magic = 0x5A4D, e_lfanew at offset 0x3C)
std::vector<std::uint8_t> dos(64, 0);
dos[0] = 'M';
dos[1] = 'Z';
std::uint32_t e_lfanew = 0x80;
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
f.write(reinterpret_cast<const char*>(dos.data()), dos.size());
// Pad up to e_lfanew
if (f.tellp() < static_cast<std::streampos>(e_lfanew))
{
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
f.write(pad.data(), pad.size());
}
// NT headers signature 'PE\0\0'
f.put('P');
f.put('E');
f.put('\0');
f.put('\0');
// FileHeader minimal
std::uint16_t machine = 0x8664; // x64
std::uint16_t num_sections = 1;
std::uint32_t dummy32 = 0;
std::uint32_t dummy32b = 0;
std::uint16_t size_optional = 0xF0;
std::uint16_t characteristics = 0;
f.write(reinterpret_cast<const char*>(&machine), sizeof(machine));
f.write(reinterpret_cast<const char*>(&num_sections), sizeof(num_sections));
f.write(reinterpret_cast<const char*>(&dummy32), sizeof(dummy32));
f.write(reinterpret_cast<const char*>(&dummy32b), sizeof(dummy32b));
std::uint32_t num_symbols = 0;
f.write(reinterpret_cast<const char*>(&num_symbols), sizeof(num_symbols));
f.write(reinterpret_cast<const char*>(&size_optional), sizeof(size_optional));
f.write(reinterpret_cast<const char*>(&characteristics), sizeof(characteristics));
// OptionalHeader minimal filler
std::uint16_t magic = 0x20b;
f.write(reinterpret_cast<const char*>(&magic), sizeof(magic));
std::vector<std::uint8_t> opt(size_optional - sizeof(magic), 0);
f.write(reinterpret_cast<const char*>(opt.data()), opt.size());
// Section header (name 8 bytes, then remaining 36 bytes)
char name[8] = {'.', 't', 'e', 'x', 't', 0, 0, 0};
f.write(name, 8);
constexpr std::uint32_t section_header_rest = 36u;
const std::streampos header_rest_pos = f.tellp();
std::vector<char> placeholder(section_header_rest, 0);
f.write(placeholder.data(), placeholder.size());
// Now write section raw data and remember its file offset
const std::streampos data_pos = f.tellp();
f.write(reinterpret_cast<const char*>(section_bytes.data()), static_cast<std::streamsize>(section_bytes.size()));
// Patch section header fields
const std::uint32_t virtual_size = static_cast<std::uint32_t>(section_bytes.size());
constexpr std::uint32_t virtual_address = 0x1000u;
const std::uint32_t size_raw_data = static_cast<std::uint32_t>(section_bytes.size());
const std::uint32_t ptr_raw_data = static_cast<std::uint32_t>(data_pos);
f.seekp(header_rest_pos, std::ios::beg);
f.write(reinterpret_cast<const char*>(&virtual_size), sizeof(virtual_size));
f.write(reinterpret_cast<const char*>(&virtual_address), sizeof(virtual_address));
f.write(reinterpret_cast<const char*>(&size_raw_data), sizeof(size_raw_data));
f.write(reinterpret_cast<const char*>(&ptr_raw_data), sizeof(ptr_raw_data));
f.seekp(0, std::ios::end);
f.close();
return true;
}
TEST(unit_test_pe_pattern_scan_more2, LoadedModuleNullBaseReturnsNull) TEST(unit_test_pe_pattern_scan_more2, LoadedModuleNullBaseReturnsNull)
{ {
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(nullptr, "DE AD"); const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(nullptr, "DE AD");
@@ -116,7 +28,6 @@ TEST(unit_test_pe_pattern_scan_more2, LoadedModuleNullBaseReturnsNull)
TEST(unit_test_pe_pattern_scan_more2, LoadedModuleInvalidOptionalHeaderReturnsNull) TEST(unit_test_pe_pattern_scan_more2, LoadedModuleInvalidOptionalHeaderReturnsNull)
{ {
// Construct in-memory buffer with DOS header but invalid optional header magic
std::vector<std::uint8_t> buf(0x200, 0); std::vector<std::uint8_t> buf(0x200, 0);
struct DosHeader struct DosHeader
{ {
@@ -128,19 +39,11 @@ TEST(unit_test_pe_pattern_scan_more2, LoadedModuleInvalidOptionalHeaderReturnsNu
dos->e_magic = 0x5A4D; dos->e_magic = 0x5A4D;
dos->e_lfanew = 0x80; dos->e_lfanew = 0x80;
// Place an NT header with wrong optional magic at e_lfanew
const auto nt_ptr = buf.data() + dos->e_lfanew; const auto nt_ptr = buf.data() + dos->e_lfanew;
// write signature nt_ptr[0] = 'P'; nt_ptr[1] = 'E'; nt_ptr[2] = 0; nt_ptr[3] = 0;
nt_ptr[0] = 'P';
nt_ptr[1] = 'E';
nt_ptr[2] = 0;
nt_ptr[3] = 0;
// craft FileHeader with size_optional_header large enough
constexpr std::uint16_t size_opt = 0xE0; constexpr std::uint16_t size_opt = 0xE0;
// file header starts at offset 4 std::memcpy(nt_ptr + 4 + 12, &size_opt, sizeof(size_opt));
std::memcpy(nt_ptr + 4 + 12, &size_opt,
sizeof(size_opt)); // size_optional_header located after 12 bytes into FileHeader
// write optional header magic to be invalid value
constexpr std::uint16_t bad_magic = 0x9999; constexpr std::uint16_t bad_magic = 0x9999;
std::memcpy(nt_ptr + 4 + sizeof(std::uint32_t) + sizeof(std::uint16_t) + sizeof(std::uint16_t), &bad_magic, std::memcpy(nt_ptr + 4 + sizeof(std::uint32_t) + sizeof(std::uint16_t) + sizeof(std::uint16_t), &bad_magic,
sizeof(bad_magic)); sizeof(bad_magic));
@@ -151,13 +54,11 @@ TEST(unit_test_pe_pattern_scan_more2, LoadedModuleInvalidOptionalHeaderReturnsNu
TEST(unit_test_pe_pattern_scan_more2, FileX86OptionalHeaderScanFindsPattern) TEST(unit_test_pe_pattern_scan_more2, FileX86OptionalHeaderScanFindsPattern)
{ {
constexpr std::string_view path = "./test_pe_x86.bin";
const std::vector<std::uint8_t> pattern = {0xDE, 0xAD, 0xBE, 0xEF}; const std::vector<std::uint8_t> pattern = {0xDE, 0xAD, 0xBE, 0xEF};
const auto f = MemFdFile::create(build_minimal_pe(pattern));
ASSERT_TRUE(f.valid());
// Use helper from this file to write a consistent minimal PE file with .text section const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "DE AD BE EF", ".text");
ASSERT_TRUE(write_minimal_pe_file(path.data(), pattern));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "DE AD BE EF", ".text");
ASSERT_TRUE(res.has_value()); ASSERT_TRUE(res.has_value());
EXPECT_GE(res->virtual_base_addr, 0u); EXPECT_GE(res->virtual_base_addr, 0u);
EXPECT_GE(res->raw_base_addr, 0u); EXPECT_GE(res->raw_base_addr, 0u);
@@ -166,97 +67,73 @@ TEST(unit_test_pe_pattern_scan_more2, FileX86OptionalHeaderScanFindsPattern)
TEST(unit_test_pe_pattern_scan_more2, FilePatternNotFoundReturnsNull) TEST(unit_test_pe_pattern_scan_more2, FilePatternNotFoundReturnsNull)
{ {
const std::string path = "./test_pe_no_pattern.bin";
std::vector<std::uint8_t> data(512, 0); std::vector<std::uint8_t> data(512, 0);
// minimal DOS/NT headers to make extract_section fail earlier or return empty data data[0] = 'M'; data[1] = 'Z';
data[0] = 'M';
data[1] = 'Z';
constexpr std::uint32_t e_lfanew = 0x80; constexpr std::uint32_t e_lfanew = 0x80;
std::memcpy(data.data() + 0x3C, &e_lfanew, sizeof(e_lfanew)); std::memcpy(data.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
// NT signature data[e_lfanew + 0] = 'P'; data[e_lfanew + 1] = 'E';
data[e_lfanew + 0] = 'P';
data[e_lfanew + 1] = 'E';
data[e_lfanew + 2] = 0;
data[e_lfanew + 3] = 0;
// FileHeader: one section, size_optional_header set low
constexpr std::uint16_t num_sections = 1; constexpr std::uint16_t num_sections = 1;
constexpr std::uint16_t size_optional_header = 0xE0; constexpr std::uint16_t size_optional_header = 0xE0;
std::memcpy(data.data() + e_lfanew + 6, &num_sections, sizeof(num_sections)); std::memcpy(data.data() + e_lfanew + 6, &num_sections, sizeof(num_sections));
std::memcpy(data.data() + e_lfanew + 4 + 12, &size_optional_header, sizeof(size_optional_header)); std::memcpy(data.data() + e_lfanew + 4 + 12, &size_optional_header, sizeof(size_optional_header));
// Optional header magic x64
constexpr std::uint16_t magic = 0x020B; constexpr std::uint16_t magic = 0x020B;
std::memcpy(data.data() + e_lfanew + 4 + sizeof(TestFileHeader), &magic, sizeof(magic)); std::memcpy(data.data() + e_lfanew + 4 + sizeof(TestFileHeader), &magic, sizeof(magic));
// Section header .text with small data that does not contain the pattern
constexpr std::size_t offset_to_segment_table = e_lfanew + 4 + sizeof(TestFileHeader) + size_optional_header; constexpr std::size_t offset_to_segment_table = e_lfanew + 4 + sizeof(TestFileHeader) + size_optional_header;
constexpr char name[8] = {'.', 't', 'e', 'x', 't', 0, 0, 0}; constexpr char name[8] = {'.', 't', 'e', 'x', 't', 0, 0, 0};
std::memcpy(data.data() + offset_to_segment_table, name, 8); std::memcpy(data.data() + offset_to_segment_table, name, 8);
std::uint32_t vs = 4, va = 0x1000, srd = 4, prd = 0x200; std::uint32_t vs = 4, va = 0x1000, srd = 4, prd = 0x200;
std::memcpy(data.data() + offset_to_segment_table + 8, &vs, 4); std::memcpy(data.data() + offset_to_segment_table + 8, &vs, 4);
std::memcpy(data.data() + offset_to_segment_table + 12, &va, 4); std::memcpy(data.data() + offset_to_segment_table + 12, &va, 4);
std::memcpy(data.data() + offset_to_segment_table + 16, &srd, 4); std::memcpy(data.data() + offset_to_segment_table + 16, &srd, 4);
std::memcpy(data.data() + offset_to_segment_table + 20, &prd, 4); std::memcpy(data.data() + offset_to_segment_table + 20, &prd, 4);
// write file
ASSERT_TRUE(write_bytes(path, data));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "AA BB CC", ".text"); const auto f = MemFdFile::create(data);
ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "AA BB CC", ".text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }
// Extra tests for pe_pattern_scan edge cases (on-disk API)
TEST(PePatternScanMore2, PatternAtStartFound) TEST(PePatternScanMore2, PatternAtStartFound)
{ {
const std::string path = "./test_pe_more_start.bin";
const std::vector<std::uint8_t> bytes = {0x90, 0x01, 0x02, 0x03, 0x04}; const std::vector<std::uint8_t> bytes = {0x90, 0x01, 0x02, 0x03, 0x04};
ASSERT_TRUE(write_minimal_pe_file(path, bytes)); const auto f = MemFdFile::create(build_minimal_pe(bytes));
ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "90 01 02", ".text"); const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "90 01 02", ".text");
EXPECT_TRUE(res.has_value()); EXPECT_TRUE(res.has_value());
} }
TEST(PePatternScanMore2, PatternAtEndFound) TEST(PePatternScanMore2, PatternAtEndFound)
{ {
const std::string path = "./test_pe_more_end.bin"; const std::vector<std::uint8_t> bytes = {0x00, 0x11, 0x22, 0x33, 0x44};
std::vector<std::uint8_t> bytes = {0x00, 0x11, 0x22, 0x33, 0x44}; const auto f = MemFdFile::create(build_minimal_pe(bytes));
ASSERT_TRUE(write_minimal_pe_file(path, bytes)); ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "22 33 44", ".text"); const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "22 33 44", ".text");
if (!res.has_value()) if (!res.has_value())
{ {
// Try to locate the section header and print the raw section bytes the scanner would read // Debug: inspect section header via the memfd path
std::ifstream in(path, std::ios::binary); std::ifstream in(f.path(), std::ios::binary);
ASSERT_TRUE(in.is_open()); if (in.is_open())
// search for ".text" name
in.seekg(0, std::ios::beg);
std::vector<char> filebuf((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
const auto it = std::search(filebuf.begin(), filebuf.end(), std::begin(".text"), std::end(".text") - 1);
if (it != filebuf.end())
{ {
const size_t pos = std::distance(filebuf.begin(), it); std::vector<char> filebuf((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
// after name, next fields: virtual_size (4), virtual_address(4), size_raw_data(4), ptr_raw_data(4) const auto it = std::search(filebuf.begin(), filebuf.end(), std::begin(".text"), std::end(".text") - 1);
const size_t meta_off = pos + 8; if (it != filebuf.end())
uint32_t virtual_size{};
uint32_t virtual_address{};
uint32_t size_raw_data{};
uint32_t ptr_raw_data{};
std::memcpy(&virtual_size, filebuf.data() + meta_off, sizeof(virtual_size));
std::memcpy(&virtual_address, filebuf.data() + meta_off + 4, sizeof(virtual_address));
std::memcpy(&size_raw_data, filebuf.data() + meta_off + 8, sizeof(size_raw_data));
std::memcpy(&ptr_raw_data, filebuf.data() + meta_off + 12, sizeof(ptr_raw_data));
std::cerr << "Parsed section header: virtual_size=" << virtual_size << " virtual_address=0x" << std::hex
<< virtual_address << std::dec << " size_raw_data=" << size_raw_data
<< " ptr_raw_data=" << ptr_raw_data << "\n";
if (ptr_raw_data + size_raw_data <= filebuf.size())
{ {
std::cerr << "Extracted section bytes:\n"; const std::size_t pos = std::distance(filebuf.begin(), it);
for (size_t i = 0; i < size_raw_data; i += 16) const std::size_t meta_off = pos + 8;
{ std::uint32_t virtual_size{}, virtual_address{}, size_raw_data{}, ptr_raw_data{};
std::fprintf(stderr, "%04zx: ", i); std::memcpy(&virtual_size, filebuf.data() + meta_off, sizeof(virtual_size));
for (size_t j = 0; j < 16 && i + j < size_raw_data; ++j) std::memcpy(&virtual_address, filebuf.data() + meta_off + 4, sizeof(virtual_address));
std::fprintf(stderr, "%02x ", static_cast<uint8_t>(filebuf[ptr_raw_data + i + j])); std::memcpy(&size_raw_data, filebuf.data() + meta_off + 8, sizeof(size_raw_data));
std::fprintf(stderr, "\n"); std::memcpy(&ptr_raw_data, filebuf.data() + meta_off + 12, sizeof(ptr_raw_data));
} std::cerr << "Parsed section header: virtual_size=" << virtual_size << " virtual_address=0x"
<< std::hex << virtual_address << std::dec << " size_raw_data=" << size_raw_data
<< " ptr_raw_data=" << ptr_raw_data << "\n";
} }
} }
} }
@@ -265,30 +142,30 @@ TEST(PePatternScanMore2, PatternAtEndFound)
TEST(PePatternScanMore2, WildcardMatches) TEST(PePatternScanMore2, WildcardMatches)
{ {
const std::string path = "./test_pe_more_wild.bin";
const std::vector<std::uint8_t> bytes = {0xDE, 0xAD, 0xBE, 0xEF}; const std::vector<std::uint8_t> bytes = {0xDE, 0xAD, 0xBE, 0xEF};
ASSERT_TRUE(write_minimal_pe_file(path, bytes)); const auto f = MemFdFile::create(build_minimal_pe(bytes));
ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "DE ?? BE", ".text"); const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "DE ?? BE", ".text");
EXPECT_TRUE(res.has_value()); EXPECT_TRUE(res.has_value());
} }
TEST(PePatternScanMore2, PatternLongerThanBuffer) TEST(PePatternScanMore2, PatternLongerThanBuffer)
{ {
const std::string path = "./test_pe_more_small.bin";
const std::vector<std::uint8_t> bytes = {0xAA, 0xBB}; const std::vector<std::uint8_t> bytes = {0xAA, 0xBB};
ASSERT_TRUE(write_minimal_pe_file(path, bytes)); const auto f = MemFdFile::create(build_minimal_pe(bytes));
ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "AA BB CC", ".text"); const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "AA BB CC", ".text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }
TEST(PePatternScanMore2, InvalidPatternParse) TEST(PePatternScanMore2, InvalidPatternParse)
{ {
const std::string path = "./test_pe_more_invalid.bin";
const std::vector<std::uint8_t> bytes = {0x01, 0x02, 0x03}; const std::vector<std::uint8_t> bytes = {0x01, 0x02, 0x03};
ASSERT_TRUE(write_minimal_pe_file(path, bytes)); const auto f = MemFdFile::create(build_minimal_pe(bytes));
ASSERT_TRUE(f.valid());
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "01 GG 03", ".text"); const auto res = PePatternScanner::scan_for_pattern_in_file(f.path(), "01 GG 03", ".text");
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
} }

View File

@@ -53,6 +53,47 @@ TEST(PredEngineTrait, CalcViewpointFromAngles)
EXPECT_NEAR(vp.z, 10.f, 1e-6f); EXPECT_NEAR(vp.z, 10.f, 1e-6f);
} }
TEST(PredEngineTrait, PredictProjectilePositionWithLaunchOffset)
{
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_offset = {5.f, 3.f, -2.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f;
// At time=0, projectile should be at launch_pos = origin + offset
const auto pos_t0 = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 0.f, 9.81f);
EXPECT_NEAR(pos_t0.x, 5.f, 1e-4f);
EXPECT_NEAR(pos_t0.y, 3.f, 1e-4f);
EXPECT_NEAR(pos_t0.z, -2.f, 1e-4f);
// At time=1 with zero pitch/yaw, should travel along X from the offset position
const auto pos_t1 = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos_t1.x, 5.f + 10.f, 1e-3f);
EXPECT_NEAR(pos_t1.y, 3.f, 1e-3f);
EXPECT_NEAR(pos_t1.z, -2.f - 9.81f * 0.5f, 1e-3f);
}
TEST(PredEngineTrait, ZeroLaunchOffsetMatchesOriginalBehavior)
{
projectile_prediction::Projectile p;
p.m_origin = {10.f, 20.f, 30.f};
p.m_launch_offset = {0.f, 0.f, 0.f};
p.m_launch_speed = 15.f;
p.m_gravity_scale = 0.5f;
projectile_prediction::Projectile p_no_offset;
p_no_offset.m_origin = {10.f, 20.f, 30.f};
p_no_offset.m_launch_speed = 15.f;
p_no_offset.m_gravity_scale = 0.5f;
const auto pos1 = PredEngineTrait::predict_projectile_position(p, 30.f, 45.f, 2.f, 9.81f);
const auto pos2 = PredEngineTrait::predict_projectile_position(p_no_offset, 30.f, 45.f, 2.f, 9.81f);
EXPECT_NEAR(pos1.x, pos2.x, 1e-6f);
EXPECT_NEAR(pos1.y, pos2.y, 1e-6f);
EXPECT_NEAR(pos1.z, pos2.z, 1e-6f);
}
TEST(PredEngineTrait, DirectAngles) TEST(PredEngineTrait, DirectAngles)
{ {
constexpr Vector3<float> origin{0.f, 0.f, 0.f}; constexpr Vector3<float> origin{0.f, 0.f, 0.f};

View File

@@ -16,3 +16,280 @@ TEST(UnitTestPrediction, PredictionTest)
EXPECT_NEAR(-42.547142, pitch.as_degrees(), 0.01f); EXPECT_NEAR(-42.547142, pitch.as_degrees(), 0.01f);
EXPECT_NEAR(-1.181189, yaw.as_degrees(), 0.01f); EXPECT_NEAR(-1.181189, yaw.as_degrees(), 0.01f);
} }
// Helper: verify aim_angles match angles derived from aim_point via CameraTrait
static void expect_angles_match_aim_point(const omath::projectile_prediction::Projectile& proj,
const omath::projectile_prediction::Target& target,
float gravity, float step, float max_time, float tolerance,
float angle_eps = 0.01f)
{
const omath::projectile_prediction::ProjPredEngineLegacy engine(gravity, step, max_time, tolerance);
const auto aim_point = engine.maybe_calculate_aim_point(proj, target);
const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target);
ASSERT_TRUE(aim_point.has_value()) << "aim_point should have a solution";
ASSERT_TRUE(aim_angles.has_value()) << "aim_angles should have a solution";
// Source engine CameraTrait: pitch = -asin(dir.z), yaw = atan2(dir.y, dir.x)
// PredEngineTrait: pitch = asin(delta.z / dist), yaw = atan2(delta.y, delta.x)
// So aim_angles.pitch == -camera_pitch, aim_angles.yaw == camera_yaw
const auto [cam_pitch, cam_yaw, cam_roll] =
omath::source_engine::CameraTrait::calc_look_at_angle(proj.m_origin, aim_point.value());
EXPECT_NEAR(aim_angles->pitch, -cam_pitch.as_degrees(), angle_eps)
<< "pitch from aim_angles must match pitch derived from aim_point";
EXPECT_NEAR(aim_angles->yaw, cam_yaw.as_degrees(), angle_eps)
<< "yaw from aim_angles must match yaw derived from aim_point";
}
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_StaticTarget)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
}
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_MovingTarget)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 3000, .m_gravity_scale = 1.0};
expect_angles_match_aim_point(proj, target, 800, 1.f / 500.f, 30, 10.f);
}
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_AirborneTarget)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 4000, .m_gravity_scale = 0.5};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 10.f);
}
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_HighArc)
{
// Target nearly directly above — high pitch angle
constexpr omath::projectile_prediction::Target target{
.m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.3};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
}
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_NegativeYaw)
{
// Target behind and to the left — negative yaw quadrant
constexpr omath::projectile_prediction::Target target{
.m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
}
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_WithLaunchOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
}
// Helper: simulate projectile flight using aim_angles and verify it reaches the target.
// Steps the projectile forward in small increments, simultaneously predicts target position,
// and checks that the minimum distance is within hit_tolerance.
static void expect_projectile_hits_target(const omath::projectile_prediction::Projectile& proj,
const omath::projectile_prediction::Target& target,
float gravity, float engine_step, float max_time, float engine_tolerance,
float hit_tolerance, float sim_step = 1.f / 2000.f)
{
using Trait = omath::source_engine::PredEngineTrait;
const omath::projectile_prediction::ProjPredEngineLegacy engine(gravity, engine_step, max_time, engine_tolerance);
const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target);
ASSERT_TRUE(aim_angles.has_value()) << "engine must find a solution";
float min_dist = std::numeric_limits<float>::max();
float best_time = 0.f;
for (float t = 0.f; t <= max_time; t += sim_step)
{
const auto proj_pos = Trait::predict_projectile_position(proj, aim_angles->pitch, aim_angles->yaw, t, gravity);
const auto tgt_pos = Trait::predict_target_position(target, t, gravity);
const float dist = proj_pos.distance_to(tgt_pos);
if (dist < min_dist)
{
min_dist = dist;
best_time = t;
}
// Early exit once distance starts increasing significantly after approaching
if (dist > min_dist + hit_tolerance * 10.f && min_dist < hit_tolerance * 100.f)
break;
}
EXPECT_LE(min_dist, hit_tolerance)
<< "Projectile must reach target. Closest approach: " << min_dist
<< " at t=" << best_time;
}
// ── Simulation hit tests: no launch offset ─────────────────────────────────
TEST(ProjectileSimulation, HitsStaticTarget_NoOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsMovingTarget_NoOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 3000, .m_gravity_scale = 1.0};
expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f);
}
TEST(ProjectileSimulation, HitsAirborneTarget_NoOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 4000, .m_gravity_scale = 0.5};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f);
}
TEST(ProjectileSimulation, HitsHighTarget_NoOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.3};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsNegativeYawTarget_NoOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
// ── Simulation hit tests: with launch offset ────────────────────────────────
TEST(ProjectileSimulation, HitsStaticTarget_SmallOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsStaticTarget_LargeXOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {300, 100, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {20, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsStaticTarget_LargeYOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {150, -200, 30}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {0, 15, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsStaticTarget_LargeZOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {100, 0, 200}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {0, 0, -10}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsStaticTarget_AllAxesOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {250, 80, 60}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {10, 5, 20}, .m_launch_offset = {8, -4, -6}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsMovingTarget_WithOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {400, 0, 50}, .m_velocity = {-30, 10, 5}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {10, -5, 2}, .m_launch_speed = 3000, .m_gravity_scale = 0.8};
expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f);
}
TEST(ProjectileSimulation, HitsAirborneTarget_WithOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {150, 80, 250}, .m_velocity = {5, -10, -30}, .m_is_airborne = true};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 50}, .m_launch_offset = {3, 7, -5}, .m_launch_speed = 4000, .m_gravity_scale = 0.5};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f);
}
TEST(ProjectileSimulation, HitsNegativeYawTarget_WithOffset)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {-5, 3, 2}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(UnitTestPrediction, AimAnglesReturnsNulloptWhenNoSolution)
{
constexpr omath::projectile_prediction::Target target{
.m_origin = {100000, 0, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 1, .m_gravity_scale = 1};
const omath::projectile_prediction::ProjPredEngineLegacy engine(9.81f, 0.1f, 2.f, 5.f);
const auto aim_point = engine.maybe_calculate_aim_point(proj, target);
const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target);
EXPECT_FALSE(aim_point.has_value());
EXPECT_FALSE(aim_angles.has_value());
}

View File

@@ -46,6 +46,22 @@ TEST(ProjPredLegacyMore, ZeroGravityUsesDirectPitchAndReturnsViewpoint)
EXPECT_NEAR(v.z, 3.f, 1e-6f); EXPECT_NEAR(v.z, 3.f, 1e-6f);
} }
TEST(ProjPredLegacyMore, ZeroGravityAimAnglesReturnsPitchAndYaw)
{
constexpr Projectile proj{ .m_origin = {0.f, 0.f, 0.f}, .m_launch_speed = 10.f, .m_gravity_scale = 0.f };
constexpr Target target{ .m_origin = {100.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false };
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<FakeEngineZeroGravity>;
const Engine engine(9.8f, 0.1f, 5.f, 1e-3f);
const auto res = engine.maybe_calculate_aim_angles(proj, target);
ASSERT_TRUE(res.has_value());
// FakeEngineZeroGravity::calc_direct_pitch_angle returns 12.5f
EXPECT_NEAR(res->pitch, 12.5f, 1e-6f);
// FakeEngineZeroGravity::calc_direct_yaw_angle returns 0.f
EXPECT_NEAR(res->yaw, 0.f, 1e-6f);
}
// Fake trait producing no valid launch angle (root < 0) // Fake trait producing no valid launch angle (root < 0)
struct FakeEngineNoSolution struct FakeEngineNoSolution
{ {
@@ -69,6 +85,9 @@ TEST(ProjPredLegacyMore, NoSolutionRootReturnsNullopt)
const auto res = engine.maybe_calculate_aim_point(proj, target); const auto res = engine.maybe_calculate_aim_point(proj, target);
EXPECT_FALSE(res.has_value()); EXPECT_FALSE(res.has_value());
const auto angles_res = engine.maybe_calculate_aim_angles(proj, target);
EXPECT_FALSE(angles_res.has_value());
} }
// Fake trait where an angle exists but the projectile does not reach target (miss) // Fake trait where an angle exists but the projectile does not reach target (miss)

View File

@@ -20,6 +20,13 @@ public:
int m_health{123}; int m_health{123};
}; };
// Extract a raw function pointer from an object's vtable
inline const void* get_vtable_entry(const void* obj, const std::size_t index)
{
const auto vtable = *static_cast<void* const* const*>(obj);
return vtable[index];
}
class RevPlayer final : omath::rev_eng::InternalReverseEngineeredObject class RevPlayer final : omath::rev_eng::InternalReverseEngineeredObject
{ {
public: public:
@@ -51,6 +58,17 @@ public:
{ {
return call_virtual_method<1, int>(); return call_virtual_method<1, int>();
} }
// Wrappers exposing call_method for testing — use vtable entries as known-good function pointers
int call_foo_via_ptr(const void* fn_ptr) const
{
return call_method<int>(fn_ptr);
}
int call_bar_via_ptr(const void* fn_ptr) const
{
return call_method<int>(fn_ptr);
}
}; };
TEST(unit_test_reverse_enineering, read_test) TEST(unit_test_reverse_enineering, read_test)
@@ -64,4 +82,39 @@ TEST(unit_test_reverse_enineering, read_test)
EXPECT_EQ(player_original.bar(), player_reversed->rev_bar()); EXPECT_EQ(player_original.bar(), player_reversed->rev_bar());
EXPECT_EQ(player_original.foo(), player_reversed->rev_foo()); EXPECT_EQ(player_original.foo(), player_reversed->rev_foo());
EXPECT_EQ(player_original.bar(), player_reversed->rev_bar_const()); EXPECT_EQ(player_original.bar(), player_reversed->rev_bar_const());
}
TEST(unit_test_reverse_enineering, call_method_with_vtable_ptr)
{
// Extract raw function pointers from Player's vtable, then call them via call_method
Player player;
const auto* rev = reinterpret_cast<const RevPlayer*>(&player);
const auto* foo_ptr = get_vtable_entry(&player, 0);
const auto* bar_ptr = get_vtable_entry(&player, 1);
EXPECT_EQ(player.foo(), rev->call_foo_via_ptr(foo_ptr));
EXPECT_EQ(player.bar(), rev->call_bar_via_ptr(bar_ptr));
EXPECT_EQ(1, rev->call_foo_via_ptr(foo_ptr));
EXPECT_EQ(2, rev->call_bar_via_ptr(bar_ptr));
}
TEST(unit_test_reverse_enineering, call_method_same_result_as_virtual)
{
// call_virtual_method delegates to call_method — both paths must agree
Player player;
const auto* rev = reinterpret_cast<const RevPlayer*>(&player);
EXPECT_EQ(rev->rev_foo(), rev->call_foo_via_ptr(get_vtable_entry(&player, 0)));
EXPECT_EQ(rev->rev_bar(), rev->call_bar_via_ptr(get_vtable_entry(&player, 1)));
}
TEST(unit_test_reverse_enineering, call_virtual_method_delegates_to_call_method)
{
Player player;
auto* rev = reinterpret_cast<RevPlayer*>(&player);
EXPECT_EQ(1, rev->rev_foo());
EXPECT_EQ(2, rev->rev_bar());
EXPECT_EQ(2, rev->rev_bar_const());
} }

View File

@@ -0,0 +1,260 @@
//
// Created by claude on 19.03.2026.
//
#include <gtest/gtest.h>
#include <omath/algorithm/targeting.hpp>
#include <omath/engines/source_engine/camera.hpp>
#include <vector>
namespace
{
using Camera = omath::source_engine::Camera;
using ViewAngles = omath::source_engine::ViewAngles;
using Targets = std::vector<omath::Vector3<float>>;
using Iter = Targets::const_iterator;
using FilterSig = bool(const omath::Vector3<float>&);
constexpr auto k_fov = omath::Angle<float, 0.f, 180.f, omath::AngleFlags::Clamped>::from_degrees(90.f);
Camera make_camera(const omath::Vector3<float>& origin, float pitch_deg, float yaw_deg)
{
ViewAngles angles{
omath::source_engine::PitchAngle::from_degrees(pitch_deg),
omath::source_engine::YawAngle::from_degrees(yaw_deg),
omath::source_engine::RollAngle::from_degrees(0.f),
};
return Camera{origin, angles, {1920.f, 1080.f}, k_fov, 0.01f, 1000.f};
}
auto get_pos = [](const omath::Vector3<float>& v) -> const omath::Vector3<float>& { return v; };
Iter find_closest(const Iter begin, const Iter end, const Camera& camera)
{
return omath::algorithm::get_closest_target_by_fov<Camera, Iter, FilterSig>(
begin, end, camera, get_pos);
}
Iter find_nearest(const Iter begin, const Iter end, const omath::Vector3<float>& origin)
{
return omath::algorithm::get_closest_target_by_distance<Iter, FilterSig>(
begin, end, origin, get_pos);
}
}
TEST(unit_test_targeting, returns_end_for_empty_range)
{
const auto camera = make_camera({0, 0, 0}, 0.f, 0.f);
Targets targets;
EXPECT_EQ(find_closest(targets.cbegin(), targets.cend(), camera), targets.cend());
}
TEST(unit_test_targeting, single_target_returns_that_target)
{
const auto camera = make_camera({0, 0, 0}, 0.f, 0.f);
Targets targets = {{100.f, 0.f, 0.f}};
EXPECT_EQ(find_closest(targets.cbegin(), targets.cend(), camera), targets.cbegin());
}
TEST(unit_test_targeting, picks_closest_to_crosshair)
{
// Camera looking forward along +X (yaw=0, pitch=0 in source engine)
const auto camera = make_camera({0, 0, 0}, 0.f, 0.f);
Targets targets = {
{100.f, 50.f, 0.f}, // off to the side
{100.f, 1.f, 0.f}, // nearly on crosshair
{100.f, -30.f, 0.f}, // off to the other side
};
const auto result = find_closest(targets.cbegin(), targets.cend(), camera);
ASSERT_NE(result, targets.cend());
EXPECT_EQ(result, targets.cbegin() + 1);
}
TEST(unit_test_targeting, picks_closest_with_vertical_offset)
{
const auto camera = make_camera({0, 0, 0}, 0.f, 0.f);
Targets targets = {
{100.f, 0.f, 50.f}, // high above
{100.f, 0.f, 2.f}, // slightly above
{100.f, 0.f, 30.f}, // moderately above
};
const auto result = find_closest(targets.cbegin(), targets.cend(), camera);
ASSERT_NE(result, targets.cend());
EXPECT_EQ(result, targets.cbegin() + 1);
}
TEST(unit_test_targeting, respects_camera_direction)
{
// Camera looking along +Y (yaw=90)
const auto camera = make_camera({0, 0, 0}, 0.f, 90.f);
Targets targets = {
{100.f, 0.f, 0.f}, // to the side relative to camera facing +Y
{0.f, 100.f, 0.f}, // directly in front
};
const auto result = find_closest(targets.cbegin(), targets.cend(), camera);
ASSERT_NE(result, targets.cend());
EXPECT_EQ(result, targets.cbegin() + 1);
}
TEST(unit_test_targeting, equidistant_targets_returns_first)
{
const auto camera = make_camera({0, 0, 0}, 0.f, 0.f);
// Two targets symmetric about the forward axis — same angular distance
Targets targets = {
{100.f, 10.f, 0.f},
{100.f, -10.f, 0.f},
};
const auto result = find_closest(targets.cbegin(), targets.cend(), camera);
ASSERT_NE(result, targets.cend());
// First target should be selected (strict < means first wins on tie)
EXPECT_EQ(result, targets.cbegin());
}
TEST(unit_test_targeting, camera_pitch_affects_selection)
{
// Camera looking upward (pitch < 0)
const auto camera = make_camera({0, 0, 0}, -40.f, 0.f);
Targets targets = {
{100.f, 0.f, 0.f}, // on the horizon
{100.f, 0.f, 40.f}, // above, closer to where camera is looking
};
const auto result = find_closest(targets.cbegin(), targets.cend(), camera);
ASSERT_NE(result, targets.cend());
EXPECT_EQ(result, targets.cbegin() + 1);
}
TEST(unit_test_targeting, many_targets_picks_best)
{
const auto camera = make_camera({0, 0, 0}, 0.f, 0.f);
Targets targets = {
{100.f, 80.f, 80.f},
{100.f, 60.f, 60.f},
{100.f, 40.f, 40.f},
{100.f, 20.f, 20.f},
{100.f, 0.5f, 0.5f}, // closest to crosshair
{100.f, 10.f, 10.f},
{100.f, 30.f, 30.f},
};
const auto result = find_closest(targets.cbegin(), targets.cend(), camera);
ASSERT_NE(result, targets.cend());
EXPECT_EQ(result, targets.cbegin() + 4);
}
// ── get_closest_target_by_distance tests ────────────────────────────────────
TEST(unit_test_targeting, distance_returns_end_for_empty_range)
{
Targets targets;
EXPECT_EQ(find_nearest(targets.cbegin(), targets.cend(), {0, 0, 0}), targets.cend());
}
TEST(unit_test_targeting, distance_single_target)
{
Targets targets = {{50.f, 0.f, 0.f}};
EXPECT_EQ(find_nearest(targets.cbegin(), targets.cend(), {0, 0, 0}), targets.cbegin());
}
TEST(unit_test_targeting, distance_picks_nearest)
{
const omath::Vector3<float> origin{0.f, 0.f, 0.f};
Targets targets = {
{100.f, 0.f, 0.f}, // distance = 100
{10.f, 0.f, 0.f}, // distance = 10 (closest)
{50.f, 0.f, 0.f}, // distance = 50
};
const auto result = find_nearest(targets.cbegin(), targets.cend(), origin);
ASSERT_NE(result, targets.cend());
EXPECT_EQ(result, targets.cbegin() + 1);
}
TEST(unit_test_targeting, distance_considers_all_axes)
{
const omath::Vector3<float> origin{0.f, 0.f, 0.f};
Targets targets = {
{30.f, 30.f, 30.f}, // distance = sqrt(2700) ~ 51.96
{50.f, 0.f, 0.f}, // distance = 50
{0.f, 0.f, 10.f}, // distance = 10 (closest)
};
const auto result = find_nearest(targets.cbegin(), targets.cend(), origin);
ASSERT_NE(result, targets.cend());
EXPECT_EQ(result, targets.cbegin() + 2);
}
TEST(unit_test_targeting, distance_from_nonzero_origin)
{
const omath::Vector3<float> origin{100.f, 100.f, 100.f};
Targets targets = {
{0.f, 0.f, 0.f}, // distance = sqrt(30000) ~ 173
{105.f, 100.f, 100.f}, // distance = 5 (closest)
{200.f, 200.f, 200.f}, // distance = sqrt(30000) ~ 173
};
const auto result = find_nearest(targets.cbegin(), targets.cend(), origin);
ASSERT_NE(result, targets.cend());
EXPECT_EQ(result, targets.cbegin() + 1);
}
TEST(unit_test_targeting, distance_equidistant_returns_first)
{
const omath::Vector3<float> origin{0.f, 0.f, 0.f};
// Both targets at distance 100, symmetric
Targets targets = {
{100.f, 0.f, 0.f},
{-100.f, 0.f, 0.f},
};
const auto result = find_nearest(targets.cbegin(), targets.cend(), origin);
ASSERT_NE(result, targets.cend());
EXPECT_EQ(result, targets.cbegin());
}
TEST(unit_test_targeting, distance_many_targets)
{
const omath::Vector3<float> origin{0.f, 0.f, 0.f};
Targets targets = {
{500.f, 0.f, 0.f},
{200.f, 200.f, 0.f},
{100.f, 100.f, 100.f},
{50.f, 50.f, 50.f},
{1.f, 1.f, 1.f}, // distance = sqrt(3) ~ 1.73 (closest)
{10.f, 10.f, 10.f},
{80.f, 0.f, 0.f},
};
const auto result = find_nearest(targets.cbegin(), targets.cend(), origin);
ASSERT_NE(result, targets.cend());
EXPECT_EQ(result, targets.cbegin() + 4);
}

View File

@@ -31,7 +31,11 @@
"dependencies": [ "dependencies": [
"glfw3", "glfw3",
"glew", "glew",
"opengl" "opengl",
{
"name": "imgui",
"features": ["glfw-binding", "opengl3-binding"]
}
] ]
}, },
"imgui": { "imgui": {