Привет, Хабр! Представляю вашему вниманию перевод статьи Adventures in osu! game hacking.


Не так давно я начал играть в OSU! и она мне понравилась. Со временем захотелось немного поковыряться во внутренностях этой игры.


Основной анализ beatmap


Итак, как мы будем разбирать beatmap? Мы можем разобрать все, начиная от названия песни, заканчивая настройками сложности. (Мы будем держать вещи простыми и анализируем только моменты времени, объекты попадания и некоторые значения, относящиеся к слайдеру.)


В стандартном режиме игры мы имеем дело с тремя типами объектов: кругом попадание, ползунком и счетчиком. В документации для формата файла .osu указано, что все объекты имеют такие составляющие: X, Y, время, тип. Все они будут включены в нашу структуру.


Я не хочу останавливаться на этом разделе слишком долго, так как это просто чтение каждой строки, ее разделение и сохранение результатов.


Получение игрового времени


Существует несколько различных способов сделать это, но самый простой — с помощью Cheat Engine. Если вы параноик, как я, вы можете сделать эту часть в автономном режиме, в конце концов, было много известных случаев автоматических запретов, связанных с использованием Cheat Engine. По крайней мере, убедитесь, что вы вышли из своего OSU!, прежде чем продолжить.


Начните с открытия Cheat Engine. Если OSU! пока не запущена, запустите её сейчас. Нажмите на значок в верхнем левом углу, чтобы открыть список процессов, отсюда выберите OSU!.exe и нажмите „Attach debugger to process”. Вернитесь к OSU!.. Теперь убедитесь, что никакая музыка не играет. Вы можете сделать это в главном меню, щелкнув на значок остановки в правом верхнем углу.


Теперь вернитесь к Cheat Engine, введите 0 в поле «Значение» и выполните первое сканирование. Как только оно будет закончено, вы увидите больше миллиона результатов. Мы сократим это до нескольких. Вернитесь к OSU! и снова начните воспроизведение музыки. Теперь вернитесь к Cheat Engine, установите для типа сканирования значение «Увеличенное значение» и нажмите «Следующее сканирование». Это значительно уменьшит количество результатов. Продолжайте нажимать кнопку «Следующее сканирование», пока не останется с несколько результатов.


Мы почти получили его. Все, что осталось сейчас, — это динамически получать это значение. Вот почему мы использовали отладчик Cheat Engine раньше. Щелкните правой кнопкой мыши на каждый адрес и выберите <> в раскрывающемся меню. Некоторые из них нам не подходят, но вы должны найти тот, который при разборке выглядит аналогичным.


13654FA8 - DB 5D E8 - fistp dword ptr [ebp-18]
13654FAB - 8B 45 E8 - mov eax, [ebp-18]
13654FAE - A3 BC5D7705 - mov [05775DBC], eax
13654FB3 - 8B 35 94382104 - mov esi, [04213894]
13654FB9 - 85 F6 - тест esi, esi

Я загрузил базовый внешний сигнатурный сканер, который мы будем использовать позже в нашей реализации.


DB 5D E8 8B 45 E8 A3
— Regular or 'IDA-style' signature.
\ XDB \ X5D \ X Е8 \ x8B \ x45 \ X Е8 \ XA3
 — Code-style signature.
ххххххх
- — Code-style mask.

Обратите внимание, что указанная выше подпись относится только к каналу Stable (Latest) release. Подписи, вероятно, будут отличаться по каналам Stable (Fallback), Beta и Cutting Edge (Experimental), но процесс его поиска будет таким же, как и выше.


Реализация


Теперь нам нужно найти идентификатор процесса OSU! и обработать его. Существует много разных способов сделать это, но вероятно проще всего использовать CreateToolhelp32Snapshot, а также Process32Next для перебора списка процессов.


inline const DWORD get_process_id() {
DWORD process_id = NULL;
HANDLE process_list = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
PROCESSENTRY32 entry = {0};
entry.dwSize = sizeof PROCESSENTRY32;
if (Process32First(process_list, &entry)) {
while (Process32Next(process_list, &entry)) {
if (_wcsicmp(entry.szExeFile, L«osu!.exe») == 0) {
process_id = entry.th32ProcessID;
}
}
}
> 
CloseHandle(process_list);
return process_id;
};
game_process_id = get_process_id();
if (!game_process_id) {
return EXIT_FAILURE;
}

Теперь у нас есть идентификатор процесса, мы можем открыть дескриптор процесса.
Поскольку нам нужно только считать его память, мы будем использовать PROCESS_VM_READ, как желаемый флаг доступа.


game_process = OpenProcess (PROCESS_VM_READ, false, game_process_id);
if (! game_process) {
return false;
}

Это была большая часть скучного материала. Теперь нам нужны только адрес времени игры и способ отправки ключевых входов, прежде чем мы сможем продолжить. Для первого из них понадобиться подписи, которые мы сделали ранее.


> inline const DWORD find_time_address() {
// scan process memory for array of bytes.
DWORD time_ptr = FindPattern(game_process, PBYTE(TIME_SIGNATURE)) + 7;
DWORD time_address = NULL;
if (!ReadProcessMemory(game_process, LPCVOID(time_ptr), &time_address, sizeof DWORD, nullptr)) {
return false;
}
return time_address;
};
inline const int32_t get_elapsed_time() {
// read and return the elapsed time in the current beatmap.
int32_t current_time = NULL;
if (!ReadProcessMemory(game_process, LPCVOID(time_address), ¤t_time, sizeof int32_t, nullptr)) {
return false;
}
return current_time;
};

