Недавно игрался в забавную игру-головоломку на iPad под названием The Incredible Machine, игра понравилась, поэтому решил поискать что-то аналогичное на PC. Обнаружилось, что эта игра является переделкой старой серии игр. Окинув взглядом серию, решил скачать The Incredible Machine 3 под Windows, обладающую довольно сносной графикой на мой вкус.
Отличная игра, но обнаружился небольшой негативный момент, состоящий в том, что в качестве саундтрека выступали MIDI-файлы, несмотря на наличие качественных композиций в CD-версии игры (согласно Wiki). Неприятность была списана на недосмотр со стороны разработчиков, разместивших игру на GOG.com (откуда она и была взята изначально), однако качественный саундтрек с CD-версии всё-таки поставлялся в виде набора MP3-файлов, но без очевидной возможности интеграции его в игру. Я решил исправить это досадное упущение и реализовать костыль, позволяющий играть в игру и наслаждаться качественным звуком.
Для начала нам необходимо провести небольшое исследование игры, чтобы определить, где хранится информация о текущем проигрываемом треке, чтобы потом реализовать тривиальную программу, которая будет считывать её и воспроизводить необходимый MP3-файл.
Начнем с наивного пути, запустим игру под отладчиком (например, OllyDbg), начнем проходить какой-нибудь уровень и посмотрим имя проигрываемого в данный момент трека. На уровне, где в данный момент находился я, это был трек с названием Pictures. Откроем карту памяти в отладчике и поищем это название. Натыкаемся на любопытную таблицу:
Мы видим перечень треков игры, хотя часть названий из списка не встречается в меню выборе трека для текущего уровня. Интересных мест с упоминанием названия трека в памяти больше не находится. Сделаем логичное предположение, что игра оперирует ID трека, а эта таблица необходима для сопоставления ID - Название. Сохраним эту табличку куда-нибудь, она нам ещё пригодится.
1000 TIM
1001 Unplugged
1007 Steel Drums
1002 New Age
1003 Hay Seed
1004 Progressive Rock
1005 Salsa
1006 Techno Rave
1013 1959 Prom
1011 Bongo Bango
1021 Ragtime
1014 Hip Hop
1012 Keep Tryin'
1017 Detective Theme
1015 Dreams
1016 Tuna Loaf
1018 60's Rock
1019 Pictures
1020 Huey Dewey
Кстати, если отвлечься и посмотреть содержимое архива с саундтреком, то мы увидим, что треки в нём идут в том же порядке, несмотря на непоследовательность ID.
Продолжим исследование. Попробуем оттолкнуться от того, как игра воспроизводит MIDI-файлы. MSDN намекает, на возможное использование WinAPI-функции midiOutOpen перед непосредственным проигрыванием композиции, также dx подсказал, что функция waveOutWrite тоже является возможным кандидатом на использование. Поставим точки останова на эти функции и попробуем поиграть. Ловим срабатывание точки на функции waveOutWrite.
Видим, что обращение произошло из модуля SOS9502. Действительно, в директории с игрой присутствует SOS9502.DLL, которая используется для проигрывания треков. Посмотрим таблицу экспортов этой библиотеки:
Солидная таблица, а в самом низу находится функция с занимательным именем sosMIDIStartSong (мы ведь помним, что в игре используется MIDI-саундтрек). Попробуем поставить на этой функции точку останова. Анализируем стек вызовов:
Отмечаем подозрительный аргумент 000003EA, который в десятичном виде соответствует числу 1002 или треку New Age из таблицы выше. Рассмотрим функцию по адресу 0041126D подробнее. В ней мы видим следующую конструкцию:
Перед нами простой switch-case, который объясняет отсутствие части треков в настройках звука в игре. Его можно представить следующим кодом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
switch(track_id) { case 1007: track_id = 1011; break; case 1005: track_id = 1021; break; case 1014: track_id = 1017; break; case 1015: track_id = 1002; break; } |
Треки с идентификаторами 1007, 1005, 1014 и 1015 действительно отсутствуют в настройках, хотя имеются в официальном саундтреке с диска. Также обращаем внимание на строку:
1 |
MOV DWORD PTR DS:[481390],ESI |
Налицо работа с глобальной переменной, куда сохраняется идентификатор проигрываемого трека. В этом нетрудно убедиться опытным путем. Исследование завершено.
Теперь напишем простую программу, которая будет читать эту переменную и проигрывать нужный MP3-файл. Но для начала давайте переименуем файлы саундтрека, чтобы имя состояло только из идентификатора и расширения. Руками это утомительно, поэтому сделаем простенький скрипт на Perl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
use strict; use warnings; use File::Copy; # Ищем все .mp3-файлы в текущей директории my @files = glob "*.mp3"; # Составляем список идентификаторов треков my @ids = map {/^(\d+)\s/ && $1} <DATA>; # Переименовываем файлы последовательно, # так как порядок треков в саундтреке и директории соответствует порядку в таблице for(my $i = 0; $i < scalar @files; $i++) { move $files[$i], $ids[$i].'.mp3'; } __DATA__ 1000 TIM 1001 Unplugged 1007 Steel Drums 1002 New Age 1003 Hay Seed 1004 Progressive Rock 1005 Salsa 1006 Techno Rave 1013 1959 Prom 1011 Bongo Bango 1021 Ragtime 1014 Hip Hop 1012 Keep Tryin' 1017 Detective Theme 1015 Dreams 1016 Tuna Loaf 1018 60's Rock 1019 Pictures 1020 Huey Dewey |
Запускаем скрипт в директории с треками и получаем то, что хотели. Вернемся к программе.
Так как нам необходимо проигрывать mp3, то надо озаботиться выбором какой-нибудь библиотеки, которая позволяет это делать (проигрывание средствами Windows, например, с помощью mciSendString - не слишком надежное и плохо переваривает некоторые файлы). Я выбрал BASS Audio Library. Переходим к коду:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
#include <stdio.h> #include <Windows.h> #include <Tlhelp32.h> #include "bass.h" #pragma comment(lib, "bass.lib") /* Имя директории, где будут лежать файлы саундтрека */ #define MUSIC_DIR L"music" /* Адрес, содержащий ID проигрываемого трека */ #define MEMORY_OFFSET 0x481390 typedef struct { DWORD id; char name[32]; } track_entry; /* Список треков и их ID */ track_entry track_list[] = { {1000, "TIM"}, {1001, "Unplugged"}, {1007, "Steel Drums"}, {1002, "New Age"}, {1003, "Hay Seed"}, {1004, "Progressive Rock"}, {1005, "Salsa"}, {1006, "Techno Rave"}, {1013, "1959 Prom"}, {1011, "Bongo Bango"}, {1021, "Ragtime"}, {1014, "Hip Hop"}, {1012, "Keep Tryin'"}, {1017, "Detective Theme"}, {1015, "Dreams"}, {1016, "Tuna Loaf"}, {1018, "60's Rock"}, {1019, "Pictures"}, {1020, "Huey Dewey"} }; /* Вспомогательная функция для получения PID процесса */ DWORD get_pid_by_name(LPCTSTR name) { PROCESSENTRY32 pe32; DWORD pid = 0; HANDLE ss; ss = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPMODULE32, 0); if(ss == INVALID_HANDLE_VALUE) return 0; pe32.dwSize = sizeof(PROCESSENTRY32); if(!Process32First(ss, &pe32)) { CloseHandle(ss); return 0; } do { if(!lstrcmp(name, pe32.szExeFile)) { pid = pe32.th32ProcessID; break; } } while(Process32Next(ss, &pe32)); CloseHandle(ss); return pid; } /* Функция поиска имени трека по ID в списке треков */ const char * get_track_name_by_id(DWORD id) { int i; for(i = 0; i < _countof(track_list); i++) { if(id == track_list[i].id) return track_list[i].name; } return "unknown"; } /* Функция проигрывания трека */ /* Да, да, очевидные комментарии */ void play_track(LPCTSTR current_dir, DWORD id) { static HSTREAM sm = 0; TCHAR file_path[MAX_PATH]; /* Формируем полный путь к треку */ wsprintf(file_path, L"%s\\%s\\%d.mp3", current_dir, MUSIC_DIR, id); if(sm != 0) { /* Завершаем проигрывание текущего трека */ BASS_StreamFree(sm); sm = 0; } /* Запускаем трек на проигрывание, указав, что его необходимо зациклить */ sm = BASS_StreamCreateFile(FALSE, file_path, 0, 0, BASS_UNICODE); BASS_ChannelFlags(sm, BASS_SAMPLE_LOOP, BASS_SAMPLE_LOOP); BASS_ChannelPlay(sm, FALSE); } int main() { HANDLE pr; DWORD pid, track_id = 0, last_track = 0; TCHAR current_dir[MAX_PATH] = {0}; /* Инициализируем библиотеку */ if(!BASS_Init(-1, 44100, 0, NULL, NULL)) { printf("Failed to init BASS library\n"); return -1; } /* Получаем путь, откуда запущен наш файл, */ /* чтобы в дальнейшем сформировать полный путь к mp3-файлу */ GetCurrentDirectory(_countof(current_dir), current_dir); /* Получаем PID целевого процесса */ pid = get_pid_by_name(TEXT("TIMWIN.EXE")); if(pid == 0) { printf("Can't find TIMWIN.EXE process\n"); BASS_Free(); return -1; } /* Открываем процесс игры с правами на чтение памяти */ pr = OpenProcess(PROCESS_VM_READ, FALSE, pid); if(pr == NULL) { printf("Failed to open process (pid=%d)\n", pid); BASS_Free(); return -1; } while(TRUE) { /* Читаем память процесса и получаем ID трека */ if(!ReadProcessMemory(pr, (LPCVOID) MEMORY_OFFSET, &track_id, sizeof(DWORD), NULL)) { printf("Can't read process memory\n"); break; } /* Выводим информацию о текущем треке */ printf("Current track - %08X - %-32s\r", track_id, get_track_name_by_id(track_id)); /* Если проигрываемый трек поменялся, то запустим новый трек на проигрывание и сохраним его ID */ if(track_id != last_track) { play_track(current_dir, track_id); last_track = track_id; } Sleep(100); } CloseHandle(pr); BASS_Free(); return 0; } |
Предельно простой код. Теперь запустим игру (не забудьте отключить музыку в игре, чтобы MIDI-треки не мешались), нашу программу, не забыв положить рядом саундтрек, и оценим результат:
Всё отлично работает. Теперь можно насладиться головоломками игры, слушая качественный звук.
Проект для MSVC и Perl-скрипт: скачать.
P.S. Один из треков игры:
И тебя с новым годом :)
По сабжу - ну ты и гик)
С новым годом, Каими и Дх. Спасибо вам за ваши интересные и уникальные посты. Пусть в новом году творческая муза не покидает вас, оставайтесь такими же крутыми :)
P.S. Новогоднюю картинку скачал, посмотрел не rar ли, нету ли чего в свойствах -- расстроился.
О, классно. Прям как будто мысли мои прочитали. А я как раз хотел предложить добавить в разделы блога рубрику (ну или подраздел) - программирование звука. А если там иногда будет присутствовать реверсинг, то было б, вообще, - супер.
А тут тебе и звук какой-никакой и элементы реверсинга. Хорошее начало.
Ну, наверное, Kaimi и dx отнесутся к этому скептически. Скажут, что мы в этом не специалисты и все такое... Хотя тема редкая (особенно в RU секторе) и интересная. Может рискнуть попробовать?
Каими, можешь поискать еще способы в стиме игры бесплатно получать, пожалуйста. Очень нужно
P.S. С новым годом!
С наступившими *slowpoke* и спасибо за занятную статью!