Fernsteuerung des Fceux-Emulators mit Python

In diesem Artikel werde ich beschreiben, wie der NES-Emulator ferngesteuert und ein Server zum Senden von Befehlen an ihn ferngesteuert wird.



Warum wird das benötigt?


Mit einigen Emulatoren verschiedener Spielekonsolen, einschließlich Fceux , können Sie benutzerdefinierte Skripte auf Lua schreiben und ausführen. Aber Lua ist eine schlechte Sprache, um ernsthafte Programme zu schreiben. Es ist eher eine Sprache zum Aufrufen von Funktionen, die in C geschrieben sind. Die Autoren von Emulatoren verwenden es nur wegen der Leichtigkeit und einfachen Einbettung. Eine genaue Emulation erfordert viele Prozessorressourcen, und die frühere Emulationsgeschwindigkeit war eines der Hauptziele der Autoren, und wenn sie sich an die Möglichkeit von Skriptaktionen erinnerten, war dies nicht an erster Stelle.

Jetzt reicht die Leistung eines durchschnittlichen Prozessors für die Emulation von NES aus. Warum nicht leistungsstarke Skriptsprachen wie Python oder JavaScript in Emulatoren verwenden?

Leider kann keiner der beliebten NES-Emulatoren diese oder andere Sprachen verwenden. Ich habe nur ein wenig bekanntes Nintaco- Projekt gefunden, das ebenfalls auf dem Fceux-Kernel basiert und aus irgendeinem Grund in Java neu geschrieben wurde. Dann habe ich beschlossen, die Möglichkeit hinzuzufügen, Skripte in Python zu schreiben, um den Emulator selbst zu steuern.

Mein Ergebnis ist der Proof-of-Concept der Fähigkeit, den Emulator zu steuern. Er gibt nicht vor, schnell oder zuverlässig zu sein, aber er funktioniert. Ich habe es selbst gemacht, aber da die Frage, wie der Emulator mithilfe von Skripten gesteuert werden kann, häufig genug ist , habe ich den Quellcode auf den Github gestellt .

Wie funktioniert es?


Auf der Seite des Emulators


Der Fceux-Emulator enthält bereits mehrere Lua-Bibliotheken in Form von kompiliertem Code . Einer von ihnen ist LuaSocket . Es ist schlecht dokumentiert, aber ich habe es geschafft, ein Beispiel für Arbeitscode in der Sammlung von Xkeeper0- Skripten zu finden . Er benutzte Sockel, um den Emulator über Mirc zu steuern. Der Code, der den TCP-Socket öffnet, lautet:

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) 

Dies ist ein Low-Level-Socket, der Daten mit 1 Byte empfängt und sendet.

Im Fceux-Emulator sieht die Hauptschleife des Lua-Skripts folgendermaßen aus:

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

Eine Überprüfung der Daten aus dem Socket:

 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 

Der Code ist recht einfach: Daten werden aus dem Socket gelesen, und wenn der nächste Befehl erkannt wird, werden sie analysiert und ausgeführt. Das Parsen und Ausführen wird mithilfe von Coroutine (Coroutinen) organisiert - dies ist ein leistungsstarkes Konzept der Lua-Sprache zum Anhalten und Fortsetzen der Codeausführung.

Und noch etwas Wichtiges an Lua-Skripten in Fceux - die Emulation kann vorübergehend aus dem Skript gestoppt werden. Wie organisiere ich die fortgesetzte Ausführung von Lua-Code und führe ihn mit einem vom Socket empfangenen Befehl erneut aus? Dies wäre nicht möglich, aber es gibt eine schlecht dokumentierte Möglichkeit, Lua-Code aufzurufen, selbst wenn die Emulation gestoppt ist (danke feos für das Zeigen darauf):

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

Mit dieser Funktion können Sie die Emulation in passiveUpdate stoppen und fortsetzen. Auf diese Weise können Sie die Installation von Haltepunkten des Emulators über einen Socket organisieren.

Serverseitiger Befehl


