Skip to content

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

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:

// 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:

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:

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)

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