В этом посте мы расскажем о процессе создания aimbot — программы, автоматически прицеливающейся во врагов в игре жанра «шутер от первого лица» (FPS). Создавать будем aimbot для игры Half-Life 2, работающей на движке Source. Aimbot будет работать внутри процесса игры и использовать для своей работы внутренние функции игры, подвергнутые реверс-инжинирингу (в отличие от других систем, работающих снаружи игры и сканирующих экран).

Для начала изучим Source SDK и используем его как руководство для реверс-инжиниринга исполняемого файла Half-Life 2. Затем мы применим полученные данные, для создания бота. К концу статьи у нас будет написан aimbot, привязывающий прицел игрока к ближайшему врагу.

▍ Реверс-инжиниринг и Source SDK


Давайте рассмотрим, как выполнить реверс-инжиниринг исполняемого файла, чтобы найти нужную нам информацию. Для создания кода, который будет автоматически прицеливаться в мишень, нужно знать следующее:

  • Позицию глаз игрока
  • Позицию глаз ближайшего врага
  • Вектор от глаза игрока к глазу врага (получаемый по двум предыдущим пунктам)
  • Способ изменения положения камеры игрока так, чтобы глаз игрока смотрел по вектору к глазу врага

Для всего, кроме третьего пункта, требуется получать информацию от состояния запущенной игры. Эту информацию получают реверс-инжинирингом игры для поиска классов, содержащих соответствующие поля. Обычно это оказывается чрезвычайно трудоёмкой задачей со множеством проб и ошибок; однако в данном случае у нас есть доступ к Source SDK, который мы используем в качестве руководства.

Начнём поиск с нахождения ссылок на позицию глаз в репозитории. Изучив несколько страниц с результатами поиска, мы выйдем на огромный класс CBaseEntity. Внутри этого класса есть две функции:

virtual Vector EyePosition(void);
virtual const QAngle &EyeAngles(void);

Так как CBaseEntity является базовым классом, от которого происходят все сущности (entity) игры, и он содержит члены для позиции глаза и углов камеры, то похоже, именно с ним нам и нужно работать. Дальше нам нужно посмотреть, откуда ссылаются на эти функции. Снова немного поискав на GitHub Source SDK, мы находим интерфейс IServerTools, у которого есть несколько весьма многообещающих функций:

virtual IServerEntity *GetIServerEntity(IClientEntity *pClientEntity) = 0;
virtual bool SnapPlayerToPosition(const Vector &org, const QAngle &ang, IClientEntity *pClientPlayer = NULL) = 0;
virtual bool GetPlayerPosition(Vector &org, QAngle &ang, IClientEntity  *pClientPlayer = NULL) = 0;

// ...

virtual CBaseEntity *FirstEntity(void) = 0;
virtual CBaseEntity *NextEntity(CBaseEntity *pEntity) = 0;

В этом интерфейсе очень удобно то, что он предоставляет доступ к позиции локального игрока, что позволяет привязать игрока к другой позиции и углу обзора, а также обеспечивает возможность итеративно обходить сущности. К тому же его экземпляр создаётся глобально и привязан к жёстко прописанной в коде строке.

#define VSERVERTOOLS_INTERFACE_VERSION_1	"VSERVERTOOLS001"
#define VSERVERTOOLS_INTERFACE_VERSION_2	"VSERVERTOOLS002"
#define VSERVERTOOLS_INTERFACE_VERSION		"VSERVERTOOLS003"
#define VSERVERTOOLS_INTERFACE_VERSION_INT	3

// ...

EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerTools, IServerTools001, VSERVERTOOLS_INTERFACE_VERSION_1, g_ServerTools);
EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerTools, IServerTools, VSERVERTOOLS_INTERFACE_VERSION, g_ServerTools);

Можем начать разработку aimbot с поиска этого класса в памяти. Запустив Half-Life 2 и подключив к нему отладчик, поищем строковые ссылки на VSERVERTOOLS.


Мы видим, откуда на них ссылаются:

