Controle remoto do emulador Fceux usando Python

No artigo, descreverei como tornar o emulador NES controlado remotamente e um servidor para enviar comandos a ele remotamente.



Por que isso é necessário?


Alguns emuladores de vários consoles de jogos, incluindo o Fceux , permitem escrever e executar scripts personalizados no Lua. Mas Lua é uma linguagem ruim para escrever programas sérios. É uma linguagem para chamar funções escritas em C. Os autores dos emuladores o usam apenas devido à leveza e facilidade de incorporação. A emulação precisa requer muitos recursos do processador, e a velocidade da emulação anterior era um dos principais objetivos dos autores, e se eles lembrassem da possibilidade de ações de script, isso não ocorreria em primeiro lugar.

Agora, o poder do processador médio é suficiente para emular o NES. Por que não usar linguagens de script poderosas como Python ou JavaScript em emuladores?

Infelizmente, nenhum dos emuladores populares do NES tem a capacidade de usar esses ou outros idiomas. Encontrei apenas um projeto Nintaco pouco conhecido, que também é baseado no kernel do Fceux, por algum motivo reescrito em Java. Decidi adicionar a capacidade de escrever scripts em Python para controlar o emulador.

Meu resultado é a Prova de Conceito da capacidade de controlar o emulador, ele não finge velocidade ou confiabilidade, mas funciona. Eu fiz isso por mim mesmo, mas como a questão de como controlar o emulador usando scripts é bastante comum , coloquei o código-fonte no github .

Como isso funciona


No lado do emulador


O emulador do Fceux já inclui várias bibliotecas Lua incluídas na forma de código compilado . Um deles é o LuaSocket . Está mal documentado, mas consegui encontrar um exemplo de código de trabalho na coleção de scripts do Xkeeper0 . Ele usou soquetes para controlar o emulador através do Mirc. Na verdade, o código que abre o soquete tcp é:

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) --it's our socket object print("Connected", sock2, err2) 

Este é um soquete de baixo nível que recebe e envia dados por 1 byte.

No emulador do Fceux, o loop principal do script Lua se parece com o seguinte:

 function main() while true do --  passiveUpdate() --,        emu.frameadvance() --       end end 

Uma verificação de dados do soquete:

 function passiveUpdate() local message, err, part = sock2:receive("*all") if not message then message = part end if message and string.len(message)>0 then --print(message) local recCommand = json.decode(message) table.insert(commandsQueue, recCommand) coroutine.resume(parseCommandCoroutine) end end 

O código é bastante simples - os dados são lidos no soquete e, se o próximo comando for detectado, ele será analisado e executado. A análise e a execução são organizadas usando corotina (corotinas) - este é um conceito poderoso da linguagem Lua para pausar e continuar a execução do código.

E mais uma coisa importante sobre os scripts Lua no Fceux - a emulação pode ser temporariamente interrompida a partir do script. Como organizar a execução contínua do código Lua e executá-lo novamente com um comando recebido do soquete? Isso não seria possível, mas existe uma capacidade pouco documentada de chamar o código Lua, mesmo quando a emulação é interrompida (obrigado por apontar para ele):

 gui.register(passiveUpdate) --undocumented. this function will call even if emulator paused 

Com ele, você pode parar e continuar emulando dentro do passiveUpdate - dessa forma, você pode organizar a instalação de pontos de interrupção do emulador por meio de um soquete.

Comando do lado do servidor


Eu uso um protocolo de texto RPC baseado em JSON muito simples. O servidor serializa o nome da função e os argumentos em uma sequência JSON e a envia pelo soquete. Além disso, a execução do código é interrompida até o emulador responder com uma linha para concluir a execução do comando. A resposta conterá os campos " FUNCTIONNAME_finished " e o resultado da função.

A ideia é implementada na classe 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]) #print(cmd) if cmd != None: if len(cmd)>1: return cmd[1] return @classmethod def call(cls, *params): """wrapper for sending [functionName, [param1, param2, ...]] to socket and wait until client return [functionName_finished, [result1,...]] answer""" sender.send(*params) funcName = params[0] return syncCall.waitUntil(funcName + "_finished") 

Com esta classe, os métodos Lua do emulador Fceux podem ser agrupados em classes 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) 

E então chamado literalmente da mesma maneira que em Lua:

 # : emu.poweron() 

Métodos de retorno de chamada


Em Lua, você pode registrar retornos de chamada - funções que serão chamadas quando uma determinada condição for atendida. Podemos portar esse comportamento para o servidor em Python usando o seguinte truque. Primeiro, salvamos o identificador da função de retorno de chamada escrita em Python e passamos para o 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): #print("check:", cmd) for callbackName in callbacks.callbackList: if cmd[0] == callbackName: hfunc = cmd[1] #print("hfunc:", hfunc) func = callbacks.functions.get(hfunc) #print("func:", func) if func: try: func(*cmd[2:]) #skip function name and function hash and save others arguments except Exception as e: callbacks.error(e) pass #TODO: thread locking sender.send(callbackName + "_finished") 

O código Lua também salva esse identificador e registra um retorno de chamada Lua regular, que transferirá o controle para o código Python. Em seguida, um segmento separado é criado no código Python, preocupado apenas em verificar se o comando de retorno de chamada de Lua não foi aceito:

 def callbacksThread(): cycle = 0 while True: cycle += 1 try: cmd = messages.parseMessages(asyncCall.waitAnswer(), callbacks.callbackList) if cmd: #print("Callback received:", cmd) callbacks.checkAllCallbacks(cmd) pass except socket.timeout: pass time.sleep(0.001) 

