OSU! Relax (основы)

в 19:31, , рубрики: c++, OSU!

Привет! Представляю вашему вниманию перевод статьи 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!

Автор: WitchLoveBoriK

Источник

Поделиться

* - обязательные к заполнению поля