7BCAB090 | 68 88FA1F7C              | push server.7C1FFA88                    | 7C1FFA88:"VSERVERTOOLS001"
7BCAB095 | 68 00C4087C              | push server.7C08C400                    |
7BCAB09A | B9 B02A337C              | mov ecx,server.7C332AB0                 |
7BCAB09F | E8 8CCA3F00              | call server.7C0A7B30                    |
7BCAB0A4 | C3                       | ret                                     |
7BCAB0A5 | CC                       | int3                                    |
7BCAB0A6 | CC                       | int3                                    |
7BCAB0A7 | CC                       | int3                                    |
7BCAB0A8 | CC                       | int3                                    |
7BCAB0A9 | CC                       | int3                                    |
7BCAB0AA | CC                       | int3                                    |
7BCAB0AB | CC                       | int3                                    |
7BCAB0AC | CC                       | int3                                    |
7BCAB0AD | CC                       | int3                                    |
7BCAB0AE | CC                       | int3                                    |
7BCAB0AF | CC                       | int3                                    |
7BCAB0B0 | 68 98FA1F7C              | push server.7C1FFA98                    | 7C1FFA98:"VSERVERTOOLS002"
7BCAB0B5 | 68 00C4087C              | push server.7C08C400                    |
7BCAB0BA | B9 BC2A337C              | mov ecx,server.7C332ABC                 |
7BCAB0BF | E8 6CCA3F00              | call server.7C0A7B30                    |
7BCAB0C4 | C3                       | ret                                     |

В ассемблерном листинге видно, что функция-член server.7C0A7B30 вызывается в server.7C332AB0 и server.7C332ABC. Эта функция получает два аргумента, один из которых — это имя-строка интерфейса. После изучения отладчика становится понятно, что второй параметр — это статический экземпляр чего-то.


Посмотрев на то, что делает в коде макрос EXPOSE_SINGLE_INTERFACE_GLOBALVAR, становится понятнее, что это синглтон CServerTools, предоставленный как глобальный интерфейс. Зная это, мы легко сможем получить указатель на этот синглтон в среде исполнения: мы просто берём адрес этой псевдофункции, перемещающей указатель в EAX, и вызываем её напрямую. Чтобы сделать это, можно написать следующий дженерик-код, который мы продолжим применять для нового использования других функций:

template <typename T>
T GetFunctionPointer(const std::string moduleName, const DWORD_PTR offset) {

    auto moduleBaseAddress{ GetModuleHandleA(moduleName.c_str()) };
    if (moduleBaseAddress == nullptr) {
        std::cerr << "Could not get base address of " << moduleName
            << std::endl;
        std::abort();
    }
    return reinterpret_cast<T>(
        reinterpret_cast<DWORD_PTR>(moduleBaseAddress) + offset);
}

IServerTools* GetServerTools() {

    constexpr auto globalServerToolsOffset{ 0x3FC400 };
    static GetServerToolsFnc getServerToolsFnc{ GetFunctionPointer<GetServerToolsFnc>(
        "server.dll", globalServerToolsOffset) };

    return getServerToolsFnc();
}

Здесь мы берём базовый адрес, в который загружена server.dll, добавляем смещение, чтобы попасть туда, откуда можно получить доступ к синглтону CServerTools, и возвращаем его как указатель вызывающей функции. Благодаря этому мы сможем вызывать нужные нам функции в интерфейсе и игра будет реагировать соответствующим образом. Нас интересуют две функции: GetPlayerPosition и SnapPlayerToPosition.

Внутри GetPlayerPosition при помощи вызова UTIL_GetLocalPlayer получается класс локального игрока, а также вызываются EyePosition и EyeAngles; внутри SnapPlayerToPosition при помощи SnapEyeAngles корректируются углы обзора игрока. Всё вместе это даёт нам то, что необходимо для получения позиций и углов обзора сущностей, благодаря чему можно выполнить соответствующие вычисления нового вектора и угла обзора, привязывающихся к глазам врагов.

Давайте разбираться по порядку, начнём с GetPlayerPosition. Так как мы можем получить указатель на IServerTools и имеем определение интерфейса, можно выполнить явный вызов GetPlayerPosition и пошагово пройтись по вызову при помощи отладчика. При этом мы попадём сюда:

