diff --git a/docs/linear_algebra/mat.md b/docs/linear_algebra/mat.md index b54cc3b..8d1efd6 100644 --- a/docs/linear_algebra/mat.md +++ b/docs/linear_algebra/mat.md @@ -1,4 +1,4 @@ -# `omath::Mat` +# `omath::Mat` — Matrix class (C++20/23) > Header: your project’s `mat.hpp` (requires `vector3.hpp`) > Namespace: `omath` diff --git a/docs/pathfinding/a_star.md b/docs/pathfinding/a_star.md new file mode 100644 index 0000000..df44820 --- /dev/null +++ b/docs/pathfinding/a_star.md @@ -0,0 +1,188 @@ +# `omath::pathfinding::Astar` — Pathfinding over a navigation mesh + +> Header: your project’s `pathfinding/astar.hpp` +> Namespace: `omath::pathfinding` +> Inputs: start/end as `Vector3`, a `NavigationMesh` +> Output: ordered list of waypoints `std::vector>` + +`Astar` exposes a single public function, `find_path`, that computes a path of 3D waypoints on a navigation mesh. Internally it reconstructs the result with `reconstruct_final_path` from a closed set keyed by `Vector3`. + +--- + +## API + +```cpp +namespace omath::pathfinding { + +struct PathNode; // holds per-node search data (see "Expected PathNode fields") + +class Astar final { +public: + [[nodiscard]] + static std::vector> + find_path(const Vector3& start, + const Vector3& end, + const NavigationMesh& nav_mesh) noexcept; + +private: + [[nodiscard]] + static std::vector> + reconstruct_final_path( + const std::unordered_map, PathNode>& closed_list, + const Vector3& current) noexcept; +}; + +} // namespace omath::pathfinding +``` + +### Semantics + +* Returns a **polyline** of 3D points from `start` to `end`. +* If no path exists, the function typically returns an **empty vector** (behavior depends on implementation details; keep this contract in unit tests). + +--- + +## What `NavigationMesh` is expected to provide + +The header doesn’t constrain `NavigationMesh`, but for A* it commonly needs: + +* **Neighborhood queries**: given a position or node key → iterable neighbors. +* **Traversal cost**: `g(u,v)` (often Euclidean distance or edge weight). +* **Heuristic**: `h(x,end)` (commonly straight-line distance on the mesh). +* **Projection / snap**: the ability to map `start`/`end` to valid nodes/points on the mesh (if they are off-mesh). + +> If your `NavigationMesh` doesn’t directly expose these, `Astar::find_path` likely does the adapter work (snapping to the nearest convex polygon/portal nodes and expanding across adjacency). + +--- + +## Expected `PathNode` fields + +Although not visible here, `PathNode` typically carries: + +* `Vector3 parent;` — predecessor position or key for backtracking +* `float g;` — cost from `start` +* `float h;` — heuristic to `end` +* `float f;` — `g + h` + +`reconstruct_final_path(closed_list, current)` walks `parent` links from `current` back to the start, **reverses** the chain, and returns the path. + +--- + +## Heuristic & optimality + +* Use an **admissible** heuristic (never overestimates true cost) to keep A* optimal. + The usual choice is **Euclidean distance** in 3D: + + ```cpp + h(x, goal) = (goal - x).length(); + ``` +* For best performance, make it **consistent** (triangle inequality holds). Euclidean distance is consistent on standard navmeshes. + +--- + +## Complexity + +Let `V` be explored vertices (or portal nodes) and `E` the traversed edges. + +* With a binary heap open list: **O(E log V)** time, **O(V)** memory. +* With a d-ary heap or pairing heap you may reduce practical constants. + +--- + +## Typical usage + +```cpp +#include "omath/pathfinding/astar.hpp" +#include "omath/pathfinding/navigation_mesh.hpp" + +using omath::Vector3; +using omath::pathfinding::Astar; + +NavigationMesh nav = /* ... load/build mesh ... */; + +Vector3 start{2.5f, 0.0f, -1.0f}; +Vector3 goal {40.0f, 0.0f, 12.0f}; + +auto path = Astar::find_path(start, goal, nav); + +if (!path.empty()) { + // feed to your agent/renderer + for (const auto& p : path) { + // draw waypoint p or push to steering + } +} else { + // handle "no path" (e.g., unreachable or disconnected mesh) +} +``` + +--- + +## Notes & recommendations + +* **Start/end snapping**: If `start` or `end` are outside the mesh, decide whether to snap to the nearest polygon/portal or fail early. Keep this behavior consistent and document it where `NavigationMesh` is defined. +* **Numerical stability**: Prefer squared distances when only comparing (`dist2`) to avoid unnecessary `sqrt`. +* **Tie-breaking**: When `f` ties are common (grid-like graphs), bias toward larger `g` or smaller `h` to reduce zig-zagging. +* **Smoothing**: A* returns a polyline that may hug polygon edges. Consider: + + * **String pulling / Funnel algorithm** over the corridor of polygons to get a straightened path. + * **Raycast smoothing** (visibility checks) to remove redundant interior points. +* **Hashing `Vector3`**: Your repo defines `std::hash>`. Ensure equality/precision rules for using float keys are acceptable (or use discrete node IDs instead). + +--- + +## Testing checklist + +* Start/end on the **same polygon** → direct path of 2 points. +* **Disconnected components** → empty result. +* **Narrow corridors** → path stays inside. +* **Obstacles blocking** → no path. +* **Floating-point noise** → still reconstructs a valid chain from parents. + +--- + +## Minimal pseudo-implementation outline (for reference) + +```cpp +// Pseudocode only — matches the header’s intent +std::vector find_path(start, goal, mesh) { + auto [snode, gnode] = mesh.snap_to_nodes(start, goal); + OpenSet open; // min-heap by f + std::unordered_map closed; + + open.push({snode, g=0, h=heuristic(snode, gnode)}); + parents.clear(); + + while (!open.empty()) { + auto current = open.pop_min(); // node with lowest f + + if (current.pos == gnode.pos) + return reconstruct_final_path(closed, current.pos); + + for (auto [nbr, cost] : mesh.neighbors(current.pos)) { + float tentative_g = current.g + cost; + if (auto it = closed.find(nbr); it == closed.end() || tentative_g < it->second.g) { + closed[nbr] = { .parent = current.pos, + .g = tentative_g, + .h = heuristic(nbr, gnode.pos), + .f = tentative_g + heuristic(nbr, gnode.pos) }; + open.push(closed[nbr]); + } + } + } + return {}; // no path +} +``` + +--- + +## FAQ + +* **Why return `std::vector>` and not polygon IDs?** + Waypoints are directly usable by agents/steering and for rendering. If you also need the corridor (polygon chain), extend the API or `PathNode` to store it. + +* **Does `find_path` modify the mesh?** + No; it should be a read-only search over `NavigationMesh`. + +--- + +*Last updated: 31 Oct 2025* diff --git a/docs/pathfinding/navigation_mesh.md b/docs/pathfinding/navigation_mesh.md new file mode 100644 index 0000000..a03f6c5 --- /dev/null +++ b/docs/pathfinding/navigation_mesh.md @@ -0,0 +1,113 @@ +# `omath::pathfinding::NavigationMesh` — Lightweight vertex graph for A* + +> Header: your project’s `pathfinding/navigation_mesh.hpp` +> Namespace: `omath::pathfinding` +> Nodes: `Vector3` (3D points) +> Storage: adjacency map `unordered_map, std::vector>>` + +A minimal navigation mesh represented as a **vertex/edge graph**. Each vertex is a `Vector3` and neighbors are stored in an adjacency list. Designed to pair with `Astar::find_path`. + +--- + +## API + +```cpp +class NavigationMesh final { +public: + // Nearest graph vertex to an arbitrary 3D point. + // On success -> closest vertex; on failure -> std::string error (e.g., empty mesh). + [[nodiscard]] + std::expected, std::string> + get_closest_vertex(const Vector3& point) const noexcept; + + // Read-only neighbor list for a vertex key. + // If vertex is absent, implementation should return an empty list (see notes). + [[nodiscard]] + const std::vector>& + get_neighbors(const Vector3& vertex) const noexcept; + + // True if the graph has no vertices/edges. + [[nodiscard]] + bool empty() const; + + // Serialize/deserialize the graph (opaque binary). + [[nodiscard]] std::vector serialize() const noexcept; + void deserialize(const std::vector& raw) noexcept; + + // Public adjacency (vertex -> neighbors) + std::unordered_map, std::vector>> m_vertex_map; +}; +``` + +--- + +## Quick start + +```cpp +using omath::Vector3; +using omath::pathfinding::NavigationMesh; + +// Build a tiny mesh (triangle) +NavigationMesh nav; +nav.m_vertex_map[ {0,0,0} ] = { {1,0,0}, {0,0,1} }; +nav.m_vertex_map[ {1,0,0} ] = { {0,0,0}, {0,0,1} }; +nav.m_vertex_map[ {0,0,1} ] = { {0,0,0}, {1,0,0} }; + +// Query the closest node to an arbitrary point +auto q = nav.get_closest_vertex({0.3f, 0.0f, 0.2f}); +if (q) { + const auto& v = *q; + const auto& nbrs = nav.get_neighbors(v); + (void)nbrs; +} +``` + +--- + +## Semantics & expectations + +* **Nearest vertex** + `get_closest_vertex(p)` should scan known vertices and return the one with minimal Euclidean distance to `p`. If the mesh is empty, expect an error (`unexpected` with a message). + +* **Neighbors** + `get_neighbors(v)` returns the adjacency list for `v`. If `v` is not present, a conventional behavior is to return a **reference to a static empty vector** (since the API is `noexcept` and returns by reference). Verify in your implementation. + +* **Graph invariants** (recommended) + + * Neighbor links are **symmetric** for undirected navigation (if `u` has `v`, then `v` has `u`). + * No self-loops unless explicitly desired. + * Vertices are unique keys; hashing uses `std::hash>` (be mindful of floating-point equality). + +--- + +## Serialization + +* `serialize()` → opaque, implementation-defined binary of the current `m_vertex_map`. +* `deserialize(raw)` → restores the internal map from `raw`. + Keep versioning in mind if you evolve the format (e.g., add a header/magic/version). + +--- + +## Performance + +Let `V = m_vertex_map.size()` and `E = Σ|neighbors(v)|`. + +* `get_closest_vertex`: **O(V)** (linear scan) unless you back it with a spatial index (KD-tree, grid, etc.). +* `get_neighbors`: **O(1)** average (hash lookup). +* Memory: **O(V + E)**. + +--- + +## Usage notes + +* **Floating-point keys**: Using `Vector3` as an unordered_map key relies on your `std::hash>` and exact `operator==`. Avoid building meshes with numerically “close but not equal” duplicates; consider canonicalizing or using integer IDs if needed. +* **Pathfinding**: Pair with `Astar::find_path(start, end, nav)`; the A* heuristic can use straight-line distance between vertex positions. + +--- + +## Minimal test ideas + +* Empty mesh → `get_closest_vertex` returns error; `empty() == true`. +* Single vertex → nearest always that vertex; neighbors empty. +* Symmetric edges → `get_neighbors(a)` contains `b` and vice versa. +* Serialization round-trip preserves vertex/edge counts and neighbor lists. diff --git a/docs/projectile_prediction/proj_pred_engine_avx2.md b/docs/projectile_prediction/proj_pred_engine_avx2.md new file mode 100644 index 0000000..56969f7 --- /dev/null +++ b/docs/projectile_prediction/proj_pred_engine_avx2.md @@ -0,0 +1,161 @@ +# `omath::projectile_prediction::ProjPredEngineAvx2` — AVX2-accelerated ballistic aim solver + +> Header: your project’s `projectile_prediction/proj_pred_engine_avx2.hpp` +> Namespace: `omath::projectile_prediction` +> Inherits: `ProjPredEngineInterface` +> Depends on: `Vector3`, `Projectile`, `Target` +> CPU: Uses AVX2 when available; falls back to scalar elsewhere (fields are marked `[[maybe_unused]]` for non-x86/AVX2 builds). + +This engine computes a **world-space aim point** (and implicitly the firing **yaw/pitch**) to intersect a moving target under a **constant gravity** model and **constant muzzle speed**. It typically scans candidate times of flight and solves for the elevation (`pitch`) that makes the vertical and horizontal kinematics meet at the same time. + +--- + +## API + +```cpp +class ProjPredEngineAvx2 final : public ProjPredEngineInterface { +public: + [[nodiscard]] + std::optional> + maybe_calculate_aim_point(const Projectile& projectile, + const Target& target) const override; + + ProjPredEngineAvx2(float gravity_constant, + float simulation_time_step, + float maximum_simulation_time); + ~ProjPredEngineAvx2() override = default; + +private: + // Solve for pitch at a fixed time-of-flight t. + [[nodiscard]] + static std::optional + calculate_pitch(const Vector3& proj_origin, + const Vector3& target_pos, + float bullet_gravity, float v0, float time); + + // Tunables (may be unused on non-AVX2 builds) + [[maybe_unused]] const float m_gravity_constant; // |g| (e.g., 9.81) + [[maybe_unused]] const float m_simulation_time_step; // Δt (e.g., 1/240 s) + [[maybe_unused]] const float m_maximum_simulation_time; // Tmax (e.g., 3 s) +}; +``` + +### Parameters (constructor) + +* `gravity_constant` — magnitude of gravity (units consistent with your world, e.g., **m/s²**). +* `simulation_time_step` — Δt used to scan candidate intercept times. +* `maximum_simulation_time` — cap on time of flight; larger allows longer-range solutions but increases cost. + +### Return (solver) + +* `maybe_calculate_aim_point(...)` + + * **`Vector3`**: a world-space **aim point** yielding an intercept under the model. + * **`std::nullopt`**: no feasible solution (e.g., target receding too fast, out of range, or kinematics inconsistent). + +--- + +## How it solves (expected flow) + +1. **Predict target at time `t`** (constant-velocity model unless your `Target` carries more): + + ``` + T(t) = target.position + target.velocity * t + ``` +2. **Horizontal/vertical kinematics at fixed `t`** with muzzle speed `v0` and gravity `g`: + + * Let `Δ = T(t) - proj_origin`, `d = length(Δ.xz)`, `h = Δ.y`. + * Required initial components: + + ``` + cosθ = d / (v0 * t) + sinθ = (h + 0.5 * g * t^2) / (v0 * t) + ``` + * If `cosθ` ∈ [−1,1] and `sinθ` ∈ [−1,1] and `sin²θ + cos²θ ≈ 1`, then + + ``` + θ = atan2(sinθ, cosθ) + ``` + + That is what `calculate_pitch(...)` returns on success. +3. **Yaw** is the azimuth toward `Δ.xz`. +4. **Pick the earliest feasible `t`** in `[Δt, Tmax]` (scanned in steps of `Δt`; AVX2 batches several `t` at once). +5. **Return the aim point.** Common choices: + + * The **impact point** `T(t*)` (useful as a HUD marker), or + * A point along the **initial firing ray** at some convenient range using `(yaw, pitch)`; both are consistent—pick the convention your caller expects. + +> The private `calculate_pitch(...)` matches step **2** and returns `nullopt` if the trigonometric constraints are violated for that `t`. + +--- + +## AVX2 notes + +* On x86/x64 with AVX2, candidate times `t` can be evaluated **8 at a time** using FMA (great for dense scans). +* On ARM/ARM64 (no AVX2), code falls back to scalar math; the `[[maybe_unused]]` members acknowledge compilation without SIMD. + +--- + +## Usage example + +```cpp +using namespace omath::projectile_prediction; + +ProjPredEngineAvx2 solver( + /*gravity*/ 9.81f, + /*dt*/ 1.0f/240.0f, + /*Tmax*/ 3.0f +); + +Projectile proj; // fill: origin, muzzle_speed, etc. +Target tgt; // fill: position, velocity + +if (auto aim = solver.maybe_calculate_aim_point(proj, tgt)) { + // Aim your weapon at *aim and fire with muzzle speed proj.v0 + // If you need yaw/pitch explicitly, replicate the pitch solve and azimuth. +} else { + // No solution (out of envelope) — pick a fallback +} +``` + +--- + +## Edge cases & failure modes + +* **Zero or tiny `v0`** → no solution. +* **Target collinear & receding faster than `v0`** → no solution. +* **`t` constraints**: if viable solutions exist only beyond `Tmax`, you’ll get `nullopt`. +* **Geometric infeasibility** at a given `t` (e.g., `d > v0*t`) causes `calculate_pitch` to fail that sample. +* **Numerical tolerance**: check `sin²θ + cos²θ` against 1 with a small epsilon (e.g., `1e-3`). + +--- + +## Performance & tuning + +* Work is roughly `O(Nt)` where `Nt ≈ Tmax / Δt`. +* Smaller `Δt` → better accuracy, higher cost. With AVX2 you can afford smaller steps. +* If you frequently miss solutions **between** steps, consider: + + * **Coarse-to-fine**: coarse scan, then local refine around the best `t`. + * **Newton on time**: root-find `‖horizontal‖ − v0 t cosθ(t) = 0` shaped from the kinematics. + +--- + +## Testing checklist + +* **Stationary target** at same height → θ ≈ 0, aim point ≈ target. +* **Higher target** → positive pitch; **lower target** → negative pitch. +* **Perpendicular moving target** → feasible at moderate speeds. +* **Very fast receding target** → `nullopt`. +* **Boundary**: `d ≈ v0*Tmax` and `h` large → verify pass/fail around thresholds. + +--- + +## See also + +* `ProjPredEngineInterface` — base interface and general contract +* `Projectile`, `Target` — data carriers for solver inputs (speed, origin, position, velocity, etc.) + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projectile_prediction/proj_pred_engine_legacy.md b/docs/projectile_prediction/proj_pred_engine_legacy.md new file mode 100644 index 0000000..fca8b17 --- /dev/null +++ b/docs/projectile_prediction/proj_pred_engine_legacy.md @@ -0,0 +1,184 @@ +# `omath::projectile_prediction::ProjPredEngineLegacy` — Legacy trait-based aim solver + +> Header: `omath/projectile_prediction/proj_pred_engine_legacy.hpp` +> Namespace: `omath::projectile_prediction` +> Inherits: `ProjPredEngineInterface` +> Template param (default): `EngineTrait = source_engine::PredEngineTrait` +> Purpose: compute a world-space **aim point** to hit a (possibly moving) target using a **discrete time scan** and a **closed-form ballistic pitch** under constant gravity. + +--- + +## Overview + +`ProjPredEngineLegacy` is a portable, trait-driven projectile lead solver. At each simulation time step `t` it: + +1. **Predicts target position** with `EngineTrait::predict_target_position(target, t, g)`. +2. **Computes launch pitch** via a gravity-aware closed form (or a direct angle if gravity is zero). +3. **Validates** that a projectile fired with that pitch (and direct yaw) actually reaches the predicted target within a **distance tolerance** at time `t`. +4. On success, **returns an aim point** computed by `EngineTrait::calc_viewpoint_from_angles(...)`. + +If no time step yields a feasible solution up to `maximum_simulation_time`, returns `std::nullopt`. + +--- + +## API + +```cpp +template +requires PredEngineConcept +class ProjPredEngineLegacy final : public ProjPredEngineInterface { +public: + ProjPredEngineLegacy(float gravity_constant, + float simulation_time_step, + float maximum_simulation_time, + float distance_tolerance); + + [[nodiscard]] + std::optional> + maybe_calculate_aim_point(const Projectile& projectile, + const Target& target) const override; + +private: + // Closed-form ballistic pitch solver (internal) + std::optional + maybe_calculate_projectile_launch_pitch_angle(const Projectile& projectile, + const Vector3& target_position) const noexcept; + + bool is_projectile_reached_target(const Vector3& target_position, + const Projectile& projectile, + float pitch, float time) const noexcept; + + const float m_gravity_constant; + const float m_simulation_time_step; + const float m_maximum_simulation_time; + const float m_distance_tolerance; +}; +``` + +### Constructor parameters + +* `gravity_constant` — magnitude of gravity (e.g., `9.81f`), world units/s². +* `simulation_time_step` — Δt for the scan (e.g., `1/240.f`). +* `maximum_simulation_time` — search horizon in seconds. +* `distance_tolerance` — max allowed miss distance at time `t` to accept a solution. + +--- + +## Trait requirements (`PredEngineConcept`) + +Your `EngineTrait` must expose **noexcept** static methods with these signatures: + +```cpp +Vector3 predict_projectile_position(const Projectile&, float pitch_deg, float yaw_deg, + float time, float gravity) noexcept; + +Vector3 predict_target_position(const Target&, float time, float gravity) noexcept; + +float calc_vector_2d_distance(const Vector3& v) noexcept; // typically length in XZ plane +float get_vector_height_coordinate(const Vector3& v) noexcept; // typically Y + +Vector3 calc_viewpoint_from_angles(const Projectile&, Vector3 target, + std::optional maybe_pitch_deg) noexcept; + +float calc_direct_pitch_angle(const Vector3& from, const Vector3& to) noexcept; +float calc_direct_yaw_angle (const Vector3& from, const Vector3& to) noexcept; +``` + +> This design lets you adapt different game/physics conventions (axes, units, handedness) without changing the solver. + +--- + +## Algorithm details + +### Time scan + +For `t = 0 .. maximum_simulation_time` in steps of `simulation_time_step`: + +1. `T = EngineTrait::predict_target_position(target, t, g)` +2. `pitch = maybe_calculate_projectile_launch_pitch_angle(projectile, T)` + + * If `std::nullopt`: continue +3. `yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, T)` +4. `P = EngineTrait::predict_projectile_position(projectile, pitch, yaw, t, g)` +5. Accept if `|P - T| <= distance_tolerance` +6. Return `EngineTrait::calc_viewpoint_from_angles(projectile, T, pitch)` + +### Closed-form pitch (gravity on) + +Implements the classic ballistic formula (low-arc branch), where: + +* `v` = muzzle speed, +* `g` = `gravity_constant * projectile.m_gravity_scale`, +* `x` = horizontal (2D) distance to target, +* `y` = vertical offset to target. + +[ +\theta ;=; \arctan!\left(\frac{v^{2} ;-; \sqrt{v^{4}-g!\left(gx^{2}+2yv^{2}\right)}}{gx}\right) +] + +* If the **discriminant** ( v^{4}-g(gx^{2}+2yv^{2}) < 0 ) ⇒ **no real solution**. +* If `g == 0`, falls back to `EngineTrait::calc_direct_pitch_angle(...)`. +* Returns **degrees** (internally converts from radians). + +--- + +## Usage example + +```cpp +using namespace omath::projectile_prediction; + +ProjPredEngineLegacy solver( + /*gravity*/ 9.81f, + /*dt*/ 1.f / 240.f, + /*Tmax*/ 3.0f, + /*tol*/ 0.05f +); + +Projectile proj; // fill: m_origin, m_launch_speed, m_gravity_scale, etc. +Target tgt; // fill: position/velocity as required by your trait + +if (auto aim = solver.maybe_calculate_aim_point(proj, tgt)) { + // Drive your turret/reticle toward *aim +} else { + // No feasible intercept in the given horizon +} +``` + +--- + +## Behavior & edge cases + +* **Zero gravity or zero distance**: uses direct pitch toward the target. +* **Negative discriminant** in the pitch formula: returns `std::nullopt` for that time step. +* **Very small `x`** (horizontal distance): the formula’s denominator `gx` approaches zero; your trait’s direct pitch helper provides a stable fallback. +* **Tolerance**: `distance_tolerance` controls acceptance; tighten for accuracy, loosen for robustness. + +--- + +## Complexity & tuning + +* Time: **O(T)** where ( T \approx \frac{\text{maximum_simulation_time}}{\text{simulation_time_step}} ) + plus trait costs for prediction and angle math per step. +* Smaller `simulation_time_step` improves precision but increases runtime. +* If needed, do a **coarse-to-fine** search: coarse Δt scan, then refine around the best hit time. + +--- + +## Testing checklist + +* Stationary, level target → pitch ≈ 0 for short ranges; accepted within tolerance. +* Elevated/depressed targets → pitch positive/negative as expected. +* Receding fast target → unsolved within horizon ⇒ `nullopt`. +* Gravity scale = 0 → identical to straight-line solution. +* Near-horizon shots (large range, small arc) → discriminant near zero; verify stability. + +--- + +## Notes + +* All angles produced/consumed by the trait in this implementation are **degrees**. +* `calc_viewpoint_from_angles` defines what “aim point” means in your engine (e.g., a point along the initial ray or the predicted impact point). Keep this consistent with your HUD/reticle. + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projectile_prediction/projectile.md b/docs/projectile_prediction/projectile.md new file mode 100644 index 0000000..a741a88 --- /dev/null +++ b/docs/projectile_prediction/projectile.md @@ -0,0 +1,96 @@ +# `omath::projectile_prediction::Projectile` — Projectile parameters for aim solvers + +> Header: `omath/projectile_prediction/projectile.hpp` +> Namespace: `omath::projectile_prediction` +> Used by: `ProjPredEngineInterface` implementations (e.g., `ProjPredEngineLegacy`, `ProjPredEngineAvx2`) + +`Projectile` is a tiny data holder that describes how a projectile is launched: **origin** (world position), **launch speed**, and a **gravity scale** (multiplier applied to the engine’s gravity constant). + +--- + +## API + +```cpp +namespace omath::projectile_prediction { + +class Projectile final { +public: + Vector3 m_origin; // Launch position (world space) + float m_launch_speed{}; // Initial speed magnitude (units/sec) + float m_gravity_scale{}; // Multiplier for global gravity (dimensionless) +}; + +} // namespace omath::projectile_prediction +``` + +--- + +## Field semantics + +* **`m_origin`** + World-space position where the projectile is spawned (e.g., muzzle or emitter point). + +* **`m_launch_speed`** + Initial speed **magnitude** in your world units per second. Direction is determined by the solver (from yaw/pitch). + + * Must be **non-negative**. Zero disables meaningful ballistic solutions. + +* **`m_gravity_scale`** + Multiplies the engine’s gravity constant provided to the solver (e.g., `g = gravity_constant * m_gravity_scale`). + + * Use `1.0f` for normal gravity, `0.0f` for no-drop projectiles, other values to simulate heavier/lighter rounds. + +> Units must be consistent across your project (e.g., meters & seconds). If `gravity_constant = 9.81f`, then `m_launch_speed` is in m/s and positions are in meters. + +--- + +## Typical usage + +```cpp +using namespace omath::projectile_prediction; + +Projectile proj; +proj.m_origin = { 0.0f, 1.6f, 0.0f }; // player eye / muzzle height +proj.m_launch_speed = 850.0f; // e.g., 850 m/s +proj.m_gravity_scale = 1.0f; // normal gravity + +// With an aim solver: +auto aim = engine->maybe_calculate_aim_point(proj, target); +if (aim) { + // rotate/aim toward *aim and fire +} +``` + +--- + +## With gravity-aware solver (outline) + +Engines typically compute the firing angles to reach a predicted target position: + +* Horizontal distance `x` and vertical offset `y` are derived from `target - m_origin`. +* Gravity used is `g = gravity_constant * m_gravity_scale`. +* Launch direction has speed `m_launch_speed` and angles solved by the engine. + +If `m_gravity_scale == 0`, engines usually fall back to straight-line (no-drop) solutions. + +--- + +## Validation & tips + +* Keep `m_launch_speed ≥ 0`. Negative values are nonsensical. +* If your weapon can vary muzzle speed (charge-up, attachments), update `m_launch_speed` per shot. +* For different ammo types (tracers, grenades), prefer tweaking **`m_gravity_scale`** (and possibly the engine’s gravity constant) to match observed arc. + +--- + +## See also + +* `ProjPredEngineInterface` — common interface for aim solvers +* `ProjPredEngineLegacy` — trait-based, time-stepped ballistic solver +* `ProjPredEngineAvx2` — AVX2-accelerated solver with fixed-time pitch solve +* `Target` — target state consumed by the solvers +* `Vector3` — math type used for positions and directions + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projectile_prediction/projectile_engine.md b/docs/projectile_prediction/projectile_engine.md new file mode 100644 index 0000000..db0cf33 --- /dev/null +++ b/docs/projectile_prediction/projectile_engine.md @@ -0,0 +1,152 @@ +# `omath::projectile_prediction::ProjPredEngineInterface` — Aim-point solver interface + +> Header: your project’s `projectile_prediction/proj_pred_engine_interface.hpp` +> Namespace: `omath::projectile_prediction` +> Depends on: `Vector3`, `Projectile`, `Target` +> Purpose: **contract** for engines that compute a lead/aim point to hit a moving target. + +--- + +## Overview + +`ProjPredEngineInterface` defines a single pure-virtual method that attempts to compute the **world-space aim point** where a projectile should be launched to intersect a target under the engine’s physical model (e.g., constant projectile speed, gravity, drag, max flight time, etc.). + +If a valid solution exists, the engine returns the 3D aim point. Otherwise, it returns `std::nullopt` (no feasible intercept). + +--- + +## API + +```cpp +namespace omath::projectile_prediction { + +class ProjPredEngineInterface { +public: + [[nodiscard]] + virtual std::optional> + maybe_calculate_aim_point(const Projectile& projectile, + const Target& target) const = 0; + + virtual ~ProjPredEngineInterface() = default; +}; + +} // namespace omath::projectile_prediction +``` + +### Semantics + +* **Input** + + * `Projectile` — engine-specific projectile properties (typical: muzzle speed, gravity vector, drag flag/coeff, max range / flight time). + * `Target` — target state (typical: position, velocity, possibly acceleration). + +* **Output** + + * `std::optional>` + + * `value()` — world-space point to aim at **now** so that the projectile intersects the target under the model. + * `std::nullopt` — no solution (e.g., target outruns projectile, blocked by constraints, numerical failure). + +* **No side effects**: method is `const` and should not modify inputs. + +--- + +## Typical usage + +```cpp +using namespace omath::projectile_prediction; + +std::unique_ptr engine = /* your implementation */; +Projectile proj = /* fill from weapon config */; +Target tgt = /* read from tracking system */; + +if (auto aim = engine->maybe_calculate_aim_point(proj, tgt)) { + // Rotate/steer to (*aim) +} else { + // Fall back: no-lead, predictive UI, or do not fire +} +``` + +--- + +## Implementation guidance (for engine authors) + +**Common models:** + +1. **No gravity, constant speed** + Closed form intersect time `t` solves `‖p_t + v_t t − p_0‖ = v_p t`. + Choose the smallest non-negative real root; aim point = `p_t + v_t t`. + +2. **Gravity (constant g), constant speed** + Solve ballistics with vertical drop: either numerical (Newton–Raphson on time) or 2D elevation + azimuth decomposition. Ensure convergence caps and time bounds. + +3. **Drag** + Typically requires numeric integration (e.g., RK4) wrapped in a root find on time-of-flight. + +**Robustness tips:** + +* **Feasibility checks:** return `nullopt` when: + + * projectile speed ≤ 0; target too fast in receding direction; solution time outside `[0, t_max]`. +* **Bounds:** clamp search time to reasonable `[t_min, t_max]` (e.g., `[0, max_flight_time]` or by range). +* **Tolerances:** use epsilons for convergence (e.g., `|f(t)| < 1e-4`, `|Δt| < 1e-4 s`). +* **Determinism:** fix iteration counts or seeds if needed for replayability. + +--- + +## Example: constant-speed, no-gravity intercept (closed form) + +```cpp +// Solve ||p + v t|| = s t where p = target_pos - shooter_pos, v = target_vel, s = projectile_speed +// Quadratic: (v·v - s^2) t^2 + 2 (p·v) t + (p·p) = 0 +inline std::optional intercept_time_no_gravity(const Vector3& p, + const Vector3& v, + float s) { + const float a = v.dot(v) - s*s; + const float b = 2.f * p.dot(v); + const float c = p.dot(p); + if (std::abs(a) < 1e-6f) { // near linear + if (std::abs(b) < 1e-6f) return std::nullopt; + float t = -c / b; + return t >= 0.f ? std::optional{t} : std::nullopt; + } + const float disc = b*b - 4.f*a*c; + if (disc < 0.f) return std::nullopt; + const float sqrtD = std::sqrt(disc); + float t1 = (-b - sqrtD) / (2.f*a); + float t2 = (-b + sqrtD) / (2.f*a); + float t = (t1 >= 0.f ? t1 : t2); + return t >= 0.f ? std::optional{t} : std::nullopt; +} +``` + +Aim point (given shooter origin `S`, target pos `T`, vel `V`): + +``` +p = T - S +t* = intercept_time_no_gravity(p, V, speed) +aim = T + V * t* +``` + +Return `nullopt` if `t*` is absent. + +--- + +## Testing checklist + +* **Stationary target**: aim point equals target position when `s > 0`. +* **Target perpendicular motion**: lead equals lateral displacement `V⊥ * t`. +* **Receding too fast**: expect `nullopt`. +* **Gravity model**: verify arc solutions exist for short & long trajectories (if implemented). +* **Numerics**: convergence within max iterations; monotonic improvement of residuals. + +--- + +## Notes + +* This is an **interface** only; concrete engines (e.g., `SimpleNoGravityEngine`, `BallisticGravityEngine`) should document their assumptions (gravity, drag, wind, bounds) and units (meters, seconds). +* The coordinate system and handedness should be consistent with `Vector3` and the rest of your math stack. + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projectile_prediction/target.md b/docs/projectile_prediction/target.md new file mode 100644 index 0000000..4594463 --- /dev/null +++ b/docs/projectile_prediction/target.md @@ -0,0 +1,70 @@ +# `omath::projectile_prediction::Target` — Target state for aim solvers + +> Header: `omath/projectile_prediction/target.hpp` +> Namespace: `omath::projectile_prediction` +> Used by: `ProjPredEngineInterface` implementations (e.g., Legacy/AVX2 engines) + +A small POD-style container describing a target’s **current pose** and **motion** for projectile lead/aim computations. + +--- + +## API + +```cpp +namespace omath::projectile_prediction { + +class Target final { +public: + Vector3 m_origin; // Current world-space position of the target + Vector3 m_velocity; // World-space linear velocity (units/sec) + bool m_is_airborne{}; // Domain hint (e.g., ignore ground snapping) +}; + +} // namespace omath::projectile_prediction +``` + +--- + +## Field semantics + +* **`m_origin`** — target position in world coordinates (same units as your `Vector3` grid). +* **`m_velocity`** — instantaneous linear velocity. Solvers commonly assume **constant velocity** between “now” and impact unless your trait injects gravity/accel. +* **`m_is_airborne`** — optional hint for engine/trait logic (e.g., apply gravity to the target, skip ground friction/snap). Exact meaning is engine-dependent. + +> Keep units consistent with your projectile model (e.g., meters & seconds). If projectiles use `g = 9.81 m/s²`, velocity should be in m/s and positions in meters. + +--- + +## Typical usage + +```cpp +using namespace omath::projectile_prediction; + +Target tgt; +tgt.m_origin = { 42.0f, 1.8f, -7.5f }; +tgt.m_velocity = { 3.0f, 0.0f, 0.0f }; // moving +X at 3 units/s +tgt.m_is_airborne = false; + +// Feed into an aim solver with a Projectile +auto aim = engine->maybe_calculate_aim_point(projectile, tgt); +``` + +--- + +## Notes & tips + +* If you track acceleration (e.g., gravity on ragdolls), your **EngineTrait** may derive it from `m_is_airborne` and world gravity; otherwise most solvers treat the target’s motion as linear. +* For highly agile targets, refresh `m_origin`/`m_velocity` every tick and re-solve; don’t reuse stale aim points. +* Precision: `Vector3` is typically enough; if you need sub-millimeter accuracy over long ranges, consider double-precision internally in your trait. + +--- + +## See also + +* `Projectile` — shooter origin, muzzle speed, gravity scale +* `ProjPredEngineInterface` — common interface for aim solvers +* `ProjPredEngineLegacy`, `ProjPredEngineAvx2` — concrete solvers using this data + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projection/camera.md b/docs/projection/camera.md new file mode 100644 index 0000000..b9cf47b --- /dev/null +++ b/docs/projection/camera.md @@ -0,0 +1,261 @@ +# `omath::projection::Camera` — Generic, trait-driven camera with screen/world conversion + +> Header: `omath/projection/camera.hpp` (this header) +> Namespace: `omath::projection` +> Template: `Camera` +> Requires: `CameraEngineConcept` +> Key features: **lazy view-projection caching**, world↔screen helpers, pluggable math via a **Trait** + +--- + +## Overview + +`Camera` is a small, zero-allocation camera wrapper. It delegates the math for **view**, **projection**, and **look-at** to a **Trait** (`TraitClass`), which lets you plug in different coordinate systems or conventions without changing the camera code. The class caches the **View×Projection** matrix and invalidates it when any parameter changes. + +Alongside the camera, the header defines: + +* `struct ViewPort { float m_width, m_height; float aspect_ratio() const; }` +* `using FieldOfView = Angle;` + +--- + +## Template & trait requirements + +```cpp +template +concept CameraEngineConcept = requires( + const omath::Vector3& cam_origin, + const omath::Vector3& look_at, + const ViewAnglesType& angles, + const omath::projection::FieldOfView& fov, + const omath::projection::ViewPort& viewport, + float znear, float zfar +) { + { T::calc_look_at_angle(cam_origin, look_at) } noexcept -> std::same_as; + { T::calc_view_matrix(angles, cam_origin) } noexcept -> std::same_as; + { T::calc_projection_matrix(fov, viewport, znear, zfar)}noexcept -> std::same_as; +}; +``` + +Your `Mat4X4Type` must behave like the library’s `Mat<4,4,...>` (supports `*`, `/`, `inverted()`, `.at(r,c)`, `.raw_array()`, and `static constexpr get_store_ordering()`). + +--- + +## Quick start + +```cpp +using Mat4 = omath::Mat<4,4,float, omath::MatStoreType::COLUMN_MAJOR>; + +// Example trait (sketch): assumes Y-up, column-major, left-handed +struct MyCamTrait { + static ViewAnglesType calc_look_at_angle(const Vector3& eye, + const Vector3& at) noexcept; + static Mat4 calc_view_matrix(const ViewAnglesType& ang, + const Vector3& eye) noexcept; + static Mat4 calc_projection_matrix(const FieldOfView& fov, + const ViewPort& vp, + float znear, float zfar) noexcept; +}; + +using Camera = omath::projection::Camera; + +omath::projection::ViewPort vp{1920, 1080}; +omath::projection::FieldOfView fov = omath::angles::degrees(70.f); + +Camera cam(/*position*/ {0,1.7f, -3}, + /*angles*/ MyViewAngles{/*...*/}, + /*viewport*/ vp, fov, + /*near*/ 0.1f, + /*far*/ 1000.f); + +// Project world → screen (origin top-left) +auto s = cam.world_to_screen({1, 1, 0}); +if (s) { + // s->x, s->y in pixels; s->z in NDC depth +} +``` + +--- + +## API + +```cpp +enum class ScreenStart { TOP_LEFT_CORNER, BOTTOM_LEFT_CORNER }; + +class Camera final { +public: + ~Camera() = default; + + Camera(const Vector3& position, + const ViewAnglesType& view_angles, + const ViewPort& view_port, + const FieldOfView& fov, + float near, float far) noexcept; + + void look_at(const Vector3& target); // recomputes view angles; invalidates cache + + // Lazily computed and cached: + const Mat4X4Type& get_view_projection_matrix() const noexcept; + + // Setters (all invalidate cached VP): + void set_field_of_view(const FieldOfView&) noexcept; + void set_near_plane(float) noexcept; + void set_far_plane(float) noexcept; + void set_view_angles(const ViewAnglesType&) noexcept; + void set_origin(const Vector3&) noexcept; + void set_view_port(const ViewPort&) noexcept; + + // Getters: + const FieldOfView& get_field_of_view() const noexcept; + const float& get_near_plane() const noexcept; + const float& get_far_plane() const noexcept; + const ViewAnglesType& get_view_angles() const noexcept; + const Vector3& get_origin() const noexcept; + + // World → Screen (pixels) via NDC; choose screen origin: + template + std::expected, Error> + world_to_screen(const Vector3& world) const noexcept; + + // World → NDC (aka “viewport” in this code) ∈ [-1,1]^3 + std::expected, Error> + world_to_view_port(const Vector3& world) const noexcept; + + // NDC → World (uses inverse VP) + std::expected, Error> + view_port_to_screen(const Vector3& ndc) const noexcept; + + // Screen (pixels) → World + std::expected, Error> + screen_to_world(const Vector3& screen) const noexcept; + + // 2D overload (z defaults to 1, i.e., far plane ray-end in NDC) + std::expected, Error> + screen_to_world(const Vector2& screen) const noexcept; + +protected: + ViewPort m_view_port{}; + FieldOfView m_field_of_view; + mutable std::optional m_view_projection_matrix; + float m_far_plane_distance{}; + float m_near_plane_distance{}; + ViewAnglesType m_view_angles; + Vector3 m_origin; + +private: + static constexpr bool is_ndc_out_of_bounds(const Mat4X4Type& ndc) noexcept; + Vector3 ndc_to_screen_position_from_top_left_corner(const Vector3& ndc) const noexcept; + Vector3 ndc_to_screen_position_from_bottom_left_corner(const Vector3& ndc) const noexcept; + Vector3 screen_to_ndc(const Vector3& screen) const noexcept; +}; +``` + +### Error handling + +All conversions return `std::expected<..., Error>` with errors from `error_codes.hpp`, notably: + +* `Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS` — clip space W=0 or NDC outside `[-1,1]`. +* `Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO` — non-invertible View×Projection matrix. + +--- + +## Coordinate spaces & conversions + +### World → NDC (`world_to_view_port`) + +1. Build (or reuse cached) `VP = P * V` (projection * view). +2. Multiply by homogeneous column from the world point. +3. Reject if `w == 0`. +4. Perspective divide → NDC in `[-1,1]^3`. +5. Reject if any component is out of range. + +Returns `{x_ndc, y_ndc, z_ndc}`. + +### NDC → Screen (pixels) + +The class offers two origins: + +* **Top-left (default)** + + ``` + x_px = (x_ndc + 1)/2 * width + y_px = ( -y_ndc/2 + 0.5) * height // flips Y + ``` +* **Bottom-left** + + ``` + x_px = (x_ndc + 1)/2 * width + y_px = ( y_ndc/2 + 0.5) * height + ``` + +### Screen (pixels) → NDC + +``` +x_ndc = screen_x / width * 2 - 1 +y_ndc = 1 - screen_y / height * 2 // Top-left screen origin assumed here +z_ndc = screen_z // Caller-provided (e.g., 0..1 depth) +``` + +### NDC → World (`view_port_to_screen`) + +Despite the method name, this function **unprojects** an NDC point back to world space: + +1. Compute `VP^{-1}`; if not invertible → error. +2. Multiply by NDC (homogeneous 4D) and divide by `w`. +3. Return world point. + +> Tip: to build a **world-space ray** from a screen pixel, unproject at `z=0` (near) and `z=1` (far). + +--- + +## Caching & invalidation + +* `get_view_projection_matrix()` computes `P*V` once and caches it. +* Any setter (`set_*`) or `look_at()` clears the cache (`m_view_projection_matrix = std::nullopt`). + +--- + +## Notes & gotchas + +* **Matrix order**: The camera multiplies `P * V`. Make sure your **Trait** matches this convention. +* **Store ordering**: The `Mat4X4Type::get_store_ordering()` is used when building homogeneous columns; ensure it’s consistent with your matrix implementation. +* **Naming quirk**: `view_port_to_screen()` returns a **world** point from **NDC** (it’s an unproject). Consider renaming to `ndc_to_world()` in your codebase for clarity. +* **FOV units**: `FieldOfView` uses the project’s `Angle` type; pass degrees via `angles::degrees(...)`. + +--- + +## Minimal trait sketch (column-major, left-handed) + +```cpp +struct LHCTrait { + static MyAngles calc_look_at_angle(const Vector3& eye, + const Vector3& at) noexcept { /* ... */ } + + static Mat4 calc_view_matrix(const MyAngles& ang, + const Vector3& eye) noexcept { + // Build from forward/right/up and translation + } + + static Mat4 calc_projection_matrix(const FieldOfView& fov, + const ViewPort& vp, + float zn, float zf) noexcept { + return omath::mat_perspective_left_handed( + fov.as_degrees(), vp.aspect_ratio(), zn, zf + ); + } +}; +``` + +--- + +## Testing checklist + +* World point centered in view projects to **screen center**. +* Points outside frustum → `WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS`. +* Inverting `VP` fails gracefully for singular matrices. +* `ScreenStart` switch flips Y as expected. +* Screen→World ray: unproject `(x,y,0)` and `(x,y,1)` and verify direction passes through the camera frustum. + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projection/error_codes.md b/docs/projection/error_codes.md new file mode 100644 index 0000000..435751b --- /dev/null +++ b/docs/projection/error_codes.md @@ -0,0 +1,79 @@ +# `omath::projection::Error` — Error codes for world/screen projection + +> Header: `omath/projection/error_codes.hpp` +> Namespace: `omath::projection` +> Type: `enum class Error : uint16_t` + +These error codes are returned by camera/projection helpers (e.g., `Camera::world_to_screen`, `Camera::screen_to_world`) wrapped in `std::expected<..., Error>`. Use them to distinguish **clipping/visibility** problems from **matrix/math** failures. + +--- + +## Enum values + +```cpp +enum class Error : uint16_t { + WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS, + INV_VIEW_PROJ_MAT_DET_EQ_ZERO, +}; +``` + +* **`WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS`** + The input point cannot produce a valid on-screen coordinate: + + * Clip-space `w == 0` (point at/infinite or behind camera plane), or + * After projection, any NDC component is outside `[-1, 1]`. + +* **`INV_VIEW_PROJ_MAT_DET_EQ_ZERO`** + The **View × Projection** matrix is not invertible (determinant ≈ 0). + Unprojection (`screen_to_world` / `view_port_to_screen`) requires an invertible matrix. + +--- + +## Typical usage + +```cpp +using omath::projection::Error; + +auto pix = cam.world_to_screen(point); +if (!pix) { + switch (pix.error()) { + case Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS: + // Cull label/marker; point is off-screen or behind camera. + break; + case Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO: + // Investigate camera/projection setup; near/far/FOV or trait bug. + break; + } +} + +// Unproject a screen pixel (top-left origin) at depth 1.0 +if (auto world = cam.screen_to_world({sx, sy, 1.0f})) { + // use *world +} else if (world.error() == Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO) { + // handle singular VP matrix +} +``` + +--- + +## When you might see these errors + +* **Out-of-bounds** + + * The world point lies outside the camera frustum. + * The point is behind the camera (clip `w <= 0`). + * Extremely large coordinates cause overflow and fail NDC bounds. + +* **Non-invertible VP** + + * Degenerate projection settings (e.g., `near == far`, zero FOV). + * Trait builds `P` or `V` incorrectly (wrong handedness/order). + * Numerical issues from nearly singular configurations. + +--- + +## Recommendations + +* Validate camera setup: `near > 0`, `far > near`, sensible FOV (e.g., 30°–120°). +* For UI markers: treat `WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS` as a simple **cull** signal. +* Log `INV_VIEW_PROJ_MAT_DET_EQ_ZERO` — it usually indicates a configuration or math bug worth fixing rather than hiding. diff --git a/docs/rev_eng/external_rev_object.md b/docs/rev_eng/external_rev_object.md new file mode 100644 index 0000000..3a983fe --- /dev/null +++ b/docs/rev_eng/external_rev_object.md @@ -0,0 +1,164 @@ +# `omath::rev_eng::ExternalReverseEngineeredObject` — typed offsets over external memory + +> Header: `omath/rev_eng/external_reverse_engineered_object.hpp` +> Namespace: `omath::rev_eng` +> Pattern: **CRTP-style wrapper** around a user-provided *ExternalMemoryManagementTrait* that actually reads/writes another process or device’s memory. + +A tiny base class for reverse-engineered objects that live **outside** your address space. You pass an absolute base address and a trait with `read_memory` / `write_memory`. Your derived types then expose strongly-typed getters/setters that delegate into the trait using **byte offsets**. + +--- + +## Quick look + +```cpp +template +class ExternalReverseEngineeredObject { +public: + explicit ExternalReverseEngineeredObject(std::uintptr_t addr) + : m_object_address(addr) {} + +protected: + template + [[nodiscard]] Type get_by_offset(std::ptrdiff_t offset) const { + return ExternalMemoryManagementTrait::read_memory(m_object_address + offset); + } + + template + void set_by_offset(std::ptrdiff_t offset, const Type& value) const { + ExternalMemoryManagementTrait::write_memory(m_object_address + offset, value); + } + +private: + std::uintptr_t m_object_address{}; +}; +``` + +--- + +## Trait requirements + +Your `ExternalMemoryManagementTrait` must provide: + +```cpp +// Reads sizeof(T) bytes starting at absolute address and returns T. +template +static T read_memory(std::uintptr_t absolute_address); + +// Writes sizeof(T) bytes to absolute address. +template +static void write_memory(std::uintptr_t absolute_address, const T& value); +``` + +> Tip: If your implementation prefers returning `bool`/`expected<>` for writes, either: +> +> * make `write_memory` `void` and throw/log internally, or +> * adjust `set_by_offset` in your fork to surface the status. + +### Common implementations + +* **Windows**: wrap `ReadProcessMemory` / `WriteProcessMemory` with a stored `HANDLE` (often captured via a singleton or embedded in the trait). +* **Linux**: `/proc//mem`, `process_vm_readv/writev`, `ptrace`. +* **Device/FPGA**: custom MMIO/driver APIs. + +--- + +## How to use (derive and map fields) + +Create a concrete type for your target structure and map known offsets: + +```cpp +struct WinRPMTrait { + template + static T read_memory(std::uintptr_t addr) { + T out{}; + SIZE_T n{}; + if (!ReadProcessMemory(g_handle, reinterpret_cast(addr), &out, sizeof(T), &n) || n != sizeof(T)) + throw std::runtime_error("RPM failed"); + return out; + } + template + static void write_memory(std::uintptr_t addr, const T& val) { + SIZE_T n{}; + if (!WriteProcessMemory(g_handle, reinterpret_cast(addr), &val, sizeof(T), &n) || n != sizeof(T)) + throw std::runtime_error("WPM failed"); + } +}; + +class Player final : public omath::rev_eng::ExternalReverseEngineeredObject { + using Base = omath::rev_eng::ExternalReverseEngineeredObject; +public: + using Base::Base; // inherit ctor (takes base address) + + // Offsets taken from your RE notes (in bytes) + Vector3 position() const { return get_by_offset>(0x30); } + void set_position(const Vector3& p) const { set_by_offset(0x30, p); } + + float health() const { return get_by_offset(0x100); } + void set_health(float h) const { set_by_offset(0x100, h); } +}; +``` + +Then: + +```cpp +Player p{ /* base address you discovered */ 0x7FF6'1234'0000ull }; +auto pos = p.position(); +p.set_health(100.f); +``` + +--- + +## Design notes & constraints + +* **Offsets are byte offsets** from the object’s **base address** passed to the constructor. +* **Type safety is on you**: `Type` must match the external layout at that offset (endian, packing, alignment). +* **No lifetime tracking**: if the target object relocates/frees, you must update/recreate the wrapper with the new base address. +* **Thread safety**: the class itself is stateless; thread safety depends on your trait implementation. +* **Endianness**: assumes the host and target endianness agree, or your trait handles conversion. +* **Error handling**: this header doesn’t prescribe it; adopt exceptions/expected/logging inside the trait. + +--- + +## Best practices + +* Centralize offsets in one place (constexprs or a small struct) and **comment source/version** (e.g., *game v1.2.3*). +* Wrap fragile multi-field writes in a trait-level **transaction** if your platform supports it. +* Validate pointers/guards (e.g., vtable signature, canary) before trusting offsets. +* Prefer **plain old data** (`struct` without virtuals) for `Type` to ensure trivial byte copies. + +--- + +## Minimal trait sketch (POSIX, `process_vm_readv`) + +```cpp +struct LinuxPvmTrait { + static pid_t pid; + + template static T read_memory(std::uintptr_t addr) { + T out{}; + iovec local{ &out, sizeof(out) }, remote{ reinterpret_cast(addr), sizeof(out) }; + if (process_vm_readv(pid, &local, 1, &remote, 1, 0) != ssize_t(sizeof(out))) + throw std::runtime_error("pvm_readv failed"); + return out; + } + + template static void write_memory(std::uintptr_t addr, const T& val) { + iovec local{ const_cast(&val), sizeof(val) }, remote{ reinterpret_cast(addr), sizeof(val) }; + if (process_vm_writev(pid, &local, 1, &remote, 1, 0) != ssize_t(sizeof(val))) + throw std::runtime_error("pvm_writev failed"); + } +}; +``` + +--- + +## Troubleshooting + +* **Garbled values** → wrong offset/Type, or target’s structure changed between versions. +* **Access denied** → missing privileges (admin/root), wrong process handle, or page protections. +* **Crashes in trait** → add bounds/sanity checks; many APIs fail on unmapped pages. +* **Writes “stick” only briefly** → the target may constantly overwrite (server authority / anti-cheat / replication). + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/rev_eng/internal_rev_object.md b/docs/rev_eng/internal_rev_object.md new file mode 100644 index 0000000..3fce7be --- /dev/null +++ b/docs/rev_eng/internal_rev_object.md @@ -0,0 +1,142 @@ +# `omath::rev_eng::InternalReverseEngineeredObject` — raw in-process offset/VTABLE access + +> Header: `omath/rev_eng/internal_reverse_engineered_object.hpp` +> Namespace: `omath::rev_eng` +> Purpose: Convenience base for **internal** (same-process) RE wrappers that: +> +> * read/write fields by **byte offset** from `this` +> * call **virtual methods** by **vtable index** + +--- + +## At a glance + +```cpp +class InternalReverseEngineeredObject { +protected: + template + [[nodiscard]] Type& get_by_offset(std::ptrdiff_t offset); + + template + [[nodiscard]] const Type& get_by_offset(std::ptrdiff_t offset) const; + + template + ReturnType call_virtual_method(auto... arg_list); +}; +``` + +* `get_by_offset(off)` — returns a **reference** to `T` located at `reinterpret_cast(this) + off`. +* `call_virtual_method(args...)` — fetches the function pointer from `(*reinterpret_cast(this))[id]` and invokes it as a free function with implicit `this` passed explicitly. + +On MSVC builds the function pointer type uses `__thiscall`; on non-MSVC it uses a plain function pointer taking `void*` as the first parameter (the typical Itanium ABI shape). + +--- + +## Example: wrapping a reverse-engineered class + +```cpp +struct Player : omath::rev_eng::InternalReverseEngineeredObject { + // Field offsets (document game/app version!) + static constexpr std::ptrdiff_t kHealth = 0x100; + static constexpr std::ptrdiff_t kPosition = 0x30; + + // Accessors + float& health() { return get_by_offset(kHealth); } + const float& health() const { return get_by_offset(kHealth); } + + Vector3& position() { return get_by_offset>(kPosition); } + const Vector3& position() const { return get_by_offset>(kPosition); } + + // Virtuals (vtable indices discovered via RE) + int getTeam() { return call_virtual_method<27, int>(); } + void setArmor(float val) { call_virtual_method<42, void>(val); } // signature must match! +}; +``` + +Usage: + +```cpp +auto* p = /* pointer to live Player instance within the same process */; +p->health() = 100.f; +int team = p->getTeam(); +``` + +--- + +## How `call_virtual_method` resolves the signature + +```cpp +template +ReturnType call_virtual_method(auto... arg_list) { +#ifdef _MSC_VER + using Fn = ReturnType(__thiscall*)(void*, decltype(arg_list)...); +#else + using Fn = ReturnType(*)(void*, decltype(arg_list)...); +#endif + return (*reinterpret_cast(this))[id](this, arg_list...); +} +``` + +* The **first parameter** is always `this` (`void*`). +* Remaining parameter types are deduced from the **actual arguments** (`decltype(arg_list)...`). + Ensure you pass arguments with the correct types (e.g., `int32_t` vs `int`, pointer/ref qualifiers), or define thin wrappers that cast to the exact signature you recovered. + +> ⚠ On 32-bit MSVC the `__thiscall` distinction matters; on 64-bit MSVC it’s ignored (all member funcs use the common x64 calling convention). + +--- + +## Safety notes (read before using!) + +Working at this level is inherently unsafe; be deliberate: + +1. **Correct offsets & alignment** + + * `get_by_offset` assumes `this + offset` is **properly aligned** for `T` and points to an object of type `T`. + * Wrong offsets or misalignment ⇒ **undefined behavior** (UB), crashes, silent corruption. + +2. **Object layout assumptions** + + * The vtable pointer is assumed to be at the **start of the most-derived subobject at `this`**. + * With **multiple/virtual inheritance**, the desired subobject’s vptr may be at a non-zero offset. If so, adjust `this` to that subobject before calling, e.g.: + + ```cpp + auto* sub = reinterpret_cast(reinterpret_cast(this) + kSubobjectOffset); + // … then reinterpret sub instead of this inside a custom helper + ``` + +3. **ABI & calling convention** + + * Indices and signatures are **compiler/ABI-specific**. Recheck after updates or different builds (MSVC vs Clang/LLVM-MSVC vs MinGW). + +4. **Strict aliasing** + + * Reinterpreting memory as unrelated `T` can violate aliasing rules. Prefer **trivially copyable** PODs and exact original types where possible. + +5. **Const-correctness** + + * The `const` overload returns `const T&` but still reinterprets memory; do not write through it. Use the non-const overload to mutate. + +6. **Thread safety** + + * No synchronization is provided. Ensure the underlying object isn’t concurrently mutated in incompatible ways. + +--- + +## Tips & patterns + +* **Centralize offsets** in `constexpr` with comments (`// game v1.2.3, sig XYZ`). +* **Guard reads**: if you have a canary or RTTI/vtable hash, check it before relying on offsets. +* **Prefer accessors** returning references**:** lets you both read and write with natural syntax. +* **Wrap tricky virtuals**: if a method takes complex/reference params, wrap `call_virtual_method` in a strongly typed member that casts exactly as needed. + +--- + +## Troubleshooting + +* **Crash on virtual call** → wrong index or wrong `this` (subobject), or mismatched signature (args/ret or calling conv). +* **Weird field values** → wrong offset, wrong type size/packing, stale layout after an update. +* **Only in 32-bit** → double-check `__thiscall` and parameter passing (register vs stack). + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/trigonometry/angle.md b/docs/trigonometry/angle.md new file mode 100644 index 0000000..b4b4670 --- /dev/null +++ b/docs/trigonometry/angle.md @@ -0,0 +1,165 @@ +# `omath::Angle` — templated angle with normalize/clamper + trig + +> Header: `omath/trigonometry/angle.hpp` +> Namespace: `omath` +> Template: `Angle` +> Requires: `std::is_arithmetic_v` +> Formatters: `std::formatter` for `char`, `wchar_t`, `char8_t` → `"{}deg"` + +--- + +## Overview + +`Angle` is a tiny value-type that stores an angle in **degrees** and automatically **normalizes** or **clamps** it into a compile-time range. It exposes conversions to/from radians, common trig (`sin/cos/tan/cot`), arithmetic with wrap/clamp semantics, and lightweight formatting. + +Two behaviors via `AngleFlags`: + +* `AngleFlags::Normalized` (default): values are wrapped into `[min, max]` using `angles::wrap_angle`. +* `AngleFlags::Clamped`: values are clamped to `[min, max]` using `std::clamp`. + +--- + +## API + +```cpp +namespace omath { + +enum class AngleFlags { Normalized = 0, Clamped = 1 }; + +template +requires std::is_arithmetic_v +class Angle { +public: + // Construction + static constexpr Angle from_degrees(const Type& deg) noexcept; + static constexpr Angle from_radians(const Type& rad) noexcept; + constexpr Angle() noexcept; // 0 deg, adjusted by flags/range + + // Accessors / conversions (degrees stored internally) + constexpr const Type& operator*() const noexcept; // raw degrees reference + constexpr Type as_degrees() const noexcept; + constexpr Type as_radians() const noexcept; + + // Trig (computed from radians) + Type sin() const noexcept; + Type cos() const noexcept; + Type tan() const noexcept; + Type atan() const noexcept; // atan(as_radians()) (rarely used) + Type cot() const noexcept; // cos()/sin() (watch sin≈0) + + // Arithmetic (wraps or clamps per flags and [min,max]) + constexpr Angle& operator+=(const Angle&) noexcept; + constexpr Angle& operator-=(const Angle&) noexcept; + constexpr Angle operator+(const Angle&) noexcept; + constexpr Angle operator-(const Angle&) noexcept; + constexpr Angle operator-() const noexcept; + + // Comparison (partial ordering) + constexpr std::partial_ordering operator<=>(const Angle&) const noexcept = default; +}; + +} // namespace omath +``` + +### Formatting + +```cpp +std::format("{}", Angle::from_degrees(45)); // "45deg" +``` + +Formatters exist for `char`, `wchar_t`, and `char8_t`. + +--- + +## Usage examples + +### Defaults (0–360, normalized) + +```cpp +using Deg = omath::Angle<>; // float, [0,360], Normalized + +auto a = Deg::from_degrees(370); // -> 10deg +auto b = Deg::from_radians(omath::angles::pi); // -> 180deg + +a += Deg::from_degrees(355); // 10 + 355 -> 365 -> wraps -> 5deg + +float s = a.sin(); // sin(5°) +``` + +### Clamped range + +```cpp +using Fov = omath::Angle; +auto fov = Fov::from_degrees(200.f); // -> 179deg (clamped) +``` + +### Signed, normalized range + +```cpp +using SignedDeg = omath::Angle; + +auto x = SignedDeg::from_degrees(190.f); // -> -170deg +auto y = SignedDeg::from_degrees(-200.f); // -> 160deg +auto z = x + y; // -170 + 160 = -10deg (wrapped if needed) +``` + +### Get/set raw degrees + +```cpp +auto yaw = SignedDeg::from_degrees(-45.f); +float deg = *yaw; // same as yaw.as_degrees() +``` + +--- + +## Semantics & notes + +* **Storage & units:** Internally stores **degrees** (`Type m_angle`). `as_radians()`/`from_radians()` use the project helpers in `omath::angles`. +* **Arithmetic honors policy:** `operator+=`/`-=` and the binary `+`/`-` apply **wrap** or **clamp** in `[min,max]`, mirroring construction behavior. +* **`atan()`**: returns `std::atan(as_radians())` (the arctangent of the *radian value*). This is mathematically unusual for an angle type and is rarely useful; prefer `tan()`/`atan2` in client code when solving geometry problems. +* **`cot()` / `tan()` singularities:** Near multiples where `sin() ≈ 0` or `cos() ≈ 0`, results blow up. Guard in your usage if inputs can approach these points. +* **Comparison:** `operator<=>` is defaulted. With normalization, distinct representatives can compare as expected (e.g., `-180` vs `180` in signed ranges are distinct endpoints). +* **No implicit numeric conversion:** There’s **no `operator Type()`**. Use `as_degrees()`/`as_radians()` (or `*angle`) explicitly—this intentional friction avoids unit mistakes. + +--- + +## Customization patterns + +* **Radians workflow:** Keep angles in degrees internally but wrap helper creators: + + ```cpp + inline Deg degf(float d) { return Deg::from_degrees(d); } + inline Deg radf(float r) { return Deg::from_radians(r); } + ``` +* **Compile-time policy:** Pick ranges/flags at the type level to enforce invariants (e.g., `YawDeg = Angle`; `FovDeg = Angle`). + +--- + +## Pitfalls & gotchas + +* Ensure `min < max` at compile time for meaningful wrap/clamp behavior. +* For normalized signed ranges, decide whether your `wrap_angle(min,max)` treats endpoints half-open (e.g., `[-180,180)`) to avoid duplicate representations; the formatter will print the stored value verbatim. +* If you need **sum of many angles**, accumulating in radians then converting back can improve numeric stability at extreme values. + +--- + +## Minimal tests + +```cpp +using A = omath::Angle<>; +REQUIRE(A::from_degrees(360).as_degrees() == 0.f); +REQUIRE(A::from_degrees(-1).as_degrees() == 359.f); + +using S = omath::Angle; +REQUIRE(S::from_degrees( 181).as_degrees() == -179.f); +REQUIRE(S::from_degrees(-181).as_degrees() == 179.f); + +using C = omath::Angle; +REQUIRE(C::from_degrees(5).as_degrees() == 10.f); +REQUIRE(C::from_degrees(25).as_degrees() == 20.f); +``` + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/trigonometry/angles.md b/docs/trigonometry/angles.md new file mode 100644 index 0000000..afef6cb --- /dev/null +++ b/docs/trigonometry/angles.md @@ -0,0 +1,107 @@ +# `omath::angles` — angle conversions, FOV helpers, and wrapping + +> Header: `omath/trigonometry/angles.hpp` +> Namespace: `omath::angles` +> All functions are `[[nodiscard]]` and `noexcept` where applicable. + +A small set of constexpr-friendly utilities for converting between degrees/radians, converting horizontal/vertical field of view, and wrapping angles into a closed interval. + +--- + +## API + +```cpp +// Degrees ↔ Radians (Type must be floating-point) +template +requires std::is_floating_point_v +constexpr Type radians_to_degrees(const Type& radians) noexcept; + +template +requires std::is_floating_point_v +constexpr Type degrees_to_radians(const Type& degrees) noexcept; + +// FOV conversion (inputs/outputs in degrees, aspect = width/height) +template +requires std::is_floating_point_v +Type horizontal_fov_to_vertical(const Type& horizontal_fov, const Type& aspect) noexcept; + +template +requires std::is_floating_point_v +Type vertical_fov_to_horizontal(const Type& vertical_fov, const Type& aspect) noexcept; + +// Wrap angle into [min, max] (any arithmetic type) +template +requires std::is_arithmetic_v +Type wrap_angle(const Type& angle, const Type& min, const Type& max) noexcept; +``` + +--- + +## Usage + +### Degrees ↔ Radians + +```cpp +float rad = omath::angles::degrees_to_radians(180.0f); // π +double deg = omath::angles::radians_to_degrees(std::numbers::pi); // 180 +``` + +### Horizontal ↔ Vertical FOV + +* `aspect` = **width / height**. +* Inputs/outputs are **degrees**. + +```cpp +float hdeg = 90.0f; +float aspect = 16.0f / 9.0f; + +float vdeg = omath::angles::horizontal_fov_to_vertical(hdeg, aspect); // ~58.0° +float hdeg2 = omath::angles::vertical_fov_to_horizontal(vdeg, aspect); // ≈ 90.0° +``` + +Formulas (in radians): + +* `v = 2 * atan( tan(h/2) / aspect )` +* `h = 2 * atan( tan(v/2) * aspect )` + +### Wrapping angles (or any periodic value) + +Wrap any numeric `angle` into `[min, max]`: + +```cpp +// Wrap degrees into [0, 360] +float a = omath::angles::wrap_angle( 370.0f, 0.0f, 360.0f); // 10 +float b = omath::angles::wrap_angle( -15.0f, 0.0f, 360.0f); // 345 +// Signed range [-180,180] +float c = omath::angles::wrap_angle( 200.0f, -180.0f, 180.0f); // -160 +``` + +--- + +## Notes & edge cases + +* **Type requirements** + + * Converters & FOV helpers require **floating-point** `Type`. + * `wrap_angle` accepts any arithmetic `Type` (floats or integers). +* **Aspect ratio** must be **positive** and finite. For `aspect == 0` the FOV helpers are undefined. +* **Units**: FOV functions accept/return **degrees** but compute internally in radians. +* **Wrapping interval**: Behavior assumes `max > min`. The result lies in the **closed interval** `[min, max]` with modulo arithmetic; if you need half-open behavior (e.g., `[min,max)`), adjust your range or post-process endpoint cases. +* **constexpr**: Converters are `constexpr`; FOV helpers are runtime constexpr-compatible except for `std::atan/std::tan` constraints on some standard libraries. + +--- + +## Quick tests + +```cpp +using namespace omath::angles; + +static_assert(degrees_to_radians(180.0) == std::numbers::pi); +static_assert(radians_to_degrees(std::numbers::pi_v) == 180.0f); + +float v = horizontal_fov_to_vertical(90.0f, 16.0f/9.0f); +float h = vertical_fov_to_horizontal(v, 16.0f/9.0f); +assert(std::abs(h - 90.0f) < 1e-5f); + +assert(wrap_angle(360.0f, 0.0f, 360.0f) == 0.0f || wrap_angle(360.0f, 0.0f, 360.0f) == 360.0f); +``` diff --git a/docs/trigonometry/view_angles.md b/docs/trigonometry/view_angles.md new file mode 100644 index 0000000..5f6b12b --- /dev/null +++ b/docs/trigonometry/view_angles.md @@ -0,0 +1,87 @@ +# `omath::ViewAngles` — tiny POD for pitch/yaw/roll + +> Header: your project’s `view_angles.hpp` +> Namespace: `omath` +> Kind: **aggregate struct** (POD), no methods, no allocation + +A minimal container for Euler angles. You choose the types for each component (e.g., raw `float` or the strong `omath::Angle<>` type), and plug it into systems like `projection::Camera`. + +--- + +## API + +```cpp +namespace omath { + template + struct ViewAngles { + PitchType pitch; + YawType yaw; + RollType roll; + }; +} +``` + +* Aggregate: supports brace-init, aggregate copying, and `constexpr` usage when the component types do. +* Semantics (units/handedness/ranges) are **entirely defined by your chosen types**. + +--- + +## Common aliases + +```cpp +// Simple, raw degrees as floats (be careful with wrapping!) +using ViewAnglesF = omath::ViewAngles; + +// Safer, policy-based angles (recommended) +using PitchDeg = omath::Angle; +using YawDeg = omath::Angle; +using RollDeg = omath::Angle; +using ViewAnglesDeg = omath::ViewAngles; +``` + +--- + +## Examples + +### Basic construction + +```cpp +omath::ViewAngles a{ 10.f, 45.f, 0.f }; // pitch, yaw, roll in degrees +``` + +### With `omath::Angle<>` (automatic wrap/clamper) + +```cpp +ViewAnglesDeg v{ + PitchDeg::from_degrees( 95.f), // -> 89deg (clamped) + YawDeg::from_degrees (-190.f), // -> 170deg (wrapped) + RollDeg::from_degrees ( 30.f) +}; +``` + +### Using with `projection::Camera` + +```cpp +using Mat4 = omath::Mat<4,4,float, omath::MatStoreType::COLUMN_MAJOR>; +using Cam = omath::projection::Camera; + +omath::projection::ViewPort vp{1920,1080}; +auto fov = omath::angles::degrees_to_radians(70.f); // or your Angle type + +Cam cam(/*position*/ {0,1.7f,-3}, + /*angles*/ ViewAnglesDeg{ PitchDeg::from_degrees(0), + YawDeg::from_degrees(0), + RollDeg::from_degrees(0) }, + /*viewport*/ vp, + /*fov*/ omath::Angle::from_degrees(70.f), + /*near*/ 0.1f, + /*far*/ 1000.f); +``` + +--- + +## Notes & tips + +* **Ranges/units**: pick types that encode your policy (e.g., signed yaw in `[-180,180]`, pitch clamped to avoid gimbal flips). +* **Handedness & order**: this struct doesn’t impose rotation order. Your math/trait layer (e.g., `MyCameraTrait`) must define how `(pitch, yaw, roll)` map to a view matrix (common orders: ZYX or XYZ). +* **Zero-cost**: with plain `float`s this is as cheap as three scalars; with `Angle<>` you gain safety at the cost of tiny wrap/clamp logic on construction/arithmetic.