Эта страница про самую базовую практику: как читать и писать память, что такое static pointer, pointer chain и offsets, как работать со строками и буферами, и какой способ чтения выбирать в C#.
Если коротко, здесь нам нужны всего три идеи:
Если нужен внешний контекст, очень полезны:
Обычно вы открываете процесс и работаете через process.Memory:
using System.Diagnostics;
using EyeAuras.Memory;
using var process = LocalProcess.ByProcessId(Process.GetCurrentProcess().Id);
var hp = process.Memory.Read<int>(someHpAddress);
var speed = process.Memory.Read<float>(someSpeedAddress);
process.Memory.Write(someFlagAddress, 1);
process.Memory в EyeAuras это и есть основной слой для работы с памятью. Через него можно:
Для простых случаев почти всегда нужны именно эти методы:
Read<T>(address) — прочитать одно значениеRead<T>(address, count) — прочитать массив значенийWrite(address, value) — записать одно значениеWrite(address, spanOrArray) — записать блок значенийRead(address, size) — прочитать сырые байтыvar hp = process.Memory.Read<int>(hpAddress);
var positionX = process.Memory.Read<float>(positionAddress);
var header = process.Memory.Read(someAddress, 64);
process.Memory.Write(someAddress, 123);
process.Memory.Write(someAddress + 4, new byte[] { 0x90, 0x90, 0x90, 0x90 });
Этот путь лучше всего подходит для:
int, float, long, bytebool, если вы точно знаете его размер в памятиstruct, которые состоят из чисел, указателей и других таких же простых structЕсли внутри структуры уже есть string, обычный byte[] или что-то похожее, лучше смотреть в сторону ReadManaged<T>(...). Ниже будет отдельный раздел про это.
Static pointer это адрес, который можно стабильно восстановить через что-то более надёжное, чем "просто число из Cheat Engine". Обычно это:
BaseAddress + RVAПростейший пример:
var mainModule = process.GetProcessModule("game.exe");
var playerManagerPtrAddress = new IntPtr(mainModule.BaseAddress.ToInt64() + 0x123456);
var playerManager = process.Memory.ReadPointer(playerManagerPtrAddress);
Смысл здесь в том, что база модуля может меняться между запусками, а RVA внутри него остаётся тем же. Поэтому мы обычно думаем не так:
0x12345678"а так:
game.exe + 0x123456"Offset это смещение внутри структуры или объекта.
Если у вас есть:
playerBase0x10 до Health0x18 до Manaто чтение выглядит так:
var health = process.Memory.Read<int>(playerBase + 0x10);
var mana = process.Memory.Read<int>(playerBase + 0x18);
Pointer chain это ситуация, когда по адресу лежит не само значение, а указатель на следующий объект.
Если совсем по-простому:
Типичный маршрут:
Если записать этот маршрут в коде, получится такая идея:
var hp = process.Memory.ReadPointerChain<int>(
(IntPtr)0x140123456,
0x10,
0x20,
0x18);
Адрес и offsets здесь примерные. Смысл не в конкретных числах, а в том, что каждый следующий шаг либо читает pointer, либо в самом конце читает уже обычное значение.
В EyeAuras для этого есть готовые методы:
ReadPointer(...)ReadPointer32(...)ReadPointerChain<T>(...)ReadPointerChain32<T>(...)var health = process.Memory.ReadPointerChain<int>(
playerManagerPtrAddress,
0x10,
0x20,
0x18);
Для 32-bit layout используйте ReadPointer32(...) и ReadPointerChain32<T>(...), иначе вы просто будете читать указатели не того размера.
Важно: если в цепочке встретится null, ReadPointerChain<T>(...) вернёт default, а не исключение. Это удобно, но может скрывать ошибки в логике, поэтому в сложных сценариях полезно логировать промежуточные адреса отдельно.
В Memory API есть три основных helper'а:
ReadString(...) — UTF-8ReadStringA(...) — однобайтная строка в стиле ASCII / ANSIReadStringU(...) — UTF-16 LE (Encoding.Unicode)var utf8Name = process.Memory.ReadString(namePtr.ToInt64(), 64);
var ansiName = process.Memory.ReadStringA(namePtr.ToInt64(), 64);
var wideName = process.Memory.ReadStringU(namePtr.ToInt64(), 64);
Практические правила здесь такие:
ReadStringU(...) это длина в байтах, а не в символах\0WriteString(...) сейчас пишет UTF-8 строку с \0 на концеЕсли строка лежит не отдельно, а внутри структуры фиксированным полем, почти всегда лучше читать её через ReadManaged<T>(...). Для этого есть отдельная страница: C# Struct Layout.
Полезный фон по строкам и marshaling:
На обычных Windows-процессах почти всегда всё просто: x86 и x64 — это little-endian.
Это значит, что байты:
01 00 00 00
означают int = 1, а не 16777216.
Для обычного чтения памяти в Windows вам почти никогда не нужно вручную разворачивать байты. Проще говоря: в обычной Windows-игре этот раздел чаще всего можно просто запомнить и жить дальше.
Но если вы:
тогда endianness уже нужно учитывать явно.
using System.Buffers.Binary;
var raw = process.Memory.Read(someAddress, 4);
var bigEndianValue = BinaryPrimitives.ReadInt32BigEndian(raw);
Полезная ссылка:
Самая частая ошибка в memory-скриптах это читать слишком много маленьких кусочков по одному полю.
Плохой маршрут:
Read<int>(...) подряд каждый тикОбычно лучше:
var buffer = new byte[0x400];
if (process.Memory.TryRead(playerBase, buffer.AsSpan()))
{
var health = BitConverter.ToInt32(buffer, 0x10);
var mana = BitConverter.ToInt32(buffer, 0x18);
var targetId = BitConverter.ToInt64(buffer, 0x28);
}
Если у вас массив однотипных элементов, удобнее читать его сразу как массив структур:
var actors = process.Memory.Read<ActorEntry>(actorsArrayAddress, actorCount);
Под капотом MemoryAccessor уже делает несколько полезных вещей:
stackalloc для небольших буферовArrayPool<byte> для части unmanaged-чтенийSpan<T>Но даже с этим batching всё равно важен. Самая быстрая оптимизация в таких задачах обычно не "написать умнее C#", а "сократить количество реальных обращений к памяти процесса".
Несколько reader'ов одновременно в целом допустимы. В тестах EyeAuras есть и несколько последовательных reader'ов, и несколько параллельных reader'ов.
Но важно понимать границу:
То есть проблема обычно не в том, что сам MemoryAccessor "ломается от потоков", а в том, что данные в процессе в этот момент уже могли измениться.
Практический совет:
Это одно из самых важных различий во всей теме. Но здесь не обязательно запоминать термины, достаточно понять практическое правило.
Сюда относятся:
Read<T>(...)TryRead<T>(...)Read<T>(..., count)Write<T>(...)Его берите, если структура простая. Например:
fixedInlineArrayИ наоборот, этот путь не подходит, если внутри уже есть:
stringbyte[]Это самый быстрый путь.
Сюда относятся:
ReadManaged<T>(...)TryReadManaged<T>(...)TryWriteManaged<T>(...)Его берите, когда структура уже "более C#-шная". Например, внутри есть:
[MarshalAs(UnmanagedType.ByValArray, ...)][MarshalAs(UnmanagedType.ByValTStr, ...)]Этот путь заметно гибче, но он медленнее.
Короткое правило:
Read<T>(...)MarshalAs или более сложный layout, берите ReadManaged<T>(...)Если сомневаетесь:
ReadManaged<T>(...)