7C08BEF0 | 55                       | push ebp                                |
7C08BEF1 | 8BEC                     | mov ebp,esp                             |
7C08BEF3 | 8B01                     | mov eax,dword ptr ds:[ecx]              |
7C08BEF5 | 83EC 0C                  | sub esp,C                               |
7C08BEF8 | 56                       | push esi                                |
7C08BEF9 | FF75 10                  | push dword ptr ss:[ebp+10]              |
7C08BEFC | FF50 04                  | call dword ptr ds:[eax+4]               |
7C08BEFF | 8BF0                     | mov esi,eax                             |
7C08BF01 | 85F6                     | test esi,esi                            |
7C08BF03 | 75 14                    | jne server.7C08BF19                     |
7C08BF05 | E8 E616E7FF              | call server.7BEFD5F0                    |
7C08BF0A | 8BF0                     | mov esi,eax                             |
7C08BF0C | 85F6                     | test esi,esi                            |
7C08BF0E | 75 09                    | jne server.7C08BF19                     |
7C08BF10 | 32C0                     | xor al,al                               |
7C08BF12 | 5E                       | pop esi                                 |
7C08BF13 | 8BE5                     | mov esp,ebp                             |
7C08BF15 | 5D                       | pop ebp                                 |
7C08BF16 | C2 0C00                  | ret C                                   |
7C08BF19 | 8B06                     | mov eax,dword ptr ds:[esi]              |
7C08BF1B | 8D4D F4                  | lea ecx,dword ptr ss:[ebp-C]            |
7C08BF1E | 51                       | push ecx                                |
7C08BF1F | 8BCE                     | mov ecx,esi                             |
7C08BF21 | FF90 08020000            | call dword ptr ds:[eax+208]             |
7C08BF27 | 8B4D 08                  | mov ecx,dword ptr ss:[ebp+8]            |
7C08BF2A | D900                     | fld st(0),dword ptr ds:[eax]            |
7C08BF2C | D919                     | fstp dword ptr ds:[ecx],st(0)           |
7C08BF2E | D940 04                  | fld st(0),dword ptr ds:[eax+4]          |
7C08BF31 | D959 04                  | fstp dword ptr ds:[ecx+4],st(0)         |
7C08BF34 | D940 08                  | fld st(0),dword ptr ds:[eax+8]          |
7C08BF37 | 8B06                     | mov eax,dword ptr ds:[esi]              |
7C08BF39 | D959 08                  | fstp dword ptr ds:[ecx+8],st(0)         |
7C08BF3C | 8BCE                     | mov ecx,esi                             |
7C08BF3E | FF90 0C020000            | call dword ptr ds:[eax+20C]             |
7C08BF44 | 8B4D 0C                  | mov ecx,dword ptr ss:[ebp+C]            |
7C08BF47 | 5E                       | pop esi                                 |
7C08BF48 | D900                     | fld st(0),dword ptr ds:[eax]            |
7C08BF4A | D919                     | fstp dword ptr ds:[ecx],st(0)           |
7C08BF4C | D940 04                  | fld st(0),dword ptr ds:[eax+4]          |
7C08BF4F | D959 04                  | fstp dword ptr ds:[ecx+4],st(0)         |
7C08BF52 | D940 08                  | fld st(0),dword ptr ds:[eax+8]          |
7C08BF55 | B0 01                    | mov al,1                                |
7C08BF57 | D959 08                  | fstp dword ptr ds:[ecx+8],st(0)         |
7C08BF5A | 8BE5                     | mov esp,ebp                             |
7C08BF5C | 5D                       | pop ebp                                 |
7C08BF5D | C2 0C00                  | ret C                                   |

Разбираться в этом довольно долго, однако в виде графа потока управления всё выглядит довольно просто:


Если построчно сопоставить дизассемблированную программу с кодом, то мы достаточно быстро найдём необходимое. Код вызывает функцию UTIL_GetLocalPlayer только тогда, когда переданный параметр pClientEntity равен null. Эта логика проверяется в блоке первой функции графа. Если существует действительная клиентская сущность, код переходит к получению позиции и углов глаза для неё, а в противном случае получает сущность локального игрока. Этот вызов происходит при исполнении команды call server.7BEFD5F0 по адресу server.7C08BF05. Как и ранее, мы можем создать указатель функции на UTIL_GetLocalPlayer и вызывать её напрямую.

CBasePlayer* GetLocalPlayer() {

    constexpr auto globalGetLocalPlayerOffset{ 0x26D5F0 };
    static GetLocalPlayerFnc getLocalPlayerFnc{ GetFunctionPointer<GetLocalPlayerFnc>(
        "server.dll", globalGetLocalPlayerOffset) };

    return getLocalPlayerFnc();
}

