mem
DMA memory read + write + pattern scanning. Combined with entity .base (see game) and the offsets table, scripts can self-service read/write any unwrapped game attribute.
Writes are risky — on you:
write_*is raw memory write with no guardrails. A wrong address or value can corrupt game state, crash the game, and in the worst case draw anti-cheat attention. Reads never corrupt state; all the danger is on the write side. Invalid addr is silently skipped (same as reads), but whether the address/value is correct is nobody's job but yours.Performance: a single
read_*/write_*blocks ~200-500µs (synchronous DMA round-trip). Per-frame multi-field read/write must usemem.scatter/mem.write_scatterto batch — otherwiseframe_updatestalls.
Module info
mem.get_module_base(name) → uint64
Module base address. Returns 0 on failure.
mem.get_module_size(name) → uint64
Module code-segment size. Returns 0 on failure.
mem.is_valid(addr) → bool
Fast pointer sanity check (non-zero, non-sentinel, in user-mode range). Does not access memory — purely local validation.
local base = mem.get_module_base("r5apex.exe")
print(string.format("base = 0x%X", base))Pattern scan
mem.find_pattern(module_name, pattern) → uint64
IDA-style byte-pattern scan. ? matches any byte; bytes separated by spaces. Returns 0 on miss.
Each call scans the entire module code segment (several MB), costing tens of milliseconds. Always cache the resolved addr — never call per-frame. Typical pattern: resolve once at script load, then use the cached addr with
read_*/scatter.
local addr = mem.find_pattern("r5apex.exe", "48 8B 05 ? ? ? ? 48 8B 88")
if addr ~= 0 then
-- RIP-relative resolve: disp32 at addr+3, target = addr+7+disp
local disp = mem.read_i32(addr + 3)
local target = addr + 7 + disp
print(string.format("target = 0x%X", target))
endSingle reads (synchronous, for cold paths / debugging)
| Function | Return | Notes |
|---|---|---|
mem.read_i8(addr) | int | -128..127 |
mem.read_u8(addr) | int | 0..255 |
mem.read_i16(addr) | int | |
mem.read_u16(addr) | int | |
mem.read_i32(addr) | int | |
mem.read_u32(addr) | int (unsigned) | |
mem.read_i64(addr) | int64 | |
mem.read_u64(addr) | uint64 | typical for pointers |
mem.read_float(addr) | number | f32 |
mem.read_double(addr) | number | f64 |
mem.read_vec2(addr) | Vec2 | 8 bytes |
mem.read_vec3(addr) | Vec3 | 12 bytes |
mem.read_bytes(addr, len) → table<byte> | array | empty table when len > 1MB |
mem.read_string(addr, max_len?) → string | string | null-terminated, default max 256, cap 4096 |
Invalid addr / read failure → returns default value (0 / "" / zero vec). Never throws.
mem.read_chain(base, {off1, off2, ...}) → uint64
Multi-level pointer dereference: Read<u64>(base + off1) → Read<u64>(prev + off2) → .... Returns 0 as soon as any step reads zero.
local lp_ptr = mem.read_chain(base, { 0x1F12345, 0x80, 0x18 })Single writes (synchronous, dangerous)
Naming / type set mirror read_* 1:1 — the signature is the read plus one value argument.
| Function | value type | Notes |
|---|---|---|
mem.write_i8(addr, v) | int | truncated to 8-bit |
mem.write_u8(addr, v) | int | |
mem.write_i16(addr, v) / mem.write_u16(addr, v) | int | |
mem.write_i32(addr, v) / mem.write_u32(addr, v) | int | |
mem.write_i64(addr, v) / mem.write_u64(addr, v) | int64 / uint64 | |
mem.write_float(addr, v) | number | f32 |
mem.write_double(addr, v) | number | f64 |
mem.write_vec2(addr, v) | Vec2 | 8 bytes |
mem.write_vec3(addr, v) | Vec3 | 12 bytes |
mem.write_bytes(addr, {0x12, 0x34, ...}) | byte table | cap 1MB |
mem.write_string(addr, str) | string | no trailing NUL; include it yourself if needed ("foo\0") |
Invalid addr → silently skipped, never throws. Address / value correctness is the author's responsibility.
local lp = game.localplayer
local off = offsets.RecvTable.DT_Player.m_iHealth -- named offset, see the offsets page
mem.write_i32(lp.self_base + off, 100) -- set own health (demo, on you)Batch reads (scatter, mandatory for per-frame multi-field)
mem.scatter(reqs_table) → results_table
Single DMA round-trip processes N read requests. Each req is { type, addr [, size] }. Returns 1-based result table in request order.
| type | meaning | 3rd arg |
|---|---|---|
"i8" / "u8" / ... "i64" / "u64" | integers | none |
"f32" / "float" | 32-bit float | none |
"f64" / "double" | 64-bit float | none |
"vec2" / "vec3" | vectors | none |
"bytes" | byte table | size required |
"string" | null-terminated string | size = max_len required |
-- Batch 3 fields in one frame
local r = mem.scatter({
{ "u32", lp_ptr + 0x0238 }, -- health
{ "vec3", lp_ptr + 0x014C }, -- origin
{ "string", lp_ptr + 0x500, 32 }, -- name
})
local health, origin, name = r[1], r[2], r[3]
print(health, origin.x, name)Performance (60 players × 4 fields per frame = 240 reads):
- Single
mem.read_u32 × 240→ ~96ms / frame (frame-killing) mem.scatter(...240 reqs)→ one round-trip, ~2ms / frame
Invalid addr / failed entry returns default — does not abort subsequent entries.
Error signals:
- Wrong type (e.g.
"u9") → that slot returnsnil+ consoleLOG_WARN(distinguishable from reading 0) - Non-table entry (passed a string / number) → that slot returns
nil+LOG_WARN
Batch writes (write_scatter)
mem.write_scatter(reqs_table) → count actually written
Single DMA round-trip writes N addresses. Each req is { type, addr, value } — mirrors mem.scatter, only the 3rd element changes from size (read) to value (write). Type set is identical to scatter reads.
mem.write_scatter({
{ "i32", base + off_a, 100 },
{ "float", base + off_b, 1.5 },
{ "vec3", base + off_c, vec3(x, y, z) },
})Entries with invalid addr / encode failure (value type mismatch) are skipped — no whole-batch failure + LOG_WARN; returns the count actually written.
Safety / Limits
- Writes have no guardrails: address / value correctness is entirely on the author; a wrong write can crash the game. Offsets drift across game updates (use the offsets table, which auto-syncs with the dumper).
- All reads/writes target the currently-attached game process; no pid parameter.
- When DMA has not attached / process is gone,
get_module_basereturns 0,read_*returns defaults,write_*skips. Guard withmem.is_valid. - A
mem.scatter/mem.write_scatterreqs table with 1000+ entries can visibly stall the current frame — split very large batches across frames.