Здравствуйте, меня зовут Борис и я играю с читами. О том, что это и как работает в онлайн играх — подробнее под катом.

Итак, что такое «чит»? «Чит» (от английского cheat) — это программа, которая позволяет упростить игру, предоставляя игроку дополнительные возможности или информацию во время игрового процесса. Читы бывают разных видов — от встроенных игровых команд (как правило, только в режимах single, к ним относятся всем известные коды iddqd, impulse 101, god и прочие) до программ, изменяющих память в заранее найденных областях, к ним относятся все трейнеры и такие инструменты как ArtMoney (уже забыт и не развивается) и Cheat Engine.
В Multiplayer`е конечно же никто не позволит воспользоваться каким-то кодом на бессмертие и бесконечные патроны, всё это по умолчанию отключено, а Cheat Engine не сможет взломать сервер, всё же он работает локально. Но что тогда можно сделать?
В памяти игры всегда хранится информация об игровом процессе, важно понять, какая информация нам интересна и может помочь, а какая бесполезна или и так уже есть на экране. В таких шутерах, как Counter Strike, важно знать, где находится враг: два точных выстрела решают исход игры. Но разве так можно? Можно!
В памяти клиента всегда хранятся структуры, описывающие игроков, находящихся в игре на данный момент. Это справедливо для союзников и не всегда для противников. Обо всем по порядку.
В движке Source есть два типа структур — LocalPlayerEntity (сам игрок) и GeneralPlayerEntity (другие игроки), описываются они примерно так:
Но нам про себя не очень интересно, как там поживают все остальные на карте?
Это всё очень занимательно, и на самом деле эти структуры несколько больше, но как их получить, а тем более отобразить на экране игрока?
Профессионалы работы с памятью процесса знают, что образ процесса имеет стартовый адрес в памяти, который выдает система при запуске, а начиная от него статично хранятся все его данные. Условно можно сказать так, точнее можно найти в интернете. Как же нам это помогает? Примерно вот так:
— С помощью Cheat Engine находим адрес нужных нам переменных и структур, вычисляем их постоянное смещение
— Во время работы игры постоянно считываем значения переменных по смещениям
— На основе полученных данных, представляем игроку информацию обо всем, что можем
О каждом пункте можно говорить долго, сейчас я покажу как выполнить второй, а для первого и второго нужны отдельные темы.
Итак, предположим, что кто-то за нас уже нашел все нужные смещения, а точнее местонахождение двух заветных структур, что нужно сделать нам?
1. Находим процесс игры в памяти, а точнее получаем HANDLE:
Хендл позволяет прочитать любую информацию внутри процесса, а также список его модулей. В Source описанные выше структуры создаются в модуле client.dll, давайте получим его адрес:
2. Получаем адрес модуля:
Работает это аналогично функции выше, так что описывать не буду.
Всё что мы получили дает нам стартовую базу для дальнейших действий. Чтобы что-то узнать, нужно об этом прочитать, давайте прочтем информацию из процесса:
3. Чтение переменных по адресу:
WinAPI функция ReadProcessMemory позволяет получить в буфер любой отрезок памяти процесса по заданному адресу, указанного размера. Достаточно просто и удобно, а данная обертка в виде шаблона избавит нас от необходимости задавать каждый раз считываемый тип. Функция несколько изменена для понятности, откуда берется хендл, в последствии его будем хранить как глобальную переменную, чтобы не передавать сотни раз в функцию одно и тоже значение, и обязательно даем обещание только читать его, не перезаписывать!
Осталось немного, просто взять то, что хранится в нашей оперативной памяти, а значит по праву принадлежит нам!
4. Считывание структуры из памяти:
Вот и всё, это выглядит страшновато, но под пристальным взглядом смысл у этой копипасты простой. То же самое выполняется и для GeneralPlayerEntity, но несколько иначе:
Единственное принципиальное отличие — что LocalPlayerEntity в памяти одна, а GeneralPlayerEntity — 64 штуки подряд (массив собственно). Причем заполнены они абсолютно случайным образом (могу ошибаться, если кто поправит — напишите) — к примеру, если на сервере 12 игроков, один из них вы, 5 союзников и шесть противников — индексы в этой структуре могут быть абсолютно разными — вплоть до того, что все могут получить индексы от 51 до 63, так что не получится просто считать первые 12 структур, нужно проверять каждую. Как видно, мы берем смещение, и с помощью адресной арифметики получаем нужную по номеру структуру, это главное отличие от предыдущей функции. Это позволяет воспользоваться циклом, в котором мы будем непрерывно читать все 64 структуры и сразу же выводить все полученные данные.
На этом всё, если данная тема будет интересна Хабру, напишу еще несколько статей о том, как получать смещения из клиента, как непосредственно показывать игроку всё, что мы получили в этой статье в удобоваримом виде, и как написать простой AimBot.
По материалам форума unknowncheats.me и чита с исходным кодом от Puddin Poppin.

Итак, что такое «чит»? «Чит» (от английского cheat) — это программа, которая позволяет упростить игру, предоставляя игроку дополнительные возможности или информацию во время игрового процесса. Читы бывают разных видов — от встроенных игровых команд (как правило, только в режимах single, к ним относятся всем известные коды iddqd, impulse 101, god и прочие) до программ, изменяющих память в заранее найденных областях, к ним относятся все трейнеры и такие инструменты как ArtMoney (уже забыт и не развивается) и Cheat Engine.
В Multiplayer`е конечно же никто не позволит воспользоваться каким-то кодом на бессмертие и бесконечные патроны, всё это по умолчанию отключено, а Cheat Engine не сможет взломать сервер, всё же он работает локально. Но что тогда можно сделать?
В памяти игры всегда хранится информация об игровом процессе, важно понять, какая информация нам интересна и может помочь, а какая бесполезна или и так уже есть на экране. В таких шутерах, как Counter Strike, важно знать, где находится враг: два точных выстрела решают исход игры. Но разве так можно? Можно!
В памяти клиента всегда хранятся структуры, описывающие игроков, находящихся в игре на данный момент. Это справедливо для союзников и не всегда для противников. Обо всем по порядку.
В движке Source есть два типа структур — LocalPlayerEntity (сам игрок) и GeneralPlayerEntity (другие игроки), описываются они примерно так:
struct tLocalPlayerEntity
{
byte Flags;//флаги положения, в основном описывает состояние прыжка
int TeamNumber;//команда
int CrosshairEntityIndex;//индекс в массиве прицелов
int Index;//индекс в массиве всех игроков
D3DXVECTOR3 Origin;//положение в координатах XYZ
D3DXVECTOR3 PunchAngle;//угол направления передвижения
D3DXVECTOR3 ViewOffset;//смещение взгляда
D3DXVECTOR3 ViewAngle;//угол взгляда
D3DXVECTOR3 Velocity;//скорость перемещения
};
Но нам про себя не очень интересно, как там поживают все остальные на карте?
struct tGeneralPlayerEntityInfo
{
int TeamNumber;//команда
int Health;//состояние здоровья
D3DXVECTOR3 Origin;//положение в координатах XYZ
D3DXVECTOR3 Velocity;//скорость перемещения
int Kills;//сколько убил
int Deaths;//сколько умер
int HasC4;//несет C4?
int Armor;//состояние брони
bool HasDefuser;//есть ли набор разминирования?
bool Dormant;//активен ли в настоящий момент, а точнее жив или мертв
byte Flags;//то же самое, про состояние прыжка
};
Это всё очень занимательно, и на самом деле эти структуры несколько больше, но как их получить, а тем более отобразить на экране игрока?
Профессионалы работы с памятью процесса знают, что образ процесса имеет стартовый адрес в памяти, который выдает система при запуске, а начиная от него статично хранятся все его данные. Условно можно сказать так, точнее можно найти в интернете. Как же нам это помогает? Примерно вот так:
— С помощью Cheat Engine находим адрес нужных нам переменных и структур, вычисляем их постоянное смещение
— Во время работы игры постоянно считываем значения переменных по смещениям
— На основе полученных данных, представляем игроку информацию обо всем, что можем
О каждом пункте можно говорить долго, сейчас я покажу как выполнить второй, а для первого и второго нужны отдельные темы.
Итак, предположим, что кто-то за нас уже нашел все нужные смещения, а точнее местонахождение двух заветных структур, что нужно сделать нам?
1. Находим процесс игры в памяти, а точнее получаем HANDLE:
HANDLE GetProcessHandleByName(const std::wstring &ProcessName)
{
PROCESSENTRY32 ProcessEntry; //Структура, хранящая данные о процессе
ProcessEntry.dwSize = sizeof(PROCESSENTRY32);
HANDLE SnapHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);//Получаем снимок всех процессов в системе
if (Process32First(SnapHandle, &ProcessEntry) == TRUE) //Получаем первый процесс из списка
{
if (!_wcsicmp(ProcessEntry.szExeFile, ProcessName.c_str())) //Сравниваем с нужным именем
{
HANDLE ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessEntry.th32ProcessID);
CloseHandle(SnapHandle);
return ProcessHandle; //если оно, получаем права на процесс и возвращаем хендл
}
else //в противном случае идем до конца списка, пока не найдем нужный процесс
{
while (Process32Next(SnapHandle, &ProcessEntry) == TRUE)
{
if (!_wcsicmp(ProcessEntry.szExeFile, ProcessName.c_str()))
{
HANDLE ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessEntry.th32ProcessID);
CloseHandle(SnapHandle);
return ProcessHandle;
}
}
}
}
CloseHandle(SnapHandle);
return nullptr ; //увы, игры нет, вернем нулевой указатель.
}
Хендл позволяет прочитать любую информацию внутри процесса, а также список его модулей. В Source описанные выше структуры создаются в модуле client.dll, давайте получим его адрес:
2. Получаем адрес модуля:
DWORD GetModuleAddressByName(const std::wstring &ModuleName, HANDLE Process)
{
MODULEENTRY32 ModuleEntry;
ModuleEntry.dwSize = sizeof(MODULEENTRY32);
HANDLE SnapHandle = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetProcessId(Process));
if (Module32First(SnapHandle, &ModuleEntry) == TRUE)
{
if (!_wcsicmp(ModuleEntry.szModule, ModuleName.c_str()))
{
auto ModuleAddress = reinterpret_cast<DWORD>(ModuleEntry.modBaseAddr);
return ModuleAddress;
}
else
{
while (Module32Next(SnapHandle, &ModuleEntry) == TRUE)
{
if (!_wcsicmp(ModuleEntry.szModule, ModuleName.c_str()))
{
auto ModuleAddress = reinterpret_cast<DWORD>(ModuleEntry.modBaseAddr);
return ModuleAddress;
}
}
}
}
CloseHandle(SnapHandle);
return NULL;
}
Работает это аналогично функции выше, так что описывать не буду.
Всё что мы получили дает нам стартовую базу для дальнейших действий. Чтобы что-то узнать, нужно об этом прочитать, давайте прочтем информацию из процесса:
3. Чтение переменных по адресу:
template <typename ReadType> void Read(HANDLE ProcessHandle, ReadType* Buffer, DWORD Address)
{
ReadProcessMemory(ProcessHandle, reinterpret_cast<LPVOID>(Address), Buffer, sizeof(ReadType), nullptr);
}
WinAPI функция ReadProcessMemory позволяет получить в буфер любой отрезок памяти процесса по заданному адресу, указанного размера. Достаточно просто и удобно, а данная обертка в виде шаблона избавит нас от необходимости задавать каждый раз считываемый тип. Функция несколько изменена для понятности, откуда берется хендл, в последствии его будем хранить как глобальную переменную, чтобы не передавать сотни раз в функцию одно и тоже значение, и обязательно даем обещание только читать его, не перезаписывать!
Осталось немного, просто взять то, что хранится в нашей оперативной памяти, а значит по праву принадлежит нам!
4. Считывание структуры из памяти:
void tLocalPlayerEntity::tLocalPlayerEntityFunctions::GetLocalPlayerEntityInfo(tLocalPlayerEntityInfo* LocalPlayerEntityInfo)
{
if (LocalPlayerEntityInfo)//получаем заготовку с адресом для чтения
{
LocalPlayerEntityInfo->Valid = false;//заранее говорим, что эта структура еще неверна, и может не оказаться верной
if (pIO.ClientModuleBaseAddress)//если у нас нет адреса client.dll, значит мы еще не присоединились, читать нечего
{
pIO.Functions.Read<DWORD>(&LocalPlayerEntityInfo->BaseAddress, pIO.ClientModuleBaseAddress + pGlobalVars.Offsets.m_dwLocalPlayer);//читаем базовый адрес
if (LocalPlayerEntityInfo->BaseAddress && pIO.EngineModuleBaseAddress)
{
// чтобы не гонять много раз системные функции, считаем один раз огромный кусок данных, а из него локально возьмем необходимое. DataChunk - массив из 5000 байтов, заполним его нулями, чтобы данные были гарантированно чистыми
ZeroMemory(&LocalPlayerEntityInfo->DataChunk, LocalPlayerEntityInfo->Sizes.Data);
pIO.Functions.Read<tDataStructs::tDataChunk>(&LocalPlayerEntityInfo->DataChunk, LocalPlayerEntityInfo->BaseAddress);
pIO.Functions.Read<DWORD>(&LocalPlayerEntityInfo->ClientStateBaseAddress, pIO.EngineModuleBaseAddress + pGlobalVars.Offsets.m_dwClientState);
if (LocalPlayerEntityInfo->ClientStateBaseAddress)
{
//считываем то, что осталось за бортом
pIO.Functions.Read<int>(&LocalPlayerEntityInfo->Index, LocalPlayerEntityInfo->ClientStateBaseAddress + pGlobalVars.Offsets.m_dwLocalPlayerIndex);
pIO.Functions.Read<D3DXVECTOR3>(&LocalPlayerEntityInfo->ViewAngle, LocalPlayerEntityInfo->ClientStateBaseAddress + pGlobalVars.Offsets.m_dwViewAngle);
pIO.Functions.Read<tDataStructs::tViewMatrix>(&LocalPlayerEntityInfo->ViewMatrix, pIO.ClientModuleBaseAddress + pGlobalVars.Offsets.m_dwViewMatrix);
//А всё остальное берем из полученного массива.
memmove_s(&LocalPlayerEntityInfo->Flags, (LocalPlayerEntityInfo->Sizes.Flags), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_fFlags], (LocalPlayerEntityInfo->Sizes.Flags));
memmove_s(&LocalPlayerEntityInfo->CrosshairEntityIndex, (LocalPlayerEntityInfo->Sizes.CrosshairEntityIndex), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_iCrossHairID], (LocalPlayerEntityInfo->Sizes.CrosshairEntityIndex));
memmove_s(&LocalPlayerEntityInfo->LifeState, (LocalPlayerEntityInfo->Sizes.LifeState), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_lifeState], (LocalPlayerEntityInfo->Sizes.LifeState));
memmove_s(&LocalPlayerEntityInfo->Origin, (LocalPlayerEntityInfo->Sizes.Origin), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_vecOrigin], (LocalPlayerEntityInfo->Sizes.Origin));
memmove_s(&LocalPlayerEntityInfo->PunchAngle, (LocalPlayerEntityInfo->Sizes.PunchAngle), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_vecPunch], (LocalPlayerEntityInfo->Sizes.PunchAngle));
memmove_s(&LocalPlayerEntityInfo->ShotsFired, (LocalPlayerEntityInfo->Sizes.ShotsFired), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_iShotsFired], (LocalPlayerEntityInfo->Sizes.ShotsFired));
memmove_s(&LocalPlayerEntityInfo->TeamNumber, (LocalPlayerEntityInfo->Sizes.TeamNumber), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_iTeamNum], (LocalPlayerEntityInfo->Sizes.TeamNumber));
memmove_s(&LocalPlayerEntityInfo->Velocity, (LocalPlayerEntityInfo->Sizes.Velocity), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_vecVelocity], (LocalPlayerEntityInfo->Sizes.Velocity));
memmove_s(&LocalPlayerEntityInfo->ViewOffset, (LocalPlayerEntityInfo->Sizes.ViewOffset), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_vecViewOffset], (LocalPlayerEntityInfo->Sizes.ViewOffset));
//считано успешно, значит структура правильная, запишем, что ее можно использовать
LocalPlayerEntityInfo->Valid = true;
}
}
}
}
}
Вот и всё, это выглядит страшновато, но под пристальным взглядом смысл у этой копипасты простой. То же самое выполняется и для GeneralPlayerEntity, но несколько иначе:
void tGeneralPlayerEntity::tGeneralPlayerEntityFunctions::GetGeneralPlayerEntityInfo(tGeneralPlayerEntityInfo* GeneralPlayerEntityInfo, const int& PlayerNumber)
{
if (GeneralPlayerEntityInfo)
{
GeneralPlayerEntityInfo->Valid = false;
if (pIO.ClientModuleBaseAddress)
{
pIO.Functions.Read<DWORD>(&GeneralPlayerEntityInfo->BaseAddress, pIO.ClientModuleBaseAddress + pGlobalVars.Offsets.m_dwEntityList + (pGlobalVars.Offsets.EntitySize * PlayerNumber));
if (GeneralPlayerEntityInfo->BaseAddress)
{
//Читаем первый кусок
ZeroMemory(&GeneralPlayerEntityInfo->DataChunk1, GeneralPlayerEntityInfo->Sizes.DataChunk1);
pIO.Functions.Read<tDataStructs::tDataChunk>(&GeneralPlayerEntityInfo->DataChunk1, GeneralPlayerEntityInfo->BaseAddress);
memmove(&GeneralPlayerEntityInfo->Dormant, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_bDormant], GeneralPlayerEntityInfo->Sizes.Dormant);
//Если игрок мертв или он наблюдатель, смысла читать его информацию нет
if (!GeneralPlayerEntityInfo->Dormant)
{
memmove(&GeneralPlayerEntityInfo->LifeState, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_lifeState], GeneralPlayerEntityInfo->Sizes.LifeState);
if (!GeneralPlayerEntityInfo->LifeState)
{
pIO.Functions.Read<DWORD>(&GeneralPlayerEntityInfo->GameResourcesBaseAddress, pIO.ClientModuleBaseAddress + pGlobalVars.Offsets.CSPlayerResource);
if (GeneralPlayerEntityInfo->GameResourcesBaseAddress)
{
//Если нам доступны его игровые данные - читаем
pIO.Functions.Read<tDataStructs::tDataChunk>(&GeneralPlayerEntityInfo->DataChunk2, GeneralPlayerEntityInfo->GameResourcesBaseAddress);
memmove_s(&GeneralPlayerEntityInfo->Kills, GeneralPlayerEntityInfo->Sizes.Kills, &GeneralPlayerEntityInfo->DataChunk2.Data[pGlobalVars.Offsets.m_iKills + ((PlayerNumber + 1) * GeneralPlayerEntityInfo->Sizes.Kills)], GeneralPlayerEntityInfo->Sizes.Kills);
memmove_s(&GeneralPlayerEntityInfo->CompetetiveRankNumber, GeneralPlayerEntityInfo->Sizes.CompetetiveRankNumber, &GeneralPlayerEntityInfo->DataChunk2.Data[pGlobalVars.Offsets.m_iCompetitiveRanking + ((PlayerNumber + 1) * GeneralPlayerEntityInfo->Sizes.CompetetiveRankNumber)], GeneralPlayerEntityInfo->Sizes.CompetetiveRankNumber);
memmove_s(&GeneralPlayerEntityInfo->Deaths, GeneralPlayerEntityInfo->Sizes.Deaths, &GeneralPlayerEntityInfo->DataChunk2.Data[pGlobalVars.Offsets.m_iDeaths + ((PlayerNumber + 1) * GeneralPlayerEntityInfo->Sizes.Deaths)], GeneralPlayerEntityInfo->Sizes.Deaths);
memmove_s(&GeneralPlayerEntityInfo->HasC4, GeneralPlayerEntityInfo->Sizes.HasC4, &GeneralPlayerEntityInfo->DataChunk2.Data[pGlobalVars.Offsets.m_iPlayerC4], GeneralPlayerEntityInfo->Sizes.HasC4);
memmove_s(&GeneralPlayerEntityInfo->HasDefuser, GeneralPlayerEntityInfo->Sizes.HasDefuser, &GeneralPlayerEntityInfo->DataChunk2.Data[pGlobalVars.Offsets.m_bHasDefuser + ((PlayerNumber + 1) * GeneralPlayerEntityInfo->Sizes.HasDefuser)], GeneralPlayerEntityInfo->Sizes.HasDefuser);
pIO.Functions.Read<DWORD>(&GeneralPlayerEntityInfo->RadarBaseAddress, pIO.ClientModuleBaseAddress + pGlobalVars.Offsets.m_dwRadarBase);
if (GeneralPlayerEntityInfo->RadarBaseAddress)
{
pIO.Functions.Read<DWORD>(&GeneralPlayerEntityInfo->RadarPointerBaseAddress, GeneralPlayerEntityInfo->RadarBaseAddress + pGlobalVars.Offsets.m_dwRadarBasePointer);
if (GeneralPlayerEntityInfo->RadarPointerBaseAddress)
{
memmove_s(&GeneralPlayerEntityInfo->BoneMatrixBaseAddress, sizeof(GeneralPlayerEntityInfo->BoneMatrixBaseAddress), &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_dwBoneMatrix], sizeof(GeneralPlayerEntityInfo->BoneMatrixBaseAddress));
if (GeneralPlayerEntityInfo->BoneMatrixBaseAddress)
{
pIO.Functions.Read<tDataStructs::tBoneMatrix>(&GeneralPlayerEntityInfo->BoneMatrix, GeneralPlayerEntityInfo->BoneMatrixBaseAddress);
pIO.Functions.Read<tDataStructs::tPlayerName>(&GeneralPlayerEntityInfo->PlayerName, GeneralPlayerEntityInfo->RadarPointerBaseAddress + (pGlobalVars.Offsets.RadarName1 * (PlayerNumber + 1) + pGlobalVars.Offsets.RadarName2));
memmove_s(&GeneralPlayerEntityInfo->Health, GeneralPlayerEntityInfo->Sizes.Health, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_iHealth], GeneralPlayerEntityInfo->Sizes.Health);
memmove_s(&GeneralPlayerEntityInfo->Flags, GeneralPlayerEntityInfo->Sizes.Flags, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_fFlags], GeneralPlayerEntityInfo->Sizes.Flags);
memmove_s(&GeneralPlayerEntityInfo->Origin, GeneralPlayerEntityInfo->Sizes.Origin, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_vecOrigin], GeneralPlayerEntityInfo->Sizes.Origin);
memmove_s(&GeneralPlayerEntityInfo->TeamNumber, GeneralPlayerEntityInfo->Sizes.TeamNumber, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_iTeamNum], GeneralPlayerEntityInfo->Sizes.TeamNumber);
memmove_s(&GeneralPlayerEntityInfo->Velocity, GeneralPlayerEntityInfo->Sizes.Velocity, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_vecVelocity], GeneralPlayerEntityInfo->Sizes.Velocity);
GeneralPlayerEntityInfo->Valid = true;
}
}
}
}
}
}
}
}
}
}
Единственное принципиальное отличие — что LocalPlayerEntity в памяти одна, а GeneralPlayerEntity — 64 штуки подряд (массив собственно). Причем заполнены они абсолютно случайным образом (могу ошибаться, если кто поправит — напишите) — к примеру, если на сервере 12 игроков, один из них вы, 5 союзников и шесть противников — индексы в этой структуре могут быть абсолютно разными — вплоть до того, что все могут получить индексы от 51 до 63, так что не получится просто считать первые 12 структур, нужно проверять каждую. Как видно, мы берем смещение, и с помощью адресной арифметики получаем нужную по номеру структуру, это главное отличие от предыдущей функции. Это позволяет воспользоваться циклом, в котором мы будем непрерывно читать все 64 структуры и сразу же выводить все полученные данные.
На этом всё, если данная тема будет интересна Хабру, напишу еще несколько статей о том, как получать смещения из клиента, как непосредственно показывать игроку всё, что мы получили в этой статье в удобоваримом виде, и как написать простой AimBot.
По материалам форума unknowncheats.me и чита с исходным кодом от Puddin Poppin.