Следующими в дизассемблированном коде идут вызовы функций EyePosition и EyeAngles. Нас интересует только получение позиций глаза, поэтому важен только первый вызов. Для получения адреса функции мы можем пошагово пройти по вызову, пока не вызовем адрес, находящийся в [EAX+0x208]. После исполнения этой команды мы перейдём на server.dll+0x119D00, таким образом узнав, где находится функция.

Vector GetEyePosition(CBaseEntity* entity) {
    
    constexpr auto globalGetEyePositionOffset{ 0x119D00 };
    static GetEyePositionFnc getEyePositionFnc{ GetFunctionPointer<GetEyePositionFnc>(
        "server.dll", globalGetEyePositionOffset) };

    return getEyePositionFnc(entity);
}

И это всё, что нам нужно от GetPlayerPosition; теперь у нас есть возможность получения указателя локальной сущности игрока и получения позиции глаза сущности. Последнее, что нам потребуется — возможность задания угла обзора игрока. Как говорилось выше, это можно сделать, вызвав функцию SnapPlayerToPosition и посмотрев, где находится функция SnapEyeAngles. Дизассемблированный код SnapEyeAngles выглядит следующим образом:

7C08C360 | 55                       | push ebp                                |
7C08C361 | 8BEC                     | mov ebp,esp                             |
7C08C363 | 8B01                     | mov eax,dword ptr ds:[ecx]              |
7C08C365 | 83EC 0C                  | sub esp,C                               |
7C08C368 | 56                       | push esi                                |
7C08C369 | FF75 10                  | push dword ptr ss:[ebp+10]              |
7C08C36C | FF50 04                  | call dword ptr ds:[eax+4]               |
7C08C36F | 8BF0                     | mov esi,eax                             |
7C08C371 | 85F6                     | test esi,esi                            |
7C08C373 | 75 14                    | jne server.7C08C389                     |
7C08C375 | E8 7612E7FF              | call server.7BEFD5F0                    |
7C08C37A | 8BF0                     | mov esi,eax                             |
7C08C37C | 85F6                     | test esi,esi                            |
7C08C37E | 75 09                    | jne server.7C08C389                     |
7C08C380 | 32C0                     | xor al,al                               |
7C08C382 | 5E                       | pop esi                                 |
7C08C383 | 8BE5                     | mov esp,ebp                             |
7C08C385 | 5D                       | pop ebp                                 |
7C08C386 | C2 0C00                  | ret C                                   |
7C08C389 | 8B06                     | mov eax,dword ptr ds:[esi]              |
7C08C38B | 8BCE                     | mov ecx,esi                             |
7C08C38D | FF90 24020000            | call dword ptr ds:[eax+224]             |
7C08C393 | 8B4D 08                  | mov ecx,dword ptr ss:[ebp+8]            |
7C08C396 | F3:0F1001                | movss xmm0,dword ptr ds:[ecx]           |
7C08C39A | F3:0F5C00                | subss xmm0,dword ptr ds:[eax]           |
7C08C39E | F3:0F1145 F4             | movss dword ptr ss:[ebp-C],xmm0         |
7C08C3A3 | F3:0F1041 04             | movss xmm0,dword ptr ds:[ecx+4]         |
7C08C3A8 | F3:0F5C40 04             | subss xmm0,dword ptr ds:[eax+4]         |
7C08C3AD | F3:0F1145 F8             | movss dword ptr ss:[ebp-8],xmm0         |
7C08C3B2 | F3:0F1041 08             | movss xmm0,dword ptr ds:[ecx+8]         |
7C08C3B7 | 8BCE                     | mov ecx,esi                             |
7C08C3B9 | F3:0F5C40 08             | subss xmm0,dword ptr ds:[eax+8]         |
7C08C3BE | 8D45 F4                  | lea eax,dword ptr ss:[ebp-C]            |
7C08C3C1 | 50                       | push eax                                |
7C08C3C2 | F3:0F1145 FC             | movss dword ptr ss:[ebp-4],xmm0         |
7C08C3C7 | E8 14CFD0FF              | call server.7BD992E0                    |
7C08C3CC | FF75 0C                  | push dword ptr ss:[ebp+C]               |
7C08C3CF | 8BCE                     | mov ecx,esi                             |
7C08C3D1 | E8 4A0FE0FF              | call server.7BE8D320                    |
7C08C3D6 | 8B06                     | mov eax,dword ptr ds:[esi]              |
7C08C3D8 | 8BCE                     | mov ecx,esi                             |
7C08C3DA | 6A FF                    | push FFFFFFFF                           |
7C08C3DC | 6A 00                    | push 0                                  |
7C08C3DE | FF90 88000000            | call dword ptr ds:[eax+88]              |
7C08C3E4 | B0 01                    | mov al,1                                |
7C08C3E6 | 5E                       | pop esi                                 |
7C08C3E7 | 8BE5                     | mov esp,ebp                             |
7C08C3E9 | 5D                       | pop ebp                                 |
7C08C3EA | C2 0C00                  | ret C                                   |

