En el artículo, describiré cómo hacer que el emulador NES se controle de forma remota y un servidor para enviarle comandos de forma remota.

¿Por qué se necesita esto?
Algunos emuladores de varias consolas de juegos, incluido
Fceux , le permiten escribir y ejecutar scripts personalizados en Lua. Pero Lua es un mal lenguaje para escribir programas serios. Es más bien un lenguaje para llamar a funciones escritas en C. Los autores de los emuladores lo usan solo por la ligereza y facilidad de incrustación. La emulación precisa requiere muchos recursos de procesador, y la velocidad de emulación anterior era uno de los principales objetivos de los autores, y si recordaban la posibilidad de acciones de secuencias de comandos, estaba lejos de ser el primero.
Ahora, la potencia del procesador promedio es suficiente para emular NES, ¿por qué no usar lenguajes de secuencias de comandos potentes como Python o JavaScript en emuladores?
Desafortunadamente, ninguno de los emuladores populares de NES tiene la capacidad de usar estos u otros idiomas. Encontré solo un proyecto
Nintaco poco conocido, que también se basa en el núcleo Fceux, por alguna razón reescrito en Java. Luego decidí agregar la capacidad de escribir scripts en Python para controlar el emulador yo mismo.
Mi resultado es la Prueba de concepto de la capacidad de controlar el emulador, no pretende acelerar ni ser confiable, pero funciona. Lo hice por mí mismo, pero como la cuestión de cómo controlar el emulador usando scripts es
bastante común , puse el código fuente en el
github .
Como funciona
En el lado del emulador
El emulador Fceux
ya incluye varias bibliotecas Lua incluidas en
forma de código compilado . Uno de ellos es
LuaSocket . Está mal documentado, pero logré encontrar un ejemplo de código de trabajo entre la
colección de scripts Xkeeper0 . Usó enchufes para controlar el emulador a través de Mirc. En realidad, el código que abre el socket tcp es:
function connect(address, port, laddress, lport) local sock, err = socket.tcp() if not sock then return nil, err end if laddress then local res, err = sock:bind(laddress, lport, -1) if not res then return nil, err end end local res, err = sock:connect(address, port) if not res then return nil, err end return sock end sock2, err2 = connect("127.0.0.1", 81) sock2:settimeout(0)
Este es un socket de bajo nivel que recibe y envía datos por 1 byte.
En el emulador Fceux, el bucle principal del script Lua se ve así:
function main() while true do
Una comprobación de datos del zócalo:
function passiveUpdate() local message, err, part = sock2:receive("*all") if not message then message = part end if message and string.len(message)>0 then
El código es bastante simple: los datos se leen desde el socket y, si se detecta el siguiente comando, se analiza y ejecuta. El análisis y la ejecución se organizan utilizando
corutina (corutinas): este es un concepto poderoso del lenguaje Lua para pausar y continuar la ejecución del código.
Y una cosa más importante sobre las secuencias de comandos Lua en Fceux: la emulación se puede detener temporalmente desde la secuencia de comandos. ¿Cómo organizar la ejecución continua del código Lua y volver a ejecutarlo con un comando recibido del socket? Esto no sería posible, pero existe una capacidad poco documentada para llamar al código Lua incluso cuando se detiene la emulación (gracias
feos por
señalarlo ):
gui.register(passiveUpdate)
Con él, puede detener y continuar emulando dentro de
passiveUpdate , de esta manera puede organizar la instalación de puntos de interrupción del emulador a través de un socket.
Comando del lado del servidor
Yo uso un protocolo de texto RPC basado en JSON muy simple. El servidor serializa el nombre y los argumentos de la función en una cadena JSON y lo envía a través del socket. Además, la ejecución del código se detiene hasta que el emulador responde con una línea para completar el comando. La respuesta contendrá los campos "
FUNCTIONNAME_finished " y el resultado de la función.
La idea se implementa en la clase
syncCall :
class syncCall: @classmethod def waitUntil(cls, messageName): """cycle for reading data from socket until needed message was read from it. All other messages will added in message queue""" while True: cmd = messages.parseMessages(asyncCall.waitAnswer(), [messageName])
Con esta clase, los métodos Lua del emulador Fceux se pueden incluir en las clases de Python:
class emu: @classmethod def poweron(cls): return syncCall.call("emu.poweron") @classmethod def pause(cls): return syncCall.call("emu.pause") @classmethod def unpause(cls): return syncCall.call("emu.unpause") @classmethod def message(cls, str): return syncCall.call("emu.message", str) @classmethod def softreset(cls): return syncCall.call("emu.softreset") @classmethod def speedmode(cls, str): return syncCall.call("emu.speedmode", str)
Y luego llamó literalmente de la misma manera que desde Lua:
Métodos de devolución de llamada
En Lua, puede registrar devoluciones de llamada, funciones que se invocarán cuando se cumpla una determinada condición. Podemos portar este comportamiento al servidor en Python usando el siguiente truco. Primero, guardamos el identificador de la función de devolución de llamada escrita en Python y lo pasamos al código Lua:
class callbacks: functions = {} callbackList = [ "emu.registerbefore_callback", "emu.registerafter_callback", "memory.registerexecute_callback", "memory.registerwrite_callback", ] @classmethod def registerfunction(cls, func): if func == None: return 0 hfunc = hash(func) callbacks.functions[hfunc] = func return hfunc @classmethod def error(cls, e): emu.message("Python error: " + str(e)) @classmethod def checkAllCallbacks(cls, cmd):
El código Lua también guarda este identificador y registra una devolución de llamada Lua regular, que transferirá el control al código Python. A continuación, se crea un hilo separado en el código de Python que solo se ocupa de verificar que no se haya aceptado el comando de devolución de llamada de Lua:
def callbacksThread(): cycle = 0 while True: cycle += 1 try: cmd = messages.parseMessages(asyncCall.waitAnswer(), callbacks.callbackList) if cmd:
El paso final es que después de ejecutar la devolución de llamada de Python, el control se devuelve a Lua utilizando el
comando "
CALLBACKNAME_finished " para informar al emulador que la devolución de llamada ha finalizado.
Cómo ejecutar un ejemplo
Eso es todo, puede enviar comandos desde la computadora portátil Jupyter en el navegador directamente al emulador Fceux.
Puede ejecutar todas las líneas de la computadora portátil de ejemplo secuencialmente y observar el resultado de la ejecución en el emulador.
Ejemplo completo:
https://github.com/spiiin/fceux_luaserver/blob/master/FceuxPythonServer.py.ipynbContiene funciones simples como leer la memoria:

