Hay un juego en Verbis Virtus con una mecánica inusual: lanzar hechizos con un micrófono.
Este no es un simulador de Hmayak Hakobyan, es un rompecabezas en primera persona con controles atípicos.
Para esto, el juego usa la biblioteca de reconocimiento de voz Sphinx.
La idea parece interesante, pero la implementación salió más o menos (el reconocimiento a menudo falla), y francamente molesto de lanzar después de los primeros 20 minutos.
Sobre cómo se ve desde el exterior, generalmente en silencio.
Los desarrolladores, desafortunadamente, no dejaron la capacidad de controlar los hechizos desde el teclado, y decidí arreglarlo.
El primer pensamiento fue realizar cambios en la biblioteca Sphinx, ya que es de código abierto. Sin embargo, descubrí que hay muchas versiones de esta biblioteca.
Después de probar tres de ellos (que corresponden aproximadamente al momento en que se lanzó el juego), todavía no pude encontrar el correcto, ya que cada uno tenía diferencias (al menos en términos del conjunto de funciones exportadas).
Por lo tanto, decidí hacer un contenedor en la parte superior de la biblioteca original del juego.
Para hacer esto, tomé el enfoque propuesto en el artículo
Generando .DLL Wrappers .
Su esencia es que puede envolver cualquier biblioteca sin ningún conocimiento de los parámetros y tipos de funciones exportadas, solo sus nombres (que se pueden extraer incluso con un editor de texto) son suficientes.
La lista de exportación se crea utilizando el archivo def del formulario:
EXPORTS func1=_func1 @1 func2=_func2 @2
Los envoltorios de funciones en sí son:
_declspec(naked) void _func1() { __asm jmp dword ptr [procs + 1 * 4]; }
Esto elimina los problemas de pasar argumentos y devolver los valores de las funciones originales.
Primero, se requería un poco de ingeniería inversa. Creé un contenedor con la única adición: registrar los nombres de las funciones llamadas.
Así que determiné dónde, cuándo y cómo funciona la lógica de la biblioteca central.
Resultó que al principio un cierto número de muestras sin procesar del micrófono fueron recolectadas por la función ps_process_raw (), y luego la decisión misma se tomó en la función ps_get_hyp ().
Más tarde (demasiado tarde), todavía pensé que valdría la pena mirar primero la documentación de Sphinx (donde se describió todo).
Se decidió agregar a la función ps_process_raw () una definición del estado de las teclas que serán responsables de los hechizos.
Para hacer esto, debe asignar estas teclas. Hacemos esto en DllMain (), junto con la obtención de las direcciones de las funciones originales. Aquí hay algunos comerciales:
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; }
El archivo settings.ini tiene la forma:
[main] 49=String 1 50=String 2
Total, en la matriz de buf habrá líneas correspondientes a hechizos. Además, mentirán por índices correspondientes a las claves necesarias.
Determinaremos el estado de las claves de la siguiente manera:
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; } }
El contenedor de la función ps_process_raw () se verá así:
_declspec(naked) void _ps_process_raw() { find_key(); __asm jmp dword ptr [procs + 78 * 4]; }
Es decir, si en el momento en que es necesario emitir en el micrófono, el usuario presionó una tecla: el puntero a la línea correspondiente a la tecla presionada se guardó en la variable global i.
Los preparativos están terminados, es hora de implementar la funcionalidad básica.
Es necesario determinar si el usuario presionó el botón de hechizo y, de ser así, cambiar el valor de retorno en la función ps_get_hyp ().
Esto requerirá una pequeña manipulación de la pila:
_declspec(naked) void _ps_get_hyp() { static unsigned int return_address; _asm {
La funcionalidad principal está en una pieza con el comentario "reemplazar resultado (si se presionó la tecla)".
Si el puntero está en la variable global, sustituimos el resultado devuelto y restablecemos la variable global.
Y si no, dejamos todo sin cambios.
Por lo tanto, puede continuar emitiendo a través del micrófono o puede usar los botones (tienen prioridad). El objetivo se logra.
Sí, hay puntos torcidos en la solución.
Por ejemplo, pasar un puntero a través de una variable global, también llamada i (decidí usarlo nuevamente después de la inicialización en DllMain).
Escalar la pila de otra persona tampoco es aceptado de alguna manera (no pensé en cómo hacerlo de manera diferente).
Sin embargo, la solución está funcionando bastante. El código principal tiene menos de 100 líneas, en su mayor parte, todo es trivial.
Código fuentearchivo defArchivo de configuración binario +Instalación:
- En la carpeta \ In Verbis Virtus \ Binaries \ Win32 \, cambie el nombre del original pocketsphinx.dll a pocketsphinx_orig.dll
- Poner envoltorios cercanos pocketsphinx.dll
- En la carpeta \ In Verbis Virtus \ Binaries \ Win32 \ UserCode, coloque settings.ini
Se aceptan críticas y sugerencias.