Для последней из этих вспомогательных функций нам понадобится что-то, что будет нажимать клавишу, когда мы ее вызываем. Опять же, есть несколько способов реализовать это, но я нашел keybd_event, но SendInput будет самым легким. Поскольку keybd_event устарел, мы будем использовать SendInput,


inline void set_key_pressed(char key, bool pressed) {
INPUT key_press = {0};
key_press.type = INPUT_KEYBOARD;
key_press.ki.wVk = VkKeyScanEx(key, GetKeyboardLayout(NULL)) & 0xFF;
key_press.ki.awScan = 0;
key_press.ki.dwExtraInfo = 0;
key_press.ki.dwFlags = (pressed? 0: KEYEVENTF_KEYUP);
SendInput(1, &key_press, sizeof INPUT);
}

Все, что осталось, это перебрать удаленные объекты и отправить входы по мере продвижения. Изначально мы находимся в начале beatmap. Теперь мы можем прочитать время, чтобы узнать, где мы находимся на самом деле.


size_t current_object = 0;
int32_t time = get_elapsed_time();
for (size_t i = 0; i < active_beatmap.hitobjects.size(); i++) {
if (active_beatmap.hitobjects.at(i).start_time > time) {
current_object = i;
break;
}
}

Обязательно добавьте проверку для карт с AudioLeadIn time.


while (current_object == 0 && get_elapsed_time() < active_beatmap.hitobjects.begin()->start_time) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}

Вот где начинается настоящая забава. Возможно, вы ожидали, что эта часть будет сложной, но логика здесь на самом деле довольно прямолинейна. Мы ждем 'start time' текущего объекта, удерживаем ключ, ждем 'end time’, а затем освобождаем его. После того, как мы выпустили ключ, мы переходим к следующему объекту и продолжаем, пока не достигнем конца beatmap.


hitobject& object = active_beatmap.hitobjects.at(current_object);
while (current_object < active_beatmap.hitobjects.size()) {
static bool key_down = false;
time = get_elapsed_time();
// hold key
if (time >= (object.start_time — 5) && !key_down) {
set_key_pressed('z', true);
key_down = true;
continue;
}
// release key
if (time > object.end_time && key_down) {
set_key_pressed('z', false);
key_down = false;
current_object++;
object = active_beatmap.hitobjects.at(current_object);
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}

Обратите внимание, что я вычитал пять миллисекунд со времени начала, это своего рода волшебное число, и ваш пробег может отличаться от него. Он не мог нажать на все кнопки и слайдеры идеально без этого. Я также добавляю две миллисекунды к концу окончания круга в классе beatmap. Поскольку круги не нужно удерживать, мы хотим как можно скорее отпустить их. Если мы отпустим их слишком быстро, нажатие может быть проигнорировано, поэтому нужны дополнительные 2 мс.


Ну, теперь мы готовы скомпилировать и протестировать OSU!Relax!

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


  1. slonopotamus
    12.12.2017 23:28

    Что я только что прочитал?

    Не так давно я начал играть в OSU! и она мне понравилась.

    Что такое OSU???

    Итак, как мы будем разбирать beatmap?

    Кого???

    Мы будем держать вещи простыми

    Мгимо финиш?

    Я не хочу останавливаться на этом разделе слишком долго, так как это просто чтение каждой строки, ее разделение и сохранение результатов.

    Что???

    По крайней мере, убедитесь, что вы вышли из своего OSU!, прежде чем продолжить. Начните с открытия Cheat Engine. Если OSU! пока не запущена, запустите её сейчас.

    Вы упоротые? Очевидно, OSU! не запущена, вы же сами только что сказали из неё выйти.

    Теперь убедитесь, что никакая музыка не играет.

    Играет музыка из кафе в соседнем доме. Что делать непонятно.

    Мы почти получили его.

    Кого???

    Он не мог нажать на все кнопки и слайдеры идеально без этого.

    Кто он???

    ваш пробег может отличаться от него

    Назовите хотя бы один сценарий при котором мой пробег мог бы с ним совпасть.

    И я так и не понял, что в результате дали все эти вот манипуляции?


    1. GrimMaple
      12.12.2017 23:50

      Отвечу за автора, заодно отвечу непосредственно автору.

      Что такое OSU???
      Google помог бы, но в целом — это такая игра.
      Кого???
      Beatmap, карту с расставленными нотами. Для игрока это очевидно, но автор поленился объяснить нормально, что за игра.
      Вы упоротые? Очевидно, OSU! не запущена, вы же сами только что сказали из неё выйти.
      Видимо, имелся ввиду внутриигровой аккаунт. Играть можно и без него.
      И я так и не понял, что в результате дали все эти вот манипуляции?

      Мы добились того, что реализовали уже давно реализованный в игре режим Relax. В osu! задача игрока под музыку навести курсор на кружочек и вовремя нажать кнопку, идея проста. Relax — это специальный режим игры, в котором нужно только наводить курсор на кружочки, щелкает игра сама. Т.е. автор, по сути, переизобрел режим игры, который уже итак есть в игре, но не через игру. Смысла в таком проекте просто ноль, что очень обидно, когда есть открытая реализация всей игры, и ей нужно немножко любви, чтобы привести ее в порядок. У текущего клиента куча проблем (впрочем, куча их и у альтернативного), но есть все шансы сделать альтернативный клиент удобнее и лучше, но вместо этого зачем-то изобретают то, что изобрел автор.


    1. izzholtik
      13.12.2017 10:44

      Эталонная реакция человека на пост OSUшника :D
      В Палату мер и весов.


      1. slonopotamus
        13.12.2017 10:53

        Все OSUшники разговаривают как гугл-транслейт?


    1. Smi1e
      14.12.2017 00:28

      Еще в копилку

      Как только оно будет закончено, вы увидите больше миллиона результатов. Мы сократим это до нескольких.