Повторив уже описанный выше процесс, мы выясним, что команда call server.7BE8D320 является вызовом SnapEyeAngles. Мы можем определить следующую функцию:

void SnapEyeAngles(CBasePlayer* player, const QAngle& angles)
{
    constexpr auto globalSnapEyeAnglesOffset{ 0x1FD320 };
    static SnapEyeAnglesFnc snapEyeAnglesFnc{ GetFunctionPointer<SnapEyeAnglesFnc>(
        "server.dll", globalSnapEyeAnglesOffset) };

    return snapEyeAnglesFnc(player, angles);
}

Итак, теперь у нас есть всё необходимое.

▍ Создание aimbot


Для создания aimbot нам нужно следующее:

  • Итеративно обойти сущности
    • Если сущность является врагом, то найти расстояние между сущностью и игроком
    • Отслеживать ближайшую сущность
  • Вычислить вектор «глаз-глаз» между игроком и ближайшим врагом
  • Корректировать углы глаза игрока так, чтобы он следовал за этим вектором

Ранее мы узнали, что для получения позиции игрока можно вызвать GetPlayerPosition. Для циклического обхода списка сущностей можно вызвать FirstEntity и NextEntity, которые возвращают указатель на экземпляр CBaseEntity. Чтобы понять, является ли сущность врагом, можно сравнить имя сущности со множеством имён враждебных NPC-сущностей. Если мы получили сущность врага, то вычисляем расстояние между игроком и сущностью и сохраняем позицию сущности, если она ближайшая из всех, пока найденных нами.

После итеративного обхода всего списка сущностей мы получаем ближайшего врага, вычисляем вектор «глаз-глаз» и корректируем углы глаза игрока при помощи функции VectorAngles.

В виде кода получим следующее:

auto* serverEntity{ reinterpret_cast<IServerEntity*>(
    GetServerTools()->FirstEntity()) };

if (serverEntity != nullptr) {
    do {
        if (serverEntity == GetServerTools()->FirstEntity()) {

            SetPlayerEyeAnglesToPosition(closestEnemyVector);
            closestEnemyDistance = std::numeric_limits<float>::max();
            closestEnemyVector = GetFurthestVector();
        }

        auto* modelName{ serverEntity->GetModelName().ToCStr() };
        if (modelName != nullptr) {
            auto entityName{ std::string{GetEntityName(serverEntity)} };

            if (IsEntityEnemy(entityName)) {
                Vector eyePosition{};
                QAngle eyeAngles{};

                GetServerTools()->GetPlayerPosition(eyePosition, eyeAngles);

                auto enemyEyePosition{ GetEyePosition(serverEntity) };

                auto distance{ VectorDistance(enemyEyePosition, eyePosition) };
                if (distance <= closestEnemyDistance) {
                    closestEnemyDistance = distance;
                    closestEnemyVector = enemyEyePosition;
                }
            }
        }

        serverEntity = reinterpret_cast<IServerEntity*>(
            GetServerTools()->NextEntity(serverEntity));

    } while (serverEntity != nullptr);
}

В коде есть несколько вспомогательных функций, которые мы ранее не рассматривали: функция GetFurthestVector возвращает вектор с максимальными значениями float в полях x, y и z; GetEntityName возвращает имя сущности в виде строки, получая член m_iName экземпляра CBaseEntity; а IsEntityEnemy просто сверяет имя сущности со множеством враждебных NPC.

Векторные вычисления и расчёт нового угла обзора происходят в показанной ниже SetPlayerEyeAnglesToPosition:

void SetPlayerEyeAnglesToPosition(const Vector& enemyEyePosition) {

    Vector eyePosition{};
    QAngle eyeAngles{};
    GetServerTools()->GetPlayerPosition(eyePosition, eyeAngles);

    Vector forwardVector{ enemyEyePosition.x - eyePosition.x,
        enemyEyePosition.y - eyePosition.y,
        enemyEyePosition.z - eyePosition.z
    };

    VectorNormalize(forwardVector);

    QAngle newEyeAngles{};
    VectorAngles(forwardVector, newEyeAngles);

    SnapEyeAngles(GetLocalPlayer(), newEyeAngles);
}