Ich verwende ein sehr einfaches JSON-basiertes RPC-Textprotokoll. Der Server serialisiert den Funktionsnamen und die Argumente in eine JSON-Zeichenfolge und sendet sie über den Socket. Ferner wird die Ausführung des Codes gestoppt, bis der Emulator mit einer Zeile antwortet, um den Befehl abzuschließen. Die Antwort enthält die Felder " FUNCTIONNAME_finished " und das Ergebnis der Funktion.

Die Idee ist in der syncCall- Klasse implementiert:

 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") 

Mit dieser Klasse können Fceux-Emulator-Lua-Methoden in Python-Klassen eingeschlossen werden:

 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) 

Und dann wörtlich genannt wie von Lua:

 # : emu.poweron() 

Rückrufmethoden


In Lua können Sie Rückrufe registrieren - Funktionen, die aufgerufen werden, wenn eine bestimmte Bedingung erfüllt ist. Wir können dieses Verhalten mit dem folgenden Trick auf den Server in Python portieren. Zuerst speichern wir die Kennung der in Python geschriebenen Rückruffunktion und übergeben sie an den Lua-Code:

 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") 

Lua-Code speichert auch diese Kennung und registriert einen regulären Lua-Rückruf, der die Kontrolle auf Python-Code überträgt. Als Nächstes wird im Python-Code ein separater Thread erstellt, der nur überprüft, ob der Rückrufbefehl von Lua nicht akzeptiert wurde:

 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) 

Der letzte Schritt besteht darin, dass nach Ausführung des Python-Rückrufs die Steuerung mit dem Befehl " CALLBACKNAME_finished " an Lua zurückgegeben wird, um den Emulator darüber zu informieren, dass der Rückruf beendet ist.

So führen Sie ein Beispiel aus


  • Auf dem System muss Python 3 und Jupyter Notebook ausgeführt sein. Sie müssen Jupyter mit dem Befehl ausführen

     jupyter notebook 

  • Öffnen Sie den Laptop FceuxPythonServer.py.ipynb und führen Sie die erste Zeile aus

  • Jetzt müssen Sie den Fceux-Emulator ausführen, die darin enthaltene ROM-Datei öffnen (ich verwende das Spiel Castlevania (U) (PRG0) [!]. Nes in meinem Beispiel) und das Lua-Skript mit dem Namen fceux_listener.lua ausführen . Es sollte eine Verbindung zu einem Server herstellen, der auf einem Jupyter-Laptop ausgeführt wird.

    Diese Aktionen können über die Befehlszeile ausgeführt werden:

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

  • Wechseln Sie nun zurück zum Jupyter Notebook. Sie sollten eine Meldung über eine erfolgreiche Verbindung zum Emulator sehen:



Das ist alles, Sie können Befehle vom Jupyter-Laptop im Browser direkt an den Fceux-Emulator senden.

Sie können alle Zeilen des Beispiel-Laptops nacheinander ausführen und das Ergebnis der Ausführung im Emulator beobachten.

Vollständiges Beispiel:
https://github.com/spiiin/fceux_luaserver/blob/master/FceuxPythonServer.py.ipynb

Es enthält einfache Funktionen wie das Lesen des Speichers:



Komplexere Rückrufbeispiele:



Und ein Skript für ein bestimmtes Spiel, mit dem Sie Feinde von Super Mario Bros. bewegen können . mit der Maus:



Laptop Run Video:


Einschränkungen und Anwendungen


Das Skript hat keinen Schutz vor Narren und ist nicht für die Ausführungsgeschwindigkeit optimiert. Es ist besser, ein binäres RPC-Protokoll anstelle eines Textprotokolls und von Gruppennachrichten zu verwenden, aber meine Implementierung erfordert keine Kompilierung. Das Skript kann auf meinem Laptop 500-1000 Mal pro Sekunde Ausführungskontexte von Lua zu Python und zurück wechseln. Dies ist für fast jede Anwendung ausreichend, außer für bestimmte Fälle des pixelweisen oder zeilenweisen Debuggens des Videoprozessors. Fceux lässt solche Vorgänge von Lua jedoch immer noch nicht zu, sodass dies keine Rolle spielt.

