Имеется игра In Verbis Virtus с необычной механикой — творить заклинания с помощью микрофона.
Это не симулятор Амаяка Акопяна, это головоломка от первого лица с нетипичным управлением.
Для этого в игре используется библиотека распознавания речи Sphinx.
Задумка выглядит интересной, но реализация вышла так-себе (распознавание очень часто промахивается), да и кастовать после первых 20 минут откровенно надоедает.
О том, как это выглядит со стороны — вообще молчу.
Разработчики, к сожалению, не оставили возможности управления заклинаниями с клавиатуры, и я решил это исправить.
Первой мыслью было внести изменения в библиотеку Sphinx, поскольку она open-source. Однако я обнаружил, что существует куча версий этой библиотеки.
Попробовав три из них (примерно соответствующие времени выхода игры), я так и не нашёл нужную, поскольку в каждой были какие-либо отличия (как минимум по набору экспортируемых функций).
Поэтому я решил сделать враппер поверх оригинальной библиотеки из игры.
Для этого я воспользовался подходом, предложенным в статье Generating .DLL Wrappers.
Суть его в том, что можно обернуть любую библиотеку безо всякого знания о параметрах и типах экспортируемых функций, достаточно только их имён (которые можно извлечь хоть текстовым редактором).
Список экспорта создаётся с помощью def-файла вида:
Сами обёртки функций имеют вид:
Таким образом устраняются проблемы с передачей аргументов и возвратом значений оригинальных функций.
Для начала требовалось немного реверс-инжиниринга. Я создал враппер с единственным дополнением — логированием имён вызываемых функций.
Так я определил где, когда и как отрабатывает основная логика библиотеки.
Выяснилось, что сначала набирается некоторое количество сырых отсчётов с микрофона функцией ps_process_raw(), а затем само решение принимается в функции ps_get_hyp().
Позже (слишком поздно) я всё-таки подумал, что сначала стоило бы взглянуть на документацию Sphinx (где всё это было описано).
Было решено добавить в функцию ps_process_raw() определение состояния клавиш, которые будут отвечать за заклинания.
Для этого нужно эти клавиши назначить. Сделаем это в DllMain(), вместе с получением адресов оригинальных функций. Вот эдак:
Файл settings.ini имеет вид:
Итого, в массиве buf будут лежать строки, соответствующие заклинаниям. Причём лежать будут по индексам, соответствующим нужным клавишам.
Определять состояние клавиш будем так:
Обёртка функции ps_process_raw() будет иметь вид:
То есть если в то время, когда надо кастовать в микрофон, пользователь нажал клавишу — в глобальной переменной i сохранился указатель на строку, соответствующую нажатой клавише.
Приготовления закончены, пора реализовывать основной функционал.
Требуется определить, была ли нажата пользователем кнопка заклинания, и если да — изменить возвращаемое значение в функции ps_get_hyp().
Для этого потребуется чуть-чуть манипуляций со стеком:
Основной функционал находится в куске с комментарием «replace result (if key was pressed)».
Если в глобальной переменной лежит указатель — подменяем возвращаемый результат и обнуляем глобальную переменную.
А если нет — то оставляем всё без изменений.
Таким образом, можно продолжать кастовать через микрофон, а можно и кнопками (они имеют приоритет). Цель достигнута.
Да, в решении есть кривые моменты.
Например передача указателя через глобальную переменную, да ещё и именуемую i (решил использовать её повторно после инициализации в DllMain).
Лазить по чужому стеку тоже как-то не принято (не придумал как сделать иначе).
Тем не менее, решение вполне рабочее. Основного кода менее 100 строк, по большей части всё тривиально.
Исходник
def-файл
Бинарник + файл настроек
Установка:
Критика и предложения принимаются.
Это не симулятор Амаяка Акопяна, это головоломка от первого лица с нетипичным управлением.
Для этого в игре используется библиотека распознавания речи Sphinx.
Задумка выглядит интересной, но реализация вышла так-себе (распознавание очень часто промахивается), да и кастовать после первых 20 минут откровенно надоедает.
О том, как это выглядит со стороны — вообще молчу.
Разработчики, к сожалению, не оставили возможности управления заклинаниями с клавиатуры, и я решил это исправить.
Первой мыслью было внести изменения в библиотеку Sphinx, поскольку она open-source. Однако я обнаружил, что существует куча версий этой библиотеки.
Попробовав три из них (примерно соответствующие времени выхода игры), я так и не нашёл нужную, поскольку в каждой были какие-либо отличия (как минимум по набору экспортируемых функций).
Поэтому я решил сделать враппер поверх оригинальной библиотеки из игры.
Для этого я воспользовался подходом, предложенным в статье Generating .DLL Wrappers.
Суть его в том, что можно обернуть любую библиотеку безо всякого знания о параметрах и типах экспортируемых функций, достаточно только их имён (которые можно извлечь хоть текстовым редактором).
Список экспорта создаётся с помощью def-файла вида:
EXPORTS func1=_func1 @1 func2=_func2 @2
Сами обёртки функций имеют вид:
_declspec(naked) void _func1() { __asm jmp dword ptr [procs + 1 * 4]; }
Таким образом устраняются проблемы с передачей аргументов и возвратом значений оригинальных функций.
Для начала требовалось немного реверс-инжиниринга. Я создал враппер с единственным дополнением — логированием имён вызываемых функций.
Так я определил где, когда и как отрабатывает основная логика библиотеки.
Выяснилось, что сначала набирается некоторое количество сырых отсчётов с микрофона функцией ps_process_raw(), а затем само решение принимается в функции ps_get_hyp().
Позже (слишком поздно) я всё-таки подумал, что сначала стоило бы взглянуть на документацию Sphinx (где всё это было описано).
Было решено добавить в функцию ps_process_raw() определение состояния клавиш, которые будут отвечать за заклинания.
Для этого нужно эти клавиши назначить. Сделаем это в DllMain(), вместе с получением адресов оригинальных функций. Вот эдак:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { HINSTANCE hinst_dll; if (fdwReason == DLL_PROCESS_ATTACH) { hinst_dll = LoadLibraryA("pocketsphinx_orig.dll"); if (!hinst_dll) return 0; for (i = 0; i < 93; i++) procs[i] = GetProcAddress(hinst_dll, import_names[i]); for (i = 0; i < 256; i++) { _itoa(i, &buf[i][0], 10); GetPrivateProfileStringA("main", &buf[i][0], 0, &buf[i][0], MAX_PATH, ".\\settings.ini"); } i = 0; } else if (fdwReason == DLL_PROCESS_DETACH) FreeLibrary(hinst_dll); return 1; }
Файл settings.ini имеет вид:
[main] 49=String 1 50=String 2
Итого, в массиве buf будут лежать строки, соответствующие заклинаниям. Причём лежать будут по индексам, соответствующим нужным клавишам.
Определять состояние клавиш будем так:
void find_key() { if(!i) { for (i = 0; i < 256; i++) if (buf[i][0]) if (GetAsyncKeyState(i) >> 1) { i = (int)&buf[i][0]; return; } if (i == 256) i = 0; } }
Обёртка функции ps_process_raw() будет иметь вид:
_declspec(naked) void _ps_process_raw() { find_key(); __asm jmp dword ptr [procs + 78 * 4]; }
То есть если в то время, когда надо кастовать в микрофон, пользователь нажал клавишу — в глобальной переменной i сохранился указатель на строку, соответствующую нажатой клавише.
Приготовления закончены, пора реализовывать основной функционал.
Требуется определить, была ли нажата пользователем кнопка заклинания, и если да — изменить возвращаемое значение в функции ps_get_hyp().
Для этого потребуется чуть-чуть манипуляций со стеком:
_declspec(naked) void _ps_get_hyp() { static unsigned int return_address; _asm { //save return address push eax mov eax, dword ptr [esp+4] mov return_address, eax pop eax //call original ps_get_hyp add esp, 4 call dword ptr [procs + 22 * 4] sub esp, 4 //replace result (if key was pressed) cmp i, 0 je end mov eax, i xor ecx,ecx mov i, ecx end: //restore return address push eax mov eax, return_address mov dword ptr [esp+4], eax pop eax ret } }
Основной функционал находится в куске с комментарием «replace result (if key was pressed)».
Если в глобальной переменной лежит указатель — подменяем возвращаемый результат и обнуляем глобальную переменную.
А если нет — то оставляем всё без изменений.
Таким образом, можно продолжать кастовать через микрофон, а можно и кнопками (они имеют приоритет). Цель достигнута.
Да, в решении есть кривые моменты.
Например передача указателя через глобальную переменную, да ещё и именуемую i (решил использовать её повторно после инициализации в DllMain).
Лазить по чужому стеку тоже как-то не принято (не придумал как сделать иначе).
Тем не менее, решение вполне рабочее. Основного кода менее 100 строк, по большей части всё тривиально.
Исходник
def-файл
Бинарник + файл настроек
Установка:
- В папке \In Verbis Virtus\Binaries\Win32\ переименовать оригинальный pocketsphinx.dll в pocketsphinx_orig.dll
- Положить рядом враппер pocketsphinx.dll
- В папку \In Verbis Virtus\Binaries\Win32\UserCode положить settings.ini
Критика и предложения принимаются.
