mirror of
https://github.com/orange-cpp/omath.git
synced 2026-02-13 07:03:25 +00:00
Documents view angle struct and related API
Adds documentation for the `omath::ViewAngles` struct, clarifying its purpose, common usage patterns, and the definition of the types of pitch, yaw and roll. Also, adds short explanations of how to use ViewAngles and what tradeoffs exist between using raw float types and strongly typed Angle<> types.
This commit is contained in:
164
docs/rev_eng/external_rev_object.md
Normal file
164
docs/rev_eng/external_rev_object.md
Normal file
@@ -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 ExternalMemoryManagementTrait>
|
||||
class ExternalReverseEngineeredObject {
|
||||
public:
|
||||
explicit ExternalReverseEngineeredObject(std::uintptr_t addr)
|
||||
: m_object_address(addr) {}
|
||||
|
||||
protected:
|
||||
template<class Type>
|
||||
[[nodiscard]] Type get_by_offset(std::ptrdiff_t offset) const {
|
||||
return ExternalMemoryManagementTrait::read_memory(m_object_address + offset);
|
||||
}
|
||||
|
||||
template<class Type>
|
||||
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<class T>
|
||||
static T read_memory(std::uintptr_t absolute_address);
|
||||
|
||||
// Writes sizeof(T) bytes to absolute address.
|
||||
template<class T>
|
||||
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/<pid>/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<class T>
|
||||
static T read_memory(std::uintptr_t addr) {
|
||||
T out{};
|
||||
SIZE_T n{};
|
||||
if (!ReadProcessMemory(g_handle, reinterpret_cast<LPCVOID>(addr), &out, sizeof(T), &n) || n != sizeof(T))
|
||||
throw std::runtime_error("RPM failed");
|
||||
return out;
|
||||
}
|
||||
template<class T>
|
||||
static void write_memory(std::uintptr_t addr, const T& val) {
|
||||
SIZE_T n{};
|
||||
if (!WriteProcessMemory(g_handle, reinterpret_cast<LPVOID>(addr), &val, sizeof(T), &n) || n != sizeof(T))
|
||||
throw std::runtime_error("WPM failed");
|
||||
}
|
||||
};
|
||||
|
||||
class Player final : public omath::rev_eng::ExternalReverseEngineeredObject<WinRPMTrait> {
|
||||
using Base = omath::rev_eng::ExternalReverseEngineeredObject<WinRPMTrait>;
|
||||
public:
|
||||
using Base::Base; // inherit ctor (takes base address)
|
||||
|
||||
// Offsets taken from your RE notes (in bytes)
|
||||
Vector3<float> position() const { return get_by_offset<Vector3<float>>(0x30); }
|
||||
void set_position(const Vector3<float>& p) const { set_by_offset(0x30, p); }
|
||||
|
||||
float health() const { return get_by_offset<float>(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<class T> static T read_memory(std::uintptr_t addr) {
|
||||
T out{};
|
||||
iovec local{ &out, sizeof(out) }, remote{ reinterpret_cast<void*>(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<class T> static void write_memory(std::uintptr_t addr, const T& val) {
|
||||
iovec local{ const_cast<T*>(&val), sizeof(val) }, remote{ reinterpret_cast<void*>(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*
|
||||
142
docs/rev_eng/internal_rev_object.md
Normal file
142
docs/rev_eng/internal_rev_object.md
Normal file
@@ -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<class Type>
|
||||
[[nodiscard]] Type& get_by_offset(std::ptrdiff_t offset);
|
||||
|
||||
template<class Type>
|
||||
[[nodiscard]] const Type& get_by_offset(std::ptrdiff_t offset) const;
|
||||
|
||||
template<std::size_t id, class ReturnType>
|
||||
ReturnType call_virtual_method(auto... arg_list);
|
||||
};
|
||||
```
|
||||
|
||||
* `get_by_offset<T>(off)` — returns a **reference** to `T` located at `reinterpret_cast<uintptr_t>(this) + off`.
|
||||
* `call_virtual_method<id, Ret>(args...)` — fetches the function pointer from `(*reinterpret_cast<void***>(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<float>(kHealth); }
|
||||
const float& health() const { return get_by_offset<float>(kHealth); }
|
||||
|
||||
Vector3<float>& position() { return get_by_offset<Vector3<float>>(kPosition); }
|
||||
const Vector3<float>& position() const { return get_by_offset<Vector3<float>>(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<std::size_t id, class ReturnType>
|
||||
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<Fn**>(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<T>` 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<void*>(reinterpret_cast<std::uintptr_t>(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*
|
||||
Reference in New Issue
Block a user