Эта функция вычисляет вектор «глаз-глаз», вычитая из вектора позиции глаза врага вектор позиции глаза игрока. Затем этот новый вектор нормализуется и передаётся функции VectorAngles для вычисления новых углов обзора. Затем углы глаза игрока корректируются под эти новые углы, что должно создавать эффект слежения за сущностью.

Как это выглядит в действии?


Вы видите почти прозрачный прицел, следующий за головой идущего по комнате NPC. Когда NPC отходит достаточно далеко, код выполняет привязку к более близкому NPC. Всё работает!

▍ Заключение


Описанные в статье методики в общем случае применимы к любой игре жанра FPS. Способ получения позиций и углов может различаться в разных игровых движках, однако векторные вычисления для создания угла обзора из вектора расстояния применимы в любом случае.

Реверс-инжиниринг Half-Life 2 был сильно упрощён открытостью Source SDK. Возможность сопоставления кода и структур данных с ассемблерным кодом существенно упростила отладку, однако обычно такого везения не бывает! Надеюсь, эта статья помогла вам понять, как работают aimbot и показала, что создавать их не очень сложно.

Полный исходный код aimbot выложен на GitHub, можете свободно с ним экспериментировать.

Комментарии (13)


  1. andrijzuz
    11.07.2022 16:45
    +3

    Интересно


  1. sappience
    11.07.2022 20:50
    +4

    Недели не прошло с того момента как программера написавшего бот для автоприцеливания осудили на два с половиной года ограничения свободы: https://habr.com/en/news/t/675440/
    Так что вы поосторожней тут.


    1. MomoDev
      12.07.2022 02:25
      +1

      На гитхабе уже давно лежат читы с тысячами звёзд

      И не то что бы автор как то этого стеснялся)


    1. 1mexanikivan
      12.07.2022 07:46
      +2

      Это же сингл игра. Данный Aimbot пригодится скорее для использования в записи разных Half-Life SFM


      1. VXP
        12.07.2022 13:23
        +1

        Вы знаете, что вообще означает SFM? Может, имели ввиду "машиним на движке игры"?


    1. FlashHaos
      12.07.2022 14:37
      +2

      Осудили программера, продававшего бот для автоприцеливания.


  1. SerJook
    11.07.2022 21:15
    +1

    А я вот не понял. Я вижу в коде константы смещения. Они одинаковые при каждом запуске?

    Привязан ли этот код к версии Half Life 2?

    Как можно подключить эту библиотеку к игре?


    1. boris768
      12.07.2022 01:29
      +2

      Смещения указаны относительно базового адреса модуля (client.dll/engine.dll/прочие), пока сами эти файлы не изменятся - смещения останутся прежними, при запуске остается их прибавить к базовому адресу
      инжектором DLL (Process Hacker, Cheat Engine, Extreme Injector), либо отредактировать импорты в hl2.exe, добавив эту библиотеку


    1. 0x131315
      12.07.2022 10:46
      +1

      Удивительно, но как показывает практика, в процессе жизни игры код практически не меняется.

      Когда-то много лет назад, начитавшись статей с васма, ради академического интереса делал что-то похожее под живую онлайн-игрушку в ольке. Естественно в паблик это не пошло и было заброшено. Но когда спустя несколько лет (и много релизов игрушки) случайно откопал это творение, стало любопытно, а работает ли оно еще? Оказалось работает.

      Там, где такое быстро отваливается - там прикладывают значительные усилия чтобы это отваливалось. Сам по себе код практически не меняется, но меняется контент. Яркий пример тому - GTA 5 с его знаменитым багом, который за много лет никто не почесался исправить.


      1. Rubilnik
        12.07.2022 15:47

        Что же за баг такой, интересно?)


        1. boris768
          12.07.2022 16:00

          1. Rubilnik
            12.07.2022 16:06

            А да этот помню, всегда был в недоумении от таких долгих загрузок


  1. SandroSmith
    13.07.2022 01:10
    +1

    Вроде не заметил в описании - а если ближайшая враждебная сущность за стенкой?

    Надо бы ещё оценивать вектор глаз-глаз на предмет коллизии.