Explorando la emulación de E/S de JVS y su implementación
• Renzo Pigliacampo
Introducción
Ya van casi dos décadas que los PCs se convirtieron en una plataforma viable para arcades, y el gran éxito de la Type X de Taito ya lo dejó por sentado para jamás regresar a las placas tradicionales. Las razónes logísticas son sencillas, componentes comunes de producción en masa son mucho más baratos y simples de dar soporte y reponer, reduciendo los costos considerablemente. No solamente esto, sino que el software sería mucho más fácil y accesible de desarrollar como nunca antes, ya que la arquitectura de la plataforma no solamente ya es muy conocida, sino que también más estandarizada. Los arcades basados en PC tienen el potencial de abrirle las puertas a una mayor cantidad de desarrolladores de software.
La única contra debe ser la piratería y el bootlegging, y si bien esto siempre fue un problema en la comunidad arcade, ahora puede volverse peor, ya que los PCs, así como es más simple desarrollar para ellos, también es igual de sencillo crackear protecciones y desencriptar información. Estos forman parte de la capa que nos impide ejecutar el software en PCs normales, en conjunto con los dispositivos de E/S (entrada y salida).
Un poco de historia
Si bien existen muchas plataformas arcades basados en PC que fueron apareciendo con el tiempo, siendo hoy en día todavía la norma, solo me voy a enfocar en dos máquinas de hardware en específico y su software: Taito Type X y Examu eX-BOARD, una fue clásica, la otra un fracaso, pero ambas fueron pioneras en el movimiento de arcades basados en PC.
Esta historia tiene lugar por el 2009-2011 (no recuerdo exactamente), cuando dumps (copias/extracciones) de datos de la mayoría de los títulos de Type X, algunos de X2, y todos los de eX-BOARD fueron publicados en foros arcade. Estos datos estaban desprotegidos, lo que significó que no era necesario ningún dispositivo o chequeo de seguridad para que los juegos funcionen. No puedo recordar si el emulador de E/S también fue lanzado al mismo tiempo, pero seguro que apareció al poco tiempo. Este primer loader (cargador) simplemente hacía eso, emular el dispositivo de E/S, de manera que eliminaba el último obstáculo que impedía que el software funcionara sin que aparezca una pantalla de I/O ERROR
(I/O es inglés para E/S).
Un tiempo después, Romhack reconstruyó este cargador en el emulador de Type X de código abierto, ttx_monitor, y más tarde en base a éste, la variante de eX-BOARD, xb_monitor. Más adelante también hizo una variante de Cave-PC, cv_monitor, pero hasta donde sé nunca liberó el código. Los emuladores de Romhack se convirtieron en los que todos usaban durante mucho tiempo, haste que recientemente aparecieron nuevas alternativas, como JConfig y TeknoParrot, y si bien éstos emulan muchos más dispositivos de E/S de otras máquinas, los núcleos de Type X y eX-BOARD están hechos en base a los emuladores de Romhack. Ahora inclusive yo entré en la tendencia, y desarrollé versiones mejoradas tanto de ttx_monitor como de xb_monitor, TTX-Monitor+ y XB-Monitor+, respectivamente.
Qué esperar
Primero, vamos a echarle un vistazo a la protección del software en un alto nivel, después veremos el hardware (y JVS en general), cómo emularlo, analizar la implementación de Romhack y construir sobre lo existente, añadiendo funcionalidades y mejoras de calidad de vida.
Como no tengo ningún arcade, desconocía cierta terminología básica como JAMMA, y todavía lo hago un poco, por lo que mis únicas fuentes de información fueron la documentación de JVS original y el código de Romhack, además de experimentar por mi cuenta con el software que usan estos dispositivos. Aún así, creo que fue suficiente como para comprender como funciona todo, de ahí el énfasis en lo de alto nivel. Dicho esto, este artículo tal vez sea informativo solo para gente casual que no está muy involucrada en el tema, y no te sorprendas si hay información errónea o inexacta presente durante la lectura.
Protección en todo su esplendor
Algunas máquinas antiguas han implementado protecciones bastante complejas, incluso al nivel de hardware, como chips o baterías suicidas. Pero por suerte en este caso no tenemos nada de esa protección kamikaze, sino un simple chequeo de dongle (llave) a través de USB, pero veámoslo en mayor detalle. El disco duro tiene dos particiones:
Imagen de disco duro clonado de Chaos Breaker.
La primera tiene una instalación de Windows XP Embedded, un cargador/lanzador de Type X y una imagen de disco virtual con los datos del juego:
La raíz de la partición C:
, el contenido de la carpeta TypeXsys
y al final la imagen de disco encriptada dentro de la carpeta data
con los datos del juego. Además, yes.txt
.
El cargador se encarga de hacer chequeos de hardware (dongle, disco duro, particiones) y si todo está en orden, se devuelve una llave que desencripta el archivo de imagen de disco, siendo posible correr el programa. Una vez ejecutado, chequea por un dispositivo JVS de E/S válido en el puerto COM2. Si está presente, el juego continúa normalmente, y sino, aparece el 'error de placa de E/S'.
Pantalla de error de E/S en Trouble Witches y GigaWing Generations, respectivammente.
También está la segunda partición, que almacena toda la configuración y datos del juego. La razón por la cual supongo que esto es así es que la primera partición podría ser de solo lectura, o por lo menos no se escribe nada ahí.
Una partición solo para esto.
En el caso de los juegos de eX-BOARD, estos fueron distribuídos en cartuchos IDE, siendo solo esto protección suficiente. O por lo menos eso es lo que pensó Examu, ya que fue crackeada al poco tiempo. A falta de mayor documentación sobre esta protección, observando el código de xb_monitor podemos ver un hook (inyección de código) en la librería IpgExKey.dll
, función _GetKeyLicense@0
:
Hook de funciones en XB-Monitor+.
Cargando el DLL en Ghidra, podemos ver todas las funciones exportadas, siendo GetKeyLicense
una de ellas:
Creo que es seguro asumir a partir de su nombre que maneja el chequeo de protección, por lo que al develover siempre verdadero es suficiente para saltárselo. Ahora que tenemos los datos desencriptados y la protección crackeada, lo único que nos queda es el dispositivo de E/S.
Dispositivo de comunicación
La placa JVS tiene la funcionalidad de E/S en una placa separada, la cual tiene un conector JAMMA para los controles, y un conector de CN2 a USB, para transferir datos entre la placa de E/S principal y la placa TAITO de E/S secundaria dentro de la propia Type X. Más allá de la conexión física por USB, se utiliza el protocolo JVS para la transferencia de datos a través de una transmisión serial RS-485. La placa secundaria está conectada al puerto COM2 en la placa madre, así que podemos decir que funciona como una interfaz entre la placa de E/S y la máquina en sí.
Esquema de la Type X, la placa de JVS principal y la placa JVS de E/S conectada a la placa de E/S secundaria.
El software lee los datos de los controles del dispositivo en el puerto COM2. Los datos son transferidos en paquetes a través del protocolo JVS:
Estructura de un paquete del protocolo JVS.
En pocas palabras, el Sync Code
determina el inicio de un paquete JVS válido, que siempre tiene un valor de 0xE0
. El Node
indica la dirección o el dispositivo/nodo secundario de destino. El Byte
determina el tamaño del resto del paquete, incluyendo el checksum (suma de control), y esta suma ayuda a identificar si un paquete está corrupto o no. La sección Data
es, bueno, para los datos, conformado por un comando y argumentos. Existe una larga lista de comandos para realizar diferentes operaciones. En la emulación, la mayoría de estos comandos están codificados a la fuerza para la inicialización de la placa de E/S, por lo que el único que realmente nos interesa es el comando 0x20
, SWINP
, o de forma más entendible, Switch Inputs (entradas/botones):
Bytes de datos del comando SWIMP
para controles normales (también los hay para paneles de mahjong y palancas dobles)
Entonces, ¿cómo emulamos este proceso?
Emulación de E/S
Como la placa JVS de E/S está conectada al puerto COM2, necesitamos un dispositivo COM falso que nos permita hacer pasar cualquier dispositivo de entrada como uno compatible con JVS. Para esto, inyectamos nuestros propios hooks para las funciones COM en la librería del sistema Kernel32
para el proceso en ejecución, que van a devolver la información correcta del dispositivo virtual falso que creamos.
El concepto es simple, creamos una corriente de datos que va a simular la estructura de transferencia JVS, construir los paquetes correctos y devolver una respuesta válida. Básicamente introducimos los datos que la corriente está esperando. La parte interesante está en la petición del comando 0x20
, donde la segunda parte de la emulación entra en juego.
Aparte del dispositivo COM falso, también necesitamos una capa de entrada verdadera, la cual podemos conectar con la falsa y generar las señales de entrada a través de un paquete de JVS virtual. Para esto, creamos dos dispositivos de DirectInput (si bien cualquier API de controles funcionaría, usamos DInput ya que los juegos mismos también la usan, por lo que lo necesitamos de todas formas): uno falso, que conectamos al proceso del juego para que no detecte ninguna entrada o control, y uno real, que va a leer las entradas de nuestro dispositivo seleccionado. De esta manera, cancelamos cualquier lectura de entrada del proveniente del juego, e inyectamos las nuestras propias en la corriente virtual de JVS, la cual entonces va a ser leída e interpretada por el programa.
La inicialización de DInput se comporta de forma normal, los dispositivos son enumerados, adquirimos el que queremos y finalmente creamos un hilo de rastreo, que estará constantemente en ejecución. Cuando presionamos una tecla/botón en el dispositivo, se va a activar una bandera en un array de estado de entrada, el cual va a ser leído por el proceso de rastreo de la corriente JVS.
Chequear si algún botón/tecla es presionada y asignar la bandera correspondiente en el array.
Entonces el rastreo de JVS obtiene el estado de la tabla de entradas y va a procesarla.
Cuando el dispositivo de JVS falso detecta la bandera, asigna el bit en el byte correspondiente en el bloque de datos del Input Switch:
Asignando los primeros 2 bytes, los cuales pertenecen a las entradas del jugador 1.
Finalmente, el paquete es enviado y la función retorna, la respuesta es almacenada para después ser interpreta por el juego como una entrada de JVS legítima.
Excelente, y ahora qué
Todo lo que vimos hasta ahora es lo que está implementado en los cargadores de Romhack. Esto incluye la emulación de E/S de JVS y el manejo de entradas. El primero funciona perfectamente, pero el segundo, si bien es servicial, en mi opinión le falta algunas funcionalidades básicas. La mayoría de éstas se hicieron populares por cargadores como JConfig, por lo que estos emuladores se sienten viejos y obsoletos en comparación.
Pero entonces, ¿por qué no usar estas alternativas modernas? La respuesta es que, por algún motivo, ninguna de ellas tiene soporte para los títulos de la eX-BOARD de Examu, y me refiero particularmente a JConfig, ya que TeknoParrot parece ser compatible, pero como este software tiene un pasado cuestionable (liberando dumps con VMProtect que solo funcionan con este emulador), no me gusta usarlo y prefiero evitarlo, inclusive si eso significa desarrollar mi propia alternativa.
Acá es cuando mi versión mejorada del único emulador de código abierto, xb_monitor, entra en juego, intentando poner al viejo cargador en la altura de las alternativas modernas. Y ya que estábamos, también decidí aplicar el mismo tratamiento a ttx_loader, porque por qué no (además de que ambos comparten la mayoría del código), aunque más adelante le encontraría una buena razón para existir. Pero antes de avanzar con estas nuevas funcionalidades, es importante remarcar que ambos proyectos sufrieron un cambio total del código, de una manera en la que personalmente creo que es mucho más legible y entendible, por lo que si querés ver como funciona todo, probablemente sea mejor usar TTX-Monitor+ y XB-Monitor+ en lugar de los originales.
Mejoras de calidad de vida
Primero, los controles. Los valores para la zona muerta estaban muy bajos, por lo que con controles modernos y más sensitivos, como el Mando de Xbox
y DualShock 4
, era literalmente imposible configurar y jugar con las palancas analógicas, y ni hablar si tenés problemas de drifting (derrape) como yo, incluso si son mínimos. Incrementando este valor a la fuerza es suficiente para solucionar este problema.
El nuevo valor para la zona muerta (500
), con la implementación vieja al lado (10
).
En la función de rastreo de entrada, solo el eje izquierdo (AxisL
) y los botones eran detectados, muy limitado. Ahora se añadió soporte para el eje derecho (AxisR
), gatillos (AxisZ
) y la cruceta/flechas (POVs
), en adición a una opción PovAsAxis
que permite usar la cruceta como si fuera la palanca izquierda (similar al botón Analógico
en los mandos DualShock).
Extracto de la función de rastreo de entrada.
Ahora que ya terminamos con los controles, veamos algunas funciones que creí que ya no serían necesarias de incluir: las funciones de registro y el wrapper (envoltorio) de DirectDraw. El primero todavía se encuentra presente en el código, y está disponible como herramienta de depuración para el desarrollo, en lugar de estar siempre activo y creando archivos de registro que a nadie le interesa. Si bien se eliminó el wrapper de DirectDraw, el de Direct3D 9 aún es requerido para XB-Monitor+, pero se redució a un solo propósito: arreglar el renderizado de la ventana en Arcana Heart 3. La implementación original era más compleja, pero ahora solamente se fuerza el uso de pantalla completa a una resolución de 640x480
, tal y como lo hacen el resto de los títulos de eX-BOARD.
La parte importante del wrapper.
Esta decisión fue a favor del uso de wrappers externos, como el excelente dgVoodoo, el cual no solo puede solucionar incompatibilidades en sistemas modernos (particularmente importante ya que estamos hablando de máquinas que salieron en 2004-2008), sino también mejorar el aspecto visual. Otros cargadores como JConfig incluyen sus propios wrappers para una experiencia más completa, aunque bajo mi experiencia éstos sean muy limitados o les falten funcionalidades, y no funcionen muy bien.
Último pero no menos importante, un SavePatch
(parche de guardado). Como se comentó al principio del artículo, la mayoría de los programas guardaban sus configuraciones y datos de puntuaciones en una partición diferente. Esto no ha cambiado, e intentarán guardar información en estos lugares. El problema está en que no todo el mundo tiene una segunda partición con la letra específica necesaria, y tampoco queremos tener todos estos archivos por todos lados. Para darle una solución, vamos a redirigir todas las operaciones de archivos y carpetas a una carpeta de guardado dedicada en el directorio raíz de la aplicación.
Para los títulos de eX-BOARD es sencillo, ya que los datos no se guardan en el disco duro, pero sino en memoria volátil (aunque no sé específicamente la forma en la que lo hace), así que creamos un archivo de SRAM virtual, el cual después es cargado en memoria. xb_monitor ubica el archivo binario de SRAM en la carpeta sv
, una estructura que también se usa en JConfig y en varios parches binarios, y en XB-Monitor+ y TTX-Monitor+ no va a ser diferente.
Pero la Type X es completamente diferente, que parece sencillo al principio, pero cuya implementación es bastante complicada. En teoría inyectamos nuestras propias funciones del sistema CreateDirectory
y CreateFile
(tanto para la variante ANSI como la de caracteres largos) y listo, pero cuando empezamos a tratar con subdirectorios, todo se complejiza más. Con la implementación actual, conseguí que todos los títulos almacenen los datos en la carpeta de guardado, pero algunos como The King Of Fighters '98, Gouketsuji Ichizoku Senzo Kuyou y Trouble Witches no logran leer estos datos. Si bien puede que sea posible arreglarlo, tal vez con un algoritmo diferente, realmente no valía la pena, considerando que otros cargadores tienen parches específicos para cada juego (TeknoParrot seguro), mientras que yo tuve un enfoque para dar una solución más dinámica.
Hooks de funciones del sistema para redirigir archivos y directorios.
Algo importante de aclarar es que solamente redirigimos las operaciones de archivos que no son relativas al directorio actual. De esta manera no rompemos los programas cuando intenten leer sus propios archivos de datos.
Introduciendo soporte para entrada de mahjong
La nueva y emocionante funcionalidad única de TTX-Monitor+ es el soporte para títulos de mahjong. Bueno, en realidad solo Taisen Hot Gimmick 5 por ahora, quizás Taisen Hot Gimmick Mix Party más adelante. Hay bastante historia con estos juegos de mahjong y su emulación de E/S de JVS.
Aparentemente, si bien ambos juegos efectivamente usan JVS para la comunicación de E/S, parece que es una implementación personalizada, ya que JVS es lo suficientemente flexible como para hacerlo. ¿Por qué? Ni idea, pudieron haberse quedado con la manera estándar (literalmente esta en el nombre, JVS) de manejar las entradas de mahjong. La gente detrás de JConfig estuvo intentando descifrar su funcionamiento, y me comentaron que un dump de JVS diferente es necesario para brindarle a los programas la información correcta que esperan. Hasta entonces, vayamos por una solución rápida.
Para nuestra suerte, los desarrolladores dejaron los controles de teclado activados para depuración, o al menos en THG5, que usa DirectInput. Pero THGMP no los tiene, aunque el ejecutable si parece asignar algunas teclas, las cuales no están activadas por algún motivo. Aún estoy investigándolo, así que con suerte puede haber una manera de desbloquear el teclado para THGMP. Al menos de esta forma los controles deberían funcionar hasta que se encuentre una manera de emular correctamente el panel de mahjong de JVS.
El único cargador que era capaz de ejecutar THG5 con los controles de depuración fue el primero de todos en publicarse, el TypeX Loader. Esto se debe a que los demás cargadores siempre crean un dispositivo de DirectInput falso para prevenir que los juegos capturen las entradas por su cuenta, y así forzar que solo las entradas virtuales de JVS sean reconocidas. Para los títulos de mahjong, este dispositivo se deshabilitaba, permitiendo que los juegos reconozcan las entradas, y a pesar de que el cargador permitía configurar los controles para el panel de mahjong, nunca fue realmente implementado y nunca funcionó. Entonces, vamos a estar haciendo lo primero, y arreglando esto último.
Con esto en mente, la idea es dar la falsa sensación de una emulación correcta, permitiendo la reasignación de los controles en el teclado a cualquier dispositivo conectado, incluyendo el mismo teclado. Para esto, implementamos un nuevo wrapper de DirectInput con un poco de complejidad algorítmica. Básicamente se encarga de administrar todas las entradas, tanto las originales como las asignadas por el usuario. De esta manera, buscamos evitar cualquier conflicto que pueda ocurrir cuando estas dos configuraciones se superpongan entre ellas.
Si bien al principio sonó sencillo, en realidad fue bastante complicado de implementar correctamente. Toda la configuración de entrada de mahjong se separó de las entradas normales para que sea más fácil y limpio de desarrollar y entender.
Versión multi-hilo del algoritmo de rastreo.
Para el rastreo de las entradas, creamos un nuevo hilo para cada botón. Al principio quería que la cantidad de hilos sea arbitraria, pero al final no estaba dando buenos resultados. Además, también se incluye un modo de un solo hilo, lo cual de hecho soluciona la inconsistencia con la que el programa maneja las señales de múltiples teclas a la vez.
El toque final
Ahora que todas las funciones y mejoras principales, planeadas o no, están implementadas, todavía queda una cosa más por hacer: la interfaz de usuario. La UI original (ttx_config) era muy simple, hecha con MFC Application Wizard. Cumplía con su trabajo, pero copiaba el código del cargador principal, lo que significaría tener que pasar todo para allá, además de añadir soporte para mahjong.
En lugar de actualizar la UI vieja, decidí desarrollar una nueva desde cero con .NET Windows Forms. Bueno, mas o menos, porque ya tenía la mayor parte hecha de otro proyecto anterior muy similar, incluyendo todas las operaciones de la interfaz y la integración de controles a través de la API de DirectInput. En el momento que desarrollé todo esto llevó una cantidad considerable de tiempo, sobre todo teniendo en cuenta que nunca había trabajado con DirectX o APIs similares anteriormente, pero al mismo tiempo me ayudó mucho a comprender como funcionaba la implementación de entrada en ttx_monitor y xb_monitor.
También puse a trabajar un poco el aspecto de diseñador gráfico con nuevos logos e iconos.
Probablemente esto no haya terminado
Mientras considero que casi todo está terminado, todavía queda ese juego de mahjong dando vueltas. Voy a ver si puedo hacerlo funcionar con los controles de depuración que, creo yo, todavía están ahí, solamente que deshabilitados. Si eso sucede, la implementación de mahjong actual debería funcionar, tal vez con algunas modificaciones.
Sin embargo, este título se comporta de manera diferente a THG5: THGMP es una colección de los primeros 4 títulos en la serie. Cada uno tiene su propio ejecutable en su propia carpeta, incluyendo el test mode (modo de pruebas) y el menú especial para seleccionarlos. Resulta que game.exe
es el proceso principal que administra la creación de procesos secundarios y la comunicación de JVS, funcionando como una tubería hacia los procesos individuales de cada juego. Por este motivo, es muy molesto depurar el programa, así que no es tan sencillo como le sería normalmente. De todos modos le voy a dar una oportunidad.
Éste fue un largo proyecto que todavía no considero terminado, pero me quedo con las experiencias de haber programado por primera vez en C, haber implementado nuevas funcionalidades en un proyecto ajeno y simplemente aprender más sobre como funcionan estas máquinas arcade modernas.
Referencias
Taito Type X User Manual - TAITO
JAMMA Video Standard (The Third Edition) (JVS) - JAMMA