Mögliche Anwendungsideen:

  • Als Beispiel für die Implementierung einer solchen Steuerung für andere Emulatoren und Sprachen
  • Spielforschung
  • Hinzufügen von Cheats oder Funktionen zum Organisieren von TAS-Passagen
  • Fügen Sie Daten und Code in Spiele ein oder extrahieren Sie sie
  • Verbesserung der Funktionen von Emulatoren - Schreiben von Debuggern, Skripten zum Aufzeichnen und Anzeigen von exemplarischen Vorgehensweisen, Skriptbibliotheken und Spieleditoren
  • Netzwerkspiel, Spielsteuerung über mobile Geräte, Remote-Dienste, Joypads oder andere Steuergeräte, Speichern und Patches in Cloud-Diensten
  • Emulatorübergreifende Funktionen
  • Verwendung von Python oder anderen Sprachbibliotheken zur Datenanalyse und Spielsteuerung (Erstellen von Bots)

Technologie-Stack


Ich habe verwendet:

Fceux - www.fceux.com/web/home.html
Dies ist ein klassischer NES-Emulator, den die meisten Leute verwenden. Es wurde lange Zeit nicht aktualisiert und bietet nicht die besten Funktionen, bleibt jedoch der Standardemulator für viele Romhacker. Ich habe mich auch dafür entschieden, weil die Lua-Sockelunterstützung integriert ist und es nicht erforderlich ist, sie selbst anzuschließen.

Json.lua - github.com/spiiin/json.lua
Dies ist eine JSON-Implementierung in reinem Lua. Ich habe es gewählt, weil ich ein Beispiel erstellen wollte, für das keine Codekompilierung erforderlich ist. Aber ich musste die Bibliothek immer noch teilen, da einige der in Fceux integrierten Bibliotheken die Bibliotheksfunktion überlasteten und die Serialisierung brachen (meine abgelehnte Pool-Anfrage an den Autor der ursprünglichen Bibliothek).

Python 3 - www.python.org
Der Fceux Lua-Server öffnet den TCP-Socket und wartet auf die von ihm empfangenen Befehle. Ein Server, der Befehle an den Emulator sendet, kann in einer beliebigen Sprache implementiert werden. Ich habe Python wegen seiner Philosophie „Batterie enthalten“ ausgewählt - die meisten Module sind in der Standardbibliothek enthalten (einschließlich der Arbeit mit Sockets und JSON). Python kennt auch die Bibliothek für die Arbeit mit neuronalen Netzen, und ich möchte versuchen, sie zum Erstellen von Bots in NES-Spielen zu verwenden.

Jupyter Notebook - jupyter.org
Jupyter Notebook ist eine sehr coole Umgebung für die interaktive Ausführung von Python-Code. Damit können Sie Befehle in einem Tabellenkalkulationseditor im Browser schreiben und ausführen. Es ist auch gut, um vorzeigbare Beispiele zu erstellen.

Dexpot - www.dexpot.de
Ich habe diesen virtuellen Desktop-Manager verwendet, um das Emulatorfenster über andere zu andocken. Dies ist sehr praktisch, wenn Sie den Server im Vollbildmodus bereitstellen, um Änderungen im Emulatorfenster sofort zu verfolgen. Mit nativen Windows-Tools können Sie das Andocken von Fenstern nicht über anderen organisieren.

Referenzen


Eigentlich das Projekt-Repository .

Nintaco - Java NES Emulator mit Fernverwaltung
Xkeeper0 Emu-Lua-Sammlung - eine Sammlung verschiedener Lua-Skripte
Mesen ist ein moderner NES-Emulator in C # mit leistungsstarken Lua-Skriptfunktionen. Bisher ohne Sockelunterstützung und Fernbedienung.
CadEditor ist mein Projekt eines universellen Level-Editors für NES und andere Plattformen sowie leistungsstarker Tools für die Erforschung von Spielen. Ich benutze das im Beitrag beschriebene Skript und den Server, um die Spiele zu erkunden und sie dem Editor hinzuzufügen.

Ich würde mich über Feedback, Tests und Versuche freuen, das Skript zu verwenden.

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


All Articles