A etapa final é que, após a execução do retorno de chamada Python, o controle é retornado para Lua usando o comando " CALLBACKNAME_finished " para informar ao emulador que o retorno de chamada foi concluído.

Como executar um exemplo


  • Você deve ter o Python 3 e o Jupyter Notebook no sistema. Você deve executar o Jupyter com o comando

     jupyter notebook 

  • Abra o laptop FceuxPythonServer.py.ipynb e execute a primeira linha

  • Agora você precisa executar o emulador do Fceux, abrir o arquivo ROM nele (eu uso o jogo Castlevania (U) (PRG0) [!]. Nes no meu exemplo) e executar o script Lua com o nome fceux_listener.lua . Ele deve se conectar a um servidor executando em um laptop Jupyter.

    Essas ações podem ser executadas usando a linha de comando:

     fceux.exe -lua fceux_listener.lua "Castlevania (U) (PRG0) [!].nes" 

  • Agora volte ao Notebook Jupyter. Você deverá ver uma mensagem sobre uma conexão bem-sucedida ao emulador:



Isso é tudo, você pode enviar comandos do laptop Jupyter no navegador diretamente para o emulador Fceux.

Você pode executar todas as linhas do laptop de exemplo sequencialmente e observar o resultado da execução no emulador.

Exemplo completo:
https://github.com/spiiin/fceux_luaserver/blob/master/FceuxPythonServer.py.ipynb

Ele contém funções simples, como a leitura de memória:



Exemplos de retorno de chamada mais complexos:



E um script para um jogo específico que permite mover inimigos de Super Mario Bros. com o mouse:



Vídeo de execução do laptop:


Limitações e aplicações


O script não tem proteção contra tolo e não é otimizado para velocidade de execução - seria melhor usar um protocolo RPC binário em vez de um texto e agrupar mensagens, mas minha implementação não requer compilação. O script pode alternar os contextos de execução de Lua para Python e voltar 500-1000 vezes por segundo no meu laptop. Isso é suficiente para quase qualquer aplicativo, exceto em casos específicos de depuração pixel a pixel ou linha a linha do processador de vídeo, mas o Fceux ainda não permite essas operações de Lua, por isso não importa.

Idéias de aplicação possíveis:

  • Como um exemplo da implementação desse controle para outros emuladores e linguagens
  • Pesquisa de jogos
  • Adicionando truques ou recursos para organizar passagens do TAS
  • Inserir ou extrair dados e códigos em jogos
  • Aprimorando os recursos dos emuladores - escrevendo depuradores, scripts para gravação e exibição de orientações, bibliotecas de scripts, editores de jogos
  • Jogo em rede, controle de jogo usando dispositivos móveis, serviços remotos, joypads ou outros dispositivos de controle, salvamento e correções em serviços em nuvem
  • Recursos de emulador cruzado
  • Usando Python ou outras bibliotecas de idiomas para análise de dados e controle de jogos (criando bots)

Pilha de tecnologia


Eu usei:

Fceux - www.fceux.com/web/home.html
Este é um emulador de NES clássico e a maioria das pessoas o usa. Ele não é atualizado há muito tempo e não é o melhor em recursos, mas continua sendo o emulador padrão para muitos romhackers. Além disso, eu o escolhi porque o suporte ao soquete Lua está integrado e não há necessidade de conectá-lo.

Json.lua - github.com/spiiin/json.lua
Esta é uma implementação JSON em Lua pura. Eu o escolhi porque queria fazer um exemplo que não requer compilação de código. Mas eu ainda tinha que bifurcar a biblioteca, porque algumas das bibliotecas embutidas no Fceux sobrecarregaram a função de biblioteca e interromperam a serialização (minha solicitação de pool rejeitada ao autor da biblioteca original).

Python 3 - www.python.org
O servidor Fceux Lua abre o soquete tcp e escuta os comandos recebidos dele. Um servidor que envia comandos para o emulador pode ser implementado em qualquer idioma. Eu escolhi o Python por sua filosofia "Bateria incluída" - a maioria dos módulos está incluída na biblioteca padrão (trabalhando com soquetes e JSON também). O Python também conhece a biblioteca para trabalhar com redes neurais, e eu quero tentar usá-las para criar bots em jogos NES.

Caderno Jupyter - jupyter.org
O Jupyter Notebook é um ambiente muito legal para a execução interativa do código Python. Com ele, você pode escrever e executar comandos em um editor de planilha dentro do navegador. Também é bom para criar exemplos apresentáveis.

Dexpot - www.dexpot.de
Eu usei esse gerenciador de área de trabalho virtual para encaixar a janela do emulador em cima de outras. Isso é muito conveniente ao implantar o servidor em tela cheia para rastreamento instantâneo de alterações na janela do emulador. As ferramentas nativas do Windows não permitem organizar o encaixe de janelas em cima de outras.

Referências


Na verdade, o repositório do projeto .

Nintaco - Emulador Java NES com Gerenciamento Remoto
Xkeeper0 coleção emu-lua - uma coleção de vários scripts Lua
Mesen é um emulador NES moderno em C # com poderosos recursos de script Lua. Até agora, sem suporte para soquete e controle remoto.
O CadEditor é o meu projeto de um editor de nível universal para o NES e outras plataformas, além de ferramentas poderosas para pesquisar jogos. Eu uso o script e o servidor descritos na publicação para explorar os jogos e adicioná-los ao editor.

Eu gostaria de receber feedback, testes e tentativas de usar o script.

Source: https://habr.com/ru/post/pt435428/


All Articles