Ejemplos de devolución de llamada más complejos:

Y un guión para un juego específico que te permite mover enemigos de
Super Mario Bros. con el mouse:

Laptop Run Video:
Limitaciones y Aplicaciones
El script no tiene protección contra los tontos y no está optimizado para la velocidad de ejecución; sería mejor usar un protocolo RPC binario en lugar de uno de texto y agrupar mensajes, pero mi implementación no requiere compilación. El script puede cambiar los contextos de ejecución de Lua a Python y viceversa 500-1000 veces por segundo en mi computadora portátil. Esto es suficiente para casi cualquier aplicación, excepto para casos específicos de depuración píxel por píxel o línea por línea del procesador de video, pero Fceux todavía no permite tales operaciones desde Lua, por lo que no importa.
Posibles ideas de aplicación:
- Como ejemplo de implementación de un control similar para otros emuladores e idiomas.
- Juego de investigación
- Agregar trucos o características para organizar pasajes TAS
- Insertar o extraer datos y código en juegos
- Mejorar las capacidades de los emuladores: escribir depuradores, guiones para grabar y ver tutoriales, bibliotecas de guiones, editores de juegos
- Juego en red, control de juegos mediante dispositivos móviles, servicios remotos, joypads u otros dispositivos de control, guardado y parches en servicios en la nube
- Características del emulador cruzado
- Uso de Python u otras bibliotecas de lenguaje para análisis de datos y control de juegos (creación de bots)
Pila de tecnología
Yo usé:
Fceux -
www.fceux.com/web/home.htmlEste es un emulador clásico de NES, y la mayoría de la gente lo usa. No se ha actualizado durante mucho tiempo, y no es la mejor en características, pero sigue siendo el emulador predeterminado para muchos romhackers. Además, lo elegí porque el soporte de socket Lua está integrado en él, y no hay necesidad de conectarlo yo mismo.
Json.lua -
github.com/spiiin/json.luaEsta es una implementación JSON en pura Lua. Lo elegí porque quería hacer un ejemplo que no requiera compilación de código. Pero aún tenía que bifurcar la biblioteca, porque algunas de las bibliotecas integradas en Fceux sobrecargaron la función de biblioteca y rompieron la serialización (mi solicitud de
grupo rechazada al autor de la biblioteca original).
Python 3 -
www.python.orgEl servidor Fceux Lua abre el socket tcp y escucha los comandos recibidos de él. Un servidor que envía comandos al emulador se puede implementar en cualquier idioma. Elegí Python por su filosofía de "batería incluida": la mayoría de los módulos están incluidos en la biblioteca estándar (también funcionan con sockets y JSON). Python también conoce la biblioteca para trabajar con redes neuronales, y quiero intentar usarlas para crear bots en juegos de NES.
Cuaderno Jupyter -
jupyter.orgJupyter Notebook es un entorno genial para ejecutar código Python de forma interactiva. Con él, puede escribir y ejecutar comandos en un editor de hojas de cálculo dentro del navegador. También es bueno para crear ejemplos presentables.
Dexpot -
www.dexpot.deUtilicé este administrador de escritorio virtual para acoplar la ventana del emulador encima de otros. Esto es muy conveniente al implementar el servidor en pantalla completa para el seguimiento instantáneo de los cambios en la ventana del emulador. Las herramientas estándar de Windows no le permiten organizar el acoplamiento de la ventana por encima de los demás.
Referencias
En realidad,
el repositorio del proyecto .
Nintaco - Emulador Java NES con gestión remota
Colección Xkeeper0 emu-lua : una colección de varios scripts Lua
Mesen es un emulador NES moderno en C # con potentes capacidades de secuencias de comandos Lua. Hasta ahora sin soporte de zócalo y control remoto.
CadEditor es mi proyecto de un editor de nivel universal para NES y otras plataformas, así como herramientas poderosas para investigar juegos. Utilizo el script y el servidor descritos en la publicación para explorar los juegos y agregarlos al editor.
Agradecería comentarios, pruebas e intentos de usar el script.